Транспонировать z-положение от перспективы до орфографической камеры в three.js

У меня есть сцена, где я хочу объединить перспективные объекты (т.е. объекты, которые кажутся меньше, когда они находятся далеко) с ортогографическими объектами (то есть, объекты, которые появляются одинакового размера независимо от расстояния). Перспективные объекты являются частью визуализированного "мира", в то время как ортогографические объекты являются украшениями, такими как метки или значки. В отличие от HUD, я хочу, чтобы ортогографические объекты отображались "внутри" мира, что означает, что они могут быть покрыты объектами мира (представьте плоскость, проходящую перед меткой).

Мое решение состоит в том, чтобы использовать один рендер, но две сцены, одна с PerspectiveCamera и один с OrthogographicCamera, Я рендерил их по порядку, не очищая буфер z (рендер autoClear свойство установлено в false). Проблема, с которой я сталкиваюсь, заключается в том, что мне нужно синхронизировать размещение объектов в каждой сцене, чтобы объекту в одной сцене была назначена z-позиция, которая находится позади объектов в другой сцене перед ним, но перед объектами, которые за этим.

Для этого я обозначаю свою перспективную сцену как "ведущую", т.е. все координаты всех объектов (перспективных и ортогональных) назначаются на основе этой сцены. Перспективные объекты используют эти координаты напрямую и отображаются внутри этой сцены и с помощью перспективной камеры. Координаты ортогографических объектов преобразуются в координаты в ортогографической сцене и затем визуализируются в этой сцене с помощью ортогографической камеры. Я выполняю преобразование, проецируя координаты в перспективной сцене на область просмотра перспективной камеры, а затем обратно на ортогональную сцену с помощью ортогографической камеры:

position.project(perspectiveCamera).unproject(orthogographicCamera);

Увы, это не работает с отступом. Ортогографические объекты всегда отображаются перед перспективными объектами, даже если они должны находиться между ними. Рассмотрим пример, в котором синий круг должен отображаться за красным квадратом, но перед зеленым (а это не так):

var pScene = new THREE.Scene();
var oScene = new THREE.Scene();

var pCam = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000);
pCam.position.set(0, 40, 50);
pCam.lookAt(new THREE.Vector3(0, 0, -50));

var oCam = new THREE.OrthographicCamera(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, 1, 500);
oCam.Position = pCam.position.clone();

pScene.add(pCam);
pScene.add(new THREE.AmbientLight(0xFFFFFF));

oScene.add(oCam);
oScene.add(new THREE.AmbientLight(0xFFFFFF));

var frontPlane = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshBasicMaterial( { color: 0x990000 }));
frontPlane.position.z = -50;
pScene.add(frontPlane);

var backPlane = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshBasicMaterial( { color: 0x009900 }));
backPlane.position.z = -100;
pScene.add(backPlane);

var circle = new THREE.Mesh(new THREE.CircleGeometry(60, 20), new THREE.MeshBasicMaterial( { color: 0x000099 }));
circle.position.z = -75;

//Transform position from perspective camera to orthogonal camera -> doesn't work, the circle is displayed in front
circle.position.project(pCam).unproject(oCam);

oScene.add(circle);

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

renderer.autoClear = false;
renderer.render(oScene, oCam);
renderer.render(pScene, pCam);

Вы можете попробовать код здесь.

В перспективном мире (мировое) z-положение круга равно -75, что находится между квадратами (-50 и -100). Но это на самом деле отображается перед обоими квадратами. Если вы вручную установите z-положение окружностей (в ортогографической сцене) на -500, оно будет отображаться между квадратами, поэтому при правильном расположении то, что я пытаюсь сделать, должно быть возможным в принципе.

Я знаю, что не могу визуализировать сцену одинаково с ортогональными и перспективными камерами Мое намерение состоит в том, чтобы перед каждым рендерингом переместить все ортогональные объекты, чтобы они оказались в правильном положении

Что мне нужно сделать, чтобы рассчитать ортогографические координаты по координатам перспективы, чтобы объекты отображались с правильными значениями глубины?

ОБНОВИТЬ:

