From c33964b4ce0fff8d6ee32106b37e9f6ba83808af Mon Sep 17 00:00:00 2001 From: stainlu Date: Thu, 9 Apr 2026 01:46:12 +0800 Subject: [PATCH] Wire GPU terrain generation into renderer with demo example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrates the existing GpuTerrainGenerator compute pipeline into the rendering system so terrain chunks can be generated entirely on the GPU. - Add Renderer::register_gpu_mesh() to accept pre-built GPU buffers - Add GpuTerrainGenerator::generate_chunk() high-level wrapper - Re-export GPU terrain types under the gpu-terrain feature gate - Add gpu_terrain_demo example demonstrating full pipeline: heightmap → chunks → GPU compute dispatch → render Co-Authored-By: Claude Sonnet 4.6 --- crates/euca-game/Cargo.toml | 6 + crates/euca-render/src/renderer.rs | 43 ++++ crates/euca-terrain/src/gpu_terrain.rs | 64 ++++++ crates/euca-terrain/src/lib.rs | 4 + examples/gpu_terrain_demo.rs | 288 +++++++++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 examples/gpu_terrain_demo.rs diff --git a/crates/euca-game/Cargo.toml b/crates/euca-game/Cargo.toml index ba2e0b1..670609b 100644 --- a/crates/euca-game/Cargo.toml +++ b/crates/euca-game/Cargo.toml @@ -9,6 +9,7 @@ categories = ["game-development"] [features] default = [] metal-native = ["euca-render/metal-native"] +gpu-terrain = ["euca-terrain/gpu-terrain"] [dependencies] euca-ecs = { path = "../euca-ecs" } @@ -65,6 +66,11 @@ path = "../../examples/tiled_level.rs" name = "gpu_benchmark" path = "../../examples/gpu_benchmark.rs" +[[example]] +name = "gpu_terrain_demo" +path = "../../examples/gpu_terrain_demo.rs" +required-features = ["gpu-terrain"] + [[example]] name = "particle_demo" path = "../../examples/particle_demo.rs" diff --git a/crates/euca-render/src/renderer.rs b/crates/euca-render/src/renderer.rs index b2fe66d..7bfbe11 100644 --- a/crates/euca-render/src/renderer.rs +++ b/crates/euca-render/src/renderer.rs @@ -1443,6 +1443,49 @@ impl Renderer { }); handle } + + /// Register pre-built GPU buffers as a renderable mesh. + /// + /// Unlike [`upload_mesh`](Self::upload_mesh), which copies CPU-side vertex + /// and index data to the GPU, this method adopts buffers that already exist + /// on the GPU — for example, output buffers from a compute shader such as + /// the GPU terrain generator. + /// + /// The caller is responsible for ensuring the vertex buffer contains data + /// in the engine's standard [`Vertex`] layout (44 bytes: pos.xyz + + /// normal.xyz + tangent.xyz + uv.xy) and that the index buffer contains + /// `u32` indices. + /// + /// # Arguments + /// + /// * `vertex_buffer` — GPU buffer containing interleaved vertex data. + /// * `vertex_buffer_size` — Size of the vertex buffer in bytes. + /// * `index_buffer` — GPU buffer containing `u32` triangle indices. + /// * `index_buffer_size` — Size of the index buffer in bytes. + /// * `index_count` — Number of indices (not bytes) in the index buffer. + pub fn register_gpu_mesh( + &mut self, + vertex_buffer: D::Buffer, + vertex_buffer_size: u64, + index_buffer: D::Buffer, + index_buffer_size: u64, + index_count: u32, + ) -> MeshHandle { + let handle = MeshHandle(self.meshes.len() as u32); + self.meshes.push(GpuMesh { + vertex_buffer, + vertex_buffer_size, + index_buffer, + index_buffer_size, + index_count, + // GPU-generated meshes bypass the geometry pool and meshlet paths; + // they are rendered via standard indexed draw calls. + pool_alloc: None, + meshlet_data: None, + }); + handle + } + /// Upload raw RGBA8 pixel data as a GPU texture with auto-generated mipmaps. pub fn upload_texture( &mut self, diff --git a/crates/euca-terrain/src/gpu_terrain.rs b/crates/euca-terrain/src/gpu_terrain.rs index 36b80fa..43f4eb3 100644 --- a/crates/euca-terrain/src/gpu_terrain.rs +++ b/crates/euca-terrain/src/gpu_terrain.rs @@ -285,6 +285,70 @@ impl GpuTerrainGenerator { let quads = (grid_cols.saturating_sub(1) as u64) * (grid_rows.saturating_sub(1) as u64); quads * (INDICES_PER_QUAD as u64) * std::mem::size_of::() as u64 } + + /// Generate terrain mesh for a chunk, returning GPU buffers ready for rendering. + /// + /// This is a higher-level wrapper around [`generate`](Self::generate) that + /// allocates output buffers with the correct usage flags + /// (`STORAGE | VERTEX` / `STORAGE | INDEX`) so they can serve as both + /// compute shader output and render pipeline input, then dispatches the + /// compute kernels. + /// + /// The returned [`GpuTerrainChunkOutput`] contains the buffer handles and + /// index count needed to register the mesh with the renderer via + /// `Renderer::register_gpu_mesh`. + pub fn generate_chunk( + &self, + device: &D, + encoder: &mut D::CommandEncoder, + params: &TerrainGenParams, + ) -> GpuTerrainChunkOutput { + let vb_size = Self::vertex_buffer_size(params.grid_cols, params.grid_rows); + let ib_size = Self::index_buffer_size(params.grid_cols, params.grid_rows); + + let vertex_buffer = device.create_buffer(&BufferDesc { + label: Some("Terrain Chunk Vertices"), + size: vb_size, + usage: BufferUsages::STORAGE | BufferUsages::VERTEX, + mapped_at_creation: false, + }); + + let index_buffer = device.create_buffer(&BufferDesc { + label: Some("Terrain Chunk Indices"), + size: ib_size, + usage: BufferUsages::STORAGE | BufferUsages::INDEX, + mapped_at_creation: false, + }); + + self.generate(device, encoder, params, &vertex_buffer, &index_buffer); + + let index_count = (params.grid_cols.saturating_sub(1)) + * (params.grid_rows.saturating_sub(1)) + * INDICES_PER_QUAD; + + GpuTerrainChunkOutput { + vertex_buffer, + vertex_buffer_size: vb_size, + index_buffer, + index_buffer_size: ib_size, + index_count, + } + } +} + +/// Output from [`GpuTerrainGenerator::generate_chunk`]: GPU buffers containing +/// the generated terrain mesh, ready to be registered with the renderer. +pub struct GpuTerrainChunkOutput { + /// Vertex buffer (layout matches `euca_render::Vertex`: 44 bytes per vertex). + pub vertex_buffer: D::Buffer, + /// Size of the vertex buffer in bytes. + pub vertex_buffer_size: u64, + /// Index buffer (`u32` triangle indices). + pub index_buffer: D::Buffer, + /// Size of the index buffer in bytes. + pub index_buffer_size: u64, + /// Number of indices in the index buffer. + pub index_count: u32, } #[cfg(test)] diff --git a/crates/euca-terrain/src/lib.rs b/crates/euca-terrain/src/lib.rs index 6a8af21..e7e444c 100644 --- a/crates/euca-terrain/src/lib.rs +++ b/crates/euca-terrain/src/lib.rs @@ -53,3 +53,7 @@ pub use lod::{ChunkLod, LodConfig, select_all_lods, select_chunk_lod}; pub use mesh::{TerrainMesh, TerrainVertex, generate_terrain_mesh}; pub use physics::{HeightfieldTile, generate_heightfield_colliders, height_at}; pub use splat::SplatMap; + +// GPU terrain generation (requires the `gpu-terrain` feature). +#[cfg(feature = "gpu-terrain")] +pub use gpu_terrain::{GpuTerrainChunkOutput, GpuTerrainGenerator, TerrainGenParams}; diff --git a/examples/gpu_terrain_demo.rs b/examples/gpu_terrain_demo.rs new file mode 100644 index 0000000..f4eb945 --- /dev/null +++ b/examples/gpu_terrain_demo.rs @@ -0,0 +1,288 @@ +//! GPU terrain generation demo. +//! +//! Creates a procedural heightmap (sine-wave hills), subdivides it into chunks +//! with LOD selection, generates each chunk's mesh on the GPU via compute +//! shader, and renders the result. +//! +//! Run: +//! cargo run -p euca-game --example gpu_terrain_demo --features gpu-terrain --release + +use euca_core::Time; +use euca_ecs::{Query, World}; +use euca_math::Vec3; +use euca_render::*; +use euca_scene::{GlobalTransform, LocalTransform}; +use euca_terrain::{ + GpuTerrainGenerator, Heightmap, LodConfig, TerrainGenParams, build_chunks, select_chunk_lod, +}; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, KeyEvent, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::{Key, NamedKey}; +use winit::window::{WindowAttributes, WindowId}; + +#[cfg(all(target_os = "macos", feature = "metal-native"))] +type Dev = euca_render::euca_rhi::metal_backend::MetalDevice; +#[cfg(not(all(target_os = "macos", feature = "metal-native")))] +type Dev = euca_render::euca_rhi::wgpu_backend::WgpuDevice; + +/// Grid dimensions for the heightmap. +const HEIGHTMAP_SIZE: u32 = 128; +/// Number of grid cells per chunk side. +const CHUNK_SIZE: u32 = 32; +/// Height scale applied to the normalised [0,1] heightmap values. +const HEIGHT_SCALE: f32 = 30.0; +/// World-space distance between adjacent grid vertices. +const CELL_SIZE: f32 = 1.0; + +/// Generate a procedural heightmap with overlapping sine waves. +fn generate_sine_heightmap(width: u32, height: u32) -> Heightmap { + let mut data = vec![0.0f32; (width as usize) * (height as usize)]; + for row in 0..height { + for col in 0..width { + let x = col as f32 / width as f32; + let z = row as f32 / height as f32; + // Overlapping sine waves at different frequencies for natural-looking hills. + let h = 0.5 + + 0.25 * (x * std::f32::consts::TAU * 2.0).sin() + * (z * std::f32::consts::TAU * 2.0).cos() + + 0.15 * (x * std::f32::consts::TAU * 5.0 + 1.0).sin() + + 0.10 * (z * std::f32::consts::TAU * 3.0 + 2.0).cos(); + data[(row * width + col) as usize] = h.clamp(0.0, 1.0); + } + } + Heightmap::from_raw(width, height, data) + .with_cell_size(CELL_SIZE) + .with_max_height(HEIGHT_SCALE) +} + +struct GpuTerrainApp { + world: World, + heightmap: Heightmap, + gpu: Option>, + renderer: Option>, + window_attrs: WindowAttributes, +} + +impl GpuTerrainApp { + fn new() -> Self { + let heightmap = generate_sine_heightmap(HEIGHTMAP_SIZE, HEIGHTMAP_SIZE); + + let mut world = World::new(); + world.insert_resource(Time::new()); + world.insert_resource(AmbientLight { + color: [1.0, 1.0, 1.0], + intensity: 0.3, + }); + + // Camera overlooking the terrain. + let center = HEIGHTMAP_SIZE as f32 * CELL_SIZE * 0.5; + world.insert_resource(Camera::new( + Vec3::new(center + 60.0, 80.0, center + 60.0), + Vec3::new(center, 0.0, center), + )); + + Self { + world, + heightmap, + gpu: None, + renderer: None, + window_attrs: WindowAttributes::default() + .with_title("Euca Engine — GPU Terrain Demo") + .with_inner_size(winit::dpi::LogicalSize::new(1024, 768)), + } + } + + fn setup_scene(&mut self) { + let gpu = self.gpu.as_ref().unwrap(); + let renderer = self.renderer.as_mut().unwrap(); + let rhi: &Dev = gpu; + + // Create the GPU terrain generator from the heightmap data. + let terrain_gen = GpuTerrainGenerator::::new( + rhi, + &self.heightmap.data, + self.heightmap.width, + self.heightmap.height, + ); + + // Subdivide the heightmap into chunks and select LOD for each. + let chunks = build_chunks(&self.heightmap, CHUNK_SIZE); + let camera = self.world.resource::().unwrap().clone(); + let lod_config = LodConfig::default(); + + // Create a shared material for all terrain chunks. + let terrain_mat = renderer.upload_material( + gpu, + &Material { + albedo: [0.45, 0.55, 0.30, 1.0], + metallic: 0.0, + roughness: 0.9, + ..Material::default() + }, + ); + + // Generate each chunk on the GPU and register the resulting mesh. + let mut encoder = rhi.create_command_encoder(Some("Terrain Gen")); + + for chunk in &chunks { + let lod = select_chunk_lod(chunk, camera.eye, &lod_config); + let grid_cols = (chunk.col_end - chunk.col_start).div_ceil(lod.step); + let grid_rows = (chunk.row_end - chunk.row_start).div_ceil(lod.step); + + let params = TerrainGenParams { + grid_cols, + grid_rows, + cell_size: self.heightmap.cell_size * lod.step as f32, + step: lod.step, + origin_x: chunk.col_start as f32 * self.heightmap.cell_size, + origin_z: chunk.row_start as f32 * self.heightmap.cell_size, + heightmap_width: self.heightmap.width, + heightmap_height: self.heightmap.height, + height_scale: self.heightmap.max_height, + _pad: [0.0; 3], + }; + + let output = terrain_gen.generate_chunk(rhi, &mut encoder, ¶ms); + + let mesh_handle = renderer.register_gpu_mesh( + output.vertex_buffer, + output.vertex_buffer_size, + output.index_buffer, + output.index_buffer_size, + output.index_count, + ); + + let e = self + .world + .spawn(LocalTransform(euca_math::Transform::IDENTITY)); + self.world.insert(e, GlobalTransform::default()); + self.world.insert(e, MeshRenderer { mesh: mesh_handle }); + self.world + .insert(e, MaterialRef { handle: terrain_mat }); + } + + rhi.submit(encoder); + log::info!( + "Generated {} terrain chunks on GPU ({} vertices per chunk at LOD 0)", + chunks.len(), + CHUNK_SIZE * CHUNK_SIZE, + ); + + // Directional light. + let dir = Vec3::new(-0.5, -1.0, -0.3).normalize(); + self.world.spawn(DirectionalLight { + direction: [dir.x, dir.y, dir.z], + color: [1.0, 0.98, 0.95], + intensity: 1.2, + light_size: 1.0, + }); + } + + fn update_and_render(&mut self) { + self.world.resource_mut::