Free course advice: Grasp JavaScript animation with GSAP via 34 free video classes, step-by-step tasks, and hands-on demos. Enroll now →
After coming throughout varied infinite carousel results on X created by some friends, I made a decision to provide it a attempt to create my very own. The concept right here is to apply R3F and shader strategies whereas making one thing that may be simply reused in different tasks.
Starter challenge
The bottom challenge is a straightforward Vite+React+TS software with an R3F Canvas, together with the next packages put in:
three
@react-three/fiber
@react-three/drei
Now that we’re all set, we are able to begin writing our first shader.
Creating the GLImage part
We need to show our photographs on planes, in order that we are able to put them in our R3F scene and play with them, add displacement results with shader and so forth.
To start with, let’s create our vertex.glsl and fragment.glsl recordsdata:
// vertex.glsl
various vec2 vUv;
void predominant() {
vec3 pos = place;
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
// VARYINGS
vUv = uv;
}
// fragment.glsl
precision highp float;
uniform sampler2D uTexture;
uniform vec2 uPlaneSizes;
uniform vec2 uImageSizes;
various vec2 vUv;
void predominant() {
vec2 ratio = vec2(
min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
);
vec2 uv = vec2(
vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
);
vec4 finalColor = texture2D(uTexture, uv);
gl_FragColor = finalColor;
}
Right here, we’re passing the UVs of our mesh to the fragment shader and utilizing them within the texture2D operate to use the picture texture to our fragments.
We’re additionally utilizing two uniforms : uPlaneSizes and uImageSizes, which permit us to recalculate the UV coordinates and have an “object-fit cowl like” impact on our aircraft. This can be very helpful later if we need to change our aircraft sizes with out distorting the photographs.
Now that our shaders are prepared, let’s create our part:
import { useTexture } from "@react-three/drei";
import { forwardRef, useMemo, useRef } from "react";
import * as THREE from "three";
import imageFragmentShader from "../shaders/picture/fragment.glsl?uncooked";
import imageImageVertexShader from "../shaders/picture/vertex.glsl?uncooked";
interface GLImageProps {
imageUrl: string;
scale: [number, number, number];
geometry: THREE.PlaneGeometry;
}
const GLImage = forwardRef(
(
{
imageUrl,
scale,
geometry,
},
forwardedRef
) => {
const localRef = useRef(null);
const imageRef = forwardedRef || localRef;
const texture = useTexture(imageUrl);
const imageSizes = useMemo(() => {
if (!texture) return [1, 1];
return [texture.image.width, texture.image.height];
}, [texture]);
const shaderArgs = useMemo(
() => ({
uniforms: {
uTexture: { worth: texture },
uPlaneSizes: { worth: new THREE.Vector2(scale[0], scale[1]) },
uImageSizes: {
worth: new THREE.Vector2(imageSizes[0], imageSizes[1]),
},
},
vertexShader: imageImageVertexShader,
fragmentShader: imageFragmentShader,
}),
[texture, scale, imageSizes]
);
return (
);
}
);
export default GLImage;
Here, we’re loading the image texture with useTexture from @react-three/drei.
Then, we create our shader material arguments (the uniforms and shader files used in it) and add it to our plane mesh with shaderMaterial.
We should obtain something like this:

Displaying multiple images
Now that our base GLImage component is ready, we will create a simple component that will map an image list and display them in a column shape, taking an image size and a gap as props:
import { useMemo, useRef } from "react";
import * as THREE from "three";
import { IMAGE_LIST } from "../constants";
import GLImage from "./GLImage";
interface CarouselProps {
position?: [number, number, number];
imageSize: [number, number];
hole: quantity;
}
const Carousel = ({ place, imageSize, hole }: CarouselProps) => {
const imageRefs = useRef([]);
const planeGeometry = useMemo(() => {
return new THREE.PlaneGeometry(1, 1, 16, 16);
}, []);
return (
{IMAGE_LIST.map((url, index) => (
{
if (el) imageRefs.present[index] = el;
}}
/>
))}
);
};
export default Carousel;
Right here, we’re mapping our picture checklist and utilizing the index of every picture to set a brand new prop named place in order that our photographs appear to be this:

