use crate::ecs::grass::components::GrassRegion;
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec3};
const MAX_INSTANCES: u32 = 500_000;
const MAX_VISIBLE: u32 = 200_000;
const BLADE_VERTICES: u32 = 7;
#[cfg(feature = "terrain")]
const HEIGHTMAP_SIZE: u32 = 256;
const BEND_MAP_SIZE: u32 = 128;
const MAX_SPECIES: usize = 8;
const MAX_INTERACTORS: usize = 16;
const WORKGROUP_SIZE: u32 = 256;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GrassInstance {
position: [f32; 3],
rotation: f32,
height: f32,
width: f32,
species_index: u32,
lod: u32,
base_color: [f32; 4],
tip_color: [f32; 4],
bend: [f32; 2],
_padding: [f32; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GrassSpeciesGpu {
base_color: [f32; 4],
tip_color: [f32; 4],
sss_color: [f32; 4],
sss_intensity: f32,
specular_power: f32,
specular_strength: f32,
blade_curvature: f32,
blade_width: f32,
blade_height_min: f32,
blade_height_max: f32,
density_scale: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct SpeciesWeights {
weights0: [f32; 4],
weights1: [f32; 4],
count: u32,
_pad: [u32; 3],
_padding: [u32; 8],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct CullingUniforms {
view_projection: [[f32; 4]; 4],
camera_position: [f32; 4],
lod_distances: [f32; 4],
lod_density_scales: [f32; 4],
total_instances: u32,
max_visible: u32,
time: f32,
_padding: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct CameraUniforms {
view: [[f32; 4]; 4],
projection: [[f32; 4]; 4],
view_projection: [[f32; 4]; 4],
camera_position: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GrassRenderUniforms {
time: f32,
wind_strength: f32,
wind_frequency: f32,
sss_intensity: f32,
wind_direction: [f32; 2],
interaction_strength: f32,
_padding: f32,
sun_direction: [f32; 3],
sun_intensity: f32,
sun_color: [f32; 3],
ambient_intensity: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Interactor {
position: [f32; 3],
radius: f32,
velocity: [f32; 3],
strength: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct InteractionUniforms {
interactor_count: u32,
bend_map_size: u32,
world_size: f32,
world_center_x: f32,
world_center_z: f32,
decay_rate: f32,
delta_time: f32,
_padding: f32,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct DrawIndirect {
vertex_count: u32,
instance_count: u32,
first_vertex: u32,
first_instance: u32,
}
pub struct GrassPass {
instance_buffer: wgpu::Buffer,
_visible_indices_buffer: wgpu::Buffer,
draw_indirect_buffer: wgpu::Buffer,
species_buffer: wgpu::Buffer,
species_weights_buffer: wgpu::Buffer,
interactor_buffer: wgpu::Buffer,
culling_uniforms_buffer: wgpu::Buffer,
camera_uniforms_buffer: wgpu::Buffer,
grass_render_uniforms_buffer: wgpu::Buffer,
interaction_uniforms_buffer: wgpu::Buffer,
#[cfg(feature = "terrain")]
heightmap_texture: wgpu::Texture,
_bend_map_textures: [wgpu::Texture; 2],
_bend_map_views: [wgpu::TextureView; 2],
_bend_map_storage_views: [wgpu::TextureView; 2],
_bend_map_sampler: wgpu::Sampler,
bend_map_index: usize,
culling_pipeline: wgpu::ComputePipeline,
reset_pipeline: wgpu::ComputePipeline,
_culling_bind_group_layout: wgpu::BindGroupLayout,
culling_bind_group: wgpu::BindGroup,
interaction_pipeline: wgpu::ComputePipeline,
_interaction_bind_group_layout: wgpu::BindGroupLayout,
interaction_bind_groups: [wgpu::BindGroup; 2],
sample_bend_pipeline: wgpu::ComputePipeline,
_sample_bend_bind_group_layout: wgpu::BindGroupLayout,
sample_bend_bind_groups: [wgpu::BindGroup; 2],
render_pipeline: wgpu::RenderPipeline,
_render_bind_group_layout: wgpu::BindGroupLayout,
render_bind_group: wgpu::BindGroup,
time: f32,
instance_count: u32,
enabled: bool,
initialized: bool,
terrain_width: f32,
terrain_depth: f32,
}
impl GrassPass {
pub fn new(
device: &wgpu::Device,
color_format: wgpu::TextureFormat,
depth_format: wgpu::TextureFormat,
) -> Self {
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Instance Buffer"),
size: (std::mem::size_of::<GrassInstance>() as u64) * MAX_INSTANCES as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let visible_indices_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Visible Indices Buffer"),
size: (std::mem::size_of::<u32>() as u64) * MAX_VISIBLE as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let draw_indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Draw Indirect Buffer"),
size: std::mem::size_of::<DrawIndirect>() as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let species_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Species Buffer"),
size: (std::mem::size_of::<GrassSpeciesGpu>() as u64) * MAX_SPECIES as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let species_weights_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Species Weights Buffer"),
size: std::mem::size_of::<SpeciesWeights>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let interactor_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Interactor Buffer"),
size: (std::mem::size_of::<Interactor>() as u64) * MAX_INTERACTORS as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let culling_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Culling Uniforms"),
size: std::mem::size_of::<CullingUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let camera_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Camera Uniforms"),
size: std::mem::size_of::<CameraUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let grass_render_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Render Uniforms"),
size: std::mem::size_of::<GrassRenderUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let interaction_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Interaction Uniforms"),
size: std::mem::size_of::<InteractionUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
#[cfg(feature = "terrain")]
let heightmap_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Grass Heightmap"),
size: wgpu::Extent3d {
width: HEIGHTMAP_SIZE,
height: HEIGHTMAP_SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let bend_map_textures: [wgpu::Texture; 2] = std::array::from_fn(|index| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(&format!("Grass Bend Map {}", index)),
size: wgpu::Extent3d {
width: BEND_MAP_SIZE,
height: BEND_MAP_SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rg32Float,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::COPY_DST,
view_formats: &[],
})
});
let bend_map_views: [wgpu::TextureView; 2] = std::array::from_fn(|index| {
bend_map_textures[index].create_view(&wgpu::TextureViewDescriptor::default())
});
let bend_map_storage_views: [wgpu::TextureView; 2] = std::array::from_fn(|index| {
bend_map_textures[index].create_view(&wgpu::TextureViewDescriptor {
format: Some(wgpu::TextureFormat::Rg32Float),
..Default::default()
})
});
let bend_map_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Grass Bend Map Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let culling_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Grass Culling Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/grass_culling.wgsl").into(),
),
});
let culling_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Culling Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
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::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let culling_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Culling Pipeline Layout"),
bind_group_layouts: &[Some(&culling_bind_group_layout)],
immediate_size: 0,
});
let culling_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Culling Pipeline"),
layout: Some(&culling_pipeline_layout),
module: &culling_shader,
entry_point: Some("main"),
compilation_options: Default::default(),
cache: None,
});
let reset_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Reset Draw Command Pipeline"),
layout: Some(&culling_pipeline_layout),
module: &culling_shader,
entry_point: Some("reset_draw_command"),
compilation_options: Default::default(),
cache: None,
});
let culling_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Grass Culling Bind Group"),
layout: &culling_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: culling_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: instance_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: visible_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: draw_indirect_buffer.as_entire_binding(),
},
],
});
let interaction_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Grass Interaction Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/grass_interaction.wgsl").into(),
),
});
let interaction_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Interaction Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
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::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: wgpu::TextureFormat::Rg32Float,
view_dimension: wgpu::TextureViewDimension::D2,
},
count: None,
},
],
});
let interaction_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Interaction Pipeline Layout"),
bind_group_layouts: &[Some(&interaction_bind_group_layout)],
immediate_size: 0,
});
let interaction_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Interaction Pipeline"),
layout: Some(&interaction_pipeline_layout),
module: &interaction_shader,
entry_point: Some("update_bend_map"),
compilation_options: Default::default(),
cache: None,
});
let interaction_bind_groups: [wgpu::BindGroup; 2] = std::array::from_fn(|index| {
let read_index = index;
let write_index = 1 - index;
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(&format!("Grass Interaction Bind Group {}", index)),
layout: &interaction_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: interaction_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: interactor_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&bend_map_views[read_index]),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(
&bend_map_storage_views[write_index],
),
},
],
})
});
let sample_bend_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Sample Bend Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let sample_bend_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Sample Bend Pipeline Layout"),
bind_group_layouts: &[
Some(&interaction_bind_group_layout),
Some(&sample_bend_bind_group_layout),
],
immediate_size: 0,
});
let sample_bend_pipeline =
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Sample Bend Pipeline"),
layout: Some(&sample_bend_pipeline_layout),
module: &interaction_shader,
entry_point: Some("sample_bend_for_instances"),
compilation_options: Default::default(),
cache: None,
});
let sample_bend_bind_groups: [wgpu::BindGroup; 2] = std::array::from_fn(|_| {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Grass Sample Bend Bind Group"),
layout: &sample_bend_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: instance_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&bend_map_sampler),
},
],
})
});
let render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Grass Render Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../../shaders/grass_render.wgsl").into(),
),
});
let render_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Render Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::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::VERTEX | 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::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Render Pipeline Layout"),
bind_group_layouts: &[Some(&render_bind_group_layout)],
immediate_size: 0,
});
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("Grass Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
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: 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 render_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Grass Render Bind Group"),
layout: &render_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: grass_render_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: instance_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: visible_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: species_buffer.as_entire_binding(),
},
],
});
Self {
instance_buffer,
_visible_indices_buffer: visible_indices_buffer,
draw_indirect_buffer,
species_buffer,
species_weights_buffer,
interactor_buffer,
culling_uniforms_buffer,
camera_uniforms_buffer,
grass_render_uniforms_buffer,
interaction_uniforms_buffer,
#[cfg(feature = "terrain")]
heightmap_texture,
_bend_map_textures: bend_map_textures,
_bend_map_views: bend_map_views,
_bend_map_storage_views: bend_map_storage_views,
_bend_map_sampler: bend_map_sampler,
bend_map_index: 0,
culling_pipeline,
reset_pipeline,
_culling_bind_group_layout: culling_bind_group_layout,
culling_bind_group,
interaction_pipeline,
_interaction_bind_group_layout: interaction_bind_group_layout,
interaction_bind_groups,
sample_bend_pipeline,
_sample_bend_bind_group_layout: sample_bend_bind_group_layout,
sample_bend_bind_groups,
render_pipeline,
_render_bind_group_layout: render_bind_group_layout,
render_bind_group,
time: 0.0,
instance_count: 0,
enabled: false,
initialized: false,
terrain_width: 100.0,
terrain_depth: 100.0,
}
}
#[cfg(feature = "terrain")]
fn generate_heightmap(&mut self, queue: &wgpu::Queue, region: &GrassRegion) {
if let Some(terrain_config) = ®ion.terrain_config {
self.terrain_width = terrain_config.width;
self.terrain_depth = terrain_config.depth;
let mut heightmap_data = vec![0.0f32; (HEIGHTMAP_SIZE * HEIGHTMAP_SIZE) as usize];
for z in 0..HEIGHTMAP_SIZE {
for x in 0..HEIGHTMAP_SIZE {
let u = x as f32 / (HEIGHTMAP_SIZE - 1) as f32;
let v = z as f32 / (HEIGHTMAP_SIZE - 1) as f32;
let world_x = (u - 0.5) * terrain_config.width;
let world_z = (v - 0.5) * terrain_config.depth;
let height = crate::ecs::terrain::sample_terrain_height(
world_x,
world_z,
terrain_config,
);
heightmap_data[(z * HEIGHTMAP_SIZE + x) as usize] = height;
}
}
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.heightmap_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytemuck::cast_slice(&heightmap_data),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(HEIGHTMAP_SIZE * 4),
rows_per_image: Some(HEIGHTMAP_SIZE),
},
wgpu::Extent3d {
width: HEIGHTMAP_SIZE,
height: HEIGHTMAP_SIZE,
depth_or_array_layers: 1,
},
);
}
}
fn upload_species_data(&self, queue: &wgpu::Queue, region: &GrassRegion) {
let mut species_data = vec![
GrassSpeciesGpu {
base_color: [0.0; 4],
tip_color: [0.0; 4],
sss_color: [0.0; 4],
sss_intensity: 0.0,
specular_power: 0.0,
specular_strength: 0.0,
blade_curvature: 0.0,
blade_width: 0.0,
blade_height_min: 0.0,
blade_height_max: 0.0,
density_scale: 0.0,
};
MAX_SPECIES
];
for (index, species) in region.species.iter().enumerate().take(MAX_SPECIES) {
species_data[index] = GrassSpeciesGpu {
base_color: species.base_color,
tip_color: species.tip_color,
sss_color: species.sss_color,
sss_intensity: species.sss_intensity,
specular_power: species.specular_power,
specular_strength: species.specular_strength,
blade_curvature: species.blade_curvature,
blade_width: species.blade_width,
blade_height_min: species.blade_height_min,
blade_height_max: species.blade_height_max,
density_scale: species.density_scale,
};
}
queue.write_buffer(&self.species_buffer, 0, bytemuck::cast_slice(&species_data));
let normalized = region.normalized_weights();
let mut weights = SpeciesWeights {
weights0: [0.0; 4],
weights1: [0.0; 4],
count: normalized.len().min(MAX_SPECIES) as u32,
_pad: [0; 3],
_padding: [0; 8],
};
for (index, &weight) in normalized.iter().enumerate().take(8) {
if index < 4 {
weights.weights0[index] = weight;
} else {
weights.weights1[index - 4] = weight;
}
}
queue.write_buffer(
&self.species_weights_buffer,
0,
bytemuck::cast_slice(&[weights]),
);
}
fn initialize_instances_cpu(&mut self, queue: &wgpu::Queue, region: &GrassRegion) {
if region.species.is_empty() {
return;
}
let terrain_width = self.terrain_width;
let terrain_depth = self.terrain_depth;
let area = terrain_width * terrain_depth;
let blades_per_sq_meter = region.config.blades_per_patch as f32 / 8.0;
let blade_count = ((area * blades_per_sq_meter) as usize).min(MAX_INSTANCES as usize);
let mut instances = Vec::with_capacity(blade_count);
let normalized_weights = region.normalized_weights();
let mut cumulative_weights = Vec::with_capacity(normalized_weights.len());
let mut sum = 0.0;
for weight in &normalized_weights {
sum += weight;
cumulative_weights.push(sum);
}
for index in 0..blade_count {
let mut rng_state = (index as u32).wrapping_mul(2654435761);
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let u = rng_state as f32 / u32::MAX as f32;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let v = rng_state as f32 / u32::MAX as f32;
let world_x = (u - 0.5) * terrain_width;
let world_z = (v - 0.5) * terrain_depth;
#[cfg(feature = "terrain")]
let world_y = if let Some(terrain_config) = ®ion.terrain_config {
crate::ecs::terrain::sample_terrain_height(world_x, world_z, terrain_config)
} else {
0.0
};
#[cfg(not(feature = "terrain"))]
let world_y = 0.0;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let species_roll = rng_state as f32 / u32::MAX as f32;
let species_index = cumulative_weights
.iter()
.position(|&w| species_roll <= w)
.unwrap_or(0);
let species = ®ion.species[species_index];
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let height_t = rng_state as f32 / u32::MAX as f32;
let height = species.blade_height_min
+ height_t * (species.blade_height_max - species.blade_height_min);
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let width = species.blade_width * (0.8 + (rng_state as f32 / u32::MAX as f32) * 0.4);
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let rotation = (rng_state as f32 / u32::MAX as f32) * std::f32::consts::TAU;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let color_var = 0.85 + (rng_state as f32 / u32::MAX as f32) * 0.3;
instances.push(GrassInstance {
position: [world_x, world_y, world_z],
rotation,
height,
width,
species_index: species_index as u32,
lod: 0,
base_color: [
species.base_color[0] * color_var,
species.base_color[1] * color_var,
species.base_color[2] * color_var,
species.base_color[3],
],
tip_color: [
species.tip_color[0] * color_var,
species.tip_color[1] * color_var,
species.tip_color[2] * color_var,
species.tip_color[3],
],
bend: [0.0, 0.0],
_padding: [0.0, 0.0],
});
}
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
self.instance_count = instances.len() as u32;
self.initialized = true;
}
}
impl PassNode<World> for GrassPass {
fn name(&self) -> &str {
"grass_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) {
self.time += world.resources.window.timing.delta_time;
let grass_entities: Vec<_> = world
.core
.query_entities(crate::ecs::GRASS_REGION)
.collect();
if grass_entities.is_empty() {
self.enabled = false;
return;
}
for entity in grass_entities {
if let Some(region) = world.core.get_grass_region(entity) {
if !region.enabled {
self.enabled = false;
continue;
}
self.enabled = true;
if !self.initialized {
#[cfg(feature = "terrain")]
self.generate_heightmap(queue, region);
self.upload_species_data(queue, region);
self.initialize_instances_cpu(queue, region);
}
if let Some(camera_matrices) =
crate::ecs::camera::queries::query_active_camera_matrices(world)
{
let view: Mat4 = camera_matrices.view;
let projection: Mat4 = camera_matrices.projection;
let view_projection = projection * view;
let camera_position = camera_matrices.camera_position;
let camera_uniforms = CameraUniforms {
view: view.into(),
projection: projection.into(),
view_projection: view_projection.into(),
camera_position: [
camera_position.x,
camera_position.y,
camera_position.z,
1.0,
],
};
queue.write_buffer(
&self.camera_uniforms_buffer,
0,
bytemuck::cast_slice(&[camera_uniforms]),
);
let culling_uniforms = CullingUniforms {
view_projection: view_projection.into(),
camera_position: [
camera_position.x,
camera_position.y,
camera_position.z,
1.0,
],
lod_distances: region.config.lod_distances,
lod_density_scales: region.config.lod_density_scales,
total_instances: self.instance_count,
max_visible: MAX_VISIBLE,
time: self.time,
_padding: 0.0,
};
queue.write_buffer(
&self.culling_uniforms_buffer,
0,
bytemuck::cast_slice(&[culling_uniforms]),
);
let sun_dir = nalgebra_glm::normalize(&Vec3::new(0.3, 0.8, 0.5));
let grass_render_uniforms = GrassRenderUniforms {
time: self.time,
wind_strength: region.config.wind_strength,
wind_frequency: region.config.wind_frequency,
sss_intensity: 0.5,
wind_direction: region.config.wind_direction,
interaction_strength: region.config.interaction_strength,
_padding: 0.0,
sun_direction: [sun_dir.x, sun_dir.y, sun_dir.z],
sun_intensity: 2.0,
sun_color: [1.0, 0.95, 0.9],
ambient_intensity: 0.3,
};
queue.write_buffer(
&self.grass_render_uniforms_buffer,
0,
bytemuck::cast_slice(&[grass_render_uniforms]),
);
let mut interactors = Vec::new();
if region.config.interactors_enabled {
for entity in world.core.query_entities(crate::ecs::GRASS_INTERACTOR) {
if let (Some(interactor), Some(transform)) = (
world.core.get_grass_interactor(entity),
world.core.get_global_transform(entity),
) {
let pos = transform.translation();
interactors.push(Interactor {
position: [pos.x, pos.y, pos.z],
radius: interactor.radius,
velocity: [0.0, 0.0, 0.0],
strength: interactor.strength,
});
}
}
}
let interactor_count = interactors.len().min(MAX_INTERACTORS);
if !interactors.is_empty() {
queue.write_buffer(
&self.interactor_buffer,
0,
bytemuck::cast_slice(&interactors[..interactor_count]),
);
}
let interaction_uniforms = InteractionUniforms {
interactor_count: interactor_count as u32,
bend_map_size: BEND_MAP_SIZE,
world_size: self.terrain_width.max(self.terrain_depth),
world_center_x: 0.0,
world_center_z: 0.0,
decay_rate: 5.0,
delta_time: world.resources.window.timing.delta_time,
_padding: 0.0,
};
queue.write_buffer(
&self.interaction_uniforms_buffer,
0,
bytemuck::cast_slice(&[interaction_uniforms]),
);
let initial_draw_command = DrawIndirect {
vertex_count: BLADE_VERTICES,
instance_count: 0,
first_vertex: 0,
first_instance: 0,
};
queue.write_buffer(
&self.draw_indirect_buffer,
0,
bytemuck::cast_slice(&[initial_draw_command]),
);
}
}
}
}
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.enabled || self.instance_count == 0 {
return Ok(context.into_sub_graph_commands());
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Grass Interaction Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.interaction_pipeline);
compute_pass.set_bind_group(0, &self.interaction_bind_groups[self.bend_map_index], &[]);
let workgroups = BEND_MAP_SIZE.div_ceil(16);
compute_pass.dispatch_workgroups(workgroups, workgroups, 1);
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Grass Sample Bend Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.sample_bend_pipeline);
compute_pass.set_bind_group(
0,
&self.interaction_bind_groups[1 - self.bend_map_index],
&[],
);
compute_pass.set_bind_group(1, &self.sample_bend_bind_groups[self.bend_map_index], &[]);
let workgroups = self.instance_count.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Grass Reset Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.reset_pipeline);
compute_pass.set_bind_group(0, &self.culling_bind_group, &[]);
compute_pass.dispatch_workgroups(1, 1, 1);
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Grass Culling Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.culling_pipeline);
compute_pass.set_bind_group(0, &self.culling_bind_group, &[]);
let workgroups = self.instance_count.div_ceil(WORKGROUP_SIZE);
compute_pass.dispatch_workgroups(workgroups, 1, 1);
}
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("Grass 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,
multiview_mask: None,
});
render_pass.set_pipeline(&self.render_pipeline);
render_pass.set_bind_group(0, &self.render_bind_group, &[]);
render_pass.draw_indirect(&self.draw_indirect_buffer, 0);
}
self.bend_map_index = 1 - self.bend_map_index;
Ok(context.into_sub_graph_commands())
}
}