Skip to content

Commit 4edd9cf

Browse files
committed
.
1 parent f2ade7e commit 4edd9cf

20 files changed

Lines changed: 253 additions & 143 deletions

crates/processing_pyo3/examples/field_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def setup():
3131
color_buf.write(colors)
3232

3333
particle = Geometry.sphere(0.18, 10, 8)
34-
mat = Material.field_pbr(color_buf)
34+
mat = Material.pbr(albedo=color_buf)
3535

3636

3737
def draw():

crates/processing_pyo3/examples/field_emit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def setup():
2727
pos_buf.write([1.0e6] * (capacity * 3))
2828

2929
color_buf = field_obj.buffer(Attribute.color())
30-
mat = Material.field_color(color_buf)
30+
mat = Material.unlit(albedo=color_buf)
3131

3232

3333
def draw():

crates/processing_pyo3/examples/field_emit_gpu.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def setup():
153153
dead_buf.write([1.0] * CAPACITY)
154154

155155
color_buf = field_obj.buffer(Attribute.color())
156-
mat = Material.field_pbr(color_buf)
156+
mat = Material.pbr(albedo=color_buf)
157157

158158
spawn = Compute(Shader(SPAWN_SHADER))
159159
motion = Compute(Shader(MOTION_SHADER))

crates/processing_pyo3/examples/field_from_mesh.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def setup():
3131
color_buf.write(colors)
3232

3333
particle = Geometry.sphere(0.18, 10, 8)
34-
mat = Material.field_pbr(color_buf)
34+
mat = Material.pbr(albedo=color_buf)
3535

3636

3737
def draw():

crates/processing_pyo3/examples/field_lifecycle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def setup():
7979
dead_buf.write([1.0] * capacity)
8080

8181
color_buf = field_obj.buffer(color_attr)
82-
mat = Material.field_color(color_buf)
82+
mat = Material.unlit(albedo=color_buf)
8383
aging = Compute(Shader(AGING_SHADER))
8484

8585

crates/processing_pyo3/examples/field_noise.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def setup():
3232
color_buf.write(colors)
3333

3434
particle = Geometry.sphere(0.18, 10, 8)
35-
mat = Material.field_pbr(color_buf)
35+
mat = Material.pbr(albedo=color_buf)
3636
noise = kernel_noise()
3737

3838

crates/processing_pyo3/examples/field_stress.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def setup():
5555
colors.append([c.r, c.g, c.b, 1.0])
5656
color_buf.write(colors)
5757

58-
mat = Material.field_pbr(color_buf)
58+
mat = Material.pbr(albedo=color_buf)
5959

6060
cube = Geometry.box(0.35, 0.35, 0.35)
6161

crates/processing_pyo3/src/material.rs

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use processing::prelude::*;
33
use pyo3::types::PyDict;
44
use pyo3::{exceptions::PyRuntimeError, prelude::*};
55

6+
use crate::color::PyColor;
67
use crate::compute::Buffer;
78
use crate::math::{PyVec2, PyVec3, PyVec4};
89
use crate::shader::Shader;
@@ -52,8 +53,51 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult<shader_va
5253
)))
5354
}
5455

