After coming throughout varied forms of picture reveal results on X created by some friends, I made a decision to provide it a try to create my very own. The concept was to apply R3F and shader methods whereas making one thing that could possibly be simply reused in different tasks.
Word: You will discover the code for all the steps as branches within the following Github repo.
Starter Venture
The bottom challenge is an easy ViteJS React utility with an R3F Canvas, together with the next packages put in:
three // ThreeJS & R3F packages
@react-three/fiber
@react-three/drei
movement // Beforehand FramerMotion
leva // So as to add tweaks to our shader
vite-plugin-glsl // For vite to work with .glsl recordsdata
Now that we’re all set, we will begin writing our first shader.
Making a Easy Picture Shader
Initially, we’re going to create our vertex.glsl
& fragment.glsl
in 2 separate recordsdata like this:
// vertex.glsl
various vec2 vUv;
void fundamental()
{
// FINAL POSITION
gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
// VARYINGS
vUv = uv;
}
And our fragment.glsl
appears to be like like this:
uniform sampler2D uTexture;
various vec2 vUv;
void fundamental()
{
// Apply texture
vec3 textureColor = texture2D(uTexture, vUv).rgb;
// FINAL COLOR
gl_FragColor = vec4(textureColor, 1.0);
}
Right here, we’re passing the UV’s of our mesh to the fragment shader and use them within the texture2D
perform to use the picture texture to our fragments.
Now that the shader recordsdata are created, we will create our fundamental element:
import { shaderMaterial, useAspect, useTexture } from "@react-three/drei";
import { lengthen } from "@react-three/fiber";
import { useRef } from "react";
import * as THREE from "three";
import imageRevealFragmentShader from "../shaders/imageReveal/fragment.glsl";
import imageRevealVertexShader from "../shaders/imageReveal/vertex.glsl";
const ImageRevealMaterial = shaderMaterial(
{
uTexture: new THREE.Texture(),
},
imageRevealVertexShader,
imageRevealFragmentShader,
(self) => {
self.clear = true;
}
);
lengthen({ ImageRevealMaterial });
const RevealImage = ({ imageTexture }) => {
const materialRef = useRef();
// LOADING TEXTURE & HANDLING ASPECT RATIO
const texture = useTexture(imageTexture, (loadedTexture) => {
if (materialRef.present) {
materialRef.present.uTexture = loadedTexture;
}
});
const { width, top } = texture.picture;
const scale = useAspect(width, top, 0.25);
return (
<mesh scale={scale}>
<planeGeometry args={[1, 1, 32, 32]} />
<imageRevealMaterial connect="materials" ref={materialRef} />
</mesh>
);
};
export default RevealImage;
Right here, we create the bottom materials utilizing shaderMaterial
from React Three Drei, after which lengthen it with R3F to make use of it in our element.
Then, we load the picture handed as a prop and deal with the ratio of it because of the useAspect
hook from React-Three/Drei.
We should always receive one thing like this:

Including the bottom impact
(Particular point out to Bruno Simon for the inspiration on this one).
Now we have to add a radial noise impact that we’re going to make use of to disclose our picture, to do that, we’re going to make use of a Perlin Noise Perform and blend it with a radial gradient identical to this:
// fragment.glsl
uniform sampler2D uTexture;
uniform float uTime;
various vec2 vUv;
#embrace ../contains/perlin3dNoise.glsl
void fundamental()
{
// Displace the UV
vec2 displacedUv = vUv + cnoise(vec3(vUv * 5.0, uTime * 0.1));
// Perlin noise
float energy = cnoise(vec3(displacedUv * 5.0, uTime * 0.2 ));
// Radial gradient
float radialGradient = distance(vUv, vec2(0.5)) * 12.5 - 7.0;
energy += radialGradient;
// Clamp the worth from 0 to 1 & invert it
energy = clamp(energy, 0.0, 1.0);
energy = 1.0 - energy;
// Apply texture
vec3 textureColor = texture2D(uTexture, vUv).rgb;
// FINAL COLOR
// gl_FragColor = vec4(textureColor, 1.0);
gl_FragColor = vec4(vec3(energy), 1.0);
}
You will discover the Perlin Noise Perform right here or within the code repository right here.
The uTime
is used to switch the noise form in time and make it really feel extra energetic.
Now we simply want to switch barely our element to move the time to our materials:
const ImageRevealMaterial = shaderMaterial(
{
uTexture: new THREE.Texture(),
uTime: 0,
},
...
);
// Within the element
useFrame(({ clock }) => {
if (materialRef.present) {
materialRef.present.uTime = clock.elapsedTime;
}
});
The useFrame
hook from R3F runs on every body and gives us a clock that we will use to get the elapsed time for the reason that render of our scene.
Right here’s the outcome we get now:



