VFX Engine
到目前为止,我们已经创建了自定义组件来在我们的3D场景中创建粒子。通常情况下,我们想要做几乎相同的事情:从空间中的一个点发射粒子并随时间对它们进行动画处理。(颜色、大小、位置等)
与其一遍又一遍地重复相同的代码,我们可以创建一个相对通用的 VFX 引擎,可用于创建不同类型的粒子效果。
它具有许多优点:
- 可重用性:您可以使用相同的引擎在项目中创建不同类型的粒子效果。
- 性能:该引擎可以优化以高效处理大量粒子,并将多个粒子系统合并为一个。
- 灵活性:您可以通过更改引擎的参数轻松自定义粒子的行为。
- 易用性:您只需几行代码即可创建复杂的粒子效果。
- 避免代码重复:您不必多次编写相同的代码。
我们将在接下来的课程中使用此 VFX 引擎来创建各种效果。虽然您可以跳过此课程并直接使用引擎,但了解它的工作原理将帮助您更深入地掌握如何在3D项目中实现性能和灵活性。
准备好构建您的 VFX 引擎了吗?让我们开始吧!
GPU Particles
我们在前面的课程中看到了如何使用 drei 的 <Instances />
组件来在我们的3D场景中创建受控粒子。
但这种方法有一个主要限制:我们可以处理的粒子数量受到 CPU 的限制。粒子越多,CPU 就要越多地处理它们,这可能导致性能问题。
这是因为在底层,<Instances />
组件会在其 useFrame
循环中计算每个 <Instance />
的位置、颜色和大小。代码可以在这里查看。
对于我们的 VFX Engine,我们希望能够生成比 <Instances />
组件可以处理的更多粒子。我们将使用 GPU 来处理粒子的颜色、位置和大小。这样我们就能够处理数以十万计的粒子(甚至上百万?👀)而不会有任何性能问题。
实例化 Mesh
虽然我们可以使用 Sprite 或 Points 来创建粒子,但我们将使用 InstancedMesh。
它不仅允许我们渲染简单的形状如点或精灵,还可以渲染像立方体、球体和自定义几何体这样的3D形状。
让我们在一个新的 vfxs
文件夹中创建一个名为 VFXParticles.jsx
的组件:
import { useMemo, useRef } from "react"; import { PlaneGeometry } from "three"; export const VFXParticles = ({ settings = {} }) => { const { nbParticles = 1000 } = settings; const mesh = useRef(); const defaultGeometry = useMemo(() => new PlaneGeometry(0.5, 0.5), []); return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <meshBasicMaterial color="orange" /> </instancedMesh> </> ); };
我们创建了每个粒子的 geometry。在这种情况下,我们使用了一个简单的平面几何,其大小在两个轴上为 0.5
。稍后我们将添加一个 prop 来传递我们想要的任何几何体。
instancedMesh
组件接受三个参数:
- 粒子的 geometry。
- 粒子的 material。我们传递了
null
以在组件内部声明性地定义它。 - 组件能够处理的 instances 的数量。对于我们来说,它表示可以同时显示的粒子的 最大数量。
让我们在 Experience.jsx
文件中用 VFXParticles
组件替换橙色立方体:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles /> </> ); };
你可以看到场景中间有一个橙色粒子。这是我们的 VFXParticles
组件。
我们的粒子数量设置为 1000
,但我们只能看到一个。这是因为所有粒子都被渲染在相同的位置信息 (0, 0, 0)
。让我们改变这一点。
实例矩阵
实例化的 mesh 使用一个 matrix 来定义每个实例的位置、旋转和缩放。通过更新我们 mesh 的 instanceMatrix 属性,我们可以单独移动、旋转和缩放每个粒子。
对于每个实例,矩阵是一个 4x4 的矩阵,用于表示粒子的变换。Three.js 的 Matrix4 类允许我们 compose
和 decompose
矩阵,以更便于理解的方式设置/获取粒子的位置、旋转和缩放。
在 VFXParticles
声明的顶部,让我们声明一些临时变量,以便在不频繁重新创建 Vectors 和 Matrices 的情况下操纵粒子:
// ... import { Euler, Matrix4, PlaneGeometry, Quaternion, Vector3 } from "three"; const tmpPosition = new Vector3(); const tmpRotationEuler = new Euler(); const tmpRotation = new Quaternion(); const tmpScale = new Vector3(1, 1, 1); const tmpMatrix = new Matrix4();
现在让我们创建一个 emit
函数来设置我们的粒子:
// ... import { useEffect } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const emit = (count) => { for (let i = 0; i < count; i++) { const position = [ randFloatSpread(5), randFloatSpread(5), randFloatSpread(5), ]; const scale = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); } }; useEffect(() => { emit(nbParticles); }, []); // ... };
emit
函数遍历我们想要发射的粒子数量,并为每个粒子设置随机的位置、旋转和缩放。然后,我们使用这些值组合矩阵,并将其设置到当前索引的实例。
你可以看到场景中的随机粒子。每个粒子都有随机的位置、旋转和缩放。
为了动画化我们的粒子,我们将定义如 lifetime、speed、direction 之类的属性,以便在 GPU 上进行计算。
在此之前,我们需要切换到一个自定义的 shader material 来处理这些属性,因为我们无法访问和控制 meshBasicMaterial
的属性。
粒子材质
我们的第一个目标是确保在 meshBasicMaterial
和新的 shaderMaterial
之间看不到任何变化。我们将创建一个简单的 shader material,使其与当前的 meshBasicMaterial
渲染粒子的方式相同。
在 VFXParticles
组件中,让我们创建一个新的 shader material:
// ... import { shaderMaterial } from "@react-three/drei"; import { extend } from "@react-three/fiber"; import { Color } from "three"; const ParticlesMaterial = shaderMaterial( { color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; } `, /* glsl */ ` uniform vec3 color; varying vec2 vUv; void main() { gl_FragColor = vec4(color, 1.0); }` ); extend({ ParticlesMaterial });
这是一个非常简单的 shader material,它接受一个 color
uniform,并以此颜色渲染粒子。这里唯一的新东西是 instanceMatrix
,我们用它来获取每个粒子的 position
、rotation
和 scale
。
请注意,我们不需要声明
instanceMatrix
属性,因为这是使用实例化时WebGLProgram
的内置属性之一。更多信息可以在这里找到。
让我们用新的 ParticlesMaterial
替换 meshBasicMaterial
:
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> </instancedMesh>
完美!我们的 position
、rotation
和 scale
仍然正常工作。粒子以略微不同的橙色渲染。 这是因为我们在 shader material 中没有考虑环境。为了简化,我们会保持这样。
我们现在准备为粒子添加自定义属性以使它们动画化。
实例化缓冲区属性
目前,我们只使用了 instanceMatrix
属性,现在我们将添加自定义属性,以便对每个粒子进行更精细的控制。
为此,我们将使用 Three.js 中的 InstancedBufferAttribute。
我们将为粒子添加以下属性:
instanceColor
: 表示粒子颜色的vector3。instanceColorEnd
: 表示粒子将随时间变换成的颜色的vector3。instanceDirection
: 表示粒子移动方向的vector3。instanceSpeed
: 定义粒子在其方向上移动速度的float。instanceRotationSpeed
: 确定粒子每个轴旋转速度的vector3。instanceLifetime
: 定义粒子生命周期的vector2。第一个值 (x
) 是开始时间,第二个值 (y
) 是生命周期/持续时间。结合一个时间 uniform,我们可以计算出粒子的年龄、进度,以及其是活着还是死去。
让我们为我们的属性创建不同的缓冲区:
// ... import { useState } from "react"; // ... export const VFXParticles = ({ settings = {} }) => { // ... const [attributeArrays] = useState({ instanceColor: new Float32Array(nbParticles * 3), instanceColorEnd: new Float32Array(nbParticles * 3), instanceDirection: new Float32Array(nbParticles * 3), instanceLifetime: new Float32Array(nbParticles * 2), instanceSpeed: new Float32Array(nbParticles * 1), instanceRotationSpeed: new Float32Array(nbParticles * 3), }); // ... }; // ...
我使用 useState
来创建我们的属性缓冲区,以避免在每次渲染时重新创建它们。我选择不使用 useMemo
方法,因为在组件生命周期内更改粒子的最大数量不是我们想要处理的。
Float32Array
用于存储属性的值。我们将粒子数量乘以属性的组件数量,以获得数组中的值总数。
在 instanceColor
属性中,前 3 个值代表第一个粒子的颜色,下 3 个值代表第二个粒子的颜色,以此类推。
让我们首先熟悉一下 InstancedBufferAttribute
以及如何使用它。我们将实现 instanceColor
属性:
// ... import { DynamicDrawUsage } from "three"; export const VFXParticles = ({ settings = {} }) => { // ... return ( <> <instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh> </> ); };
在我们的 <instancedMesh />
中,我们添加了一个 <instancedBufferAttribute />
组件以定义 instanceColor
属性。我们将其附加到 mesh 的 geometry-attributes-instanceColor
属性。我们将 attributeArrays.instanceColor
数组作为数据源,设置 itemSize
为 3
,因为我们有一个 vector3,将 count
设置为 nbParticles
。
usage
属性设置为 DynamicDrawUsage
,以告诉渲染器数据将被频繁更新。其他可能的值和更多详细信息可以在这里找到。
我们不会在每帧更新它们,而是在每次发射新粒子时更新数据。足以将其视为 DynamicDrawUsage
。
很好,让我们在文件顶部创建一个虚拟的 tmpColor
变量来操作粒子的颜色:
// ... const tmpColor = new Color();
现在,让我们更新 emit
函数以设置 instanceColor
属性:
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); } };
我们首先从 mesh 的几何体中获取 instanceColor
属性。然后在我们想要发射的粒子数量上进行循环,为每个粒子设置一个随机颜色。
让我们更新 particlesMaterial
以使用 instanceColor
属性代替 color uniform:
const ParticlesMaterial = shaderMaterial( { // color: new Color("white"), }, /* glsl */ ` varying vec2 vUv; varying vec3 vColor; attribute vec3 instanceColor; void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; } `, /* glsl */ ` varying vec3 vColor; varying vec2 vUv; void main() { gl_FragColor = vec4(vColor, 1.0); }` ); // ...
我们在顶点着色器中添加了 attribute vec3 instanceColor;
,并设置了 vColor
varying 以将颜色传递给片段着色器。然后,我们将 gl_FragColor
设置为 vColor
以用其颜色渲染粒子。
我们已成功为每个粒子设置了随机颜色。粒子已用其颜色渲染。
很好,让我们为粒子添加其他属性。首先,更新我们的 emit
函数,以用随机值设置 instanceColorEnd
、instanceDirection
、instanceLifetime
、instanceSpeed
和 instanceRotationSpeed
属性:
const emit = (count) => { const instanceColor = mesh.current.geometry.getAttribute("instanceColor"); const instanceColorEnd = mesh.current.geometry.getAttribute("instanceColorEnd"); const instanceDirection = mesh.current.geometry.getAttribute("instanceDirection"); const instanceLifetime = mesh.current.geometry.getAttribute("instanceLifetime"); const instanceSpeed = mesh.current.geometry.getAttribute("instanceSpeed"); const instanceRotationSpeed = mesh.current.geometry.getAttribute("instanceRotationSpeed"); for (let i = 0; i < count; i++) { // ... tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(Math.random(), Math.random(), Math.random()); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(5, 20); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); } };
并为每个属性创建 instancedBufferAttribute
组件:
<instancedMesh args={[defaultGeometry, null, nbParticles]} ref={mesh}> <particlesMaterial color="orange" /> <instancedBufferAttribute attach={"geometry-attributes-instanceColor"} args={[attributeArrays.instanceColor]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceColorEnd"} args={[attributeArrays.instanceColorEnd]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceDirection"} args={[attributeArrays.instanceDirection]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceLifetime"} args={[attributeArrays.instanceLifetime]} itemSize={2} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceSpeed"} args={[attributeArrays.instanceSpeed]} itemSize={1} count={nbParticles} usage={DynamicDrawUsage} /> <instancedBufferAttribute attach={"geometry-attributes-instanceRotationSpeed"} args={[attributeArrays.instanceRotationSpeed]} itemSize={3} count={nbParticles} usage={DynamicDrawUsage} /> </instancedMesh>
现在是为我们的粒子添加运动、颜色和生命周期逻辑的时候了。
粒子生命周期
为了计算我们的粒子的行为,我们需要将经过的时间传递给着色器。我们将使用 shaderMaterial
的 uniforms
属性来将时间传递给它。
让我们更新 ParticlesMaterial
来添加一个 uTime
uniform:
const ParticlesMaterial = shaderMaterial( { uTime: 0, }, /* glsl */ ` uniform float uTime; // ... `, /* glsl */ ` // ... ` );
在 useFrame
循环中,我们将更新 uTime
uniform:
// ... import { useFrame } from "@react-three/fiber"; // ... export const VFXParticles = ({ settings = {} }) => { // ... useFrame(({ clock }) => { if (!mesh.current) { return; } mesh.current.material.uniforms.uTime.value = clock.elapsedTime; }); // ... };
在顶点着色器中,我们将基于 uTime
uniform 和 instanceLifetime
属性计算每个粒子的年龄和进度。我们将把进度传递给片段着色器,以使用名为 vProgress
的 varying 动画粒子。
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; gl_Position = projectionMatrix * modelViewMatrix * vec4(instanceMatrix * vec4(position, 1.0)); vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
年龄通过从 uTime
中减去 startTime
来计算。然后通过将年龄除以 duration
来计算进度。
现在在片段着色器中,我们将基于进度在 instanceColor
和 instanceColorEnd
之间插值粒子的颜色:
varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; varying vec2 vUv; void main() { vec3 finalColor = mix(vColor, vColorEnd, vProgress); gl_FragColor = vec4(finalColor, 1.0); }
我们可以看到粒子的颜色随时间变化,但我们面临一个问题。所有粒子在开始时都是可见的,而它们的开始时间是随机的。我们需要隐藏尚未出生的粒子。
为防止未出生和死亡的粒子被渲染,我们将在片段着色器中使用 discard
关键字:
// ... void main() { if (vProgress < 0.0 || vProgress > 1.0) { discard; } // ... }
discard
关键字告诉渲染器丢弃当前片段并不渲染它。
完美,现在我们的粒子随着时间的推移出生、存活和消亡。我们现在可以添加移动和旋转逻辑。
粒子运动
通过使用粒子的 方向、速度 和 年龄,我们可以计算出它们随时间变化的 位置。
在 顶点着色器 中,我们调整 gl_Position
来考虑粒子的 方向 和 速度。
首先,我们标准化 方向,以避免当方向不是单位向量时粒子移动得更快:
vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0);
然后,我们根据 速度 和 年龄 计算粒子的 偏移量:
vec3 offset = normalizedDirection * age * instanceSpeed;
获取 实例位置:
vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz;
并将 偏移量 应用于它:
vec3 finalPosition = instancePosition + offset;
最后,应用 modelViewMatrix 获取模型视图位置 mvPosition
,将位置转换为 世界空间:
vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0);
应用 projectionMatrix 将世界位置转换为 相机空间:
gl_Position = projectionMatrix * mvPosition;
以下是我们完整的 顶点着色器 到目前为止:
uniform float uTime; varying vec2 vUv; varying vec3 vColor; varying vec3 vColorEnd; varying float vProgress; attribute float instanceSpeed; attribute vec3 instanceRotationSpeed; attribute vec3 instanceDirection; attribute vec3 instanceColor; attribute vec3 instanceColorEnd; attribute vec2 instanceLifetime; // x: startTime, y: duration void main() { float startTime = instanceLifetime.x; float duration = instanceLifetime.y; float age = uTime - startTime; vProgress = age / duration; vec3 normalizedDirection = length(instanceDirection) > 0.0 ? normalize(instanceDirection) : vec3(0.0); vec3 offset = normalizedDirection * age * instanceSpeed; vec4 startPosition = modelMatrix * instanceMatrix * vec4(position, 1.0); vec3 instancePosition = startPosition.xyz; vec3 finalPosition = instancePosition + offset; vec4 mvPosition = modelViewMatrix * vec4(finalPosition, 1.0); gl_Position = projectionMatrix * mvPosition; vUv = uv; vColor = instanceColor; vColorEnd = instanceColorEnd; }
粒子现在以不同速度向多个方向运动。这很混乱,但这是因为我们的随机值。
通过在 emit
函数中调整我们的随机值来解决这个问题,以更清晰地观察粒子的运动:
for (let i = 0; i < count; i++) { const position = [ randFloatSpread(0.1), randFloatSpread(0.1), randFloatSpread(0.1), ]; const scale = [randFloatSpread(1), randFloatSpread(1), randFloatSpread(1)]; const rotation = [ randFloatSpread(Math.PI), randFloatSpread(Math.PI), randFloatSpread(Math.PI), ]; tmpPosition.set(...position); tmpRotationEuler.set(...rotation); tmpRotation.setFromEuler(tmpRotationEuler); tmpScale.set(...scale); tmpMatrix.compose(tmpPosition, tmpRotation, tmpScale); mesh.current.setMatrixAt(i, tmpMatrix); tmpColor.setRGB(1, 1, 1); instanceColor.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); tmpColor.setRGB(0, 0, 0); instanceColorEnd.set([tmpColor.r, tmpColor.g, tmpColor.b], i * 3); const direction = [randFloatSpread(0.5), 1, randFloatSpread(0.5)]; instanceDirection.set(direction, i * 3); const lifetime = [randFloat(0, 5), randFloat(0.1, 5)]; instanceLifetime.set(lifetime, i * 2); const speed = randFloat(1, 5); instanceSpeed.set([speed], i); const rotationSpeed = [ randFloatSpread(1), randFloatSpread(1), randFloatSpread(1), ]; instanceRotationSpeed.set(rotationSpeed, i * 3); }
开始成形!
稍后我们将添加简单的 UI 控件 来调整变量。现在让我们通过添加旋转逻辑来完善粒子。
在为移动分离方向和速度时,旋转将使用单个 instanceRotationSpeed
属性来定义每个轴的旋转速度。
在 顶点着色器 中,我们可以根据 旋转速度 和 年龄 计算粒子的 旋转:
vec3 rotationSpeed = instanceRotationSpeed * age;
然后,为了能够将这种“偏移旋转”应用于粒子,我们需要将其转换为旋转矩阵:
mat4 rotX = rotationX(rotationSpeed.x); mat4 rotY = rotationY(rotationSpeed.y); mat4 rotZ = rotationZ(rotationSpeed.z); mat4 rotationMatrix = rotZ * rotY * rotX;
rotationX
、rotationY
和 rotationZ
是返回绕 X、Y、Z 轴的旋转矩阵的函数。我们将在 顶点着色器 中 main
函数之上定义它们:
mat4 rotationX(float angle) { float s = sin(angle); float c = cos(angle); return mat4( 1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1 ); } mat4 rotationY(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ); } mat4 rotationZ(float angle) { float s = sin(angle); float c = cos(angle); return mat4( c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ); }
若想了解更多关于旋转矩阵的信息,您可以查看这个维基百科文章,由 Simon Dev 创作的精彩课程 Game Math Explained Simply,或是 The Book of Shaders 中的矩阵部分。
最后,我们可以将 旋转矩阵 应用于粒子的 起始位置:
vec4 startPosition = modelMatrix * instanceMatrix * rotationMatrix * vec4(position, 1.0);
让我们试试看:
粒子现在正在移动,变色并旋转!✨
完美,我们为我们的 VFX 引擎 打下了坚实的基础。在添加更多功能和控件之前,让我们准备引擎的第二个重要部分:发射器。
发射器
在示例和教程中,粒子系统的这个部分常常被忽视。但要把粒子轻松高效地整合到你的项目中,这是至关重要的:
- 轻松:因为你的
<VFXParticles />
组件将位于层级结构的顶部,你的发射器可以从场景中的任何子组件生成它们。使得从特定点生成它们,将它们附加到移动对象或移动骨骼变得简单。 - 高效:因为不需要每次想生成粒子时重新创建实例化网格、编译着色器材质和设置属性,你可以复用相同的 VFXParticles 组件,只需调用一个函数来生成具有所需设置的粒子。
useVFX
我们希望能够从项目中的任何地方调用 VFXParticles
组件中的 emit
函数。为此,我们将创建一个名为 useVFX
的自定义 hook,用于注册和注销 VFXParticles 组件中的发射器。
我们将使用 Zustand,因为它是一种在 React 中管理全局状态的简单高效的方法,具有出色的性能。
让我们将其添加到项目中:
yarn add zustand
在 vfxs
文件夹中,让我们创建一个 VFXStore.js
文件:
import { create } from "zustand"; export const useVFX = create((set, get) => ({ emitters: {}, registerEmitter: (name, emitter) => { if (get().emitters[name]) { console.warn(`Emitter ${name} already exists`); return; } set((state) => { state.emitters[name] = emitter; return state; }); }, unregisterEmitter: (name) => { set((state) => { delete state.emitters[name]; return state; }); }, emit: (name, ...params) => { const emitter = get().emitters[name]; if (!emitter) { console.warn(`Emitter ${name} not found`); return; } emitter(...params); }, }));
内容包括:
- emitters:一个对象,用来存储我们
VFXParticles
组件的所有发射器。 - registerEmitter:一个注册具有特定名称的发射器的函数。
- unregisterEmitter:一个注销具有特定名称的发射器的函数。
- emit:一个函数,用来从项目中的任何地方调用具有特定名称和参数的发射器。
让我们将其加入 VFXParticles
组件:
// ... import { useVFX } from "./VFXStore"; // ... export const VFXParticles = ({ name, settings = {} }) => { // ... const registerEmitter = useVFX((state) => state.registerEmitter); const unregisterEmitter = useVFX((state) => state.unregisterEmitter); useEffect(() => { // emit(nbParticles); registerEmitter(name, emit); return () => { unregisterEmitter(name); }; }, []); // ... }; // ...
我们给 VFXParticles
组件增加了一个 name
属性以识别发射器。然后使用 useVFX
hook 获取 registerEmitter
和 unregisterEmitter
函数。
在 useEffect
hook 内,使用 registerEmitter
函数注册发射器,当组件挂载时注册发射器,并在卸载时注销发射器。
在 Experience
组件中,给 VFXParticles
组件添加 name
属性:
// ... import { VFXParticles } from "./vfxs/VFXParticles"; export const Experience = () => { return ( <> {/* ... */} <VFXParticles name="sparks" /> </> ); };
VFXEmitter
现在我们有了 useVFX
钩子,可以创建一个 VFXEmitter
组件,该组件负责从我们的 VFXParticles
组件中产生粒子。
在 vfxs
文件夹中,创建一个 VFXEmitter.jsx
文件:
import { forwardRef, useImperativeHandle, useRef } from "react"; import { useVFX } from "./VFXStore"; export const VFXEmitter = forwardRef( ({ emitter, settings = {}, ...props }, forwardedRef) => { const { duration = 1, nbParticles = 1000, spawnMode = "time", // time, burst loop = false, delay = 0, } = settings; const emit = useVFX((state) => state.emit); const ref = useRef(); useImperativeHandle(forwardedRef, () => ref.current); return ( <> <object3D {...props} ref={ref} /> </> ); } );
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.