The magical world of Particles with React Three Fiber and Shaders

November 8, 2022 / 26 min read /

0 Likes β€’

0 Replies β€’

0 Mentions

Last Updated November 8, 2022

Since writing The Study of Shaders with React Three Fiber, I've continued building new scenes to perfect my shader skills and learn new techniques to achieve even more ambitious creations. While shaders on their own unlocked a new realm of what's possible to do on the web, there's one type of 3D object that I've overlooked until recently: particles!

Whether it's to create galaxies, stars, smoke, fire, or even some other abstract effects, particles are the best tool to help you create scenes that can feel truly magical πŸͺ„.

However, particles can also feel quite intimidating at first. It takes a lot of practice to get familiar with the core concepts of particle-based scenes such as attributes or buffer geometries and advanced ones like combining them with custom shaders or using Frame Buffer Objects to push those scenes even further.

In this article, you will find all the tips and techniques I learned regarding particles, from creating simple particle systems with standard and buffer geometries to customizing how they look, controlling their movement with shaders, and techniques to scale the number of particles even further. You'll also get a deeper understanding of attributes, a key shader concept I overlooked in my previous blog post that is essential for these use cases.

An introduction to attributes

Before we can jump into creating gorgeous particle-based scenes with React Three Fiber, we have to talk about attributes.

What are attributes?

Attributes are pieces of data associated with each vertex of a mesh. If you've been playing with React Three Fiber and created some meshes, you've already used attributes without knowing! Each geometry associated with a mesh has a set of pre-defined attributes such as:

  • ArrowAn icon representing an arrow
    The position attribute: an array of data representing all the positions of each vertex of a given geometry.
  • ArrowAn icon representing an arrow
    The uv attribute: an array of data representing the UV coordinates of a given geometry.

These are just two examples among many possibilities, but you'll find these in pretty much any geometry you'll use. You can easily take a peek at them to see what kind of data it contains:

Logging the attributes of a geometry

1
const Scene = () => {
2
const mesh = useRef();
3
4
useEffect(() => {
5
console.log(mesh.current.geometry.attributes);
6
}, []);
7
8
return <mesh ref={mesh}>{/* ... */}</mesh>;
9
};

You should see something like this:

Screenshot showcasing the output printed when logging the attributes of a geometry
Screenshot showcasing the output printed when logging the attributes of a geometry

If you're feeling confused right now, do not worry πŸ˜„. I was too! Seeing data like this can feel intimidating at first, but we'll make sense of all this just below.

Playing with attributes

This long array with lots of numbers represents the value of the x, y, and z coordinates for each vertex of our geometry. It's one-dimensional (no nested data), where each value x, y, and z of a given vertex is right next to the ones from the other vertex. I built the little widget below to illustrate in a more approachable way how the values of that position array translate to points in space:

Position attributes array to vertex visualizer

Now that we know how to interpret that data, we can start having some fun with it. You can easily manipulate and modify attributes and create some nice effects without the need to touch shader code.

Below is an example where we use attributes to twist a boxGeometry along its y-axis.

We do this effect by:

  • ArrowAn icon representing an arrow
    Copying the original position attribute of the geometry.
1
// Get the current attributes of the geometry
2
const currentPositions = mesh.current.geometry.attributes.position;
3
// Copy the attributes
4
const originalPositions = currentPositions.clone();
  • ArrowAn icon representing an arrow
    Looping through each value of the array and applying a rotation.
1
const originalPositionsArray = originalPositions?.array || [];
2
3
// Go through each vector (series of 3 values) and modify the values
4
for (let i = 0; i < originalPositionsArray.length; i = i + 3) {
5
// ...
6
}
  • ArrowAn icon representing an arrow
    Pass the newly generated data to the geometry to replace the original position attribute array.
