Skip to content

Commit e4a0101

Browse files
committed
.
1 parent 0cdf5d1 commit e4a0101

24 files changed

Lines changed: 2642 additions & 103 deletions

Cargo.lock

Lines changed: 67 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ too_many_arguments = "allow"
2424

2525
[workspace.dependencies]
2626
bevy = { git = "https://github.com/processing/bevy", branch = "main", features = ["file_watcher", "shader_format_wesl", "free_camera", "pan_camera"] }
27-
bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" }
27+
bevy_naga_reflect = { path = "../../tychedelia/bevy_naga_reflect" }
2828
bevy_cuda = { git = "https://github.com/tychedelia/bevy_cuda" }
2929
naga = { version = "29", features = ["wgsl-in"] }
3030
wesl = { version = "0.3", default-features = false }
@@ -165,6 +165,38 @@ path = "examples/camera_controllers.rs"
165165
name = "compute_readback"
166166
path = "examples/compute_readback.rs"
167167

168+
[[example]]
169+
name = "field_basic"
170+
path = "examples/field_basic.rs"
171+
172+
[[example]]
173+
name = "field_animated"
174+
path = "examples/field_animated.rs"
175+
176+
[[example]]
177+
name = "field_oriented"
178+
path = "examples/field_oriented.rs"
179+
180+
[[example]]
181+
name = "field_colored"
182+
path = "examples/field_colored.rs"
183+
184+
[[example]]
185+
name = "field_colored_pbr"
186+
path = "examples/field_colored_pbr.rs"
187+
188+
[[example]]
189+
name = "field_emit"
190+
path = "examples/field_emit.rs"
191+
192+
[[example]]
193+
name = "field_lifecycle"
194+
path = "examples/field_lifecycle.rs"
195+
196+
[[example]]
197+
name = "field_from_mesh"
198+
path = "examples/field_from_mesh.rs"
199+
168200
[profile.wasm-release]
169201
inherits = "release"
170202
opt-level = "z"

crates/processing_core/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,6 @@ pub enum ProcessingError {
5656
PipelineCompileError(String),
5757
#[error("Pipeline not ready after {0} frames")]
5858
PipelineNotReady(u32),
59+
#[error("Field not found")]
60+
FieldNotFound,
5961
}

