From 04ca5df062a6ae3e48166350f7e0857712711b37 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 19:50:51 +0900 Subject: [PATCH] feat(renderer): add Blinn-Phong shader, light uniforms, mesh pipeline Co-Authored-By: Claude Sonnet 4.6 --- crates/voltex_renderer/src/lib.rs | 2 + crates/voltex_renderer/src/light.rs | 41 ++++++++++++++ crates/voltex_renderer/src/mesh_shader.wgsl | 57 +++++++++++++++++++ crates/voltex_renderer/src/pipeline.rs | 61 ++++++++++++++++++++- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 crates/voltex_renderer/src/light.rs create mode 100644 crates/voltex_renderer/src/mesh_shader.wgsl diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index 389ad39..77e26f1 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -1,4 +1,5 @@ pub mod gpu; +pub mod light; pub mod obj; pub mod pipeline; pub mod vertex; @@ -6,5 +7,6 @@ pub mod mesh; pub mod camera; pub use gpu::{GpuContext, DEPTH_FORMAT}; +pub use light::{CameraUniform, LightUniform}; pub use mesh::Mesh; pub use camera::{Camera, FpsController}; diff --git a/crates/voltex_renderer/src/light.rs b/crates/voltex_renderer/src/light.rs new file mode 100644 index 0000000..fe78eb1 --- /dev/null +++ b/crates/voltex_renderer/src/light.rs @@ -0,0 +1,41 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct CameraUniform { + pub view_proj: [[f32; 4]; 4], + pub model: [[f32; 4]; 4], + pub camera_pos: [f32; 3], + pub _padding: f32, +} + +impl CameraUniform { + pub fn new() -> Self { + Self { + view_proj: [[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]], + 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]], + camera_pos: [0.0; 3], + _padding: 0.0, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct LightUniform { + pub direction: [f32; 3], + pub _padding1: f32, + pub color: [f32; 3], + pub ambient_strength: f32, +} + +impl LightUniform { + pub fn new() -> Self { + Self { + direction: [0.0, -1.0, -1.0], + _padding1: 0.0, + color: [1.0, 1.0, 1.0], + ambient_strength: 0.1, + } + } +} diff --git a/crates/voltex_renderer/src/mesh_shader.wgsl b/crates/voltex_renderer/src/mesh_shader.wgsl new file mode 100644 index 0000000..7d7feef --- /dev/null +++ b/crates/voltex_renderer/src/mesh_shader.wgsl @@ -0,0 +1,57 @@ +struct CameraUniform { + view_proj: mat4x4, + model: mat4x4, + camera_pos: vec3, +}; + +struct LightUniform { + direction: vec3, + 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, +}; + +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(model_v: VertexInput) -> VertexOutput { + var out: VertexOutput; + let world_pos = camera.model * vec4(model_v.position, 1.0); + out.world_pos = world_pos.xyz; + out.world_normal = (camera.model * vec4(model_v.normal, 0.0)).xyz; + out.clip_position = camera.view_proj * world_pos; + out.uv = model_v.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); + let ambient = light.ambient_strength * light.color; + let diff = max(dot(normal, light_dir), 0.0); + let diffuse = diff * light.color; + 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 = spec * light.color * 0.5; + let result = (ambient + diffuse + specular) * tex_color.rgb; + return vec4(result, tex_color.a); +} diff --git a/crates/voltex_renderer/src/pipeline.rs b/crates/voltex_renderer/src/pipeline.rs index f0eeeeb..39e8068 100644 --- a/crates/voltex_renderer/src/pipeline.rs +++ b/crates/voltex_renderer/src/pipeline.rs @@ -1,4 +1,5 @@ -use crate::vertex::Vertex; +use crate::vertex::{Vertex, MeshVertex}; +use crate::gpu::DEPTH_FORMAT; pub fn create_render_pipeline( device: &wgpu::Device, @@ -53,3 +54,61 @@ pub fn create_render_pipeline( cache: None, }) } + +pub fn create_mesh_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("Mesh Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("mesh_shader.wgsl").into()), + }); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Mesh Pipeline Layout"), + bind_group_layouts: &[camera_light_layout, texture_layout], + immediate_size: 0, + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Mesh 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, + 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, + }) +}