1
// Apply the modified position vector coordinates to the current position attributes array
2
currentPositions.array[i] = modifiedPositionVector.x;
3
currentPositions.array[i + 1] = modifiedPositionVector.y;
4
currentPositions.array[i + 2] = modifiedPositionVector.z;

Attributes with Shaders

I briefly touched upon this subject when I introduced the notion of uniforms in The Study of Shaders with React Three Fiber but could not find a meaningful way to tackle it without making an already long article even longer.

We saw that we use uniforms to pass data from our Javascript code to a shader. Attributes are pretty similar in that regard as well, but there is one key difference:

  • ArrowAn icon representing an arrow
    Data passed to a shader via a uniform remains constant between each vertex of a mesh (and pixels as well)
  • ArrowAn icon representing an arrow
    Data passed via an attribute can be different for each vertex, allowing us to more fine-tuned controls of our vertex shader.

You can see that attributes allow us to control each vertex of a mesh, but not only! For particle-based scenes, we will heavily rely on them to:

  • ArrowAn icon representing an arrow
    position our particles in space
  • ArrowAn icon representing an arrow
    move, scale, or animate our particles through time
  • ArrowAn icon representing an arrow
    customize each particle in a unique way

That is why it's necessary to have a somewhat clear understanding of attributes before getting started with particles.

Particles in React Three Fiber

Now that we know more about attributes, we can finally bring our focus to the core of this article: particles.

Our first scene with Particles

Remember how we can define a mesh as follows: mesh = geometry + material? Well, that definition also applies to points, the construct we use to create particles:

points = geometry + material

The only difference at this stage is that our points will use a specific type of material, the pointsMaterial.

Below you'll find an example of a particle system in React Three Fiber. As you can see, we're creating a system in the shape of a sphere by using

  • ArrowAn icon representing an arrow
    points
  • ArrowAn icon representing an arrow
    sphereGeometry for our geometry
  • ArrowAn icon representing an arrow
    pointsMaterial for our material

Now you may ask me: this is great, but what if I want to position my particles more organically? What about creating a randomized cloud of particles? Well, this is where the notion of attributes comes into play!

Using BufferGeometry and attributes to create custom geometries

In Three.js and React Three Fiber, we can create custom geometries thanks to the use of:

  • ArrowAn icon representing an arrow
    bufferGeometry
  • ArrowAn icon representing an arrow
    bufferAttribute
  • ArrowAn icon representing an arrow
    our newly acquired knowledge of attributes πŸŽ‰

When working with Particles, using a bufferGeometry can be really powerful: it gives us full-control over the placement of each particle, and later we'll also see how this lets us animate them.

Let's take a look at how we can define a custom geometry in React Three Fiber with the following code example:

Custom geometry with bufferGeometry and bufferAttribute

1
const CustomGeometryParticles = () => {
2
const particlesPosition = [
3
/* ... */
4
];
5
6
return (
7
<points ref={points}>
8
<bufferGeometry>
9
<bufferAttribute
10
attach="attributes-position"
11
count={particlesPosition.length / 3}
12
array={particlesPosition}
13
itemSize={3}
14
/>
15
</bufferGeometry>
16
<pointsMaterial
17
size={0.015}
18
color="#5786F5"
19
sizeAttenuation
20
depthWrite={false}
21
/>
22
</points>
23
);
24
};

In the code snippet above, we can see that:

  1. ArrowAn icon representing an arrow
    We are rendering a bufferGeometry as the geometry of our points.
  2. ArrowAn icon representing an arrow
    In this bufferGeometry, we're using the bufferAttribute element that lets us set the position attribute of our geometry.

Now let's take a look at the props that we're passing to the bufferAttribute element:

  • ArrowAn icon representing an arrow
    count is the total number of vertex our geometry will have. In our case, it is the number of particles we will end up rendering.
  • ArrowAn icon representing an arrow
    attach is how we specify the name of our attribute. In this case, we set it as attributes-position so the data we're feeding to the bufferAttribute is available under the position attribute.
  • ArrowAn icon representing an arrow
    itemSize represents the number of values from our attributes array associated with one item/vertex. In this case, it's set to 3 as we're dealing with the position attribute that has three components x, y, and z.

