普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月7日首页

Three.js 透视相机完全指南:从入门到精通

作者 烛阴
2026年2月6日 22:25

什么是透视相机?

生活中的类比

想象你拿着一部手机摄像头对着一个房间:

  • 📱 手机摄像头 = Three.js 中的相机
  • 🎬 摄像头能看到的范围 = 视锥体(Frustum)
  • 📐 摄像头的视角宽度 = 视野角度(FOV)

透视相机就像真实的摄像头一样,离物体越近,看到的范围越小;离物体越远,看到的范围越大。这就是"透视"的含义。

代码示例

import * as THREE from 'three';

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    40,      // 视野角度(FOV)
    2,       // 宽高比(aspect ratio)(一般3D游戏中的屏幕适配都是在调整这个值)
    0.1,     // 近裁剪面(near)
    1000     // 远裁剪面(far)
);

camera.position.z = 120; // 相机位置

四个关键参数详解

参数 1️⃣:视野角度(FOV - Field of View)

定义:相机能看到的垂直视角范围,单位是度数(°)。

直观理解

  • 🔭 FOV = 10° → 像用望远镜看,视角很窄,看到的东西很小但很清晰
  • 📷 FOV = 40° → 像用普通手机摄像头,视角适中
  • 🐟 FOV = 90° → 像用鱼眼镜头,视角很宽,看到很多东西但会变形
// 小视角 - 看得清楚但范围小
const camera1 = new THREE.PerspectiveCamera(20, 2, 0.1, 1000);

// 中等视角 - 平衡
const camera2 = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 大视角 - 看得多但容易变形
const camera3 = new THREE.PerspectiveCamera(75, 2, 0.1, 1000);

实际应用

  • 🎮 第一人称游戏:FOV 通常 60-90°
  • 🏢 建筑可视化:FOV 通常 40-50°
  • 🎥 电影效果:FOV 通常 24-35°

参数 2️⃣:宽高比(Aspect Ratio)

定义:相机画面的宽度与高度的比例

计算方式

const aspect = window.innerWidth / window.innerHeight;
// 例如:1920 / 1080 = 1.777...

为什么重要

  • ✅ 如果 aspect 与实际画布比例一致,画面不会被拉伸
  • ❌ 如果不一致,圆形会变成椭圆,正方形会变成矩形
// 假设窗口是 1920×1080
const aspect = 1920 / 1080; // ≈ 1.78

const camera = new THREE.PerspectiveCamera(40, aspect, 0.1, 1000);

响应式设计

function onWindowResize() {
    const width = window.innerWidth;
    const height = window.innerHeight;
    
    camera.aspect = width / height;
    camera.updateProjectionMatrix(); // 重要!更新投影矩阵
    
    renderer.setSize(width, height);
}

window.addEventListener('resize', onWindowResize);

参数 3️⃣:近裁剪面(Near)

定义:相机能看到的最近距离。比这个距离更近的物体不会被显示。

为什么需要它

  • 🎯 性能优化:不渲染相机背后的物体
  • 🔍 避免穿模:防止相机进入物体内部时看到内部结构
// near = 0.1 意味着距离相机 0.1 单位以内的物体看不到
const camera = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 如果物体在 z = 0.05,它会被裁剪掉
const cube = new THREE.Mesh(geometry, material);
cube.position.z = 0.05; // ❌ 看不到

参数 4️⃣:远裁剪面(Far)

定义:相机能看到的最远距离。比这个距离更远的物体不会被显示。

为什么需要它

  • 🎯 性能优化:不渲染太远的物体
  • 🔍 深度精度:提高深度缓冲的精度
// far = 1000 意味着距离相机 1000 单位以外的物体看不到
const camera = new THREE.PerspectiveCamera(40, 2, 0.1, 1000);

// 如果物体在 z = 1500,它会被裁剪掉
const star = new THREE.Mesh(geometry, material);
star.position.z = 1500; // ❌ 看不到

视锥体的可视范围计算

什么是视锥体?

视锥体是一个四棱锥形的空间,只有在这个空间内的物体才能被看到。

计算可视范围的公式

在距离相机 distance 处,可视范围的大小为:

垂直高度 = 2 × tan(FOV/2) × distance
水平宽度 = 垂直高度 × aspect

实际计算示例

假设我们有这样的相机配置:

const fov = 40;           // 视野角度
const aspect = 2;         // 宽高比(2:1)
const distance = 120;     // 相机距离(z = 120)

// 计算垂直可视高度
const vFOV = fov * Math.PI / 180; // 转换为弧度
const height = 2 * Math.tan(vFOV / 2) * distance;
// height = 2 × tan(20°) × 120
// height = 2 × 0.364 × 120
// height ≈ 87.2

// 计算水平可视宽度
const width = height * aspect;
// width = 87.2 × 2
// width ≈ 174.4

结论:在 z=120 处,相机能看到的范围是:

  • 📏 宽度:174.4 单位
  • 📏 高度:87.2 单位

坐标范围的确定

场景布局示例

