Three.js案例
多场景切换

多场景切换

今天,我们分析下 Three.js 官网的案例,先看下效果

它的功能可以概括为:

  1. 左场景渲染实体球体
  2. 右球体渲染线框球体
  3. 拖动中间的滑块支持覆盖对方场景

在详细分析之前,我先讲解下它的大概思路

  1. 创建两个场景 sceneL,sceneR; sceneL 用于渲染实体,sceneR 用于渲染线框
  2. 通过浏览器鼠标事件,记录拖拽的鼠标位置,然后更新 sliderPos,更新后再次调用 renderer 的 setScissor 方法来更新可视范围

所以,这个案例的重点就是理解 sliderPos 的更新逻辑,以及在 animate 函数中如何利用 sliderPos 更新可视范围。

左场景、右场景渲染原理

Three.js 本身支持在同一个渲染器中多次调用 renderer.render(scene, camera),只要在每次调用之前,使用 setScissorsetViewport 等功能去限定绘制区域,就能把画布分割成若干段进行独立渲染。

先理解以下几点原理:

  1. 多场景(Scene):创建了左、右两个场景
sceneL = new THREE.Scene();
sceneL.background = new THREE.Color(0xbcd48f);
 
sceneR = new THREE.Scene();
sceneR.background = new THREE.Color(0x8fbcd4);
  1. 同一个相机(Camera):在本案例中,两个场景可以共享同一个相机,或者根据需求使用不同的相机。共享相机时,场景的视角和位置保持一致,只是渲染到画布不同比例的区域。
camera = new THREE.PerspectiveCamera(
  35,
  window.innerWidth / window.innerHeight,
  0.1,
  100
);
camera.position.z = 6;
  1. 多个 render 调用:使用 renderer.render(scene, camera) 绘制一个场景后,继续调用 renderer.render(anotherScene, camera),就能让后者叠加或覆盖前者指定的区域。
  2. 配合 Scissor Test:如果不做任何裁剪,第二次渲染会覆盖掉第一次渲染。如果先调用 renderer.setScissor(...),则只会在指定矩形区域内绘制,从而实现并排或分块显示。
function animate() {
  // 第一次渲染:绘制左场景
  renderer.setScissor(0, 0, sliderPos, window.innerHeight);
  renderer.render(sceneL, camera);
 
  // 第二次渲染:绘制右场景
  renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight);
  renderer.render(sceneR, camera);
}

在这个代码块里:

  1. 在每一帧(frame)开始时,我们通过 renderer.setScissor(0, 0, sliderPos, window.innerHeight) 设定了一个从屏幕左下角 (0,0) 开始,宽度为 sliderPos、高度为 window.innerHeight 的矩形区域。这个矩形就是左场景的可见范围。
  2. 随后,调用 renderer.render(sceneL, camera) 就只会在这个矩形区域内绘制场景 sceneL。这意味着左场景的背景、模型和光照等都将出现在画布左侧。
  3. 接下来,再设置 renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight)。相当于把绘制区域移到了右半部分(从 sliderPos 到窗口宽度的整个高度)。
  4. 通过 renderer.render(sceneR, camera) 把右场景在该矩形区域内完成渲染。因为这个渲染并不会去覆盖左侧的区域(除非矩形范围有重叠),所以最后的结果是画布左侧是场景 L,右侧是场景 R。
  5. 这种先后调用渲染,再加上矩形剪裁范围的技巧,可以让你在同一个 Canvas 中并排放置任意数量的场景视图,比如你还可以多次调用 renderer.render 并调整 setScissor,实现多段分屏或九宫格效果等。

setScissor 的作用

在上面之前,我们反复出现了 renderer.setScissor(...)。这个函数的核心就是开启并配置 WebGL 的 Scissor Test(剪刀裁剪测试)。它会限制随后的绘制操作只能在一个矩形区域内进行,矩形区域之外的画面不会被改变。下面的代码片段和更深入的说明专门解释它的工作机制与参数意义。

