feat(renderer): add GPU instancing with instance buffer and pipeline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
crates/voltex_renderer/src/instanced_shader.wgsl
Normal file
81
crates/voltex_renderer/src/instanced_shader.wgsl
Normal file
@@ -0,0 +1,81 @@
|
||||
struct CameraUniform {
|
||||
view_proj: mat4x4<f32>,
|
||||
model: mat4x4<f32>,
|
||||
camera_pos: vec3<f32>,
|
||||
_pad: f32,
|
||||
};
|
||||
|
||||
struct LightUniform {
|
||||
direction: vec3<f32>,
|
||||
_pad0: f32,
|
||||
color: vec3<f32>,
|
||||
ambient_strength: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> camera: CameraUniform;
|
||||
@group(0) @binding(1) var<uniform> light: LightUniform;
|
||||
|
||||
@group(1) @binding(0) var t_diffuse: texture_2d<f32>;
|
||||
@group(1) @binding(1) var s_diffuse: sampler;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) tangent: vec4<f32>,
|
||||
};
|
||||
|
||||
struct InstanceInput {
|
||||
@location(4) model_0: vec4<f32>,
|
||||
@location(5) model_1: vec4<f32>,
|
||||
@location(6) model_2: vec4<f32>,
|
||||
@location(7) model_3: vec4<f32>,
|
||||
@location(8) color: vec4<f32>,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@location(0) world_normal: vec3<f32>,
|
||||
@location(1) world_pos: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) inst_color: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput {
|
||||
let inst_model = mat4x4<f32>(
|
||||
instance.model_0,
|
||||
instance.model_1,
|
||||
instance.model_2,
|
||||
instance.model_3,
|
||||
);
|
||||
|
||||
var out: VertexOutput;
|
||||
let world_pos = inst_model * vec4<f32>(vertex.position, 1.0);
|
||||
out.world_pos = world_pos.xyz;
|
||||
out.world_normal = normalize((inst_model * vec4<f32>(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<f32> {
|
||||
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<f32>(lit, tex_color.a * in.inst_color.a);
|
||||
}
|
||||
164
crates/voltex_renderer/src/instancing.rs
Normal file
164
crates/voltex_renderer/src/instancing.rs
Normal file
@@ -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::<InstanceData>() 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::<InstanceData>()) 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::<InstanceData>()) 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::<InstanceData>(), 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user