use crate::ecs::grass::{GrassDomain, GrassSettings};
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec3};
use wgpu::util::DeviceExt;
const MAX_BLADES: u32 = 524_288;
const MAX_BLADE_TILES: u32 = 4096;
const MAX_FAR_TILES: u32 = 8192;
pub(crate) const MAX_TYPES: usize = 8;
const MAX_PUSHERS: usize = 8;
const TILE_SIZE: f32 = 8.0;
const HIGH_INDEX_COUNT: u32 = 33;
const LOW_INDEX_COUNT: u32 = 15;
const LOW_FIRST_INDEX: u32 = 33;
const LOW_DRAW_OFFSET: u64 = 20;
const FAR_DRAW_OFFSET: u64 = 40;
const FAR_VERTEX_COUNT: u32 = 384;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct GrassUniforms {
view_projection: [[f32; 4]; 4],
frustum_planes: [[f32; 4]; 6],
camera_position: [f32; 4],
camera_right: [f32; 4],
camera_tile: [i32; 4],
sun_direction: [f32; 4],
sun_color: [f32; 4],
ambient: [f32; 4],
wind: [f32; 4],
radii: [f32; 4],
params: [f32; 4],
counts: [u32; 4],
domain: [f32; 4],
height_map: [f32; 4],
height_bounds: [f32; 4],
splat: [f32; 4],
pushers: [[f32; 4]; 8],
pusher_strength: [[f32; 4]; 2],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct GpuGrassType {
shape: [f32; 4],
curve: [f32; 4],
color_base: [f32; 4],
color_tip: [f32; 4],
misc: [f32; 4],
misc2: [f32; 4],
}
pub struct GrassPass {
uniforms_buffer: wgpu::Buffer,
types_buffer: wgpu::Buffer,
dispatch_buffer: wgpu::Buffer,
draw_commands_buffer: wgpu::Buffer,
blade_tile_buffer: wgpu::Buffer,
far_tile_buffer: wgpu::Buffer,
high_blade_buffer: wgpu::Buffer,
low_blade_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
cull_pipeline: wgpu::ComputePipeline,
generate_pipeline: wgpu::ComputePipeline,
high_pipeline: wgpu::RenderPipeline,
low_pipeline: wgpu::RenderPipeline,
far_pipeline: wgpu::RenderPipeline,
cull_bind_group: wgpu::BindGroup,
generate_bind_group_layout: wgpu::BindGroupLayout,
render_bind_group_layout: wgpu::BindGroupLayout,
generate_bind_group: wgpu::BindGroup,
high_bind_group: wgpu::BindGroup,
low_bind_group: wgpu::BindGroup,
height_texture: wgpu::Texture,
height_texture_view: wgpu::TextureView,
height_resolution: u32,
height_revision: Option<u64>,
#[cfg(feature = "terrain")]
terrain_share: Option<crate::ecs::terrain::TerrainShare>,
#[cfg(feature = "terrain")]
terrain_bound: bool,
types_revision: Option<u64>,
tile_dispatch_workgroups: u32,
time: f32,
active: bool,
}
impl GrassPass {
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Uniforms Buffer"),
size: std::mem::size_of::<GrassUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let types_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Types Buffer"),
size: (std::mem::size_of::<GpuGrassType>() * MAX_TYPES) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let dispatch_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Dispatch Buffer"),
size: 16,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let draw_commands_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Draw Commands Buffer"),
size: 64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let blade_tile_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Blade Tile Buffer"),
size: (MAX_BLADE_TILES as usize * 16) as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let far_tile_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Far Tile Buffer"),
size: (MAX_FAR_TILES as usize * 16) as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let high_blade_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass High Blade Buffer"),
size: (MAX_BLADES as usize * 64) as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let low_blade_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Grass Low Blade Buffer"),
size: (MAX_BLADES as usize * 64) as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let indices = build_indices();
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Grass Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let (height_texture, height_texture_view) = create_height_texture(device, 1);
let compute_stage = wgpu::ShaderStages::COMPUTE;
let cull_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Cull Bind Group Layout"),
entries: &[
buffer_layout_entry(0, compute_stage, wgpu::BufferBindingType::Uniform),
buffer_layout_entry(
2,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
buffer_layout_entry(
3,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
buffer_layout_entry(
4,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
buffer_layout_entry(
5,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
],
});
let cull_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Grass Cull Bind Group"),
layout: &cull_bind_group_layout,
entries: &[
bind_entry(0, &uniforms_buffer),
bind_entry(2, &dispatch_buffer),
bind_entry(3, &blade_tile_buffer),
bind_entry(4, &far_tile_buffer),
bind_entry(5, &draw_commands_buffer),
],
});
let generate_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Generate Bind Group Layout"),
entries: &[
buffer_layout_entry(0, compute_stage, wgpu::BufferBindingType::Uniform),
buffer_layout_entry(
1,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: true },
),
buffer_layout_entry(
3,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: true },
),
buffer_layout_entry(
5,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
buffer_layout_entry(
6,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
buffer_layout_entry(
7,
compute_stage,
wgpu::BufferBindingType::Storage { read_only: false },
),
texture_layout_entry(8, compute_stage),
],
});
let binding_resources = GrassBindingResources {
uniforms: &uniforms_buffer,
types: &types_buffer,
blade_tiles: &blade_tile_buffer,
draw_commands: &draw_commands_buffer,
high_blades: &high_blade_buffer,
low_blades: &low_blade_buffer,
far_tiles: &far_tile_buffer,
height_view: &height_texture_view,
};
let generate_bind_group =
create_generate_bind_group(device, &generate_bind_group_layout, &binding_resources);
let render_stage = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
let render_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Grass Render Bind Group Layout"),
entries: &[
buffer_layout_entry(0, render_stage, wgpu::BufferBindingType::Uniform),
buffer_layout_entry(
1,
render_stage,
wgpu::BufferBindingType::Storage { read_only: true },
),
buffer_layout_entry(
2,
render_stage,
wgpu::BufferBindingType::Storage { read_only: true },
),
buffer_layout_entry(
3,
render_stage,
wgpu::BufferBindingType::Storage { read_only: true },
),
texture_layout_entry(4, render_stage),
],
});
let high_bind_group = create_render_bind_group(
device,
&render_bind_group_layout,
&binding_resources,
&high_blade_buffer,
"Grass High Render Bind Group",
);
let low_bind_group = create_render_bind_group(
device,
&render_bind_group_layout,
&binding_resources,
&low_blade_buffer,
"Grass Low Render Bind Group",
);
let cull_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Grass Cull Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/grass_common.wgsl"),
include_str!("../../shaders/grass_cull.wgsl"),
),
);
let generate_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Grass Generate Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/grass_common.wgsl"),
include_str!("../../shaders/grass_generate.wgsl"),
),
);
let render_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Grass Render Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/grass_common.wgsl"),
include_str!("../../shaders/grass_render.wgsl"),
),
);
let far_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Grass Far Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/grass_common.wgsl"),
include_str!("../../shaders/grass_far.wgsl"),
),
);
let cull_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Cull Pipeline Layout"),
bind_group_layouts: &[Some(&cull_bind_group_layout)],
immediate_size: 0,
});
let generate_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Grass Generate Pipeline Layout"),
bind_group_layouts: &[Some(&generate_bind_group_layout)],
immediate_size: 0,
});
let cull_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Cull Pipeline"),
layout: Some(&cull_pipeline_layout),
module: &cull_shader,
entry_point: Some("cull_tiles"),
compilation_options: Default::default(),
cache: None,
});
let generate_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Grass Generate Pipeline"),
layout: Some(&generate_pipeline_layout),
module: &generate_shader,
entry_point: Some("generate_blades"),
compilation_options: Default::default(),
cache: 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 high_pipeline = create_render_pipeline(
device,
&render_pipeline_layout,
&render_shader,
"vs_high",
"fs_blade",
color_format,
"Grass High Pipeline",
);
let low_pipeline = create_render_pipeline(
device,
&render_pipeline_layout,
&render_shader,
"vs_low",
"fs_blade",
color_format,
"Grass Low Pipeline",
);
let far_pipeline = create_render_pipeline(
device,
&render_pipeline_layout,
&far_shader,
"vs_far",
"fs_far",
color_format,
"Grass Far Pipeline",
);
Self {
uniforms_buffer,
types_buffer,
dispatch_buffer,
draw_commands_buffer,
blade_tile_buffer,
far_tile_buffer,
high_blade_buffer,
low_blade_buffer,
index_buffer,
cull_pipeline,
generate_pipeline,
high_pipeline,
low_pipeline,
far_pipeline,
cull_bind_group,
generate_bind_group_layout,
render_bind_group_layout,
generate_bind_group,
high_bind_group,
low_bind_group,
height_texture,
height_texture_view,
height_resolution: 1,
height_revision: None,
#[cfg(feature = "terrain")]
terrain_share: None,
#[cfg(feature = "terrain")]
terrain_bound: false,
types_revision: None,
tile_dispatch_workgroups: 0,
time: 0.0,
active: false,
}
}
fn rebuild_height_bindings(
&mut self,
device: &wgpu::Device,
terrain_view: Option<&wgpu::TextureView>,
) {
let height_view = terrain_view.unwrap_or(&self.height_texture_view);
let binding_resources = GrassBindingResources {
uniforms: &self.uniforms_buffer,
types: &self.types_buffer,
blade_tiles: &self.blade_tile_buffer,
draw_commands: &self.draw_commands_buffer,
high_blades: &self.high_blade_buffer,
low_blades: &self.low_blade_buffer,
far_tiles: &self.far_tile_buffer,
height_view,
};
let generate_bind_group = create_generate_bind_group(
device,
&self.generate_bind_group_layout,
&binding_resources,
);
let high_bind_group = create_render_bind_group(
device,
&self.render_bind_group_layout,
&binding_resources,
&self.high_blade_buffer,
"Grass High Render Bind Group",
);
let low_bind_group = create_render_bind_group(
device,
&self.render_bind_group_layout,
&binding_resources,
&self.low_blade_buffer,
"Grass Low Render Bind Group",
);
self.generate_bind_group = generate_bind_group;
self.high_bind_group = high_bind_group;
self.low_bind_group = low_bind_group;
}
fn sync_height_map(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
let Some(height_map) = &world.resources.grass.height_map else {
return;
};
let resolution = height_map.resolution.max(1);
if height_map.heights.len() != (resolution * resolution) as usize {
return;
}
if resolution != self.height_resolution {
let (texture, view) = create_height_texture(device, resolution);
self.height_texture = texture;
self.height_texture_view = view;
self.height_resolution = resolution;
self.height_revision = None;
#[cfg(feature = "terrain")]
let rebuild = !self.terrain_bound;
#[cfg(not(feature = "terrain"))]
let rebuild = true;
if rebuild {
self.rebuild_height_bindings(device, None);
}
}
if self.height_revision != Some(height_map.revision) {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.height_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytemuck::cast_slice(&height_map.heights),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(resolution * 4),
rows_per_image: Some(resolution),
},
wgpu::Extent3d {
width: resolution,
height: resolution,
depth_or_array_layers: 1,
},
);
self.height_revision = Some(height_map.revision);
}
}
}
fn create_height_texture(
device: &wgpu::Device,
resolution: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Grass Height Texture"),
size: wgpu::Extent3d {
width: resolution,
height: resolution,
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 view = texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
(texture, view)
}
fn texture_layout_entry(
binding: u32,
visibility: wgpu::ShaderStages,
) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2Array,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
}
}
struct GrassBindingResources<'resources> {
uniforms: &'resources wgpu::Buffer,
types: &'resources wgpu::Buffer,
blade_tiles: &'resources wgpu::Buffer,
draw_commands: &'resources wgpu::Buffer,
high_blades: &'resources wgpu::Buffer,
low_blades: &'resources wgpu::Buffer,
far_tiles: &'resources wgpu::Buffer,
height_view: &'resources wgpu::TextureView,
}
fn create_generate_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
resources: &GrassBindingResources<'_>,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Grass Generate Bind Group"),
layout,
entries: &[
bind_entry(0, resources.uniforms),
bind_entry(1, resources.types),
bind_entry(3, resources.blade_tiles),
bind_entry(5, resources.draw_commands),
bind_entry(6, resources.high_blades),
bind_entry(7, resources.low_blades),
wgpu::BindGroupEntry {
binding: 8,
resource: wgpu::BindingResource::TextureView(resources.height_view),
},
],
})
}
fn create_render_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
resources: &GrassBindingResources<'_>,
blade_buffer: &wgpu::Buffer,
label: &str,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(label),
layout,
entries: &[
bind_entry(0, resources.uniforms),
bind_entry(1, resources.types),
bind_entry(2, blade_buffer),
bind_entry(3, resources.far_tiles),
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(resources.height_view),
},
],
})
}
fn buffer_layout_entry(
binding: u32,
visibility: wgpu::ShaderStages,
ty: wgpu::BufferBindingType,
) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility,
ty: wgpu::BindingType::Buffer {
ty,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn bind_entry(binding: u32, buffer: &wgpu::Buffer) -> wgpu::BindGroupEntry<'_> {
wgpu::BindGroupEntry {
binding,
resource: buffer.as_entire_binding(),
}
}
fn create_render_pipeline(
device: &wgpu::Device,
layout: &wgpu::PipelineLayout,
shader: &wgpu::ShaderModule,
vertex_entry: &str,
fragment_entry: &str,
color_format: wgpu::TextureFormat,
label: &str,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(layout),
vertex: wgpu::VertexState {
module: shader,
entry_point: Some(vertex_entry),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: shader,
entry_point: Some(fragment_entry),
targets: &[Some(wgpu::ColorTargetState {
format: color_format,
blend: None,
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: 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,
})
}
fn build_indices() -> [u32; 48] {
[
0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, 4, 5, 6, 6, 5, 7, 8, 9, 10, 10, 9, 11, 10, 11, 12, 12,
11, 13, 12, 13, 14, 0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, 4, 5, 6,
]
}
pub(crate) fn gpu_types(settings: &GrassSettings) -> [GpuGrassType; MAX_TYPES] {
let mut output = [GpuGrassType::default(); MAX_TYPES];
for (slot, params) in output.iter_mut().zip(settings.types.iter()) {
*slot = GpuGrassType {
shape: [
params.height_min,
params.height_max,
params.width,
params.density,
],
curve: [
params.tilt_min,
params.tilt_max,
params.bend_min,
params.bend_max,
],
color_base: params.color_base,
color_tip: params.color_tip,
misc: [
params.gloss,
params.wind_response,
params.clump_height_variance,
params.clump_pull,
],
misc2: [params.clump_face_away, params.vertex_distribution, 0.0, 0.0],
};
}
output
}
fn query_sun(world: &World) -> (Vec3, Vec3) {
for entity in world
.core
.query_entities(crate::ecs::world::LIGHT | crate::ecs::world::GLOBAL_TRANSFORM)
{
if let (Some(light), Some(transform)) = (
world.core.get_light(entity),
world.core.get_global_transform(entity),
) && light.light_type == crate::ecs::light::components::LightType::Directional
{
let toward_light = -transform.forward_vector();
let color = light.color * light.intensity;
return (toward_light.normalize(), color);
}
}
(
Vec3::new(0.35, 0.8, 0.35).normalize(),
Vec3::new(4.0, 3.8, 3.2),
)
}
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) {
let settings = &world.resources.grass;
self.active = settings.enabled && !settings.types.is_empty();
if !self.active {
return;
}
self.sync_height_map(device, queue, world);
#[cfg(feature = "terrain")]
let terrain_sample = {
if self.terrain_share.is_none() {
self.terrain_share = Some(world.resources.terrain_render.share.clone());
}
let terrain_enabled = world.resources.terrain.enabled;
let terrain_mode = self
.terrain_share
.as_ref()
.expect("terrain share")
.clone()
.with(|shared| {
if terrain_enabled && shared.cache_ready {
shared
.cache_view
.clone()
.map(|view| (view, shared.grass_texel_size))
} else {
None
}
});
match &terrain_mode {
Some((view, _)) => {
if !self.terrain_bound {
let view = view.clone();
self.rebuild_height_bindings(device, Some(&view));
self.terrain_bound = true;
}
}
None => {
if self.terrain_bound {
self.rebuild_height_bindings(device, None);
self.terrain_bound = false;
}
}
}
terrain_mode.map(|(_, texel_size)| {
(
[0.0, 0.0, texel_size, 2.0],
[
world.resources.terrain.height_min,
world.resources.terrain.height_max,
crate::ecs::terrain::TERRAIN_GRASS_LEVEL as f32,
(world
.resources
.terrain
.level_count
.clamp(2, crate::ecs::terrain::TERRAIN_COLLIDER_LAYER)
- 1) as f32,
],
)
})
};
#[cfg(not(feature = "terrain"))]
let terrain_sample: Option<([f32; 4], [f32; 4])> = None;
#[cfg(feature = "terrain")]
let terrain_splat = [
world.resources.terrain.rock_slope,
world.resources.terrain.snow_height,
0.0,
0.0,
];
#[cfg(not(feature = "terrain"))]
let terrain_splat = [0.0f32; 4];
let settings = &world.resources.grass;
let Some(camera_matrices) =
crate::ecs::camera::queries::query_active_camera_matrices(world)
else {
self.active = false;
return;
};
self.time += world.resources.window.timing.delta_time;
let view: Mat4 = camera_matrices.view;
let projection: Mat4 = camera_matrices.projection;
let view_projection = projection * view;
let frustum_planes = super::extract_frustum_planes(&view_projection);
let view_inverse = nalgebra_glm::inverse(&view);
let camera_position = Vec3::new(
view_inverse[(0, 3)],
view_inverse[(1, 3)],
view_inverse[(2, 3)],
);
let camera_right = Vec3::new(
view_inverse[(0, 0)],
view_inverse[(1, 0)],
view_inverse[(2, 0)],
);
let camera_tile_x = (camera_position.x / TILE_SIZE).floor() as i32;
let camera_tile_z = (camera_position.z / TILE_SIZE).floor() as i32;
let half_span = (settings.far_radius / TILE_SIZE).ceil() as i32 + 4;
let tiles_per_side = half_span * 2 + 1;
let total_tiles = (tiles_per_side * tiles_per_side) as u32;
self.tile_dispatch_workgroups = total_tiles.div_ceil(64);
let (sun_direction, sun_color) = query_sun(world);
let ambient = world.resources.render_settings.ambient_light;
let wind = &world.resources.wind;
let wind_planar = nalgebra_glm::Vec2::new(wind.direction.x, wind.direction.z);
let wind_direction = if wind_planar.norm() > f32::EPSILON {
wind_planar.normalize()
} else {
nalgebra_glm::Vec2::new(1.0, 0.0)
};
let (finite_flag, domain_rect) = match settings.domain {
GrassDomain::Finite {
center,
half_extents,
} => (
1u32,
[
center.x - half_extents.x,
center.y - half_extents.y,
center.x + half_extents.x,
center.y + half_extents.y,
],
),
GrassDomain::Infinite => (0u32, [0.0; 4]),
};
let mut pushers = [[0.0f32; 4]; MAX_PUSHERS];
let mut pusher_strength = [[0.0f32; 4]; 2];
let pusher_count = settings.pushers.len().min(MAX_PUSHERS);
for (index, pusher) in settings.pushers.iter().take(MAX_PUSHERS).enumerate() {
pushers[index] = [
pusher.position.x,
pusher.position.y,
pusher.position.z,
pusher.radius,
];
pusher_strength[index / 4][index % 4] = pusher.strength;
}
let uniforms = GrassUniforms {
view_projection: view_projection.into(),
frustum_planes: frustum_planes.map(|plane| [plane.x, plane.y, plane.z, plane.w]),
camera_position: [camera_position.x, camera_position.y, camera_position.z, 0.0],
camera_right: [camera_right.x, camera_right.y, camera_right.z, 0.0],
camera_tile: [camera_tile_x, camera_tile_z, tiles_per_side, half_span],
sun_direction: [sun_direction.x, sun_direction.y, sun_direction.z, 0.0],
sun_color: [sun_color.x, sun_color.y, sun_color.z, 0.0],
ambient: [
ambient[0] * ambient[3],
ambient[1] * ambient[3],
ambient[2] * ambient[3],
0.0,
],
wind: [
wind_direction.x,
wind_direction.y,
wind.strength * 0.12,
wind.turbulence.clamp(0.0, 1.0),
],
radii: [
settings.high_radius,
settings.low_radius,
settings.far_radius,
settings.ground_height,
],
params: [
settings.type_noise_scale.max(1.0),
settings.clump_scale.max(0.2),
0.0,
self.time,
],
counts: [
settings.types.len().min(MAX_TYPES) as u32,
pusher_count as u32,
finite_flag,
u32::from(terrain_sample.is_none()),
],
domain: domain_rect,
height_map: match (&terrain_sample, &settings.height_map) {
(Some((height_map, _)), _) => *height_map,
(None, Some(height_map)) => [
height_map.origin.x,
height_map.origin.y,
height_map.world_span.max(1.0),
1.0,
],
(None, None) => [0.0; 4],
},
height_bounds: match (&terrain_sample, &settings.height_map) {
(Some((_, bounds)), _) => *bounds,
(None, Some(height_map)) => {
[height_map.height_min, height_map.height_max, 0.0, 0.0]
}
(None, None) => [0.0; 4],
},
splat: terrain_splat,
pushers,
pusher_strength,
};
queue.write_buffer(&self.uniforms_buffer, 0, bytemuck::bytes_of(&uniforms));
if self.types_revision != Some(settings.types_revision) {
let types = gpu_types(settings);
queue.write_buffer(&self.types_buffer, 0, bytemuck::cast_slice(&types));
self.types_revision = Some(settings.types_revision);
}
queue.write_buffer(
&self.dispatch_buffer,
0,
bytemuck::cast_slice(&[0u32, 1, 1, 0]),
);
queue.write_buffer(
&self.draw_commands_buffer,
0,
bytemuck::cast_slice(&[
HIGH_INDEX_COUNT,
0,
0,
0,
0,
LOW_INDEX_COUNT,
0,
LOW_FIRST_INDEX,
0,
0,
FAR_VERTEX_COUNT,
0,
0,
0,
0,
0,
]),
);
}
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() || !self.active {
return Ok(context.into_sub_graph_commands());
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Grass Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.cull_pipeline);
compute_pass.set_bind_group(0, &self.cull_bind_group, &[]);
compute_pass.dispatch_workgroups(self.tile_dispatch_workgroups, 1, 1);
compute_pass.set_pipeline(&self.generate_pipeline);
compute_pass.set_bind_group(0, &self.generate_bind_group, &[]);
compute_pass.dispatch_workgroups_indirect(&self.dispatch_buffer, 0);
}
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_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.set_pipeline(&self.high_pipeline);
render_pass.set_bind_group(0, &self.high_bind_group, &[]);
render_pass.draw_indexed_indirect(&self.draw_commands_buffer, 0);
render_pass.set_pipeline(&self.low_pipeline);
render_pass.set_bind_group(0, &self.low_bind_group, &[]);
render_pass.draw_indexed_indirect(&self.draw_commands_buffer, LOW_DRAW_OFFSET);
render_pass.set_pipeline(&self.far_pipeline);
render_pass.set_bind_group(0, &self.high_bind_group, &[]);
render_pass.draw_indirect(&self.draw_commands_buffer, FAR_DRAW_OFFSET);
}
Ok(context.into_sub_graph_commands())
}
}