bevy-voxel-pipeline について
Bevy 0.19 向けのボクセルメッシュ構築スキルです。block-mesh-rs の出力をメッシュに変換する方法に加え、方式の選択やチャンク間のパディング、タスクプールの限界を説明します。
主な機能
- 01greedy_quads と visible_block_faces の比較
- 02block-mesh-rs 出力の Bevy Mesh 接続
- 03チャンク間の隣接判定に必要なパディング
- 04非同期メッシュタスクと完了確認
- 05UV、メッシュ属性、実装上の注意点
活用シーン
- 01静的地形向けの少ないクアッド数の選択
- 02頻繁に編集するチャンク向けの高速再生成
- 03Bevy 0.19 用チャンクメッシュの構築
- 04本番スケジューリング前のタスク境界の検討
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ chrisgliddon/ bevy-skills --skill bevy-voxel-pipelineインストール後、Claude Code で/bevy-voxel-pipelineを入力して読み込まれたか確認します。
詳細
Bevy 0.19 — ボクセルメッシュ生成パイプライン
このスキルを使う場面
- チャンク単位のメッシュ生成で、Minecraft 風のボクセルワールドを構築する場合。
greedy_quads(大きな四角形を少数生成。静的な地形に最適)とvisible_block_faces(面ごとに四角形を1つ生成。編集時の高速な再メッシュに最適)を選ぶ場合。block-mesh-rsの出力をBevy 0.19のMeshに接続する場合。bevy-voxel-runtimeがスケジュールする、ワーカー側のみで完結するメッシュ生成を実装する場合。
基本パターン
use bevy::asset::RenderAssetUsages;
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use block_mesh::ndshape::{ConstShape, ConstShape3u32};
use block_mesh::{
greedy_quads, GreedyQuadsBuffer, MergeVoxel, Voxel, VoxelVisibility,
RIGHT_HANDED_Y_UP_CONFIG,
};
// 18^3 — the standard "chunk plus padding" block-mesh expects.
// Each axis gets 1 cell of padding on each side so neighbour lookups are
// in-bounds. The user-visible chunk is 16^3.
type ChunkShape = ConstShape3u32<18, 18, 18>;
#[derive(Clone, Copy, Eq, PartialEq, Default)]
pub struct BlockId(pub u16);
impl Voxel for BlockId {
fn get_visibility(&self) -> VoxelVisibility {
if self.0 == 0 {
VoxelVisibility::Empty
} else {
VoxelVisibility::Opaque
}
}
}
impl MergeVoxel for BlockId {
type MergeValue = u16;
fn merge_value(&self) -> Self::MergeValue { self.0 }
}
/// Build a Bevy 0.19 Mesh from a padded chunk of blocks.
/// Returns `None` for an entirely empty chunk so callers can skip spawning.
pub fn mesh_chunk(blocks: &[BlockId]) -> Option<Mesh> {
assert_eq!(blocks.len(), ChunkShape::SIZE as usize);
let mut buffer = GreedyQuadsBuffer::new(blocks.len());
greedy_quads(
blocks,
&ChunkShape {},
[0; 3],
[17, 17, 17],
&RIGHT_HANDED_Y_UP_CONFIG.faces,
&mut buffer,
);
if buffer.quads.num_quads() == 0 {
return None;
}
let num_indices = buffer.quads.num_quads() * 6;
let num_vertices = buffer.quads.num_quads() * 4;
let mut indices = Vec::with_capacity(num_indices);
let mut positions = Vec::with_capacity(num_vertices);
let mut normals = Vec::with_capacity(num_vertices);
for (group, face) in buffer.quads.groups.iter().zip(RIGHT_HANDED_Y_UP_CONFIG.faces.iter()) {
for quad in group.iter() {
indices.extend_from_slice(&face.quad_mesh_indices(positions.len() as u32));
positions.extend_from_slice(&face.quad_mesh_positions(quad, 1.0));
normals.extend_from_slice(&face.quad_mesh_normals());
}
}
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
mesh.try_insert_attribute(Mesh::ATTRIBUTE_POSITION, positions).ok()?;
mesh.try_insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals).ok()?;
mesh.insert_indices(Indices::U32(indices));
Some(mesh)
}
ワーカープリミティブのみ — 本番環境向けの編集スケジューラーではありません
以下では、タスクの所有権と、ブロッキングを伴わない完了確認を示します。結果の適用は意図的に
行いません。編集頻度の高いゲームを本番環境で実装する場合は、まずセクションのリビジョン/ライフサイクルトークンを検証し、
重複する作業をまとめ、ワーカー、結果、アップロード、コライダーの各予算を守る必要があります。ワールドに接続する前に
bevy-voxel-runtimeを参照してください。
use bevy::prelude::*;
use bevy::tasks::{futures::check_ready, AsyncComputeTaskPool, Task};
#[derive(Clone, Copy)]
struct MeshToken {
section: IVec3,
revision: u64,
epoch: u64,
}
struct MeshBuild {
token: MeshToken,
mesh: Option<Mesh>,
}
#[derive(Component)]
struct MeshTask(Task<MeshBuild>);
#[derive(Resource, Default)]
struct CompletedMeshes(Vec<MeshBuild>);
# fn _kick(mut commands: Commands) {
let pool = AsyncComputeTaskPool::get();
let blocks = vec![crate::BlockId::default(); 18 * 18 * 18]; // replace from chunk store
let token = MeshToken { section: IVec3::ZERO, revision: 7, epoch: 2 };
let task = pool.spawn(async move {
MeshBuild { token, mesh: crate::mesh_chunk(&blocks) }
});
commands.spawn(MeshTask(task));
# }
# fn _poll(mut commands: Commands, mut completed: ResMut<CompletedMeshes>, mut q: Query<(Entity, &mut MeshTask)>) {
for (entity, mut task) in &mut q {
if let Some(result) = check_ready(&mut task.0) {
commands.entity(entity).remove::<MeshTask>();
completed.0.push(result); // validate token in the bounded apply stage
}
}
# }
注意点
- パディングは必須です。
block-meshでチャンク間の隣接ブロックの可視性を正しく計算するには、各軸に1セル分のパディングが必要です。「16立方」のチャンクは18×18×18で格納されます。チャンク内の編集は、6つの隣接チャンクのパディングにも影響します。 - グリーディメッシュとシンプルなメッシュ。 クレートに含まれる球形入力のベンチマークでは、
greedy_quadsはメッシュ生成時間が約3倍になる一方で、四角形の数はおよそ3分の1になると報告されています。ブロックの分布に応じて再ベンチマークしてください。シンプルなメッシュ生成は編集頻度の高いチャンクに適している一方、グリーディメッシュは安定した地形に適していることが多いです。 - UVは自動では生成されません。
block-meshはUVを出力しません。四角形の面の向きとブロックIDを基に、自分で計算する必要があります。テクスチャアトラスと四角形ごとのオフセットを組み合わせるのが一般的な方法です(bevy-voxel-dataを参照)。 block-mesh-rsはBevyに依存しないクレート0.2.0です(最終更新は2022年)。依存先はどちらも純粋なRust製のilatticeとndshapeです。Bevyには一切依存しないため、Bevy 0.19と互換性があります。- 単一チャンクのメッシュ生成内で
par_iterを使う必要はありません。 block-meshはすでに高速です。並列化の効果が得られるのはチャンク間であり、チャンク内ではありません。AsyncComputeTaskPoolでN個のチャンクメッシュ生成タスクを起動してください。 - タスクプールはスケジューラーではありません。 編集ごとにタスクを1つ起動したり、座標だけで結果を適用したりしないでください。ダーティチャンクの波及、リビジョン、古い結果の破棄、重複作業の統合、優先度、バックプレッシャー、メッシュとコライダーのアトミックな入れ替えには、
bevy-voxel-runtimeを使用してください。 - Bevy 0.19では、
try_insert_attributeはResult<(), MeshAccessError>を返します。 新しく構築したメッシュではExtractedToRenderWorldは発生しませんが、メッシュの所有権が後で変更されても安全を保てるよう、結果を処理するか、呼び出し元に伝播してください。 RenderAssetUsagesはbevy::assetにあります(bevy::render::render_assetからは非公開で再エクスポートされています)。公開されている再エクスポート先はbevy::asset::RenderAssetUsagesです。
関連項目
bevy-voxel-data— 安定したブロックID、密なパレット、アトラスデータ。bevy-voxel-runtime— 本番環境向けのスケジューリング、 リビジョン検証、アップロード数の制限、コライダーの入れ替え、テレメトリー。bevy-assets— チャンクデータの読み込みとメッシュハンドルの管理。
コメント
Markdown 対応