1import package::render::clustered_forward;2import package::render::clustered_forward::ClusterableObjectIndexRanges;3import package::render::mesh_view_bindings::light_probes;4import package::render::mesh_view_types::{5 LightProbe, LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE,6 LIGHT_PROBE_FLAG_PARALLAX_CORRECT7};9/// The result of searching for a light probe.10///11/// Light probe iterators yield values of this type. Note that multiple light12/// probes can affect a single fragment.13struct LightProbeQueryResult {14 /// The index of the light probe texture or textures in the binding array or15 /// arrays.16 texture_index: i32,17 /// A scale factor that's applied to the diffuse and specular light from the18 /// light probe. This is in units of cd/m² (candela per square meter).19 intensity: f32,20 /// Transform from world space to the light probe model space. In light probe21 /// model space, the light probe is a 1×1×1 cube centered on the origin.22 light_from_world: mat4x4<f32>,23 /// The boundaries of the simulated space used for parallax correction,24 /// specified as *half* extents in light probe space.25 parallax_correction_bounds: vec3<f32>,26 /// The weight of this light probe, determined by the position of the27 /// fragment within the falloff range. The sum of the weights of all light28 /// probes affecting a fragment need not be 1.29 weight: f32,30 /// The flags that the light probe has: a combination of31 /// `LIGHT_PROBE_FLAG_*`.32 flags: u32,33};35fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {36 let matrix4x4 = mat4x4<f32>(37 matrix[0],38 matrix[1],39 matrix[2],40 vec4<f32>(0.0, 0.0, 0.0, 1.0));41 return transpose(matrix4x4);42}44/// A type that allows iterating through the list of light probes that overlap45/// the current fragment.46///47/// This is the version used when light probes are clustered.48@if(AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)49struct LightProbeIterator {50 /// The current offset in the light probes list.51 current_offset: u32,52 /// The last offset in the light probes list.53 end_offset: u32,54 /// The world-space position of the current fragment.55 world_position: vec3<f32>,56 /// True if we're searching for an irradiance volume; false if we're57 /// searching for a reflection probe.58 is_irradiance_volume: bool,59}61/// Creates a new light probe iterator ready to iterate through light probes in62/// the froxel containing the `world_position`.63@if(AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)64fn light_probe_iterator_new(65 world_position: vec3<f32>,66 is_irradiance_volume: bool,67 clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,68) -> LightProbeIterator {69 // Reflection probe indices are followed by irradiance volume indices in the70 // cluster index list. Use this fact to create our bracketing range of71 // indices.73 if is_irradiance_volume {74 return LightProbeIterator(75 (*clusterable_object_index_ranges).first_irradiance_volume_index_offset,76 (*clusterable_object_index_ranges).first_decal_offset,77 world_position,78 true79 );80 }82 return LightProbeIterator(83 (*clusterable_object_index_ranges).first_reflection_probe_index_offset,84 (*clusterable_object_index_ranges).first_irradiance_volume_index_offset,85 world_position,86 false87 );88}90/// Searches for a light probe that contains the fragment and returns the next91/// such probe.92///93/// Note that multiple light probes can affect a fragment. The caller is94/// generally expected to blend their influences together in a weighted sum.95@if(AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)96fn light_probe_iterator_next(iterator: ptr<function, LightProbeIterator>) -> LightProbeQueryResult {97 let world_position = (*iterator).world_position;99 var result: LightProbeQueryResult;100 result.texture_index = -1;101 result.weight = 0.0;103 while ((*iterator).current_offset < (*iterator).end_offset) {104 let light_probe_index = i32(clustered_forward::get_clusterable_object_id(105 (*iterator).current_offset));106 (*iterator).current_offset += 1u;108 // FIXME: This happens when one or more images for the light probe109 // aren't loaded yet. Really, though, we shouldn't be clustering such110 // objects at all.111 if (light_probe_index < 0) {112 continue;113 }115 var light_probe: LightProbe;116 if (*iterator).is_irradiance_volume {117 light_probe = light_probes.irradiance_volumes[light_probe_index];118 } else {119 light_probe = light_probes.reflection_probes[light_probe_index];120 }122 // Unpack the inverse transform.123 let light_from_world =124 transpose_affine_matrix(light_probe.light_from_world_transposed);126 // Transform the point into local space, with the cube edges at ±0.5 on127 // each axis.128 let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;129 // Avoid division by zero.130 let falloff = max(light_probe.falloff, vec3(0.0001));131 // Calculate the per-axis weight by doing a linear ramp from 0.0 at the132 // inside of the falloff region to 1.0 at the outside of the falloff133 // region.134 let axis_weights = saturate((1.0 - 2.0 * abs(probe_space_pos)) / (2.0 * falloff));135 // The actual weight is the minimum of all the per-axis weights.136 let weight = min(min(axis_weights.x, axis_weights.y), axis_weights.z);137 // If the resulting weight is zero, we're outside the light probe138 // entirely. Bail.139 if (weight == 0.0) {140 continue;141 }143 result.texture_index = light_probe.cubemap_index;144 result.intensity = light_probe.intensity;145 result.light_from_world = light_from_world;146 result.parallax_correction_bounds = light_probe.parallax_correction_bounds;147 result.flags = light_probe.flags;148 result.weight = weight;149 return result;150 }152 return result;153}155/// A type that allows iterating through the list of light probes that overlap156/// the current fragment.157///158/// This is the version that's used when sufficient storage buffers aren't159/// available and consequently when light probes aren't clustered. It simply does160/// a brute force search of all light probes. Because platforms without161/// sufficient SSBO bindings typically lack bindless shaders, there will usually162/// only be one of each type of light probe present anyway.163@if(!AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)164struct LightProbeIterator {165 /// The current index in the list of light probes for this cluster.166 current_index: u32,167 /// The last index in the list.168 end_index: u32,169 /// The position of the current fragment.170 world_position: vec3<f32>,171 /// True if we're searching for irradiance volumes; false if we're searching172 /// for reflection probes.173 is_irradiance_volume: bool,174}176/// Creates a new light probe iterator ready to search through light probes.177@if(!AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)178fn light_probe_iterator_new(179 world_position: vec3<f32>,180 is_irradiance_volume: bool,181 clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,182) -> LightProbeIterator {183 return LightProbeIterator(184 0,185 select(186 light_probes.reflection_probe_count,187 light_probes.irradiance_volume_count,188 is_irradiance_volume189 ),190 world_position,191 is_irradiance_volume192 );193}195/// Searches for a light probe that contains the fragment and returns the next196/// such probe.197///198/// Note that, theoretically, multiple light probes can affect a fragment, and199/// the caller is generally expected to blend their influences together in a200/// weighted sum. In practice, this version of `light_probe_iterator_next` is201/// used on platforms that lack bindless shaders, so there will only be at most202/// one light probe that affects the current fragment in the first place.203@if(!AVAILABLE_STORAGE_BUFFER_BINDINGS__GE_3)204fn light_probe_iterator_next(iterator: ptr<function, LightProbeIterator>) -> LightProbeQueryResult {205 var result: LightProbeQueryResult;206 result.texture_index = -1;207 result.weight = 0.0;209 while (true) {210 let light_probe_index = (*iterator).current_index;212 var light_probe: LightProbe;213 if is_irradiance_volume {214 light_probe = light_probes.irradiance_volumes[light_probe_index];215 } else {216 light_probe = light_probes.reflection_probes[light_probe_index];217 }219 // Unpack the inverse transform.220 let light_from_world =221 transpose_affine_matrix(light_probe.light_from_world_transposed);223 // Transform the point into local space, with the cube edges at ±0.5 on224 // each axis.225 let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;226 // Avoid division by zero.227 let falloff = max(light_probe.falloff, vec3(0.0001));228 // Calculate the per-axis weight by doing a linear ramp from 0.0 at the229 // inside of the falloff region to 1.0 at the outside of the falloff230 // region.231 let axis_weights = saturate((1.0 - 2.0 * abs(probe_space_pos)) / (2.0 * falloff));232 // The actual weight is the minimum of all the per-axis weights.233 let weight = min(min(axis_weights.x, axis_weights.y), axis_weights.z);234 // If the resulting weight is zero, we're outside the light probe235 // entirely. Bail.236 if (weight == 0.0) {237 continue;238 }240 result.texture_index = light_probe.cubemap_index;241 result.intensity = light_probe.intensity;242 result.light_from_world = light_from_world;243 result.parallax_correction_bounds = light_probe.parallax_correction_bounds;244 result.flags = light_probe.flags;245 result.weight = weight;246 return result;247 }249 return result;250}