use crate::ecs::light::components::LightType;
use crate::ecs::world::{GLOBAL_TRANSFORM, LIGHT, WATER, World};
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec3};
use wgpu::util::DeviceExt;
const MAX_BODIES: usize = 16;
const UNIFORM_STRIDE: u64 = 768;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct WaterUniform {
render_vp: [[f32; 4]; 4],
inv_render_vp: [[f32; 4]; 4],
cur_vp: [[f32; 4]; 4],
prev_vp: [[f32; 4]; 4],
model: [[f32; 4]; 4],
extents: [f32; 4],
shallow_color: [f32; 4],
deep_color: [f32; 4],
foam_color: [f32; 4],
wave: [f32; 4],
wave_dir_time: [f32; 4],
params: [f32; 4],
sun_dir: [f32; 4],
sun_color: [f32; 4],
camera_pos: [f32; 4],
resolution: [f32; 4],
fog_color: [f32; 4],
fog_params: [f32; 4],
pad: [[f32; 4]; 15],
}
pub struct WaterPass {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
uniform_buffer: wgpu::Buffer,
linear_sampler: wgpu::Sampler,
point_sampler: wgpu::Sampler,
ibl_sampler: wgpu::Sampler,
fallback_environment_view: wgpu::TextureView,
vertex_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
index_count: u32,
grid_resolution: u32,
refraction_texture: wgpu::Texture,
refraction_view: wgpu::TextureView,
depth_copy_texture: wgpu::Texture,
depth_copy_view: wgpu::TextureView,
copy_size: (u32, u32),
color_format: wgpu::TextureFormat,
}
fn build_grid(resolution: u32) -> (Vec<[f32; 2]>, Vec<u32>) {
let segments = resolution.max(2);
let count = segments + 1;
let mut vertices = Vec::with_capacity((count * count) as usize);
for z in 0..count {
for x in 0..count {
let fx = x as f32 / segments as f32 * 2.0 - 1.0;
let fz = z as f32 / segments as f32 * 2.0 - 1.0;
vertices.push([fx, fz]);
}
}
let mut indices = Vec::with_capacity((segments * segments * 6) as usize);
for z in 0..segments {
for x in 0..segments {
let top_left = z * count + x;
let top_right = top_left + 1;
let bottom_left = top_left + count;
let bottom_right = bottom_left + 1;
indices.push(top_left);
indices.push(bottom_left);
indices.push(top_right);
indices.push(top_right);
indices.push(bottom_left);
indices.push(bottom_right);
}
}
(vertices, indices)
}
fn create_fallback_environment_view(device: &wgpu::Device) -> wgpu::TextureView {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Water Fallback Environment"),
size: wgpu::Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 6,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba16Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("Water Fallback Environment View"),
dimension: Some(wgpu::TextureViewDimension::Cube),
..Default::default()
})
}
fn create_copy_textures(
device: &wgpu::Device,
color_format: wgpu::TextureFormat,
width: u32,
height: u32,
) -> (
wgpu::Texture,
wgpu::TextureView,
wgpu::Texture,
wgpu::TextureView,
) {
let refraction_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Water Refraction Texture"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: color_format,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let refraction_view = refraction_texture.create_view(&wgpu::TextureViewDescriptor::default());
let depth_copy_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Water Depth Copy Texture"),
size: wgpu::Extent3d {
width: width.max(1),
height: height.max(1),
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let depth_copy_view = depth_copy_texture.create_view(&wgpu::TextureViewDescriptor::default());
(
refraction_texture,
refraction_view,
depth_copy_texture,
depth_copy_view,
)
}
impl WaterPass {
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"water.wgsl",
include_str!("../../shaders/water.wgsl"),
);
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Water Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(UNIFORM_STRIDE),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::Cube,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Water Pipeline Layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Water Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: 8,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &[wgpu::VertexAttribute {
format: wgpu::VertexFormat::Float32x2,
offset: 0,
shader_location: 0,
}],
}],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[
Some(wgpu::ColorTargetState {
format: color_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rg16Float,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
..Default::default()
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: Some(true),
depth_compare: Some(wgpu::CompareFunction::GreaterEqual),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Water Uniform Buffer"),
size: UNIFORM_STRIDE * MAX_BODIES as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Water Linear Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let point_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Water Point Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
..Default::default()
});
let ibl_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Water IBL Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
let fallback_environment_view = create_fallback_environment_view(device);
let grid_resolution = 192;
let (vertices, indices) = build_grid(grid_resolution);
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Water Vertex Buffer"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Water Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
let index_count = indices.len() as u32;
let (refraction_texture, refraction_view, depth_copy_texture, depth_copy_view) =
create_copy_textures(device, color_format, 1, 1);
Self {
pipeline,
bind_group_layout,
uniform_buffer,
linear_sampler,
point_sampler,
ibl_sampler,
fallback_environment_view,
vertex_buffer,
index_buffer,
index_count,
grid_resolution,
refraction_texture,
refraction_view,
depth_copy_texture,
depth_copy_view,
copy_size: (1, 1),
color_format,
}
}
fn rebuild_grid(&mut self, device: &wgpu::Device, resolution: u32) {
if resolution == self.grid_resolution {
return;
}
let (vertices, indices) = build_grid(resolution);
self.vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Water Vertex Buffer"),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
self.index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Water Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
});
self.index_count = indices.len() as u32;
self.grid_resolution = resolution;
}
}
fn query_sun(world: &World) -> (Vec3, Vec3) {
let mut direction = Vec3::new(0.35, 0.85, 0.4).normalize();
let mut color = Vec3::new(1.0, 0.96, 0.9);
let mut found = false;
world
.core
.query()
.with(LIGHT | GLOBAL_TRANSFORM)
.iter(|_, table, index| {
if found {
return;
}
let light = &table.light[index];
if light.light_type == LightType::Directional {
let matrix = table.global_transform[index].0;
let forward = Vec3::new(matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]);
let mut toward = forward.normalize();
if toward.y < 0.0 {
toward = -toward;
}
direction = toward;
color = light.color * light.intensity.max(0.0);
found = true;
}
});
(direction, color)
}
impl PassNode<World> for WaterPass {
fn name(&self) -> &str {
"water_pass"
}
fn reads(&self) -> Vec<&str> {
vec![]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color", "depth", "velocity"]
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> crate::render::wgpu::rendergraph::Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
> {
if !context.is_pass_enabled() || !context.configs.resources.render_settings.water_enabled {
return Ok(context.into_sub_graph_commands());
}
let world = context.configs;
let mut bodies: Vec<(crate::ecs::water::components::Water, Mat4)> = Vec::new();
world
.core
.query()
.with(WATER | GLOBAL_TRANSFORM)
.iter(|_, table, index| {
if bodies.len() >= MAX_BODIES {
return;
}
let body = table.water[index].clone();
if body.enabled {
bodies.push((body, table.global_transform[index].0));
}
});
if bodies.is_empty() {
return Ok(context.into_sub_graph_commands());
}
let camera = match crate::ecs::camera::queries::query_active_camera_matrices(world) {
Some(matrices) => matrices,
None => return Ok(context.into_sub_graph_commands()),
};
let max_resolution = bodies
.iter()
.map(|(body, _)| body.tessellation.clamp(4, 512))
.max()
.unwrap_or(192);
self.rebuild_grid(context.device, max_resolution);
let scene_color = context.get_texture("color")?;
let scene_depth = context.get_texture("depth")?;
let width = scene_color.width();
let height = scene_color.height();
if (width, height) != self.copy_size {
let (refraction_texture, refraction_view, depth_copy_texture, depth_copy_view) =
create_copy_textures(context.device, self.color_format, width, height);
self.refraction_texture = refraction_texture;
self.refraction_view = refraction_view;
self.depth_copy_texture = depth_copy_texture;
self.depth_copy_view = depth_copy_view;
self.copy_size = (width, height);
}
context.encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: scene_color,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &self.refraction_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
context.encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: scene_depth,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::DepthOnly,
},
wgpu::TexelCopyTextureInfo {
texture: &self.depth_copy_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::DepthOnly,
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let render_vp = camera.projection * camera.view;
let render_vp_array: [[f32; 4]; 4] = render_vp.into();
let inv_render_vp: [[f32; 4]; 4] = render_vp
.try_inverse()
.unwrap_or_else(Mat4::identity)
.into();
let cur_vp: [[f32; 4]; 4] =
Mat4::from(world.resources.renderer_state.view_projection).into();
let prev_vp: [[f32; 4]; 4] =
Mat4::from(world.resources.renderer_state.prev_view_projection).into();
let camera_position = camera.camera_position;
let (sun_direction, sun_color) = query_sun(world);
let (fog_color, fog_params) = match &world.resources.renderer_state.active_view.fog {
Some(fog) => (
[fog.color[0], fog.color[1], fog.color[2], 1.0],
[fog.start, fog.end, 0.0, 0.0],
),
None => ([0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]),
};
let time = world.resources.window.timing.uptime_milliseconds as f32 / 1000.0;
let prev_time = time - world.resources.window.timing.delta_time.max(0.0);
for (index, (body, transform)) in bodies.iter().enumerate() {
let direction = Vec3::new(
body.wave_direction_radians.cos(),
0.0,
body.wave_direction_radians.sin(),
);
let model: [[f32; 4]; 4] = (*transform).into();
let uniform = WaterUniform {
render_vp: render_vp_array,
inv_render_vp,
cur_vp,
prev_vp,
model,
extents: [
body.half_extents.x,
body.half_extents.y,
body.specular_strength,
0.0,
],
shallow_color: [
body.shallow_color[0],
body.shallow_color[1],
body.shallow_color[2],
body.depth_fade_distance,
],
deep_color: [
body.deep_color[0],
body.deep_color[1],
body.deep_color[2],
body.edge_foam_distance,
],
foam_color: [
body.foam_color[0],
body.foam_color[1],
body.foam_color[2],
body.foam_amount,
],
wave: [
body.wave_amplitude,
body.wave_steepness,
body.wave_length,
body.wave_speed,
],
wave_dir_time: [direction.x, direction.z, time, prev_time],
params: [
body.roughness,
body.fresnel_power,
body.reflection_strength,
body.refraction_strength,
],
sun_dir: [sun_direction.x, sun_direction.y, sun_direction.z, 0.0],
sun_color: [sun_color.x, sun_color.y, sun_color.z, 0.0],
camera_pos: [camera_position.x, camera_position.y, camera_position.z, 1.0],
resolution: [width.max(1) as f32, height.max(1) as f32, 0.0, 0.0],
fog_color,
fog_params,
pad: [[0.0; 4]; 15],
};
context.queue.write_buffer(
&self.uniform_buffer,
index as u64 * UNIFORM_STRIDE,
bytemuck::bytes_of(&uniform),
);
}
let environment_view = world
.resources
.ibl_views
.prefiltered_view
.as_ref()
.unwrap_or(&self.fallback_environment_view);
let bind_group = context
.device
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Water Bind Group"),
layout: &self.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
buffer: &self.uniform_buffer,
offset: 0,
size: wgpu::BufferSize::new(UNIFORM_STRIDE),
}),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&self.refraction_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&self.depth_copy_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::Sampler(&self.linear_sampler),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::Sampler(&self.point_sampler),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(environment_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&self.ibl_sampler),
},
],
});
let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
let (velocity_view, velocity_load, velocity_store) =
context.get_color_attachment("velocity")?;
let (depth_view, depth_load, depth_store) = context.get_depth_attachment("depth")?;
{
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Water Pass"),
color_attachments: &[
Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: color_load,
store: color_store,
},
depth_slice: None,
}),
Some(wgpu::RenderPassColorAttachment {
view: velocity_view,
resolve_target: None,
ops: wgpu::Operations {
load: velocity_load,
store: velocity_store,
},
depth_slice: None,
}),
],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: depth_view,
depth_ops: Some(wgpu::Operations {
load: depth_load,
store: depth_store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.pipeline);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
for index in 0..bodies.len() {
let offset = index as u32 * UNIFORM_STRIDE as u32;
render_pass.set_bind_group(0, &bind_group, &[offset]);
render_pass.draw_indexed(0..self.index_count, 0, 0..1);
}
}
Ok(context.into_sub_graph_commands())
}
}