Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/light_probe/light_probe.wgsl
9492 views
#define_import_path bevy_pbr::light_probe

#import bevy_pbr::clustered_forward
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
#import bevy_pbr::mesh_view_bindings::light_probes
#import bevy_pbr::mesh_view_types::{
    LightProbe, LIGHT_PROBE_FLAG_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE,
    LIGHT_PROBE_FLAG_PARALLAX_CORRECT
}

// The result of searching for a light probe.
//
// Light probe iterators yield values of this type. Note that multiple light
// probes can affect a single fragment.
struct LightProbeQueryResult {
    // The index of the light probe texture or textures in the binding array or
    // arrays.
    texture_index: i32,
    // A scale factor that's applied to the diffuse and specular light from the
    // light probe. This is in units of cd/m² (candela per square meter).
    intensity: f32,
    // Transform from world space to the light probe model space. In light probe
    // model space, the light probe is a 1×1×1 cube centered on the origin.
    light_from_world: mat4x4<f32>,
    // The boundaries of the simulated space used for parallax correction,
    // specified as *half* extents in light probe space.
    parallax_correction_bounds: vec3<f32>,
    // The weight of this light probe, determined by the position of the
    // fragment within the falloff range. The sum of the weights of all light
    // probes affecting a fragment need not be 1.
    weight: f32,
    // The flags that the light probe has: a combination of
    // `LIGHT_PROBE_FLAG_*`.
    flags: u32,
};

fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {
    let matrix4x4 = mat4x4<f32>(
        matrix[0],
        matrix[1],
        matrix[2],
        vec4<f32>(0.0, 0.0, 0.0, 1.0));
    return transpose(matrix4x4);
}

#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3

// A type that allows iterating through the list of light probes that overlap
// the current fragment.
//
// This is the version used when light probes are clustered.
struct LightProbeIterator {
    // The current offset in the light probes list.
    current_offset: u32,
    // The last offset in the light probes list.
    end_offset: u32,
    // The world-space position of the current fragment.
    world_position: vec3<f32>,
    // True if we're searching for an irradiance volume; false if we're
    // searching for a reflection probe.
    is_irradiance_volume: bool,
}

