Three.js后期处理与物理模拟

554 阅读10分钟

目录


后期处理

在Three.js中实现后期处理效果,如Bloom(辉光效果)、SSAO(屏幕空间环境光遮蔽)和Depth of Field(景深效果),通常涉及使用THREE.EffectComposer、相关的Pass(通道)以及WebGL渲染器的后处理管道。

初始化Effect Composer

首先,你需要初始化一个THREE.EffectComposer实例,它将作为所有后处理效果的容器,并与你的场景和渲染器关联起来。

import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';

// 初始化场景、相机、渲染器等...
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();

// 创建Effect Composer
const composer = new EffectComposer(renderer);

添加Render Pass

RenderPass用于渲染基础场景到一个纹理上,这是后续后处理的基础。

const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

Bloom 效果

Bloom效果可以增加场景中明亮区域的光辉感。

import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  strength, // 辉光强度
  radius,   // 辉光模糊半径
  threshold  // 亮度阈值,低于此值的像素不参与辉光计算
);
composer.addPass(bloomPass);

SSAO 效果

SSAO用于增强场景中的阴影和深度感,模拟间接光照效果。

import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';

const ssaoPass = new SSAOPass(scene, camera);
ssaoPass.renderToScreen = true; // 直接输出到屏幕
composer.addPass(ssaoPass);

Depth of Field 效果

Depth of Field模仿真实相机的景深效果,使场景中部分区域模糊以突出焦点。

import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';

const bokehPass = new BokehPass(scene, camera, {
  focusDistance, // 焦点距离
  aperture,      // 光圈大小,影响模糊程度
  maxBlur,       // 最大模糊量
});
bokehPass.renderToScreen = true;
composer.addPass(bokehPass);

综合使用

在每帧渲染时,你需要调用composer.render()而不是直接使用renderer.render()来应用所有的后处理效果。

function animate() {
  requestAnimationFrame(animate);

  // 更新场景中的对象...

  // 渲染后处理效果
  composer.render();
}

animate();

请注意,上述代码示例中涉及的strengthradiusthresholdfocusDistanceaperturemaxBlur等参数需要根据具体需求进行调整。同时,确保已经正确导入了所需的后期处理库文件。

结合使用和优化

在实际项目中,你可能会结合多种效果。例如,你可能希望Bloom和SSAO一起使用,但不使用Depth of Field。在这种情况下,你可以调整Pass的顺序,因为Pass通常是自下而上依次执行的。通常,更复杂的效果(如Bloom)应该放在简单效果(如SSAO)之后,因为它们可能会修改更基本的图像信息。

// 添加Bloom Pass
composer.addPass(bloomPass);

// 添加SSAO Pass
composer.addPass(ssaoPass);

在这种情况下,SSAO首先应用,然后是Bloom效果。如果bloomPass在ssaoPass之前,那么SSAO的效果可能会被Bloom Pass覆盖或减弱。

调整和优化性能

由于后处理效果会增加渲染的复杂性,可能会对性能产生影响。以下是一些优化策略:

  • 降低分辨率:通过设置EffectComposer的renderTarget的分辨率小于实际屏幕分辨率,可以减少计算量。
  • 调整效果参数:根据设备性能,适当调整每个效果的强度、模糊半径等参数。
  • 使用Pass的enabled属性:在不需要某些效果时,可以临时禁用它们以提高性能。
  • 避免不必要的渲染:确保只有在场景或相机发生变化时才重新渲染,而不是每帧都渲染。

着色器

在Three.js中,着色器是使用GLSL(OpenGL Shading Language)编写的,这是一种编程语言,允许开发者在GPU级别定制图形渲染。Three.js将GLSL代码包装在JavaScript对象中,以便更容易地在Web应用程序中使用。

着色器类型

顶点着色器 (Vertex Shader): 定义了顶点如何在3D空间中移动和变形。它接收输入(attribute和uniform),处理它们,并输出新的顶点位置(gl_Position)和法线(gl_Normal)。

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

片段着色器 (Fragment Shader):

定义了每个像素的颜色。它接收顶点着色器的输出和其他数据,并输出最终的颜色值(gl_FragColor)。

