💪

[Bevy] StandardMaterialを拡張する

2025/02/25に公開

BevyではStandardMaterialを使うことで基本的な物理ベースのレンダリングができます。
でも、、、
でも、、
でも、
もっとすごいことをしたい!
Shadertoyにあるようなかっこいいやつを作りたい!

そんなときどうすればよいのでしょうか?

ExntendedMaterialでStandardMaterialをパワーアップ!

ExntendedMaterialを使うことでStandardMaterialの機能をさらに拡張してVertex, Frangmentの両方のシェーダーを編集できます。
使用例はこちらです。なんかそれっぽいやつが作れるみたいです。
ExtendedMaterialの使い方は簡単です。

  1. これまで使っていたStandardMaterialをExtendedMaterialの中にいれて
  2. MyExtensionに追加したいデータをいれて
  3. あとは好きなようにシェーダーを編集するだけです!

以下のパスをスコープに入れておきます。

extended_material.rs
use bevy::{
    pbr::{ExtendedMaterial, MaterialExtension, OpaqueRendererMethod},
    prelude::*,
    render::render_resource::*,
};

main関数は以下のようにします。

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

setupというシステムの引数にcommandとMeshとMaterial(比較のためにStandard, Extendedの両方)を入れておきます。

extended_material.rs
fn setup(
    mut commands: Commands, 
    mut meshes: ResMut<Assets<Mesh>>,
    mut extendedmats: ResMut<Assets<ExtendedMaterial<StandardMaterial, MyExtension>>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
){
...

StandardMaterialでは↓ですが、

...
    commands.spawn((
        Mesh3d(meshes.add(Sphere::new(1.0))),
        MeshMaterial3d(materials.add(StandardMaterial{
            base_color: Color::linear_rgb(1., 0., 0.),
            ..default()
        })),
    ));
...
}

ExtededMaterialではこのようにします。ExtendedMaterialのbaseフィールドにStandardMaterialを、extensionフィールドに自作したMyExtensionを入れておきます。

...
    commands.spawn((
        Mesh3d(meshes.add(Sphere::new(1.0))),
        MeshMaterial3d(
            extendedmats.add(ExtendedMaterial{
                base: StandardMaterial{
                    base_color: Color::srgb(0.9, 0.1, 0.2), 
                    ..default()
                },
                extension: MyExtension{hoge: 2.0},
            })
        ),
    ));
...
}

#[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()
    }
}

なにがうれしいのか

  1. シェーダーで任意のデータを使える(MyExtensionのhoge: u32の部分です)
  2. StandardMaterialのOutputを自由に書き換えられる

といったメリットがあります。
WGSLのコードを用いて説明します。

extended_material.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}, // インプット、アウトプットの構造体の正体が書かれています
    pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
}

// Vertexシェーダーで使いたいフィールドを定義します
struct Vertex {
    @builtin(instance_index) instance_index: u32,
    @location(0) position: vec3<f32>,
    @location(1) normal: vec3<f32>,
    @location(2) uv: vec2<f32>,
};

// 自分で定義した任意のデータです
// BindGroupの0番(つまり@group(0))はBevyがView関連のデータを扱うためのグループ
// BindGroupの1番(つまり@group(1))はStandardMaterialのデータを扱うためのグループ
// BindGroupの2番(つまり@group(2))ではStandardMaterialがuniformを扱うときに
// インデックスの0から始めるため、もし番号がかぶるといけないから100番から使ってます
// もし100個以上のuniformを使う場合は適宜変更して下さい
@group(2) @binding(100) var<uniform> hoge: u32;

最初は必要な関数を使えるようにするためにいろいろとパスをスコープに入れておきます。
forward_io::{VertexOutput, FragmentOutput}というのはbevy_pbrというcrateで定義されているVertex, Fragmentの出力形式のstructです。またVertexシェーダーのインプットの構造体も定義されています。

Vertex, Fragmentシェーダーはこんな感じです。

extended_material.wgsl
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
    var out : VertexOutput;
    
    var vert = vertex; // vertexはImutableなので編集するための変数にいれます

    // ↓ ** の中でお好きなようにデータをいじっちゃってください
    // **********************************************************
    vert.position.x *= 0.5; // たとえばx座標の値を半分にできます
    // **********************************************************


    // あとは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;
}

こんな感じで自由にStandardMaterialのデータを扱えるようになります。

おまけ

https://www.youtube.com/watch?v=wkdzSYkvThc

Chris Biscardiがかっこいいシェーダーを作ってました。やっぱり彼はすごいですね。

Discussion