Vulkan VK_EXT_mesh_shader ジオメトリパイプライン最適化:従来シェーダー廃止への移行戦略【2026年5月】
Vulkan VK_EXT_mesh_shaderで頂点・ジオメトリ・テッセレーションシェーダーを完全置き換え。GPU負荷50%削減の最新レンダリングパイプライン移行ガイド
約13分で読めますVulkan 1.3.280(2026年3月リリース)で正式に拡張仕様が確定したVK_EXT_mesh_shaderは、従来の頂点シェーダー・ジオメトリシェーダー・テッセレーションシェーダーステージを完全に置き換える新世代のレンダリングパイプラインです。
AMD RDNA 3(Radeon RX 7000シリーズ)およびNVIDIA Ada Lovelace(RTX 40シリーズ)アーキテクチャで完全サポートされ、DirectX 12のMesh Shaderと同等の機能を提供しつつ、Vulkan特有の細粒度メモリ制御により最大50%のGPU負荷削減を実現します。
本記事では、Khronos Groupが2026年4月に公開した公式移行ガイドおよびNVIDIA開発者ブログの最新実装事例を基に、従来のジオメトリパイプラインからメッシュシェーダーへの段階的移行戦略と最適化テクニックを解説します。
VK_EXT_mesh_shaderが従来パイプラインを置き換える理由
従来のVulkanレンダリングパイプラインは、頂点シェーダー(VS)→テッセレーション制御シェーダー(TCS)→テッセレーション評価シェーダー(TES)→ジオメトリシェーダー(GS)→フラグメントシェーダー(FS)という固定的な5段階構成でした。
この設計は2000年代初頭のGPUアーキテクチャに最適化されたもので、現代のコンピュートユニット統合型GPU(RDNA 3、Ada Lovelace)では以下の深刻な非効率性が顕在化しています。
従来パイプラインの技術的限界(2026年時点の測定結果):
- 固定ステージ間のバッファ転送オーバーヘッド: VS→TES間で頂点データを中間バッファに書き出し、次ステージで再読み込みするため、メモリバンド幅を平均35%浪費(AMD測定)
- ジオメトリシェーダーのスケーラビリティ欠如: GSは単一ワークグループ内でしか並列化できず、1プリミティブあたり最大4頂点出力の制限により、複雑なメッシュ生成で性能が線形劣化
- テッセレーション固定機能の制約: TESの出力トポロジー(三角形・クワッド・アイソライン)が固定され、Nanite型の可変LODメッシュ生成に対応不可
以下の図は従来パイプラインとメッシュシェーダーパイプラインの構造比較です。
graph TD
subgraph 従来パイプライン
A1[頂点シェーダー] -->|中間バッファ| B1[テッセレーション制御]
B1 -->|パッチデータ| C1[テッセレーション評価]
C1 -->|頂点データ| D1[ジオメトリシェーダー]
D1 -->|プリミティブ| E1[ラスタライザ]
E1 --> F1[フラグメントシェーダー]
end
subgraph メッシュシェーダーパイプライン
A2[タスクシェーダー] -->|メッシュレット| B2[メッシュシェーダー]
B2 -->|直接プリミティブ出力| C2[ラスタライザ]
C2 --> D2[フラグメントシェーダー]
end
style A1 fill:#ff9999
style B1 fill:#ff9999
style C1 fill:#ff9999
style D1 fill:#ff9999
style A2 fill:#99ff99
style B2 fill:#99ff99
従来パイプラインでは各ステージが独立したメモリ空間を持ち、ステージ間遷移でGPUキャッシュをフラッシュする必要がありました。メッシュシェーダーはタスクシェーダー(TS)とメッシュシェーダー(MS)の2ステージに統合され、両者がワークグループ共有メモリ(Shared Memory)を共有します。
VK_EXT_mesh_shaderの技術的優位性:
- コンピュートシェーダー型並列化: TSとMSはコンピュートシェーダーと同じディスパッチモデルを使用し、GPU全体のコンピュートユニットを動的に活用
- メッシュレット単位の処理: 64〜256頂点のメッシュレット(meshlet)を単位とし、L1キャッシュに収まるサイズで処理することでキャッシュヒット率を85%以上に向上(NVIDIA測定)
- 動的プリミティブ生成: MSから直接ラスタライザにプリミティブを出力し、中間バッファを完全排除
メッシュシェーダーパイプラインの実装:拡張機能の有効化
VK_EXT_mesh_shaderは2026年3月のVulkan 1.3.280でコア仕様に統合されましたが、後方互換性のため拡張機能として有効化する必要があります。
以下のコードはデバイス作成時に拡張を有効化し、メッシュシェーダー機能の利用可能性を確認する実装です。
#include <vulkan/vulkan.h>
#include <vector>
#include <iostream>
// メッシュシェーダー機能の確認
VkPhysicalDeviceMeshShaderFeaturesEXT meshShaderFeatures = {};
meshShaderFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT;
meshShaderFeatures.pNext = nullptr;
VkPhysicalDeviceFeatures2 deviceFeatures2 = {};
deviceFeatures2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
deviceFeatures2.pNext = &meshShaderFeatures;
vkGetPhysicalDeviceFeatures2(physicalDevice, &deviceFeatures2);
if (!meshShaderFeatures.taskShader || !meshShaderFeatures.meshShader) {
std::cerr << "VK_EXT_mesh_shader not supported" << std::endl;
return VK_ERROR_FEATURE_NOT_PRESENT;
}
// デバイス拡張の有効化
std::vector<const char*> deviceExtensions = {
VK_EXT_MESH_SHADER_EXTENSION_NAME, // "VK_EXT_mesh_shader"
VK_KHR_SPIRV_1_4_EXTENSION_NAME // SPIR-V 1.4以上が必要
};
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.pNext = &meshShaderFeatures; // 機能を有効化
deviceCreateInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = deviceExtensions.data();
VkDevice device;
VkResult result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &device);
重要な注意点:
VkPhysicalDeviceMeshShaderFeaturesEXT::meshShaderQueriesを有効にすると、vkCmdWriteTimestampでTS/MSステージのGPUタイムスタンプを取得可能(性能プロファイリングに必須)primitiveFragmentShadingRate機能を併用すると、メッシュレット単位で可変レートシェーディング(VRS)を制御できる(後述の最適化テクニックで活用)
タスクシェーダーとメッシュシェーダーの実装パターン
メッシュシェーダーパイプラインでは、タスクシェーダー(TS)がカリング・LOD選択を担当し、メッシュシェーダー(MS)が実際の頂点・プリミティブ生成を行います。
以下はUE5 Nanite型の階層的メッシュレットカリングを実装したGLSL例です(SPIR-V 1.4でコンパイル)。
// task_shader.glsl
#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
// メッシュレット記述子(ストレージバッファから読み込み)
struct Meshlet {
vec3 boundingSphereCenter;
float boundingSphereRadius;
uint vertexOffset;
uint triangleOffset;
uint vertexCount;
uint triangleCount;
};
layout(set = 0, binding = 0) readonly buffer MeshletBuffer {
Meshlet meshlets[];
};
layout(set = 0, binding = 1) uniform CameraUBO {
mat4 viewProj;
vec4 frustumPlanes[6]; // 視錐台平面方程式
vec3 cameraPos;
} camera;
// ワークグループ共有メモリ(カリング結果を共有)
shared uint visibleMeshletIndices[32];
shared uint visibleCount;
void main() {
uint meshletIndex = gl_GlobalInvocationID.x;
if (gl_LocalInvocationIndex == 0) {
visibleCount = 0;
}
barrier();
// 視錐台カリング
if (meshletIndex < meshlets.length()) {
Meshlet m = meshlets[meshletIndex];
bool visible = true;
// 球体と視錐台の交差判定
for (int i = 0; i < 6; i++) {
float dist = dot(camera.frustumPlanes[i].xyz, m.boundingSphereCenter)
+ camera.frustumPlanes[i].w;
if (dist < -m.boundingSphereRadius) {
visible = false;
break;
}
}
// 距離ベースLOD選択(カメラから64m以内のみ描画)
float distToCamera = length(camera.cameraPos - m.boundingSphereCenter);
if (distToCamera > 64.0) {
visible = false;
}
if (visible) {
uint index = atomicAdd(visibleCount, 1);
if (index < 32) {
visibleMeshletIndices[index] = meshletIndex;
}
}
}
barrier();
// 可視メッシュレットのみメッシュシェーダーにディスパッチ
EmitMeshTasksEXT(visibleCount, 1, 1);
}
タスクシェーダーではEmitMeshTasksEXTでメッシュシェーダーのワークグループ数を動的に決定します。視錐台外のメッシュレットを事前カリングすることで、後段MSの起動回数を平均60%削減できます(AMD測定、オープンワールドシーン)。
次にメッシュシェーダーの実装です。
// mesh_shader.glsl
#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
layout(triangles, max_vertices = 64, max_primitives = 126) out;
// メッシュシェーダー出力
layout(location = 0) out PerVertexData {
vec3 worldPos;
vec3 normal;
vec2 texCoord;
} vertexOutput[];
layout(set = 0, binding = 0) readonly buffer MeshletBuffer {
Meshlet meshlets[];
};
layout(set = 0, binding = 2) readonly buffer VertexBuffer {
vec3 positions[];
};
layout(set = 0, binding = 3) readonly buffer NormalBuffer {
vec3 normals[];
};
layout(set = 0, binding = 4) readonly buffer IndexBuffer {
uint indices[];
};
void main() {
uint meshletIndex = gl_WorkGroupID.x;
Meshlet m = meshlets[meshletIndex];
uint threadID = gl_LocalInvocationIndex;
// 頂点データの出力(ワークグループ内で並列処理)
if (threadID < m.vertexCount) {
uint vertexIndex = m.vertexOffset + threadID;
gl_MeshVerticesEXT[threadID].gl_Position =
camera.viewProj * vec4(positions[vertexIndex], 1.0);
vertexOutput[threadID].worldPos = positions[vertexIndex];
vertexOutput[threadID].normal = normals[vertexIndex];
// テクスチャ座標は省略
}
// プリミティブインデックスの出力
uint triangleCount = m.triangleCount;
if (threadID < triangleCount) {
uint indexOffset = m.triangleOffset + threadID * 3;
gl_PrimitiveTriangleIndicesEXT[threadID] =
uvec3(indices[indexOffset], indices[indexOffset + 1], indices[indexOffset + 2]);
}
// 頂点数・プリミティブ数を設定
if (threadID == 0) {
SetMeshOutputsEXT(m.vertexCount, triangleCount);
}
}
MSではSetMeshOutputsEXTで出力する頂点数・プリミティブ数を動的に指定できます。従来のジオメトリシェーダーの固定4頂点制限を撤廃し、メッシュレットサイズに応じて最適な出力数を選択可能です。
以下の図はタスクシェーダーとメッシュシェーダーの処理フローを示しています。
sequenceDiagram
participant CPU
participant TS as タスクシェーダー<br/>(ワークグループ単位)
participant MS as メッシュシェーダー<br/>(メッシュレット単位)
participant Rasterizer as ラスタライザ
CPU->>TS: vkCmdDrawMeshTasksEXT(totalMeshlets)
loop 各ワークグループ(32スレッド)
TS->>TS: メッシュレット境界球を読み込み
TS->>TS: 視錐台カリング実行
TS->>TS: LOD選択(距離ベース)
TS->>TS: 可視メッシュレット数をカウント
end
TS->>MS: EmitMeshTasksEXT(visibleCount)
loop 各可視メッシュレット
MS->>MS: 頂点データ読み込み(64頂点)
MS->>MS: 頂点変換(MVP行列)
MS->>MS: プリミティブインデックス設定
MS->>Rasterizer: SetMeshOutputsEXT(頂点数, 三角形数)
end
Rasterizer->>Rasterizer: フラグメントシェーダー実行
この図から分かるように、TSとMSはGPU上で完全に非同期実行され、中間バッファを介さずに直接データを渡します。
パイプライン作成と描画コマンドの発行
メッシュシェーダーパイプラインの作成は通常のグラフィックスパイプラインと類似していますが、頂点入力ステートが不要な点が異なります。
// パイプラインシェーダーステージの設定
VkPipelineShaderStageCreateInfo taskStage = {};
taskStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
taskStage.stage = VK_SHADER_STAGE_TASK_BIT_EXT;
taskStage.module = taskShaderModule; // task_shader.spvから作成
taskStage.pName = "main";
VkPipelineShaderStageCreateInfo meshStage = {};
meshStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
meshStage.stage = VK_SHADER_STAGE_MESH_BIT_EXT;
meshStage.module = meshShaderModule; // mesh_shader.spvから作成
meshStage.pName = "main";
VkPipelineShaderStageCreateInfo fragmentStage = {};
fragmentStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragmentStage.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragmentStage.module = fragmentShaderModule;
fragmentStage.pName = "main";
std::vector<VkPipelineShaderStageCreateInfo> stages = {
taskStage, meshStage, fragmentStage
};
// グラフィックスパイプライン作成
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = static_cast<uint32_t>(stages.size());
pipelineInfo.pStages = stages.data();
// pVertexInputState は nullptr(頂点入力不要)
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = &depthStencil;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
VkPipeline meshShaderPipeline;
vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &meshShaderPipeline);
描画コマンドの発行にはvkCmdDrawMeshTasksEXTを使用します。
// コマンドバッファの記録
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, meshShaderPipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);
// メッシュレット総数をタスクシェーダーにディスパッチ
uint32_t totalMeshlets = 1024; // モデルの総メッシュレット数
vkCmdDrawMeshTasksEXT(commandBuffer,
(totalMeshlets + 31) / 32, // ワークグループ数(32の倍数に切り上げ)
1, 1);
vkCmdDrawMeshTasksEXTの第2〜4引数はタスクシェーダーのワークグループ数(X, Y, Z)を指定します。1次元ディスパッチの場合、X軸のみを使用します。
従来パイプラインからの段階的移行戦略
既存のVulkanアプリケーションでは、頂点シェーダーベースのパイプラインが大量に実装されています。完全な書き換えはリスクが高いため、以下の段階的移行戦略を推奨します。
フェーズ1: ジオメトリシェーダーの置き換え(優先度:高)
ジオメトリシェーダーは最も性能劣化が顕著なステージです。パーティクルシステム・草葉生成・デバッグ線描画など、動的プリミティブ生成を行うシェーダーを最優先でメッシュシェーダーに移行します。
移行前のジオメトリシェーダー例:
// 従来のジオメトリシェーダー(非効率)
#version 450
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
void main() {
vec4 pos = gl_in[0].gl_Position;
// ビルボード4頂点を生成
gl_Position = pos + vec4(-1, -1, 0, 0);
EmitVertex();
gl_Position = pos + vec4(1, -1, 0, 0);
EmitVertex();
gl_Position = pos + vec4(-1, 1, 0, 0);
EmitVertex();
gl_Position = pos + vec4(1, 1, 0, 0);
EmitVertex();
EndPrimitive();
}
メッシュシェーダーへの移行後:
// メッシュシェーダー版(効率的)
#version 460
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 1) in;
layout(triangles, max_vertices = 4, max_primitives = 2) out;
layout(location = 0) out vec2 texCoord[];
void main() {
vec4 pos = particlePositions[gl_WorkGroupID.x];
// 4頂点を並列生成(1ワークグループで完結)
gl_MeshVerticesEXT[0].gl_Position = pos + vec4(-1, -1, 0, 0);
gl_MeshVerticesEXT[1].gl_Position = pos + vec4(1, -1, 0, 0);
gl_MeshVerticesEXT[2].gl_Position = pos + vec4(-1, 1, 0, 0);
gl_MeshVerticesEXT[3].gl_Position = pos + vec4(1, 1, 0, 0);
texCoord[0] = vec2(0, 0);
texCoord[1] = vec2(1, 0);
texCoord[2] = vec2(0, 1);
texCoord[3] = vec2(1, 1);
// 2三角形のインデックス
gl_PrimitiveTriangleIndicesEXT[0] = uvec3(0, 1, 2);
gl_PrimitiveTriangleIndicesEXT[1] = uvec3(1, 3, 2);
SetMeshOutputsEXT(4, 2);
}
この変更だけで、パーティクル描画性能が平均3.2倍向上します(NVIDIA RTX 4070測定、10万パーティクル)。
フェーズ2: 静的メッシュの頂点シェーダー保持とハイブリッド構成(優先度:中)
UI・2Dスプライト・単純な静的メッシュは従来の頂点シェーダーのまま保持し、複雑な3Dモデル・地形のみメッシュシェーダーに移行します。
Vulkanは同一レンダーパス内で複数のパイプラインを切り替え可能なため、以下のように描画タイプ別にパイプラインを使い分けます。
// レンダーパス内でのハイブリッド描画
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// 静的メッシュは従来の頂点シェーダーパイプライン
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, vertexShaderPipeline);
vkCmdDrawIndexed(commandBuffer, staticMeshIndexCount, 1, 0, 0, 0);
// 地形・大規模モデルはメッシュシェーダーパイプライン
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, meshShaderPipeline);
vkCmdDrawMeshTasksEXT(commandBuffer, terrainMeshletCount / 32, 1, 1);
vkCmdEndRenderPass(commandBuffer);
フェーズ3: テッセレーションシェーダーの置き換え(優先度:低)
テッセレーションは地形LOD・水面シミュレーションなど特定用途に限定されるため、最後に移行します。メッシュシェーダーではmax_verticesを256まで拡張できるため、テッセレーション係数に応じて動的に頂点数を調整可能です。
以下の図は段階的移行の推奨順序を示しています。
gantt
title メッシュシェーダー移行ロードマップ(2026年5月〜2027年)
dateFormat YYYY-MM
section フェーズ1
ジオメトリシェーダー廃止 :done, p1, 2026-05, 2026-07
パーティクルシステム移行 :done, p1-1, 2026-05, 2026-06
動的プリミティブ生成移行 :done, p1-2, 2026-06, 2026-07
section フェーズ2
大規模メッシュ移行 :active, p2, 2026-07, 2026-10
地形レンダリング移行 :p2-1, 2026-07, 2026-09
キャラクターモデル移行 :p2-2, 2026-09, 2026-10
section フェーズ3
テッセレーション置き換え :p3, 2026-10, 2027-01
水面シミュレーション移行 :p3-1, 2026-10, 2026-12
地形LODシステム移行 :p3-2, 2026-12, 2027-01
section 最適化
メッシュレットサイズ調整 :p4, 2027-01, 2027-03
タスクシェーダーカリング最適化 :p4-1, 2027-01, 2027-02
VRS統合 :p4-2, 2027-02, 2027-03
最適化テクニック:可変レートシェーディングとの統合
VK_EXT_mesh_shaderはVK_KHR_fragment_shading_rate拡張と組み合わせることで、メッシュレット単位でシェーディングレートを制御できます。
以下はメッシュシェーダーから直接VRSレートを設定する実装です(Vulkan 1.3.285以降で利用可能)。
// mesh_shader_vrs.glsl
#version 460
#extension GL_EXT_mesh_shader : require
#extension GL_EXT_fragment_shading_rate : require
layout(local_size_x = 32) in;
layout(triangles, max_vertices = 64, max_primitives = 126) out;
// プリミティブごとのシェーディングレート出力
layout(location = 0) perprimitiveEXT out uint shadingRate[];
void main() {
uint meshletIndex = gl_WorkGroupID.x;
Meshlet m = meshlets[meshletIndex];
// 距離ベースでシェーディングレートを決定
float distToCamera = length(camera.cameraPos - m.boundingSphereCenter);
uint rate;
if (distToCamera < 16.0) {
rate = gl_ShadingRateFlag1x1EXT; // フル解像度
} else if (distToCamera < 48.0) {
rate = gl_ShadingRateFlag2x2EXT; // 2x2ピクセルで1フラグメント
} else {
rate = gl_ShadingRateFlag4x4EXT; // 4x4ピクセルで1フラグメント
}
// 全プリミティブに同じレートを設定
uint triangleCount = m.triangleCount;
for (uint i = gl_LocalInvocationIndex; i < triangleCount; i += 32) {
shadingRate[i] = rate;
}
// 頂点・プリミティブ出力(前述のコードと同様)
// ...
SetMeshOutputsEXT(m.vertexCount, triangleCount);
}
この実装により、遠方のメッシュレットのシェーディング負荷を最大93.75%削減できます(4x4レート適用時)。NVIDIAの測定では、オープンワールドゲームで平均52%のフラグメントシェーダー実行時間短縮を達成しています。
出典: Unsplash / Unsplash License
まとめ
Vulkan VK_EXT_mesh_shaderによる従来ジオメトリパイプラインの置き換えは、2026年5月時点で以下の明確な技術的優位性を持ちます。
- 中間バッファ排除によるメモリバンド幅35%削減: タスクシェーダーとメッシュシェーダーがワークグループ共有メモリを直接利用
- 動的カリングによるMS起動回数60%削減: 視錐台カリング・オクルージョンカリングをTS段階で実行
- コンピュートシェーダー型並列化: GPU全体のコンピュートユニットを動的活用し、固定ステージの制約を撤廃
- VRS統合によるフラグメントシェーダー負荷52%削減: メッシュレット単位でシェーディングレートを制御
移行戦略としては、性能劣化が顕著なジオメトリシェーダーを最優先で置き換え、静的メッシュは従来パイプラインとのハイブリッド構成を維持することで、リスクを最小化しつつ段階的に移行することを推奨します。
Khronos Groupは2026年4月の公式ガイドで「2027年末までに主要なゲームエンジンがメッシュシェーダーを標準採用する」と予測しており、今後2年間で従来のジオメトリパイプラインは完全に廃止される方向性が明確になっています。
参考リンク
- Vulkan Guide: Mesh Shading - Khronos Group - 公式実装ガイド(2026年4月更新)
- VK_EXT_mesh_shader Extension Specification - Khronos Registry - 拡張仕様書
- Mesh Shaders in Vulkan: A Deep Dive - NVIDIA Developer Blog - NVIDIA実装事例とベンチマーク(2026年3月)
- AMD RDNA 3 Mesh Shader Performance Analysis - GPUOpen - AMD性能測定レポート(2026年2月)
- Vulkan Samples: Mesh Shader Example - Khronos GitHub - 公式サンプルコード