void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 输出红色像素
}

ShaderMaterial 和 Uniforms

在Three.js中,你可以创建自定义的ShaderMaterial,将自定义的顶点和片段着色器代码传递给它。Uniforms是着色器中可从JavaScript动态改变的值。

const uniforms = {
    uTime: { value: 0.0 },
    uResolution: { value: new THREE.Vector2() },
};

const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader: document.getElementById('vertexShader').textContent,
    fragmentShader: document.getElementById('fragmentShader').textContent,
});

ShaderLib 和 ShaderChunk

Three.js维护了一个名为ShaderLib的对象,其中包含预定义的着色器实现,比如MeshStandardMaterial使用的物理渲染着色器。这些着色器代码通常被拆分成多个小块,存储在ShaderChunk中,便于重用和组合。

例如,MeshPhysicalMaterial的顶点着色器代码可能包含如下部分:

#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <color_pars_vertex>
#include <displacementmap_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

void main() {
    // ... 实现 ...
}

#include指令在编译时被替换为ShaderChunk中对应代码块的内容。

物理渲染 (MeshStandardMaterial)

MeshStandardMaterial使用了基于物理的渲染(PBR),它考虑了表面的反射、漫射、金属度、粗糙度等因素。其着色器代码通常比较复杂,涉及到环境光、镜面反射、菲涅尔效应等计算。

uniform vec3 diffuse;
uniform float opacity;

#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <envmap_pars_fragment>
#include <cube_uv_reflection_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_physical_pars_fragment>
#include <shadowmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

void main() {
    // ... 实现 ...
}

请注意,为了正确使用MeshStandardMaterial,你需要提供合适的纹理(如颜色、金属度、粗糙度贴图)和光源设置。

实例化

在处理大量相似对象时,使用实例化可以减少Draw Call,提高性能。实例化通常涉及在GPU上共享顶点数据,然后用不同的属性(如位置、颜色、纹理坐标)来区分每个实例。

// 创建实例化几何体
const geometry = new THREE.InstancedBufferGeometry().copy(originalGeometry);

// 添加实例数据
geometry.instanceCount = count;
geometry.addAttribute('instanceColor', new THREE.InstancedBufferAttribute(new Float32Array(count * 3), 3));

// 分配实例属性
const material = new THREE.MeshStandardMaterial({ color: 0x4080ff });
material.vertexColors = true;

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

带有自定义着色器的粒子系统

在Three.js中,你可以创建粒子系统并使用自定义着色器来控制粒子的外观和行为。这通常涉及到Points几何体和ShaderMaterial的组合。

// 创建粒子几何体
const geometry = new THREE.Geometry();
for (let i = 0; i < numParticles; i++) {
    const particle = new THREE.Vector3(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1).normalize();
    geometry.vertices.push(particle);
}

// 自定义着色器
const uniforms = {
    time: { value: 0.0 },
    sizeAttenuation: { value: 1.0 },
    particleColor: { value: new THREE.Color(0x4080ff) },
};

const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader: document.getElementById('particleVertexShader').textContent,
    fragmentShader: document.getElementById('particleFragmentShader').textContent,
    blending: THREE.AdditiveBlending,
    transparent: true,
});

// 创建粒子对象
const particles = new THREE.Points(geometry, material);
scene.add(particles);

在自定义着色器中,你可能需要处理粒子的位置、大小、颜色等属性,以及时间变化的影响。

// 顶点着色器
uniform float time;
attribute vec3 position;

void main() {
    vec3 newPosition = position + vec3(0.0, sin(time), 0.0);
    gl_PointSize = 5.0; // 可以从uniforms获取
    gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}

// 片段着色器
uniform vec3 particleColor;

void main() {
    gl_FragColor = vec4(particleColor, 1.0);
}

