我有一个场景,我想要将透视对象(即远处看起来较小的物体)与正交对象(即不论距离看起来大小相同的物体)组合在一起。透视对象是渲染的“世界”的一部分,而正交对象是装饰,如标签或图标。与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,它将显示在正方形之间,因此在正确定位的情况下,我正在尝试的原则应该是可能的。
我知道我不能用正交和透视相机渲染场景。我的目的是在每次渲染之前重新定位所有正交对象,使它们看起来处于正确的位置。
我需要做什么才能从透视坐标计算正交坐标,以便使用正确的深度值渲染对象?
更新:
如果有人遇到类似的问题,我已经在问题的当前解决方案中添加了答案。然而,由于该解决方案不能提供与正射相机相同的质量。如果索索能解释为什么正射相机不能按预期工作和/或提供问题的解决方案,我仍然会很高兴。
你非常接近你所期望的结果。您忘记更新相机矩阵,必须计算出操作project
和project
可以正常工作:
pCam.updateMatrixWorld ( false );
oCam.updateMatrixWorld ( false );
circle.position.project(pCam).unproject(oCam);
在渲染中,场景的每个网格通常由模型矩阵,视图矩阵和投影矩阵变换。
如果在另一个片段“后面”或“之前”绘制片段,则取决于片段的深度值。而对于正交投影,视图空间的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)
在透视投影中,投影矩阵描述了从针孔相机到视口的2D点看世界中3D点的映射。 相机平截头体(截头金字塔)中的眼睛空间坐标被映射到立方体(标准化设备坐标)。
透视投影矩阵:
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 = z ndc * 0.5 + 0.5
,因此也可以通过标准化设备坐标而不是深度值进行计算。
归一化的设备坐标可以通过project
的THREE.PerspectiveCamera
函数轻松计算。 project
从wolrd空间转换到视图空间,从视图空间到标准化设备坐标。
为了找到正交投影之间的Z坐标,必须将中间标准化设备Z坐标变换为视图空间Z坐标。这可以通过unproject
的THREE.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>
我找到了一个只涉及透视相机的解决方案,并根据它们与相机的距离来缩放装饰。它类似于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);
该解决方案的问题在于,在大多数情况下,计算足够精确以提供精确的尺寸。但是偶尔出现一些舍入误差会使装饰无法正确渲染。