Luxar Viewer API Documentation - v2026.10.19
    Preparing search index...

    Module rendering/materials/mesh-physical/refraction-apodization

    Edge apodization of the glass refraction shift (both backends).

    Three's transmission samples the scene behind a glass at the screen projection of position + refract(…)·thickness and never checks that the projection is still on screen. Where the shift carries it past an edge, the texture sampler clamps to the border texel, and the border is smeared inward as streaks. A camera inside a refracting shell is the worst case: every pixel is glass, and on WebGL a double-sided shell is refracted twice (three pre-renders its back faces into the very texture those faces then sample), so the overshoot compounds.

    The fix shapes the SHIFT FIELD rather than the sample. Per fragment, with the unshifted point at NDC b and the shift s (both from the same ray three will trace), each axis has room = 1 − |b| to its nearest border and the shift is soft-limited against it: the identity up to REFRACTION_SHIFT_KNEE·room, then a tanh roll-off that approaches REFRACTION_SHIFT_CEILING·room. The vector keeps its direction — the smaller of the two axis factors scales both — so the field is:

    • zero on every screen border (room 0 there);
    • continuous (and C¹ through the knee), so no seam appears where it engages;
    • the identity wherever the shift stays within the knee of the room, which is every pixel of an ordinary glass away from the edges — no change in the centre;
    • well-conditioned: the ceiling is what keeps edge detail. Were the shift allowed to approach the WHOLE room, a large shift would saturate there, the sample point b + s would sit on the border for a whole band of pixels, and that band would show the border column smeared across it — the very streaks being removed, only on screen instead of clamped. Capped at a fraction c of the room, the sample point still advances at least (1 − c) as fast as the pixel, so the edge band is at most stretched by 1 / (1 − c) (4× at most here). Stretch rises smoothly from 1× at the knee toward that bound near the border, never collapsing the band.

    The shaped shift is applied by shortening the ray, not by rewriting the sample coordinate, so it goes through three's own code path on both backends (three's TSL refraction function is private). A point a fraction λ along the world ray projects to the fraction φ = λw₁ / ((1−λ)w₀ + λw₁) of the screen shift (the perspective-correct interpolation of the clip ws at its two ends), so the wanted fraction κ needs exactly λ = κw₀ / (κw₀ + (1−κ)w₁). Three receives thickness·λ; attenuationDistance·λ keeps the Beer–Lambert exponent (path length over distance) exactly what it was, so a tinted glass does not lighten near the edges.

    With dispersion, three traces three IORs; the scale is taken from the most refracted one (ior + halfSpread), which bounds the other two.

    The TypeScript functions here are the reference the shader text and the TSL twin (./refraction-apodization-tsl.ts) mirror; the unit tests hold all of them to it.

    REFRACTION_SHIFT_KNEE
    REFRACTION_SHIFT_CEILING
    REFRACTION_MIN_RAY_SCALE
    DISPERSION_HALF_SPREAD_PER_UNIT
    REFRACTION_APODIZATION_GLSL_FUNCTIONS
    TRANSMISSION_PARS_INCLUDE
    TRANSMISSION_NORMAL_LINE
    REFRACTION_APODIZATION_GLSL_APPLY
    apodizeShiftAxis
    apodizedShiftFraction
    rayScaleForShiftFraction
    apodizeRefractionShiftGlsl