tirbofish/dropbear · commit
67ad3f2ebbdff0af80d2bdd1fc93f3d669ee2f40
feature: using WESL shaders for eay modularisation.
wip: remake shaders to better suit PBR and hopefully not AI generated.
Signature present but could not be verified.
Unverified
@@ -37,7 +37,7 @@ env_logger = "0.11" futures = "0.3" gilrs = "0.11" git2 = { version = "0.20", features = ["vendored-openssl"] } -glam = { version = "0.30", features = ["serde", "mint", "bytemuck", "rkyv", "bytechec"] } # required to be at 0.30 because of rapier3d not being updated yet +glam = { version = "0.30", features = ["serde", "mint", "bytemuck", "rkyv", "bytecheck"] } # required to be at 0.30 because of rapier3d not being updated yet hecs = { version = "0.11", features = ["serde"] } log = "0.4" log-once = "0.4" @@ -64,7 +64,7 @@ tree-sitter-kotlin = "0.3" libloading = "0.9" indexmap = "2.11" sha2 = "0.11" -wesl = "0.3" +wesl = { version = "0.3", features = ["package"] } dashmap = "6.1" open = "5.3" memory-stats = "1.2" @@ -96,6 +96,12 @@ naga = { version = "29", features = ["wgsl-in"] } uuid = { version = "1.22", features = ["v4", "serde"] } splines = "5.0" +[patch.crates-io] +egui = { git = "https://github.com/emilk/egui", branch = "main" } +emath = { git = "https://github.com/emilk/egui", branch = "main" } +epaint = { git = "https://github.com/emilk/egui", branch = "main" } +ecolor = { git = "https://github.com/emilk/egui", branch = "main" } + [workspace.dependencies.image] version = "0.25" default-features = false @@ -1,6 +1,5 @@ [package] name = "dropbear-engine" -description = "A game engine made by tirbofish. Thats really it..." version.workspace = true edition.workspace = true @@ -52,6 +51,7 @@ uuid.workspace = true float-derive.workspace = true rkyv.workspace = true bevy_mikktspace.workspace = true +wesl.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -60,3 +60,4 @@ rfd.workspace = true slank = { path = "../slank", features = ["download-slang"] } naga.workspace = true anyhow.workspace = true +wesl.workspace = true @@ -1,7 +1,17 @@ use slank::{SlangShaderBuilder, SlangTarget}; -use std::path::Path; fn main() -> anyhow::Result<()> { + + println!("cargo:rerun-if-changed=src/shaders"); + + compile_slang_shaders()?; + + compile_wesl_shaders()?; + + Ok(()) +} + +fn compile_slang_shaders() -> anyhow::Result<()> { // to copy paste: // let shader = Shader::from_slang(graphics.clone(), &slank::compiled::CompiledSlangShader::from_bytes("light cube", include_slang!("light_cube"))); @@ -13,43 +23,20 @@ fn main() -> anyhow::Result<()> { .add_source_path("src/shaders/blit.slang")? .compile_to_out_dir(SlangTarget::SpirV)?; - println!("cargo:rerun-if-changed=src/shaders"); - - validate_wgsl()?; - Ok(()) } -fn validate_wgsl() -> anyhow::Result<()> { - let shader_dir = Path::new("src/shaders"); - - for entry in std::fs::read_dir(shader_dir)? { - let path = entry?.path(); - if path.extension().and_then(|e| e.to_str()) != Some("wgsl") { - continue; - } - - println!("cargo:rerun-if-changed={}", path.display()); - - let src = std::fs::read_to_string(&path)?; - - let mut frontend = naga::front::wgsl::Frontend::new(); - let module = frontend.parse(&src).unwrap_or_else(|e| { - panic!( - "WGSL parse error in {}:\n{}", - path.display(), - e.emit_to_string(&src) - ); - }); - - let mut validator = naga::valid::Validator::new( - naga::valid::ValidationFlags::all(), - naga::valid::Capabilities::all(), - ); - validator.validate(&module).unwrap_or_else(|e| { - panic!("WGSL validation error in {}:\n{:?}", path.display(), e); - }); - } +fn compile_wesl_shaders() -> anyhow::Result<()> { + wesl::PkgBuilder::new("dropbear_shaders") + .scan_root("src/shaders") + .expect("failed to scan WESL files") + .validate() + .inspect_err(|e| { + eprintln!("{e}"); + panic!("{e}"); + })? + .build_artifact() + .expect("failed to build artifact"); Ok(()) -} +} @@ -1,6 +1,6 @@ use crate::pipelines::create_render_pipeline; use crate::texture::{Texture, TextureBuilder}; -use wgpu::Operations; +use wgpu::{Operations, ShaderModuleDescriptor}; use wgpu::util::DeviceExt; #[repr(C)] @@ -122,7 +122,21 @@ impl HdrPipeline { }); // We'll cover the shader next - let shader = wgpu::include_wgsl!("../shaders/hdr.wgsl"); + let source = wesl::Wesl::new("src/shaders") + .add_package(&crate::shader::code::PACKAGE) + .compile(&"dropbear_shaders::hdr".parse().unwrap()) + .inspect_err(|e| { + panic!("{e}"); + }) + .unwrap() + .to_string(); + + + let shader = ShaderModuleDescriptor { + label: Some("hdr shader"), + source: wgpu::ShaderSource::Wgsl(source.into()), + }; + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, bind_group_layouts: &[Some(&layout)], @@ -7,7 +7,7 @@ use crate::texture::Texture; use std::sync::Arc; use wgpu::{CompareFunction, DepthBiasState, StencilState}; -/// As defined in `shaders/shader.wgsl` +/// As defined in `shaders/shader.wesl` pub struct MainRenderPipeline { shader: Shader, pipeline_layout: wgpu::PipelineLayout, @@ -21,9 +21,18 @@ pub struct MainRenderPipeline { impl DropbearShaderPipeline for MainRenderPipeline { fn new(graphics: Arc<SharedGraphicsContext>) -> Self { + let source = wesl::Wesl::new("src/shaders") + .add_package(&crate::shader::code::PACKAGE) + .compile(&"dropbear_shaders::shader".parse().unwrap()) + .inspect_err(|e| { + panic!("{e}"); + }) + .unwrap() + .to_string(); + let shader = Shader::new( graphics.clone(), - include_str!("../shaders/shader.wgsl"), + &source, Some("viewport shaders"), ); @@ -104,6 +113,10 @@ impl DropbearShaderPipeline for MainRenderPipeline { } } + fn shader(&self) -> &Shader { + &self.shader + } + fn pipeline_layout(&self) -> &wgpu::PipelineLayout { &self.pipeline_layout } @@ -111,10 +124,6 @@ impl DropbearShaderPipeline for MainRenderPipeline { fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } - - fn shader(&self) -> &Shader { - &self.shader - } } impl MainRenderPipeline { @@ -6,6 +6,11 @@ use std::ops::Deref; use std::sync::Arc; use wgpu::ShaderModule; +pub mod code { + wesl::wesl_pkg!(dropbear_shaders); + pub use dropbear_shaders::*; +} + /// A nice little struct that stored basic information about a WGPU shaders. pub struct Shader { /// The label of the shader. @@ -21,7 +26,7 @@ pub struct Shader { /// The content of the shader as a readable string content, in the case you need to look /// at the original source. - pub content: String, + pub content: Option<String>, } impl Deref for Shader { @@ -57,7 +62,7 @@ impl Shader { None => "shader".into(), }, module, - content: shader_file_contents.to_string(), + content: Some(shader_file_contents.to_string()), } } @@ -75,7 +80,30 @@ impl Shader { Self { label: shader.label().clone(), module, - content: String::from_utf8(shader.source.clone()).unwrap_or_default(), + content: None, + } + } + + /// At this point, just create the shader manually lol + pub fn from_descriptor( + graphics: Arc<SharedGraphicsContext>, + desc: wgpu::ShaderModuleDescriptor, + label: Option<&str> + ) -> Self { + let module = graphics.device.create_shader_module(desc); + puffin::profile_function!(); + + log::debug!("Created new shaders under the label: {:?}", label); + + CompiledSlangShader::from_bytes("light cube", slank::include_slang!("light_cube")); + + Self { + label: match label { + Some(label) => label.into(), + None => "shader".into(), + }, + module, + content: None, } } } @@ -0,0 +1,57 @@ +struct MorphTargetInfo { + num_vertices: u32, + num_targets: u32, + base_offset: u32, + weight_offset: u32, + uses_morph: u32, +} + +@group(2) @binding(0) var<storage, read> s_skinning: array<mat4x4<f32>>; +@group(2) @binding(1) var<storage, read> s_morph_deltas: array<f32>; +@group(2) @binding(2) var<storage, read> s_morph_weights: array<f32>; +@group(2) @binding(3) var<uniform> u_morph_info: MorphTargetInfo; + +fn apply_morph(base_pos: vec3<f32>, vertex_id: u32) -> vec3<f32> { + var result = base_pos; + for (var t = 0u; t < u_morph_info.num_targets; t++) { + let weight = s_morph_weights[u_morph_info.weight_offset + t]; + let idx = u_morph_info.base_offset + (t * u_morph_info.num_vertices + vertex_id) * 3u; + let delta = vec3<f32>( + s_morph_deltas[idx], + s_morph_deltas[idx + 1u], + s_morph_deltas[idx + 2u], + ); + result += delta * weight; + } + return result; +} + +struct AnimatedVertex { + position: vec3<f32>, + normal: vec3<f32>, + tangent: vec3<f32>, +} + +fn apply_animation( + position: vec3<f32>, + normal: vec3<f32>, + tangent: vec3<f32>, + joints: vec4<u32>, + weights: vec4<f32>, + vertex_id: u32, +) -> AnimatedVertex { + // Morph targets are applied first (in bind-pose space, per glTF spec) + let morphed_pos = apply_morph(position, vertex_id); + + let skin_matrix = + weights.x * s_skinning[joints.x] + + weights.y * s_skinning[joints.y] + + weights.z * s_skinning[joints.z] + + weights.w * s_skinning[joints.w]; + + let skinned_pos = (skin_matrix * vec4<f32>(morphed_pos, 1.0)).xyz; + let skinned_normal = normalize((skin_matrix * vec4<f32>(normal, 0.0)).xyz); + let skinned_tangent = normalize((skin_matrix * vec4<f32>(tangent, 0.0)).xyz); + + return AnimatedVertex(skinned_pos, skinned_normal, skinned_tangent); +} @@ -0,0 +1 @@ +const PI: f32 = 3.14159265358979; @@ -0,0 +1,2 @@ +@group(3) @binding(0) var env_map: texture_cube<f32>; +@group(3) @binding(1) var env_sampler: sampler; @@ -0,0 +1,28 @@ +struct MaterialUniform { + base_colour: vec4<f32>, + emissive: vec3<f32>, + emissive_strength: f32, + metallic: f32, + roughness: f32, + normal_scale: f32, + occlusion_strength: f32, + alpha_cutoff: f32, + uv_tiling: vec2<f32>, + + has_normal_texture: u32, + has_emissive_texture: u32, + has_metallic_texture: u32, + has_occlusion_texture: u32, +} + +@group(1) @binding(0) var<uniform> u_material: MaterialUniform; +@group(1) @binding(1) var t_diffuse: texture_2d<f32>; +@group(1) @binding(2) var s_diffuse: sampler; +@group(1) @binding(3) var t_normal: texture_2d<f32>; +@group(1) @binding(4) var s_normal: sampler; +@group(1) @binding(5) var t_emissive: texture_2d<f32>; +@group(1) @binding(6) var s_emissive: sampler; +@group(1) @binding(7) var t_metallic: texture_2d<f32>; +@group(1) @binding(8) var s_metallic: sampler; +@group(1) @binding(9) var t_occlusion: texture_2d<f32>; +@group(1) @binding(10) var s_occlusion: sampler; @@ -0,0 +1,67 @@ +// Maps HDR values to linear values +// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces +fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> { + let m1 = mat3x3( + 0.59719, 0.07600, 0.02840, + 0.35458, 0.90834, 0.13383, + 0.04823, 0.01566, 0.83777, + ); + let m2 = mat3x3( + 1.60475, -0.10208, -0.00327, + -0.53108, 1.10813, -0.07276, + -0.07367, -0.00605, 1.07602, + ); + let v = m1 * hdr; + let a = v * (v + 0.0245786) - 0.000090537; + let b = v * (0.983729 * v + 0.4329510) + 0.238081; + return clamp(m2 * (a / b), vec3(0.0), vec3(1.0)); +} + +struct VertexOutput { + @location(0) uv: vec2<f32>, + @builtin(position) clip_position: vec4<f32>, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vi: u32, +) -> VertexOutput { + var out: VertexOutput; + // Generate a triangle that covers the whole screen + out.uv = vec2<f32>( + f32((vi << 1u) & 2u), + f32(vi & 2u), + ); + out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0); + // We need to invert the y coordinate so the image + // is not upside down + out.uv.y = 1.0 - out.uv.y; + return out; +} + +@group(0) +@binding(0) +var hdr_image: texture_2d<f32>; + +@group(0) +@binding(1) +var hdr_sampler: sampler; + +struct PostProcessSettings { + gamma: f32, + _pad0: f32, + _pad1: f32, + _pad2: f32, +} + +@group(0) +@binding(2) +var<uniform> post_process: PostProcessSettings; + +@fragment +fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> { + let hdr = textureSample(hdr_image, hdr_sampler, vs.uv); + let sdr = aces_tone_map(hdr.rgb); + let gamma_corrected = pow(sdr, vec3<f32>(1.0 / post_process.gamma)); + return vec4(gamma_corrected, hdr.a); +} @@ -1,67 +0,0 @@ -// Maps HDR values to linear values -// Based on http://www.oscars.org/science-technology/sci-tech-projects/aces -fn aces_tone_map(hdr: vec3<f32>) -> vec3<f32> { - let m1 = mat3x3( - 0.59719, 0.07600, 0.02840, - 0.35458, 0.90834, 0.13383, - 0.04823, 0.01566, 0.83777, - ); - let m2 = mat3x3( - 1.60475, -0.10208, -0.00327, - -0.53108, 1.10813, -0.07276, - -0.07367, -0.00605, 1.07602, - ); - let v = m1 * hdr; - let a = v * (v + 0.0245786) - 0.000090537; - let b = v * (0.983729 * v + 0.4329510) + 0.238081; - return clamp(m2 * (a / b), vec3(0.0), vec3(1.0)); -} - -struct VertexOutput { - @location(0) uv: vec2<f32>, - @builtin(position) clip_position: vec4<f32>, -}; - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, -) -> VertexOutput { - var out: VertexOutput; - // Generate a triangle that covers the whole screen - out.uv = vec2<f32>( - f32((vi << 1u) & 2u), - f32(vi & 2u), - ); - out.clip_position = vec4<f32>(out.uv * 2.0 - 1.0, 0.0, 1.0); - // We need to invert the y coordinate so the image - // is not upside down - out.uv.y = 1.0 - out.uv.y; - return out; -} - -@group(0) -@binding(0) -var hdr_image: texture_2d<f32>; - -@group(0) -@binding(1) -var hdr_sampler: sampler; - -struct PostProcessSettings { - gamma: f32, - _pad0: f32, - _pad1: f32, - _pad2: f32, -} - -@group(0) -@binding(2) -var<uniform> post_process: PostProcessSettings; - -@fragment -fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> { - let hdr = textureSample(hdr_image, hdr_sampler, vs.uv); - let sdr = aces_tone_map(hdr.rgb); - let gamma_corrected = pow(sdr, vec3<f32>(1.0 / post_process.gamma)); - return vec4(gamma_corrected, hdr.a); -} @@ -0,0 +1,112 @@ +import super::common::animation; +import super::common::material; +import super::common::environment; + +struct Globals { + num_lights: u32, + ambient_strength: f32, +} + +struct CameraUniform { + view_pos: vec4<f32>, + view: mat4x4<f32>, + view_proj: mat4x4<f32>, + inv_proj: mat4x4<f32>, + inv_view: mat4x4<f32>, +} + +struct Light { + position: vec4<f32>, + direction: vec4<f32>, // x, y, z, outer_cutoff_angle + color: vec4<f32>, // r, g, b, light_type (0=directional, 1=point, 2=spot) + constant: f32, + lin: f32, + quadratic: f32, + cutoff: f32, +} + +// per-frame +@group(0) @binding(0) var<uniform> u_globals: Globals; +@group(0) @binding(1) var<uniform> u_camera: CameraUniform; +@group(0) @binding(2) var<storage, read> s_light_array: array<Light>; + +struct InstanceInput { + @location(8) model_matrix_0: vec4<f32>, + @location(9) model_matrix_1: vec4<f32>, + @location(10) model_matrix_2: vec4<f32>, + @location(11) model_matrix_3: vec4<f32>, + + @location(12) normal_matrix_0: vec3<f32>, + @location(13) normal_matrix_1: vec3<f32>, + @location(14) normal_matrix_2: vec3<f32>, +}; + +struct VertexInput { + @builtin(vertex_index) vertex_id: u32, + @location(0) position: vec3<f32>, + @location(1) normal: vec3<f32>, + @location(2) tangent: vec4<f32>, + @location(3) tex_coords0: vec2<f32>, + @location(4) tex_coords1: vec2<f32>, + @location(5) colour0: vec4<f32>, + @location(6) joints: vec4<u32>, + @location(7) weights: vec4<f32>, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) tex_coords: vec2<f32>, + @location(1) world_normal: vec3<f32>, + @location(2) world_position: vec3<f32>, + @location(3) world_tangent: vec3<f32>, + @location(4) world_bitangent: vec3<f32>, + @location(5) world_view_position: vec3<f32>, +}; + + + +@vertex +fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { + let model_matrix = mat4x4<f32>( + instance.model_matrix_0, + instance.model_matrix_1, + instance.model_matrix_2, + instance.model_matrix_3, + ); + let normal_matrix = mat3x3<f32>( + instance.normal_matrix_0, + instance.normal_matrix_1, + instance.normal_matrix_2, + ); + + let anim = animation::apply_animation( + model.position, + model.normal, + model.tangent.xyz, + model.joints, + model.weights, + model.vertex_id, + ); + + let world_position = model_matrix * vec4<f32>(anim.position, 1.0); + let world_normal = normalize(normal_matrix * anim.normal); + let world_tangent = normalize(normal_matrix * anim.tangent); + let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w); + + var out: VertexOutput; + out.clip_position = u_camera.view_proj * world_position; + out.tex_coords = model.tex_coords0; + out.world_normal = world_normal; + out.world_position = world_position.xyz; + out.world_tangent = world_tangent; + out.world_bitangent = world_bitangent; + out.world_view_position = u_camera.view_pos.xyz; + return out; +} + +@fragment +fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let uv = in.tex_coords * material::u_material.uv_tiling; + + return textureSample(material::t_diffuse, material::s_diffuse, uv); +} @@ -1,409 +0,0 @@ -const PI: f32 = 3.14159265358979; - -struct Globals { - num_lights: u32, - ambient_strength: f32, -} - -struct CameraUniform { - view_pos: vec4<f32>, - view: mat4x4<f32>, - view_proj: mat4x4<f32>, - inv_proj: mat4x4<f32>, - inv_view: mat4x4<f32>, -} - -struct Light { - position: vec4<f32>, - direction: vec4<f32>, // x, y, z, outer_cutoff_angle - color: vec4<f32>, // r, g, b, light_type (0=directional, 1=point, 2=spot) - constant: f32, - lin: f32, - quadratic: f32, - cutoff: f32, -} - -struct MaterialUniform { - base_colour: vec4<f32>, - emissive: vec3<f32>, - emissive_strength: f32, - metallic: f32, - roughness: f32, - normal_scale: f32, - occlusion_strength: f32, - alpha_cutoff: f32, - uv_tiling: vec2<f32>, - - has_normal_texture: u32, - has_emissive_texture: u32, - has_metallic_texture: u32, - has_occlusion_texture: u32, -} - -struct MorphTargetInfo { - num_vertices: u32, - num_targets: u32, - base_offset: u32, - weight_offset: u32, - uses_morph: u32, -} - -// per-frame -@group(0) @binding(0) var<uniform> u_globals: Globals; -@group(0) @binding(1) var<uniform> u_camera: CameraUniform; -@group(0) @binding(2) var<storage, read> s_light_array: array<Light>; - -// per-material -@group(1) @binding(0) var<uniform> u_material: MaterialUniform; -@group(1) @binding(1) var t_diffuse: texture_2d<f32>; -@group(1) @binding(2) var s_diffuse: sampler; -@group(1) @binding(3) var t_normal: texture_2d<f32>; -@group(1) @binding(4) var s_normal: sampler; -@group(1) @binding(5) var t_emissive: texture_2d<f32>; -@group(1) @binding(6) var s_emissive: sampler; -@group(1) @binding(7) var t_metallic: texture_2d<f32>; -@group(1) @binding(8) var s_metallic: sampler; -@group(1) @binding(9) var t_occlusion: texture_2d<f32>; -@group(1) @binding(10) var s_occlusion: sampler; - -// animation -@group(2) @binding(0) var<storage, read> s_skinning: array<mat4x4<f32>>; -@group(2) @binding(1) var<storage, read> s_morph_deltas: array<f32>; -@group(2) @binding(2) var<storage, read> s_morph_weights: array<f32>; -@group(2) @binding(3) var<uniform> u_morph_info: MorphTargetInfo; - -// environment -@group(3) @binding(0) var env_map: texture_cube<f32>; -@group(3) @binding(1) var env_sampler: sampler; - -struct InstanceInput { - @location(8) model_matrix_0: vec4<f32>, - @location(9) model_matrix_1: vec4<f32>, - @location(10) model_matrix_2: vec4<f32>, - @location(11) model_matrix_3: vec4<f32>, - - @location(12) normal_matrix_0: vec3<f32>, - @location(13) normal_matrix_1: vec3<f32>, - @location(14) normal_matrix_2: vec3<f32>, -}; - -struct VertexInput { - @builtin(vertex_index) vertex_id: u32, - @location(0) position: vec3<f32>, - @location(1) normal: vec3<f32>, - @location(2) tangent: vec4<f32>, - @location(3) tex_coords0: vec2<f32>, - @location(4) tex_coords1: vec2<f32>, - @location(5) colour0: vec4<f32>, - @location(6) joints: vec4<u32>, - @location(7) weights: vec4<f32>, -}; - -struct VertexOutput { - @builtin(position) clip_position: vec4<f32>, - @location(0) tex_coords: vec2<f32>, - @location(1) world_normal: vec3<f32>, - @location(2) world_position: vec3<f32>, - @location(3) world_tangent: vec3<f32>, - @location(4) world_bitangent: vec3<f32>, - @location(5) world_view_position: vec3<f32>, -}; - -fn apply_morph(base_pos: vec3<f32>, vertex_id: u32) -> vec3<f32> { - var result = base_pos; - for (var t = 0u; t < u_morph_info.num_targets; t++) { - let weight = s_morph_weights[u_morph_info.weight_offset + t]; - let idx = u_morph_info.base_offset + (t * u_morph_info.num_vertices + vertex_id) * 3u; - let delta = vec3<f32>( - s_morph_deltas[idx], - s_morph_deltas[idx + 1u], - s_morph_deltas[idx + 2u], - ); - result += delta * weight; - } - return result; -} - -@vertex -fn vs_main(model: VertexInput, instance: InstanceInput) -> VertexOutput { - let model_matrix = mat4x4<f32>( - instance.model_matrix_0, - instance.model_matrix_1, - instance.model_matrix_2, - instance.model_matrix_3, - ); - let normal_matrix = mat3x3<f32>( - instance.normal_matrix_0, - instance.normal_matrix_1, - instance.normal_matrix_2, - ); - - var skin_matrix = mat4x4<f32>( - vec4<f32>(1.0, 0.0, 0.0, 0.0), - vec4<f32>(0.0, 1.0, 0.0, 0.0), - vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(0.0, 0.0, 0.0, 1.0), - ); - if (dot(model.weights, vec4<f32>(1.0)) > 0.0) { - let j = model.joints; - let w = model.weights; - skin_matrix = - s_skinning[j.x] * w.x + - s_skinning[j.y] * w.y + - s_skinning[j.z] * w.z + - s_skinning[j.w] * w.w; - } - - let morphed_pos = select( - model.position, - apply_morph(model.position, model.vertex_id), - u_morph_info.uses_morph != 0u, - ); - let world_position = model_matrix * skin_matrix * vec4<f32>(morphed_pos, 1.0); - - let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz; - let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz; - - let world_normal = normalize(normal_matrix * skin_normal); - let world_tangent = normalize(normal_matrix * skin_tangent); - let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w); - - var out: VertexOutput; - out.clip_position = u_camera.view_proj * world_position; - out.tex_coords = model.tex_coords0; - out.world_normal = world_normal; - out.world_position = world_position.xyz; - out.world_tangent = world_tangent; - out.world_bitangent = world_bitangent; - out.world_view_position = u_camera.view_pos.xyz; - return out; -} - -fn distribution_ggx(n_dot_h: f32, roughness: f32) -> f32 { - let a = roughness * roughness; - let a2 = a * a; - let d = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0; - return a2 / (PI * d * d); -} - -fn geometry_schlick_ggx(n_dot_x: f32, roughness: f32) -> f32 { - let r = roughness + 1.0; - let k = (r * r) / 8.0; - return n_dot_x / (n_dot_x * (1.0 - k) + k); -} - -fn geometry_smith(n_dot_v: f32, n_dot_l: f32, roughness: f32) -> f32 { - return geometry_schlick_ggx(n_dot_v, roughness) - * geometry_schlick_ggx(n_dot_l, roughness); -} - -fn fresnel_schlick(cos_theta: f32, f0: vec3<f32>) -> vec3<f32> { - return f0 + (1.0 - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); -} - -fn fresnel_schlick_roughness(cos_theta: f32, f0: vec3<f32>, roughness: f32) -> vec3<f32> { - let r1 = vec3<f32>(1.0 - roughness); - return f0 + (max(r1, f0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0); -} - -fn pbr_direct( - n: vec3<f32>, - v: vec3<f32>, - l: vec3<f32>, - albedo: vec3<f32>, - f0: vec3<f32>, - roughness: f32, - metallic: f32, -) -> vec3<f32> { - let h = normalize(v + l); - - let n_dot_v = max(dot(n, v), 0.0001); - let n_dot_l = max(dot(n, l), 0.0); - let n_dot_h = max(dot(n, h), 0.0); - let h_dot_v = max(dot(h, v), 0.0); - - let D = distribution_ggx(n_dot_h, roughness); - let G = geometry_smith(n_dot_v, n_dot_l, roughness); - let F = fresnel_schlick(h_dot_v, f0); - - let numerator = D * G * F; - let denominator = 4.0 * n_dot_v * n_dot_l + 0.0001; - let specular = numerator / denominator; - - let k_s = F; - let k_d = (1.0 - k_s) * (1.0 - metallic); - - return (k_d * albedo / PI + specular) * n_dot_l; -} - -fn directional_light_pbr( - light: Light, - n: vec3<f32>, - v: vec3<f32>, - albedo: vec3<f32>, - f0: vec3<f32>, - roughness: f32, - metallic: f32, -) -> vec3<f32> { - let l = normalize(-light.direction.xyz); - return pbr_direct(n, v, l, albedo, f0, roughness, metallic) * light.color.rgb; -} - -fn point_light_pbr( - light: Light, - n: vec3<f32>, - v: vec3<f32>, - albedo: vec3<f32>, - f0: vec3<f32>, - roughness: f32, - metallic: f32, - world_pos: vec3<f32>, -) -> vec3<f32> { - let to_light = light.position.xyz - world_pos; - let dist = length(to_light); - let l = to_light / dist; - let atten = 1.0 / (light.constant + light.lin * dist + light.quadratic * dist * dist); - return pbr_direct(n, v, l, albedo, f0, roughness, metallic) * light.color.rgb * atten; -} - -fn spot_light_pbr( - light: Light, - n: vec3<f32>, - v: vec3<f32>, - albedo: vec3<f32>, - f0: vec3<f32>, - roughness: f32, - metallic: f32, - world_pos: vec3<f32>, -) -> vec3<f32> { - let to_light = light.position.xyz - world_pos; - let dist = length(to_light); - let l = to_light / dist; - let atten = 1.0 / (light.constant + light.lin * dist + light.quadratic * dist * dist); - - let spot_dir = normalize(light.direction.xyz); - let theta = dot(-l, spot_dir); - let outer_cutoff = light.direction.w; - let epsilon = light.cutoff - outer_cutoff; - let cone_factor = clamp((theta - outer_cutoff) / epsilon, 0.0, 1.0); - - return pbr_direct(n, v, l, albedo, f0, roughness, metallic) - * light.color.rgb * atten * cone_factor; -} - -fn ibl( - n: vec3<f32>, - v: vec3<f32>, - albedo: vec3<f32>, - f0: vec3<f32>, - roughness: f32, - metallic: f32, -) -> vec3<f32> { - let n_dot_v = max(dot(n, v), 0.0001); - let num_mips = f32(textureNumLevels(env_map)); - - let F = fresnel_schlick_roughness(n_dot_v, f0, roughness); - let k_s = F; - let k_d = (1.0 - k_s) * (1.0 - metallic); - - // diffuse irradiance: most-blurred mip approximates hemisphere integral - let irradiance = textureSampleLevel(env_map, env_sampler, n, num_mips - 1.0).rgb; - let diffuse_ibl = k_d * albedo * irradiance; - - // specular: rougher surfaces sample blurrier mips - let r = reflect(-v, n); - let specular_mip = roughness * roughness * (num_mips - 1.0); - let prefiltered = textureSampleLevel(env_map, env_sampler, r, specular_mip).rgb; - - // analytic BRDF integration approximation (no LUT required) - let env_brdf_x = exp(-6.9 * roughness * roughness * n_dot_v); - let env_brdf = F * (1.0 - env_brdf_x) + f0 * env_brdf_x; - let specular_ibl = prefiltered * env_brdf; - - return diffuse_ibl + specular_ibl; -} - -fn get_normal(in: VertexOutput, uv: vec2<f32>) -> vec3<f32> { - if u_material.has_normal_texture == 0u { - return normalize(in.world_normal); - } - - let raw = textureSample(t_normal, s_normal, uv).rgb; - let unpacked = normalize((raw * 2.0 - 1.0) * vec3<f32>(u_material.normal_scale, u_material.normal_scale, 1.0)); - - let n = normalize(in.world_normal); - // Gram-Schmidt re-orthogonalise tangent against normal - var t = normalize(in.world_tangent); - t = normalize(t - n * dot(n, t)); - - let b_in = normalize(in.world_bitangent); - let handedness = select(-1.0, 1.0, dot(cross(n, t), b_in) >= 0.0); - let b = cross(n, t) * handedness; - - let tbn = mat3x3<f32>(t, b, n); - return normalize(tbn * unpacked); -} - -@fragment -fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - - // albedo + alpha - let albedo_sample = textureSample(t_diffuse, s_diffuse, uv); - let base_colour = albedo_sample * u_material.base_colour; - if base_colour.a < u_material.alpha_cutoff { - discard; - } - let albedo = base_colour.rgb; - - // metallic / roughness — glTF: B = metallic, G = roughness - var metallic = u_material.metallic; - var roughness = u_material.roughness; - if u_material.has_metallic_texture != 0u { - let mr = textureSample(t_metallic, s_metallic, uv); - metallic *= mr.b; - roughness *= mr.g; - } - metallic = clamp(metallic, 0.0, 1.0); - roughness = clamp(roughness, 0.04, 1.0); - - // occlusion - var occlusion = 1.0; - if u_material.has_occlusion_texture != 0u { - let occ = textureSample(t_occlusion, s_occlusion, uv).r; - occlusion = 1.0 + u_material.occlusion_strength * (occ - 1.0); - } - - // emissive - var emissive = u_material.emissive * u_material.emissive_strength; - if u_material.has_emissive_texture != 0u { - emissive *= textureSample(t_emissive, s_emissive, uv).rgb; - } - - let n = get_normal(in, uv); - let v = normalize(u_camera.view_pos.xyz - in.world_position); - let f0 = mix(vec3<f32>(0.04), albedo, metallic); - - // direct lighting (Cook-Torrance GGX) - var lo = vec3<f32>(0.0); - for (var i = 0u; i < u_globals.num_lights; i++) { - let light = s_light_array[i]; - let light_type = i32(light.color.w + 0.1); - - if light_type == 0 { - lo += directional_light_pbr(light, n, v, albedo, f0, roughness, metallic); - } else if light_type == 1 { - lo += point_light_pbr(light, n, v, albedo, f0, roughness, metallic, in.world_position); - } else if light_type == 2 { - lo += spot_light_pbr(light, n, v, albedo, f0, roughness, metallic, in.world_position); - } - } - - // image-based lighting - let ambient_ibl = ibl(n, v, albedo, f0, roughness, metallic) - * occlusion - * u_globals.ambient_strength; - - let colour = lo + ambient_ibl + emissive; - return vec4<f32>(colour, base_colour.a); -}