In this tutorial, we will learn how to create an engaging bulge effect on text using React Three Fiber.
Over the past few weeks, I have been experimenting with combining 3D and 2D elements to create interesting effects. Today, I will walk you through the process of replicating a bulge effect on text.
To simplify the process and maintain a structured approach to combining HTML with 3D, we will utilize React Three Fiber.
Let’s get started!
Setup
First, let’s set up our 3D scene by creating:
– a plane (where our text bulge effect will be displayed).
– our HTML text element.
With drei, you can directly insert HTML elements inside your Scene components using the HTML component. This is useful in our case as we will need access to our HTML within our 3D scene.
It is also important to wrap the title within a single div that spans the entire viewport width and height. Similarly, for the plane, with R3F’s useThree() hook, we can easily retrieve the viewport sizes.
For now, let’s set the plane’s opacity to 0 to see our HTML element:
“`jsx
function Scene() {
const { viewport } = useThree();
return (
<>
WILL
WE
MEET ?
>
);
}
“`
Converting HTML to Texture
Now, the main trick for this effect is to convert our div into a texture that we will apply to our plane. For that, we will use the html2canvas library to generate an image from our DOM element and then convert it into a texture.
To streamline this process for future projects, let’s create a custom hook named useDomToCanvas.
“`jsx
const useDomToCanvas = (domEl) => {
const [texture, setTexture] = useState();
useEffect(() => {
if (!domEl) return;
const convertDomToCanvas = async () => {
const canvas = await html2canvas(domEl, { backgroundColor: null });
setTexture(new THREE.CanvasTexture(canvas));
};
convertDomToCanvas();
}, [domEl]);
return texture;
};
“`
We can also enhance the hook to handle resizing, as the div may remain behind the canvas. We simply need to recall the function when the window is resized. To prevent excessive draw calls, let’s incorporate a debounce.
“`jsx
const debouncedResize = debounce(() => {
convertDomToCanvas();
}, 100);
window.addEventListener(“resize”, debouncedResize);
“`
Implementing the Bulge Effect
Now, to achieve the bulge effect, we will use shader programs to access the vertices of the plane. Although shader programming might seem difficult, don’t worry – in our case, it will be a simple effect. We will break it down into three small steps so you can easily follow what’s happening.
For an introduction to shaders, you can also refer to the Lewis Lepton YouTube series.
First, let’s use a shaderMaterial as the material for the plane and create our fragment and vertex shaders.
“`jsx
// Scene.jsx
…
“`
Step 1: First, the idea is to draw a circle on our plane. To achieve this, we will utilize the UV coordinates and the GLSL distance function. Let’s encapsulate the code into a function to enhance clarity.
“`glsl
// fragment.glsl
…
float circle(vec2 uv, vec2 circlePosition, float radius) {
float dist = distance(circlePosition, uv);
return 1. – smoothstep(0.0, radius, dist);
}
void main() {
float circleShape = circle(vUv, vec2(0.5), 0.5);
gl_FragColor = vec4(vec3(circleShape), 1.);
}
“`
Step 2: Now, we will dynamically adjust the circle’s origin position based on mouse movement. With R3F, accessing normalized mouse positions is straightforward using useFrame(). By passing mouse positions as uniforms to the fragment shader, we will observe the circle’s movement.
“`jsx
// Scene.jsx
…
useFrame((state, delta) => {
const mouse = state.mouse;
materialRef.current.uniforms.uMouse.value = mouse;
});
// fragment.glsl
…
void main() {
vec2 mousePositions = uMouse * 0.5 + 0.5;
float circleShape = circle(vUv, mousePositions, 0.5);
gl_FragColor = vec4(vec3(circleShape), 1.);
}
“`
Step 3: Now, we just need to call the circle function in the vertex shader and adjust the z position based on the circle. And… voilà! We have our bulge effect! (Also, don’t forget to replace the texture in the fragment shader.)
“`glsl
// vertex.glsl
void main() {
vec3 newPosition = position;
// Elevation
vec2 mousePositions = uMouse * 0.5 + 0.5;
float circleShape = circle(uv, mousePositions, 0.2);
float intensity = 0.7;
newPosition.z += circleShape * intensity;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
}
// fragment.glsl
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 finalTexture = texture2D(uTexture, vUv);
gl_FragColor = vec4(finalTexture);
}
“`
Adding Lighting
To enhance the 3D appearance, let’s incorporate lighting effects. While coding custom lighting effects within the fragment shader can be complex, we can leverage existing libraries like customShaderMaterial. With customShaderMaterial, we will seamlessly integrate standardMaterial and a pointLight to achieve stunning shading effects.
“`jsx
// Scene.jsx
Congratulations! You have successfully implemented the effect.
I have included a GUI within the repository so you can play with positions and light color. I would love to see your creations and how you build upon this demo. Feel free to share your experiments with me on Twitter!