Computer Graphics面接対策

Computer Graphics開発者面接問題

レベル別にまとめた、選りすぐりの computer graphics 面接問題 15問。基礎、実践上のトレードオフ、シニアレベルの本番運用での判断を確認できます。

Computer GraphicsのAI面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

初級向け質問

1リニアブレンドスキニングの概要と、複数の影響を受ける頂点に対してボーン行列がどのように適用されるかを説明してください。

リニアブレンドスキニング (Linear Blend Skinning, LBS) は、基礎となる骨格階層構造に基づいて3Dメッシュを変形アニメーションさせる幾何学的変形手法です。スケルタルアニメーションでは、各アニメーションボーンは基準姿勢(バインドポーズ)を基準として相対的に移動します。複数のボーンの影響を受ける頂点を変換する手順は次のとおりです。 1. メッシュ空間における頂点の元の位置を、ボーンの逆バインドポーズ行列($B_i^{-1}$)を掛けることで各ボーンのローカル空間に変換します。 2. 次に、ボーンのアニメーションボーン行列($M_i$)を用いて、頂点をボーンのローカル空間から現在のアニメーション姿勢空間へと変換します。この合成変換 $S_i = M_i \cdot B_i^{-1}$ はスキニングパレット行列と呼ばれます。 3. 最終的なスキニング後の頂点位置は、影響を与えるすべてのボーンにわたる線形重み付き合計として計算されます。 $$v' = \sum_{i=1}^{k} w_i \cdot (M_i \cdot B_i^{-1} \cdot v)$$ ここで、スカラーのボーンウェイト $w_i$ は正規化されている必要があります(すなわち $\sum w_i = 1.0$)。 GPU上では、通常これが頂点シェーダー(または事前計算としてのコンピュートスキニングパス)で実行されます。頂点ボーンインデックス属性を使用してユニフォームバッファまたは構造化バッファから事前計算されたボーンパレットを取得し、位置を線形補間(ブレンド)します。頂点法線および接線は、ブレンドされたスキニング行列の回転成分を用いて変換され、再正規化されます。

#version 450

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in uvec4 inBoneIndices; // Up to 4 bone influences
layout(location = 3) in vec4 inBoneWeights;  // Normalized: sum to 1.0

layout(set = 0, binding = 0) uniform BonePalette {
    mat4 boneMatrices[128]; // Pre-multiplied: M_i * B_i^-1
};

layout(location = 0) out vec3 outNormal;

void main() {
    mat4 skinMatrix = inBoneWeights.x * boneMatrices[inBoneIndices.x] +
                      inBoneWeights.y * boneMatrices[inBoneIndices.y] +
                      inBoneWeights.z * boneMatrices[inBoneIndices.z] +
                      inBoneWeights.w * boneMatrices[inBoneIndices.w];

    vec4 skinnedPosition = skinMatrix * vec4(inPosition, 1.0);
    gl_Position = u_ViewProjection * skinnedPosition;
    
    // Transform normal with rotational part of skinMatrix and normalize
    outNormal = normalize(mat3(skinMatrix) * inNormal);
}
AI コーチを使ってこの質問に答えてみる

2GPU (Graphics Processing Unit) インスタンシングについて説明し、類似した多数のオブジェクトを効率的にレンダリングするためにどのようなインスタンスごとのデータやレイアウトが用いられるかを述べてください。

GPUインスタンシングは、同じ基本ジオメトリ(頂点バッファとインデックスバッファを共有)の複数のコピーを1回のドローコール(Direct3DのDrawIndexedInstancedやOpenGLのglDrawElementsInstancedなど)で描画するレンダリング手法であり、CPUとGPU間のドライバオーバーヘッドやAPIのドローコール数を劇的に削減します。 インスタンスごとのデータ(Per-Instance Data): 各インスタンスの見た目や動作に変化を持たせるため、主に以下のようなインスタンスごとのデータが提供されます。 - トランスフォームデータ: ワールド行列、またはパックされた位置・回転・スケール。 - マテリアルプロパティ: 色合い(カラーチント)、UVオフセット/スケール、またはマテリアルIDインデックス。 - 動的パラメータ: アニメーションフェーズ、ライトマップオフセット、または表示/非表示フラグ。 データレイアウトとアクセス方法: 1. インスタンス頂点バッファ: インスタンスごとのステップレート(例: `D3D11_INPUT_PER_INSTANCE_DATA`)でバインドされた専用の頂点バッファ。GPUはインスタンスごとにバッファを自動的に進めます。 2. StructuredBuffer / Uniform Buffer (SSBO / Constant Buffer): インスタンスデータをバッファ配列にアップロードし、頂点シェーダーが組み込みのシステムインスタンス識別子(HLSLの`SV_InstanceID`、GLSLの`gl_InstanceID`)を使ってインデックス参照します。 インスタンスごとのデータをコンパクトに保つこと(例えば、完全な4x4行列の代わりに3x4アフィン行列や位置+クォータニオンを使用したり、パックされたFP16/uint32の色情報を使用したりする)で、GPUメモリ帯域幅を最小限に抑え、キャッシュ使用率を最適化できます。

