Skip to content

Commit ff3dbe3

Browse files
stainluclaude
andcommitted
Add water shader, GPU particles in MOBA, and Metal mesh shader path
Three rendering features: 1. Water shader: multi-octave sine wave displacement, fresnel transparency, specular highlights. New water.wgsl shader, water_pipeline in Renderer with alpha blend + no depth write. WaterChunk marker component tags water terrain entities. Added elapsed_time to SceneUniforms across all shader variants. 2. GPU particles in MOBA: tower flame emitters (team-tinted orange/red) and hero dust particles (emit when moving, stop when stationary). Added set_emit_rate() to GpuParticleSystem for runtime emission control. 3. Metal mesh shaders: cfg-gated enable_mesh_shaders() on Renderer<MetalDevice> with MSL mesh+object+fragment shaders. Outputs PBR-compatible geometry via cooperative threadgroups (64 triangles per chunk). Compiles and validates via create_mesh_render_pipeline(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 69b57de commit ff3dbe3

23 files changed

Lines changed: 617 additions & 7 deletions

crates/euca-editor/src/gizmo.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub fn gizmo_draw_commands(
139139
shaft_center,
140140
),
141141
aabb: None,
142+
is_water: false,
142143
});
143144

144145
// Arrow tip
@@ -152,6 +153,7 @@ pub fn gizmo_draw_commands(
152153
tip_center,
153154
),
154155
aabb: None,
156+
is_water: false,
155157
});
156158
}
157159
cmds
@@ -196,6 +198,7 @@ pub fn gizmo_draw_commands(
196198
pos,
197199
),
198200
aabb: None,
201+
is_water: false,
199202
});
200203
}
201204
}
@@ -229,6 +232,7 @@ pub fn gizmo_draw_commands(
229232
shaft_center,
230233
),
231234
aabb: None,
235+
is_water: false,
232236
});
233237

234238
// Cube endpoint (instead of arrow tip)
@@ -242,6 +246,7 @@ pub fn gizmo_draw_commands(
242246
cube_center,
243247
),
244248
aabb: None,
249+
is_water: false,
245250
});
246251
}
247252
cmds

crates/euca-game/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ fn collect_draw_commands(world: &World) -> Vec<DrawCommand> {
409409
material: mat.handle,
410410
model_matrix,
411411
aabb: None,
412+
is_water: false,
412413
}
413414
})
414415
.collect()

crates/euca-render/benches/render_bench.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fn extract_draw_commands(world: &World) -> Vec<DrawCommand> {
5858
material: mat.handle,
5959
model_matrix: gt.0.to_matrix(),
6060
aabb: None,
61+
is_water: false,
6162
})
6263
.collect()
6364
}

crates/euca-render/shaders/metal/pbr.metal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ struct SceneUniforms {
4949
float4 probe_enabled;
5050
float4 shadow_params;
5151
float4 ibl_params;
52+
float4 elapsed_time;
5253
};
5354

