shader.wgsl (8900B)
1 const PI: f32 = 3.14159265; 2 const EPS: f32 = 1e-4; 3 4 const MIN_ROUGHNESS: f32 = 0.04; 5 const INV_GAMMA: f32 = 1.0 / 2.2; 6 7 struct Globals { 8 time: f32, 9 _pad0: f32, 10 resolution: vec2<f32>, 11 12 cam_pos: vec3<f32>, 13 _pad3: f32, 14 15 light_dir: vec3<f32>, 16 _pad1: f32, 17 18 light_color: vec3<f32>, 19 _pad2: f32, 20 21 fill_light_dir: vec3<f32>, 22 _pad4: f32, 23 24 fill_light_color: vec3<f32>, 25 _pad5: f32, 26 27 view_proj: mat4x4<f32>, 28 inv_view_proj: mat4x4<f32>, 29 light_view_proj: mat4x4<f32>, 30 }; 31 32 @group(0) @binding(0) 33 var<uniform> globals: Globals; 34 @group(0) @binding(1) var shadow_map: texture_depth_2d; 35 @group(0) @binding(2) var shadow_sampler: sampler_comparison; 36 37 struct Object { 38 model: mat4x4<f32>, 39 normal: mat4x4<f32>, 40 }; 41 42 @group(1) @binding(0) 43 var<uniform> object: Object; 44 45 struct Material { 46 base_color_factor: vec4<f32>, 47 metallic_factor: f32, 48 roughness_factor: f32, 49 ao_strength: f32, 50 _pad0: f32, 51 }; 52 53 @group(2) @binding(0) var<uniform> material: Material; 54 @group(2) @binding(1) var material_sampler: sampler; 55 @group(2) @binding(2) var basecolor_tex: texture_2d<f32>; 56 @group(2) @binding(3) var ao_mr_tex: texture_2d<f32>; 57 @group(2) @binding(4) var normal_tex: texture_2d<f32>; 58 59 // IBL 60 @group(3) @binding(0) var irradiance_map: texture_cube<f32>; 61 @group(3) @binding(1) var ibl_sampler: sampler; 62 @group(3) @binding(2) var prefiltered_map: texture_cube<f32>; 63 @group(3) @binding(3) var brdf_lut: texture_2d<f32>; 64 65 struct VSIn { 66 @location(0) pos: vec3<f32>, 67 @location(1) normal: vec3<f32>, 68 @location(2) uv: vec2<f32>, 69 @location(3) tangent: vec4<f32>, 70 }; 71 72 struct VSOut { 73 @builtin(position) clip: vec4<f32>, 74 @location(0) world_pos: vec3<f32>, 75 @location(1) world_normal: vec3<f32>, 76 @location(2) uv: vec2<f32>, 77 @location(3) world_tangent: vec4<f32>, // xyz + w 78 }; 79 80 81 @vertex 82 fn vs_main(v: VSIn) -> VSOut { 83 var out: VSOut; 84 85 // For now: identity model matrix. Next step is per-object transforms. 86 let world4 = object.model * vec4<f32>(v.pos, 1.0); 87 out.world_pos = world4.xyz; 88 89 let n4 = object.normal * vec4<f32>(v.normal, 0.0); 90 let t4 = object.model * vec4<f32>(v.tangent.xyz, 0.0); 91 92 out.world_normal = normalize(n4.xyz); 93 out.uv = v.uv; 94 out.clip = globals.view_proj * world4; 95 out.world_tangent = vec4<f32>(normalize(t4.xyz), v.tangent.w); 96 return out; 97 } 98 99 fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); } 100 fn saturate3(v: vec3<f32>) -> vec3<f32> { return clamp(v, vec3<f32>(0.0), vec3<f32>(1.0)); } 101 102 fn safe_normalize(v: vec3<f32>) -> vec3<f32> { 103 let l2 = dot(v, v); 104 if (l2 <= EPS) { return vec3<f32>(0.0, 0.0, 1.0); } 105 return v * inverseSqrt(l2); 106 } 107 108 // Fresnel (Schlick) 109 fn fresnel_schlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> { 110 let ct = saturate(cosTheta); 111 let f = pow(1.0 - ct, 5.0); 112 return F0 + (1.0 - F0) * f; 113 } 114 115 // Fresnel with roughness attenuation for IBL 116 fn fresnel_schlick_roughness(cosTheta: f32, F0: vec3<f32>, roughness: f32) -> vec3<f32> { 117 let ct = saturate(cosTheta); 118 let f = pow(1.0 - ct, 5.0); 119 return F0 + (max(vec3<f32>(1.0 - roughness), F0) - F0) * f; 120 } 121 122 // Specular IBL using split-sum approximation 123 fn specular_ibl(N: vec3<f32>, V: vec3<f32>, F0: vec3<f32>, roughness: f32) -> vec3<f32> { 124 let R = reflect(-V, N); 125 126 // Sample pre-filtered environment at roughness-based mip level 127 let MAX_REFLECTION_LOD = 4.0; // mip_count - 1 128 let prefiltered = textureSampleLevel( 129 prefiltered_map, 130 ibl_sampler, 131 R, 132 roughness * MAX_REFLECTION_LOD 133 ).rgb; 134 135 // Sample BRDF LUT 136 let NdotV = saturate(dot(N, V)); 137 let brdf = textureSample(brdf_lut, ibl_sampler, vec2<f32>(NdotV, roughness)).rg; 138 139 // Combine using split-sum: prefiltered * (F0 * scale + bias) 140 return prefiltered * (F0 * brdf.x + brdf.y); 141 } 142 143 // GGX / Trowbridge-Reitz NDF 144 fn ggx_ndf(NdotH: f32, alpha: f32) -> f32 { 145 let a2 = alpha * alpha; 146 let d = (NdotH * NdotH) * (a2 - 1.0) + 1.0; 147 return a2 / (PI * d * d); 148 } 149 150 // Smith geometry with Schlick-GGX (UE4 k) 151 fn smith_g_schlick_ggx(NdotV: f32, NdotL: f32, alpha: f32) -> f32 { 152 let k = alpha + 1.0; 153 let k2 = (k * k) / 8.0; 154 155 let gv = NdotV / (NdotV * (1.0 - k2) + k2); 156 let gl = NdotL / (NdotL * (1.0 - k2) + k2); 157 return gv * gl; 158 } 159 160 fn diffuse_lambert(diffuseColor: vec3<f32>) -> vec3<f32> { 161 return diffuseColor / PI; 162 } 163 164 // RG normal map decode (optionally flips Y for your convention) 165 fn decode_normal_rg(tex: vec3<f32>) -> vec3<f32> { 166 let x = tex.r * 2.0 - 1.0; 167 var y = tex.g * 2.0 - 1.0; 168 y = -y; // <- keep your current behavior here 169 170 let z2 = max(1.0 - x*x - y*y, 0.0); 171 let z = sqrt(z2); 172 return safe_normalize(vec3<f32>(x, y, z)); 173 } 174 175 fn calc_shadow(world_pos: vec3<f32>) -> f32 { 176 let light_clip = globals.light_view_proj * vec4<f32>(world_pos, 1.0); 177 let ndc = light_clip.xyz / light_clip.w; 178 179 // Convert from NDC [-1,1] to UV [0,1], flip Y for texture coords 180 let shadow_uv = vec2<f32>(ndc.x * 0.5 + 0.5, -ndc.y * 0.5 + 0.5); 181 182 // Out-of-bounds = fully lit 183 if shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0 { 184 return 1.0; 185 } 186 187 let ref_depth = ndc.z; 188 189 // 3x3 PCF for soft shadow edges 190 let texel_size = 1.0 / 2048.0; 191 var shadow = 0.0; 192 for (var y = -1i; y <= 1i; y++) { 193 for (var x = -1i; x <= 1i; x++) { 194 let offset = vec2<f32>(f32(x), f32(y)) * texel_size; 195 shadow += textureSampleCompareLevel( 196 shadow_map, 197 shadow_sampler, 198 shadow_uv + offset, 199 ref_depth, 200 ); 201 } 202 } 203 return shadow / 9.0; 204 } 205 206 fn build_tbn(Ng: vec3<f32>, world_tangent: vec4<f32>) -> mat3x3<f32> { 207 var T = safe_normalize(world_tangent.xyz); 208 T = safe_normalize(T - Ng * dot(Ng, T)); 209 let B = safe_normalize(cross(Ng, T)) * world_tangent.w; 210 return mat3x3<f32>(T, B, Ng); 211 } 212 213 @fragment 214 fn fs_main(in: VSOut) -> @location(0) vec4<f32> { 215 let Ng = safe_normalize(in.world_normal); 216 let tbn = build_tbn(Ng, in.world_tangent); 217 218 let n_ts = decode_normal_rg(textureSample(normal_tex, material_sampler, in.uv).xyz); 219 let N = safe_normalize(tbn * n_ts); 220 221 let V = safe_normalize(globals.cam_pos - in.world_pos); 222 let L = safe_normalize(-globals.light_dir); 223 let H = safe_normalize(V + L); 224 225 let NdotL = saturate(dot(N, L)); 226 let NdotV = saturate(dot(N, V)); 227 let NdotH = saturate(dot(N, H)); 228 let VdotH = saturate(dot(V, H)); 229 230 let bc_tex = textureSample(basecolor_tex, material_sampler, in.uv); 231 let ao_mr = textureSample(ao_mr_tex, material_sampler, in.uv); 232 233 // glTF metallicRoughnessTexture: G=roughness, B=metallic 234 let baseColor = bc_tex.rgb * material.base_color_factor.rgb; 235 let metallic = saturate(ao_mr.b * material.metallic_factor); 236 let rough_in = ao_mr.g * material.roughness_factor; 237 238 // AO: R channel; strength lerp from 1 -> ao 239 let ao_tex = ao_mr.r; 240 let ao = 1.0 + (ao_tex - 1.0) * saturate(material.ao_strength); 241 //let ao = 1.0; 242 243 let roughness = clamp(rough_in, MIN_ROUGHNESS, 1.0); 244 let alpha = roughness * roughness; 245 246 let F0 = mix(vec3<f32>(0.04), baseColor, metallic); 247 let diffuseColor = baseColor * (1.0 - metallic); 248 249 let D = ggx_ndf(NdotH, alpha); 250 let Gs = smith_g_schlick_ggx(NdotV, NdotL, alpha); 251 let F = fresnel_schlick(VdotH, F0); 252 253 let denom = max(4.0 * NdotV * NdotL, EPS); 254 let spec = (D * Gs) * F / denom; 255 256 let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic); 257 let diff = kd * diffuse_lambert(diffuseColor); 258 259 let shadow = calc_shadow(in.world_pos); 260 let direct = (diff + spec) * (globals.light_color * NdotL) * shadow; 261 262 // Fill light contribution 263 let L2 = safe_normalize(-globals.fill_light_dir); 264 let H2 = safe_normalize(V + L2); 265 let NdotL2 = saturate(dot(N, L2)); 266 let NdotH2 = saturate(dot(N, H2)); 267 let VdotH2 = saturate(dot(V, H2)); 268 269 let D2 = ggx_ndf(NdotH2, alpha); 270 let Gs2 = smith_g_schlick_ggx(NdotV, NdotL2, alpha); 271 let F2 = fresnel_schlick(VdotH2, F0); 272 let denom2 = max(4.0 * NdotV * NdotL2, EPS); 273 let spec2 = (D2 * Gs2) * F2 / denom2; 274 let kd2 = (vec3<f32>(1.0) - F2) * (1.0 - metallic); 275 let diff2 = kd2 * diffuse_lambert(diffuseColor); 276 let fill = (diff2 + spec2) * (globals.fill_light_color * NdotL2); 277 278 // IBL ambient lighting with energy conservation 279 let irradiance = textureSample(irradiance_map, ibl_sampler, N).rgb; 280 281 // Specular IBL 282 let specular_ambient = specular_ibl(N, V, F0, roughness); 283 284 // Energy conservation: kS is already accounted for in specular_ibl via F0 285 // kD ensures diffuse doesn't add energy where specular dominates 286 let kS = fresnel_schlick_roughness(NdotV, F0, roughness); 287 let kD = (1.0 - kS) * (1.0 - metallic); 288 289 let diffuse_ambient = kD * diffuseColor * irradiance; 290 let ambient = (diffuse_ambient + specular_ambient) * ao; 291 292 var col = direct + fill + ambient; 293 294 // simple tonemap + gamma 295 col = col / (col + vec3<f32>(1.0)); 296 col = pow(col, vec3<f32>(INV_GAMMA)); 297 298 return vec4<f32>(saturate3(col), 1.0); 299 }