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
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)910const numPoints = count1112const 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 <mesh3 position={[-5, -3.5, 0]}4 onPointerMove={(e) => {5 console.log("hello")6 uniforms.mousePos.value.x = e.point.x7 uniforms.mousePos.value.y = e.point.y8 }}9 onPointerOut={() => {10 uniforms.mousePos.value.x = -15011 uniforms.mousePos.value.y = -15012 }}13 >14 <planeGeometry args={[100, 100]} />15 <meshBasicMaterial color={"#28282b"} />16 </mesh>17 <points ref={pointsRef}>18 <bufferGeometry attach="geometry">19 <bufferAttribute20 attachObject={["attributes", "position"]}21 array={new Float32Array(numPoints * 3)}22 itemSize={3}23 onUpdate={(self) => (self.needsUpdate = true)}24 />25 <bufferAttribute26 attachObject={["attributes", "vScale"]}27 array={new Float32Array(numPoints)}28 itemSize={1}29 onUpdate={(self) => (self.needsUpdate = true)}30 />31 </bufferGeometry>32 <shaderMaterial33 ref={shaderMaterial}34 transparent35 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;67 attribute float vScale;89 void main() {10 vec3 tempPos = vec3(position.xyz);1112 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 }1920 vec4 modelPosition = modelMatrix * vec4(tempPos, 1.);21 vec4 viewPosition = viewMatrix * modelPosition;22 vec4 projectionPosition = projectionMatrix * viewPosition;23242526 gl_Position = projectionPosition;27 gl_PointSize = uSize * vScale * uPixelRatio;28 gl_PointSize *= (1.0 / -viewPosition.z);29 }`3031const 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.039 );4041 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)23 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)45 const pointOnCurve = new THREE.Vector3()67 for (let i = 0; i < numPoints; i++) {8 progresses.current[i] += 0.0006 // Adjust the animation speed here (smaller value for slower animation)910 if (progresses.current[i] >= 1) {11 progresses.current[i] = 0 // Reset progress to 0 when it reaches 112 }1314 const progress = progresses.current[i]1516 curve.getPointAt(progress, pointOnCurve)1718 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]2122 vScale[i] = Math.random() * 1.123 }2425 if (pointsRef.current && pointsRef.current.geometry.attributes && pointsRef.current.geometry.attributes.position) {26 pointsRef.current.geometry.attributes.position.array = updatedPositions27 pointsRef.current.geometry.attributes.vScale.array = vScale28 pointsRef.current.geometry.attributes.position.needsUpdate = true2930 shaderMaterial.current.uniforms.time.value = clock.elapsedTime31 }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]23curve.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:
with 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)23for (let i = 0; i < numPoints; i++) {4 const xOffset = Math.random() * 0.295 const yOffset = Math.random() * 0.296 offset[i * 3] = xOffset7 offset[i * 3 + 1] = yOffset8 offset[i * 3 + 2] = 09}