// Creates a new light probe iterator ready to iterate through light probes in
// the froxel containing the `world_position`.
fn light_probe_iterator_new(
    world_position: vec3<f32>,
    is_irradiance_volume: bool,
    clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> LightProbeIterator {
    // Reflection probe indices are followed by irradiance volume indices in the
    // cluster index list. Use this fact to create our bracketing range of
    // indices.

    if is_irradiance_volume {
        return LightProbeIterator(
            (*clusterable_object_index_ranges).first_irradiance_volume_index_offset,
            (*clusterable_object_index_ranges).first_decal_offset,
            world_position,
            true
        );
    }

    return LightProbeIterator(
        (*clusterable_object_index_ranges).first_reflection_probe_index_offset,
        (*clusterable_object_index_ranges).first_irradiance_volume_index_offset,
        world_position,
        false
    );
}

// Searches for a light probe that contains the fragment and returns the next
// such probe.
//
// Note that multiple light probes can affect a fragment. The caller is
// generally expected to blend their influences together in a weighted sum.
fn light_probe_iterator_next(iterator: ptr<function, LightProbeIterator>) -> LightProbeQueryResult {
    let world_position = (*iterator).world_position;

    var result: LightProbeQueryResult;
    result.texture_index = -1;
    result.weight = 0.0;

    while ((*iterator).current_offset < (*iterator).end_offset) {
        let light_probe_index = i32(clustered_forward::get_clusterable_object_id(
            (*iterator).current_offset));
        (*iterator).current_offset += 1u;

        // FIXME: This happens when one or more images for the light probe
        // aren't loaded yet. Really, though, we shouldn't be clustering such
        // objects at all.
        if (light_probe_index < 0) {
            continue;
        }

        var light_probe: LightProbe;
        if (*iterator).is_irradiance_volume {
            light_probe = light_probes.irradiance_volumes[light_probe_index];
        } else {
            light_probe = light_probes.reflection_probes[light_probe_index];
        }

        // Unpack the inverse transform.
        let light_from_world =
            transpose_affine_matrix(light_probe.light_from_world_transposed);

        // Transform the point into local space, with the cube edges at ±0.5 on
        // each axis.
        let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;
        // Avoid division by zero.
        let falloff = max(light_probe.falloff, vec3(0.0001));
        // Calculate the per-axis weight by doing a linear ramp from 0.0 at the
        // inside of the falloff region to 1.0 at the outside of the falloff
        // region.
        let axis_weights = saturate((1.0 - 2.0 * abs(probe_space_pos)) / (2.0 * falloff));
        // The actual weight is the minimum of all the per-axis weights.
        let weight = min(min(axis_weights.x, axis_weights.y), axis_weights.z);
        // If the resulting weight is zero, we're outside the light probe
        // entirely. Bail.
        if (weight == 0.0) {
            continue;
        }

        result.texture_index = light_probe.cubemap_index;
        result.intensity = light_probe.intensity;
        result.light_from_world = light_from_world;
        result.parallax_correction_bounds = light_probe.parallax_correction_bounds;
        result.flags = light_probe.flags;
        result.weight = weight;
        return result;
    }

    return result;
}

#else   // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3

// A type that allows iterating through the list of light probes that overlap
// the current fragment.
//
// This is the version that's used when sufficient storage buffers aren't
// available and consequently when light probes aren't clustered. It simply does
// a brute force search of all light probes. Because platforms without
// sufficient SSBO bindings typically lack bindless shaders, there will usually
// only be one of each type of light probe present anyway.
struct LightProbeIterator {
    // The current index in the list of light probes for this cluster.
    current_index: u32,
    // The last index in the list.
    end_index: u32,
    // The position of the current fragment.
    world_position: vec3<f32>,
    // True if we're searching for irradiance volumes; false if we're searching
    // for reflection probes.
    is_irradiance_volume: bool,
}

// Creates a new light probe iterator ready to search through light probes.
fn light_probe_iterator_new(
    world_position: vec3<f32>,
    is_irradiance_volume: bool,
    clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> LightProbeIterator {
    return LightProbeIterator(
        0,
        select(
            light_probes.reflection_probe_count,
            light_probes.irradiance_volume_count,
            is_irradiance_volume
        ),
        world_position,
        is_irradiance_volume
    );
}

// Searches for a light probe that contains the fragment and returns the next
// such probe.
//
// Note that, theoretically, multiple light probes can affect a fragment, and
// the caller is generally expected to blend their influences together in a
// weighted sum. In practice, this version of `light_probe_iterator_next` is
// used on platforms that lack bindless shaders, so there will only be at most
// one light probe that affects the current fragment in the first place.
fn light_probe_iterator_next(iterator: ptr<function, LightProbeIterator>) -> LightProbeQueryResult {
    var result: LightProbeQueryResult;
    result.texture_index = -1;
    result.weight = 0.0;

    while (true) {
        let light_probe_index = (*iterator).current_index;

        var light_probe: LightProbe;
        if is_irradiance_volume {
            light_probe = light_probes.irradiance_volumes[light_probe_index];
        } else {
            light_probe = light_probes.reflection_probes[light_probe_index];
        }

        // Unpack the inverse transform.
        let light_from_world =
            transpose_affine_matrix(light_probe.light_from_world_transposed);

        // Transform the point into local space, with the cube edges at ±0.5 on
        // each axis.
        let probe_space_pos = (light_from_world * vec4<f32>(world_position, 1.0f)).xyz;
        // Avoid division by zero.
        let falloff = max(light_probe.falloff, vec3(0.0001));
        // Calculate the per-axis weight by doing a linear ramp from 0.0 at the
        // inside of the falloff region to 1.0 at the outside of the falloff
        // region.
        let axis_weights = saturate((1.0 - 2.0 * abs(probe_space_pos)) / (2.0 * falloff));
        // The actual weight is the minimum of all the per-axis weights.
        let weight = min(min(axis_weights.x, axis_weights.y), axis_weights.z);
        // If the resulting weight is zero, we're outside the light probe
        // entirely. Bail.
        if (weight == 0.0) {
            continue;
        }

        result.texture_index = light_probe.cubemap_index;
        result.intensity = light_probe.intensity;
        result.light_from_world = light_from_world;
        result.parallax_correction_bounds = light_probe.parallax_correction_bounds;
        result.flags = light_probe.flags;
        result.weight = weight;
        return result;
    }

    return result;
}

#endif  // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3