Now when it comes to creating the attributes array itself, let's look at the particlePositions array located in our particle scene code.

Generating a position attribute array

1
const count = 2000;
2
3
const particlesPosition = useMemo(() => {
4
// Create a Float32Array of count*3 length
5
// -> we are going to generate the x, y, and z values for 2000 particles
6
// -> thus we need 6000 items in this array
7
const positions = new Float32Array(count * 3);
8
9
for (let i = 0; i < count; i++) {
10
// Generate random values for x, y, and z on every loop
11
let x = (Math.random() - 0.5) * 2;
12
let y = (Math.random() - 0.5) * 2;
13
let z = (Math.random() - 0.5) * 2;
14
15
// We add the 3 values to the attribute array for every loop
16
positions.set([x, y, z], i * 3);
17
}
18
19
return positions;
20
}, [count]);
  1. ArrowAn icon representing an arrow
    First, we specify a Float32Array with a length of count * 3. We're going to render count particles, e.g. 2000, and each particle has three values (x, y, and z) associated with its position, i.e. *6000 values in total.
  2. ArrowAn icon representing an arrow
    Then, we create a loop, and for each particle, we set all the values for x, y, and z. In this case, we're using some level of randomness to position our particles randomly.
  3. ArrowAn icon representing an arrow
    Finally, we're adding all three values to the array at the position i * 3 with positions.set([x,y,z], i*3).

The code sandbox below showcases what we can render with this technique of using custom geometries. In this example, I created two different position attribute arrays that place particles randomly:

  • ArrowAn icon representing an arrow
    at the surface of a sphere
  • ArrowAn icon representing an arrow
    in a box, which you can render by changing the shape prop to box and hitting reload.

We can see that using custom geometries lets us get a more organic render for our particle system, which looks prettier and opens up way more possibilities than standard geometries ✨.

Customizing and animating Particles with Shaders

Now that we know how to create a particle system based on custom geometries, we can start focusing on the fun part: animating particles! πŸŽ‰

There are two ways to approach animating particles:

  1. ArrowAn icon representing an arrow
    Using attributes (easier)
  2. ArrowAn icon representing an arrow
    Using shaders (a bit harder)

We'll look at both ways, although, as you may expect, if you know me a little bit through the work I share on Twitter, we're going to focus a lot on the second one. A little bit of challenge never hurts!

Animating Particles with attributes

For this part, we will see how to animate our particles by updating our position attribute array on every frame using the useFrame hook. If you've animated meshes with React Three Fiber before, this method should be straightforward!

We just saw how to create an attributes array; updating it is pretty much the same process:

  • ArrowAn icon representing an arrow
    We loop through the current values of the attributes array. It can be all the values or just some of them.
  • ArrowAn icon representing an arrow
    Update them.
  • ArrowAn icon representing an arrow
    And finally, the most important: set the needsUpdate field of our position attribute to true.

Animate particles via attributes in React Three Fiber

1
useFrame((state) => {
2
const { clock } = state;
3
4
for (let i = 0; i < count; i++) {
5
const i3 = i * 3;
6
7
points.current.geometry.attributes.position.array[i3] +=
8
Math.sin(clock.elapsedTime + Math.random() * 10) * 0.01;
9
points.current.geometry.attributes.position.array[i3 + 1] +=
10
Math.cos(clock.elapsedTime + Math.random() * 10) * 0.01;
11
points.current.geometry.attributes.position.array[i3 + 2] +=
12
Math.sin(clock.elapsedTime + Math.random() * 10) * 0.01;
13
}
14
15
points.current.geometry.attributes.position.needsUpdate = true;
16
});

The scene rendered below uses this technique to move the particles around their initial position, making the particle system feel a bit more alive ✨

