将z-位置从透视转换为正交相机in three.js

问题描述 投票:3回答:2

我有一个场景,我想要将透视对象(即远处看起来较小的物体)与正交对象(即不论距离看起来大小相同的物体)组合在一起。透视对象是渲染的“世界”的一部分,而正交对象是装饰,如标签或图标。与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);

你可以try out the code here

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

我知道我不能用正交和透视相机渲染场景。我的目的是在每次渲染之前重新定位所有正交对象,使它们看起来处于正确的位置。

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

更新:

如果有人遇到类似的问题,我已经在问题的当前解决方案中添加了答案。然而,由于该解决方案不能提供与正射相机相同的质量。如果索索能解释为什么正射相机不能按预期工作和/或提供问题的解决方案,我仍然会很高兴。

javascript three.js camera projection coordinate-transformation
2个回答
2
投票

你非常接近你所期望的结果。您忘记更新相机矩阵,必须计算出操作projectproject可以正常工作:

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

说明:

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

  • 投影矩阵: 投影矩阵描述了从场景的3D点到视口的2D点的映射。投影矩阵从视图空间转换到剪辑空间,剪辑空间中的坐标转换为范围(-1,-1,-1)到(1,1,1)范围内的规范化设备坐标(NDC)通过用剪辑坐标的w分量来划分。
  • 查看矩阵: 视图矩阵描述了查看场景的方向和位置。视图矩阵从wolrd空间转换为视图(眼睛)空间。在视口的坐标系中,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;

投影矩阵描述了从场景的3D点到视口的2D点的映射。它从眼睛空间变换到剪辑空间,并且通过用剪辑坐标的w分量划分,剪辑空间中的坐标被变换为标准化设备坐标(NDC)。

在正交投影中,眼睛空间中的坐标线性映射到标准化设备坐标。

Orthographic Projection

在正交投影中,眼睛空间中的坐标线性映射到标准化设备坐标。

orthographic projection

正投影矩阵:

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)

Orthographic Z function

Perspective Projection

在透视投影中,投影矩阵描述了从针孔相机到视口的2D点看世界中3D点的映射。 相机平截头体(截头金字塔)中的眼睛空间坐标被映射到立方体(标准化设备坐标)。

透视投影enter image description here

透视投影矩阵:

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

Perspective Z function

请参阅Stack Overflow问题How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?答案的详细说明

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

归一化的设备坐标可以通过projectTHREE.PerspectiveCamera函数轻松计算。 project从wolrd空间转换到视图空间,从视图空间到标准化设备坐标。

为了找到正交投影之间的Z坐标,必须将中间标准化设备Z坐标变换为视图空间Z坐标。这可以通过unprojectTHREE.PerspectiveCamera函数来完成。 unproject从标准化设备坐标转换为视图空间,从视图空间转换为世界sapce。

另见OpenGL - Mouse coordinates to Space coordinates

看例子:

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>

1
投票

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

要将它们缩放到正确的大小,而不是任何不改变的大小,我使用该函数计算屏幕大小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);

该解决方案的问题在于,在大多数情况下,计算足够精确以提供精确的尺寸。但是偶尔出现一些舍入误差会使装饰无法正确渲染。

© www.soinside.com 2019 - 2024. All rights reserved.