Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...
    MEGA_FRAGMENT_SHADER: "\n precision highp float;\n\n in vec2 vUv;\n out vec4 fragColor;\n\n // ============================================================\n // Common uniforms\n // ============================================================\n\n uniform sampler2D uHdrScene;\n uniform vec2 uResolution;\n\n // EOG: applied to every frame regardless of effect toggles\n uniform float uExposure; // log2 stops; 0 = neutral\n uniform float uGlobalOffset; // additive lift\n uniform float uGlobalGamma; // gamma power (1.0 = linear)\n\n // ============================================================\n // Lens distortion (chromatic + radial)\n // ============================================================\n #ifdef USE_LENS_DISTORTION\n uniform vec2 uDistortion; // radial coefficient [x, y]\n uniform vec2 uPrincipalPoint; // optical center offset\n uniform vec2 uFocalLength; // focal length scale [fx, fy]\n uniform float uSkew; // skew correction (radians)\n uniform float uDispersion; // chromatic dispersion strength\n\n // Brown-Conrady radial distortion + camera intrinsic matrix.\n //\n // Y-CONVENTION (load-bearing): the canonical distortion map is\n // defined in TOP-DOWN uv space (what the TSL twin's uv() delivers\n // under WebGPURenderer, and what the TS picking mirror in\n // picking-system/lens-distortion.ts uses). This GLSL runs ONLY\n // under WebGLRenderer (resolveMaterialBackend keys on the same\n // renderer class as framebufferYDown), where the fullscreen\n // triangle delivers BOTTOM-UP uv. Radial/focal terms are even\n // under the y-flip, but principalPoint.y and skew are ODD — so we\n // apply the exact flip-conjugation by negating those two intrinsics\n // instead of flipping uv on entry/exit (branch-free, and\n // bit-identical to the canonical formula when ppy = skew = 0, i.e.\n // every shipped preset). Re-syncing this matrix verbatim from the\n // TSL twin WOULD REINTRODUCE the wrong-direction bug — the\n // conjugation-identity unit test in lens-distortion.test.ts guards\n // that.\n vec2 applyDistortion(vec2 uv, vec2 distortionCoeff) {\n vec2 xn = 2.0 * (uv - 0.5);\n float r2 = dot(xn, xn);\n vec3 xDistorted = vec3((1.0 + distortionCoeff * r2) * xn, 1.0);\n mat3 kk = mat3(\n vec3(uFocalLength.x, 0.0, 0.0),\n vec3(-uSkew * uFocalLength.x, uFocalLength.y, 0.0),\n vec3(uPrincipalPoint.x, -uPrincipalPoint.y, 1.0)\n );\n return (kk * xDistorted).xy * 0.5 + 0.5;\n }\n\n // Mask out-of-bounds samples so distortion doesn't bleed garbage\n // texels in from edge clamping.\n float distortionBorder(vec2 uv) {\n return float(uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);\n }\n #endif\n\n // ============================================================\n // Bloom\n // ============================================================\n #ifdef USE_BLOOM\n uniform sampler2D uBloomTexture;\n uniform float uBloomIntensity;\n #endif\n\n // Sample (scene + bloom) at a single UV. Bloom is additively mixed\n // into the HDR sample BEFORE chromatic lens distortion samples from\n // it; a distorted sample picks up bloom at the distorted UV too.\n vec3 sampleHdrPlusBloom(vec2 uv) {\n vec3 result = texture(uHdrScene, uv).rgb;\n #ifdef USE_BLOOM\n result += texture(uBloomTexture, uv).rgb * uBloomIntensity;\n #endif\n return result;\n }\n\n // ============================================================\n // Detector noise (Bob Jenkins hash + Anscombe Poisson + Gaussian)\n // ============================================================\n #ifdef USE_DETECTOR_NOISE\n uniform float uTime;\n uniform float uReadoutSigma;\n uniform float uPhotonGain;\n uniform float uFpnSigma;\n\n uint bobJenkinsHash(uint a) {\n a = (a + 0x7ed55d16u) + (a << 12u);\n a = (a ^ 0xc761c23cu) ^ (a >> 19u);\n a = (a + 0x165667b1u) + (a << 5u);\n a = (a + 0xd3a2646cu) ^ (a << 9u);\n a = (a + 0xfd7046c5u) + (a << 3u);\n a = (a ^ 0xb55a4f09u) ^ (a >> 16u);\n return a;\n }\n\n uint rnguint2(vec2 x) {\n uint a = bobJenkinsHash(floatBitsToUint(x.x));\n uint b = bobJenkinsHash(floatBitsToUint(x.y));\n return bobJenkinsHash(a ^ b);\n }\n\n uint rnguint3(vec3 x) {\n uint a = rnguint2(x.xy);\n uint b = bobJenkinsHash(floatBitsToUint(x.z));\n return bobJenkinsHash(a ^ b);\n }\n\n float rngfloat2(vec2 x) { return float(rnguint2(x)) / 4294967296.0; }\n float rngfloat3(vec3 x) { return float(rnguint3(x)) / 4294967296.0; }\n\n // Clamped logistic ≈ Gaussian (faster than Box-Muller).\n // 0.5513 normalizes to ~unit variance after the [-4, 4] clamp.\n float clampedLogistic(float u) {\n float f = clamp(u, 0.0001, 0.9999);\n float logit = log(f / (1.0 - f));\n return clamp(logit, -4.0, 4.0) * 0.5513;\n }\n\n vec3 normal3_temporal(vec3 seed) {\n return vec3(\n clampedLogistic(rngfloat3(seed)),\n clampedLogistic(rngfloat3(seed + vec3(13.37, 7.31, 19.93))),\n clampedLogistic(rngfloat3(seed + vec3(31.17, 41.23, 53.59)))\n );\n }\n\n vec3 normal3_fixed(vec2 seed) {\n return vec3(\n clampedLogistic(rngfloat2(seed)),\n clampedLogistic(rngfloat2(seed + vec2(13.37, 7.31))),\n clampedLogistic(rngfloat2(seed + vec2(31.17, 41.23)))\n );\n }\n\n // Anscombe variance-stabilizing transform for Poisson approximation.\n float anscombeForward(float x) { return 2.0 * sqrt(max(x + 0.375, 0.0)); }\n float anscombeInverse(float y) {\n float x = (y * 0.5) * (y * 0.5) - 0.375;\n return max(x, 0.0);\n }\n\n vec3 poissonNoise(vec3 seed, vec3 lambda) {\n vec3 y = vec3(\n anscombeForward(lambda.r),\n anscombeForward(lambda.g),\n anscombeForward(lambda.b)\n );\n y += normal3_temporal(seed);\n return vec3(\n anscombeInverse(y.r),\n anscombeInverse(y.g),\n anscombeInverse(y.b)\n );\n }\n\n vec3 applyDetectorNoise(vec3 intensity, vec2 uv) {\n float wrappedTime = mod(uTime, 1000.0);\n vec3 temporalSeed = vec3(uv * 1000.0, wrappedTime);\n vec2 fixedSeed = uv * 1000.0;\n\n // 1. Shot noise (Poisson)\n vec3 photonCount = intensity / max(uPhotonGain, 0.0001);\n vec3 noisyPhotons = poissonNoise(temporalSeed, photonCount);\n vec3 afterShot = noisyPhotons * uPhotonGain;\n\n // Anscombe forward-then-inverse has a 3/8 bias that brightens\n // pure-zero pixels (visible as glow under vignette). Fade the\n // shot-noise effect to zero in the darkest pixels.\n vec3 shotW = smoothstep(vec3(0.0), vec3(0.01), intensity);\n afterShot = mix(intensity, afterShot, shotW);\n\n // 2. Readout noise (Gaussian, temporal)\n vec3 readoutNoise = normal3_temporal(temporalSeed + vec3(100.0)) * uReadoutSigma;\n\n // 3. Fixed-pattern noise (Gaussian, static per-pixel)\n vec3 fpn = normal3_fixed(fixedSeed) * uFpnSigma;\n\n return max(afterShot + readoutNoise + fpn, vec3(0.0));\n }\n #endif\n\n // ============================================================\n // Tone mapping — uses THREE's built-in chunk.\n // The chunk declares the toneMappingExposure uniform and the\n // mode functions (Linear/Reinhard/Cineon/ACESFilmic/AgX/Neutral).\n // ============================================================\n #include <tonemapping_pars_fragment>\n\n // ============================================================\n // Vignette\n // ============================================================\n #ifdef USE_VIGNETTE\n uniform float uVignetteDarkness;\n uniform float uVignetteOffset;\n #endif\n\n // Linear → sRGB encoding (matches THREE.SRGBColorSpace output).\n // Equivalent to what THREE injects via the colorspace_fragment\n // chunk when outputColorSpace is sRGB. Inlined here because the\n // chunk is keyed to gl_FragColor and we use a GLSL3 out variable.\n vec3 linearToSRGB(vec3 c) {\n vec3 safe = max(c, vec3(0.0));\n return mix(\n 1.055 * pow(safe, vec3(1.0 / 2.4)) - 0.055,\n safe * 12.92,\n vec3(lessThanEqual(safe, vec3(0.0031308)))\n );\n }\n\n // ============================================================\n // Main\n // ============================================================\n void main() {\n vec2 uv = vUv;\n vec3 color;\n\n // (1+2) Sample (scene + bloom). When lens distortion is on, the\n // sample happens at chromatically-distorted UVs per channel.\n // Bloom is additively mixed BEFORE chromatic distortion\n // samples the buffer.\n #ifdef USE_LENS_DISTORTION\n {\n vec2 distR = uDistortion * (1.0 - uDispersion);\n vec2 distG = uDistortion;\n vec2 distB = uDistortion * (1.0 + uDispersion);\n vec2 uvR = applyDistortion(uv, distR);\n vec2 uvG = applyDistortion(uv, distG);\n vec2 uvB = applyDistortion(uv, distB);\n float r = sampleHdrPlusBloom(uvR).r * distortionBorder(uvR);\n float g = sampleHdrPlusBloom(uvG).g * distortionBorder(uvG);\n float b = sampleHdrPlusBloom(uvB).b * distortionBorder(uvB);\n color = vec3(r, g, b);\n }\n #else\n color = sampleHdrPlusBloom(uv);\n #endif\n\n // Capture-mode early exit: when LUXAR_CAPTURE_RAW_HDR is set,\n // the host disables USE_DETECTOR_NOISE / USE_VIGNETTE /\n // USE_LENS_DISTORTION and wants the pre-EOG linear HDR pixels for\n // EXR export. Skip every downstream step (EOG, tone mapping,\n // vignette, sRGB encoding). Output is the scene+bloom sample.\n #ifdef LUXAR_CAPTURE_RAW_HDR\n fragColor = vec4(color, 1.0);\n return;\n #endif\n\n // (3) Detector noise\n #ifdef USE_DETECTOR_NOISE\n color = applyDetectorNoise(color, uv);\n #endif\n\n // (4) EOG: Exposure → Offset → Gamma\n color *= exp2(uExposure);\n color = max(color + vec3(uGlobalOffset), vec3(0.0));\n color = pow(color, vec3(1.0 / uGlobalGamma));\n\n // (5) Tone mapping — LUXAR_TONE_MAPPING_MODE is a Luxar-internal\n // compressed ID (1..6) set by toneMappingModeDefine() in the\n // material file. THREE.NoToneMapping is aliased to mode 1\n // (Linear) so it clamps to [0,1]. Functions come from\n // <tonemapping_pars_fragment>.\n #if LUXAR_TONE_MAPPING_MODE == 1\n color = LinearToneMapping(color);\n #elif LUXAR_TONE_MAPPING_MODE == 2\n color = ReinhardToneMapping(color);\n #elif LUXAR_TONE_MAPPING_MODE == 3\n color = CineonToneMapping(color);\n #elif LUXAR_TONE_MAPPING_MODE == 4\n color = ACESFilmicToneMapping(color);\n #elif LUXAR_TONE_MAPPING_MODE == 5\n color = AgXToneMapping(color);\n #elif LUXAR_TONE_MAPPING_MODE == 6\n color = NeutralToneMapping(color);\n #endif\n\n // (6) Vignette: multiplicative darkening (preserves color ratios).\n #ifdef USE_VIGNETTE\n {\n vec2 vc = (uv - 0.5) / uVignetteOffset;\n float d2 = dot(vc, vc);\n float vf = 1.0 - smoothstep(0.0, 1.5, d2) * uVignetteDarkness;\n color *= vf;\n }\n #endif\n\n // (7) sRGB encoding. Required when the output reaches a display\n // surface (backbuffer or sRGB-encoded ldrTarget read by FXAA\n // and then written to backbuffer).\n // LUXAR_CAPTURE_LINEAR_LDR disables this step for the\n // visible-ldr EXR capture mode (post-tone-mapping, pre-sRGB).\n #ifndef LUXAR_CAPTURE_LINEAR_LDR\n color = linearToSRGB(color);\n #endif\n\n // Alpha forced to 1.0 — input alpha may be NaN/Inf from heavy\n // additive blending of points/lines.\n fragColor = vec4(color, 1.0);\n }\n" = ...

    Mega-shader fragment shader. Defines that the host toggles:

    • USE_LENS_DISTORTION — chromatic + radial distortion sampling
    • USE_BLOOM — additive bloom mix
    • USE_DETECTOR_NOISE — physics-based detector noise
    • USE_VIGNETTE — multiplicative vignette darkening
    • LUXAR_TONE_MAPPING_MODE — internal mode ID (1..6); see material.ts
    • LUXAR_CAPTURE_RAW_HDR — early-exit after sample+bloom; bypasses EOG, tone mapping, vignette, sRGB encoding. Used by captureHDRPixels('hdr-effects-pre-tone') to produce linear-HDR-with-bloom output for EXR export.
    • LUXAR_CAPTURE_LINEAR_LDR — skips ONLY the final sRGB encoding. Used by captureHDRPixels('visible-ldr') to produce a post-tone-mapping linear LDR capture.

    Note: toneMappingExposure is provided by THREE's <tonemapping_pars_fragment> chunk; we deliberately do not redeclare it. The host pins it to 1.0 because uExposure already pre-multiplies before the tone-mapping call.