From 17ea3f48565f848d8cb98aedadf05ca5c66a484b Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 15:14:12 +0900 Subject: [PATCH] feat(renderer): add GPU instancing with instance buffer and pipeline Co-Authored-By: Claude Opus 4.6 (1M context) --- .../voltex_renderer/src/instanced_shader.wgsl | 81 +++++++++ crates/voltex_renderer/src/instancing.rs | 164 ++++++++++++++++++ crates/voltex_renderer/src/lib.rs | 2 + 3 files changed, 247 insertions(+) create mode 100644 crates/voltex_renderer/src/instanced_shader.wgsl create mode 100644 crates/voltex_renderer/src/instancing.rs diff --git a/crates/voltex_renderer/src/instanced_shader.wgsl b/crates/voltex_renderer/src/instanced_shader.wgsl new file mode 100644 index 0000000..a7193ad --- /dev/null +++ b/crates/voltex_renderer/src/instanced_shader.wgsl @@ -0,0 +1,81 @@ +struct CameraUniform { + view_proj: mat4x4, + model: mat4x4, + camera_pos: vec3, + _pad: f32, +}; + +struct LightUniform { + direction: vec3, + _pad0: f32, + color: vec3, + ambient_strength: f32, +}; + +@group(0) @binding(0) var camera: CameraUniform; +@group(0) @binding(1) var light: LightUniform; + +@group(1) @binding(0) var t_diffuse: texture_2d; +@group(1) @binding(1) var s_diffuse: sampler; + +struct VertexInput { + @location(0) position: vec3, + @location(1) normal: vec3, + @location(2) uv: vec2, + @location(3) tangent: vec4, +}; + +struct InstanceInput { + @location(4) model_0: vec4, + @location(5) model_1: vec4, + @location(6) model_2: vec4, + @location(7) model_3: vec4, + @location(8) color: vec4, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_normal: vec3, + @location(1) world_pos: vec3, + @location(2) uv: vec2, + @location(3) inst_color: vec4, +}; + +@vertex +fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { + let inst_model = mat4x4( + instance.model_0, + instance.model_1, + instance.model_2, + instance.model_3, + ); + + var out: VertexOutput; + let world_pos = inst_model * vec4(vertex.position, 1.0); + out.world_pos = world_pos.xyz; + out.world_normal = normalize((inst_model * vec4(vertex.normal, 0.0)).xyz); + out.clip_position = camera.view_proj * world_pos; + out.uv = vertex.uv; + out.inst_color = instance.color; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let tex_color = textureSample(t_diffuse, s_diffuse, in.uv); + let normal = normalize(in.world_normal); + let light_dir = normalize(-light.direction); + + let ndotl = max(dot(normal, light_dir), 0.0); + let diffuse = light.color * ndotl; + + let view_dir = normalize(camera.camera_pos - in.world_pos); + let half_dir = normalize(light_dir + view_dir); + let spec = pow(max(dot(normal, half_dir), 0.0), 32.0); + let specular = light.color * spec * 0.3; + + let ambient = light.color * light.ambient_strength; + + let lit = (ambient + diffuse + specular) * tex_color.rgb * in.inst_color.rgb; + return vec4(lit, tex_color.a * in.inst_color.a); +} diff --git a/crates/voltex_renderer/src/instancing.rs b/crates/voltex_renderer/src/instancing.rs new file mode 100644 index 0000000..75ae47c --- /dev/null +++ b/crates/voltex_renderer/src/instancing.rs @@ -0,0 +1,164 @@ +use bytemuck::{Pod, Zeroable}; +use crate::vertex::MeshVertex; +use crate::gpu::DEPTH_FORMAT; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct InstanceData { + pub model: [[f32; 4]; 4], // 64 bytes + pub color: [f32; 4], // 16 bytes +} + +impl InstanceData { + pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { offset: 0, shader_location: 4, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 16, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 32, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 48, shader_location: 7, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 64, shader_location: 8, format: wgpu::VertexFormat::Float32x4 }, + ], + }; +} + +pub struct InstanceBuffer { + pub buffer: wgpu::Buffer, + pub capacity: usize, + pub count: usize, +} + +impl InstanceBuffer { + pub fn new(device: &wgpu::Device, capacity: usize) -> Self { + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Instance Buffer"), + size: (capacity * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + InstanceBuffer { buffer, capacity, count: 0 } + } + + pub fn update(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, instances: &[InstanceData]) { + self.count = instances.len(); + if instances.is_empty() { return; } + + if instances.len() > self.capacity { + self.capacity = instances.len().next_power_of_two(); + self.buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Instance Buffer"), + size: (self.capacity * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + } + + queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(instances)); + } +} + +pub fn create_instanced_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, + camera_light_layout: &wgpu::BindGroupLayout, + texture_layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Instanced Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("instanced_shader.wgsl").into()), + }); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Instanced Pipeline Layout"), + bind_group_layouts: &[camera_light_layout, texture_layout], + immediate_size: 0, + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Instanced Pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[MeshVertex::LAYOUT, InstanceData::LAYOUT], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false }, + multiview_mask: None, + cache: None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_instance_data_size() { + assert_eq!(std::mem::size_of::(), 80); + } + + #[test] + fn test_instance_data_layout_attributes() { + assert_eq!(InstanceData::LAYOUT.attributes.len(), 5); + assert_eq!(InstanceData::LAYOUT.array_stride, 80); + assert_eq!(InstanceData::LAYOUT.step_mode, wgpu::VertexStepMode::Instance); + } + + #[test] + fn test_instance_data_layout_locations() { + let attrs = InstanceData::LAYOUT.attributes; + assert_eq!(attrs[0].shader_location, 4); + assert_eq!(attrs[1].shader_location, 5); + assert_eq!(attrs[2].shader_location, 6); + assert_eq!(attrs[3].shader_location, 7); + assert_eq!(attrs[4].shader_location, 8); + } + + #[test] + fn test_instance_data_layout_offsets() { + let attrs = InstanceData::LAYOUT.attributes; + assert_eq!(attrs[0].offset, 0); + assert_eq!(attrs[1].offset, 16); + assert_eq!(attrs[2].offset, 32); + assert_eq!(attrs[3].offset, 48); + assert_eq!(attrs[4].offset, 64); + } + + #[test] + fn test_instance_data_pod() { + let data = InstanceData { + model: [[1.0,0.0,0.0,0.0],[0.0,1.0,0.0,0.0],[0.0,0.0,1.0,0.0],[0.0,0.0,0.0,1.0]], + color: [1.0, 1.0, 1.0, 1.0], + }; + let bytes: &[u8] = bytemuck::bytes_of(&data); + assert_eq!(bytes.len(), 80); + } +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index 5fed72b..d8ce101 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -34,6 +34,7 @@ pub mod bloom; pub mod tonemap; pub mod forward_pass; pub mod auto_exposure; +pub mod instancing; pub use gpu::{GpuContext, DEPTH_FORMAT}; pub use light::{CameraUniform, LightUniform, LightData, LightsUniform, MAX_LIGHTS, LIGHT_DIRECTIONAL, LIGHT_POINT, LIGHT_SPOT}; @@ -71,6 +72,7 @@ pub use bloom::{BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT}; pub use tonemap::{TonemapUniform, aces_tonemap}; pub use forward_pass::{ForwardPass, sort_transparent_back_to_front}; pub use auto_exposure::AutoExposure; +pub use instancing::{InstanceData, InstanceBuffer, create_instanced_pipeline}; pub use png::parse_png; pub use jpg::parse_jpg; pub use gltf::{parse_gltf, GltfData, GltfMesh, GltfMaterial};