use crate::ecs::camera::components::Projection;
use crate::ecs::camera::queries::query_active_camera_matrices;
use crate::ecs::water::Water;
use crate::ecs::world::{WATER, World};
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
const MAX_POLYGON_VERTICES: usize = 16;
const MAX_WATER_REGIONS: usize = 16;
struct WaterRegionBuffers {
params_buffer: wgpu::Buffer,
polygon_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
}
pub struct WaterPass {
uniform_buffer: wgpu::Buffer,
pipeline: wgpu::RenderPipeline,
region_buffer_pool: Vec<WaterRegionBuffers>,
active_region_count: usize,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct WaterUniforms {
view: [[f32; 4]; 4],
projection: [[f32; 4]; 4],
inv_view_projection: [[f32; 4]; 4],
camera_position: [f32; 4],
resolution: [f32; 2],
time: f32,
z_near: f32,
z_far: f32,
_padding: [f32; 7],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct WaterParams {
base_color: [f32; 4],
water_color: [f32; 4],
base_height: f32,
wave_height: f32,
choppy: f32,
speed: f32,
frequency: f32,
specular_strength: f32,
fresnel_power: f32,
edge_feather_distance: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct WaterPolygon {
vertices: [[f32; 4]; MAX_POLYGON_VERTICES],
vertex_count: u32,
use_bounds: u32,
_padding: [f32; 2],
}
impl Default for WaterPolygon {
fn default() -> Self {
Self {
vertices: [[0.0; 4]; MAX_POLYGON_VERTICES],
vertex_count: 0,
use_bounds: 0,
_padding: [0.0; 2],
}
}
}
impl Default for WaterParams {
fn default() -> Self {
Self {
base_color: [0.0, 0.09, 0.18, 1.0],
water_color: [0.48, 0.54, 0.36, 1.0],
base_height: 0.0,
wave_height: 0.6,
choppy: 4.0,
speed: 0.8,
frequency: 0.16,
specular_strength: 1.0,
fresnel_power: 3.0,
edge_feather_distance: 2.0,
}
}
}
impl From<&Water> for WaterParams {
fn from(water: &Water) -> Self {
Self {
base_color: water.base_color,
water_color: water.water_color,
base_height: water.base_height,
wave_height: water.wave_height,
choppy: water.choppy,
speed: water.speed,
frequency: water.frequency,
specular_strength: water.specular_strength,
fresnel_power: water.fresnel_power,
edge_feather_distance: water.edge_feather_distance,
}
}
}
impl WaterPolygon {
fn from_water(water: &Water) -> Self {
let mut polygon = Self::default();
if let Some(bounds) = &water.bounds {
polygon.use_bounds = 1;
polygon.vertex_count = bounds.len().min(MAX_POLYGON_VERTICES) as u32;
for (index, vertex) in bounds.iter().take(MAX_POLYGON_VERTICES).enumerate() {
polygon.vertices[index] = [vertex[0], vertex[1], 0.0, 0.0];
}
}
polygon
}
}
impl WaterPass {
pub fn new(
device: &wgpu::Device,
color_format: wgpu::TextureFormat,
depth_format: wgpu::TextureFormat,
) -> Self {
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Water Uniform Buffer"),
size: std::mem::size_of::<WaterUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
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: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let shader = device.create_shader_module(wgpu::include_wgsl!("../../shaders/water.wgsl"));
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Water Pipeline Layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Water Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vertex_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fragment_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: depth_format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::GreaterEqual,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let mut region_buffer_pool = Vec::with_capacity(MAX_WATER_REGIONS);
for index in 0..MAX_WATER_REGIONS {
let params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&format!("Water Params Buffer {}", index)),
size: std::mem::size_of::<WaterParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let polygon_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(&format!("Water Polygon Buffer {}", index)),
size: std::mem::size_of::<WaterPolygon>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("Water Bind Group {}", index)),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: polygon_buffer.as_entire_binding(),
},
],
});
region_buffer_pool.push(WaterRegionBuffers {
params_buffer,
polygon_buffer,
bind_group,
});
}
Self {
uniform_buffer,
pipeline,
region_buffer_pool,
active_region_count: 0,
}
}
fn collect_active_waters(world: &World) -> Vec<&Water> {
let mut waters = Vec::new();
for entity in world.query_entities(WATER) {
if world.get_render_mesh(entity).is_some() {
continue;
}
if let Some(water) = world.get_water(entity)
&& water.enabled
&& !water.is_volumetric
{
waters.push(water);
if waters.len() >= MAX_WATER_REGIONS {
break;
}
}
}
waters
}
}
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"]
}
fn prepare(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
let waters = Self::collect_active_waters(world);
if waters.is_empty() {
self.active_region_count = 0;
return;
}
let Some(camera_matrices) = query_active_camera_matrices(world) else {
self.active_region_count = 0;
return;
};
let view = camera_matrices.view;
let projection = camera_matrices.projection;
let view_projection = projection * view;
let inv_view_projection = nalgebra_glm::inverse(&view_projection);
let view_inverse = nalgebra_glm::inverse(&view);
let camera_position = [
view_inverse[(0, 3)],
view_inverse[(1, 3)],
view_inverse[(2, 3)],
1.0,
];
let time = world.resources.window.timing.uptime_milliseconds as f32 / 1000.0;
let (width, height) = world
.resources
.window
.cached_viewport_size
.unwrap_or((1920, 1080));
let (z_near, z_far) = world
.resources
.active_camera
.and_then(|entity| world.get_camera(entity))
.map(|camera| match &camera.projection {
Projection::Perspective(persp) => (persp.z_near, persp.z_far.unwrap_or(2000.0)),
Projection::Orthographic(ortho) => (ortho.z_near, ortho.z_far),
})
.unwrap_or((0.1, 2000.0));
let uniforms = WaterUniforms {
view: view.into(),
projection: projection.into(),
inv_view_projection: inv_view_projection.into(),
camera_position,
resolution: [width as f32, height as f32],
time,
z_near,
z_far,
_padding: [0.0; 7],
};
queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
self.active_region_count = waters.len().min(MAX_WATER_REGIONS);
for (index, water) in waters.iter().take(self.active_region_count).enumerate() {
let params = WaterParams::from(*water);
let polygon = WaterPolygon::from_water(water);
queue.write_buffer(
&self.region_buffer_pool[index].params_buffer,
0,
bytemuck::cast_slice(&[params]),
);
queue.write_buffer(
&self.region_buffer_pool[index].polygon_buffer,
0,
bytemuck::cast_slice(&[polygon]),
);
}
}
fn execute<'r, 'e>(
&mut self,
context: PassExecutionContext<'r, 'e, World>,
) -> crate::render::wgpu::rendergraph::Result<
Vec<crate::render::wgpu::rendergraph::SubGraphRunCommand<'r>>,
> {
if self.active_region_count == 0 {
return Ok(context.into_sub_graph_commands());
}
let (color_view, color_load, color_store) = context.get_color_attachment("color")?;
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 Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target: None,
ops: wgpu::Operations {
load: color_load,
store: color_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,
});
render_pass.set_pipeline(&self.pipeline);
for region in self
.region_buffer_pool
.iter()
.take(self.active_region_count)
{
render_pass.set_bind_group(0, ®ion.bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}
Ok(context.into_sub_graph_commands())
}
}