indicatrix 0.9.0

A physically-based spectral gemstone renderer that turns GemCAD-style cutting instructions into rendered output.
Documentation
// Surface phase: shading normal, frame rotation, interior absorption, facet finish lookup, and arm selection.
fn bounce_surface(
    s: ptr<function, BounceRay>,
    h: ptr<function, BounceHit>,
    rc: RayConstants,
    bounce: u32,
    seed0: u32,
    observer: vec3<f32>,
) -> u32 {
    let hit = (*h).hit;
    var local_origin = (*s).origin;
    let current_origin = &local_origin;
    var local_dir = (*s).dir;
    let current_dir = &local_dir;
    var local_is_extraordinary = (*s).is_extraordinary;
    let is_extraordinary = &local_is_extraordinary;
    var local_stokes = (*s).stokes;
    let stokes = &local_stokes;
    var local_lambdas = (*s).lambdas;
    let lambdas = &local_lambdas;
    let is_anisotropic = rc.is_anisotropic;
    let is_biaxial = rc.is_biaxial;
    let c_axis = rc.c_axis;
    let n_alpha_hero = rc.n_alpha_hero;
    let n_beta_hero = rc.n_beta_hero;
    let n_gamma_hero = rc.n_gamma_hero;
    let biax_ax0 = rc.biax_ax0;
    let biax_ax1 = rc.biax_ax1;
    let biax_ax2 = rc.biax_ax2;
    let n_o_hero_seed = rc.n_o_hero_seed;
    let n_e_hero_seed = rc.n_e_hero_seed;
    let alpha_o_hoisted = rc.alpha_o_hoisted;
    let alpha_e_hoisted = rc.alpha_e_hoisted;
    let alpha_beta_hoisted = rc.alpha_beta_hoisted;

    let hit_point = (*current_origin) + hit.t * (*current_dir);
    // See shading_normal_near_edge's own doc comment. `shading_normal_for_hit` is that
    // function unchanged for a planar stone and the concave-stone rule otherwise.
    var normal = shading_normal_for_hit(hit_point, hit.facet_idx, hit.normal, material.edge_rounding_radius);
    // `wave_dir_at_bounce` is the wave normal `k` (== (*current_k)), used for
    // EVERY index lookup, cos_i/sin_i, Snell/Fresnel evaluation, TIR decision, and
    // the Stokes plane-of-incidence frame below -- see
    // optics::raytracer::refraction's "wave normal vs Poynting direction" design
    // note. The Poynting/energy direction `S` (== (*current_dir)) is used directly
    // (geometric origin-advance/intersection only) where still needed.
    // `wave_dir_at_bounce == (*current_dir)` trivially outside the crystal and for the
    // uniaxial ordinary eigenmode, so every such case is bit-identical to the plain
    // all-`(*current_dir)` code.
    let wave_dir_at_bounce = (*s).k;

    // Plane-of-incidence frame rotation (signed psi via atan2).
    let cpn_raw = cross(wave_dir_at_bounce, normal);
    let cpn_len2 = dot(cpn_raw, cpn_raw);
    var current_plane_normal: vec3<f32>;
    if (cpn_len2 > 0.0) {
        current_plane_normal = cpn_raw / sqrt(cpn_len2);
    } else {
        current_plane_normal = vec3<f32>(0.0, 0.0, 0.0);
    }
    if ((*s).have_prev_plane_normal && cpn_len2 > 1e-6 && dot((*s).prev_plane_normal, (*s).prev_plane_normal) > 1e-6) {
        let psi = signed_frame_rotation_psi((*s).prev_plane_normal, current_plane_normal, wave_dir_at_bounce);
        let rot = mueller_frame_rotation(psi);
        for (var k: u32 = 0u; k < NUM_CHANNELS; k = k + 1u) {
            local_stokes[k] = rot * local_stokes[k];
            (*s).stokes[k] = local_stokes[k];
        }
    }
    (*s).prev_plane_normal = current_plane_normal;
    (*s).have_prev_plane_normal = true;

    if ((*s).inside_gem) {
        normal = -normal;
        // A scattering-active material's extinction for this segment was
        // already applied above (in the `maybe_scatter_or_extinguish` no-scatter
        // branch) -- applying the plain absorption loop here too would charge
        // this segment's absorption TWICE. `material.scattering_sigma_s <= 0.0`
        // is exactly the pre-Task-1 case (including every existing scene), where
        // the block above never ran and this is the ONLY absorption
        // application, matching the CPU's identical guard.
        if (material.scattering_sigma_s <= 0.0) {
            // P1 (assigned-mode absorption): optics::raytracer::absorption::
            // channel_absorption_alphas_assigned. `(*is_extraordinary)` names which
            // eigenmode this path was assigned to at its most recent air->crystal
            // entry -- computed fresh from the CURRENT wave normal
            // (`wave_dir_at_bounce`) every bounce, not read off `(*stokes)[k]`'s
            // (possibly azimuth-drifted) Stokes state. The isotropic branch is the
            // Stokes-independent eigenmode midpoint -- see the scatter block's own
            // comment above.
            var alphas_interior: array<f32, 8>;
            for (var k: u32 = 0u; k < NUM_CHANNELS; k = k + 1u) {
                alphas_interior[k] = 0.0;
            }
            if (is_anisotropic) {
                for (var k: u32 = 0u; k < NUM_CHANNELS; k = k + 1u) {
                    if (is_biaxial && material.has_beta_ray != 0u) {
                        alphas_interior[k] = assigned_mode_alpha_biaxial(
                            alpha_o_hoisted[k], alpha_beta_hoisted[k], alpha_e_hoisted[k],
                            n_alpha_hero, n_beta_hero, n_gamma_hero, biax_ax0, biax_ax1, biax_ax2,
                            c_axis, wave_dir_at_bounce, (*is_extraordinary),
                        );
                    } else {
                        alphas_interior[k] = assigned_mode_alpha_uniaxial(
                            alpha_o_hoisted[k], alpha_e_hoisted[k], c_axis, wave_dir_at_bounce, (*is_extraordinary),
                            n_o_hero_seed, n_e_hero_seed,
                        );
                    }
                }
            } else {
                let eigen_a = ordinary_eigen_polarization(wave_dir_at_bounce, c_axis);
                let eigen_b = extraordinary_eigen_polarization(wave_dir_at_bounce, c_axis);
                for (var k: u32 = 0u; k < NUM_CHANNELS; k = k + 1u) {
                    alphas_interior[k] = isotropic_channel_alpha(
                        alpha_o_hoisted[k], alpha_e_hoisted[k], c_axis, eigen_a, eigen_b,
                    );
                }
            }
            for (var k: u32 = 0u; k < NUM_CHANNELS; k = k + 1u) {
                // (absorption path scale): mirrors
                // optics::raytracer::absorption::apply_absorption's own
                // `path_len * ctx.material.absorption_path_scale` multiply exactly.
                let scaled_hit_t = hit.t * material.absorption_path_scale;
                // `exp_poly`, not the `exp()` builtin --
                // `apply_absorption` calls `crate::simd::exp_f32x8`, not `f32::exp`.
                let trans_factor = exp_poly(-alphas_interior[k] * scaled_hit_t);
                local_stokes[k] = local_stokes[k] * trans_factor;
            }
        }
        (*s).stokes = local_stokes;
    }

    // Which specular/diffuse treatment
    // this facet gets. Bounds-checked (mirrors optics::raytracer::trace_spectral_ray_inner's
    // `facet_finishes.get(hit_rec.facet_idx).copied().unwrap_or_default()`): an
    // index past the end of a shorter-than-`planes` buffer defaults to
    // `facet_finish::POLISHED`, exactly as the CPU's `Default` does.
    // `select`'s two value arguments are BOTH evaluated in WGSL (no short-circuit),
    // so an `if` guard is used instead of `select` here -- indexing
    // `facet_finishes[hit.facet_idx]` unconditionally would attempt an
    // out-of-bounds storage-buffer read whenever `facet_idx >= arrayLength(...)`
    // (harmless per WGSL's robustness guarantees, but its RESULT is
    // implementation-defined, so this must never be allowed to influence `finish`).
    var finish: u32 = 0u;
    if (hit.facet_idx < arrayLength(&facet_finishes)) {
        finish = facet_finishes[hit.facet_idx];
    }
    // Captured before any of the three bounce-dispatch arms below run -- see
    // optics::raytracer::dispatch_bounce's doc comment for why `was_internal_reflection`
    // is always computed against the PRE-bounce `(*inside_gem)`. The TIR/reflect/refract
    // arms below never need this explicitly (they either never touch `(*inside_gem)` at
    // all, or only flip it once at their very end, after which nothing else in that
    // arm reads it this bounce) -- only the frosted arm, which can update `(*inside_gem)`
    // itself, needs the pre-bounce value spelled out separately.
    let pre_bounce_inside_gem = (*s).inside_gem;

    (*h).hit_point = hit_point;
    (*h).shading_normal = normal;
    (*h).wave_dir_at_bounce = wave_dir_at_bounce;
    (*h).finish = finish;
    (*h).pre_bounce_inside_gem = pre_bounce_inside_gem;

    let idx = bounce_index_setup(s, h, rc);

    if (finish == FACET_FINISH_FROSTED) {
        (*h).arm = ARM_FROSTED;
    } else if (idx.sin2_t > 1.0) {
        (*h).arm = ARM_TIR;
    } else if (idx.uniaxial_nondegenerate) {
        if (!(*s).inside_gem) {
            (*h).arm = ARM_UNI_ENTRY;
        } else {
            (*h).arm = ARM_UNI_INTERNAL;
        }
    } else {
        (*h).arm = ARM_GENERIC;
    }

    return PHASE_NEXT;
}