pub const LIT_FORWARD_WGSL: &str = "// Lit Forward Shader\n// Multi-light forward rendering with Blinn-Phong lighting + Shadow Mapping\n// Supports: 4 directional + 16 point + 8 spot lights\n\n// --- Vertex Shader ---\n\n// Camera uniform block containing view and projection matrices\nstruct CameraUniforms {\n view_projection: mat4x4<f32>, // Combined projection * view matrix\n camera_position: vec4<f32>, // Camera position in world space (w is padding)\n};\n\n@group(0) @binding(0)\nvar<uniform> camera: CameraUniforms;\n\n// Vertex input from the vertex buffer\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n};\n\n// Output from vertex shader to fragment shader\nstruct VertexOutput {\n @builtin(position) clip_position: vec4<f32>,\n @location(0) world_position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n};\n\n// Model transform uniform\nstruct ModelUniforms {\n model_matrix: mat4x4<f32>,\n normal_matrix: mat4x4<f32>,\n};\n\n@group(1) @binding(0)\nvar<uniform> model: ModelUniforms;\n\n@vertex\nfn vs_main(input: VertexInput) -> VertexOutput {\n var out: VertexOutput;\n \n // Transform vertex position to world space\n let world_pos = model.model_matrix * vec4<f32>(input.position, 1.0);\n out.world_position = world_pos.xyz;\n \n // Transform to clip space\n out.clip_position = camera.view_projection * world_pos;\n \n // Transform normal to world space (using normal matrix to handle non-uniform scales)\n out.normal = normalize((model.normal_matrix * vec4<f32>(input.normal, 0.0)).xyz);\n \n // Pass through UV\n out.uv = input.uv;\n \n return out;\n}\n\n\n// --- Fragment Shader ---\n\n// Material properties uniform\nstruct MaterialUniforms {\n base_color: vec4<f32>, // RGBA base color\n emissive: vec3<f32>, // RGB emissive color\n specular_power: f32, // Specular exponent (shininess)\n ambient: vec3<f32>, // Ambient color\n _padding: f32, // Alignment padding\n};\n\n@group(2) @binding(0)\nvar<uniform> material: MaterialUniforms;\n\n// --- Light Structures (must match Rust repr(C) layout) ---\n\nstruct DirectionalLight {\n direction: vec4<f32>, // xyz = direction, w = padding\n color: vec4<f32>, // rgb = color, a = intensity\n shadow_view_proj: mat4x4<f32>, // Light\'s view-projection for shadow mapping\n shadow_params: vec4<f32>, // x = atlas_index (-1 = no shadow), y = bias, z = normal_bias, w = padding\n};\n\nstruct PointLight {\n position: vec4<f32>, // xyz = position, w = range\n color: vec4<f32>, // rgb = color, a = intensity\n shadow_params: vec4<f32>, // x = atlas_index (-1 = no shadow), y = bias, z = normal_bias, w = padding\n};\n\nstruct SpotLight {\n position: vec4<f32>, // xyz = position, w = range\n direction: vec4<f32>, // xyz = direction, w = inner_cone_cos\n color: vec4<f32>, // rgb = color, a = intensity\n params: vec4<f32>, // x = outer_cone_cos, yzw = padding\n shadow_view_proj: mat4x4<f32>, // Light\'s view-projection for shadow mapping\n shadow_params: vec4<f32>, // x = atlas_index (-1 = no shadow), y = bias, z = normal_bias, w = padding\n};\n\n// Light arrays with fixed sizes matching LitForwardLane defaults\nconst MAX_DIRECTIONAL_LIGHTS: u32 = 4u;\nconst MAX_POINT_LIGHTS: u32 = 16u;\nconst MAX_SPOT_LIGHTS: u32 = 8u;\n\nstruct LightingUniforms {\n directional_lights: array<DirectionalLight, 4>,\n point_lights: array<PointLight, 16>,\n spot_lights: array<SpotLight, 8>,\n num_directional_lights: u32,\n num_point_lights: u32,\n num_spot_lights: u32,\n _padding: u32,\n};\n\n@group(3) @binding(0)\nvar<uniform> lights: LightingUniforms;\n\n// Shadow atlas (2D array depth texture) and comparison sampler\n@group(3) @binding(1)\nvar shadow_atlas: texture_depth_2d_array;\n\n@group(3) @binding(2)\nvar shadow_sampler: sampler_comparison;\n\n// --- Shadow Sampling ---\n\n/// Samples the shadow atlas with PCF (Percentage Closer Filtering).\n/// Returns a shadow factor: 1.0 = fully lit, 0.0 = fully in shadow.\nfn sample_shadow_pcf(\n shadow_vp: mat4x4<f32>,\n world_pos: vec3<f32>,\n N: vec3<f32>,\n atlas_index: i32,\n bias: f32,\n normal_bias: f32,\n) -> f32 {\n if (atlas_index < 0) {\n return 1.0; // No shadow map for this light\n }\n\n // Apply normal bias to push the sample point along the surface normal\n let biased_pos = world_pos + N * normal_bias;\n\n // Project world position into light clip space\n let light_clip = shadow_vp * vec4<f32>(biased_pos, 1.0);\n let light_ndc = light_clip.xyz / light_clip.w;\n\n // Convert from NDC [-1,1] to UV [0,1] (note: Y is flipped)\n let shadow_uv = vec2<f32>(\n light_ndc.x * 0.5 + 0.5,\n 1.0 - (light_ndc.y * 0.5 + 0.5),\n );\n\n // If outside the shadow map, treat as lit\n if (shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0) {\n return 1.0;\n }\n\n // Depth in [0,1] range (RH zero-to-one projection)\n let depth = light_ndc.z - bias;\n\n // 3x3 PCF kernel for soft shadow edges\n let texel_size = 1.0 / 2048.0; // Atlas resolution\n var shadow = 0.0;\n for (var y = -1; y <= 1; y++) {\n for (var x = -1; x <= 1; x++) {\n let offset = vec2<f32>(f32(x), f32(y)) * texel_size;\n shadow += textureSampleCompareLevel(\n shadow_atlas,\n shadow_sampler,\n shadow_uv + offset,\n atlas_index,\n depth,\n );\n }\n }\n return shadow / 9.0;\n}\n\n// --- Lighting Functions ---\n\n/// Calculates attenuation for point/spot lights based on distance and range\nfn calculate_attenuation(distance: f32, range: f32) -> f32 {\n // Smooth attenuation that reaches zero at range\n let normalized_distance = distance / range;\n let attenuation = saturate(1.0 - normalized_distance * normalized_distance);\n return attenuation * attenuation;\n}\n\n/// Calculates spotlight cone attenuation\nfn calculate_spot_attenuation(\n light_dir: vec3<f32>,\n spot_direction: vec3<f32>,\n inner_cone_cos: f32,\n outer_cone_cos: f32\n) -> f32 {\n let cos_angle = dot(-light_dir, spot_direction);\n return smoothstep(outer_cone_cos, inner_cone_cos, cos_angle);\n}\n\n/// Blinn-Phong BRDF calculation\nfn blinn_phong(\n N: vec3<f32>, // Surface normal (normalized)\n V: vec3<f32>, // View direction (normalized)\n L: vec3<f32>, // Light direction (normalized, pointing toward light)\n light_color: vec3<f32>,\n light_intensity: f32,\n diffuse_color: vec3<f32>,\n specular_power: f32\n) -> vec3<f32> {\n // Diffuse component (Lambertian)\n let NdotL = max(dot(N, L), 0.0);\n let diffuse = diffuse_color * NdotL;\n \n // Specular component (Blinn-Phong)\n let H = normalize(L + V); // Half vector\n let NdotH = max(dot(N, H), 0.0);\n // Only apply specular if the light is actually hitting the front of the surface\n var specular_strength = 0.0;\n if (NdotL > 0.0) {\n specular_strength = pow(NdotH, specular_power);\n }\n let specular = vec3<f32>(specular_strength);\n \n return (diffuse + specular) * light_color * light_intensity;\n}\n\n/// Calculate contribution from all directional lights (with shadows)\nfn calculate_directional_lights(\n world_position: vec3<f32>,\n N: vec3<f32>,\n V: vec3<f32>,\n diffuse_color: vec3<f32>,\n specular_power: f32\n) -> vec3<f32> {\n var result = vec3<f32>(0.0);\n \n for (var i = 0u; i < lights.num_directional_lights && i < MAX_DIRECTIONAL_LIGHTS; i++) {\n let light = lights.directional_lights[i];\n let L = -normalize(light.direction.xyz); // Reverse direction (toward light)\n \n // Shadow factor\n let shadow = sample_shadow_pcf(\n light.shadow_view_proj,\n world_position,\n N,\n i32(light.shadow_params.x),\n light.shadow_params.y,\n light.shadow_params.z,\n );\n \n result += blinn_phong(\n N, V, L,\n light.color.rgb,\n light.color.a,\n diffuse_color,\n specular_power\n ) * shadow;\n }\n \n return result;\n}\n\n/// Calculate contribution from all point lights\nfn calculate_point_lights(\n world_position: vec3<f32>,\n N: vec3<f32>,\n V: vec3<f32>,\n diffuse_color: vec3<f32>,\n specular_power: f32\n) -> vec3<f32> {\n var result = vec3<f32>(0.0);\n \n for (var i = 0u; i < lights.num_point_lights && i < MAX_POINT_LIGHTS; i++) {\n let light = lights.point_lights[i];\n let light_vec = light.position.xyz - world_position;\n let distance = length(light_vec);\n \n // Skip if outside range\n if (distance > light.position.w) {\n continue;\n }\n \n let L = normalize(light_vec);\n let attenuation = calculate_attenuation(distance, light.position.w);\n \n result += blinn_phong(\n N, V, L,\n light.color.rgb,\n light.color.a * attenuation,\n diffuse_color,\n specular_power\n );\n }\n \n return result;\n}\n\n/// Calculate contribution from all spot lights (with shadows)\nfn calculate_spot_lights(\n world_position: vec3<f32>,\n N: vec3<f32>,\n V: vec3<f32>,\n diffuse_color: vec3<f32>,\n specular_power: f32\n) -> vec3<f32> {\n var result = vec3<f32>(0.0);\n \n for (var i = 0u; i < lights.num_spot_lights && i < MAX_SPOT_LIGHTS; i++) {\n let light = lights.spot_lights[i];\n let light_vec = light.position.xyz - world_position;\n let distance = length(light_vec);\n \n // Skip if outside range\n if (distance > light.position.w) {\n continue;\n }\n \n let L = normalize(light_vec);\n let distance_attenuation = calculate_attenuation(distance, light.position.w);\n let spot_attenuation = calculate_spot_attenuation(\n L,\n normalize(light.direction.xyz),\n light.direction.w,\n light.params.x\n );\n let total_attenuation = distance_attenuation * spot_attenuation;\n \n // Skip if outside cone\n if (total_attenuation <= 0.0) {\n continue;\n }\n\n // Shadow factor\n let shadow = sample_shadow_pcf(\n light.shadow_view_proj,\n world_position,\n N,\n i32(light.shadow_params.x),\n light.shadow_params.y,\n light.shadow_params.z,\n );\n \n result += blinn_phong(\n N, V, L,\n light.color.rgb,\n light.color.a * total_attenuation,\n diffuse_color,\n specular_power\n ) * shadow;\n }\n \n return result;\n}\n\n@fragment\nfn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {\n // Prepare surface data\n let N = normalize(input.normal);\n let V = normalize(camera.camera_position.xyz - input.world_position);\n \n // Calculate base diffuse color (material * vertex color)\n let diffuse_color = material.base_color.rgb;\n \n // Start with ambient lighting\n var final_color = material.ambient * diffuse_color;\n \n // Add contributions from all light types (with shadow mapping)\n final_color += calculate_directional_lights(input.world_position, N, V, diffuse_color, material.specular_power);\n final_color += calculate_point_lights(input.world_position, N, V, diffuse_color, material.specular_power);\n final_color += calculate_spot_lights(input.world_position, N, V, diffuse_color, material.specular_power);\n \n // Add emissive\n final_color += material.emissive;\n \n // Simple tone mapping (Reinhard)\n final_color = final_color / (final_color + vec3<f32>(1.0));\n \n // Gamma correction\n final_color = pow(final_color, vec3<f32>(1.0 / 2.2));\n \n return vec4<f32>(final_color, material.base_color.a);\n}\n";Expand description
Lit forward rendering shader with multi-light Blinn-Phong lighting.
Supports:
- Up to 4 directional lights
- Up to 16 point lights
- Up to 8 spot lights
Uses Blinn-Phong BRDF with Reinhard tone mapping.