Despite being the easiest, this method is also pretty expensive: on every frame, we have to loop through very long attribute arrays and update them. Over and over. As you might expect, this becomes a real problem as the number of particles grows. Thus it's preferable to delegate that part to the GPU with a sweet shader, which also has the added benefit to be more elegant. (a totally non-biased opinion from someone who dedicated weeks of their life working with shaders πŸ˜„).

How to animate our particles with a vertex shader

First and foremost, it's time to say goodbye to our pointsMaterial πŸ‘‹, and replace it with a shaderMaterial as follows:

How to use a custom shaderMaterial with particles and a custom buffer geometry

1
const CustomGeometryParticles = (props) => {
2
const { count } = props;
3
const points = useRef();
4
5
const particlesPosition = useMemo(() => ({
6
// We set out positions here as we did before
7
)}, [])
8
9
const uniforms = useMemo(() => ({
10
uTime: {
11
value: 0.0
12
},
13
// Add any other attributes here
14
}), [])
15
16
useFrame((state) => {
17
const { clock } = state;
18
19
points.current.material.uniforms.uTime.value = clock.elapsedTime;
20
});
21
22
return (
23
<points ref={points}>
24
<bufferGeometry>
25
<bufferAttribute
26
attach="attributes-position"
27
count={particlesPosition.length / 3}
28
array={particlesPosition}
29
itemSize={3}
30
/>
31
</bufferGeometry>
32
<shaderMaterial
33
depthWrite={false}
34
fragmentShader={fragmentShader}
35
vertexShader={vertexShader}
36
uniforms={uniforms}
37
/>
38
</points>
39
);
40
}

As we learned in The Study of Shaders with React Three Fiber, we need to specify two functions for our shaderMaterial:

  • ArrowAn icon representing an arrow
    the fragment shader: this is where we'll focus on the next part to customize our particles
  • ArrowAn icon representing an arrow
    the vertex shader: this is where we'll animate our particles

Vertex shader code that applies a rotation along the y-axis

1
uniform float uTime;
2
3
void main() {
4
vec3 particlePosition = position * rotation3dY(uTime * 0.2);
5
6
vec4 modelPosition = modelMatrix * vec4(particlePosition, 1.0);
7
vec4 viewPosition = viewMatrix * modelPosition;
8
vec4 projectedPosition = projectionMatrix * viewPosition;
9
10
gl_Position = projectedPosition;
11
gl_PointSize = 3.0;
12
}

As you can see in the snippet above, when it comes to the code, animating particles using a shader is very similar to animating a mesh. With the vertex shader, you get to interact with the vertices of a geometry, which are the particles themselves in this use case.

Since we're there, let's iterate on that shader code to make the resulting scene even better: make the particles close to the center of the sphere move faster than the ones on the outskirts.

Enhanced version of the previous vertex shader

1
uniform float uTime;
2
uniform float uRadius;
3
4
void main() {
5
float distanceFactor = pow(uRadius - distance(position, vec3(0.0)), 2.0);
6
vec3 particlePosition = position * rotation3dY(uTime * 0.2 * distanceFactor);
7
8
vec4 modelPosition = modelMatrix * vec4(particlePosition, 1.0);
9
vec4 viewPosition = viewMatrix * modelPosition;
10
vec4 projectedPosition = projectionMatrix * viewPosition;
11
12
gl_Position = projectedPosition;
13
gl_PointSize = 3.0;
14
}

Which renders as the following once we wire this shader to our React Three Fiber code with a uTime and uRadius uniform:

How to change the size and appearance of our particles with shaders

This entire time, our particles were simple tiny squares, which is a bit boring. In this part, we'll look at how to fix this with some well-thought-out shader code.

First, let's look at the size. All our particles are the same size right now which does not really give off an organic vibe to this scene. To address that, we can tweak the gl_PointSize property in our vertex shader code.

