携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情 >>
透视相机 PerspectiveCamera
透视相机是threeJs中最常用的相机,他可以提供出符合我们直觉的近大远小的效果,PerspectiveCamera 定义了一个 视锥frustum,视锥指的就是切掉顶的三角锥或者实心金字塔,实心体solid一般指的是 圆柱,球体,立方体,圆锥,还有梯形立方体。
PerspectiveCamera通过四个属性来定义一个视锥.
near定义了视锥的前端,far定义了后端,fov是视野, 通过计算正确的高度来从摄像机的位置获得指定的以near为单位的视野, 定义的是视锥的前端和后端的高度.aspect间接地定义了视锥前端和后端的宽度, 实际上视锥的宽度是通过高度乘以aspect来得到的,fov夹角要注意是Z轴和Y轴的夹角。
这里需要借助一些工具来更好的理解透视相机的渲染原理,首先是利用上一篇构造的平面+立方+球体,然后添加两个相机,一个展现相机角度,一个绘制相机视角示意图,方便更好的去理解透视相机的渲染原理。
- 略过构造平面的部分
- 为了方便观察, 我们采用的是半球光光源
- lil-gui 里有关于max和min的调节,可以调节near和far,所以构造一个Helper
- 将页面分成两块,绘制两个场景,两个相机,添加CameraHelper可以把透视相机的视锥体绘制出来
//html的部分
<div class="split">
<div id="view1" tabindex="1"></div>
<div id="view2" tabindex="2"></div>
</div>
//css
.split {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
}
.split>div {
width: 100%;
height: 100%;
}
//js
const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);
- 先获取页面上的view1和view2将OrbitControls分给view1
const view1Elem = document.querySelector('#view1');
const view2Elem = document.querySelector('#view2');
const controls = new OrbitControls(camera, view1Elem);
- 添加第二个相机
const camera2 = new THREE.PerspectiveCamera(
60, // fov
2, // aspect
0.1, // near
500, // far
);
camera2.position.set(40, 10, 30);
camera2.lookAt(0, 5, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
- 我们需要使用剪刀功能从每个摄影机的视角渲染场景,以仅渲染画布的一部分。 这个函数接受一个元素, 计算这个元素在canvas上的重叠面积, 这将设置剪刀函数和视角长宽并返回aspect
function setScissorForElement(elem) {
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
// 计算canvas的尺寸
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
// 设置剪函数以仅渲染一部分场景
const positiveYUpBottom = canvasRect.height - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
// 返回aspect
return width / height;
}
- 处理render函数
resizeRendererToDisplaySize(renderer);
// 启用剪刀函数
renderer.setScissorTest(true);
// 渲染主视野
{
const aspect = setScissorForElement(view1Elem);
// 用计算出的aspect修改摄像机参数
camera.aspect = aspect;
camera.updateProjectionMatrix();
cameraHelper.update();
// 来原视野中不要绘制cameraHelper
cameraHelper.visible = false;
scene.background.set(0x000000);
// 渲染
renderer.render(scene, camera);
}
// 渲染第二台摄像机
{
const aspect = setScissorForElement(view2Elem);
// 调整aspect
camera2.aspect = aspect;
camera2.updateProjectionMatrix();
// 在第二台摄像机中绘制cameraHelper
cameraHelper.visible = true;
scene.background.set(0x000040);
renderer.render(scene, camera2);
}
好了,现在左侧可以看到主摄像机的视角, 右侧则是辅摄像机观察主摄像机和主摄像机的视锥轮廓. 可以调整near, far, fov和用鼠标移动摄像机来观察视锥轮廓和场景之间的关系.
这里还需要注意的一点就是,gui是不知道像素间的先后关系的,所以会出现z冲突,表现起来就是,绘制物体的着色会出现破裂和混乱的情况,如下图
这个时候我们需要做的就是,声明renderer的时候,指定logarithmicDepthBuffer这个属性,当然,这个办法不是很推荐,最重要的是,请不要选择太小的near,太大的far,根据自己的需求,选择合适的远近距离,既不丢失重要的近景,也不让远处的东西消失不见,这个是一个长久的课题。