pub const FORWARD_PLUS_WGSL: &str = "// Forward+ Fragment Shader\n// Tile-based deferred light lookup with Blinn-Phong shading\n//\n// This shader uses the pre-computed light culling data to only\n// iterate over lights that actually affect the current pixel\'s tile.\n\n// --- Vertex Shader (same as lit_forward.wgsl) ---\n\nstruct CameraUniforms {\n view_projection: mat4x4<f32>,\n camera_position: vec4<f32>,\n};\n\n@group(0) @binding(0)\nvar<uniform> camera: CameraUniforms;\n\nstruct VertexInput {\n @location(0) position: vec3<f32>,\n @location(1) normal: vec3<f32>,\n @location(2) uv: vec2<f32>,\n @location(3) color: vec3<f32>,\n};\n\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 @location(3) color: vec3<f32>,\n};\n\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 let world_pos = model.model_matrix * vec4<f32>(input.position, 1.0);\n out.world_position = world_pos.xyz;\n out.clip_position = camera.view_projection * world_pos;\n out.normal = normalize((model.normal_matrix * vec4<f32>(input.normal, 0.0)).xyz);\n out.uv = input.uv;\n out.color = input.color;\n return out;\n}\n\n// --- Fragment Shader ---\n\nstruct MaterialUniforms {\n base_color: vec4<f32>,\n emissive: vec3<f32>,\n specular_power: f32,\n ambient: vec3<f32>,\n _padding: f32,\n};\n\n@group(2) @binding(0)\nvar<uniform> material: MaterialUniforms;\n\n// --- Forward+ Light Data ---\n\n// Unified light structure (matches GpuLight in Rust, 64 bytes)\nstruct GpuLight {\n position: vec3<f32>,\n range: f32,\n color: vec3<f32>,\n intensity: f32,\n direction: vec3<f32>,\n light_type: u32, // 0 = directional, 1 = point, 2 = spot\n inner_cone_cos: f32,\n outer_cone_cos: f32,\n _padding: vec2<f32>,\n};\n\n// Tile info uniform\nstruct TileInfo {\n tile_count: vec2<u32>,\n tile_size: u32,\n max_lights_per_tile: u32,\n};\n\n@group(3) @binding(0)\nvar<storage, read> lights: array<GpuLight>;\n\n@group(3) @binding(1)\nvar<storage, read> light_indices: array<u32>;\n\n@group(3) @binding(2)\nvar<storage, read> light_grid: array<u32>; // [offset, count] pairs per tile\n\n@group(3) @binding(3)\nvar<uniform> tile_info: TileInfo;\n\n// --- Lighting Functions ---\n\nfn calculate_attenuation(distance: f32, range: f32) -> f32 {\n let normalized_distance = distance / range;\n let attenuation = saturate(1.0 - normalized_distance * normalized_distance);\n return attenuation * attenuation;\n}\n\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\nfn blinn_phong(\n N: vec3<f32>,\n V: vec3<f32>,\n L: vec3<f32>,\n light_color: vec3<f32>,\n light_intensity: f32,\n diffuse_color: vec3<f32>,\n specular_power: f32\n) -> vec3<f32> {\n let NdotL = max(dot(N, L), 0.0);\n let diffuse = diffuse_color * NdotL;\n \n let H = normalize(L + V);\n let NdotH = max(dot(N, H), 0.0);\n let specular_strength = pow(NdotH, specular_power);\n let specular = vec3<f32>(specular_strength);\n \n return (diffuse + specular) * light_color * light_intensity;\n}\n\n// Calculate contribution from a single light\nfn calculate_light_contribution(\n light: GpuLight,\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 // Directional light\n if (light.light_type == 0u) {\n let L = -normalize(light.direction);\n return blinn_phong(N, V, L, light.color, light.intensity, diffuse_color, specular_power);\n }\n \n // Point/Spot light common setup\n let light_vec = light.position - world_position;\n let distance = length(light_vec);\n \n if (distance > light.range) {\n return vec3<f32>(0.0);\n }\n \n let L = normalize(light_vec);\n var attenuation = calculate_attenuation(distance, light.range);\n \n // Spot light cone attenuation\n if (light.light_type == 2u) {\n let spot_attenuation = calculate_spot_attenuation(\n L,\n normalize(light.direction),\n light.inner_cone_cos,\n light.outer_cone_cos\n );\n attenuation *= spot_attenuation;\n }\n \n if (attenuation <= 0.0) {\n return vec3<f32>(0.0);\n }\n \n return blinn_phong(N, V, L, light.color, light.intensity * attenuation, diffuse_color, specular_power);\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 let diffuse_color = material.base_color.rgb * input.color;\n \n // Calculate tile index from clip position\n let tile_x = u32(input.clip_position.x) / tile_info.tile_size;\n let tile_y = u32(input.clip_position.y) / tile_info.tile_size;\n let tile_index = tile_y * tile_info.tile_count.x + tile_x;\n \n // Read light grid for this tile\n let light_offset = light_grid[tile_index * 2u];\n let light_count = light_grid[tile_index * 2u + 1u];\n \n // Start with ambient lighting\n var final_color = material.ambient * diffuse_color;\n \n // Iterate only over lights that affect this tile\n for (var i = 0u; i < light_count; i++) {\n let light_index = light_indices[light_offset + i];\n let light = lights[light_index];\n \n final_color += calculate_light_contribution(\n light,\n input.world_position,\n N, V,\n diffuse_color,\n material.specular_power\n );\n }\n \n // Add emissive\n final_color += material.emissive;\n \n // Reinhard tone mapping\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
Forward+ rendering shader with tile-based light lookup.
Uses pre-computed light culling data to only iterate over lights
that actually affect the current pixel’s tile. Implements Blinn-Phong
lighting with the same quality as LIT_FORWARD_WGSL but with
O(lights_per_tile) complexity instead of O(all_lights).