ThreeJs学习笔记【day13】摄像机 【1】

468 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情 >>

透视相机 PerspectiveCamera

透视相机是threeJs中最常用的相机,他可以提供出符合我们直觉的近大远小的效果,PerspectiveCamera 定义了一个 视锥frustum,视锥指的就是切掉顶的三角锥或者实心金字塔,实心体solid一般指的是 圆柱,球体,立方体,圆锥,还有梯形立方体。

PerspectiveCamera通过四个属性来定义一个视锥. near定义了视锥的前端, far定义了后端, fov是视野, 通过计算正确的高度来从摄像机的位置获得指定的以near为单位的视野, 定义的是视锥的前端和后端的高度. aspect间接地定义了视锥前端和后端的宽度, 实际上视锥的宽度是通过高度乘以aspect来得到的,fov夹角要注意是Z轴和Y轴的夹角。

image.png

这里需要借助一些工具来更好的理解透视相机的渲染原理,首先是利用上一篇构造的平面+立方+球体,然后添加两个相机,一个展现相机角度,一个绘制相机视角示意图,方便更好的去理解透视相机的渲染原理。

  • 略过构造平面的部分
  • 为了方便观察, 我们采用的是半球光光源
  • 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);
    }

好了,现在左侧可以看到主摄像机的视角, 右侧则是辅摄像机观察主摄像机和主摄像机的视锥轮廓. 可以调整nearfarfov和用鼠标移动摄像机来观察视锥轮廓和场景之间的关系.

这里还需要注意的一点就是,gui是不知道像素间的先后关系的,所以会出现z冲突,表现起来就是,绘制物体的着色会出现破裂和混乱的情况,如下图

image.png

这个时候我们需要做的就是,声明renderer的时候,指定logarithmicDepthBuffer这个属性,当然,这个办法不是很推荐,最重要的是,请不要选择太小的near,太大的far,根据自己的需求,选择合适的远近距离,既不丢失重要的近景,也不让远处的东西消失不见,这个是一个长久的课题。