博客 / 詳情

返回

THREE.js 攝像機分享

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

前置代碼

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    html,
    body {
      margin: 0;
      height: 100%;
    }

    #c {
      width: 100%;
      height: 100%;
      display: block;
    }

    .split {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      display: flex;
    }

    .split>div {
      width: 100%;
      height: 100%;
    }
  </style>
</head>

<body>

  <canvas id="c">

  </canvas>
  <div class="split">
    <div id="view1"></div>
    <div id="view2"></div>
  </div>
  <script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/three@0.174.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.174.0/examples/jsm/"
        }   
    }
    </script>
  <script type="module" src="./index.js"></script>
</body>

</html>

透視攝像機PerspectiveCamera

PerspectiveCamera 通過四個屬性來定義一個視錐, near定義視錐前端, far定義遠端, fov是視野, 通過計算正確的高度來從攝像機的位置獲取指定的以near為單位的視野, 定義的是視錐的前端和遠端的高度 aspect間接地定義了視錐前端和遠端的寬度, 實際上視錐的寬度是通過高度乘以 aspect 來得到的

ScreenShot_2026-02-07_130738_245

 下面這個例子我們使用 three 的剪函數, 把視圖分成兩部分, 主視圖正常渲染, 輔視圖用來觀察 cameraHelper 的渲染

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";

function main() {
  const canvas = document.querySelector("#c");
  const view1Elem = document.querySelector("#view1");
  const view2Elem = document.querySelector("#view2");

  const renderer = new THREE.WebGLRenderer({ antialias: true, canvas });

  // #region 左視圖的相機
  const fov = 45;
  const aspect = 2; // the canvas default
  const near = 0.1;
  const far = 100;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.set(0, 10, 20);

  const cameraHelper = new THREE.CameraHelper(camera);

  const controls = new OrbitControls(camera, view1Elem);
  controls.target.set(0, 5, 0);
  controls.update();

  // #endregion

  // #region 右視圖的相機
  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();

  // #endregion

  /**
   * 設置裁剪區域和視口, 返回寬高比
   * @param {HTMLElement} elem
   * @returns
   */
  function setScissorForElement(elem) {
    // 獲取 canvas 與元素的邊界矩形
    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 設置裁剪區域和視口
    renderer.setScissor(left, positiveYUpBottom, width, height);
    renderer.setViewport(left, positiveYUpBottom, width, height);

    return width / height;
  }

  // gui 使用,限制對象中屬性的最大值最小值
  class MinMaxGUIHelper {
    constructor(obj, minProp, maxProp, minDif) {
      this.obj = obj;
      this.minProp = minProp;
      this.maxProp = maxProp;
      this.minDif = minDif;
    }
    get min() {
      return this.obj[this.minProp];
    }
    set min(v) {
      this.obj[this.minProp] = v;
      this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
    }
    get max() {
      return this.obj[this.maxProp];
    }
    set max(v) {
      this.obj[this.maxProp] = v;
      this.min = this.min; // this will call the min setter
    }
  }

  // #region 添加相機屬性的gui界面
  const gui = new GUI();
  gui.add(camera, "fov", 1, 180);
  const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
  gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
  gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");

  // #endregion

  const scene = new THREE.Scene();
  scene.background = new THREE.Color("black");
  scene.add(cameraHelper);

  {
    const planeSize = 40;

    const loader = new THREE.TextureLoader();
    const texture = loader.load("https://threejs.org/manual/examples/resources/images/checker.png");
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;
    //texture.colorSpace = THREE.SRGBColorSpace;
    const repeats = planeSize / 2;
    texture.repeat.set(repeats, repeats);

    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
    const planeMat = new THREE.MeshPhongMaterial({
      map: texture,
      side: THREE.DoubleSide,
    });
    const mesh = new THREE.Mesh(planeGeo, planeMat);
    mesh.rotation.x = Math.PI * -0.5;
    scene.add(mesh);
  }

  {
    const cubeSize = 4;
    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
    const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
    scene.add(mesh);
  }

  {
    const sphereRadius = 3;
    const sphereWidthDivisions = 32;
    const sphereHeightDivisions = 16;
    const sphereGeo = new THREE.SphereGeometry(
      sphereRadius,
      sphereWidthDivisions,
      sphereHeightDivisions,
    );
    const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
    scene.add(mesh);
  }

  {
    const color = 0xffffff;
    const intensity = 3;
    const light = new THREE.DirectionalLight(color, intensity);
    light.position.set(0, 10, 0);
    light.target.position.set(-5, 0, 0);
    scene.add(light);
    scene.add(light.target);
  }

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }

    return needResize;
  }

  function render() {
    resizeRendererToDisplaySize(renderer);

    // 啓用剪刀函數
    renderer.setScissorTest(true);

    // #region 視圖1 渲染
    const aspect1 = setScissorForElement(view1Elem);
    camera.aspect = aspect1;
    camera.updateProjectionMatrix();
    // 不在視圖 1中渲染 helper
    cameraHelper.visible = false;
    cameraHelper.update();
    renderer.render(scene, camera);
    // #endregion

    // #region 視圖2 渲染
    const aspect2 = setScissorForElement(view2Elem);
    camera2.aspect = aspect2;
    camera2.updateProjectionMatrix();
    // 在第二台攝像機中繪製cameraHelper
    cameraHelper.visible = true;
    // 單獨給視圖 2 設置個背景色
    scene.background.set(0x000040);
    renderer.render(scene, camera2);

    // #endregion

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

main();

ScreenShot_2026-02-07_130826_995

正交攝像機OrthographicCamera

與透視攝像機不同的是, 它需要設置left right top bottom nearfar 指定一個長方形, 使得視野是平行的而不是透視的

使用 zoom 屬性可以縮放世界 -> 屏幕的映射比例, 不改變實際尺寸

< 1 看到更多 > 1 看到更少

import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GUI } from "three/addons/libs/lil-gui.module.min.js";