struct InstanceData {
    float4x4 worldMatrix;
    float4   colorTint;
};

StructuredBuffer<InstanceData> gInstanceData : register(t0);

struct VSInput {
    float3 position : POSITION;
    float3 normal   : NORMAL;
};

struct VSOutput {
    float4 position : SV_POSITION;
    float4 color    : COLOR;
};

VSOutput main(VSInput input, uint instanceID : SV_InstanceID)
{
    VSOutput output;
    InstanceData inst = gInstanceData[instanceID];
    
    float4 worldPos = mul(inst.worldMatrix, float4(input.position, 1.0));
    output.position = mul(gViewProjMatrix, worldPos);
    output.color    = inst.colorTint;
    return output;
}
AI コーチを使ってこの質問に答えてみる

3視錐台カリング(frustum culling)、オクルージョンカリング(occlusion culling)、背面カリング(back-face culling)について説明し、それぞれがレンダラ内のどの段階で実行されるのが一般的かを述べてください。

視錐台カリング(frustum culling)、オクルージョンカリング(occlusion culling)、背面カリング(back-face culling)は、レンダリングパイプラインの異なる段階と粒度で不可視のプリミティブを破棄する、互いに補完的な 3 つの可視性判定手法です。 1. 視錐台カリング: カメラの視錐台(view frustum)の外側に完全に位置するジオメトリを破棄します。通常、ドローコールの発行前に CPU 上でおおまかなバウンディングボリューム(AABB: Axis-Aligned Bounding Box やバウンディングスフィアなど)を用いて判定されるか、GPU 駆動(GPU-driven)のレンダリングパイプラインではコンピュートシェーダを介して GPU 上で実行されます。 2. オクルージョンカリング: 視錐台の内部にはあるものの、他の不透明なジオメトリの背後に隠れているオブジェクトやプリミティブを破棄します。本格的なラスタライズの前に、CPU 上(ソフトウェアラスタライゼーションや事前計算された可視性データを利用)または GPU 上(ハードウェアオクルージョンクエリ、GPU コンピュートによる Hi-Z 深度バッファテスト、メッシュレットカリングなど)で実行されます。 3. 背面カリング: 表面の法線ベクトルがカメラから離れる方向を向いている個々のポリゴンを破棄します。従来は三角形のセットアップやラスタライズ段階において、スクリーン空間の頂点の巻き順(winding order)に基づいて GPU の固定機能ハードウェアによって自動的に処理されますが、クラスタ単位の法線コーン(メッシュシェーダなど)を用いて大まかに評価されることもあります。

struct Plane { glm::vec3 normal; float distance; };
struct Sphere { glm::vec3 center; float radius; };

bool isSphereInsideFrustum(const Sphere& sphere, const Plane frustumPlanes[6]) {
    for (int i = 0; i < 6; ++i) {
        // Signed distance from plane to sphere center
        float dist = glm::dot(frustumPlanes[i].normal, sphere.center) + frustumPlanes[i].distance;
        if (dist < -sphere.radius) {
            return false; // Completely outside
        }
    }
    return true; // Inside or intersecting
}
AI コーチを使ってこの質問に答えてみる

4ベジェ曲線、B-スプライン、Catmull-Romスプラインなどのスプライン曲線は、どのようにして滑らかなパスやジオメトリストリップを生成しますか。

スプライン曲線は、滑らかな3Dパス、カメラ軌道、および押し出しジオメトリストリップ(リボン、道路、チューブなど)を定義するための媒介変数(パラメータ)表現 $\mathbf{P}(t)$ を提供します。 1. **曲線の種類と特性:** - **ベジェ曲線(Bézier Curves):** バーンスタイン基底関数を用いて定式化されます。始点と終点のみを補間(通過)し、中間の制御点は接線ハンドルを定義します。セグメント同士を $C^1$ 連続性で接続するには、接線ハンドルが一直線上に並んでいる必要があります。 - **B-スプライン(B-splines):** ノットベクトル上の基底関数を用いて構築されます。局所的な制御性と高い媒介変数連続性(3次スプラインで $C^2$)を提供しますが、通常は内部の制御点を通過しません。 - **Catmull-Romスプライン:** すべての内部制御点を直接通過しながら、自動的に $C^1$ 連続性を確保する補間スプラインの一種であり、ユーザーが作成するパスに最適です。 2. **パスとジオメトリの生成:** - パラメータ $t$ でスプラインを評価することで、位置 $\mathbf{P}(t)$ と接線ベクトル $\mathbf{T}(t) = \mathbf{P}'(t)$ が生成されます。 - 3Dのリボンやチューブを押し出し成形するには、曲線に沿った直交座標系(法線 $\mathbf{N}(t)$ および陪法線 $\mathbf{B}(t)$)が必要です。 - 標準的なフルネ・セレ(Frenet-Serret)フレームは、曲率 $\kappa = 0$ となる変曲点で破綻するか反転してしまいます。不自然なリボンのねじれを防ぐため、**並進移動フレーム(Bishopフレーム / Parallel Transport Frames)**は回転の捩れを最小限に抑えながら、曲線に沿って基準となる向きを滑らかに伝播させます。