Infinite impact on scroll
For the infinite scroll impact, we’re going to maneuver all our planes alongside the Y-axis based mostly on the wheel velocity (utilizing Lenis in my case for simplicity). On every body, we apply a modulo operate to the aircraft positions to wrap them again to the highest or backside relying on their present Y place:
// Carousel.tsx
const totalHeight = IMAGE_LIST.size * hole + IMAGE_LIST.size * imageSize[1];
useFrame(() => {
imageRefs.present.forEach((ref) => {
if (!ref) return;
ref.place.y =
mod(ref.place.y + totalHeight / 2, totalHeight) - totalHeight / 2;
});
});
useLenis(({ velocity }) => {
imageRefs.present.forEach((ref) => {
if (ref) {
ref.place.y -= velocity * 0.005;
}
});
});
The mod() operate right here is used to wrap every aircraft again into the legitimate vary in order that, every time a aircraft strikes too far up or down, its place is recalculated and it re-enters the loop seamlessly, maintaining the carousel infinite:
operate mod(n: quantity, m: quantity) {
return ((n % m) + m) % m;
}
We should always now have one thing like this:
Displacement impact on the planes
First, we would like our planes to stretch vertically relying on the wheel velocity. To do that, we’re going to add a uScrollSpeed uniform to the fabric arguments and replace this uniform on scroll in our Carousel part:
// GLImage.tsx
const shaderArgs = useMemo(
() => ({
uniforms: {
// ...
uScrollSpeed: { worth: 0.0 },
},
vertexShader: imageImageVertexShader,
fragmentShader: imageFragmentShader,
}),
[texture, scale, imageSizes]
);
// Carousel.tsx
useLenis(({ velocity }) => {
imageRefs.present.forEach((ref) => {
if (ref) {
ref.place.y -= velocity * 0.005;
ref.materials.uniforms.uScrollSpeed.worth = velocity * 0.005;
}
});
});
Then in our vertex.glsl shader, we’re going to make use of this uniform and displace our vertex on the Y axis with PI and a sin() operate which is able to permit us to have that “rounded” displacement:
uniform float uScrollSpeed;
various vec2 vUv;
#outline PI 3.141592653
void predominant() {
vec3 pos = place;
// Y Displacement in response to the scroll pace
float yDisplacement = -sin(uv.x * PI) * uScrollSpeed;
pos.y += yDisplacement;
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
// VARYINGS
vUv = uv;
}
We should always now have one thing like this:
Wavy impact on the carousel
Now, let’s add a wavy impact to our carousel, I need the planes to be displaced in a curved form in response to their place in my 3D scene.
To do that, we’re going so as to add two extra uniforms to our shader and use them with the world place of our planes similar to this:
// vertex.glsl
uniform float uScrollSpeed;
uniform float uCurveStrength;
uniform float uCurveFrequency;
various vec2 vUv;
#outline PI 3.141592653
void predominant() {
vec3 pos = place;
vec3 worldPosition = (modelMatrix * vec4(place, 1.0)).xyz;
// X Displacement relying on the world place Y
float xDisplacement = uCurveStrength * cos(worldPosition.y * uCurveFrequency);
pos.x += xDisplacement;
pos.x -= uCurveStrength;
// Y Displacement in response to the scroll pace
float yDisplacement = -sin(uv.x * PI) * uScrollSpeed;
pos.y += yDisplacement;
gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
// VARYINGS
vUv = uv;
}
Right here, we’re utilizing a cosinus on the worldPosition.y of our planes, in order that the “prime” of the curve will at all times be on the heart of our canvas (as a result of when y=0, cos(y)=1).
And don’t neglect so as to add some props to the GLimage in addition to including the uniforms in our shader materials arguments:
// GLImage.tsx
const shaderArgs = useMemo(
() => ({
uniforms: {
// ...
uCurveStrength: 0 ,
uCurveFrequency: ,
},
vertexShader: imageImageVertexShader,
fragmentShader: imageFragmentShader,
}),
[texture, curveStrength, curveFrequency, scale, imageSizes]
);
We should always now have one thing like this:
Word : as a bonus, you’ll be able to add leva or every other tweaking library in an effort to tweak and discover the perfect values for the curveStrength and curveFrequency of the carousel.
Enjoying round and going additional
Now that we have now an simply reusable and tweakable part, we are able to play with it, add extra of them within the scene, change the wheel path, the wheel pace and so forth.
Listed here are some examples of what you are able to do with it:






If you wish to go additional, you’ll be able to attempt including some noise displacement to the fragment shader based mostly on the wheel velocity. This might give your carousel a extra natural really feel and can also be an important train.
It’s also possible to add a path prop to the Carousel part to create a horizontal carousel as an alternative of a vertical one, like I did in a number of the examples.
Lastly, should you’d wish to see extra of my work, ensure to observe me on X or Linkedin. I publish all my tasks and experiments there.
Thanks for studying! 🙂