function main() {
  const canvas = document.querySelector("#c");
  const view1Elem = document.querySelector("#view1");
  const view2Elem = document.querySelector("#view2");

  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    canvas,
    logarithmicDepthBuffer: true,
  });

  // #region 左視圖的相機
  const size = 1;
  const near = 5;
  const far = 50;
  const camera = new THREE.OrthographicCamera(-size, size, size, -size, near, far);
  camera.zoom = 0.2;
  camera.position.set(0, 20, 0);
  // camera.lookAt(0, 0, 0);
  const cameraHelper = new THREE.CameraHelper(camera);

  const controls = new OrbitControls(camera, view1Elem);
  controls.target.set(2, 0, 0);
  controls.update();

  // #endregion

  // #region 右視圖的相機
  const camera2 = new THREE.PerspectiveCamera(
    60, // fov
    2, // aspect
    0.1, // near
    500, // far
  );
  camera2.position.set(40, 10, 30);
  camera2.lookAt(0, 10, 0);

  const controls2 = new OrbitControls(camera2, view2Elem);
  controls2.target.set(0, 5, 0);
  controls2.update();

  // #endregion

  /**
   * 設置裁剪區域和視口, 返回寬高比
   * @param {HTMLElement} elem
   * @returns
   */
  function setScissorForElement(elem) {
    // 獲取 canvas 與元素的邊界矩形
    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 設置裁剪區域和視口
    renderer.setScissor(left, positiveYUpBottom, width, height);
    renderer.setViewport(left, positiveYUpBottom, width, height);

    return width / height;
  }

  // gui 使用,限制對象中屬性的最大值最小值
  class MinMaxGUIHelper {
    constructor(obj, minProp, maxProp, minDif) {
      this.obj = obj;
      this.minProp = minProp;
      this.maxProp = maxProp;
      this.minDif = minDif;
    }
    get min() {
      return this.obj[this.minProp];
    }
    set min(v) {
      this.obj[this.minProp] = v;
      this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
    }
    get max() {
      return this.obj[this.maxProp];
    }
    set max(v) {
      this.obj[this.maxProp] = v;
      this.min = this.min; // this will call the min setter
    }
  }

  // #region 添加相機屬性的gui界面
  const gui = new GUI();
  // gui.add(camera, "fov", 1, 180);
  const minMaxGUIHelper = new MinMaxGUIHelper(camera, "near", "far", 0.1);
  gui.add(minMaxGUIHelper, "min", 0.1, 50, 0.1).name("near");
  gui.add(minMaxGUIHelper, "max", 0.1, 50, 0.1).name("far");
  gui.add(camera, "zoom", 0.01, 1).name("zoom").listen(); // 調整相機展現多少單位大小

  // #endregion

  const scene = new THREE.Scene();
  scene.background = new THREE.Color("black");
  scene.add(cameraHelper);

  {
    const planeSize = 40;

    const loader = new THREE.TextureLoader();
    const texture = loader.load("https://threejs.org/manual/examples/resources/images/checker.png");
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;
    //texture.colorSpace = THREE.SRGBColorSpace;
    const repeats = planeSize / 2;
    texture.repeat.set(repeats, repeats);

    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
    const planeMat = new THREE.MeshPhongMaterial({
      map: texture,
      side: THREE.DoubleSide,
    });
    const mesh = new THREE.Mesh(planeGeo, planeMat);
    mesh.rotation.x = Math.PI * -0.5;
    scene.add(mesh);
  }

  {
    const cubeSize = 4;
    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
    const cubeMat = new THREE.MeshPhongMaterial({ color: "#8AC" });
    const mesh = new THREE.Mesh(cubeGeo, cubeMat);
    mesh.position.set(cubeSize + 1, cubeSize / 2, 0);
    scene.add(mesh);
  }

  {
    const sphereRadius = 3;
    const sphereWidthDivisions = 32;
    const sphereHeightDivisions = 16;
    const sphereGeo = new THREE.SphereGeometry(
      sphereRadius,
      sphereWidthDivisions,
      sphereHeightDivisions,
    );
    const sphereMat = new THREE.MeshPhongMaterial({ color: "#CA8" });
    const mesh = new THREE.Mesh(sphereGeo, sphereMat);
    mesh.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
    scene.add(mesh);
  }

  {
    const color = 0xffffff;
    const intensity = 3;
    const light = new THREE.DirectionalLight(color, intensity);
    light.position.set(0, 10, 0);
    light.target.position.set(-5, 0, 0);
    scene.add(light);
    scene.add(light.target);
  }

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }

    return needResize;
  }

  function render() {
    resizeRendererToDisplaySize(renderer);

    // 啓用剪刀函數
    renderer.setScissorTest(true);

    // #region 視圖1 渲染
    const aspect1 = setScissorForElement(view1Elem);
    camera.left = -aspect1;
    camera.right = aspect1;
    camera.updateProjectionMatrix();
    // 不在視圖 1中渲染 helper
    cameraHelper.visible = false;
    cameraHelper.update();
    renderer.render(scene, camera);
    // #endregion

    // #region 視圖2 渲染
    const aspect2 = setScissorForElement(view2Elem);
    camera2.aspect = aspect2;
    camera2.updateProjectionMatrix();
    // 在第二台攝像機中繪製cameraHelper
    cameraHelper.visible = true;
    // 單獨給視圖 2 設置個背景色
    scene.background.set(0x000040);
    renderer.render(scene, camera2);

    // #endregion

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

main();

ScreenShot_2026-02-07_130922_840

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.