56+
/// Apply an `albedo` value to a material, dispatching by Python type. The
57+
/// material's backing asset is swapped between plain-PBR and field-buffer
58+
/// variants as needed; all other `StandardMaterial` state survives the swap.
59+
fn apply_albedo(entity: Entity, value: &Bound<'_, PyAny>) -> PyResult<()> {
60+
if let Ok(buf) = value.extract::<PyRef<Buffer>>() {
61+
return material_set_albedo_buffer(entity, buf.entity)
62+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")));
63+
}
64+
if let Ok(c) = value.extract::<PyRef<PyColor>>() {
65+
let srgba: bevy::color::Srgba = c.0.into();
66+
return material_set_albedo_color(entity, [srgba.red, srgba.green, srgba.blue, srgba.alpha])
67+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")));
68+
}
69+
if let Ok(rgba) = value.extract::<[f32; 4]>() {
70+
return material_set_albedo_color(entity, rgba)
71+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")));
72+
}
73+
if let Ok(rgb) = value.extract::<[f32; 3]>() {
74+
return material_set_albedo_color(entity, [rgb[0], rgb[1], rgb[2], 1.0])
75+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")));
76+
}
77+
Err(PyRuntimeError::new_err(format!(
78+
"unsupported albedo type: {} (expected Color, Buffer, or [r,g,b,(a)])",
79+
value.get_type().name()?
80+
)))
81+
}
82+
83+
fn apply_kwargs(entity: Entity, kwargs: &Bound<'_, PyDict>) -> PyResult<()> {
84+
for (key, value) in kwargs.iter() {
85+
let name: String = key.extract()?;
86+
if name == "albedo" {
87+
apply_albedo(entity, &value)?;
88+
continue;
89+
}
90+
let v = py_to_shader_value(&value)?;
91+
material_set(entity, &name, v).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
92+
}
93+
Ok(())
94+
}
95+
5596
#[pymethods]
5697
impl Material {
98+
/// Construct a material. With no args, returns a default PBR. With a
99+
/// `shader` arg, returns a custom material. Any kwargs (`albedo=...`,
100+
/// `roughness=...`, etc.) are applied after construction.
57101
#[new]
58102
#[pyo3(signature = (shader=None, **kwargs))]
59103
pub fn new(shader: Option<&Shader>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
@@ -64,48 +108,48 @@ impl Material {
64108
material_create_pbr().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?
65109
};
66110

67-
let mat = Self { entity };
68111
if let Some(kwargs) = kwargs {
69-
for (key, value) in kwargs.iter() {
70-
let name: String = key.extract()?;
71-
let value = py_to_shader_value(&value)?;
72-
material_set(mat.entity, &name, value)
73-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
74-
}
112+
apply_kwargs(entity, kwargs)?;
75113
}
76-
Ok(mat)
114+
Ok(Self { entity })
77115
}
78116

117+
/// PBR-lit material. `albedo` accepts a `Color` (solid) or a `Buffer`
118+
/// (per-particle, indexed by per-instance tag — used with `Field`s).
119+
#[staticmethod]
79120
#[pyo3(signature = (**kwargs))]
80-
pub fn set(&self, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
81-
let Some(kwargs) = kwargs else {
82-
return Ok(());
83-
};
84-
for (key, value) in kwargs.iter() {
85-
let name: String = key.extract()?;
86-
let value = py_to_shader_value(&value)?;
87-
material_set(self.entity, &name, value)
88-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
121+
pub fn pbr(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
122+
let entity = material_create_pbr().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
123+
if let Some(kwargs) = kwargs {
124+
apply_kwargs(entity, kwargs)?;
89125
}
90-
Ok(())
126+
Ok(Self { entity })
91127
}
92128

93-
/// Unlit per-particle color material. Each particle samples its color from
94-
/// the given buffer indexed by per-instance tag.
129+
/// Unlit material — same shape as `pbr` but skips lighting calculations
130+
/// (the per-particle / solid color is the final output).
95131
#[staticmethod]
96-
pub fn field_color(buffer: &Buffer) -> PyResult<Self> {
97-
let entity = material_create_field_color(buffer.entity)
132+
#[pyo3(signature = (**kwargs))]
133+
pub fn unlit(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
134+
let entity = material_create_pbr().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
135+
material_set(entity, "unlit", shader_value::ShaderValue::Float(1.0))
98136
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
137+
if let Some(kwargs) = kwargs {
138+
apply_kwargs(entity, kwargs)?;
139+
}
99140
Ok(Self { entity })
100141
}
101142

102-
/// PBR-lit per-particle color material. Same tag-indexed lookup as
103-
/// `field_color`, but composed with `StandardMaterial` for proper lighting.
104-
#[staticmethod]
105-
pub fn field_pbr(buffer: &Buffer) -> PyResult<Self> {
106-
let entity = material_create_field_pbr(buffer.entity)
107-
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
108-
Ok(Self { entity })
143+
/// Patch one or more material properties. `albedo` is special-cased and
144+
/// may swap the backing asset type between solid-color and buffer-color
145+
/// variants — all other `StandardMaterial` state (roughness, metallic,
146+
/// emissive, alpha_mode, unlit, etc.) is preserved across the swap.
147+
#[pyo3(signature = (**kwargs))]
148+
pub fn set(&self, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
149+
let Some(kwargs) = kwargs else {
150+
return Ok(());
151+
};
152+
apply_kwargs(self.entity, kwargs)
109153
}
110154
}
111155

Lines changed: 26 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
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).
1+
//! `FieldMaterial` — `ExtendedMaterial<StandardMaterial, FieldExtension>` whose
2+
//! per-particle color comes from a storage buffer indexed by the per-instance
3+
//! tag (set to slot index by the pack pass).
4+
//!
5+
//! Lit vs unlit is just the `unlit` flag on the base `StandardMaterial`;
6+
//! `apply_pbr_lighting` short-circuits to `base_color * particle_colors[tag]`
7+
//! when `unlit = true`, so a single extension serves both cases.
38
49
use std::ops::Deref;
510

@@ -11,92 +16,50 @@ use bevy::shader::ShaderRef;
1116

1217
use crate::render::material::UntypedMaterial;
1318

14-
pub struct FieldColorMaterialPlugin;
19+
pub struct FieldMaterialPlugin;
1520

16-
impl Plugin for FieldColorMaterialPlugin {
21+
impl Plugin for FieldMaterialPlugin {
1722
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());
23+
embedded_asset!(app, "field.wgsl");
24+
app.add_plugins(MaterialPlugin::<FieldMaterial>::default());
2225
}
2326
}
2427

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>;
28+
/// PBR material extended with a per-particle color buffer. Set the base
29+
/// `StandardMaterial`'s `unlit` flag to switch between lit and unlit behavior;
30+
/// the rest of the material works identically either way.
31+
pub type FieldMaterial = ExtendedMaterial<StandardMaterial, FieldExtension>;
7332

7433
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
75-
pub struct FieldPbrExtension {
34+
pub struct FieldExtension {
7635
#[storage(100, read_only)]
7736
pub colors: Handle<ShaderBuffer>,
7837
}
7938

80-
impl MaterialExtension for FieldPbrExtension {
39+
impl MaterialExtension for FieldExtension {
8140
fn fragment_shader() -> ShaderRef {
82-
"embedded://processing_render/field/field_pbr.wgsl".into()
41+
"embedded://processing_render/field/field.wgsl".into()
8342
}
8443

8544
fn deferred_fragment_shader() -> ShaderRef {
86-
"embedded://processing_render/field/field_pbr.wgsl".into()
45+
"embedded://processing_render/field/field.wgsl".into()
8746
}
8847
}
8948

90-
pub fn add_field_pbr_materials(
49+
/// Sibling of `add_processing_materials` / `add_custom_materials`. Promotes
50+
/// `UntypedMaterial(handle)` entities whose handle is a [`FieldMaterial`]
51+
/// to having the typed `MeshMaterial3d<FieldMaterial>` component required
52+
/// by the render pipeline.
53+
pub fn add_field_materials(
9154
mut commands: Commands,
9255
meshes: Query<(Entity, &UntypedMaterial)>,
9356
) {
9457
for (entity, handle) in meshes.iter() {
9558
let handle = handle.deref().clone();
96-
if let Ok(handle) = handle.try_typed::<FieldPbrMaterial>() {
59+
if let Ok(handle) = handle.try_typed::<FieldMaterial>() {
9760
commands
9861
.entity(entity)
99-
.insert(MeshMaterial3d::<FieldPbrMaterial>(handle));
62+
.insert(MeshMaterial3d::<FieldMaterial>(handle));
10063
}
10164
}
10265
}

crates/processing_render/src/field/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ impl Plugin for FieldPlugin {
3333
fn build(&self, app: &mut App) {
3434
app.add_plugins(GpuInstanceBatchPlugin);
3535
app.add_plugins(pack::FieldPackPlugin);
36-
app.add_plugins(material::FieldColorMaterialPlugin);
36+
app.add_plugins(material::FieldMaterialPlugin);
3737
app.add_plugins(kernels::FieldKernelsPlugin);
3838
}
3939
}

0 commit comments

Comments
 (0)