// Stage 4 forward lit shader: a single directional light with Lambert diffuse, // ambient, and a Blinn-Phong specular term scaled by material roughness/metallic // (PBR-lite). Output is linear color; an sRGB surface format converts on write. struct Globals { view_proj: mat4x4, camera_pos: vec4, // xyz world-space camera position light_dir: vec4, // xyz unit vector pointing TOWARD the light light_color: vec4, // rgb light color * intensity ambient: vec4, // rgb ambient term }; struct ObjectData { model: mat4x4, normal_mtx: mat4x4, // inverse-transpose of model (3x3 in a 4x4) albedo: vec4, mr: vec4, // x = metallic, y = roughness }; @group(0) @binding(0) var globals: Globals; @group(1) @binding(0) var obj: ObjectData; struct VsOut { @builtin(position) clip_pos: vec4, @location(0) world_pos: vec3, @location(1) world_normal: vec3, @location(2) uv: vec2, }; @vertex fn vs_main( @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, ) -> VsOut { let world = obj.model * vec4(position, 1.0); var out: VsOut; out.world_pos = world.xyz; out.world_normal = (obj.normal_mtx * vec4(normal, 0.0)).xyz; out.uv = uv; out.clip_pos = globals.view_proj * world; return out; } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { let n = normalize(in.world_normal); let l = normalize(globals.light_dir.xyz); let v = normalize(globals.camera_pos.xyz - in.world_pos); let h = normalize(l + v); let albedo = obj.albedo.rgb; let metallic = obj.mr.x; let roughness = clamp(obj.mr.y, 0.04, 1.0); let ndl = max(dot(n, l), 0.0); let ndh = max(dot(n, h), 0.0); // Metals have no diffuse; dielectrics get a fixed 0.04 specular, metals // tint their specular by the albedo. let diffuse = albedo * (1.0 - metallic); let spec_color = mix(vec3(0.04), albedo, metallic); let spec_power = mix(8.0, 256.0, 1.0 - roughness); let spec = spec_color * pow(ndh, spec_power) * select(0.0, 1.0, ndl > 0.0); let direct = (diffuse * ndl + spec) * globals.light_color.rgb; let ambient = albedo * globals.ambient.rgb; return vec4(ambient + direct, obj.albedo.a); }