|
1 | 1 | # Particles |
2 | 2 |
|
3 | | -A `Particles` is a GPU-resident container of named attribute buffers, drawn by |
4 | | -instancing a geometry once per element. The libprocessing analogue of a Houdini |
5 | | -point cloud. |
6 | | - |
7 | | -## Pieces |
8 | | - |
9 | | -- **`compute::Buffer`** (`crates/processing_render/src/compute.rs`) — typed GPU |
10 | | - storage with CPU-side write, GPU readback, and a Python wrapper that tracks |
11 | | - element type. Backs every Particles attribute buffer. |
12 | | -- **`Attribute`** (`crates/processing_render/src/geometry/attribute.rs`) — |
13 | | - named typed attribute identity (`AttributeFormat::{Float, Float2, Float3, |
14 | | - Float4}`), shared between Geometries and Particles. Builtins: `position`, |
15 | | - `normal`, `color`, `uv`, plus the particles-only `rotation` (Float4 quat), |
16 | | - `scale` (Float3), `dead` (Float, 0=alive). |
17 | | -- **Upstream `processing/bevy`** commit `ee443e51` adds `GpuBatchedMesh3d` and |
18 | | - `GpuInstanceBatchReservations` — a fixed-capacity batch where a compute pass |
19 | | - writes per-instance transforms into the upstream input buffer before |
20 | | - `early_gpu_preprocess` consumes them. |
21 | | - |
22 | | -## Construction |
23 | | - |
24 | | -Empty: |
25 | | - |
26 | | -```rust |
27 | | -let velocity = geometry_attribute_create("velocity", AttributeFormat::Float3)?; |
28 | | -let p = particles_create(10_000, vec![geometry_attribute_position(), velocity])?; |
29 | | -``` |
30 | | - |
31 | | -One zero-initialized buffer per requested attribute, sized |
32 | | -`capacity * attr.format.byte_size()`. |
33 | | - |
34 | | -Mesh-seeded: |
35 | | - |
36 | | -```rust |
37 | | -let source = geometry_sphere(5.0, 32, 24)?; |
38 | | -let p = particles_create_from_geometry( |
39 | | - source, |
40 | | - vec![position_attr, uv_attr, color_attr], |
41 | | -)?; |
42 | | -``` |
43 | | - |
44 | | -Capacity = mesh vertex count. Builtins seed from the matching mesh attribute |
45 | | -(`position` ← `ATTRIBUTE_POSITION`, `normal` ← `ATTRIBUTE_NORMAL`, `color` ← |
46 | | -`ATTRIBUTE_COLOR`, `uv` ← `ATTRIBUTE_UV_0`); particles-only builtins and custom |
47 | | -attributes start at zero. |
48 | | - |
49 | | -## Apply |
50 | | - |
51 | | -```rust |
52 | | -let spin = compute_create(shader_create(SPIN_WGSL)?)?; |
53 | | -compute_set(spin, "dt", ShaderValue::Float(0.016))?; |
54 | | -particles_apply(p, spin)?; |
55 | | -``` |
56 | | - |
57 | | -`particles_apply` binds each attribute buffer by name; bindings the shader |
58 | | -doesn't declare are skipped. Workgroup size is fixed at 64. |
59 | | - |
60 | | -Built-in kernels: `particles_kernel_noise()` (uniforms `scale`, `strength`, |
61 | | -`time`), `particles_kernel_transform()` (`translate`, `rotation_axis`, |
62 | | -`rotation_angle`, `scale`, with identity defaults seeded so unset uniforms are |
63 | | -no-ops). |
64 | | - |
65 | | -## Pack pass |
66 | | - |
67 | | -Bridges Particles attribute buffers into the per-instance slots reserved by |
68 | | -`GpuBatchedMesh3d`. Runs as render-schedule systems: |
69 | | - |
70 | | -- `extract_particles_draws` (ExtractSchedule) — copies Particles + buffer |
71 | | - handles into the render world keyed by `ParticlesDraw` markers. |
72 | | -- `prepare_pack_bind_groups` (RenderSystems::PrepareBindGroups) — looks up or |
73 | | - builds the pack pipeline for the specialization key + bind group. |
74 | | -- `dispatch_pack` (Core3d, before `early_gpu_preprocess`) — dispatches. |
75 | | - |
76 | | -The pack shader (`particles/pack.wgsl`) is specialized per |
77 | | -`(HAS_ROTATION, HAS_SCALE, HAS_DEAD)`. For each slot it writes: |
78 | | - |
79 | | -- `mesh_input_buffer[base+i].world_from_local` — `mat3x4` from rotation × scale |
80 | | - + position translation. |
81 | | -- `mesh_input_buffer[base+i].tag = i` — slot index, available via |
82 | | - `mesh_functions::get_tag(instance_index)`. |
83 | | -- `MeshCullingData[base+i].dead` — from the `dead` buffer if present, else 0. |
84 | | - |
85 | | -## Materials |
86 | | - |
87 | | -`ParticlesMaterial = ExtendedMaterial<StandardMaterial, ParticlesExtension>` |
88 | | -binds a `colors: Handle<ShaderBuffer>` and reads `particle_colors[mesh.tag]`. |
89 | | -Lit vs unlit is the `unlit` flag on the base `StandardMaterial`; |
90 | | -`apply_pbr_lighting` short-circuits when set. |
91 | | - |
92 | | -Immediate-mode: |
93 | | - |
94 | | -```rust |
95 | | -graphics_record_command(g, DrawCommand::FillBuffer(color_buffer_entity))?; |
96 | | -graphics_record_command(g, DrawCommand::Particles { particles, geometry: shape })?; |
97 | | -``` |
98 | | - |
99 | | -`fill(buffer)` sets the ambient albedo source; the next |
100 | | -`DrawCommand::Particles` allocates a `ParticlesMaterial` carrying that buffer. |
101 | | - |
102 | | -Explicit: |
103 | | - |
104 | | -```rust |
105 | | -let mat = material_create_pbr()?; |
106 | | -material_set_albedo_buffer(mat, color_buffer_entity)?; |
107 | | -material_set(mat, "roughness", ShaderValue::Float(0.4))?; |
108 | | -``` |
109 | | - |
110 | | -`material_set_albedo_buffer` / `material_set_albedo_color` swap the backing |
111 | | -asset between plain PBR and `ParticlesMaterial` while preserving every other |
112 | | -`StandardMaterial` field. |
113 | | - |
114 | | -Custom shaders (per-particle UV, per-particle scalars, anything beyond color) |
115 | | -require a `CustomMaterial` that reads `mesh.tag` and indexes its own buffer. |
116 | | - |
117 | | -## Emit |
118 | | - |
119 | | -CPU-driven: |
120 | | - |
121 | | -```rust |
122 | | -particles_emit( |
123 | | - p, |
124 | | - n, |
125 | | - vec![ |
126 | | - (position_attr, position_bytes), // n * 12 bytes |
127 | | - (color_attr, color_bytes), // n * 16 bytes |
128 | | - (dead_attr, vec![0u8; n * 4]), // alive |
129 | | - ], |
130 | | -)?; |
131 | | -``` |
132 | | - |
133 | | -Writes to `[head, head+n) mod capacity` and advances `emit_head`. Two writes |
134 | | -when wrapping. No GPU allocator, no compaction. Capacity is a visible contract: |
135 | | -`>= peak_emission_rate × longest_lifespan`. |
136 | | - |
137 | | -GPU-driven: |
138 | | - |
139 | | -```rust |
140 | | -particles_emit_gpu(p, n, spawn_kernel)?; |
141 | | -``` |
142 | | - |
143 | | -Auto-binds attribute buffers and `emit_range: vec4<f32> = (base_slot, n, |
144 | | -capacity, 0)`. The kernel derives its target slot from `emit_range`. |
145 | | - |
146 | | -No auto-defaults — if the field has a `dead` attribute, the caller must |
147 | | -include it (typically `n` zero-floats) or new slots inherit the previous |
148 | | -occupant's death. |
149 | | - |
150 | | -## Lifecycle |
151 | | - |
152 | | -`dead` is a builtin Float attribute (0=alive, non-zero=dead). When registered, |
153 | | -the pack pass writes it into `MeshCullingData::dead`; non-zero slots are |
154 | | -skipped in preprocessing. |
155 | | - |
156 | | -Aging is user-managed via an apply kernel that increments age and flips |
157 | | -`dead` when age exceeds ttl. See `particles_lifecycle.rs`. Seed `dead = 1.0` |
158 | | -for unemitted ring slots so they don't render before being filled. |
159 | | - |
160 | | -## Examples |
161 | | - |
162 | | -- `particles_basic` — sphere-mesh-seeded particle cloud, PBR per-particle color. |
163 | | -- `particles_animated` — 10×10×10 grid rotating around Y via custom apply. |
164 | | -- `particles_oriented` — per-particle quaternion + scale. |
165 | | -- `particles_colored` / `particles_colored_pbr` — explicit material setup. |
166 | | -- `particles_emit` — continuous CPU ring-buffer emission. |
167 | | -- `particles_emit_gpu` — fountain spawned by a compute kernel. |
168 | | -- `particles_lifecycle` — emit + age + shrink-on-death. |
169 | | -- `particles_from_mesh` — sphere mesh as position source. |
170 | | -- `particles_noise` — built-in noise kernel jittering positions. |
171 | | -- `particles_stress` — 1M cubes on a grid, R/G/B lights, transform spin. |
| 3 | +`Particles` are a collection of attribute buffers that can be used in order to sequence compute shaders. They are |
| 4 | +isomorphic to `Mesh` in the sense that they contain attributes and sets of data. In this way, you can think of a |
| 5 | +`Mesh` as the CPU representation of a `Particles` object, and the `Particles` object as the GPU representation of a |
| 6 | +`Mesh`. This allows convenient initialization of particle simulations from existing meshes, or using a mesh as a |
| 7 | +constraint for a particle simulation, like a volume or a surface. |
| 8 | + |
| 9 | +Another way to consider particles would be as the compute equivalent of `Graphics`. Where the `Grpahics` object |
| 10 | +allows you to issue high level rasterization commands, the `Particles` object allows you to issue high level compute |
| 11 | +commands. In this way, you can think of a `Particles` object as a compute shader that is executed on the GPU, and the |
| 12 | +attributes as the inputs and outputs of the compute shader. In practice, a compute shader may also require additional |
| 13 | +data, such as textures or bound vertex buffers, but the `Particles` object provides a high level abstraction for |
| 14 | +sequencing compute shaders and managing their inputs and outputs. |
0 commit comments