4、渲染器Renderer:WebGLRenderer配置、抗锯齿与阴影、渲染循环(requestAnimationFrame)、性能监控(Stats.js)

渲染器,说白了就是Three.js的「画师」。你把场景、相机、灯光都摆好了,最后得靠它一笔一笔画到屏幕上。我个人习惯把渲染器配置放在项目的最前面,因为很多坑其实在初始化阶段就能避开。

4.1 WebGLRenderer基础配置

先看一个最基础的初始化代码:

const renderer = new THREE.WebGLRenderer({
  antialias: true,      // 抗锯齿
  alpha: false,         // 是否透明背景
  precision: 'highp',   // 精度:highp | mediump | lowp
  powerPreference: 'high-performance' // 优先使用独立显卡
});

renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

这里有几个关键点,我一个个说。

antialias(抗锯齿):说白了就是让物体边缘不那么「狗啃」。我在项目中遇到过,有些低端手机开了抗锯齿反而卡顿,因为它是通过采样实现的。如果你做移动端项目,建议先关掉,看看效果能不能接受。

setPixelRatio:这个我踩过坑。以前我直接写 window.devicePixelRatio,结果在Retina屏上帧率直接掉到30。后来我习惯限制最大值为2,视觉上差别不大,性能却好很多。

注意:setPixelRatio 不要超过2。超过2对画质提升微乎其微,但性能开销翻倍。我曾经在4K屏上试过3倍,直接卡成PPT。

4.2 抗锯齿与阴影的取舍

抗锯齿和阴影,是渲染器里最吃性能的两个功能。你想想看,它们本质上都是在做「额外计算」。

阴影配置

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 柔和阴影

阴影类型有几种:

类型 性能 效果 适用场景
BasicShadowMap 最快 锯齿明显 低端设备、调试
PCFShadowMap 中等 边缘柔和 大多数场景
PCFSoftShadowMap 较慢 非常柔和 高质量场景
VSMShadowMap 中等 无锯齿但可能漏光 特定需求

我个人习惯:开发阶段用PCFSoftShadowMap看效果,上线前根据设备降级。嗯,这里要注意——不是所有光源都能投射阴影。只有DirectionalLight、SpotLight、PointLight支持,AmbientLight不行。

小技巧:如果你发现阴影边缘闪烁,通常是shadow map分辨率不够。可以试试:light.shadow.mapSize.width = 2048。但别设太高,1024到2048之间比较稳妥。

4.3 渲染循环:requestAnimationFrame

为什么不用setInterval?因为requestAnimationFrame是浏览器专门为动画设计的。它会等浏览器准备好再执行,不会浪费CPU。而且页面切到后台时会自动暂停,省电又省资源。

标准写法:

function animate() {
  requestAnimationFrame(animate);

  // 更新逻辑
  mesh.rotation.x += 0.01;
  controls.update();

  // 渲染
  renderer.render(scene, camera);
}

animate();

这里有个细节:requestAnimationFrame(animate) 要写在函数最前面。为什么?因为如果后面代码报错,至少下一帧还能继续跑。我曾经把这句话写在最后,结果某次更新逻辑抛异常,整个动画就停了,排查了半天。

另外,如果你需要控制帧率,可以加个时间戳判断:

let lastTime = 0;
const targetFPS = 30;
const interval = 1000 / targetFPS;

function animate(time) {
  requestAnimationFrame(animate);

  const delta = time - lastTime;
  if (delta < interval) return;

  lastTime = time - (delta % interval);
  // 更新和渲染
}

这个技巧我在做AR项目时用过。有些手机跑60帧发热严重,降到30帧用户根本感觉不到,但手机温度降了一大截。

4.4 性能监控:Stats.js

Stats.js是Mr.doob写的一个小工具,就一个功能——显示FPS。但你别小看它,我每次做性能优化都靠它。

用法很简单:

import Stats from 'three/examples/jsm/libs/stats.module.js';

const stats = new Stats();
stats.dom.style.left = '80px'; // 避免和Three.js默认的左上角重叠
document.body.appendChild(stats.dom);

function animate() {
  stats.begin();
  // ... 你的渲染逻辑
  stats.end();
  requestAnimationFrame(animate);
}

Stats显示三个指标:

  • FPS:帧率,稳定60说明流畅,低于30就要优化了
  • MS:每帧渲染耗时,单位毫秒。16ms以内是60帧,33ms以内是30帧
  • MB:内存占用(部分浏览器支持)
我的经验:别只看FPS。有时候FPS掉到40,但MS只有20ms,说明瓶颈不在渲染,可能在CPU计算上。这时候就该检查你的更新逻辑了。

我曾经接手过一个项目,FPS只有20多。用Stats一看,MS高达45ms。再细查,发现场景里有个模型面数超过100万。嗯,这就是典型的「模型太精细,渲染扛不住」。后来用Decimate减面到10万,FPS直接回到55。

4.5 综合配置示例

最后,给一个我常用的生产环境配置模板:

const renderer = new THREE.WebGLRenderer({
  antialias: true,
  powerPreference: 'high-performance'
});

renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;

// 性能监控
const stats = new Stats();
document.body.appendChild(stats.dom);

// 渲染循环
function animate() {
  stats.begin();
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  stats.end();
}
animate();

// 窗口自适应
window.addEventListener('resize', () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
});

这个配置我用了两年多,在几十个项目里验证过。你直接拿去用,基本不会出大问题。当然,具体项目还得微调——比如做建筑可视化,阴影质量可以再高点;做移动端游戏,抗锯齿和阴影可能都得关掉。

记住一句话:渲染器配置没有银弹。理解每个参数的含义,根据你的目标设备做取舍,这才是正解。