14.4 C
New York
Wednesday, November 26, 2025

Creating Wavy Infinite Carousels in React Three Fiber with GLSL Shaders




Free course advice: Grasp JavaScript animation with GSAP by means of 34 free video classes, step-by-step tasks, and hands-on demos. Enroll now →

After coming throughout numerous 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 methods whereas making one thing that may be simply reused in different tasks.


Free GSAP 3 Express Course


Study fashionable internet animation utilizing GSAP 3 with 34 hands-on video classes and sensible tasks — good for all ability ranges.


Test it out

Starter mission

The bottom mission 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 pictures 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.

Initially, let’s create our vertex.glsl and fragment.glsl information:

// 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 perform 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 airplane. This can be very helpful later if we need to change our airplane sizes with out distorting the photographs.

Now that our shaders are prepared, let’s create our <GLImage> 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<THREE.Mesh, GLImageProps>(
  (
    {
      imageUrl,
      scale,
      geometry,
    },
    forwardedRef
  ) => {
    const localRef = useRef<THREE.Mesh>(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 (
      <mesh place={[0, 0, 0]} ref={imageRef} scale={scale}>
        <primitive object={geometry} connect="geometry" />
        <shaderMaterial {...shaderArgs} />
      </mesh>
    );
  }
);

export default GLImage;

Right here, we’re loading the picture texture with useTexture from @react-three/drei.

Then, we create our shader materials arguments (the uniforms and shader information utilized in it) and add it to our airplane mesh with shaderMaterial.

We should always get hold of one thing like this:

Displaying a picture on a airplane geometry with shaders

Displaying a number of pictures

Now that our base GLImage part is prepared, we’ll create a easy <Carousel> part that may map a picture checklist and show them in a column form, taking a picture dimension and a spot as props:

import { useMemo, useRef } from "react";
import * as THREE from "three";
import { IMAGE_LIST } from "../constants";
import GLImage from "./GLImage";

interface CarouselProps {
  place?: [number, number, number];
  imageSize: [number, number];
  hole: quantity;
}

const Carousel = ({ place, imageSize, hole }: CarouselProps) => {
  const imageRefs = useRef<THREE.Mesh[]>([]);

  const planeGeometry = useMemo(() => {
    return new THREE.PlaneGeometry(1, 1, 16, 16);
  }, []);

  return (
    <group place= [0, 0, 0]>
      {IMAGE_LIST.map((url, index) => (
        <GLImage
          key={index}
          imageUrl={url}
          scale={[imageSize[0], imageSize[1], 1]}
          geometry={planeGeometry}
          place={[0, index * (imageSize[1] + hole), 0]}
          ref={(el) => {
            if (el) imageRefs.present[index] = el;
          }}
        />
      ))}
    </group>
  );
};

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 pictures appear to be this:

Column format for our pictures

Infinite impact on scroll

For the infinite scroll impact, we’re going to maneuver all our planes alongside the Y-axis primarily based on the wheel velocity (utilizing Lenis in my case for simplicity). On every body, we apply a modulo perform to the airplane 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() perform right here is used to wrap every airplane again into the legitimate vary in order that, at any time when a airplane strikes too far up or down, its place is recalculated and it re-enters the loop seamlessly, retaining the carousel infinite:

perform 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() perform 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 keeping with 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 would like the planes to be displaced in a curved form in keeping with 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 keeping with 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 “high” 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 overlook 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: ,
      uCurveFrequency:  0 ,
    },
    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 one of the best values for the curveStrength and curveFrequency of the carousel.

Enjoying round and going additional

Now that we’ve got an simply reusable and tweakable <Carousel> part, we are able to play with it, add extra of them within the scene, change the wheel route, the wheel pace and so forth.

Listed below are some examples of what you are able to do with it:

Examples obtainable within the demo

If you wish to go additional, you’ll be able to strive including some noise displacement to the fragment shader primarily based on the wheel velocity. This might give your carousel a extra natural really feel and can also be an ideal train.
You can too add a route prop to the Carousel part to create a horizontal carousel as an alternative of a vertical one, like I did in among the examples.

Lastly, in the event you’d wish to see extra of my work, be certain to observe me on X or Linkedin. I submit all my tasks and experiments there.

Thanks for studying! 🙂



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles