FDNavigate back to the homepage

Creating amazing particle effect along a curve in React Three Fiber

Rick
August 2nd, 2023 · 2 min read

The idea I had was to connect boxes with particle streams, so after having done this cool little side project a friend showed me a website which shows an example of this

blueYard website landing page example

So the implementation is probably different but particles following a path is occurring here.

The curve

A curve in three can be a few things like bexier catmullrom etc and you can define it like below:

1const points = [
2 new THREE.Vector3(-3.4, -3.5, 0),
3 new THREE.Vector3(1, 0, 0),
4 new THREE.Vector3(6, 0, 0),
5 new THREE.Vector3(8, 0, 0)
6 // Add more points here (200 points in total)
7]
8const curve = new THREE.CatmullRomCurve3(points)
9
10const numPoints = count
11
12const progresses = useRef(new Array(numPoints).fill(0).map(() => Math.random()))

Define the points in an array and use in the instantiation of the CatmullRomCurve3 class. The progress along this curve is in the range 0-1 and we are using

1Math.random()

As we want the particles to start along the curve at different positions and then progress over time resetting at 1.0 and starting at 0.0.

Defining the render method for points and buffferAttributes

Below is an example of how to setup points with a custom shader and custom bufferAttributes.

1<group>
2 <mesh
3 position={[-5, -3.5, 0]}
4 onPointerMove={(e) => {
5 console.log("hello")
6 uniforms.mousePos.value.x = e.point.x
7 uniforms.mousePos.value.y = e.point.y
8 }}
9 onPointerOut={() => {
10 uniforms.mousePos.value.x = -150
11 uniforms.mousePos.value.y = -150
12 }}
13 >
14 <planeGeometry args={[100, 100]} />
15 <meshBasicMaterial color={"#28282b"} />
16 </mesh>
17 <points ref={pointsRef}>
18 <bufferGeometry attach="geometry">
19 <bufferAttribute
20 attachObject={["attributes", "position"]}
21 array={new Float32Array(numPoints * 3)}
22 itemSize={3}
23 onUpdate={(self) => (self.needsUpdate = true)}
24 />
25 <bufferAttribute
26 attachObject={["attributes", "vScale"]}
27 array={new Float32Array(numPoints)}
28 itemSize={1}
29 onUpdate={(self) => (self.needsUpdate = true)}
30 />
31 </bufferGeometry>
32 <shaderMaterial
33 ref={shaderMaterial}
34 transparent
35 depthWite={false}
36 fragmentShader={fragmentShader}
37 vertexShader={vertexShader}
38 uniforms={uniforms}
39 />
40 </points>
41</group>

This is pretty self explanatory. One small point to make is that we are using a plane position behind the points as a selector for our touch events as if we use the points directly you can get some weird artefact’s where you push the push with repulsion and then it bounces back.

The Shaders for the custom points material

1const vertexShader = `
2 uniform float uPixelRatio;
3 uniform float uSize;
4 uniform float time;
5 uniform vec3 mousePos;
6
7 attribute float vScale;
8
9 void main() {
10 vec3 tempPos = vec3(position.xyz);
11
12 vec3 seg = position - mousePos;
13 vec3 dir = normalize(seg);
14 float dist = length(seg);
15 if (dist < 30.){
16 float force = clamp(1. / (dist * dist), 0., 1.);
17 tempPos += dir * force * 1.1;
18 }
19
20 vec4 modelPosition = modelMatrix * vec4(tempPos, 1.);
21 vec4 viewPosition = viewMatrix * modelPosition;
22 vec4 projectionPosition = projectionMatrix * viewPosition;
23
24
25
26 gl_Position = projectionPosition;
27 gl_PointSize = uSize * vScale * uPixelRatio;
28 gl_PointSize *= (1.0 / -viewPosition.z);
29 }`
30
31const fragmentShader = `
32void main() {
33 float _radius = 0.4;
34 vec2 dist = gl_PointCoord-vec2(0.5);
35 float strength = 1.-smoothstep(
36 _radius-(_radius*0.4),
37 _radius+(_radius*0.3),
38 dot(dist,dist)*4.0
39 );
40
41 gl_FragColor = vec4(0.2, 0.2, 6., strength);
42}
43`

The vertex shader is where the repulsion happens based on a pointer event in R3F and the idea came from this stackOverflow answer.

