☝️

[Bevy] インスタンス化したメッシュの識別方法

に公開

メッシュをインスタンス化したときにシェーダーコード内でそのメッシュが全体のどれなのかが知りたいことがありますね。例えば草原を作りたいときは大量の草のメッシュをインスタンス化します。キャラクターに当たっている草は折れ曲がっているので、キャラクターに接触しているメッシュにだけ他とは違う処理をします。この場合メッシュを識別するためにメッシュの識別番号(Index)を使いますね。
本記事ではBevyで識別番号(Index)をどのようにつかうのか簡単に説明します。
動作環境:bevy 0.16.0-rc.3

bevyでメッシュをインスタンス化する

bevyでは同じメッシュとマテリアルのコンポーネントをもつエンティティは自動的にインスタンス化されます。公式サンプル
各メッシュごとの処理をするためにはインスタンスの番号(Index)を使います。

MeshTagを使ったIndex

赤いボールを10個生成します。一番左のボールが最初にレンダリングされたボールで座標は(0., 0., 0.)です。そのあとに右側にX座標を2ずつずらして10個になるまでスポーンしました。

もし左から0~9のIndexをつけることができたら、それぞれのボールをシェーダーコードで識別できそうです。
しかしbevyのinstance_index(built-in)はレンダリングした順番にIndexを振ってくれません。毎回バラバラの番号になっています。
試しにinstance_indexを使ってボールを縦に伸ばしてみると、、

↑このようになります。本来なら左から順番に0, 1, 2, ... , 9となってほしいですが、そうはなりません。これでは困るので改善案が出されました。そのPRが0.16からマージされ新たに使えるようになったコンポーネントがMeshTagです。MeshTagを使えば、Indexを最初から順番に振っていくことができます(自分で好きなunsigned intの値を格納できます)。
するとボールを順番に縦に伸ばせるようになります。

奇数の時だけ伸ばすとかもできます。

使用したコード

以下が使用したコードです。ちょっと長いですがmain.rsのほうは赤いボールを10個レンダリングしているだけです。MeshTagにforループのIndexを使用してます。

main.rs
use bevy::{
    color::palettes::basic::RED,
    pbr::{ExtendedMaterial, MaterialExtension},
    prelude::*,
    render::{render_resource::*, mesh::MeshTag,},
};

/// This example uses a shader source file from the assets subdirectory
const SHADER_ASSET_PATH: &str = "shaders/test.wgsl";

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MaterialPlugin::<
            ExtendedMaterial<StandardMaterial, MyExtension>,
        >::default())
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ExtendedMaterial<StandardMaterial, MyExtension>>>,
) {
    let mesh = meshes.add(Sphere::new(1.0));
    let mat = materials.add(ExtendedMaterial {
        base: StandardMaterial {
            base_color: RED.into(),
             ..Default::default()
        },
        extension: MyExtension { hoge: 1.0},
    });

    for i in 0..=9 {
        // sphere
        commands.spawn((
            Mesh3d(mesh.clone()),
            MeshMaterial3d(mat.clone()),
            MeshTag(i),
            Transform::from_xyz(i as f32 * 3.0, 0., 0.0),
        ));
    }
    // light
    commands.spawn((
        DirectionalLight::default(),
        Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    // camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-10., 0., 30.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
struct MyExtension{
    #[uniform(100)]
    hoge: f32,
}

impl MaterialExtension for MyExtension {
    fn vertex_shader() -> ShaderRef {
        SHADER_ASSET_PATH.into()
    }
    fn fragment_shader() -> ShaderRef {
        SHADER_ASSET_PATH.into()
    }
}

こちらはWGSLのコードです。Vertex shaderの***で囲まれているところで好きなようにメッシュの頂点データを変更できます。

test.wgsl
#import bevy_pbr::{
    pbr_fragment::pbr_input_from_standard_material,
    pbr_functions::alpha_discard,
    mesh_functions,
    view_transformations::position_world_to_clip,
    forward_io::{VertexOutput, FragmentOutput, Vertex}, // インプット、アウトプットの構造体の正体が書かれています
    pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
}

@group(2) @binding(100) var<uniform> hoge: u32;

@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
    var out : VertexOutput;
    
    var vert = vertex; // vertexはImutableなので編集するための変数にいれます
    let tag = mesh_functions::get_tag(vertex.instance_index); // Indexとして使います

    // ↓ ** の中でお好きなようにデータをいじっちゃってください
    // **********************************************************
    vert.position.y *= f32(tag + 1); // tagを使って左から順番に縦にのばしています
    // **********************************************************


    // あとはStandardMaterialのデータを正しく描画するために座標と法線とuvを変換しておきます
    var world_from_local = mesh_functions::get_world_from_local(vert.instance_index);
    out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vert.position, 1.0));
    out.position = position_world_to_clip(out.world_position.xyz);

    // UVを修正します
    out.uv = vert.uv;

    // normalを修正します
    var world_normal = mesh_functions::mesh_normal_local_to_world(vert.normal, vert.instance_index);
    out.world_normal = world_normal;

    // output 
    return out;
}

@fragment
fn fragment(in : VertexOutput, @builtin(front_facing) is_front: bool,) -> FragmentOutput {
    // StandardMaterialのデータを正しく描画するために影をつけてます
    var pbr_input = pbr_input_from_standard_material(in, is_front);
    var out: FragmentOutput;
    out.color = apply_pbr_lighting(pbr_input);

    // **********************************************************
    out.color = out.color * 2.0; // 色を変更することができます
    // **********************************************************
    
    return out;
}

参考

公式のサンプルが公開されています。
https://github.com/bevyengine/bevy/blob/fb159f984683038179a3cc6277355af85c8855e8/examples/shader/automatic_instancing.rs
https://github.com/bevyengine/bevy/blob/fb159f984683038179a3cc6277355af85c8855e8/assets/shaders/automatic_instancing.wgsl
こちらも試してみてください。

Discussion