假设我们要在场景中放置一个 5×4 的网格(5列 4行),每个物体之间间距为 15 单位:

const spread = 15; // 间距

// 物体位置计算
for (let x = -2; x <= 2; x++) {
    for (let y = -1; y <= 2; y++) {
        const obj = createObject();
        obj.position.x = x * spread;  // x: -30, -15, 0, 15, 30
        obj.position.y = y * spread;  // y: -15, 0, 15, 30
        scene.add(obj);
    }
}

对象分布范围

  • X 轴:-30 到 30(宽度 60)
  • Y 轴:-15 到 30(高度 45)

检查是否超出可视范围

// 相机可视范围
const visibleWidth = 174.4;
const visibleHeight = 87.2;

// 对象范围
const objectWidth = 60;
const objectHeight = 45;

// 检查
console.log(`宽度是否超出: ${objectWidth > visibleWidth}`); // false ✅
console.log(`高度是否超出: ${objectHeight > visibleHeight}`); // false ✅

如果超出范围怎么办?

问题:当 spread=20 时,对象范围变为 80×60,超出了可视范围。

解决方案

方案 1:增加相机距离

// 原来
camera.position.z = 120;

// 改为
camera.position.z = 160; // 距离越远,看到的范围越大

方案 2:增加视野角度

// 原来
const fov = 40;

// 改为
const fov = 60; // 视角越大,看到的范围越大

方案 3:减小间距

// 原来
const spread = 20;

// 改为
const spread = 15; // 物体靠得更近

方案 4:使用正交相机

如果你不需要透视效果,可以使用正交相机(OrthographicCamera),它的可视范围不会随距离变化:

const camera = new THREE.OrthographicCamera(
    -100,  // left
    100,   // right
    50,    // top
    -50,   // bottom
    0.1,   // near
    1000   // far
);

实战代码

完整的响应式相机设置

import * as THREE from 'three';

class ResponsiveCamera {
    constructor() {
        this.fov = 40;
        this.near = 0.1;
        this.far = 1000;
        this.distance = 120;
        
        this.updateCamera();
    }
    
    updateCamera() {
        const aspect = window.innerWidth / window.innerHeight;
        
        this.camera = new THREE.PerspectiveCamera(
            this.fov,
            aspect,
            this.near,
            this.far
        );
        
        this.camera.position.z = this.distance;
    }
    
    // 计算指定深度处的可视范围
    getVisibleRange(depth = null) {
        const vFOV = (this.fov * Math.PI) / 180;
        // 如果没有指定深度,使用相机的默认距离
        const distance = depth !== null ? depth : this.distance;
        const height = 2 * Math.tan(vFOV / 2) * distance;
        const width = height * (window.innerWidth / window.innerHeight);

        return { width, height };
    }
    
    // 检查物体是否在可视范围内
    isObjectVisible(obj) {
        const pos = obj.position;
        
        // 计算物体相对于相机的距离(沿着相机的视线方向)
        const distanceFromCamera = this.camera.position.z - pos.z;
        
        // 计算物体所在深度的可视范围
        const range = this.getVisibleRange(distanceFromCamera);
        console.log('物体距离相机:', distanceFromCamera, '该深度的可视范围:', range);

        return (
            Math.abs(pos.x) <= range.width / 2 &&
            Math.abs(pos.y) <= range.height / 2 &&
            distanceFromCamera >= this.near &&
            distanceFromCamera <= this.far
        );
    }
    
    // 窗口大小改变时更新
    onWindowResize() {
        this.updateCamera();
        this.camera.updateProjectionMatrix();
    }
}

// 使用示例
// 使用示例
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x222222);

// 4. 创建渲染器:将3D场景渲染到网页上
const renderer = new THREE.WebGLRenderer({ antialias: true }); // antialias开启抗锯齿
renderer.setSize(window.innerWidth, window.innerHeight); // 设置渲染尺寸
// 将渲染器的画布添加到网页中
document.body.appendChild(renderer.domElement);

const camera = new ResponsiveCamera();
window.addEventListener('resize', () => camera.onWindowResize());

// 添加光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
scene.add(directionalLight);

// 检查物体是否可见
const cube = new THREE.Mesh(new THREE.BoxGeometry(8, 8, 8), createMaterial());
cube.position.set(20, 20, -10);
scene.add(cube);

function animate(time) {
    renderer.render(scene, camera.getCamera());
    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

console.log(camera.isObjectVisible(cube)); // true or false

小结

  1. FOV 和 distance 决定可视范围

    • FOV 越大,看到的范围越大
    • distance 越大,看到的范围越大
  2. aspect 必须与画布比例一致

    • 否则画面会被拉伸变形
  3. near 和 far 定义了深度范围

    • 在这个范围外的物体看不到

📂 核心代码与完整示例:    my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

昨天以前首页

THREE.js 摄像机

作者 龙猫不热
2026年2月6日 15:04

前置代码

<!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 来得到的

下面这个例子我们使用 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();

image.png


正交摄像机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();


image.png


❌
❌