The Fragment shader just uses opacity to turn the square points into circles using opacity or strength. Its a simple SDF excluding anything outside of a circle.

How to update the progress along the curve

First of we want a random scale attribute for the points so that each point is scaled different, random is natural and better!

Secondly we have positions and progresses float32 array which will be used to store positions and progress of each point.

These can be accessed like so:

1const updatedPositions = new Float32Array(numPoints * 3)
2
3 updatedPositions[i] = ...

We are going to update this on the CPU as we aren’t talking about alot of particles and we need to use the threejs api which we cant really do in glsl shaders.

1useFrame(({ clock }) => {
2 const updatedPositions = new Float32Array(numPoints * 3)
3 const vScale = new Float32Array(numPoints)
4
5 const pointOnCurve = new THREE.Vector3()
6
7 for (let i = 0; i < numPoints; i++) {
8 progresses.current[i] += 0.0006 // Adjust the animation speed here (smaller value for slower animation)
9
10 if (progresses.current[i] >= 1) {
11 progresses.current[i] = 0 // Reset progress to 0 when it reaches 1
12 }
13
14 const progress = progresses.current[i]
15
16 curve.getPointAt(progress, pointOnCurve)
17
18 updatedPositions[i * 3] = pointOnCurve.x + offset[i * 3]
19 updatedPositions[i * 3 + 1] = pointOnCurve.y + offset[i * 3 + 1]
20 updatedPositions[i * 3 + 2] = pointOnCurve.z + offset[i * 3 + 1]
21
22 vScale[i] = Math.random() * 1.1
23 }
24
25 if (pointsRef.current && pointsRef.current.geometry.attributes && pointsRef.current.geometry.attributes.position) {
26 pointsRef.current.geometry.attributes.position.array = updatedPositions
27 pointsRef.current.geometry.attributes.vScale.array = vScale
28 pointsRef.current.geometry.attributes.position.needsUpdate = true
29
30 shaderMaterial.current.uniforms.time.value = clock.elapsedTime
31 }
32 })

We loop over each point and increase the progress array of each particle in this loop.

Then theres a conditional where by if we go over 1.0 of progress (the max) we set the progress to zero again.

After which we can get a point on the curve by using this modified progress and have a vec3 (pointOnCurve) which stores it temporarily:

1const progress = progresses.current[i]
2
3curve.getPointAt(progress, pointOnCurve)

getPointAt is part of the threejs API and theres a few others aswell, go take alook!

We can then use this point and an offset to update the positions:

1updatedPositions[i * 3] = pointOnCurve.x + offset[i * 3]
2updatedPositions[i * 3 + 1] = pointOnCurve.y + offset[i * 3 + 1]
3updatedPositions[i * 3 + 2] = pointOnCurve.z + offset[i * 3 + 1]

Why do we need an offset?

without offset:

example of points on curve without a constant offset

with offset:

example of points on curve with a constant offset

The offset is a constant offset for each point and does not change with progression along the curve. It widens the curve rather than having a single line of points:

1const offset = new Float32Array(numPoints * 3)
2
3for (let i = 0; i < numPoints; i++) {
4 const xOffset = Math.random() * 0.29
5 const yOffset = Math.random() * 0.29
6 offset[i * 3] = xOffset
7 offset[i * 3 + 1] = yOffset
8 offset[i * 3 + 2] = 0
9}

More articles from theFrontDev

Creating Realistic Water Ripples - A Beginner's Guide to Reflective Effects

This article provides a step-by-step tutorial for beginners, covering the basics of creating realistic reflective water ripples using shaders and reflective materials. Discover the techniques to bring your virtual water surfaces to life and add a basic reflective material.

June 3rd, 2023 · 3 min read

Immersive Portals in Three and R3F - Unleashing the Power of Stencil Masks from Drei

Dive into the world of portals in Three.js with the powerful stencil mask functionality from Drei. Experience seamless transitions and captivating environments as you traverse these enchanting gateways. With precise control over visibility using stencil masks, create immersive and interactive scenes that transport your audience to new dimensions. Discover the endless creative possibilities and unlock a realm of storytelling potential with portals in Three.js. Step into a world where reality and imagination converge, and embark on an immersive journey like never before.

May 21st, 2023 · 2 min read
© 2021–2023 theFrontDev
Link to $https://twitter.com/TheFrontDevLink to $https://github.com/Richard-ThompsonLink to $https://www.linkedin.com/in/richard-thompson-248ba3111/