We can do multiple things with the point size:

  • ArrowAn icon representing an arrow
    Making it a function of the position with some Perlin noise
  • ArrowAn icon representing an arrow
    Making it a function of the distance from the center of your geometry
  • ArrowAn icon representing an arrow
    Simply making it random

Anything is possible! For this example, we'll pick the second one:

Now, when it comes to the particle pattern itself, we can modify it in the fragment shader. I like to make my particles look like tiny points of light that we can luckily achieve with a few lines of code.

Fragment shader that changes the appearance of our particles

1
varying float vDistance;
2
3
void main() {
4
vec3 color = vec3(0.34, 0.53, 0.96);
5
// Create a strength variable that's bigger the closer to the center of the particle the pixel is
6
float strength = distance(gl_PointCoord, vec2(0.5));
7
strength = 1.0 - strength;
8
// Make it decrease in strength *faster* the further from the center by using a power of 3
9
strength = pow(strength, 3.0);
10
11
// Ensure the color is only visible close to the center of the particle
12
color = mix(vec3(0.0), color, strength);
13
gl_FragColor = vec4(color, strength);
14
}

We can now make the colors of the particles a parameter of the material through a uniform and also make it a function of the distance to the center, for example:

Enhanced version of the previous fragment shader

1
varying float vDistance;
2
3
void main() {
4
vec3 color = vec3(0.34, 0.53, 0.96);
5
float strength = distance(gl_PointCoord, vec2(0.5));
6
strength = 1.0 - strength;
7
strength = pow(strength, 3.0);
8
9
// Make particle close to the *center of the scene* a warmer color
10
// and the ones on the outskirts a cooler color
11
color = mix(color, vec3(0.97, 0.70, 0.45), vDistance * 0.5);
12
color = mix(vec3(0.0), color, strength);
13
// Here we're passing the strength in the alpha channel to make sure the outskirts
14
// of the particle are not visible
15
gl_FragColor = vec4(color, strength);
16
}

In the end, we get a beautiful set of custom particles with just a few lines of GLSL sprinkled on top of our particle system πŸͺ„

Going beyond with Frame Buffer Objects

What if we wanted to render a lot more particles onto our scene? What about 100's of thousands? That would be pretty cool, right? With this advanced technique I'm about to show you, it is possible! And on top of that, with little to no frame drop πŸ”₯!

This technique is named Frame Buffer Object (FBO). I stumbled upon it when I wanted to reproduce one of @winkerVSbecks attractor scenes from his blog post Three ways to create 3D particle effects.

Long story short, I wanted to build the same attractor effect but with shaders. The problem was that in an attractor, the position of a particle is dictated by its previous one, which doesn't work by just relying on the position attributes and a vertex shader: there's no way to get the updated position back to our Javascript code after it's been updated in our vertex shader and feed it back to the shader to calculate the next one! Thankfully, thanks to using an FBO, I figured out a way to render this scene.

How does a Frame Buffer Object work with particles?

I've seen many people using this technique in Three.js codebases. Here is how it goes: instead of initiating our particles positions array and passing it as an attribute and then render them, we are going to have 3 phases with two render passes.

  1. ArrowAn icon representing an arrow
    The simulation pass. We set the positions of the particles as a Data Texture to a shader material. They are then read, returned, and sometimes modified in the material's fragment shader (you heard me right!).
  2. ArrowAn icon representing an arrow
    Create a WebGLRenderTarget, a "texture" we can render to off-screen where we will add a small scene containing our material from the simulation pass and a small plane. We then set it as the current render target, thus rendering our simulation material with its Data Texture that is filled with position data.
  3. ArrowAn icon representing an arrow
    The render pass. We can now read the texture rendered in the render target. The texture data is the positions array of our particles, which we can now pass as a uniform to our particles' shaderMaterial.

