快乐小兵 發表於 2026-2-7 13:10:00

THREE.js 摄像机分享

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<h2 data-id="heading-0">前置代码</h2>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;

&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;title&gt;Document&lt;/title&gt;
&lt;style&gt;
    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&gt;div {
      width: 100%;
      height: 100%;
    }
&lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;

&lt;canvas id="c"&gt;

&lt;/canvas&gt;
&lt;div class="split"&gt;
    &lt;div id="view1"&gt;&lt;/div&gt;
    &lt;div id="view2"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script type="importmap"&gt;
    {
      "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/"
      }   
    }
    &lt;/script&gt;
&lt;script type="module" src="./index.js"&gt;&lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;</pre>
</div>
<div>
<div>
<h2 data-id="heading-1">透视摄像机<code>PerspectiveCamera</code></h2>
<p><code>PerspectiveCamera</code> 通过四个属性来定义一个视锥, <code>near</code>定义视锥前端, <code>far</code>定义远端, <code>fov</code>是视野, 通过计算正确的高度来从摄像机的位置获取指定的以<code>near</code>为单位的视野, 定义的是视锥的前端和远端的高度 <code>aspect</code>间接地定义了视锥前端和远端的宽度, 实际上视锥的宽度是通过高度乘以 <code>aspect</code> 来得到的</p>
</div>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260207130750989-1976833347.png" alt="ScreenShot_2026-02-07_130738_245" loading="lazy"></p>
<p>&nbsp;下面这个例子我们使用 three 的剪函数, 把视图分成两部分, 主视图正常渲染, 辅视图用来观察&nbsp;<code>cameraHelper</code>&nbsp;的渲染</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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;
    }
    set min(v) {
      this.obj = v;
      this.obj = Math.max(this.obj, v + this.minDif);
    }
    get max() {
      return this.obj;
    }
    set max(v) {
      this.obj = 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();</pre>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260207130833262-1026145108.png" alt="ScreenShot_2026-02-07_130826_995" loading="lazy"></p>
<div>
<div>
<h2 data-id="heading-2">正交摄像机<code>OrthographicCamera</code></h2>
<p>与透视摄像机不同的是, 它需要设置<code>left</code> <code>right</code> <code>top</code> <code>bottom</code> <code>near</code> 和 <code>far</code> 指定一个长方形, 使得视野是平行的而不是透视的</p>
<p>使用 <code>zoom</code> 属性可以缩放<code>世界 -&gt; 屏幕</code>的映射比例, 不改变实际尺寸</p>
<p><code>&lt; 1</code> 看到更多 <code>&gt; 1</code> 看到更少</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">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;
    }
    set min(v) {
      this.obj = v;
      this.obj = Math.max(this.obj, v + this.minDif);
    }
    get max() {
      return this.obj;
    }
    set max(v) {
      this.obj = 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();</pre>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260207130927712-739831907.png" alt="ScreenShot_2026-02-07_130922_840" loading="lazy"></p>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19588317
頁: [1]
查看完整版本: THREE.js 摄像机分享