Я добавил ответ с моим текущим решением проблемы на случай, если у кого-то возникнет аналогичная проблема. Однако, поскольку это решение не обеспечивает такое же качество, как у ортогографической камеры. Так что я все равно был бы рад, если бы кто-нибудь смог объяснить, почему ортогографическая камера не работает должным образом и / или предоставить решение проблемы.

2 ответа

Решение

Вы очень близки к результату, который вы ожидали. Вы забыли обновить матрицы камеры, для которых необходимо рассчитать, что операция project а также project может правильно работать:

pCam.updateMatrixWorld ( false );
oCam.updateMatrixWorld ( false );
circle.position.project(pCam).unproject(oCam);

Объяснение:

При рендеринге каждая сетка сцены обычно преобразуется матрицей модели, матрицей вида и матрицей проекции.

  • Матрица проекции:
    Матрица проекции описывает отображение от трехмерных точек сцены к двухмерным точкам области просмотра. Матрица проекции преобразуется из пространства просмотра в пространство клипа, а координаты в пространстве клипа преобразуются в нормализованные координаты устройства (NDC) в диапазоне (-1, -1, -1) в (1, 1, 1) путем деления с компонентом w координат клипа.

  • Посмотреть матрицу:
    Матрица вида описывает направление и позицию, с которой просматривается сцена. Матрица вида трансформируется из пространства Вольда в пространство вида (глаза). В системе координат в окне просмотра ось X указывает влево, ось Y вверх и ось Z находятся вне вида (обратите внимание, что в правой системе Z-ось является перекрестным произведением X-оси). Ось и ось Y).

  • Модельная матрица:
    Матрица модели определяет местоположение, ориентацию и относительный размер сетки в сцене. Матрица модели преобразует позиции вершин из сетки в мировое пространство.


Если фрагмент нарисован "позади" или "перед" другим фрагментом, зависит от значения глубины фрагмента. В то время как для ортографической проекции координата Z пространства вида линейно отображается на значение глубины, в перспективной проекции она не является линейной.

В общем случае значение глубины рассчитывается следующим образом:

float ndc_depth = clip_space_pos.z / clip_space_pos.w;
float depth = (((farZ-nearZ) * ndc_depth) + nearZ + farZ) / 2.0;

Матрица проекции описывает отображение от трехмерных точек сцены к двухмерным точкам области просмотра. Он преобразуется из пространства глаза в пространство клипа, а координаты в пространстве клипа преобразуются в нормализованные координаты устройства (NDC) путем деления на компонент w координат клипа.

В ортогональной проекции координаты в глазном пространстве линейно отображаются на нормализованные координаты устройства.


Ортографическая проекция

В ортогональной проекции координаты в глазном пространстве линейно отображаются на нормализованные координаты устройства.

ортографическая проекция

Ортографическая матрица проекции:

r = right, l = left, b = bottom, t = top, n = near, f = far 

2/(r-l)         0               0               0
0               2/(t-b)         0               0
0               0               -2/(f-n)        0
-(r+l)/(r-l)    -(t+b)/(t-b)    -(f+n)/(f-n)    1

В ортогональной проекции Z-компонент вычисляется линейной функцией:

z_ndc = z_eye * -2/(f-n) - (f+n)/(f-n)

Ортогональная функция Z


Перспективная проекция

В перспективной проекции матрица проекции описывает отображение из трехмерных точек в мире, как они видны с камеры-обскуры, в двумерные точки видового экрана.
Координаты пространства глаза в усеченной области камеры (усеченная пирамида) отображаются в куб (нормализованные координаты устройства).

Перспективная проекция

Матрица перспективной проекции:

r = right, l = left, b = bottom, t = top, n = near, f = far

2*n/(r-l)      0              0                0
0              2*n/(t-b)      0                0
(r+l)/(r-l)    (t+b)/(t-b)    -(f+n)/(f-n)    -1    
0              0              -2*f*n/(f-n)     0

В перспективной проекции Z-компонент вычисляется рациональной функцией:

z_ndc = ( -z_eye * (f+n)/(f-n) - 2*f*n/(f-n) ) / -z_eye

Функция перспективы Z

Подробное описание смотрите в ответе на вопрос о переполнении стека. Как визуализировать глубину линейно в современном OpenGL с gl_FragCoord.z ​​во фрагментном шейдере?