In the end, we're using the simulation pass as a buffer to store data and do a lot of heavy calculations on the GPU by processing our positions in a fragment shader, and we do that on every single frame. Hence the name Frame Buffer Object. I hope I did not lose you there πŸ˜…. Maybe the diagram below, as well as the following code snippet will help πŸ‘‡:

Diagram illustrating how the Frabe Buffer Objects allows to store and update particles position data in a vertex shader and then be read as a texture.
Diagram illustrating how the Frabe Buffer Objects allows to store and update particles position data in a vertex shader and then be read as a texture.

Setting up a simulation material

1
import { extend } from '@react-three/fiber';
2
// ... other imports
3
4
const generatePositions = (width, height) => {
5
// we need to create a vec4 since we're passing the positions to the fragment shader
6
// data textures need to have 4 components, R, G, B, and A
7
const length = width * height * 4;
8
const data = new Float32Array(length);
9
10
// Fill Float32Array here
11
12
return data;
13
};
14
15
// Create a custom simulation shader material
16
class SimulationMaterial extends THREE.ShaderMaterial {
17
constructor(size) {
18
// Create a Data Texture with our positions data
19
const positionsTexture = new THREE.DataTexture(
20
generatePositions(size, size),
21
size,
22
size,
23
THREE.RGBAFormat,
24
THREE.FloatType
25
);
26
positionsTexture.needsUpdate = true;
27
28
const simulationUniforms = {
29
// Pass the positions Data Texture as a uniform
30
positions: { value: positionsTexture },
31
};
32
33
super({
34
uniforms: simulationUniforms,
35
vertexShader: simulationVertexShader,
36
fragmentShader: simulationFragmentShader,
37
});
38
}
39
}
40
41
// Make the simulation material available as a JSX element in our canva
42
extend({ SimulationMaterial: SimulationMaterial });

Setting up an FBO with a simulation material in React Three Fiber

1
import { useFBO } from '@react-three/drei';
2
import { useFrame, createPortal } from '@react-three/fiber';
3
4
const FBOParticles = () => {
5
const size = 128;
6
7
// This reference gives us direct access to our points
8
const points = useRef();
9
const simulationMaterialRef = useRef();
10
11
// Create a camera and a scene for our FBO
12
const scene = new THREE.Scene();
13
const camera = new THREE.OrthographicCamera(
14
-1,
15
1,
16
1,
17
-1,
18
1 / Math.pow(2, 53),
19
1
20
);
21
22
// Create a simple square geometry with custom uv and positions attributes
23
const positions = new Float32Array([
24
-1,
25
-1,
26
0,
27
1,
28
-1,
29
0,
30
1,
31
1,
32
0,
33
-1,
34
-1,
35
0,
36
1,
37
1,
38
0,
39
-1,
40
1,
41
0,
42
]);
43
const uvs = new Float32Array([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0]);
44
45
// Create our FBO render target
46
const renderTarget = useFBO(size, size, {
47
minFilter: THREE.NearestFilter,
48
magFilter: THREE.NearestFilter,
49
format: THREE.RGBAFormat,
50
stencilBuffer: false,
51
type: THREE.FloatType,
52
});
53
54
// Generate a "buffer" of vertex of size "size" with normalized coordinates
55
const particlesPosition = useMemo(() => {
56
const length = size * size;
57
const particles = new Float32Array(length * 3);
58
for (let i = 0; i < length; i++) {
59
let i3 = i * 3;
60
particles[i3 + 0] = (i % size) / size;
61
particles[i3 + 1] = i / size / size;
62
}
63
return particles;
64
}, [size]);
65
66
const uniforms = useMemo(
67
() => ({
68
uPositions: {
69
value: null,
70
},
71
}),
72
[]
73
);
74
75
useFrame((state) => {
76
const { gl, clock } = state;
77
78
// Set the current render target to our FBO
79
gl.setRenderTarget(renderTarget);
80
gl.clear();
81
// Render the simulation material with square geometry in the render target
82
gl.render(scene, camera);
83
// Revert to the default render target
84
gl.setRenderTarget(null);
85
86
// Read the position data from the texture field of the render target
87
// and send that data to the final shaderMaterial via the `uPositions` uniform
88
points.current.material.uniforms.uPositions.value = renderTarget.texture;
89
90
simulationMaterialRef.current.uniforms.uTime.value = clock.elapsedTime;
91
});
92
93
return (
94
<>
95
{/* Render off-screen our simulation material and square geometry */}
96
{createPortal(
97
<mesh>
98
<simulationMaterial ref={simulationMaterialRef} args={[size]} />
99
<bufferGeometry>
100
<bufferAttribute
101
attach="attributes-position"
102
count={positions.length / 3}
103
array={positions}
104
itemSize={3}
105
/>
106
<bufferAttribute
107
attach="attributes-uv"
108
count={uvs.length / 2}
109
array={uvs}
110
itemSize={2}
111
/>
112
</bufferGeometry>
113
</mesh>,
114
scene
115
)}
116
<points ref={points}>
117
<bufferGeometry>
118
<bufferAttribute
119
attach="attributes-position"
120
count={particlesPosition.length / 3}
121
array={particlesPosition}
122
itemSize={3}
123
/>
124
</bufferGeometry>
125
<shaderMaterial
126
blending={THREE.AdditiveBlending}
127
depthWrite={false}
128
fragmentShader={fragmentShader}
129
vertexShader={vertexShader}
130
uniforms={uniforms}
131
/>
132
</points>
133
</>
134
);
135
};

