Creating an Interactive Shader-Based Visualization in Three.js

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.

Leave a comment

Your email address will not be published. Required fields are marked *