renderer.setScissorTest(true);
 
// 下面这两行在每次渲染某个场景前调用
renderer.setScissor(x, y, width, height);
renderer.render(scene, camera);

关于这部分,需要理解以下要点:

  1. 开启和关闭renderer.setScissorTest(true) 表示开启剪裁功能,如果改为 false,则关闭。通常在初始化渲染器时就一次性开启,然后每帧都根据需求改变剪裁范围。
  2. 坐标系:函数的参数 (x, y, width, height) 采用的是“像素坐标”,默认原点 (0, 0) 在 Canvas 左下角,x 往右递增,y 往上递增。
  3. 局部绘制:设置完剪裁区域后,所有随后的 WebGL 绘制操作只会影响这个矩形范围,其他区域依然保留上一帧的像素内容,不会被清空或覆盖。
  4. 分屏渲染:通过多次不同的 setScissor 设置,就能把一次渲染循环分成多个小区域,分别渲染不同的场景或同一场景的不同角度。
  5. 性能考虑:使用 scissor 并不会大幅降低性能;相反,有时还可以减少无用的像素填充。但是如果场景本身十分复杂,重复渲染多场景会增加 GPU 负担,需要权衡。

Scissor Test 本质是让 GPU 在光栅化阶段只处理指定矩形区域内的片元(fragment)。在这个阶段之外的所有计算还会进行,比如顶点着色、模型变换等。但在最终的像素绘制时,只有在 setScissor 界定的矩形范围内,才会更新到帧缓冲区。这样当我们把左场景绘制在左侧矩形后,再把右场景绘制在右侧矩形时,两个结果就能“并列”呈现。

场景可视范围的切换与滑块交互

最后一部分,咱们来讲解:用户如何在页面上拖动滑块来动态修改两个场景的分割线。监听滑块拖拽事件,修改全局变量 sliderPos,让每次渲染的左、右场景可见范围跟随用户操作来变化。

let sliderPos = window.innerWidth / 2;
 
function onPointerMove(e) {
  // 将sliderPos限制在 0 到 window.innerWidth 之间
  sliderPos = Math.max(0, Math.min(window.innerWidth, e.pageX));
 
  // 假设slider是一个DOM元素,通过left来移动它
  slider.style.left = sliderPos - slider.offsetWidth / 2 + "px";
}
 
function animate() {
  // 渲染左侧区域
  renderer.setScissor(0, 0, sliderPos, window.innerHeight);
  renderer.render(sceneL, camera);
 
  // 渲染右侧区域
  renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight);
  renderer.render(sceneR, camera);
}
  1. sliderPos 作为全局变量sliderPos 代表左右场景分割线在水平方向上的像素坐标。初始化时令它在窗口宽度一半的位置,也就是居中。
  2. onPointerMove(e):每当用户拖动滑块,都会触发 pointermove 事件,获取当前指针的 e.pageX 来更新 sliderPos。使用 Math.maxMath.min 是为了确保滑块不会越过浏览器窗口的边缘。
  3. slider.style.left:不仅要更新渲染时的可视范围,还要把滑块对应的 HTML 元素移动到正确的视觉位置,让用户看到分割线在页面中左右移动。
  4. animate() 中使用 sliderPos:渲染循环每一帧都会依据最新的 sliderPos 调用两次 renderer.setScissor 并渲染两场景。这意味着当用户拖动滑块时,可视范围立刻跟随更新,实现实时的场景切换效果。
  5. 可拓展性:类似思路不仅局限于滑块。比如,你可以在 GUI 面板里做一个滑动条,或者在 VR/AR 场景中根据头部位置来动态改变分割区域等。只要能在渲染时根据某种交互逻辑设置 scissor 区域,就能实现不同形式的场景切换。

案例地址

https://threejs.org/examples/#webgl_multiple_scenes_comparison (opens in a new tab)

代码地址

https://github.com/mrdoob/three.js/blob/master/examples/webgl_multiple_scenes_comparison.html (opens in a new tab)