Creating magical scenes with FBO

To demonstrate the power of FBO, let's look at two scenes I built with this technique πŸ‘€.

The first one renders a particle system in the shape of a sphere with randomly positioned points. In the simulationMaterial, I applied a curl-noise to the position data of the particles, which yields the gorgeous effect you can see below ✨!

In this scene, we:

  • ArrowAn icon representing an arrow
    render 128 x 128 (the resolution of our render target) particles.
  • ArrowAn icon representing an arrow
    apply a curl noise to each of our particles in our simulation pass.
  • ArrowAn icon representing an arrow
    pass all that data along to the renderMaterial that takes care to render each vertex with that position data and also the particle size using the gl_pointSize property.

Finally, one last scene, just for fun! I ported to React Three Fiber a Three.js demo from an article written by @nicoptere that does a pretty good job at deep diving into the FBO technique.

In it, I pass not only one but two Data Textures:

  • ArrowAn icon representing an arrow
    the first one contains the data to position the particles as a box
  • ArrowAn icon representing an arrow
    the second one as a sphere

Then in the fragment shader of the simulationMaterial, we use GLSL's mix function to alternate over time between the two "textures" which results in this scene where the particles morph from one shape to another.

Conclusion

From zero to FBO, you now know pretty much everything I know about particles as of writing these words πŸŽ‰! There's, of course, still a lot more to explore, but I hope this blog post was a good introduction to the basics and more advanced techniques and that it can serve as a guide to get back to during your own journey with Particles and React Three Fiber.

Techniques like FBO enable almost limitless possibilities for particle-based scenes, and I can't wait to see what you'll get to create with it ✨. I couldn't resist sharing this with you in this write-up πŸͺ„. Frame Buffer Objects have a various set of use cases, not just limited to particles that I haven't explored deeply enough yet. That will probably be a topic for a future blog post, who knows?

As a productive next step to push your particle skills even further, I can only recommend to hack on your own. You now have all the tools to get started πŸ˜„.

Fetching Replies...

Do you have any questions, comments or simply wish to contact me privately? Don’t hesitate to shoot me a DM on Twitter.


Have a wonderful day.
Maxime

Subscribe to my newsletter

Get email from me about my ideas, frontend development resources and tips as well as exclusive previews of upcoming articles.