use super::grass::{GpuGrassType, MAX_TYPES, gpu_types};
use super::projection::calculate_cascade_shadows;
use crate::ecs::terrain::{
TERRAIN_CACHE_SIZE, TERRAIN_COLLIDER_LAYER, TERRAIN_GRASS_LEVEL, TerrainShare, TerrainSnapshot,
TerrainSnapshotState,
};
use crate::ecs::world::World;
use crate::render::wgpu::rendergraph::{PassExecutionContext, PassNode};
use nalgebra_glm::{Mat4, Vec3};
use wgpu::util::DeviceExt;
const MAX_LEVELS: usize = 12;
const TILE_TEXELS: i32 = 64;
const BLOCK_CELLS: u32 = 32;
const BLOCK_INDEX_COUNT: u32 = BLOCK_CELLS * BLOCK_CELLS * 6;
const MAX_JOBS: usize = 1024;
const MAX_INSTANCES: usize = 512;
const SNAPSHOT_BYTES: u64 = (TERRAIN_CACHE_SIZE as u64) * (TERRAIN_CACHE_SIZE as u64) * 4;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct TerrainProduceUniforms {
continental_spline: [[f32; 4]; 4],
erosion_spline: [[f32; 4]; 4],
frequencies: [f32; 4],
warp_detail: [f32; 4],
erosion_params: [f32; 4],
seeds: [u32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct TerrainRenderUniforms {
view_projection: [[f32; 4]; 4],
camera_position: [f32; 4],
sun_direction: [f32; 4],
sun_color: [f32; 4],
ambient: [f32; 4],
params: [f32; 4],
splat: [f32; 4],
rock_color: [f32; 4],
snow_color: [f32; 4],
wind_time: [f32; 4],
frustum_planes: [[f32; 4]; 6],
level_regions: [[f32; 4]; 12],
cascade_view_projections: [[[f32; 4]; 4]; 4],
cascade_splits: [f32; 4],
cascade_atlas_offsets: [[f32; 4]; 4],
cascade_atlas_scale: [f32; 4],
}
pub struct TerrainPass {
produce_uniforms_buffer: wgpu::Buffer,
render_uniforms_buffer: wgpu::Buffer,
types_buffer: wgpu::Buffer,
jobs_buffer: wgpu::Buffer,
instances_buffer: wgpu::Buffer,
index_buffer: wgpu::Buffer,
cache_view: wgpu::TextureView,
cache_texture: wgpu::Texture,
ambient_view: wgpu::TextureView,
shadow_sampler: wgpu::Sampler,
draw_indirect_buffer: wgpu::Buffer,
produce_pipeline: wgpu::ComputePipeline,
ambient_pipeline: wgpu::ComputePipeline,
cull_pipeline: wgpu::ComputePipeline,
render_pipeline: wgpu::RenderPipeline,
produce_bind_group: wgpu::BindGroup,
ambient_bind_group: wgpu::BindGroup,
cull_bind_group: wgpu::BindGroup,
render_bind_group_layout: wgpu::BindGroupLayout,
render_bind_group: Option<wgpu::BindGroup>,
share: Option<TerrainShare>,
level_windows: Vec<Option<[i32; 2]>>,
collider_window: Option<[i32; 2]>,
pending_jobs: Vec<[i32; 4]>,
job_count: u32,
settings_revision: Option<u64>,
grass_types_revision: Option<u64>,
encode_snapshot: Option<([i32; 2], u64)>,
time: f32,
active: bool,
}
impl TerrainPass {
pub fn new(device: &wgpu::Device, color_format: wgpu::TextureFormat) -> Self {
let produce_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Produce Uniforms"),
size: std::mem::size_of::<TerrainProduceUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let render_uniforms_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Render Uniforms"),
size: std::mem::size_of::<TerrainRenderUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let types_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain 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 jobs_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Jobs Buffer"),
size: (MAX_JOBS * 16) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let instances_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Instances Buffer"),
size: (MAX_INSTANCES * 16) as u64,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let draw_indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Draw Indirect Buffer"),
size: 20,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let indices = build_block_indices();
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Terrain Index Buffer"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let cache_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Terrain Height Cache"),
size: wgpu::Extent3d {
width: TERRAIN_CACHE_SIZE,
height: TERRAIN_CACHE_SIZE,
depth_or_array_layers: MAX_LEVELS as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::STORAGE_BINDING
| wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let cache_view = cache_texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
let ambient_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Terrain Ambient Cache"),
size: wgpu::Extent3d {
width: TERRAIN_CACHE_SIZE,
height: TERRAIN_CACHE_SIZE,
depth_or_array_layers: MAX_LEVELS as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::R32Float,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let ambient_view = ambient_texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Terrain Shadow Sampler"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let produce_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Terrain Produce 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::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: wgpu::TextureFormat::R32Float,
view_dimension: wgpu::TextureViewDimension::D2Array,
},
count: None,
},
],
});
let produce_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Terrain Produce Bind Group"),
layout: &produce_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: produce_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: jobs_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&cache_view),
},
],
});
let ambient_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Terrain Ambient 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 {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2Array,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::StorageTexture {
access: wgpu::StorageTextureAccess::WriteOnly,
format: wgpu::TextureFormat::R32Float,
view_dimension: wgpu::TextureViewDimension::D2Array,
},
count: None,
},
],
});
let ambient_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Terrain Ambient Bind Group"),
layout: &ambient_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: produce_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: jobs_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&cache_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: wgpu::BindingResource::TextureView(&ambient_view),
},
],
});
let render_stage = wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT;
let render_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Terrain Render Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: render_stage,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: render_stage,
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: render_stage,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2Array,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::FRAGMENT,
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::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2Array,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 5,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Depth,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 6,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering),
count: None,
},
],
});
let produce_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Terrain Produce Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/terrain_produce.wgsl"),
),
);
let render_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Terrain Render Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/grass_common.wgsl"),
include_str!("../../shaders/terrain_render.wgsl"),
),
);
let produce_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Terrain Produce Pipeline Layout"),
bind_group_layouts: &[Some(&produce_bind_group_layout)],
immediate_size: 0,
});
let produce_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Terrain Produce Pipeline"),
layout: Some(&produce_pipeline_layout),
module: &produce_shader,
entry_point: Some("produce_tiles"),
compilation_options: Default::default(),
cache: None,
});
let ambient_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Terrain Ambient Shader",
concat!(
include_str!("../../shaders/terrain_common.wgsl"),
include_str!("../../shaders/terrain_ao.wgsl"),
),
);
let ambient_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Terrain Ambient Pipeline Layout"),
bind_group_layouts: &[Some(&ambient_bind_group_layout)],
immediate_size: 0,
});
let ambient_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Terrain Ambient Pipeline"),
layout: Some(&ambient_pipeline_layout),
module: &ambient_shader,
entry_point: Some("produce_ambient"),
compilation_options: Default::default(),
cache: None,
});
let cull_shader = crate::render::wgpu::shader_compose::compile_wgsl(
device,
"Terrain Cull Shader",
include_str!("../../shaders/terrain_cull.wgsl"),
);
let cull_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Terrain Cull 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: false },
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,
},
],
});
let cull_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Terrain Cull Bind Group"),
layout: &cull_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: render_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: instances_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: draw_indirect_buffer.as_entire_binding(),
},
],
});
let cull_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Terrain Cull Pipeline Layout"),
bind_group_layouts: &[Some(&cull_bind_group_layout)],
immediate_size: 0,
});
let cull_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("Terrain Cull Pipeline"),
layout: Some(&cull_pipeline_layout),
module: &cull_shader,
entry_point: Some("cull_blocks"),
compilation_options: Default::default(),
cache: None,
});
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Terrain 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("Terrain Render Pipeline"),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &render_shader,
entry_point: Some("vs_terrain"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &render_shader,
entry_point: Some("fs_terrain"),
targets: &[
Some(wgpu::ColorTargetState {
format: color_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
}),
Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::R32Float,
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,
});
Self {
produce_uniforms_buffer,
render_uniforms_buffer,
types_buffer,
jobs_buffer,
instances_buffer,
index_buffer,
cache_view,
cache_texture,
ambient_view,
shadow_sampler,
draw_indirect_buffer,
produce_pipeline,
ambient_pipeline,
cull_pipeline,
render_pipeline,
produce_bind_group,
ambient_bind_group,
cull_bind_group,
render_bind_group_layout,
render_bind_group: None,
share: None,
level_windows: vec![None; MAX_LEVELS],
collider_window: None,
pending_jobs: Vec::new(),
job_count: 0,
settings_revision: None,
grass_types_revision: None,
encode_snapshot: None,
time: 0.0,
active: false,
}
}
}
fn build_block_indices() -> Vec<u32> {
let mut indices = Vec::with_capacity(BLOCK_INDEX_COUNT as usize);
for cell_z in 0..BLOCK_CELLS {
for cell_x in 0..BLOCK_CELLS {
let top_left = cell_z * (BLOCK_CELLS + 1) + cell_x;
let top_right = top_left + 1;
let bottom_left = top_left + BLOCK_CELLS + 1;
let bottom_right = bottom_left + 1;
indices.extend_from_slice(&[
top_left,
bottom_left,
top_right,
top_right,
bottom_left,
bottom_right,
]);
}
}
indices
}
fn pack_spline(points: &[[f32; 2]; 8]) -> [[f32; 4]; 4] {
let mut output = [[0.0f32; 4]; 4];
for (index, point) in points.iter().enumerate() {
let slot = &mut output[index / 2];
let lane = (index % 2) * 2;
slot[lane] = point[0];
slot[lane + 1] = point[1];
}
output
}
type DirectionalLight = (
crate::ecs::light::components::Light,
crate::ecs::transform::components::GlobalTransform,
);
fn query_sun(world: &World) -> (Vec3, Vec3, Option<DirectionalLight>) {
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;
let directional = if light.cast_shadows {
Some((light.clone(), *transform))
} else {
None
};
return (toward_light.normalize(), color, directional);
}
}
(
Vec3::new(0.35, 0.8, 0.35).normalize(),
Vec3::new(4.0, 3.8, 3.2),
None,
)
}
impl PassNode<World> for TerrainPass {
fn name(&self) -> &str {
"terrain_pass"
}
fn reads(&self) -> Vec<&str> {
vec!["shadow_depth"]
}
fn writes(&self) -> Vec<&str> {
vec![]
}
fn reads_writes(&self) -> Vec<&str> {
vec!["color", "depth", "entity_id"]
}
fn invalidate_bind_groups(&mut self) {
self.render_bind_group = None;
}
fn prepare(&mut self, _device: &wgpu::Device, queue: &wgpu::Queue, world: &World) {
if self.share.is_none() {
self.share = Some(world.resources.terrain_render.share.clone());
}
let share = self.share.as_ref().expect("terrain share").clone();
share.with(|shared| {
if let Some(snapshot) = shared.snapshot.as_mut()
&& matches!(snapshot.state, TerrainSnapshotState::Encoded)
{
let map_done = snapshot.map_done.clone();
snapshot
.buffer
.slice(..)
.map_async(wgpu::MapMode::Read, move |_| {
map_done.store(true, std::sync::atomic::Ordering::Relaxed);
});
snapshot.state = TerrainSnapshotState::Mapping;
}
});
let settings = &world.resources.terrain;
self.active = settings.enabled;
if !self.active {
share.with(|shared| {
shared.cache_ready = false;
});
self.encode_snapshot = None;
return;
}
let Some(camera_matrices) =
crate::ecs::camera::queries::query_active_camera_matrices(world)
else {
self.active = false;
return;
};
if self.settings_revision != Some(settings.revision) {
self.level_windows = vec![None; MAX_LEVELS];
self.collider_window = None;
self.pending_jobs.clear();
self.settings_revision = Some(settings.revision);
let produce_uniforms = TerrainProduceUniforms {
continental_spline: pack_spline(&settings.continental_spline),
erosion_spline: pack_spline(&settings.erosion_spline),
frequencies: [
settings.continental_frequency,
settings.erosion_frequency,
settings.ridge_frequency,
settings.warp_frequency,
],
warp_detail: [
settings.warp_strength,
settings.detail_amplitude,
settings.texel_size,
settings.origin_flatten_radius,
],
erosion_params: [settings.erosion_sharpness, 0.0, 0.0, 0.0],
seeds: [settings.seed, 0, 0, 0],
};
queue.write_buffer(
&self.produce_uniforms_buffer,
0,
bytemuck::bytes_of(&produce_uniforms),
);
}
let view: Mat4 = camera_matrices.view;
let projection: Mat4 = camera_matrices.projection;
let view_projection = projection * view;
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 level_count = (settings.level_count as usize).clamp(2, TERRAIN_COLLIDER_LAYER as usize);
let cache_size = TERRAIN_CACHE_SIZE as i32;
let half_cache = cache_size / 2;
for level in 0..level_count {
let texel = settings.texel_size * (1u32 << level) as f32;
let tile_world = texel * TILE_TEXELS as f32;
let window_origin = [
(camera_position.x / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
(camera_position.z / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
];
let previous = self.level_windows[level];
if previous == Some(window_origin) {
continue;
}
for tile_z in 0..(cache_size / TILE_TEXELS) {
for tile_x in 0..(cache_size / TILE_TEXELS) {
let origin = [
window_origin[0] + tile_x * TILE_TEXELS,
window_origin[1] + tile_z * TILE_TEXELS,
];
let covered = previous.is_some_and(|old| {
origin[0] >= old[0]
&& origin[1] >= old[1]
&& origin[0] + TILE_TEXELS <= old[0] + cache_size
&& origin[1] + TILE_TEXELS <= old[1] + cache_size
});
if !covered {
self.pending_jobs
.push([origin[0], origin[1], level as i32, level as i32]);
}
}
}
self.level_windows[level] = Some(window_origin);
}
let collider_request = share.with(|shared| {
if shared.snapshot.is_none() {
shared.snapshot_request.take()
} else {
None
}
});
self.encode_snapshot = None;
if let Some(request_center) = collider_request {
let grass_texel = settings.texel_size * (1u32 << TERRAIN_GRASS_LEVEL) as f32;
let tile_world = grass_texel * TILE_TEXELS as f32;
let window_origin = [
(request_center[0] / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
(request_center[1] / tile_world).floor() as i32 * TILE_TEXELS - half_cache,
];
if self.collider_window != Some(window_origin) {
let previous = self.collider_window;
for tile_z in 0..(cache_size / TILE_TEXELS) {
for tile_x in 0..(cache_size / TILE_TEXELS) {
let origin = [
window_origin[0] + tile_x * TILE_TEXELS,
window_origin[1] + tile_z * TILE_TEXELS,
];
let covered = previous.is_some_and(|old| {
origin[0] >= old[0]
&& origin[1] >= old[1]
&& origin[0] + TILE_TEXELS <= old[0] + cache_size
&& origin[1] + TILE_TEXELS <= old[1] + cache_size
});
if !covered {
self.pending_jobs.push([
origin[0],
origin[1],
TERRAIN_GRASS_LEVEL as i32,
TERRAIN_COLLIDER_LAYER as i32,
]);
}
}
}
self.collider_window = Some(window_origin);
}
self.encode_snapshot = Some((window_origin, settings.revision));
}
let job_count = self.pending_jobs.len().min(MAX_JOBS);
if job_count > 0 {
let jobs: Vec<[i32; 4]> = self.pending_jobs.drain(..job_count).collect();
queue.write_buffer(&self.jobs_buffer, 0, bytemuck::cast_slice(&jobs));
}
self.job_count = job_count as u32;
let mut level_regions = [[0.0f32; 4]; 12];
for (level, region) in level_regions.iter_mut().enumerate().take(level_count) {
let block_world =
(settings.texel_size * (1u32 << level) as f32 * BLOCK_CELLS as f32) as f64;
let snap = block_world * 2.0;
let origin = [
(camera_position.x as f64 / snap).floor() * snap - snap,
(camera_position.z as f64 / snap).floor() * snap - snap,
];
let half_extent = snap * 1.5;
*region = [
(origin[0] + half_extent) as f32,
(origin[1] + half_extent) as f32,
half_extent as f32,
0.0,
];
}
let frustum_planes = super::extract_frustum_planes(&view_projection);
queue.write_buffer(
&self.draw_indirect_buffer,
0,
bytemuck::cast_slice(&[BLOCK_INDEX_COUNT, 0, 0, 0, 0]),
);
self.time += world.resources.window.timing.delta_time;
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 (sun_direction, sun_color, directional_light) = query_sun(world);
let ambient = world.resources.render_settings.ambient_light;
let shadows_wanted = world
.resources
.renderer_state
.active_view
.shadow_depth_enabled
&& directional_light.is_some();
let mut cascade_view_projections = [[[0.0f32; 4]; 4]; 4];
let mut cascade_splits = [0.0f32; 4];
let mut cascade_atlas_offsets = [[0.0f32; 4]; 4];
let mut shadows_enabled = 0.0f32;
let mut shadow_bias = 0.0f32;
if shadows_wanted {
let cascade_result = calculate_cascade_shadows(world, directional_light.as_ref());
cascade_view_projections = cascade_result.cascade_view_projections;
cascade_splits = cascade_result.cascade_split_distances;
shadow_bias = cascade_result.shadow_bias;
shadows_enabled = cascade_result.shadows_enabled;
let slot_resolution = crate::render::wgpu::passes::CASCADE_SLOT_RESOLUTION;
let slots = [[0.0, 0.0], [0.5, 0.0], [0.0, 0.5], [0.5, 0.5]];
for cascade_index in 0..4 {
cascade_atlas_offsets[cascade_index] = [
slots[cascade_index][0],
slots[cascade_index][1],
cascade_result.cascade_diameters[cascade_index] / slot_resolution,
0.0,
];
}
}
let render_uniforms = TerrainRenderUniforms {
view_projection: view_projection.into(),
camera_position: [camera_position.x, camera_position.y, camera_position.z, 0.0],
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,
],
params: [
settings.texel_size,
level_count as f32,
settings.height_min,
settings.height_max,
],
splat: [
settings.rock_slope,
settings.snow_height,
world.resources.grass.types.len().min(MAX_TYPES) as f32,
world.resources.grass.type_noise_scale.max(1.0),
],
rock_color: [
settings.rock_color[0],
settings.rock_color[1],
settings.rock_color[2],
world.resources.grass.clump_scale.max(0.2),
],
snow_color: settings.snow_color,
wind_time: [
wind_direction.x,
wind_direction.y,
wind.strength * 0.12,
self.time,
],
frustum_planes: frustum_planes.map(|plane| [plane.x, plane.y, plane.z, plane.w]),
level_regions,
cascade_view_projections,
cascade_splits,
cascade_atlas_offsets,
cascade_atlas_scale: [0.5, 0.5, shadows_enabled, shadow_bias],
};
queue.write_buffer(
&self.render_uniforms_buffer,
0,
bytemuck::bytes_of(&render_uniforms),
);
if self.grass_types_revision != Some(world.resources.grass.types_revision) {
let types = gpu_types(&world.resources.grass);
queue.write_buffer(&self.types_buffer, 0, bytemuck::cast_slice(&types));
self.grass_types_revision = Some(world.resources.grass.types_revision);
}
let grass_level = TERRAIN_GRASS_LEVEL as usize;
let grass_origin = self.level_windows[grass_level].unwrap_or([0, 0]);
let grass_texel_size = settings.texel_size * (1u32 << TERRAIN_GRASS_LEVEL) as f32;
let cache_view = self.cache_view.clone();
share.with(|shared| {
if shared.cache_view.is_none() {
shared.cache_view = Some(cache_view);
}
shared.cache_ready = true;
shared.grass_origin_texels = grass_origin;
shared.grass_texel_size = grass_texel_size;
});
}
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());
}
if self.job_count > 0 {
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Terrain Produce Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(&self.produce_pipeline);
compute_pass.set_bind_group(0, &self.produce_bind_group, &[]);
compute_pass.dispatch_workgroups(8, 8, self.job_count);
compute_pass.set_pipeline(&self.ambient_pipeline);
compute_pass.set_bind_group(0, &self.ambient_bind_group, &[]);
compute_pass.dispatch_workgroups(8, 8, self.job_count);
}
{
let mut compute_pass =
context
.encoder
.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Terrain Cull 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((12 * 36u32).div_ceil(64), 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 (entity_view, entity_load, entity_store) = context.get_color_attachment("entity_id")?;
if self.render_bind_group.is_none() {
let shadow_view = context.get_texture_view("shadow_depth")?;
self.render_bind_group = Some(context.device.create_bind_group(
&wgpu::BindGroupDescriptor {
label: Some("Terrain Render Bind Group"),
layout: &self.render_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: self.render_uniforms_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: self.instances_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(&self.cache_view),
},
wgpu::BindGroupEntry {
binding: 3,
resource: self.types_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: wgpu::BindingResource::TextureView(&self.ambient_view),
},
wgpu::BindGroupEntry {
binding: 5,
resource: wgpu::BindingResource::TextureView(shadow_view),
},
wgpu::BindGroupEntry {
binding: 6,
resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
},
],
},
));
}
{
let mut render_pass = context
.encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Terrain 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,
}),
Some(wgpu::RenderPassColorAttachment {
view: entity_view,
resolve_target: None,
ops: wgpu::Operations {
load: entity_load,
store: entity_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.as_ref(), &[]);
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed_indirect(&self.draw_indirect_buffer, 0);
}
if let Some((origin, revision)) = self.encode_snapshot.take()
&& let Some(share) = self.share.as_ref()
{
let buffer = context.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Terrain Snapshot Buffer"),
size: SNAPSHOT_BYTES,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
context.encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &self.cache_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: TERRAIN_COLLIDER_LAYER,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(TERRAIN_CACHE_SIZE * 4),
rows_per_image: Some(TERRAIN_CACHE_SIZE),
},
},
wgpu::Extent3d {
width: TERRAIN_CACHE_SIZE,
height: TERRAIN_CACHE_SIZE,
depth_or_array_layers: 1,
},
);
share.with(|shared| {
shared.snapshot = Some(TerrainSnapshot {
buffer,
map_done: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
origin_texels: origin,
texel_size: shared.grass_texel_size,
revision,
state: TerrainSnapshotState::Encoded,
});
});
}
Ok(context.into_sub_graph_commands())
}
}