You perhaps see it coming, however we’re going to make use of this on our Alpha channel after which scale back or improve the radius of our radial gradient to point out/conceal the picture.
You may strive it your self by including the picture to the RGB channels of our ultimate coloration within the fragment shader and the energy to the alpha channel. It is best to get one thing like this:
Now, how can we animate the radius of the impact.
Animating the impact
To do that, it’s fairly easy truly, we’re simply going so as to add a brand new uniform uProgress
in our Fragment Shader that can go from 0 to 1 and use it to have an effect on the radius:
// fragment.glsl
uniform float uProgress;
...
// Radial gradient
float radialGradient = distance(vUv, vec2(0.5)) * 12.5 - 7.0 * uProgress;
...
// Opacity animation
float opacityProgress = smoothstep(0.0, 0.7, uProgress);
// FINAL COLOR
gl_FragColor = vec4(textureColor, energy * opacityProgress);
We’re additionally utilizing the progress so as to add a bit of opacity animation in the beginning of the impact to cover our picture fully at first.
Now we will move the brand new uniform to our materials and use Leva to regulate the progress of the impact:
const ImageRevealMaterial = shaderMaterial(
{
uTexture: new THREE.Texture(),
uTime: 0,
uProgress: 0,
},
...
);
...
// LEVA TO CONTROL REVEAL PROGRESS
const { revealProgress } = useControls({
revealProgress: { worth: 0, min: 0, max: 1 },
});
// UPDATING UNIFORMS
useFrame(({ clock }) => {
if (materialRef.present) {
materialRef.present.uTime = clock.elapsedTime;
materialRef.present.uProgress = revealProgress;
}
});
Now you must have one thing like this:
We will animate the progress in a whole lot of alternative ways. To maintain it easy, we’re going to create a button in our app that can animate a revealProgress
prop of our element utilizing movement/react
(beforehand Framer Movement):
// App.jsx
// REVEAL PROGRESS ANIMATION
const [isRevealed, setIsRevealed] = useState(false);
const revealProgress = useMotionValue(0);
const handleReveal = () => {
animate(revealProgress, isRevealed ? 0 : 1, {
length: 1.5,
ease: "easeInOut",
});
setIsRevealed(!isRevealed);
};
...
<Canvas>
<RevealImage
imageTexture="./img/texture.webp"
revealProgress={revealProgress}
/>
</Canvas>
<button
onClick={handleReveal}
className="yourstyle"
>
SHOW/HIDE
</button>
We’re utilizing a MotionValue from movement/react and passing it to our element props.
Then we merely have to make use of it within the useFrame
hook like this:
// UPDATING UNIFORMS
useFrame(({ clock }) => {
if (materialRef.present) {
materialRef.present.uTime = clock.elapsedTime;
materialRef.present.uProgress = revealProgress.get();
}
});
It is best to receive one thing like this:
Including displacement
Another factor I love to do so as to add extra “life” to the impact is to displace the vertices, making a wave synchronized with the progress of the impact. It’s truly fairly easy, as we solely have to barely modify our vertex shader:
uniform float uProgress;
various vec2 vUv;
void fundamental()
{
vec3 newPosition = place;
// Calculate the space to the middle of our airplane
float distanceToCenter = distance(vec2(0.5), uv);
// Wave impact
float wave = (1.0 - uProgress) * sin(distanceToCenter * 20.0 - uProgress * 5.0);
// Apply the wave impact to the place Z
newPosition.z += wave;
// FINAL POSITION
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
// VARYINGS
vUv = uv;
}
Right here, the depth and place of the wave is determined by the uProgress
uniform.
We should always receive one thing like this:
It’s fairly delicate however it’s the sort of element that makes the distinction for my part.
Going additional
And right here it’s! You may have your reveal impact prepared! I hope you had some enjoyable creating this impact. Now you may strive varied issues with it to make it even higher and apply your shader expertise. For instance, you may attempt to add extra tweaks with Leva to personalize it as you want, and you can too attempt to animate it on scroll, make the airplane rotate, and many others.
I’ve made a bit of instance of what you are able to do with it, that you could find on my Twitter account right here.
Thanks for studying! 🙂