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

    Variable GSPLAT_PICK_VERTEX_SHADERConst

    GSPLAT_PICK_VERTEX_SHADER: "\n precision highp float;\n\n \nbool isInvalidFloat(float v) {\n return isnan(v) || isinf(v);\n}\n\nfloat sanitizePositive(float v, float fallback) {\n return (isInvalidFloat(v) || v <= 0.0) ? fallback : v;\n}\n\nfloat sanitizeNonNegative(float v, float fallback) {\n return (isInvalidFloat(v) || v < 0.0) ? fallback : v;\n}\n\n// Per-element opacity sanitizer: NaN/Inf route to the 1.0 opaque\n// identity (corruption stays LOUD), finite values clamp to [0, 1]\n// (alpha is opacity, never HDR — Python pins the range at write; this\n// guards hand-crafted zarr). The clamp keeps the zero boundary\n// CONTINUOUS (a -1e-4 epsilon vanishes like +0.0 renders, instead of\n// jumping to full opacity) and keeps the value mediump-varying-safe.\nfloat sanitizeAlpha(float v) {\n return isInvalidFloat(v) ? 1.0 : clamp(v, 0.0, 1.0);\n}\n\n \nfloat perspectiveNearFade(int isOrtho, float viewZ, float nearCull) {\n if (isOrtho == 1) return 1.0;\n if (viewZ >= 0.0) return 0.0;\n return smoothstep(nearCull, nearCull * 2.0, -viewZ);\n}\n\n \nint luxarIsOrthoProjection() {\n return projectionMatrix[3][3] > 0.5 ? 1 : 0;\n}\nfloat luxarProjectionSizeScale() {\n return abs(projectionMatrix[1][1]);\n}\n\n\n in vec2 aQuadCorner;\n\n // Draw-slot -> storage-slot mapping (identity in Phase 1; permuted\n // by the sort worker in Phase 2+). Also the pick ELEMENT id: the\n // pick buffer must report the storage slot -- the id the rest of\n // the pipeline (loaders, selection) addresses splats by -- not the\n // transient draw slot.\n \n// Element index split into two 16-bit halves, low in .x and high in .y.\n// The pick pass carries the index through an RGBA32F buffer, and float32\n// has a 24-bit mantissa — so a single float channel cannot represent\n// consecutive indices past 16,777,216, while a node's capacity reaches\n// 2^25 on a 32768-texel device. Both halves are <= 65535, hence exact,\n// and the pick decoder recombines them (see picking-system/pick-render.ts).\n// Kept in INT space: doing the split on a float would already have lost\n// the bit it is meant to preserve.\nvec2 luxarElementIdSplit(uint i) {\n return vec2(float(i & 0xFFFFu), float(i >> 16u));\n}\n\nin uint aSortedIndex;\nin uint aSortedIndexB;\nuniform int uSortedIndexSlot;\n\nuint luxarSortedIndex() {\n return uSortedIndexSlot == 1 ? aSortedIndexB : aSortedIndex;\n}\n\n// The STORAGE slot's id parts — the id the rest of the pipeline (loaders,\n// selection) addresses elements by, not the transient draw slot. Mesh reads\n// the shared split directly instead, off gl_VertexID (spec §6.5).\nvec2 luxarElementIdParts() {\n return luxarElementIdSplit(luxarSortedIndex());\n}\n// Projected-density thinning (scene/density-guard.ts): the fraction of this\n// node's elements to DROP, chosen per STORAGE index with a deterministic\n// integer hash so the kept subset is stable under depth re-sorting, identical\n// across the visual and picking passes, and spatially uniform (storage order\n// is Hilbert/BSP-coherent, so a prefix would be a hole). A material that does\n// not set the uniform reads 0 and drops nothing.\nuniform float uDensityDrop;\nbool luxarDensityDropped() {\n if (uDensityDrop <= 0.0) return false;\n uint h = luxarSortedIndex();\n h ^= h >> 16u;\n h *= 0x7feb352du;\n h ^= h >> 15u;\n h *= 0x846ca68bu;\n h ^= h >> 16u;\n return float(h) * (1.0 / 4294967296.0) < uDensityDrop;\n}\n\n\n // Splat data texture: RGBA32F, 4 texels/splat (see\n // rendering/element-texture-layout.ts). Picking needs texels 0-2\n // only (center/amplitude/cholesky) -- color is not fetched.\n uniform highp sampler2D uSplatTex;\n\n uniform vec2 uResolution;\n uniform float uTruncate;\n uniform float uNearCull;\n uniform float uMaxExtentFactor;\n uniform float uCov2DDilation; // 2D-covariance low-pass dilation in CSS px² (visual-shader parity)\n uniform float uPixelRatio;\n uniform float uNodeId;\n uniform int uLabelFilterIndex;\n\n flat out mediump float vAmplitude2D;\n flat out highp vec3 vL2D;\n flat out highp vec2 vCenterScreen;\n flat out highp float vNodeId;\n flat out highp vec2 vElementId;\n\n mat3 unpackCholesky3D(vec2 c01, vec2 c23, vec2 c45) {\n return mat3(\n c01.x, c01.y, c23.y,\n 0.0, c23.x, c45.x,\n 0.0, 0.0, c45.y\n );\n }\n\n // Parity with visual shader-glsl.ts invalidCov2D — reject Σ_2D\n // entries that are NaN/Inf so a degenerate splat can't poison the\n // eigendecomposition and produce undefined pick geometry.\n bool invalidCov2D(mat2 S) {\n return isInvalidFloat(S[0][0]) || isInvalidFloat(S[0][1])\n || isInvalidFloat(S[1][0]) || isInvalidFloat(S[1][1]);\n }\n\n vec3 cholesky2x2(mat2 S) {\n float L00 = sqrt(max(S[0][0], 1e-8));\n float invL00 = 1.0 / L00;\n float L10 = S[1][0] * invL00;\n float L11 = sqrt(max(S[1][1] - L10 * L10, 1e-8));\n float invL11 = 1.0 / L11;\n return vec3(invL00, L10, invL11);\n }\n\n void main() {\n // === Splat-texture fetch prologue (visual-shader parity) ===\n // Width is a multiple of 4, so a splat's texels share one row.\n // Projected-density thinning: a splat the visual pass dropped must not\n // be pickable either (same hash, same uniform value).\n if (luxarDensityDropped()) {\n gl_Position = vec4(0.0, 0.0, -2.0, 1.0);\n return;\n }\n int splatBase = int(luxarSortedIndex()) * 4;\n int splatTexW = LUXAR_SPLAT_TEX_W;\n ivec2 texel0 = ivec2(splatBase % splatTexW, splatBase / splatTexW);\n vec4 splatT0 = texelFetch(uSplatTex, texel0, 0);\n vec4 splatT1 = texelFetch(uSplatTex, ivec2(texel0.x + 1, texel0.y), 0);\n vec4 splatT2 = texelFetch(uSplatTex, ivec2(texel0.x + 2, texel0.y), 0);\n vec4 splatT3 = texelFetch(uSplatTex, ivec2(texel0.x + 3, texel0.y), 0);\n vec3 aCenter = splatT0.xyz;\n float aAmplitude = splatT0.w;\n vec2 aCholesky01 = splatT1.xy;\n vec2 aCholesky23 = splatT1.zw;\n vec2 aCholesky45 = splatT2.xy;\n float aLabelIndex = splatT3.z;\n if (uLabelFilterIndex > 0 && int(aLabelIndex + 0.5) != uLabelFilterIndex) {\n gl_Position = vec4(0.0, 0.0, -2.0, 1.0);\n return;\n }\n\n vec4 centerCam4 = modelViewMatrix * vec4(aCenter, 1.0);\n vec3 centerCam = centerCam4.xyz;\n\n // Clip-space centre through the projection THIS draw uses. The screen\n // centre, the covariance Jacobian, the coverage extent and the ortho\n // branch all come from it and from P, so a splat is placed and sized\n // for whatever camera three draws with: a cube-capture face (fov -90\n // flips P), a zoomed or asymmetric frustum, an embedder's camera.\n // CPU mirror + tests: projection-math.ts.\n vec4 centerClip = projectionMatrix * centerCam4;\n int isOrtho = luxarIsOrthoProjection();\n float invW = 1.0 / centerClip.w;\n\n // Unified near handling — see the visual gsplat shader: the\n // shared perspectiveNearFade subsumes the old standalone\n // behind-camera reject; ortho falls through to NDC clipping.\n // 1e-20 floor = degenerate-smoothstep guard only; uNearCull is\n // scene-bounds-scaled (an absolute 1e-4 faded out tiny-unit\n // scenes entirely).\n float depthFade = perspectiveNearFade(isOrtho, centerCam.z, max(uNearCull, 1e-20));\n if (depthFade < 0.01) {\n gl_Position = vec4(0.0, 0.0, -2.0, 1.0);\n return;\n }\n\n mat3 R = mat3(modelViewMatrix);\n mat3 L3D = unpackCholesky3D(aCholesky01, aCholesky23, aCholesky45);\n mat3 L_cam = R * L3D;\n mat3 Sigma_cam = L_cam * transpose(L_cam);\n\n float zDepth = -centerCam.z;\n\n // Coverage fade applies in BOTH projections (matches the visual\n // shader — ortho projected size is depth-independent, divisor 1)\n // so pickability tracks what is actually visible. Computed\n // UNCONDITIONALLY (visual twin updated in lockstep): below\n // maxExtent*0.5 the smoothstep is 0 and the fade is a no-op, so\n // no size gate is needed — the former maxLateralVar > 0.01 gate\n // skipped the fade for sigma < 0.1 world-unit splats.\n float halfResY = 0.5 * uResolution.y;\n float coverageFade;\n {\n // 1e-20 floors = pure div-by-zero/sqrt guards, matching the\n // visual shader: maxLateralVar is world-unit² (an absolute\n // 1e-8 floor coverage-culled every splat of a tiny-unit\n // scene); zDepth is bounded by the scene-relative near fade.\n float maxLateralVar = max(Sigma_cam[0][0], max(Sigma_cam[1][1], Sigma_cam[2][2]));\n float extentDivisor = (isOrtho == 1) ? 1.0 : max(zDepth, 1e-20);\n float projectedExtent = (halfResY * luxarProjectionSizeScale()) * sqrt(max(maxLateralVar, 1e-20)) * uTruncate / extentDivisor;\n float maxExtent = max(uResolution.x, uResolution.y) * uMaxExtentFactor;\n coverageFade = 1.0 - smoothstep(maxExtent * 0.5, maxExtent, projectedExtent);\n if (coverageFade < 0.01) {\n gl_Position = vec4(0.0, 0.0, -2.0, 1.0);\n return;\n }\n }\n\n float nearFade = min(depthFade, coverageFade);\n\n // Projection Jacobian at the splat centre, general form (valid for any\n // P): J[k] = res/2 * (P[k].xy / w - clip.xy * P[k].w / w^2). For\n // three's symmetric perspective P it is the classic\n // [[fx/z, 0], [0, fy/z], [fx*x/z^2, fy*y/z^2]]; for ortho (w = 1,\n // P[k].w = 0) it is [[fx, 0], [0, fy], [0, 0]]. The x terms use P00\n // (not a shared fx = fy), so a camera aspect that differs from the\n // buffer aspect is honoured instead of assumed away.\n vec2 halfRes = 0.5 * uResolution;\n vec2 clipTerm = centerClip.xy * (invW * invW);\n mat3x2 J;\n J[0] = halfRes * (projectionMatrix[0].xy * invW - clipTerm * projectionMatrix[0].w);\n J[1] = halfRes * (projectionMatrix[1].xy * invW - clipTerm * projectionMatrix[1].w);\n J[2] = halfRes * (projectionMatrix[2].xy * invW - clipTerm * projectionMatrix[2].w);\n\n vec2 JS0 = J[0] * Sigma_cam[0][0] + J[1] * Sigma_cam[0][1] + J[2] * Sigma_cam[0][2];\n vec2 JS1 = J[0] * Sigma_cam[1][0] + J[1] * Sigma_cam[1][1] + J[2] * Sigma_cam[1][2];\n vec2 JS2 = J[0] * Sigma_cam[2][0] + J[1] * Sigma_cam[2][1] + J[2] * Sigma_cam[2][2];\n\n mat2 Sigma2D;\n Sigma2D[0][0] = JS0.x * J[0].x + JS1.x * J[1].x + JS2.x * J[2].x;\n Sigma2D[1][0] = JS0.x * J[0].y + JS1.x * J[1].y + JS2.x * J[2].y;\n Sigma2D[0][1] = Sigma2D[1][0];\n Sigma2D[1][1] = JS0.y * J[0].y + JS1.y * J[1].y + JS2.y * J[2].y;\n\n // 2D low-pass dilation — visual-shader parity (shader-glsl.ts). Widens\n // the pickable footprint to match the dilated visual splat, so what you\n // click matches what you see.\n float dilationPixelRatio = max(uPixelRatio, 1.0);\n float cov2DDilation = uCov2DDilation * dilationPixelRatio * dilationPixelRatio;\n Sigma2D[0][0] += cov2DDilation;\n Sigma2D[1][1] += cov2DDilation;\n\n // Visual-shader parity (shader-glsl.ts) + TSL-side parity\n // (gsplat-pick.tsl.ts): reject splats with NaN/Inf Σ_2D or\n // amplitude so picking and rendering agree on which elements\n // are pickable across WebGL and WebGPU backends.\n if (invalidCov2D(Sigma2D) || isInvalidFloat(aAmplitude)) {\n gl_Position = vec4(0.0, 0.0, -2.0, 1.0);\n return;\n }\n\n // For picking, always use max projection (no ray integration needed)\n vAmplitude2D = aAmplitude * nearFade;\n\n vL2D = cholesky2x2(Sigma2D);\n\n float trace = Sigma2D[0][0] + Sigma2D[1][1];\n float det = Sigma2D[0][0] * Sigma2D[1][1] - Sigma2D[0][1] * Sigma2D[1][0];\n float disc = max(trace * trace - 4.0 * det, 0.0);\n float sqrtDisc = sqrt(disc);\n float lambda1 = max(0.5 * (trace + sqrtDisc), 1e-6);\n float lambda2 = max(0.5 * (trace - sqrtDisc), 1e-6);\n\n vec2 majorAxis;\n if (abs(Sigma2D[0][1]) > 1e-6) {\n majorAxis = normalize(vec2(lambda1 - Sigma2D[1][1], Sigma2D[0][1]));\n } else {\n // Near-diagonal covariance: pick axis with larger variance\n majorAxis = (Sigma2D[0][0] >= Sigma2D[1][1]) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);\n }\n vec2 minorAxis = vec2(-majorAxis.y, majorAxis.x);\n\n float extent1 = uTruncate * sqrt(lambda1);\n float extent2 = uTruncate * sqrt(lambda2);\n\n float maxExtentPx = max(uResolution.x, uResolution.y) * uMaxExtentFactor;\n float largestExtent = max(extent1, extent2);\n if (largestExtent > maxExtentPx) {\n float clampScale = maxExtentPx / largestExtent;\n extent1 *= clampScale;\n extent2 *= clampScale;\n }\n\n // Screen centre in pixels from the clip-space centre (gl_FragCoord\n // convention: origin at the viewport's bottom-left corner).\n vCenterScreen = (centerClip.xy * invW * 0.5 + 0.5) * uResolution;\n\n vec2 quadOffset = aQuadCorner.x * majorAxis * extent1\n + aQuadCorner.y * minorAxis * extent2;\n vec2 screenPos = vCenterScreen + quadOffset;\n vec2 ndcXY = (screenPos / uResolution) * 2.0 - 1.0;\n\n // Depth through the same projection (the clip-space centre above).\n float ndcZ = centerClip.z / centerClip.w;\n\n gl_Position = vec4(ndcXY, ndcZ, 1.0);\n\n vNodeId = uNodeId;\n // Storage slot, NOT gl_InstanceID (the draw slot): identical\n // under Phase-1 identity ordering, and stays correct once the\n // sort worker permutes draw order (Phase 2+).\n vElementId = luxarElementIdParts();\n }\n" = ...

    Picking vertex shader for gsplats. Adds uNodeId/vNodeId/vElementId, strips colormap. Uses tighter truncation (1.5σ) for more precise picking.