高级着色器技术

  • 混合模式 (Blending): 可以通过调整blending属性和blendEquation、blendSrc、blendDst等属性来实现不同类型的混合效果。
  • 深度测试 (Depth Testing): 通过depthTest属性控制是否进行深度测试,以决定哪个像素应该覆盖另一个。
  • 剪裁平面 (Clipping Planes): 使用THREE.ClipPlanes和clipIntersection、clipShadows等属性可以实现基于剪裁平面的局部遮罩效果。
  • 阴影 (Shadows): 通过ShadowMaterial或在自定义着色器中处理阴影映射,可以实现物体的阴影效果。

优化

  • 使用缓冲区几何体 (BufferGeometry): 对于大型几何体,使用缓冲区几何体可以提高性能,因为它减少了内存使用和数据传输。
  • 分批渲染 (Batching): 尽可能地合并相似的几何体和材质,减少Draw Call数量。
  • 优化着色器 (Optimize Shaders): 减少着色器中的计算量,使用高效的算法,避免不必要的纹理采样和数学运算。

在Three.js中添加物理模拟通常需要引入第三方物理引擎,如Cannon.js或Ammo.js。这些引擎提供了物理世界的基本组件,如刚体、约束、碰撞检测等。

Cannon.js物理引擎

首先,确保你已经安装了Cannon.js库,可以通过npm安装:

npm install cannon

然后,在你的Three.js场景中引入Cannon.js和相应的Three.js插件:

import * as THREE from 'three';
import * as CANNON from 'cannon';
import { CannonJSPlugin } from 'three-cannon-plugin'; // 如果使用的话

接着,创建物理世界、刚体和碰撞器:

// 创建物理世界
const world = new CANNON.World();
world.gravity.set(0, -9.81, 0); // 设置重力

// 创建刚体
const sphereBody = new CANNON.Body({ mass: 1 }); // 刚体质量
const sphereShape = new CANNON.Sphere(1); // 球体半径
sphereBody.addShape(sphereShape);

// 创建球体几何体和材质
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 创建球体对象
const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);

// 将Three.js对象与Cannon.js对象关联
const cannonMesh = new CannonJSPlugin(world, sphereMesh);
cannonMesh.body = sphereBody;

接下来,设置碰撞检测和更新物理世界:

// 创建地面
const groundBody = new CANNON.Body({ mass: 0 });
const groundShape = new CANNON.Plane(); // 平面形状
groundBody.addShape(groundShape);
world.addBody(groundBody);

// 每帧更新物理世界
function animate() {
  requestAnimationFrame(animate);

  world.step(1 / 60); // 每帧步进

  cannonMesh.update(); // 将Cannon.js的更新应用于Three.js对象

  renderer.render(scene, camera);
}

animate();

实际项目中可能需要处理更多细节,如碰撞检测回调、约束、动力学等。Cannon.js和Ammo.js都有丰富的API,可以用来创建复杂的物理交互。在使用物理引擎时,务必查阅官方文档以获取完整的功能和示例。

Ammo.js物理引擎

在使用Ammo.js进行物理模拟时,你需要先加载WebAssembly模块,然后初始化物理世界、刚体和碰撞器。

首先,确保你已经安装了ammo.js库,可以通过npm安装:

npm install ammo.js

然后,引入Ammo.js库和Three.js插件:

import * as THREE from 'three';
import * as ammo from 'ammo.js/dist/ammo.wasm.js';
import { AmmoJSPlugin } from 'three-ammojs-plugin'; // 如果使用的话

接下来,加载WebAssembly模块并初始化Ammo.js:

// 加载WebAssembly模块
let Ammo;
ammo().then((module) => {
  Ammo = module.Ammo;
  
  // 初始化物理世界
  const world = new Ammo.btDefaultCollisionConfiguration();
  const dispatcher = new Ammo.btCollisionDispatcher(world);
  const overlappingPairCache = new Ammo.btDbvtBroadphase();
  const solver = new Ammo.btSequentialImpulseConstraintSolver();
  const dynamicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, world);
  dynamicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0)); // 设置重力

  // 开始物理模拟
  startPhysics(dynamicsWorld);
});

创建刚体和碰撞器:

function createRigidBody(shape, transform) {
  const body = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(1, null, shape));
  body.setWorldTransform(transform);
  dynamicsWorld.addRigidBody(body);
  return body;
}

