diff --git a/GUIDE.md b/GUIDE.md index 4e46d08..2cd14d6 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -146,6 +146,7 @@ let draw_commands: Vec = { material: mat.handle, model_matrix: gt.0.to_matrix(), aabb: None, + is_water: false, }).collect() }; renderer.draw(gpu, &camera, &light, &ambient, &draw_commands); diff --git a/crates/euca-editor/src/gizmo.rs b/crates/euca-editor/src/gizmo.rs index 6d3adc8..6726648 100644 --- a/crates/euca-editor/src/gizmo.rs +++ b/crates/euca-editor/src/gizmo.rs @@ -139,6 +139,7 @@ pub fn gizmo_draw_commands( shaft_center, ), aabb: None, + is_water: false, }); // Arrow tip @@ -152,6 +153,7 @@ pub fn gizmo_draw_commands( tip_center, ), aabb: None, + is_water: false, }); } cmds @@ -196,6 +198,7 @@ pub fn gizmo_draw_commands( pos, ), aabb: None, + is_water: false, }); } } @@ -229,6 +232,7 @@ pub fn gizmo_draw_commands( shaft_center, ), aabb: None, + is_water: false, }); // Cube endpoint (instead of arrow tip) @@ -242,6 +246,7 @@ pub fn gizmo_draw_commands( cube_center, ), aabb: None, + is_water: false, }); } cmds diff --git a/crates/euca-game/src/main.rs b/crates/euca-game/src/main.rs index 7cc3c38..3ea40e6 100644 --- a/crates/euca-game/src/main.rs +++ b/crates/euca-game/src/main.rs @@ -409,6 +409,7 @@ fn collect_draw_commands(world: &World) -> Vec { material: mat.handle, model_matrix, aabb: None, + is_water: false, } }) .collect() diff --git a/crates/euca-render/README.md b/crates/euca-render/README.md index 102ce5d..875fac0 100644 --- a/crates/euca-render/README.md +++ b/crates/euca-render/README.md @@ -40,7 +40,7 @@ use euca_render::*; let renderer = Renderer::new(&gpu_context); let mesh = renderer.upload_mesh(&gpu_context, &mesh_data); let material = renderer.upload_material(&gpu_context, &material_data); -let cmd = DrawCommand { mesh, material, model_matrix, aabb: None }; +let cmd = DrawCommand { mesh, material, model_matrix, aabb: None, is_water: false }; ``` ## License diff --git a/crates/euca-render/benches/render_bench.rs b/crates/euca-render/benches/render_bench.rs index 8e734df..95ef233 100644 --- a/crates/euca-render/benches/render_bench.rs +++ b/crates/euca-render/benches/render_bench.rs @@ -58,6 +58,7 @@ fn extract_draw_commands(world: &World) -> Vec { material: mat.handle, model_matrix: gt.0.to_matrix(), aabb: None, + is_water: false, }) .collect() } diff --git a/crates/euca-render/shaders/metal/pbr.metal b/crates/euca-render/shaders/metal/pbr.metal index 6c2fb01..2478267 100644 --- a/crates/euca-render/shaders/metal/pbr.metal +++ b/crates/euca-render/shaders/metal/pbr.metal @@ -49,6 +49,7 @@ struct SceneUniforms { float4 probe_enabled; float4 shadow_params; float4 ibl_params; + float4 elapsed_time; }; struct MaterialUniforms { diff --git a/crates/euca-render/shaders/pbr.wgsl b/crates/euca-render/shaders/pbr.wgsl index 90d2a4a..b486df3 100644 --- a/crates/euca-render/shaders/pbr.wgsl +++ b/crates/euca-render/shaders/pbr.wgsl @@ -50,6 +50,7 @@ struct SceneUniforms { probe_enabled: vec4, shadow_params: vec4, ibl_params: vec4, + elapsed_time: vec4, } struct MaterialUniforms { diff --git a/crates/euca-render/shaders/pbr_bindless.wgsl b/crates/euca-render/shaders/pbr_bindless.wgsl index 5bf8427..844de57 100644 --- a/crates/euca-render/shaders/pbr_bindless.wgsl +++ b/crates/euca-render/shaders/pbr_bindless.wgsl @@ -50,6 +50,7 @@ struct SceneUniforms { probe_enabled: vec4, shadow_params: vec4, ibl_params: vec4, + elapsed_time: vec4, } // Bindless material: uniform data + texture indices into the binding array. diff --git a/crates/euca-render/shaders/water.wgsl b/crates/euca-render/shaders/water.wgsl new file mode 100644 index 0000000..a60af40 --- /dev/null +++ b/crates/euca-render/shaders/water.wgsl @@ -0,0 +1,213 @@ +// Water surface shader with animated wave displacement and fresnel-based transparency. +// Uses the same bind group layout as PBR (group 0 = instance, group 1 = scene) +// but omits the material bind group — all water properties are hardcoded in the shader. + +diagnostic(off, derivative_uniformity); + +// --------------------------------------------------------------------------- +// Structures +// --------------------------------------------------------------------------- + +struct InstanceData { + model: mat4x4, + normal_matrix: mat4x4, + material_id: u32, + _pad0: u32, + _pad1: u32, + _pad2: u32, +} + +struct PointLightData { + position: vec4, + color: vec4, +} + +struct SpotLightData { + position: vec4, + direction: vec4, + color: vec4, + cone: vec4, +} + +struct SceneUniforms { + camera_pos: vec4, + light_direction: vec4, + light_color: vec4, + ambient_color: vec4, + camera_vp: mat4x4, + light_vp: mat4x4, + inv_vp: mat4x4, + cascade_vps: array, 3>, + cascade_splits: vec4, + point_lights: array, + spot_lights: array, + num_point_lights: vec4, + num_spot_lights: vec4, + probe_sh: array, 9>, + probe_enabled: vec4, + shadow_params: vec4, + ibl_params: vec4, + elapsed_time: vec4, +} + +// --------------------------------------------------------------------------- +// Bindings +// --------------------------------------------------------------------------- + +@group(0) @binding(0) var instances: array; + +@group(1) @binding(0) var scene: SceneUniforms; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const PI: f32 = 3.14159265359; + +// Water color palette +const WATER_SHALLOW: vec3 = vec3(0.10, 0.35, 0.55); +const WATER_DEEP: vec3 = vec3(0.04, 0.12, 0.25); +const WATER_SPECULAR_COLOR: vec3 = vec3(1.0, 0.95, 0.85); + +// Wave parameters: each octave is (direction_x, direction_z, frequency, amplitude) +const WAVE_OCTAVE_COUNT: i32 = 4; + +// --------------------------------------------------------------------------- +// Wave functions +// --------------------------------------------------------------------------- + +/// Multi-octave Gerstner-inspired sine wave displacement. +/// Returns vec3(dx, dy, dz) world-space displacement. +fn wave_displacement(world_xz: vec2, time: f32) -> vec3 { + // Four octaves with decreasing amplitude and increasing frequency. + // Directions are hand-picked for organic-looking interference patterns. + let dirs = array, 4>( + normalize(vec2(1.0, 0.6)), + normalize(vec2(-0.7, 1.0)), + normalize(vec2(0.3, -0.8)), + normalize(vec2(-0.5, -0.4)) + ); + let freqs = array(1.2, 2.5, 4.1, 6.8); + let amps = array(0.08, 0.04, 0.02, 0.01); + let speeds = array(1.0, 1.3, 0.9, 1.6); + + var displacement = vec3(0.0, 0.0, 0.0); + for (var i = 0; i < WAVE_OCTAVE_COUNT; i++) { + let phase = dot(dirs[i], world_xz) * freqs[i] + time * speeds[i]; + let s = sin(phase); + let c = cos(phase); + // Vertical displacement + displacement.y += s * amps[i]; + // Horizontal displacement (Gerstner-style lateral motion) + displacement.x += dirs[i].x * c * amps[i] * 0.3; + displacement.z += dirs[i].y * c * amps[i] * 0.3; + } + return displacement; +} + +/// Compute the wave-displaced normal by finite-difference sampling. +fn wave_normal(world_xz: vec2, time: f32) -> vec3 { + let eps = 0.1; + let hc = wave_displacement(world_xz, time).y; + let hx = wave_displacement(world_xz + vec2(eps, 0.0), time).y; + let hz = wave_displacement(world_xz + vec2(0.0, eps), time).y; + // Tangent vectors along X and Z, cross product gives the normal. + let tx = vec3(eps, hx - hc, 0.0); + let tz = vec3(0.0, hz - hc, eps); + return normalize(cross(tz, tx)); +} + +// --------------------------------------------------------------------------- +// Vertex stage +// --------------------------------------------------------------------------- + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) tangent: vec3, + @location(3) uv: vec2, +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_pos: vec3, + @location(1) world_normal: vec3, + @location(2) uv: vec2, +} + +@vertex +fn vs_main(in: VertexInput, @builtin(instance_index) iid: u32) -> VertexOutput { + let model = instances[iid].model; + let time = scene.elapsed_time.x; + + let world_pos_flat = (model * vec4(in.position, 1.0)).xyz; + let disp = wave_displacement(world_pos_flat.xz, time); + let world_pos = world_pos_flat + disp; + + var out: VertexOutput; + out.clip_position = scene.camera_vp * vec4(world_pos, 1.0); + out.world_pos = world_pos; + out.world_normal = wave_normal(world_pos_flat.xz, time); + out.uv = in.uv; + return out; +} + +// --------------------------------------------------------------------------- +// Fragment stage +// --------------------------------------------------------------------------- + +/// Schlick Fresnel approximation for water (F0 ~0.02 for water at normal incidence). +fn fresnel_water(cos_theta: f32) -> f32 { + let f0 = 0.02; + return f0 + (1.0 - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let time = scene.elapsed_time.x; + let N = normalize(in.world_normal); + let V = normalize(scene.camera_pos.xyz - in.world_pos); + let L = normalize(-scene.light_direction.xyz); + let H = normalize(V + L); + + let NdotV = max(dot(N, V), 0.0); + let NdotL = max(dot(N, L), 0.0); + let NdotH = max(dot(N, H), 0.0); + + // --- Fresnel --- + let fresnel = fresnel_water(NdotV); + + // --- Water color: blend shallow/deep based on view angle --- + // At grazing angles, see more of the surface (reflections). Looking straight + // down, see deeper into the water. + let depth_factor = 1.0 - NdotV; + let water_color = mix(WATER_SHALLOW, WATER_DEEP, depth_factor * 0.6); + + // --- Animated caustic-like color variation --- + // Subtle color shift from overlapping sine waves to suggest subsurface caustics. + let caustic_phase = dot(in.world_pos.xz, vec2(3.7, 2.9)) + time * 0.8; + let caustic = 0.03 * sin(caustic_phase) * sin(caustic_phase * 0.7 + 1.3); + let base_color = water_color + vec3(caustic * 0.5, caustic, caustic * 0.3); + + // --- Diffuse lighting --- + let light_intensity = scene.light_color.w; + let radiance = scene.light_color.rgb * light_intensity; + let ambient_intensity = scene.ambient_color.w; + let ambient = scene.ambient_color.rgb * ambient_intensity; + let diffuse = base_color * (ambient + radiance * NdotL * 0.6); + + // --- Specular highlight (Blinn-Phong for water — sharper than GGX at low roughness) --- + let spec_power = 256.0; + let spec = pow(NdotH, spec_power) * fresnel; + let specular = WATER_SPECULAR_COLOR * radiance * spec; + + // --- Combine --- + // Fresnel controls the mix: at grazing angles, more reflection (brighter); + // at normal incidence, more transmission (see-through). + let color = diffuse + specular; + + // Alpha: minimum ~0.4 (water is never fully invisible), rises with fresnel. + let alpha = mix(0.4, 0.75, fresnel); + + return vec4(color, alpha); +} diff --git a/crates/euca-render/src/extract.rs b/crates/euca-render/src/extract.rs index 1b6c3ba..01c17d6 100644 --- a/crates/euca-render/src/extract.rs +++ b/crates/euca-render/src/extract.rs @@ -122,6 +122,7 @@ impl RenderExtractor { material: mat_ref.handle, model_matrix, aabb: None, + is_water: false, }; self.entities[slot] = Some(RenderEntity { mesh: mesh_renderer.mesh, @@ -136,6 +137,7 @@ impl RenderExtractor { material: mat_ref.handle, model_matrix, aabb: None, + is_water: false, }; self.entities[free] = Some(RenderEntity { mesh: mesh_renderer.mesh, @@ -149,6 +151,7 @@ impl RenderExtractor { material: mat_ref.handle, model_matrix, aabb: None, + is_water: false, }); self.entities.push(Some(RenderEntity { mesh: mesh_renderer.mesh, diff --git a/crates/euca-render/src/lib.rs b/crates/euca-render/src/lib.rs index 39faeaf..8b0255f 100644 --- a/crates/euca-render/src/lib.rs +++ b/crates/euca-render/src/lib.rs @@ -138,7 +138,7 @@ pub use light::{AmbientLight, DirectionalLight, PointLight, SpotLight}; pub use light_probe::{LightProbe, LightProbeGrid, evaluate_sh}; pub use lod::{LodSettings, lod_select_system}; pub use material::{AlphaMode, Material, MaterialHandle, MaterialRef}; -pub use mesh::{GroundOffset, Mesh, MeshHandle, MeshRenderer}; +pub use mesh::{GroundOffset, Mesh, MeshHandle, MeshRenderer, WaterChunk}; pub use meshlet::{ GpuMeshlet, MAX_MESHLET_TRIANGLES, MAX_MESHLET_VERTICES, Meshlet, MeshletMesh, meshletize, }; diff --git a/crates/euca-render/src/mesh.rs b/crates/euca-render/src/mesh.rs index 84d5c3a..4bbef15 100644 --- a/crates/euca-render/src/mesh.rs +++ b/crates/euca-render/src/mesh.rs @@ -12,6 +12,14 @@ pub struct MeshRenderer { pub mesh: MeshHandle, } +/// Marker component indicating this entity's mesh should be rendered with +/// the water shader pipeline instead of the standard PBR pipeline. +/// +/// When present alongside [`MeshRenderer`] and [`MaterialRef`], the draw +/// command collector should set [`DrawCommand::is_water`] to `true`. +#[derive(Clone, Copy, Debug)] +pub struct WaterChunk; + /// Visual vertical offset applied at render time so that a mesh's bottom /// sits on the ground plane, without altering the entity's logical position. /// diff --git a/crates/euca-render/src/renderer.rs b/crates/euca-render/src/renderer.rs index ca97e9f..a8138de 100644 --- a/crates/euca-render/src/renderer.rs +++ b/crates/euca-render/src/renderer.rs @@ -207,6 +207,9 @@ pub struct DrawCommand { /// When provided and occlusion culling is enabled, objects fully behind /// previously rendered geometry are skipped. pub aabb: Option<(euca_math::Vec3, euca_math::Vec3)>, + /// When true, this command is rendered with the water shader pipeline + /// instead of the standard PBR pipeline. + pub is_water: bool, } #[repr(C)] @@ -272,6 +275,8 @@ struct SceneUniforms { shadow_params: [f32; 4], /// IBL parameters: x=enabled (0.0 or 1.0), y=intensity, z=unused, w=unused. ibl_params: [f32; 4], + /// Elapsed time: x=seconds since renderer creation, y/z/w=padding. + elapsed_time: [f32; 4], } struct GpuMaterial { @@ -411,6 +416,10 @@ pub struct Renderer Renderer { }, }); + let water_shader = rhi.create_shader(&euca_rhi::ShaderDesc { + label: Some("Water Shader"), + source: euca_rhi::ShaderSource::Wgsl(WATER_SHADER.into()), + }); + let water_pipeline = rhi.create_render_pipeline(&euca_rhi::RenderPipelineDesc { + label: Some("Water Pipeline"), + layout: &[&instance_bgl, &scene_bgl], + vertex: euca_rhi::VertexState { + module: &water_shader, + entry_point: "vs_main", + buffers: &[Vertex::RHI_LAYOUT], + }, + fragment: Some(euca_rhi::FragmentState { + module: &water_shader, + entry_point: "fs_main", + targets: &[Some(euca_rhi::ColorTargetState { + format: euca_rhi::TextureFormat::Rgba16Float, + blend: Some(euca_rhi::BlendState::ALPHA_BLENDING), + write_mask: euca_rhi::ColorWrites::ALL, + })], + }), + primitive: euca_rhi::PrimitiveState { + topology: euca_rhi::PrimitiveTopology::TriangleList, + front_face: euca_rhi::FrontFace::Ccw, + cull_mode: Some(euca_rhi::Face::Back), + ..Default::default() + }, + depth_stencil: Some(euca_rhi::DepthStencilState { + format: euca_rhi::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: euca_rhi::CompareFunction::Less, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: euca_rhi::MultisampleState { + count: MSAA_SAMPLE_COUNT, + mask: !0, + alpha_to_coverage_enabled: false, + }, + }); + let sky_pipeline = rhi.create_render_pipeline(&euca_rhi::RenderPipelineDesc { label: Some("Sky Pipeline"), layout: &[&scene_bgl], @@ -1076,6 +1126,8 @@ impl Renderer { ibl_intensity: 1.0, _ibl_dummy_cube: ibl_dummy_cube, _ibl_dummy_brdf: ibl_dummy_brdf, + water_pipeline, + start_time: std::time::Instant::now(), metalfx_enabled: false, } } @@ -1689,11 +1741,18 @@ impl Renderer { &self, commands: &'a [DrawCommand], camera_pos: euca_math::Vec3, - ) -> (Vec<&'a DrawCommand>, Vec<&'a DrawCommand>) { + ) -> ( + Vec<&'a DrawCommand>, + Vec<&'a DrawCommand>, + Vec<&'a DrawCommand>, + ) { let mut opaque = Vec::new(); let mut transparent = Vec::new(); + let mut water = Vec::new(); for cmd in commands { - if self.materials[cmd.material.0 as usize].is_transparent { + if cmd.is_water { + water.push(cmd); + } else if self.materials[cmd.material.0 as usize].is_transparent { transparent.push(cmd); } else { opaque.push(cmd); @@ -1704,7 +1763,7 @@ impl Renderer { let db = Self::distance_to_camera(&b.model_matrix, camera_pos); db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal) }); - (opaque, transparent) + (opaque, transparent, water) } fn distance_to_camera(model_matrix: &Mat4, camera_pos: euca_math::Vec3) -> f32 { @@ -1862,7 +1921,8 @@ impl Renderer { let rhi: &D = gpu; let vp = camera.view_projection_matrix(gpu.aspect_ratio()); let light_vp = Self::light_vp(light); - let (opaque_cmds, transparent_cmds) = self.partition_commands(commands, camera.eye); + let (opaque_cmds, transparent_cmds, water_cmds) = + self.partition_commands(commands, camera.eye); let opaque_cmds = self.apply_occlusion_culling(&opaque_cmds, vp); let (opaque_instances, opaque_batches) = Self::build_batches_from_refs(&opaque_cmds); // Pre-build transparent batches so we can ensure capacity before @@ -1872,8 +1932,17 @@ impl Renderer { } else { (Vec::new(), Vec::new()) }; - // Ensure capacity for the larger of opaque/transparent sets. - let max_needed = opaque_instances.len().max(trans_instances.len()); + // Pre-build water batches. + let (water_instances, water_batches) = if !water_cmds.is_empty() { + Self::build_batches_from_refs(&water_cmds) + } else { + (Vec::new(), Vec::new()) + }; + // Ensure capacity for the largest of opaque/transparent/water sets. + let max_needed = opaque_instances + .len() + .max(trans_instances.len()) + .max(water_instances.len()); if max_needed > 0 { self.ensure_instance_capacity(rhi, max_needed); } @@ -2007,6 +2076,7 @@ impl Renderer { 0.0, 0.0, ], + elapsed_time: [self.start_time.elapsed().as_secs_f32(), 0.0, 0.0, 0.0], }; self.scene_buffer .write_bytes(&**gpu, bytemuck::bytes_of(&scene)); @@ -2257,6 +2327,31 @@ impl Renderer { ); } } + + // ── Water pass: rendered after opaque + transparent with its own shader ── + if !water_cmds.is_empty() { + if !water_instances.is_empty() { + self.instance_buffer.write(&**gpu, &water_instances); + } + pass.set_pipeline(&self.water_pipeline); + pass.set_bind_group(0, &self.instance_bind_group, &[]); + pass.set_bind_group(1, &self.scene_bind_group, &[]); + for batch in &water_batches { + let mesh = &self.meshes[batch.mesh.0 as usize]; + pass.set_vertex_buffer(0, &mesh.vertex_buffer, 0, mesh.vertex_buffer_size); + pass.set_index_buffer( + &mesh.index_buffer, + euca_rhi::IndexFormat::Uint32, + 0, + mesh.index_buffer_size, + ); + pass.draw_indexed( + 0..mesh.index_count, + 0, + batch.instance_start..batch.instance_start + batch.instance_count, + ); + } + } } // Clear pending decals after rendering — they must be re-submitted each frame. @@ -2572,6 +2667,8 @@ const PBR_BINDLESS_SHADER: &str = include_str!("../shaders/pbr_bindless.wgsl"); const SKY_SHADER: &str = include_str!("../shaders/sky.wgsl"); +const WATER_SHADER: &str = include_str!("../shaders/water.wgsl"); + #[cfg(test)] mod tests { use super::*; @@ -2817,7 +2914,7 @@ mod tests { ); } - /// `SceneUniforms` must include the `ibl_params` field as the last vec4. + /// `SceneUniforms` must include the `ibl_params` field. #[test] fn scene_uniforms_contains_ibl_params() { let uniforms = SceneUniforms { @@ -2838,6 +2935,7 @@ mod tests { probe_enabled: [0.0; 4], shadow_params: [1.0, 0.01, 0.03, 0.5], ibl_params: [1.0, 0.8, 0.0, 0.0], + elapsed_time: [0.0; 4], }; assert_eq!(uniforms.ibl_params[0], 1.0, "ibl enabled flag"); assert!((uniforms.ibl_params[1] - 0.8).abs() < 1e-6, "ibl intensity"); diff --git a/crates/euca-web/src/lib.rs b/crates/euca-web/src/lib.rs index 130d180..2a77af0 100644 --- a/crates/euca-web/src/lib.rs +++ b/crates/euca-web/src/lib.rs @@ -256,6 +256,7 @@ impl WebAppRunner { material: mat.handle, model_matrix: gt.0.to_matrix(), aabb: None, + is_water: false, }) .collect::>() }; diff --git a/examples/client.rs b/examples/client.rs index 989997c..81b732c 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -311,6 +311,7 @@ fn collect_draw_commands(world: &World) -> Vec { material: mat.handle, model_matrix, aabb: None, + is_water: false, } }) .collect() diff --git a/examples/dota_client.rs b/examples/dota_client.rs index 757df46..214317f 100644 --- a/examples/dota_client.rs +++ b/examples/dota_client.rs @@ -964,6 +964,7 @@ fn spawn_moba_terrain( let terrain_offset = Vec3::new(-half, 0.0, -half); for chunk in chunks { + let is_water = chunk.surface == SurfaceType::Water; let mesh_handle = renderer.upload_mesh(gpu, &chunk.mesh); let mat = mat_handles .get(&chunk.surface) @@ -977,6 +978,9 @@ fn spawn_moba_terrain( world.insert(entity, GlobalTransform::default()); world.insert(entity, MeshRenderer { mesh: mesh_handle }); world.insert(entity, MaterialRef { handle: mat }); + if is_water { + world.insert(entity, WaterChunk); + } } // ── 6. Physics collider for click-to-move raycasting ───────────────── @@ -2504,11 +2508,13 @@ fn collect_draw_commands(world: &World) -> Vec { model_matrix = from_origin * scale_mat * to_origin * model_matrix; } + let is_water = world.get::(e).is_some(); commands.push(DrawCommand { mesh: mr.mesh, material: mat.handle, model_matrix, aabb: None, + is_water, }); } diff --git a/examples/editor.rs b/examples/editor.rs index fbb0b63..fd4eff7 100644 --- a/examples/editor.rs +++ b/examples/editor.rs @@ -460,6 +460,7 @@ fn collect_draw_commands(world: &World) -> Vec { material: mat.handle, model_matrix, aabb: None, + is_water: false, } }) .collect() @@ -503,6 +504,7 @@ fn append_selection_outline( material: mat, model_matrix: t.to_matrix(), aabb: None, + is_water: false, }); } } @@ -546,6 +548,7 @@ fn append_foliage_instances(world: &World, gpu: &GpuContext, cmds: &mut Vec