LIT_FORWARD_WGSL

Constant LIT_FORWARD_WGSL 

Source
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.