In this article, we’ll break down an interactive Three.js project that utilizes shaders to create an engaging visual effect. The project leverages a custom vertex and fragment shader, raycasting, and a displacement texture to generate a glowing interactive experience.
Overview
The script initializes a Three.js scene and renders a shader-based interactive effect on a plane. The primary features include:
- Point Cloud Rendering: A plane is converted into a point cloud and animated using shaders.
- Mouse Interaction: A displacement texture captures mouse movements, affecting the shader animation.
- Custom Shaders: A vertex shader modifies the position of points dynamically, while a fragment shader controls the glow effect.
- Real-time Updates: The rendering adjusts dynamically with window resizing.
1. Implementing Mouse Interaction with Raycasting
Displacement Texture for Interactivity
A canvas is created to store a displacement texture, which captures mouse movement and influences the shader animation.
let displacement = {};
displacement.canvas = document.createElement("canvas");
displacement.canvas.width = 128;
displacement.canvas.height = 128;
displacement.context = displacement.canvas.getContext("2d");
displacement.texture = new THREE.CanvasTexture(displacement.canvas);
Raycasting to Track Mouse Position
A raycaster detects where the mouse intersects with an invisible plane (interactivePlane). This allows for smooth tracking of user movement.
window.addEventListener("pointermove", (e) => {
displacement.screenMouse.x = (e.clientX / sizes.width) * 2 - 1;
displacement.screenMouse.y = -(e.clientY / sizes.height) * 2 + 1;
});
2. Creating the Shader-Based Point Cloud
Instead of rendering a standard plane, we transform it into a point cloud using GLSL shaders. This involves:
- Vertex Shader: Alters vertex positions dynamically.
- Fragment Shader: Defines the color and glow effect of each point.
mesh = new THREE.Points(
new THREE.PlaneGeometry(10, 10, 64, 64),
new THREE.ShaderMaterial({
vertexShader: vShader,
fragmentShader: fShader,
uniforms: {
uTime: new THREE.Uniform(0),
uDisplacementTexture: new THREE.Uniform(displacement.texture),
uResolution: new THREE.Uniform(new THREE.Vector2(sizes.width, sizes.height)),
uPicTexture: new THREE.Uniform(new THREE.TextureLoader().load("./wave.png")),
},
})
);
scene.add(mesh);
3. Writing the GLSL Shaders
Vertex Shader (interactiveV.glsl)
This shader deforms the plane’s points based on the displacement texture and randomized wave movements.
uniform sampler2D uDisplacementTexture;
uniform float uTime;
attribute float aIntensity;
attribute float aAngle;
varying vec3 vColor;
void main() {
vec3 newPos = position;
float displacementIntensity = texture(uDisplacementTexture, uv).r;
displacementIntensity = smoothstep(0.1, 0.3, displacementIntensity);
vec3 displacement = vec3(cos(aAngle) * 0.2, sin(aAngle) * 0.2, 1.0);
displacement *= normalize(displacement);
displacement *= displacementIntensity * aIntensity * 3.0;
newPos += displacement;
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(newPos, 1.0);
vColor = vec3(pow(texture(uPicTexture, uv).r, 2.0));
gl_PointSize = 0.15 * texture(uPicTexture, uv).r * 500.0 / -viewPosition.z;
}
Fragment Shader (interactiveF.glsl)
This shader applies a glow effect to each point, discarding pixels outside the defined radius.
varying vec3 vColor;
void main() {
vec2 uv = gl_PointCoord;
float distToCenter = distance(uv, vec2(0.5));
if (distToCenter > 0.5) discard;
vec3 glowColor = vColor * 1.5;
gl_FragColor = vec4(glowColor, 1.0);
}
4. Animating the Scene
The animate() function ensures smooth updates:
function animate() {
let elapsedTime = clock.getElapsedTime();
control.update();
displacement.raycaster.setFromCamera(displacement.screenMouse, camera);
let intersects = displacement.raycaster.intersectObject(displacement.interactivePlane);
if (intersects.length) {
let uv = intersects[0].uv;
displacement.canvasMouse.set(uv.x * displacement.canvas.width, (1 - uv.y) * displacement.canvas.height);
}
displacement.texture.needsUpdate = true;
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
Conclusion
This article demonstrates how to create an interactive Three.js scene using raycasting, displacement textures, and custom shaders. By capturing user input and dynamically modifying shader attributes, we achieve a visually engaging and interactive experience.
Key Takeaways: ✔ Implementing custom shaders for interactive effects
✔ Utilizing raycasting for precise mouse tracking
✔ Modifying vertex attributes for dynamic geometry deformation
✔ Handling window resizing to maintain responsiveness
This approach can be expanded to more complex effects, such as fluid simulations, interactive particles, and real-time generative art.