// 创建球体
const sphereShape = new Ammo.btSphereShape(1);
const sphereTransform = new Ammo.btTransform();
sphereTransform.setIdentity();
sphereTransform.setOrigin(new Ammo.btVector3(0, 1, 0));
const sphereBody = createRigidBody(sphereShape, sphereTransform);

// 创建地面
const planeShape = new Ammo.btStaticPlaneShape(new Ammo.btVector3(0, 1, 0), 1);
const planeTransform = new Ammo.btTransform();
planeTransform.setIdentity();
planeTransform.setOrigin(new Ammo.btVector3(0, -1, 0));
const planeBody = createRigidBody(planeShape, planeTransform);

创建Three.js对象并与Ammo.js对象关联:

// 创建球体几何体和材质
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

// 创建球体对象
const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphereMesh);

// 将Three.js对象与Ammo.js对象关联
const ammoMesh = new AmmoJSPlugin(dynamicsWorld, sphereMesh);
ammoMesh.body = sphereBody;

最后,每帧更新物理世界:

function startPhysics(world) {
  function step() {
    requestAnimationFrame(step);

    world.stepSimulation(1 / 60, 10, 1 / 60);

    ammoMesh.update();

    renderer.render(scene, camera);
  }

  step();
}

Ammo.js的API与Cannon.js有所不同,但概念相似。在实际项目中,你可能需要处理更多的细节,如碰撞回调、约束、关节等。Ammo.js提供了一个更强大的物理模拟引擎,但也需要更多的学习和调试。

在使用Ammo.js进行更复杂的物理模拟时,你可能会遇到以下情况:

碰撞检测和响应:

Ammo.js提供了btCollisionWorldrayTest()convexSweepTest()contactTest()等方法来进行碰撞检测。你可以设置碰撞回调函数来处理特定的碰撞事件。

// 设置碰撞回调
world.setCollisionWorldOverlappingPairCallback(new Ammo.btOverlapCallback() {
  processOverlap(pairCacheProxy0, pairCacheProxy1) {
    // 处理碰撞
  }
});

动力学约束:

如连接两个物体的铰链、滑轮或绳索。Ammo.js提供了各种类型的约束,如btHingeConstraintbtSliderConstraint等。

// 创建铰链约束
const hingeConstraint = new Ammo.btHingeConstraint(bodyA, bodyB, pivotInA, pivotInB, axisInA, axisInB);
hingeConstraint.enableAngularMotor(true, 0, 1);
world.addConstraint(hingeConstraint);

刚体类型:

除了静态和动态刚体外,还可以创建kinematic刚体,它们不受到力的影响,但可以通过直接设置其位置和速度来移动。

const kinematicBody = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(0, null, shape));
kinematicBody.setActivationState(Ammo.btCollisionObject.ACTIVE_TAG);
kinematicBody.setCollisionFlags(kinematicBody.getCollisionFlags() | Ammo.btCollisionObject.CF_KINEMATIC_OBJECT);
world.addRigidBody(kinematicBody);

动画和控制器:

有时你可能需要控制物体的运动,例如创建一个跟随鼠标或键盘的物体。为此,你可以直接操作刚体的位置和速度,或者创建一个控制器。

function updateBodyPosition(body, position) {
  const trans = body.getWorldTransform();
  trans.setOrigin(position);
  body.setWorldTransform(trans);
}

接触回调:

你可以设置接触回调来处理物体间的接触,例如检测物体是否落地。

world.setContactAddedCallback((dispatcher, proxy0, proxy1) => {
  const body0 = Ammo.castObject(proxy0.getBody(), Ammo.btRigidBody);
  const body1 = Ammo.castObject(proxy1.getBody(), Ammo.btRigidBody);

  if (body0.getUserPointer() === 'myObject' && body1.getUserPointer() === 'ground') {
    // 地面与物体接触
  }
});

性能优化:

根据场景需求,可能需要优化碰撞检测的精度、减少不必要的刚体或减少模拟的帧率。

Web Workers:

为了减轻主线程的负担,你可以考虑在Web Workers中运行Ammo.js的物理模拟。这需要额外的同步机制,如使用postMessage和onmessage事件。