/* Frontend-only canonical depth renderer. * * The server sends uint16 little-endian depth codes. This module performs * only the display conversion: code -> q01/q99-clamped 8-bit index -> Turbo * LUT -> RGBA (Google Turbo = Rerun's default depth colormap, blue→red ramp). * No colorized depth frame is uploaded, persisted, or returned by the API. */ (function () { const DEPTH_QMAX = 4095; let lut = null; let lutPromise = null; const VERTEX_SHADER = `#version 300 es in vec2 a_position; out vec2 v_uv; void main() { v_uv = a_position * 0.5 + 0.5; gl_Position = vec4(a_position, 0.0, 1.0); } `; const FRAGMENT_SHADER = `#version 300 es precision highp float; precision highp usampler2D; in vec2 v_uv; uniform usampler2D u_depth; uniform sampler2D u_lut; uniform float u_q01; uniform float u_q99; out vec4 out_color; void main() { // Typed-array texture rows start at the top for this presentation // path; flip only the texture lookup, never the stored samples. uint code = texture(u_depth, vec2(v_uv.x, 1.0 - v_uv.y)).r; // Rerun-style adaptive range: clamp to [q01, q99] then map to LUT. float span = max(u_q99 - u_q01, 1.0); float clamped = clamp(float(code), u_q01, u_q99); uint c8 = uint((clamped - u_q01) * 255.0 / span + 0.5); out_color = texelFetch(u_lut, ivec2(int(c8), 0), 0); } `; function compileShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { gl.deleteShader(shader); return null; } return shader; } function createGpuState(canvas) { const gl = canvas.getContext('webgl2', { alpha: false, antialias: false, depth: false, preserveDrawingBuffer: false, }); if (!gl) return null; const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER); const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER); if (!vertex || !fragment) return null; const program = gl.createProgram(); gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program); if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null; const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ -1, -1, 1, -1, -1, 1, 1, 1, ]), gl.STATIC_DRAW); const depthTexture = gl.createTexture(); const lutTexture = gl.createTexture(); const position = gl.getAttribLocation(program, 'a_position'); const state = { gl, program, buffer, depthTexture, lutTexture, position, depthWidth: 0, depthHeight: 0, depthAllocated: false, q01Loc: gl.getUniformLocation(program, 'u_q01'), q99Loc: gl.getUniformLocation(program, 'u_q99') }; gl.useProgram(program); gl.uniform1i(gl.getUniformLocation(program, 'u_depth'), 0); gl.uniform1i(gl.getUniformLocation(program, 'u_lut'), 1); gl.bindTexture(gl.TEXTURE_2D, lutTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); // OpenCV gives BGR; upload the equivalent RGBA display order. const lutPixels = new Uint8Array(256 * 4); lut.forEach((bgr, i) => { lutPixels[i * 4] = bgr[2]; lutPixels[i * 4 + 1] = bgr[1]; lutPixels[i * 4 + 2] = bgr[0]; lutPixels[i * 4 + 3] = 255; }); gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, lutPixels); return state; } function renderGpu(canvas, codes, width, height) { let state = canvas._depthWebglState; if (!state) { state = createGpuState(canvas); if (!state) return false; canvas._depthWebglState = state; } const { gl } = state; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } gl.viewport(0, 0, width, height); gl.useProgram(state.program); gl.bindBuffer(gl.ARRAY_BUFFER, state.buffer); gl.enableVertexAttribArray(state.position); gl.vertexAttribPointer(state.position, 2, gl.FLOAT, false, 0, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, state.depthTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); if (state.depthWidth !== width || state.depthHeight !== height) { // Allocate immutable storage once. Repeated texImage2D calls can // force the browser to retire/recreate the texture and stall the // compositor. The frame path below only updates the existing // storage with texSubImage2D. if (state.depthAllocated) { // Immutable storage cannot be resized. This is normally only // reached if a source changes resolution after mount. gl.deleteTexture(state.depthTexture); state.depthTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, state.depthTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); } if (typeof gl.texStorage2D === 'function') { gl.texStorage2D(gl.TEXTURE_2D, 1, gl.R16UI, width, height); state.depthAllocated = true; } else { gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16UI, width, height, 0, gl.RED_INTEGER, gl.UNSIGNED_SHORT, null); state.depthAllocated = false; } state.depthWidth = width; state.depthHeight = height; } gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RED_INTEGER, gl.UNSIGNED_SHORT, codes); const range = _rangeFor(canvas); gl.uniform1f(state.q01Loc, range.q01); gl.uniform1f(state.q99Loc, range.q99); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, state.lutTexture); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); return true; } function _rangeFor(canvas) { const range = canvas._depthRange; if (range && Number.isFinite(range.q01) && Number.isFinite(range.q99) && range.q99 > range.q01) { return range; } return { q01: 0, q99: DEPTH_QMAX }; } function loadJetLut() { if (lut) return Promise.resolve(lut); if (lutPromise) return lutPromise; lutPromise = fetch('/api/v1/video/depth-color-lut') .then(response => { if (!response.ok) throw new Error('Depth color LUT request failed'); return response.json(); }) .then(data => { if (!data || data.name !== 'google_turbo' || data.order !== 'bgr' || !Array.isArray(data.values) || data.values.length !== 256) { throw new Error('Invalid Turbo depth LUT'); } lut = data.values; return lut; }); return lutPromise; } function render(canvas, codes, width, height) { if (!canvas || !codes || !lut || codes.length !== width * height) { return false; } try { if (renderGpu(canvas, codes, width, height)) return true; } catch (_) { canvas._depthWebglState = null; } if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } const ctx = canvas.getContext('2d'); if (!ctx) return false; let image = canvas._depthImageData; if (!image || image.width !== width || image.height !== height) { image = canvas._depthImageData = ctx.createImageData(width, height); } const range = _rangeFor(canvas); const span = Math.max(1, range.q99 - range.q01); // CPU fallback: per-pixel adaptive mapping (q01/q99) through Turbo LUT. const pixels = image.data; for (let src = 0, dst = 0; src < codes.length; src++, dst += 4) { const code = Math.max(0, Math.min(DEPTH_QMAX, codes[src])); const clamped = Math.max(range.q01, Math.min(range.q99, code)); const c8 = Math.round((clamped - range.q01) * 255 / span); const bgr = lut[Math.max(0, Math.min(255, c8))]; pixels[dst] = bgr[2]; pixels[dst + 1] = bgr[1]; pixels[dst + 2] = bgr[0]; pixels[dst + 3] = 255; } ctx.putImageData(image, 0, 0); return true; } window.DepthRenderer = { DEPTH_QMAX, loadJetLut, render }; })();