我有一个场景,我想将透视图对象(即,距离较远的对象看起来较小)和正射影像对象(即,与距离无关而大小相同的对象)组合在一起.透视图对象是渲染的“世界”的一部分,而正交图对象是装饰物,如标签或图标.与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,则它会显示在正方形之间,因此,如果位置正确,则我尝试的方法原则上应该可行.
我知道我无法使用正交摄影机和透视相机渲染场景.我的意图是在每次渲染之前重新定位所有正交物体,以使其看起来在正确的位置.
我该怎么做才能从透视坐标中计算出正交坐标,以便用正确的深度值渲染对象?
更新:
如果有人遇到类似问题,我已经用当前解决方案添加了答案.但是,由于此解决方案无法提供与正交摄影机相同的质量.因此,如果somoeone能够解释为什么正交摄影机不能按预期方式工作和/或为该问题提供解决方案,我仍然很高兴.
解决方法:
您非常接近预期的结果.您忘记了更新相机矩阵,必须计算这些矩阵以确保操作项目和项目可以正常工作:
pCam.updateMatrixWorld ( false );
oCam.updateMatrixWorld ( false );
circle.position.project(pCam).unproject(oCam);
说明:
在渲染中,通常通过模型矩阵,视图矩阵和投影矩阵来变换场景的每个网格.
>投影矩阵:
投影矩阵描述了从场景的3D点到视口的2D点的映射.投影矩阵从视图空间转换为剪辑空间,并将剪辑空间中的坐标转换为范围为(-1,-1,-1)至(1、1、1)的归一化设备坐标(NDC)通过除以剪辑坐标的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;
投影矩阵描述了从场景的3D点到视口的2D点的映射.它从眼睛空间转换到剪辑空间,并且通过除以剪辑坐标的w分量,将剪辑空间中的坐标转换为归一化设备坐标(NDC).
在正交投影中,眼睛空间中的坐标线性映射到标准化设备坐标.
正投影
在正交投影中,眼睛空间中的坐标线性映射到标准化设备坐标.
正射投影矩阵:
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点的映射.摄像机视锥中的眼睛空间坐标(截断的金字塔)映射到立方体(标准化的设备坐标).
透视投影矩阵:
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
有关堆栈溢出问题How to render depth linearly in modern OpenGL with gl_FragCoord.z in fragment shader?的答案,请参见详细说明.
在您的情况下,这意味着您必须以这种方式在正投影中选择圆的Z坐标,并且深度值介于透视投影中的对象深度之间.
由于在两种情况下深度值都不是depth = z ndc * 0.5 0.5,因此也可以通过归一化的设备坐标而不是深度值来进行计算.
可以通过THREE.PerspectiveCamera
的投影功能轻松地计算出标准化的设备坐标.该项目从世界空间过渡到视图空间,从视图空间过渡到标准化设备坐标.
为了找到正投影中介于两者之间的Z坐标,必须将中间标准化设备Z坐标转换为视图空间Z坐标.这可以通过THREE.PerspectiveCamera
的取消投影功能完成.取消投影将从规范化的设备坐标转换为视图空间,并从视图空间转换为世界空间.
进一步参见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>