多场景切换
今天,我们分析下 Three.js 官网的案例,先看下效果
它的功能可以概括为:
- 左场景渲染实体球体
- 右球体渲染线框球体
- 拖动中间的滑块支持覆盖对方场景
在详细分析之前,我先讲解下它的大概思路
- 创建两个场景 sceneL,sceneR; sceneL 用于渲染实体,sceneR 用于渲染线框
- 通过浏览器鼠标事件,记录拖拽的鼠标位置,然后更新 sliderPos,更新后再次调用 renderer 的 setScissor 方法来更新可视范围
所以,这个案例的重点就是理解 sliderPos 的更新逻辑,以及在 animate 函数中如何利用 sliderPos 更新可视范围。
左场景、右场景渲染原理
Three.js 本身支持在同一个渲染器中多次调用 renderer.render(scene, camera)
,只要在每次调用之前,使用 setScissor
或 setViewport
等功能去限定绘制区域,就能把画布分割成若干段进行独立渲染。
先理解以下几点原理:
- 多场景(Scene):创建了左、右两个场景
sceneL = new THREE.Scene();
sceneL.background = new THREE.Color(0xbcd48f);
sceneR = new THREE.Scene();
sceneR.background = new THREE.Color(0x8fbcd4);
- 同一个相机(Camera):在本案例中,两个场景可以共享同一个相机,或者根据需求使用不同的相机。共享相机时,场景的视角和位置保持一致,只是渲染到画布不同比例的区域。
camera = new THREE.PerspectiveCamera(
35,
window.innerWidth / window.innerHeight,
0.1,
100
);
camera.position.z = 6;
- 多个
render
调用:使用renderer.render(scene, camera)
绘制一个场景后,继续调用renderer.render(anotherScene, camera)
,就能让后者叠加或覆盖前者指定的区域。 - 配合 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);
}
在这个代码块里:
- 在每一帧(frame)开始时,我们通过
renderer.setScissor(0, 0, sliderPos, window.innerHeight)
设定了一个从屏幕左下角 (0,0) 开始,宽度为sliderPos
、高度为window.innerHeight
的矩形区域。这个矩形就是左场景的可见范围。 - 随后,调用
renderer.render(sceneL, camera)
就只会在这个矩形区域内绘制场景sceneL
。这意味着左场景的背景、模型和光照等都将出现在画布左侧。 - 接下来,再设置
renderer.setScissor(sliderPos, 0, window.innerWidth, window.innerHeight)
。相当于把绘制区域移到了右半部分(从sliderPos
到窗口宽度的整个高度)。 - 通过
renderer.render(sceneR, camera)
把右场景在该矩形区域内完成渲染。因为这个渲染并不会去覆盖左侧的区域(除非矩形范围有重叠),所以最后的结果是画布左侧是场景 L,右侧是场景 R。 - 这种先后调用渲染,再加上矩形剪裁范围的技巧,可以让你在同一个 Canvas 中并排放置任意数量的场景视图,比如你还可以多次调用
renderer.render
并调整setScissor
,实现多段分屏或九宫格效果等。
setScissor
的作用
在上面之前,我们反复出现了 renderer.setScissor(...)
。这个函数的核心就是开启并配置 WebGL 的 Scissor Test(剪刀裁剪测试)。它会限制随后的绘制操作只能在一个矩形区域内进行,矩形区域之外的画面不会被改变。下面的代码片段和更深入的说明专门解释它的工作机制与参数意义。
renderer.setScissorTest(true);
// 下面这两行在每次渲染某个场景前调用
renderer.setScissor(x, y, width, height);
renderer.render(scene, camera);
关于这部分,需要理解以下要点:
- 开启和关闭:
renderer.setScissorTest(true)
表示开启剪裁功能,如果改为false
,则关闭。通常在初始化渲染器时就一次性开启,然后每帧都根据需求改变剪裁范围。 - 坐标系:函数的参数
(x, y, width, height)
采用的是“像素坐标”,默认原点(0, 0)
在 Canvas 左下角,x
往右递增,y
往上递增。 - 局部绘制:设置完剪裁区域后,所有随后的 WebGL 绘制操作只会影响这个矩形范围,其他区域依然保留上一帧的像素内容,不会被清空或覆盖。
- 分屏渲染:通过多次不同的
setScissor
设置,就能把一次渲染循环分成多个小区域,分别渲染不同的场景或同一场景的不同角度。 - 性能考虑:使用 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);
}
sliderPos
作为全局变量:sliderPos
代表左右场景分割线在水平方向上的像素坐标。初始化时令它在窗口宽度一半的位置,也就是居中。onPointerMove(e)
:每当用户拖动滑块,都会触发 pointermove 事件,获取当前指针的e.pageX
来更新sliderPos
。使用Math.max
和Math.min
是为了确保滑块不会越过浏览器窗口的边缘。slider.style.left
:不仅要更新渲染时的可视范围,还要把滑块对应的 HTML 元素移动到正确的视觉位置,让用户看到分割线在页面中左右移动。- 在
animate()
中使用sliderPos
:渲染循环每一帧都会依据最新的sliderPos
调用两次renderer.setScissor
并渲染两场景。这意味着当用户拖动滑块时,可视范围立刻跟随更新,实现实时的场景切换效果。 - 可拓展性:类似思路不仅局限于滑块。比如,你可以在 GUI 面板里做一个滑动条,或者在 VR/AR 场景中根据头部位置来动态改变分割区域等。只要能在渲染时根据某种交互逻辑设置 scissor 区域,就能实现不同形式的场景切换。
案例地址
https://threejs.org/examples/#webgl_multiple_scenes_comparison (opens in a new tab)