5455
struct MaterialUniforms {

crates/euca-render/shaders/pbr.wgsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct SceneUniforms {
5050
probe_enabled: vec4<f32>,
5151
shadow_params: vec4<f32>,
5252
ibl_params: vec4<f32>,
53+
elapsed_time: vec4<f32>,
5354
}
5455

5556
struct MaterialUniforms {

crates/euca-render/shaders/pbr_bindless.wgsl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ struct SceneUniforms {
5050
probe_enabled: vec4<f32>,
5151
shadow_params: vec4<f32>,
5252
ibl_params: vec4<f32>,
53+
elapsed_time: vec4<f32>,
5354
}
5455

5556
// Bindless material: uniform data + texture indices into the binding array.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Animated water surface shader.
2+
// Multi-octave sine wave vertex displacement, fresnel transparency, specular highlights.
3+
//
4+
// Uses the same instance (group 0) and scene (group 1) bind groups as the PBR
5+
// shader — no per-material group needed since water properties are baked in.
6+
7+
diagnostic(off, derivative_uniformity);
8+
9+
// ---------------------------------------------------------------------------
10+
// Shared structures (must match pbr.wgsl layout exactly)
11+
// ---------------------------------------------------------------------------
12+
13+
struct InstanceData {
14+
model: mat4x4<f32>,
15+
normal_matrix: mat4x4<f32>,
16+
material_id: u32,
17+
_pad0: u32,
18+
_pad1: u32,
19+
_pad2: u32,
20+
}
21+
22+
struct PointLightData {
23+
position: vec4<f32>,
24+
color: vec4<f32>,
25+
}
26+
27+
struct SpotLightData {
28+
position: vec4<f32>,
29+
direction: vec4<f32>,
30+
color: vec4<f32>,
31+
cone: vec4<f32>,
32+
}
33+
34+
struct SceneUniforms {
35+
camera_pos: vec4<f32>,
36+
light_direction: vec4<f32>,
37+
light_color: vec4<f32>,
38+
ambient_color: vec4<f32>,
39+
camera_vp: mat4x4<f32>,
40+
light_vp: mat4x4<f32>,
41+
inv_vp: mat4x4<f32>,
42+
cascade_vps: array<mat4x4<f32>, 3>,
43+
cascade_splits: vec4<f32>,
44+
point_lights: array<PointLightData, 4>,
45+
spot_lights: array<SpotLightData, 2>,
46+
num_point_lights: vec4<f32>,
47+
num_spot_lights: vec4<f32>,
48+
probe_sh: array<vec4<f32>, 9>,
49+
probe_enabled: vec4<f32>,
50+
shadow_params: vec4<f32>,
51+
ibl_params: vec4<f32>,
52+
elapsed_time: vec4<f32>,
53+
}
54+
55+
// ---------------------------------------------------------------------------
56+
// Bindings
57+
// ---------------------------------------------------------------------------
58+
59+
@group(0) @binding(0) var<storage, read> instances: array<InstanceData>;
60+
@group(1) @binding(0) var<uniform> scene: SceneUniforms;
61+
62+
// ---------------------------------------------------------------------------
63+
// Constants
64+
// ---------------------------------------------------------------------------
65+
66+
const WATER_COLOR_DEEP: vec3<f32> = vec3<f32>(0.02, 0.12, 0.22);
67+
const WATER_COLOR_SHALLOW: vec3<f32> = vec3<f32>(0.10, 0.35, 0.45);
68+
const SPECULAR_COLOR: vec3<f32> = vec3<f32>(1.0, 0.95, 0.85);
69+
const WAVE_HEIGHT: f32 = 0.15;
70+
const BASE_ALPHA: f32 = 0.55;
71+
72+
// ---------------------------------------------------------------------------
73+
// Vertex stage
74+
// ---------------------------------------------------------------------------
75+
76+
struct VertexInput {
77+
@location(0) position: vec3<f32>,
78+
@location(1) normal: vec3<f32>,
79+
@location(2) tangent: vec3<f32>,
80+
@location(3) uv: vec2<f32>,
81+
}
82+
83+
struct VertexOutput {
84+
@builtin(position) clip_position: vec4<f32>,
85+
@location(0) world_pos: vec3<f32>,
86+
@location(1) world_normal: vec3<f32>,
87+
@location(2) uv: vec2<f32>,
88+
}
89+
90+
// Multi-octave wave displacement. Each octave has a different direction,
91+
// frequency, and phase speed so the surface looks organic rather than tiled.
92+
fn wave_height(xz: vec2<f32>, t: f32) -> f32 {
93+
var h: f32 = 0.0;
94+
// Octave 1: broad swell
95+
h += sin(xz.x * 1.2 + xz.y * 0.8 + t * 1.4) * 0.45;
96+
// Octave 2: cross chop
97+
h += sin(xz.x * 2.5 - xz.y * 1.7 + t * 2.1) * 0.25;
98+
// Octave 3: fine ripple
99+
h += sin(xz.x * 5.0 + xz.y * 4.3 + t * 3.6) * 0.12;
100+
// Octave 4: micro detail
101+
h += sin(xz.x * 8.7 - xz.y * 7.1 + t * 5.0) * 0.06;
102+
return h * WAVE_HEIGHT;
103+
}
104+
105+
// Analytical normal from the partial derivatives of the wave function.
106+
fn wave_normal(xz: vec2<f32>, t: f32) -> vec3<f32> {
107+
let eps = 0.05;
108+
let hc = wave_height(xz, t);
109+
let hx = wave_height(xz + vec2<f32>(eps, 0.0), t);
110+
let hz = wave_height(xz + vec2<f32>(0.0, eps), t);
111+
let dx = (hx - hc) / eps;
112+
let dz = (hz - hc) / eps;
113+
return normalize(vec3<f32>(-dx, 1.0, -dz));
114+
}
115+
116+
@vertex
117+
fn vs_main(in: VertexInput, @builtin(instance_index) iid: u32) -> VertexOutput {
118+
let model = instances[iid].model;
119+
let t = scene.elapsed_time.x;
120+
121+
var world_pos = (model * vec4<f32>(in.position, 1.0)).xyz;
122+
123+
// Displace Y by the wave function
124+
world_pos.y += wave_height(world_pos.xz, t);
125+
126+
var out: VertexOutput;
127+
out.clip_position = scene.camera_vp * vec4<f32>(world_pos, 1.0);
128+
out.world_pos = world_pos;
129+
out.world_normal = wave_normal(world_pos.xz, t);
130+
out.uv = in.uv;
131+
return out;
132+
}
133+
134+
// ---------------------------------------------------------------------------
135+
// Fragment stage
136+
// ---------------------------------------------------------------------------
137+
138+
@fragment
139+
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
140+
let N = normalize(in.world_normal);
141+
let V = normalize(scene.camera_pos.xyz - in.world_pos);
142+
let L = normalize(-scene.light_direction.xyz);
143+
144+
// Fresnel: more reflective at glancing angles
145+
let NdotV = max(dot(N, V), 0.0);
146+
let fresnel = pow(1.0 - NdotV, 4.0) * 0.8 + 0.2;
147+
148+
// Blend between deep and shallow water based on view angle
149+
let water_color = mix(WATER_COLOR_DEEP, WATER_COLOR_SHALLOW, NdotV);
150+
151+
// Ambient contribution
152+
let ambient = water_color * scene.ambient_color.xyz * scene.ambient_color.w;
153+
154+
// Diffuse (wrap lighting for softer underwater look)
155+
let NdotL = max(dot(N, L), 0.0);
156+
let diffuse = water_color * scene.light_color.xyz * scene.light_color.w * NdotL * 0.6;
157+
158+
// Specular (Blinn-Phong, tight highlight for water)
159+
let H = normalize(L + V);
160+
let NdotH = max(dot(N, H), 0.0);
161+
let spec = pow(NdotH, 128.0) * fresnel;
162+
let specular = SPECULAR_COLOR * scene.light_color.xyz * scene.light_color.w * spec;
163+
164+
let color = ambient + diffuse + specular;
165+
166+
// Alpha: base transparency boosted at glancing angles (fresnel)
167+
let alpha = mix(BASE_ALPHA, 0.85, fresnel);
168+
169+
return vec4<f32>(color, alpha);
170+
}

crates/euca-render/src/extract.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ impl RenderExtractor {
122122
material: mat_ref.handle,
123123
model_matrix,
124124
aabb: None,
125+
is_water: false,
125126
};
126127
self.entities[slot] = Some(RenderEntity {
127128
mesh: mesh_renderer.mesh,
@@ -136,6 +137,7 @@ impl RenderExtractor {
136137
material: mat_ref.handle,
137138
model_matrix,
138139
aabb: None,
140+
is_water: false,
139141
};
140142
self.entities[free] = Some(RenderEntity {
141143
mesh: mesh_renderer.mesh,
@@ -149,6 +151,7 @@ impl RenderExtractor {
149151
material: mat_ref.handle,
150152
model_matrix,
151153
aabb: None,
154+
is_water: false,
152155
});
153156
self.entities.push(Some(RenderEntity {
154157
mesh: mesh_renderer.mesh,

crates/euca-render/src/gpu_particles.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,11 @@ impl<D: RenderDevice> GpuParticleSystem<D> {
408408
self.position = pos;
409409
}
410410

411+
/// Change the emission rate at runtime (particles per second).
412+
pub fn set_emit_rate(&mut self, rate: f32) {
413+
self.config.emit_rate = rate;
414+
}
415+
411416
/// Run the compute emit + update passes.
412417
pub fn update(&mut self, device: &D, encoder: &mut D::CommandEncoder, dt: f32) {
413418
self.elapsed += dt;

crates/euca-render/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub use motion_blur::{MotionBlurPass, MotionBlurSettings};
150150
pub use occlusion::{HzbPyramid, OcclusionCuller, OcclusionResult};
151151
pub use plugin::RenderPlugin;
152152
pub use post_process::{PostProcessSettings, PostProcessStack};
153-
pub use renderer::{DrawCommand, RenderQuality, Renderer};
153+
pub use renderer::{DrawCommand, RenderQuality, Renderer, WaterChunk};
154154
pub use ssgi::{SsgiExecuteParams, SsgiPass, SsgiSettings, step_size as ssgi_step_size};
155155
pub use ssr::{
156156
SsrExecuteParams, SsrPass, SsrSettings, compute_step_count, passes_roughness_filter,

0 commit comments

Comments
 (0)