3D-кино с трекингом глаз: технический разбор моей реализации и открытые вопросы
- суббота, 2 мая 2026 г. в 00:00:04
В моей домашней коллекции есть несколько фильмов в формате Top-Bottom стереопары — Marvel-овские, «Аватар», «Гравитация». Без 3D-телевизора или VR-очков смотреть их без потерь нельзя: на обычном мониторе видна сжатая по вертикали стереопара. Поляризованные очки и активные затворы на десктопе работают плохо или дорого. Анаглифные красно-синие очки убивают цвет.
Хотелось третьего варианта: смотреть на обычном мониторе, без очков, с минимальным железом, и при этом получать ощущение глубины. Идея, на которую опирался — head-coupled perspective: показывать изображение для одного глаза за раз, но переключать его в зависимости от того, куда смотрит зритель. Двигаешь голову влево — видишь левое изображение из стереопары. Двигаешь вправо — правое. Между этими положениями — плавный переход. Мозг получает не настоящую стереопару (двух кадров одновременно нет), а правильную картинку с правильной перспективой для текущего положения головы.
Эффект известен с 2008 года, когда Johnny Chung Lee выложил свой Wii Remote head-tracking demo — выступал тогда с этим на TED, видео разошлось в пределах разработческого сообщества по всему миру. С тех пор подход не раз пытались перенести на новые платформы: на iPhone X с TrueDepth-камерой, на Android, на VR-headset’ы. Свежее академическое исследование 2025 года из японского университета — статья «Real-time 3D Light-field Viewing with Eye-tracking on Conventional Displays» — описывает примерно эту же идею для light-field контента. И самое важное для контекста этой статьи: 1 октября 2025 года бывший инженер Meta Daniel Habib опубликовал свой проект True3D — head-tracked Window Mode на чистом веб-стеке. Это сейчас одна из самых обсуждаемых демок в индустрии 3D-видео, и ниже мне придётся подробно разобрать, что у Habib’а получилось и почему мой подход устроен принципиально иначе.
Habib работал в Meta и ушёл, чтобы сделать True3D Labs. На сайте lab.true3d.com/targets лежит живая демка: открываешь в Chrome, разрешаешь камеру, и на экране в комнате «висит» 3D-сцена — серия концентрических мишеней на разных глубинах. Двигаешь головой — мишени двигаются ровно так, как двигались бы реальные предметы в окне. Без очков. Habib называет это Window Mode. В пресс-релизах и видео он показывает, среди прочего, диснеевский «Steamboat Willie» 1928 года, рендеренный в Window Mode: ранний Микки Маус будто заякорен в твоей комнате, и ты смотришь на него «через окно» монитора.
Технологически True3D работает так:
Front-camera + MediaPipe FaceLandmarker для трекинга 6DoF позиции головы (включая iris-landmarks для поправки на положение зрачка)
Метрическая оценка расстояния глаз от веб-камеры по apparent diameter глаз и FOV камеры
Off-axis projection matrix — расчёт асимметричного frustum’а, который и создаёт эффект «окна»
Рендер сцены из перспективы зрителя через spatial-player library
Ключевая фраза из их документации: «scene is treated as a literal window: the camera origin is your eye position, and the projection math ensures that parallax and occlusion behave exactly as they would if you were looking through a glass window». Это именно то, что у Lee на Wii в 2008-м, только теперь в браузере и без инфракрасных датчиков на голове.
Самое интересное для технической дискуссии — то, что они рендерят. Не готовое видео. У True3D под капотом volumetric video pipeline: контент представлен как voxels или 3D Gaussian splats. Гауссовый сплат — это точечный трёхмерный эллипсоид с цветом, прозрачностью и spherical-harmonic-коэффициентами для view-dependent цвета. Технология родом из работы Inria 2023 года «3D Gaussian Splatting for Real-Time Radiance Field Rendering», даёт реал-тайм рендер 3D-сцены с разных углов на потребительском железе. Steamboat Willie у Habib’а — это не плоское видео, прокинутое в Window Mode, это исходник, превращённый в volumetric scene через их pipeline (.splv файлы для динамики, .vv для статики). Когда зритель двигает голову, рендер делается из новой точки в 3D-сцене — а значит, off-axis projection даёт принципиально корректную картинку с любой позиции глаз.
И вот здесь у моей реализации огромный архитектурный компромисс.
Я не имею права превращать готовый коммерческий 3D-фильм в Gaussian splats. Во-первых, это юридически серая зона (производное произведение). Во-вторых, processing-pipeline для двухчасового фильма в splats — это часы (а то и сутки) GPU-времени на каждом фильме, и сжатие splv-формата сейчас далеко от MPEG-эффективности. Реалистично у меня есть только готовая Top-Bottom стереопара в MKV, и нужно работать с ней.
То есть у меня нет volumetric data. У меня есть два фиксированных 2D-вида (левый глаз и правый глаз), снятых из двух фиксированных позиций камер. Любое промежуточное положение головы — это интерполяция между этими двумя видами. И отсюда сразу следует: настоящий off-axis projection (как у Habib’а) у меня в принципе не работает — пересчитывать frustum не из чего. Нет 3D-сцены, есть две картинки.
Поэтому мой подход — гибридный: переключение видов (показать тот глаз, в чью сторону повёрнута голова) + 2D UV-shift двух готовых видов в шейдере для имитации параллакса. Это не математически корректное Window Mode, это трюк. Дальше — рассказ, как этот трюк сделан, что в нём работает, а где он трещит.
Стек:
React + Vite — UI и сборка
Three.js + WebGL — рендер видео через ShaderMaterial
MediaPipe Face Landmarker (478 точек, включая iris) — трекинг лица
Custom fragment shader (GLSL) — извлечение видов из стереопары + view switching
Поток данных в одном кадре:
webcam (30 FPS) → MediaPipe.detectForVideo() → 478 landmarks → eye center + iris position → smoothing pipeline (4 stages) → predictive tracker (Holt's method) → uHeadPosition uniform → fragment shader → switch / blend между left/right view → canvas
Видео-источник — MKV в формате Top-Bottom: верхняя половина кадра — правый глаз, нижняя — левый. Распаковывается на лету через THREE.VideoTexture, без перекодирования. Аудио в AC3 предварительно конвертируется в AAC отдельным npm-скриптом — браузеры AC3 не поддерживают.
MediaPipe Face Landmarker даёт 478 трёхмерных landmark’ов. Для head-coupled perspective нужны:
Координаты глаз (центр) — для определения горизонтального и вертикального смещения головы
Расстояние между глазами — для оценки расстояния от камеры (Z-координата)
Iris-landmarks (468 для левого, 473 для правого) — для определения направления взгляда в дополнение к положению головы
Конфигурация модели:
import { FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision'; const vision = await FilesetResolver.forVisionTasks( 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm' ); const faceLandmarker = await FaceLandmarker.createFromOptions(vision, { baseOptions: { modelAssetPath: 'https://storage.googleapis.com/mediapipe-models/' + 'face_landmarker/face_landmarker/float16/1/face_landmarker.task', delegate: 'GPU' }, outputFaceBlendshapes: false, outputFacialTransformationMatrixes: false, runningMode: 'VIDEO', numFaces: 1 });
delegate: 'GPU' критичен — на CPU модель работает в 5-7 раз медленнее. На интегрированной графике (Intel Iris Xe) нейросеть берёт ~3-4 ms на кадр. У True3D, кстати, тот же самый MediaPipe — это сейчас стандарт de-facto для лицевого трекинга в браузере. Вся разница — в том, что они делают с координатами после извлечения.
Извлечение позиции и взгляда:
const detect = async () => { const results = await faceLandmarker.detectForVideo(cameraVideo, Date.now()); if (!results.faceLandmarks || !results.faceLandmarks.length) return; const landmarks = results.faceLandmarks[0]; const leftEye = landmarks[33]; const rightEye = landmarks[263]; const centerX = (leftEye.x + rightEye.x) / 2; const centerY = (leftEye.y + rightEye.y) / 2; const eyeDistance = Math.hypot(rightEye.x - leftEye.x, rightEye.y - leftEye.y); // Iris-based gaze tracking const leftIris = landmarks[468]; const leftEyeInner = landmarks[133]; const leftEyeWidth = Math.abs(leftEye.x - leftEyeInner.x); const leftEyeCenterX = (leftEyeInner.x + leftEye.x) / 2; let leftGaze = (leftIris.x - leftEyeCenterX) / (leftEyeWidth / 2); // Non-linear amplification for weak signals leftGaze *= 3.0; leftGaze = Math.sign(leftGaze) * Math.pow(Math.abs(leftGaze), 0.7); leftGaze = Math.max(-2.0, Math.min(2.0, leftGaze)); const normalizedX = (centerX - 0.5) * 2; const normalizedY = (centerY - 0.5) * 2; const normalizedZ = eyeDistance * 10; onPositionUpdate({ x: normalizedX, y: normalizedY, z: normalizedZ, gazeX: leftGaze }); };
Усиление gaze через Math.pow(x, 0.7) нужно потому, что iris-движения мелкие — несколько пикселей в пределах глаза. Без нелинейной функции мелкий поворот зрачка едва заметен в выходе.
В сыром виде эти числа использовать нельзя: они дёргаются на каждом кадре из-за шумов модели и микродвижений зрачков. Дальше — пайплайн сглаживания.
Одно экспоненциальное сглаживание (стандартный EMA-фильтр) даёт либо лагающую, либо дёрганую картинку. Если коэффициент 0.7 — позиция видимо отстаёт от движения. Если 0.95 — почти не дёргается, но реакция на резкое движение падает.
Я собрал четырёхступенчатый пайплайн:
let smoothedPosition = { x: 0, y: 0, z: 1 }; let previousPosition = { x: 0, y: 0, z: 1 }; const velocityBuffer = []; const smoothingFactor = 0.95; const maxJumpThreshold = 0.05; const maxVelocityThreshold = 0.15; const velocityBufferSize = 5; function smoothPosition(rawPosition) { // Stage 1: exponential moving average smoothedPosition.x = smoothedPosition.x * smoothingFactor + rawPosition.x * (1 - smoothingFactor); smoothedPosition.y = smoothedPosition.y * smoothingFactor + rawPosition.y * (1 - smoothingFactor); smoothedPosition.z = smoothedPosition.z * smoothingFactor + rawPosition.z * (1 - smoothingFactor); // Stage 2: track velocity in sliding window const dx = smoothedPosition.x - previousPosition.x; const dy = smoothedPosition.y - previousPosition.y; const dz = smoothedPosition.z - previousPosition.z; velocityBuffer.push(Math.hypot(dx, dy, dz)); if (velocityBuffer.length > velocityBufferSize) velocityBuffer.shift(); const avgVelocity = velocityBuffer.reduce((a, b) => a + b, 0) / velocityBuffer.length; // Stage 3: scale movement if velocity exceeds physical limits if (avgVelocity > maxVelocityThreshold) { const scale = maxVelocityThreshold / avgVelocity; smoothedPosition.x = previousPosition.x + dx * scale; smoothedPosition.y = previousPosition.y + dy * scale; smoothedPosition.z = previousPosition.z + dz * scale; } // Stage 4: hard cap on per-frame jumps if (Math.abs(dx) > maxJumpThreshold) { smoothedPosition.x = previousPosition.x + Math.sign(dx) * maxJumpThreshold; } if (Math.abs(dy) > maxJumpThreshold) { smoothedPosition.y = previousPosition.y + Math.sign(dy) * maxJumpThreshold; } previousPosition = { ...smoothedPosition }; return smoothedPosition; }
Каждый этап — на конкретный класс артефактов. EMA убирает высокочастотный шум модели (5-10 пикселей дрожания на неподвижном лице). Velocity buffer ловит ложные срабатывания: иногда модель «теряет» лицо и возвращается с другой позиции в соседнем кадре. Velocity scaling ограничивает максимальную скорость — голова физически не может двигаться быстрее. Hard jump threshold — последняя страховка на случай выпадения трекинга (рука перекрыла лицо).
Числа подбирались эмпирически. smoothingFactor = 0.95 — компромисс между лагом и стабильностью. maxVelocityThreshold = 0.15 соответствует ~30 см/с реального движения головы. У Habib’а в твиттере про latency сказано так: «If that delay is too long, the scene lags and looks wobbly. Filtering out jitter and rejecting outliers keeps edges steady instead of swimming». Дословно та же проблема, к которой я пришёл, и тот же класс решений.
Помимо сглаживания, есть отдельный PredictiveTracker на double exponential smoothing (Holt’s method), который пытается компенсировать end-to-end лаг. Он предсказывает позицию на 20 ms вперёд:
class PredictiveTracker { constructor(alpha = 0.7, beta = 0.8) { this.alpha = alpha; this.beta = beta; this.position = { x: 0, y: 0, z: 0 }; this.velocity = { x: 0, y: 0, z: 0 }; this.measuredLatency = 0.020; this.latencyHistory = []; this.lastRenderTime = performance.now(); } update(measurement, deltaTime) { const dt = Math.max(deltaTime, 1e-6); ['x', 'y', 'z'].forEach(axis => { const prevPos = this.position[axis]; // Holt's level update with prediction this.position[axis] = this.alpha * measurement[axis] + (1 - this.alpha) * (prevPos + this.velocity[axis] * dt); // Holt's trend update const velocityEstimate = (this.position[axis] - prevPos) / dt; this.velocity[axis] = this.beta * velocityEstimate + (1 - this.beta) * this.velocity[axis]; }); return this.position; } measureLatency() { const now = performance.now(); const frameTime = (now - this.lastRenderTime) / 1000; this.lastRenderTime = now; this.latencyHistory.push(frameTime); if (this.latencyHistory.length > 30) this.latencyHistory.shift(); if (this.latencyHistory.length > 5) { const avg = this.latencyHistory.reduce((a, b) => a + b, 0) / this.latencyHistory.length; this.measuredLatency = Math.min(avg * 1.5, 0.050); } } predict(futureTime = null) { const t = futureTime ?? this.measuredLatency; return { x: this.position.x + this.velocity.x * t, y: this.position.y + this.velocity.y * t, z: this.position.z + this.velocity.z * t }; } }
measuredLatency измеряется динамически по среднему frame time через performance.now() — компенсация подстраивается под реальную нагрузку. Тормозит браузер — prediction time увеличивается; рендер быстрый — уменьшается.
Top-Bottom MKV открывается как обычный <video> элемент. Three.js берёт его в VideoTexture:
const texture = new THREE.VideoTexture(video); texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; texture.format = THREE.RGBAFormat; texture.generateMipmaps = false; // Critical for sharpness texture.colorSpace = THREE.SRGBColorSpace; texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
generateMipmaps = false — иначе на текстуре 1080×1080 (половина от 1080×2160 Top-Bottom) появляется заметное замыливание.
Текстура подаётся в ShaderMaterial вместе с uniform’ами:
const material = new THREE.ShaderMaterial({ uniforms: { uTexture: { value: texture }, uHeadPosition: { value: new THREE.Vector3(0, 0, 0) }, uFormat3D: { value: 0.0 }, // 0 = top-bottom, 1 = side-by-side uDepthStrength: { value: 0.5 }, uConvergence: { value: 0.0 }, uTexelSize: { value: new THREE.Vector2(1/width, 1/height) } }, vertexShader: STEREO_SHADER.vertexShader, fragmentShader: STEREO_SHADER.fragmentShader });
В render-loop’е каждое изменение позиции головы (после сглаживания и prediction) пробрасывается в uHeadPosition:
function animate() { requestAnimationFrame(animate); predictiveTracker.measureLatency(); const headPos = predictiveTracker.predict(); material.uniforms.uHeadPosition.value.set(headPos.x, headPos.y, headPos.z); renderer.render(scene, camera); }
Вот тут начинается самое спорное. И именно тут лежит главное отличие от True3D.
precision highp float; uniform sampler2D uTexture; uniform vec3 uHeadPosition; uniform float uFormat3D; uniform float uDepthStrength; uniform float uConvergence; varying vec2 vUv; void main() { float viewPosition = uHeadPosition.x; // Dead zone for stability around center if (abs(viewPosition) < 0.05) viewPosition = 0.0; // Extract base UVs for left and right views from stereopair vec2 leftBaseUv, rightBaseUv; if (uFormat3D > 0.5) { // Side-by-side: left half = left eye, right half = right eye leftBaseUv = vec2(vUv.x * 0.5, vUv.y); rightBaseUv = vec2(vUv.x * 0.5 + 0.5, vUv.y); } else { // Top-bottom: bottom half = left eye, top half = right eye leftBaseUv = vec2(vUv.x, vUv.y * 0.5 + 0.5); rightBaseUv = vec2(vUv.x, vUv.y * 0.5); } // Apply parallax shift in opposite directions for each eye float parallax = viewPosition * uDepthStrength * 0.03; float convergenceShift = uConvergence * 0.02; vec2 leftUv = vec2( clamp(leftBaseUv.x + parallax - convergenceShift, 0.0, 1.0), leftBaseUv.y ); vec2 rightUv = vec2( clamp(rightBaseUv.x - parallax + convergenceShift, 0.0, 1.0), rightBaseUv.y ); vec4 leftColor = texture2D(uTexture, leftUv); vec4 rightColor = texture2D(uTexture, rightUv); // View switching with smooth blend zone vec4 finalColor; float blendZone = 0.3; if (abs(viewPosition) < 0.001) { finalColor = leftColor; } else if (viewPosition < -blendZone) { finalColor = leftColor; } else if (viewPosition > blendZone) { finalColor = rightColor; } else { float blendFactor = smoothstep(-blendZone, blendZone, viewPosition); finalColor = mix(leftColor, rightColor, blendFactor); } gl_FragColor = finalColor; }
По шагам. Шаг 1: вычисляется позиция головы (только X — лево/право). Если человек практически по центру, view зануляется через dead zone, чтобы не было дрожи на нулевой точке. Шаг 2: из стереопары извлекаются базовые UV для левого и правого вида — каждый вид занимает половину текстуры. Шаг 3: к UV каждого вида добавляется горизонтальный сдвиг в противоположных направлениях — это псевдо-параллакс. Шаг 4: между левым и правым видом плавный переход через smoothstep в зоне ±0.3.
Помимо normal-режима в шейдере есть отдельные ветки для anaglyph (красно-синие очки): красный канал берётся из левого вида, зелёный и синий — из правого. Здесь стереоэффект полноценный, без переключения видов — он работает за счёт разделения цветовых каналов на физическом уровне через очки.
Сравните это с True3D:
Моя реализация | True3D Window Mode | |
|---|---|---|
Источник | Готовая Top-Bottom стереопара (2 фиксированных вида) | Volumetric scene (voxels / Gaussian splats) |
Перспектива | UV-shift двух 2D-видов | Off-axis projection в 3D-сцене |
При движении головы | Переключение / blend между двумя готовыми видами | Рендер из новой точки 3D-сцены |
Зона эффекта | Только между двумя позициями съёмочных камер | Любая позиция в пределах FOV |
Occlusion | Невозможен — данных нет | Корректный — сцена 3D |
Параллакс | Имитированный через UV-shift | Геометрически корректный |
И вот тут видна моя архитектурная боль: я пытаюсь делать Window Mode на данных, для которых Window Mode принципиально не выводится. У меня нет той сцены, из которой нужно было бы рендерить новые виды. У Habib’а в Steamboat Willie работает корректное окклюжен — потому что у него под капотом 3D-данные, и вид «из другой точки» — это просто новый рендер той же сцены. У меня всё, что я могу — это интерполировать между двумя готовыми снимками. И эта интерполяция везде, где появляется глубинная граница, неизбежно ломается.
MediaPipe-трекинг стабильно. На i7-1260P с интегрированной графикой держится 30-35 FPS трекинга при 60 FPS рендера видео. iris-landmarks дают разумную оценку направления взгляда.
Извлечение Top-Bottom на лету, без перекодирования.
Anaglyph-режим работает корректно. С нормальными красно-синими очками глубина видна полноценно.
Плавный transition в blend zone — пока зритель не двигается резко, переход глаз-в-глаз глазом не ловится.
Несколько связанных проблем, которые я не решил.
Когда зритель быстро поворачивает голову, происходит следующее: сглаживание предсказывает один вид, картинка переключается, через 30-40 ms приходит новая измерительная точка с другим знаком — и картинка переключается обратно. Получается визуальное «дрожание» в течение 100-200 ms.
Я не нашёл хорошего способа отличить намеренное движение головы (на которое нужно реагировать быстро) от случайного дрожания (на которое реагировать вообще не нужно). Velocity threshold помогает с шумом MediaPipe, но не с реальными быстрыми движениями зрителя.
В blend zone, когда mix(leftColor, rightColor, ...) интерполирует между двумя видами, на границах глубины (контуры лиц, волосы, рёбра объектов) возникает заметный «ghosting»: там, где левый и правый виды показывают объект на разных позициях, простая линейная интерполяция даёт двоение.
Я экспериментировал с edge detection в шейдере (abs(leftColor - rightColor) с порогом), но это даёт ложные срабатывания на любых высокочастотных текстурах (трава, одежда), а реальные depth-границы пропускает.
Корректное решение — view synthesis с использованием depth map: вместо смешивания двух готовых видов нужно из одного вида плюс карты глубины восстановить промежуточный. Это техника DIBR (depth-image-based rendering). У меня есть Python-скрипт, который через OpenCV StereoSGBM пытается извлечь disparity:
stereo = cv2.StereoSGBM_create( minDisparity=0, numDisparities=16 * 6, blockSize=7, P1=8 * 3 * 7**2, P2=32 * 3 * 7**2, disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=32, mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY ) disparity = stereo.compute(left_gray, right_gray).astype(np.float32) / 16.0 depth_smooth = cv2.bilateralFilter( cv2.normalize(disparity, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), d=5, sigmaColor=50, sigmaSpace=50 )
Качество получается слишком плохим для real-world фильмов: SGBM хорошо работает на статичных сценах с чёткими текстурами, а на движении (туман, частицы, motion blur) — даёт мусор. Я не интегрировал этот pipeline в плеер, потому что результат хуже, чем простое переключение видов.
Top-Bottom — это 1920×2160, что после распаковки даёт 1920×1080 на глаз. Половина исходного разрешения теряется. Просто масштабировать через THREE.LinearFilter помогает мало — детали уже потеряны при кодировании. Real-time апскейл (FSR/DLSS-аналог в браузере) не существует. Запускать DepthAnything или MiDaS на 60 FPS на интегрированной графике — нет, не получится.
End-to-end лаг от движения головы до обновления изображения:
Webcam capture: ~16 ms
MediaPipe inference: ~30 ms
Smoothing pipeline: ~3 ms
Three.js render + browser presentation: ~16 ms
Итого: ~65 ms в среднем
Predictive tracker компенсирует часть, но не всё. Для bullet-proof эффекта надо < 20 ms total — это уровень VR-headset с iris-tracking, а не webcam + MediaPipe в браузере.
Самое фундаментальное и то, что отличает мою реализацию от True3D. Я делаю 2D UV-shift двух готовых видов. Настоящий head-coupled perspective требует off-axis projection: пересчёт frustum’а виртуальной камеры в зависимости от позиции глаза, как у Lee и у Habib’а. У них это работает, потому что у них 3D-сцена с реальной геометрией. У меня — два готовых снимка с двух фиксированных позиций.
Эффект работает только в зоне между этими двумя позициями. Если зритель сдвинется больше, чем расстояние между изначальными камерами при съёмке, рендерить будет нечего — данных за пределами этого диапазона у меня нет. UV-shift это маскирует, но не решает. Habib про то же самое в своих заметках: «You cannot ‘look around’ objects unless that visual data exists, which becomes evident when foreground occlusions reveal missing detail». У него это проявляется на краях сцены. У меня — везде, потому что мой «диапазон визуальных данных» это два кадра, а не volumetric reconstruction.
Что я попробовал бы дальше, если бы знал направление:
Заменить SGBM на нейросетевую depth estimation. DepthAnything v2 даёт куда более чистые карты глубины, чем classical stereo matching. Вопрос — можно ли запустить её через WebGPU + ONNX Runtime Web в реальном времени, и хватит ли качества для DIBR без артефактов. Бенчмарков на реальных фильмах в браузере я не нашёл.
Frame interpolation между left и right views. RIFE, DAIN, FILM — нейросетевые интерполяторы для motion. Если бы они работали как «view interpolators» (а не «time interpolators»), можно было бы синтезировать промежуточные виды между двумя готовыми. Кто-нибудь применял их именно к стереопарам, а не к последовательным кадрам?
WebGPU compute shader для view synthesis. Если уйти от UV-shift’а к настоящей DIBR — реально ли уложить это в frame budget на 60 FPS? Compute shader с 1920×1080 disparity-map’ой и forward warping.
Альтернативы MediaPipe для трекинга. WebGazer.js быстрее, но iris-точность хуже. ONNX Runtime Web с лёгкой моделью + SharedArrayBuffer для отдельного thread’а — возможно ли получить total latency < 20 ms?
Гибрид: depth на CPU отдельным thread’ом. Если depth-extraction через OpenCV.js (или custom WASM) делать в Web Worker не на каждый кадр, а на каждый, скажем, 5-й, и интерполировать между ключевыми — можно ли получить достаточное качество для blend-зоны при приемлемой нагрузке?
Принципиальный вопрос: можно ли из готовой стереопары восстановить volumetric scene в реальном времени, чтобы делать настоящий off-axis projection? Habib решает это за счёт того, что у него на входе уже volumetric data (Gaussian splats). Я начинаю с двух 2D-видов. Существуют ли работы, где на лету (~33 ms на кадр) реконструируется хотя бы упрощённая 3D-сцена из стереопары достаточного качества для off-axis projection? Или это упирается в фундаментальное ограничение — два фиксированных кадра не дают volumetric data?
Если у вас был опыт с похожими системами или вы видели работы, которые я не нашёл — напишите в комментариях. На замечание «всё это не нужно, купи 3D-телевизор» — справедливо, но любопытство сильнее.