在 three.js 中将 z 位置从透视图转置到正交相机

Transpose z-position from perspective to orthographic camera in three.js

我有一个场景,我想将透视对象(即远离时看起来更小的对象)与正交对象(即无论距离如何都显示相同大小的对象)组合起来。透视对象是渲染的一部分 "world",而正交对象是装饰物,如标签或图标。与 HUD 不同,我希望正交对象呈现 "within" 世界,这意味着它们可以被世界对象覆盖(想象一个平面从标签前经过)。

我的解决方案是使用一个渲染器,但使用两个场景,一个使用 PerspectiveCamera,一个使用 OrthogographicCamera。我在不清除 z 缓冲区的情况下按顺序渲染它们(渲染器的 autoClear 属性 设置为 false)。我面临的问题是我需要同步每个场景中对象的放置,以便为一个场景中的对象分配一个 z 位置,该位置位于 另一个场景中的对象后面 在它之前,但在它后面的对象之前。

为此,我将透视场景指定为 "leading" 场景,即。所有对象的所有坐标(透视和正交)都是基于这个场景分配的。透视对象直接使用这些坐标,并在该场景中使用透视相机进行渲染。正交对象的坐标被转换为正交场景中的坐标,然后使用正交相机在该场景中进行渲染。我通过将透视场景中的坐标投影到透视相机的视图窗格然后使用正交相机返回正交场景来进行转换:

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);

你可以try out the code here.

在透视世界中,圆的(世界)z 位置为 -75,位于正方形(-50 和 -100)之间。但它实际上显示在两个方块的前面。如果您手动将圆的 z 位置(在正交场景中)设置为 -500,它会显示在正方形之间,因此通过正确的定位,原则上我正在尝试的应该是可能的。

我知道我无法用正交相机和透视相机渲染相同的场景。我的意图是在每次渲染之前重新定位所有正交对象,以便它们 看起来 处于正确的位置。

我需要做什么才能根据透视坐标计算正交坐标,以便使用正确的深度值渲染对象?

更新:

我已经添加了我当前解决问题的答案,以防有人遇到类似问题。但是,由于此解决方案无法提供与正交相机相同的质量。因此,如果有人能解释为什么正交相机无法按预期工作,我仍然会很高兴and/or 提供解决问题的方法。

我找到了一个解决方案,它只涉及透视相机,并根据装饰物与相机的距离来缩放装饰物。它类似于 answer posted to a similar question,但不完全相同。我的具体问题是,我不仅需要装饰物的大小与它们与相机的距离无关,我还需要控制它们在屏幕上的 确切大小 .

为了将它们缩放到正确的大小,而不是任何不会改变的大小,我使用该函数在屏幕上计算size found in this answer 计算屏幕上已知长度的向量两端的位置,并检查投影到屏幕的长度。从长度的差异我可以计算出确切的比例因子:

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);

此解决方案的问题在于,在大多数情况下,计算足够精确,可以提供准确的大小。但时不时会出现一些舍入错误,导致装饰无法正确呈现。

您与预期的结果非常接近。你忘了更新相机矩阵,必须计算projectproject操作才能正常工作:

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

解释:

在渲染中,场景的每个网格通常由模型矩阵、视图矩阵和投影矩阵进行变换。

  • 投影矩阵:
    投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。投影矩阵从视图space转换为剪辑space,剪辑space中的坐标转换为范围为(-1, -1)的归一化设备坐标(NDC) , -1) 到 (1, 1, 1) 除以剪辑坐标的 w 分量。

  • 查看矩阵:
    视图矩阵描述了观察场景的方向和位置。视图矩阵从世界 space 转换为视图(眼睛)space。在视口的坐标系中,X 轴指向左侧,Y 轴指向上方,Z 轴指向视图外(请注意,在右手系统中,Z 轴是 X 轴的叉积轴和 Y 轴)。

  • 模型矩阵:
    模型矩阵定义场景中网格的位置、方向和相对大小。模型矩阵将顶点位置从网格变换到世界 space.


如果一个片段绘制"behind"或"before"另一个片段,取决于片段的深度值。对于正射投影,视图的 Z 坐标 space 线性映射到深度值,而在透视投影中它是 而不是 线性。

一般来说,深度值的计算方式如下:

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

投影矩阵描述了从场景的 3D 点到视口的 2D 点的映射。它从眼睛space变换到剪辑space,剪辑space中的坐标通过除以剪辑坐标的w分量转换为归一化设备坐标(NDC)。

在正交投影中,眼睛中的坐标 space 线性映射到归一化设备坐标。


正射投影

在正交投影中,眼睛中的坐标 space 线性映射到归一化设备坐标。

正交投影矩阵:

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)


透视投影

在透视投影中,投影矩阵描述了从针孔相机看到的世界中的 3D 点到视口的 2D 点的映射。
相机平截头体(截棱锥)中的眼睛 space 坐标映射到立方体(归一化设备坐标)。

透视投影

透视投影矩阵:

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

在Perspective Projection中,Z分量由有理函数计算:

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

在 Stack Overflow 问题的答案中查看详细说明 How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?


在您的情况下,这意味着您必须以这种方式选择正交投影中圆的 Z 坐标,即 depth 值介于 depths 透视投影中的对象。
由于在这两种情况下深度值都是 depth = z ndc * 0.5 + 0.5,因此也可以通过标准化设备坐标而不是深度值进行计算。

通过 THREE.PerspectiveCameraproject 函数可以轻松计算标准化设备坐标。 project 从世界 space 转换为视图 space,从视图 space 转换为规范化设备坐标。

要在正交投影中找到介于两者之间的 Z 坐标,必须将中间归一化设备 Z 坐标转换为视图 space Z 坐标。这可以通过 THREE.PerspectiveCameraunproject 函数来完成。 unproject 从标准化设备坐标转换为视图 space,从视图 space 转换为世界空间。

进一步查看


看例子:

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>