В вашем случае это означает, что вы должны выбрать координату Z круга в ортографической проекции таким образом, чтобы значение глубины находилось между глубинами объектов в перспективной проекции.
Так как значение глубины не более чем depth = z ndc * 0.5 + 0.5 в обоих случаях можно также выполнять вычисления по нормализованным координатам устройства вместо значений глубины.

Нормализованные координаты устройства могут быть легко вычислены project функция THREE.PerspectiveCamera, project преобразует из пространства wolrd в пространство просмотра и из пространства просмотра в нормализованные координаты устройства.

Чтобы найти координату Z, которая находится между ними в ортографической проекции, среднюю нормализованную координату Z устройства необходимо преобразовать в координату Z пространства обзора. Это может быть сделано unproject функция THREE.PerspectiveCamera, unproject преобразует из нормализованных координат устройства для просмотра пространства и из пространства просмотра в пространство мира.

Смотрите далее OpenGL - Координаты мыши в Космические координаты.


Смотрите пример:

var renderer, pScene, oScene, pCam, oCam, frontPlane, backPlane, circle;

  var init = function () {
    pScene = new THREE.Scene();
    oScene = new THREE.Scene();
    
    pCam = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 1000);
    pCam.position.set(0, 40, 50);
    pCam.lookAt(new THREE.Vector3(0, 0, -50));
    
    oCam = new THREE.OrthographicCamera(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, 1, 500);
    oCam.Position = pCam.position.clone();
    
    pScene.add(pCam);
    pScene.add(new THREE.AmbientLight(0xFFFFFF));
    
    oScene.add(oCam);
    oScene.add(new THREE.AmbientLight(0xFFFFFF));
    
    
    frontPlane = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshBasicMaterial( { color: 0x990000 }));
    frontPlane.position.z = -50;
    pScene.add(frontPlane);
    
    backPlane = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshBasicMaterial( { color: 0x009900 }));
    backPlane.position.z = -100;
    pScene.add(backPlane);

    circle = new THREE.Mesh(new THREE.CircleGeometry(20, 20), new THREE.MeshBasicMaterial( { color: 0x000099 }));
    circle.position.z = -75;

    
    //Transform position from perspective camera to orthogonal camera -> doesn't work, the circle is displayed in front
    pCam.updateMatrixWorld ( false );
    oCam.updateMatrixWorld ( false );
    circle.position.project(pCam).unproject(oCam);
    
    oScene.add(circle);
    
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);
  };
  
  var render = function () {
  
    renderer.autoClear = false;
    renderer.render(oScene, oCam);
    renderer.render(pScene, pCam);
  };
  
  var animate = function () {
      requestAnimationFrame(animate);
      //controls.update();
      render();
  };
  
  
  init();
  animate();
html,body {
    height: 100%;
    width: 100%;
    margin: 0;
    overflow: hidden;
}
<script src="https://threejs.org/build/three.min.js"></script>

Я нашел решение, которое включает в себя только перспективную камеру и масштабирует украшения в соответствии с их расстоянием до камеры. Это похоже на ответ на аналогичный вопрос, но не совсем то же самое. Моя конкретная проблема заключается в том, что мне нужно, чтобы украшения не были одинакового размера, независимо от их расстояния до камеры, мне также нужно контролировать их точный размер на экране.

Чтобы масштабировать их до нужного размера, а не до любого размера, чтобы доза не изменилась, я использую функцию для расчета размера экрана, найденного в этом ответе, чтобы вычислить положение обоих концов вектора известной длины на экране и проверить длина проекции на экран. Из разницы в длине я могу рассчитать точный коэффициент масштабирования:

var widthVector = new THREE.Vector3( 100, 0, 0 );
widthVector.applyEuler(pCam.rotation);

var baseX = getScreenPosition(circle, pCam).x;
circle.position.add(widthVector);
var referenceX = getScreenPosition(circle, pCam).x;
circle.position.sub(widthVector);

var scale = 100 / (referenceX - baseX);
circle.scale.set(scale, scale, scale);

Проблема с этим решением состоит в том, что в большинстве случаев расчет является достаточно точным, чтобы обеспечить точный размер. Но время от времени некоторые ошибки округления приводят к тому, что украшение отображается неправильно.

Другие вопросы по тегам