struct Vector3 { float x, y, z; };

Vector3 EvaluateCatmullRom(const Vector3& p0, const Vector3& p1, const Vector3& p2, const Vector3& p3, float t) {
    float t2 = t * t;
    float t3 = t2 * t;
    return 0.5f * ( (2.0f * p1) +
                    (-p0 + p2) * t +
                    (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t2 +
                    (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t3 );
}
AI コーチを使ってこの質問に答えてみる

5コマンドバッファとは何ですか。また、ハイエンドエンジンにおいてマルチスレッドでのコマンド記録が重要となるのはなぜですか?

コマンドバッファ(Direct3D 12ではコマンドリスト)とは、GPUキューでの後続の送信および非同期実行に向けて、パイプライン状態の設定、記述子(デスクリプタ)のバインド、ドローコールの発行、パイプラインバリアの記録などのグラフィックス、コンピュート、転送コマンドをCPU側で記録するためのメモリ上のデータ構造です。ハイエンドエンジンにおいてマルチスレッドコマンド記録が極めて重要となるのは、CPU側でのドローコールの準備、状態のバインド、カリングが従来から主なボトルネックとなってきたためです。明示的なAPI(Explicit API)は、シングルスレッドコンテキストの制約を排除することで、エンジンが1フレームを複数のCPUワーカースレッドに分散して独立した描画タスクに分割できるようにします。例えば、シャドウパス、Gバッファチャンク、ポストプロセスを同時に記録できます。現代のAPIでは、プライマリ/セカンダリコマンドバッファ(Vulkan)やコマンドリスト/バンドル(D3D12)を介してこれを実現します。セカンダリコマンドバッファやバンドルにより、ワーカースレッドは送信スレッド上のプライマリコマンドバッファ内で実行できる描画コマンドのサブセットを記録でき、マルチコアCPUの使用率を最大化し、GPUキューのストールを最小限に抑えることができます。

// Worker Thread Job:
void RecordShadowPassChunk(VkCommandBuffer secondaryCmdBuf, const RenderJob& job) {
    VkCommandBufferInheritanceInfo inheritInfo{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO };
    inheritInfo.renderPass = job.shadowRenderPass;
    
    VkCommandBufferBeginInfo beginInfo{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT;
    beginInfo.pInheritanceInfo = &inheritInfo;
    
    vkBeginCommandBuffer(secondaryCmdBuf, &beginInfo);
    vkCmdBindPipeline(secondaryCmdBuf, VK_PIPELINE_BIND_POINT_GRAPHICS, job.pipeline);
    vkCmdDrawIndexed(secondaryCmdBuf, job.indexCount, 1, 0, 0, 0);
    vkEndCommandBuffer(secondaryCmdBuf);
}

// Main Thread Submission:
// vkCmdExecuteCommands(primaryCmdBuf, secondaryCount, secondaryCmdBuffers.data());
// vkQueueSubmit(queue, 1, &submitInfo, fence);
AI コーチを使ってこの質問に答えてみる

6プッシュ定数(push constants)やルート定数(root constants)はユニフォーム/定数バッファとどう異なり、どのような場面で使用すべきですか?

プッシュ定数(Vulkan)やルート定数(DirectX 12)は、少量のユニフォームデータをコマンドバッファやルートシグネチャ内にインラインで直接渡す仕組みを提供し、ディスクリプタを介したGPUバッファリソースの割り当て・更新・バインドのオーバーヘッドを回避します。対照的に、UBO (Uniform Buffer Object) や定数バッファ(Constant Buffer)は、専用に割り当てられたGPUメモリをバックストアとし、ディスクリプタ、ディスクリプタテーブル、またはディスクリプタセットを介してパイプラインにバインドされます。プッシュ定数やルート定数はコマンドストリーム自体に埋め込まれるため、オブジェクトの変換行列、マテリアル/メッシュのインデックス、時間、動的オフセットなど、ドローコールごとに頻繁に更新されるデータの受け渡しに最適です。ただしサイズには厳しい制約があり、例えばVulkanでの最小保証サイズはわずか128バイトであり、D3D12のルートシグネチャ空間はルートディスクリプタやテーブルと共有で64 DWORDに制限されています。データサイズが制限を超える場合、フレームごとのカメラ/ビュー行列、グローバルなシーン照明、環境設定など複数のドローコールでデータを共有する場合、またはパスをまたいで永続的な保持が必要な場合には、ユニフォーム/定数バッファを使用すべきです。

// Push Constant setup
struct PushData {
    glm::mat4 modelMatrix;
    uint32_t materialIndex;
};

// Command buffer recording: inline write directly into command stream
PushData data = { object.transform, object.matID };
vkCmdPushConstants(cmdBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushData), &data);
vkCmdDrawIndexed(cmdBuffer, indexCount, 1, 0, 0, 0);

// Compared to UBO: requires updating mapped GPU buffer, managing offsets/ring buffers, and binding descriptor sets
vkCmdBindDescriptorSets(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &perObjectDescriptorSet, 0, nullptr);
AI コーチを使ってこの質問に答えてみる

7リアルタイムレンダラーにおいて、頂点がモデル空間からスクリーン空間に至るまでに経由する各座標空間の流れを説明してください。

リアルタイムレンダリングパイプラインにおいて、頂点は一般にいくつかの座標空間を経由して変換されます。具体的には、モデル(ローカル)空間、ワールド空間、ビュー(カメラ)空間、クリップ空間、正規化デバイス座標(NDC: Normalized Device Coordinates)、そしてスクリーン(ビューポート/ウィンドウ)空間です。 頂点はまず、アセットのローカル原点を基準とするモデル空間から始まります。モデル行列(またはワールド行列)を乗算することで、共有されたワールド空間内に配置および回転・拡大縮小されます。次にビュー行列を乗算することで、カメラを原点として所定の視線方向を向いたビュー空間へと変換されます。 続いて、射影行列を乗算して座標を4次元のクリップ空間 $(x_c, y_c, z_c, w_c)$ に変換し、視体積(view volume)に対してジオメトリのクリッピングを行います。クリッピング後、固定機能ハードウェアが透視除算($x_c, y_c, z_c$ を $w_c$ で除算)を実行し、3次元の正規化デバイス座標(NDC)を生成します。最後に、ビューポート変換によってNDC座標が2次元のスクリーン空間のピクセル座標およびデプスバッファ値へとマッピングされます。

// Vertex Shader Stage
float4 worldPos = mul(modelMatrix, float4(inPosition, 1.0));
float4 viewPos  = mul(viewMatrix, worldPos);
float4 clipPos  = mul(projMatrix, viewPos); // Output to rasterizer

// Hardware Fixed-Function Stages:
// 1. Clipping against [-w, w]
// 2. Perspective Divide: ndcPos = clipPos.xyz / clipPos.w;
// 3. Viewport Transform -> Screen pixel coordinates (x_px, y_px)
AI コーチを使ってこの質問に答えてみる

中級向け質問

8LBS(Linear Blend Skinning)とDQS(Dual-Quaternion Skinning)について、変形の品質、アーティファクト、および実装の複雑さの観点から比較してください。

リニアブレンドスキニング(LBS: Linear Blend Skinning)とデュアルクォータニオンスキニング(DQS: Dual-Quaternion Skinning)は、スケルトンメッシュの変形に対する2つの異なるアプローチです。 1. 変形の品質とアーティファクト: - LBSは、ボーンの変換行列を線形補間して変換後の頂点を計算します。高速ですが、大きな回転やねじれが生じる際に体積の損失(ボリュームロス)が発生し、特にねじれ軸に沿って円筒状のジオメトリが潰れる「キャンディラッパー(飴の包み紙)」アーティファクトが顕著に現れます。 - DQSは、ボーンの剛体変換を単位デュアルクォータニオン(回転と平行移動の組み合わせ)として表現します。ブレンド時(DLB: Dual Linear Blendingなどを使用)に自然に体積が維持され、キャンディラッパーのようなねじれアーティファクトが解消されます。ただし、関節を極端に曲げた際に膨らみや挟み込み(ピンチング)が生じるといった独自のアーティファクトが発生します。 2. エンジニアリングおよび実装の複雑さ: - LBSは、標準的な4x4行列パイプラインを使用して、完全なアフィン変換(平行移動、回転、および非一様スケーリングやせん断)をネイティブにサポートします。 - DQSがネイティブに扱えるのは剛体変換のみです。スケーリング(特に非一様スケーリング)を扱うには、マルチパス変形、極分解、またはスケールとせん断の分離が必要になります。さらに、ブレンド時に対蹠性(antipodality)の処理(デュアルクォータニオンの内積をチェックして最短の回転経路を選択し、メッシュの反転や崩壊を防ぐ処理)が必要となるため、シェーダーの計算およびアセットパイプラインがより複雑になります。

struct DualQuat {
    float4 rot;
    float4 trans;
};

DualQuat BlendDualQuaternions(uint4 indices, float4 weights, StructuredBuffer<DualQuat> boneDQs)
{
    DualQuat dq0 = boneDQs[indices.x];
    DualQuat blended = dq0;
    blended.rot *= weights.x;
    blended.trans *= weights.x;

    [unroll]
    for (int i = 1; i < 4; ++i)
    {
        DualQuat dqi = boneDQs[indices[i]];
        // Antipodality check: ensure shortest path
        float signVal = dot(dq0.rot, dqi.rot) < 0.0 ? -1.0 : 1.0;
        blended.rot += dqi.rot * (weights[i] * signVal);
        blended.trans += dqi.trans * (weights[i] * signVal);
    }
    float len = length(blended.rot);
    blended.rot /= len;
    blended.trans /= len;
    return blended;
}
AI コーチを使ってこの質問に答えてみる

9アニメーション付きキャラクターにおいて、一部のメッシュでのみ変形が正しく行われません。どのアセットデータやシェーダーデータを調査しますか?

アニメーション付きキャラクターにおいて一部のメッシュでのみ変形が正しく行われない場合、通常はアセットパイプライン、頂点レイアウト、またはシェーダー定数間でのデータの不整合が原因です。体系的な調査項目は以下のとおりです。 1. 頂点レイアウトとボーンインデックスの境界値:頂点のボーンインデックスがスケルトンのボーン数を超えていないか、パックされたデータ型をオーバーフローしていないか(例:スケルトンに256本を超えるボーンがある場合にuint8/ubyte4を使用してインデックスがラップアラウンドするなど)を確認します。 2. ボーンウェイトの正規化:頂点ごとのボーンウェイトの合計が1.0になっていることを検証します。ウェイトが正規化されていないと、頂点がスケルトンに向かって収縮したり、スケルトンから不自然に引き伸ばされたりします。 3. インバースバインドポーズ行列(IBM: Inverse Bind Pose Matrices):メッシュのインバースバインド行列が、スケルトンのレストポーズおよび座標空間と一致していることを確認します。バインドポーズが一致していないと、メッシュの形状が大きく崩れたり、不正なオフセットが生じたりします。 4. 頂点あたりの最大インフルエンス数:DCCツール(Digital Content Creation tool)のエクスポーターが、頂点バッファレイアウトやシェーダーでサポートされている数(例:4インフルエンス)を超える頂点あたりのボーンインフルエンス(例:8インフルエンス)を出力し、再正規化されずにウェイトが欠落していないかを確認します。 5. スケルトン階層とパレットインデックス:メッシュ内のボーンインデックスのマッピングが、定数バッファまたは構造化バッファにアップロードされたボーン行列パレットと一致しているかを検証します。

struct SkinVertex {
    float position[3];
    uint8_t boneIndices[4];
    uint8_t boneWeights[4]; // UNORM8
};

void ValidateMeshSkinData(const std::vector<SkinVertex>& vertices, uint32_t maxBoneCount)
{
    for (size_t i = 0; i < vertices.size(); ++i)
    {
        const auto& v = vertices[i];
        int weightSum = 0;
        for (int b = 0; b < 4; ++b)
        {
            assert(v.boneIndices[b] < maxBoneCount && "Bone index exceeds palette size!");
            weightSum += v.boneWeights[b];
        }
        assert(std::abs(weightSum - 255) <= 1 && "Bone weights do not normalize to 1.0!");
    }
}
AI コーチを使ってこの質問に答えてみる

10モーフターゲット(ブレンドシェイプ)とは何ですか?また、顔のアニメーションにおいてスキニング(skeletal skinning)とどのように組み合わされますか?

モーフターゲット(ブレンドシェイプ)は、基準となるレストポーズ(rest-pose)メッシュに対する頂点ごとの差分オフセット(位置の差分、法線の差分、および必要に応じて接線の差分)として保存される幾何学的な変形を表します。各モーフターゲットはスカラーの重み(通常は 0.0 から 1.0)によって制御され、変形後の頂点属性は次のように計算されます:`Morphed_Attribute = Base_Attribute + Sum(Weight_i * Delta_i)`。顔のアニメーションなどでモーフターゲットとスキニング(skeletal skinning)を組み合わせる場合:1. 評価順序:モーフターゲットの差分は、スキニングが適用される前に、ニュートラル/バインドポーズのモデル空間で評価される必要があります。2. スキニングパス:変形された頂点位置や法線は、その後スキニングのボーン行列によって変換されます。スキニングの前にモーフィングを適用することで、頭部の回転や顎関節の動きに合わせて表情が自然に変形するようになります。パフォーマンスと帯域幅の観点からは、数十個のブレンドシェイプに対してメッシュ全体のコピーを愚直に保存・読み出しすると、メモリ帯域幅に大きな負荷がかかります。実用的な実装では、疎な差分データ(値がゼロではない頂点のみ)の保存、差分フォーマットの圧縮(FP16 や量子化整数など)、あるいは複数回の描画パスの前に GPU コンピュートシェーダーの事前パスを用いて変形後頂点を一度だけ計算する手法が用いられます。

struct VertexInput {
    float3 position : POSITION;
    float3 normal   : NORMAL;
    uint4  boneIndices : BLENDINDICES;
    float4 boneWeights : BLENDWEIGHT;
};

// 1. Accumulate morph deltas in local rest space
float3 morphedPos = input.position;
float3 morphedNorm = input.normal;

for (int i = 0; i < activeMorphCount; ++i) {
    morphedPos  += morphDeltasPos[i]  * morphWeights[i];
    morphedNorm += morphDeltasNorm[i] * morphWeights[i];
}
morphedNorm = normalize(morphedNorm);

// 2. Skin morphed geometry to world space
float4 skinnedPos = 0;
float3 skinnedNorm = 0;
for (int b = 0; b < 4; ++b) {
    float4x4 boneMat = BoneMatrices[input.boneIndices[b]];
    skinnedPos  += mul(boneMat, float4(morphedPos, 1.0)) * input.boneWeights[b];
    skinnedNorm += mul((float3x3)boneMat, morphedNorm)   * input.boneWeights[b];
}
AI コーチを使ってこの質問に答えてみる

11視覚的安定性、頂点属性の保持、およびパフォーマンスのバランスを取る幾何学的LOD(Level of Detail)の選択手法、メッシュ簡略化、および遷移戦略について説明してください。

幾何学的LOD(Level of Detail)は、オブジェクトがカメラから遠ざかるにつれてメッシュの複雑さを削減し、視覚的な忠実度とフレームレートのバランスを取りながら描画パフォーマンスを最適化します。 1. LODの選択: LODの切り替えは、カメラの画角(FOV)や解像度の変化に対応するため、固定的なワールド空間の距離ではなく、スクリーン空間のメトリクス(投影されたバウンディングスフィアの直径、画面高さに対する割合、投影ピクセル誤差など)に基づいて選択する必要があります。距離の境界付近でLODが頻繁に切り替わる現象(LODスラッシング)を防ぐため、詳細度を上げる閾値と下げる閾値を個別に設けるヒステリシスを適用します。 2. メッシュの簡略化: オフラインでの生成では、反復的なエッジ崩壊(edge collapse)を伴う二次誤差メトリクス(QEM: Quadric Error Metrics)が広く利用されます。視覚的な品質を維持するために、簡略化アルゴリズムは境界のシルエットを維持し、幾何学的歪みにペナルティを科す必要があります。また、二次誤差メトリクスに属性誤差項を組み込むことで、頂点属性(UVシーム、法線の分離、頂点カラー、スキニングウェイト)も保持しなければなりません。 3. 遷移戦略: 突発的な視覚的変化(ポッピング)を防ぐため、エンジンでは以下のような手法が採用されます。 - ディザリングによるクロスフェード / スクリーンドアスティップリング: ピクセルシェーダでインターリーブされたディザパターン(例: Bayer行列)を用いてピクセルを破棄することで、アルファブレンディングを必要とせず、Early-Zを阻害することなくLOD間を滑らかにフェードさせます。 - ジオモーフィング(Geomorphing): 短い遷移期間中に、隣接するLODメッシュ間でGPU上の頂点位置を補間します。

float CalculateLODDither(float2 screenPos, float lodBlendFactor)
{
    const float bayer4x4[16] = {
         0.0/16.0,  8.0/16.0,  2.0/16.0, 10.0/16.0,
        12.0/16.0,  4.0/16.0, 14.0/16.0,  6.0/16.0,
         3.0/16.0, 11.0/16.0,  1.0/16.0,  9.0/16.0,
        15.0/16.0,  7.0/16.0, 13.0/16.0,  5.0/16.0
    };
    uint2 pixelCoord = (uint2)screenPos.xy % 4;
    float threshold = bayer4x4[pixelCoord.y * 4 + pixelCoord.x];
    return (lodBlendFactor - threshold);
}

// In pixel shader: if (CalculateLODDither(input.position.xy, lodTransitionAlpha) < 0.0) discard;
AI コーチを使ってこの質問に答えてみる

12インデックスバッファや頂点キャッシュの最適化とは何ですか。また、なぜ三角形の順序が変換後キャッシュ(post-transform cache)の効率に影響を与えるのですか。

頂点キャッシュ(またはインデックスバッファ)の最適化とは、メッシュ内の三角形インデックスと頂点データを並べ替え、GPUのハードウェア頂点キャッシュにおけるヒット率を最大化する処理です。GPUには主に2つの頂点キャッシュが存在します。 1. 変換後キャッシュ(Post-Transform Cache):頂点シェーダーの変換出力結果(位置や頂点属性)を保持する小さなFIFO/LRUキャッシュです。隣接する三角形が頂点を共有している場合、インデックスストリーム内でそれらの頂点を近い順序で参照することにより、同一の頂点に対して頂点シェーダーを複数回実行することなく、キャッシュされたシェーダー出力を再利用できます。 2. 変換前キャッシュ(Pre-Transform Cache):生の頂点バッファデータ用となるGPUのL1/L2メモリキャッシュです。頂点バッファデータを最適化済みインデックスの初回アクセス順序に合わせて並べ替えることで、空間的局所性とメモリ帯域幅の利用効率を最大化します。 三角形の順序は、変換後キャッシュへのアクセス順序を直接決定します。Tom ForsythのアルゴリズムやTipsifyなどの最適化アルゴリズムは、頂点の共有数(価数)とキャッシュ位置に基づいて動的な再利用スコアを割り当て、最近キャッシュされた頂点の残りの参照を優先的に完了させる三角形を処理することで、平均キャッシュミス率(ACMR: Average Cache Miss Ratio)を最小化します。

float calculateVertexScore(int cachePosition, int remainingValence) {
    if (remainingValence == 0) return -1.0f;
    float score = 0.0f;
    if (cachePosition >= 0) {
        if (cachePosition < 3) {
            score = 0.75f; // Recent vertex in cache (bonus for immediate reuse)
        } else {
            score = std::pow(1.0f - (cachePosition - 3) / 29.0f, 1.5f); // Gradual falloff
        }
    }
    // Bonus for vertices with few remaining triangles (clearing valence faster)
    score += 2.0f * std::pow(remainingValence, -0.5f);
    return score;
}
AI コーチを使ってこの質問に答えてみる

上級向け質問

13CPU-GPU間のストールと描画の不自然なポッピング(オブジェクトの突然の出現)を回避するオクルージョンカリング手法にはどのようなものがありますか?

従来のハードウェアオクルージョンクエリでは、同一フレーム内でCPUが可視性判定結果を待機すると、同期的なCPU-GPUリードバックストールが発生します。リードバックを1フレーム遅延させればストールは回避できますが、時間的なレイテンシが生じ、新たに可視となったオブジェクトが即座にレンダリングされずに目に見えるポッピングが発生します。CPU-GPUストールと視覚的ポッピングの双方を防ぐため、近年の本番アーキテクチャでは以下の手法が採用されています。 1. 2フェーズGPU駆動Hi-Zオクルージョンカリング: 前フレームから生成された階層型Zバッファ(Hi-Z: Hierarchical-Z)デプス深度ピラミッドに対して、GPUがバウンディングボックスの交差判定を行います。可視であることが判明しているオブジェクトをフェーズ1で描画し(現フレームの初期深度を生成)、直前まで遮蔽されていたオブジェクトはフェーズ2で更新後の現フレームHi-Zバッファと再テストします。新たに露出したオブジェクトはライティングやポストプロセスの直前に即座にレンダリングされるため、CPUリードバックを一切発生させずにポッピングを排除できます。 2. CPUソフトウェアラスタライゼーション: 簡略化されたオクルーダーメッシュから、低解像度の深度バッファをCPUワーカースレッド上でのみ(SIMDを活用して)ラスタライズします。CPUが同期的にバウンディングボックスをテストするため、GPUクエリの発行やGPUからCPUへのデータ転送レイテンシが発生しません。 3. 保守的なバウンディングと時間的ヒステリシス: バウンディングボリュームを意図的に拡大するか、非可視状態への遷移判定を意図的に遅延させることで、急激なカメラ移動時における過剰な早期カリングを防ぎます。

// Phase 1: Render instances visible in the previous frame
[numthreads(64, 1, 1)]
void Phase1_CullCS(uint id : SV_DispatchThreadID) {
    if (id >= totalInstances) return;
    Instance inst = instances[id];
    if (wasVisibleLastFrame[id] && TestHiZ(inst.bounds, prevFrameHiZ)) {
        AppendDraw(phase1DrawBuffer, inst);
        currentVisibility[id] = true;
    }
}
// [Phase 1 draws -> depth buffer written -> Hi-Z updated for current frame]

// Phase 2: Test previously occluded objects against updated Hi-Z to avoid popping
[numthreads(64, 1, 1)]
void Phase2_CullCS(uint id : SV_DispatchThreadID) {
    if (id >= totalInstances) return;
    if (!currentVisibility[id] && TestHiZ(instances[id].bounds, currentFrameHiZ)) {
        AppendDraw(phase2DrawBuffer, instances[id]);
        currentVisibility[id] = true;
    }
}
AI コーチを使ってこの質問に答えてみる

14オーサリングされたメッシュからランタイムのGPU(Graphics Processing Unit)バッファに至る典型的なアセット処理パイプラインについて、接線生成、量子化、バリデーション、および最適化を含めて説明してください。

標準的なアセット処理パイプラインは、DCCツールで作成された生のメッシュ(FBX、glTF、USD)を、主に5つのステージを経て高性能かつGPU対応のバイナリ形式に変換します。 1. 取り込みとバリデーション(Ingestion and Validation):重複または未使用の頂点を削除し、縮退三角形(面積ゼロの三角形)を破棄し、多様体(マニホールド)ジオメトリを検証し、NaNを処理し、マルチマテリアルメッシュを個別のサブメッシュに分割してソースメッシュをクリーンアップします。 2. 接線空間の生成(Tangent Space Generation):法線ベイクツールとの視覚的一致を保証するため、標準化されたアルゴリズム(主にMikkTSpace)を用いて接線と従接線を計算します。これにより、UVの継ぎ目や反転されたUVチャートが適切に処理されます(利き手方向をtangent.wに格納)。 3. 最適化(Optimization):頂点変換後の頂点キャッシュ効率を高めるためにインデックスを並べ替え(Forsyth/Tipsifyなど)、頂点フェッチの局所性を高めるために頂点バッファを並べ替え、LOD(Level of Detail)レベルやメッシュレットを生成します。 4. 量子化と属性パッキング(Quantization and Attribute Packing):メモリ使用量とメモリ帯域幅を削減するために頂点属性を量子化します。位置は16ビットのhalf/unormまたは正規化整数に、法線および接線は8ビットのSNORMまたは八面体エンコーディング(Oct16/Oct32)に、UVは16ビットの浮動小数点数/unormに変換します。属性はインターリーブ(AoS: Array of Structures)されるか、複数のストリームに分割(SoA: Structure of Arrays、例:深度プリパス専用の頂点位置のみのストリーム)されます。 5. クッキングとシリアライズ(Cooking and Serialization):ランタイムでのポインタ付け替えが不要になるよう、バッファ、境界ボリューム(AABB/境界球)、およびLODテーブルをフラットなバイナリファイルにシリアライズし、ステージングメモリ経由でGPUバッファへの高速なDMA転送を可能にします。

// Compress a float3 normal into 2D octahedral coordinates (8-bit SNORM each)
vec2 OctEncode(vec3 n) {
    n /= (abs(n.x) + abs(n.y) + abs(n.z));
    vec2 oct = (n.z >= 0.0) ? n.xy : (1.0 - abs(n.yx)) * sign(n.xy);
    return oct * 0.5 + 0.5;
}
// Stored as 2x 8-bit unorm/snorm (2 bytes vs 12 bytes float3)
AI コーチを使ってこの質問に答えてみる

15大規模な静的シーンにおけるメッシュレット、クラスタカリング、メッシュシェーダー、および高密度マイクロジオメトリパイプラインについて説明してください。

メッシュレットパイプラインや高密度マイクロジオメトリアーキテクチャ(Unreal EngineのNaniteなど)は、インデックスバッファを用いた大規模な描画コール(draw call)を、「メッシュレット」と呼ばれる境界付きの小さなジオメトリクラスタに置き換えます。1. メッシュレット: メッシュレットは、通常32〜128頂点および最大128〜256ポリゴン(三角形)に制限されたジオメトリクラスタです。各メッシュレットにはローカル頂点インデックス、属性ストリーム、および事前計算されたバウンディングデータ(バウンディングスフィアや法線コーン)が含まれます。2. メッシュシェーダーと増幅(タスク)シェーダー: これらは従来の固定機能頂点処理、プリミティブアセンブリ、およびジオメトリシェーダーのパイプラインを置き換えます。増幅(タスク)シェーダーは、メッシュレットのグループ全体に対してクラスタ単位の視錐台カリング、オクルージョンカリング、および法線コーンに基づく背面カリングを評価します。カリングを通過したメッシュレットはメッシュシェーダーをディスパッチし、スレッドグループがオンチップ共有メモリ(LDS)上で協調して頂点を変換し、ラスタライザへプリミティブインデックスを直接出力します。3. 高密度マイクロジオメトリパイプライン: ジオメトリの高密度化によってサブピクセルサイズの三角形が発生すると、深刻なクアッドオーバードロー(標準ハードウェアが2×2ピクセルのヘルパークアッドをラスタライズする際、わずか1ピクセルしかカバーしていなくても完全なピクセルシェーダーを実行してしまう現象)が生じます。近年の高密度マイクロジオメトリシステムでは、階層的クラスタLOD(Level of Detail)構造(DAG)を用いて約1ピクセルのエッジ長を保証するクラスタLODを動的に選択し、大きなポリゴン向けのハードウェアラスタライズと、サブピクセル単位のマイクロポリゴン向けカスタムコンピュートソフトウェアラスタライザを組み合わせて処理することが一般的です。

#define MAX_VERTS 64
#define MAX_PRIMS 128

struct MeshletPayload { uint meshletIndices[32]; };

[outputtopology("triangle")]
[numthreads(32, 1, 1)]
void MainMS(
    in uint gtid : SV_GroupThreadID,
    in uint gid : SV_GroupID,
    in payload MeshletPayload payloadData,
    out vertices VertexOutput outVerts[MAX_VERTS],
    out indices uint3 outIndices[MAX_PRIMS]
) {
    uint meshletId = payloadData.meshletIndices[gid];
    Meshlet m = meshlets[meshletId];
    SetMeshOutputCounts(m.vertexCount, m.primitiveCount);
    
    // Cooperatively transform vertices
    for (uint v = gtid; v < m.vertexCount; v += 32) {
        outVerts[v] = TransformVertex(m.vertexOffset + v);
    }
    // Output local triangle indices
    for (uint p = gtid; p < m.primitiveCount; p += 32) {
        outIndices[p] = GetMeshletTriangle(m.triangleOffset + p);
    }
}
AI コーチを使ってこの質問に答えてみる