crates/processing_render/src/compute.rs

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -277,44 +277,57 @@ pub fn set_compute_property(
277277
.get_mut(entity)
278278
.map_err(|_| ProcessingError::ComputeNotFound)?;
279279

280-
let category = compute
281-
.shader
282-
.reflection()
283-
.parameter(&name)
284-
.map(|p| p.category())
285-
.ok_or_else(|| ProcessingError::UnknownShaderProperty(name.clone()))?;
286-
287-
match (&value, category) {
288-
(ShaderValue::Buffer(buf_entity), ParameterCategory::Storage { read_only }) => {
280+
// Resource values (buffers / textures) bind directly to top-level parameters
281+
// and need a category check. Scalar / vector / matrix values may target
282+
// either a top-level uniform or a nested struct field (e.g. `params.dt`),
283+
// so we let `apply_reflect_field` handle the path resolution itself.
284+
match value {
285+
ShaderValue::Buffer(buf_entity) => {
286+
let category = compute
287+
.shader
288+
.reflection()
289+
.parameter(&name)
290+
.map(|p| p.category())
291+
.ok_or_else(|| ProcessingError::UnknownShaderProperty(name.clone()))?;
292+
let ParameterCategory::Storage { read_only } = category else {
293+
return Err(ProcessingError::InvalidArgument(format!(
294+
"property `{name}` expects {category:?}, got Buffer",
295+
)));
296+
};
289297
let mut buffer = p_buffers
290-
.get_mut(*buf_entity)
298+
.get_mut(buf_entity)
291299
.map_err(|_| ProcessingError::BufferNotFound)?;
292300
compute.shader.insert(&name, buffer.handle.clone());
293301
if !read_only {
294302
buffer.bound_rw = true;
295303
}
296304
Ok(())
297305
}
298-
(ShaderValue::Texture(img_entity), ParameterCategory::Texture)
299-
| (ShaderValue::Texture(img_entity), ParameterCategory::StorageTexture) => {
306+
ShaderValue::Texture(img_entity) => {
307+
let category = compute
308+
.shader
309+
.reflection()
310+
.parameter(&name)
311+
.map(|p| p.category())
312+
.ok_or_else(|| ProcessingError::UnknownShaderProperty(name.clone()))?;
313+
if !matches!(
314+
category,
315+
ParameterCategory::Texture | ParameterCategory::StorageTexture
316+
) {
317+
return Err(ProcessingError::InvalidArgument(format!(
318+
"property `{name}` expects {category:?}, got Texture",
319+
)));
320+
}
300321
let image = p_images
301-
.get(*img_entity)
322+
.get(img_entity)
302323
.map_err(|_| ProcessingError::ImageNotFound)?;
303324
compute.shader.insert(&name, image.handle.clone());
304325
Ok(())
305326
}
306-
(ShaderValue::Buffer(_), cat) | (ShaderValue::Texture(_), cat) => {
307-
Err(ProcessingError::InvalidArgument(format!(
308-
"property `{name}` expects {cat:?}, got {value:?}",
309-
)))
310-
}
311-
(_, ParameterCategory::Uniform) => {
312-
let reflect_value: Box<dyn PartialReflect> = shader_value_to_reflect(&value)?;
327+
v => {
328+
let reflect_value: Box<dyn PartialReflect> = shader_value_to_reflect(&v)?;
313329
apply_reflect_field(&mut compute.shader, &name, &*reflect_value)
314330
}
315-
(_, cat) => Err(ProcessingError::InvalidArgument(format!(
316-
"property `{name}` expects {cat:?}, got non-resource value"
317-
))),
318331
}
319332
}
320333

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Per-particle color material for [`Field`] rasterization.
2+
//
3+
// Reads `mesh.tag` (written by the pack pass as the per-particle slot index)
4+
// and looks up a per-particle color from a storage buffer. Unlit — outputs the
5+
// looked-up color directly.
6+
7+
#import bevy_pbr::{
8+
mesh_functions,
9+
view_transformations::position_world_to_clip
10+
}
11+
12+
@group(#{MATERIAL_BIND_GROUP}) @binding(0)
13+
var<storage, read> particle_colors: array<vec4<f32>>;
14+
15+
struct Vertex {
16+
@builtin(instance_index) instance_index: u32,
17+
@location(0) position: vec3<f32>,
18+
};
19+
20+
struct VertexOutput {
21+
@builtin(position) clip_position: vec4<f32>,
22+
@location(0) color: vec4<f32>,
23+
};
24+
25+
@vertex
26+
fn vertex(vertex: Vertex) -> VertexOutput {
27+
var out: VertexOutput;
28+
let tag = mesh_functions::get_tag(vertex.instance_index);
29+
let world_from_local = mesh_functions::get_world_from_local(vertex.instance_index);
30+
let world_position = mesh_functions::mesh_position_local_to_world(
31+
world_from_local,
32+
vec4<f32>(vertex.position, 1.0),
33+
);
34+
out.clip_position = position_world_to_clip(world_position.xyz);
35+
out.color = particle_colors[tag];
36+
return out;
37+
}
38+
39+
@fragment
40+
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
41+
return in.color;
42+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// PBR per-particle color material for [`Field`] rasterization.
2+
//
3+
// Composes with `StandardMaterial` via `ExtendedMaterial`. The base material
4+
// supplies all PBR properties (roughness, metallic, etc.); we modulate the
5+
// resulting `base_color` by the per-particle color looked up from a storage
6+
// buffer indexed by `mesh.tag` (the per-instance slot index written by the
7+
// pack pass).
8+
9+
#import bevy_pbr::{
10+
pbr_fragment::pbr_input_from_standard_material,
11+
pbr_functions::alpha_discard,
12+
mesh_functions,
13+
}
14+
15+
#ifdef PREPASS_PIPELINE
16+
#import bevy_pbr::{
17+
prepass_io::{VertexOutput, FragmentOutput},
18+
pbr_deferred_functions::deferred_output,
19+
}
20+
#else
21+
#import bevy_pbr::{
22+
forward_io::{VertexOutput, FragmentOutput},
23+
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
24+
}
25+
#endif
26+
27+
@group(#{MATERIAL_BIND_GROUP}) @binding(100)
28+
var<storage, read> particle_colors: array<vec4<f32>>;
29+
30+
@fragment
31+
fn fragment(
32+
in: VertexOutput,
33+
@builtin(front_facing) is_front: bool,
34+
) -> FragmentOutput {
35+
var pbr_input = pbr_input_from_standard_material(in, is_front);
36+
37+
let tag = mesh_functions::get_tag(in.instance_index);
38+
pbr_input.material.base_color = pbr_input.material.base_color * particle_colors[tag];
39+
40+
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
41+
42+
#ifdef PREPASS_PIPELINE
43+
let out = deferred_output(in, pbr_input);
44+
#else
45+
var out: FragmentOutput;
46+
out.color = apply_pbr_lighting(pbr_input);
47+
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
48+
#endif
49+
50+
return out;
51+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! `FieldColorMaterial` — unlit material that reads a per-particle color from a
2+
//! storage buffer indexed by the per-instance tag (set to slot index by the pack pass).
3+
4+
use std::ops::Deref;
5+
6+
use bevy::asset::embedded_asset;
7+
use bevy::pbr::{ExtendedMaterial, MaterialExtension, MaterialPlugin};
8+
use bevy::prelude::*;
9+
use bevy::render::{render_resource::AsBindGroup, storage::ShaderBuffer};
10+
use bevy::shader::ShaderRef;
11+
12+
use crate::render::material::UntypedMaterial;
13+
14+
pub struct FieldColorMaterialPlugin;
15+
16+
impl Plugin for FieldColorMaterialPlugin {
17+
fn build(&self, app: &mut App) {
18+
embedded_asset!(app, "field_color.wgsl");
19+
embedded_asset!(app, "field_pbr.wgsl");
20+
app.add_plugins(MaterialPlugin::<FieldColorMaterial>::default());
21+
app.add_plugins(MaterialPlugin::<FieldPbrMaterial>::default());
22+
}
23+
}
24+
25+
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
26+
pub struct FieldColorMaterial {
27+
#[storage(0, read_only)]
28+
pub colors: Handle<ShaderBuffer>,
29+
}
30+
31+
impl Material for FieldColorMaterial {
32+
fn vertex_shader() -> ShaderRef {
33+
"embedded://processing_render/field/field_color.wgsl".into()
34+
}
35+
36+
fn fragment_shader() -> ShaderRef {
37+
"embedded://processing_render/field/field_color.wgsl".into()
38+
}
39+
}
40+
41+
#[derive(Component, Clone)]
42+
pub struct FieldColorMaterial3d(pub Handle<FieldColorMaterial>);
43+
44+
impl bevy::asset::AsAssetId for FieldColorMaterial3d {
45+
type Asset = FieldColorMaterial;
46+
fn as_asset_id(&self) -> AssetId<Self::Asset> {
47+
self.0.id()
48+
}
49+
}
50+
51+
/// Sibling to `add_processing_materials` / `add_custom_materials`. Promotes
52+
/// `UntypedMaterial(handle)` entities whose handle is a [`FieldColorMaterial`]
53+
/// to having the typed `MeshMaterial3d<FieldColorMaterial>` component required
54+
/// by the render pipeline.
55+
pub fn add_field_color_materials(
56+
mut commands: Commands,
57+
meshes: Query<(Entity, &UntypedMaterial)>,
58+
) {
59+
for (entity, handle) in meshes.iter() {
60+
let handle = handle.deref().clone();
61+
if let Ok(handle) = handle.try_typed::<FieldColorMaterial>() {
62+
commands
63+
.entity(entity)
64+
.insert(MeshMaterial3d::<FieldColorMaterial>(handle));
65+
}
66+
}
67+
}
68+
69+
/// PBR-lit per-particle color material. Wraps `StandardMaterial` via
70+
/// `ExtendedMaterial` so the user gets standard PBR lighting behavior on top
71+
/// of per-particle albedo from a storage buffer.
72+
pub type FieldPbrMaterial = ExtendedMaterial<StandardMaterial, FieldPbrExtension>;
73+
74+
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
75+
pub struct FieldPbrExtension {
76+
#[storage(100, read_only)]
77+
pub colors: Handle<ShaderBuffer>,
78+
}
79+
80+
impl MaterialExtension for FieldPbrExtension {
81+
fn fragment_shader() -> ShaderRef {
82+
"embedded://processing_render/field/field_pbr.wgsl".into()
83+
}
84+
85+
fn deferred_fragment_shader() -> ShaderRef {
86+
"embedded://processing_render/field/field_pbr.wgsl".into()
87+
}
88+
}
89+
90+
pub fn add_field_pbr_materials(
91+
mut commands: Commands,
92+
meshes: Query<(Entity, &UntypedMaterial)>,
93+
) {
94+
for (entity, handle) in meshes.iter() {
95+
let handle = handle.deref().clone();
96+
if let Ok(handle) = handle.try_typed::<FieldPbrMaterial>() {
97+
commands
98+
.entity(entity)
99+
.insert(MeshMaterial3d::<FieldPbrMaterial>(handle));
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)