From afb95c9fb1d9894a31aaf2e29cbc7fb41449063b Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 14:52:17 +0900 Subject: [PATCH] feat(renderer): add forward transparency pass with alpha blending Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_renderer/src/forward_pass.rs | 184 ++++++++++++++++++ .../voltex_renderer/src/forward_shader.wgsl | 67 +++++++ crates/voltex_renderer/src/lib.rs | 2 + 3 files changed, 253 insertions(+) create mode 100644 crates/voltex_renderer/src/forward_pass.rs create mode 100644 crates/voltex_renderer/src/forward_shader.wgsl diff --git a/crates/voltex_renderer/src/forward_pass.rs b/crates/voltex_renderer/src/forward_pass.rs new file mode 100644 index 0000000..8a71fee --- /dev/null +++ b/crates/voltex_renderer/src/forward_pass.rs @@ -0,0 +1,184 @@ +use crate::vertex::MeshVertex; +use crate::hdr::HDR_FORMAT; +use crate::gpu::DEPTH_FORMAT; +use crate::mesh::Mesh; + +pub struct ForwardPass { + pipeline: wgpu::RenderPipeline, +} + +impl ForwardPass { + pub fn new( + device: &wgpu::Device, + camera_light_layout: &wgpu::BindGroupLayout, + texture_layout: &wgpu::BindGroupLayout, + ) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Forward Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("forward_shader.wgsl").into()), + }); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Forward Pipeline Layout"), + bind_group_layouts: &[camera_light_layout, texture_layout], + immediate_size: 0, + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Forward Pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[MeshVertex::LAYOUT], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: HDR_FORMAT, + blend: Some(wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }), + 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: None, // No culling for transparent objects (see both sides) + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: DEPTH_FORMAT, + depth_write_enabled: false, // Don't write depth (preserve opaque depth) + depth_compare: wgpu::CompareFunction::LessEqual, + 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, + }); + + ForwardPass { pipeline } + } + + pub fn render<'a>( + &'a self, + encoder: &'a mut wgpu::CommandEncoder, + hdr_view: &wgpu::TextureView, + depth_view: &wgpu::TextureView, + camera_light_bg: &'a wgpu::BindGroup, + texture_bg: &'a wgpu::BindGroup, + meshes: &'a [&Mesh], + ) { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Forward Transparency Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: hdr_view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, camera_light_bg, &[]); + rpass.set_bind_group(1, texture_bg, &[]); + + for mesh in meshes { + rpass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + rpass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); + rpass.draw_indexed(0..mesh.num_indices, 0, 0..1); + } + } +} + +/// Sort transparent objects back-to-front by distance from camera. +pub fn sort_transparent_back_to_front( + items: &mut Vec<(usize, [f32; 3])>, // (index, center_position) + camera_pos: [f32; 3], +) { + items.sort_by(|a, b| { + let da = dist_sq(a.1, camera_pos); + let db = dist_sq(b.1, camera_pos); + db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal) + }); +} + +fn dist_sq(a: [f32; 3], b: [f32; 3]) -> f32 { + let dx = a[0] - b[0]; + let dy = a[1] - b[1]; + let dz = a[2] - b[2]; + dx * dx + dy * dy + dz * dz +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sort_back_to_front() { + let mut items = vec![ + (0, [1.0, 0.0, 0.0]), // close + (1, [10.0, 0.0, 0.0]), // far + (2, [5.0, 0.0, 0.0]), // mid + ]; + sort_transparent_back_to_front(&mut items, [0.0, 0.0, 0.0]); + assert_eq!(items[0].0, 1); // farthest first + assert_eq!(items[1].0, 2); + assert_eq!(items[2].0, 0); // closest last + } + + #[test] + fn test_sort_equal_distance() { + let mut items = vec![ + (0, [1.0, 0.0, 0.0]), + (1, [0.0, 1.0, 0.0]), + (2, [0.0, 0.0, 1.0]), + ]; + // All at distance 1.0 from origin — should not crash + sort_transparent_back_to_front(&mut items, [0.0, 0.0, 0.0]); + assert_eq!(items.len(), 3); + } + + #[test] + fn test_sort_empty() { + let mut items: Vec<(usize, [f32; 3])> = vec![]; + sort_transparent_back_to_front(&mut items, [0.0, 0.0, 0.0]); + assert!(items.is_empty()); + } +} diff --git a/crates/voltex_renderer/src/forward_shader.wgsl b/crates/voltex_renderer/src/forward_shader.wgsl new file mode 100644 index 0000000..ba9b89b --- /dev/null +++ b/crates/voltex_renderer/src/forward_shader.wgsl @@ -0,0 +1,67 @@ +struct CameraUniform { + view_proj: mat4x4, + model: mat4x4, + camera_pos: vec3, + alpha: 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 VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_normal: vec3, + @location(1) world_pos: vec3, + @location(2) uv: vec2, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + let world_pos = camera.model * vec4(in.position, 1.0); + out.world_pos = world_pos.xyz; + out.world_normal = normalize((camera.model * vec4(in.normal, 0.0)).xyz); + out.clip_position = camera.view_proj * world_pos; + out.uv = in.uv; + 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); + + // Diffuse + let ndotl = max(dot(normal, light_dir), 0.0); + let diffuse = light.color * ndotl; + + // Specular (Blinn-Phong) + 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.5; + + // Ambient + let ambient = light.color * light.ambient_strength; + + let lit = (ambient + diffuse + specular) * tex_color.rgb; + return vec4(lit, camera.alpha); +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index b5ef411..32a065a 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -32,6 +32,7 @@ pub mod rt_shadow; pub mod hdr; pub mod bloom; pub mod tonemap; +pub mod forward_pass; pub use gpu::{GpuContext, DEPTH_FORMAT}; pub use light::{CameraUniform, LightUniform, LightData, LightsUniform, MAX_LIGHTS, LIGHT_DIRECTIONAL, LIGHT_POINT, LIGHT_SPOT}; @@ -67,6 +68,7 @@ pub use rt_shadow::{RtShadowResources, RtShadowUniform, RT_SHADOW_FORMAT}; pub use hdr::{HdrTarget, HDR_FORMAT}; 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 png::parse_png; pub use jpg::parse_jpg; pub use gltf::{parse_gltf, GltfData, GltfMesh, GltfMaterial};