Yannisyou

Unlock Dynamic Web Experiences: Building Wavy Infinite Carousels with React Three Fiber and GLSL Shaders

The web is constantly evolving, moving beyond static pages to deliver rich, interactive, and immersive user experiences. As developers, we’re always looking for new ways to captivate our audience and make our applications stand out. One incredibly powerful technique is the creation of dynamic, visually stunning 3D elements right within the browser.

Today, we’re diving deep into an exciting project: building a “Wavy Infinite Carousel” using the combined power of React Three Fiber and GLSL Shaders. Imagine a carousel that not only scrolls endlessly but also features a mesmerizing, undulating wave effect, adding a layer of sophisticated visual appeal. This guide on CodesHours will walk you through everything you need to know, from setting up your project to implementing complex shader logic, making it accessible for both beginners and intermediate developers.

What is React Three Fiber? Your Gateway to 3D Web

Before we jump into the wavy magic, let’s understand our primary tool: React Three Fiber (R3F). In essence, React Three Fiber is a React renderer for Three.js. Three.js is a powerful JavaScript library that makes it easier to display 3D graphics on a web browser using WebGL. While Three.js is fantastic, integrating it directly into a React application can sometimes feel a bit clunky due to its imperative nature.

This is where R3F shines. It allows you to build 3D scenes declaratively with reusable, self-contained React components. Instead of directly manipulating Three.js objects, you describe your scene using JSX, much like building a standard React UI. This approach brings all the benefits of React’s component-based architecture, state management, and lifecycle methods to your 3D development, making it incredibly intuitive and efficient.

Demystifying GLSL Shaders: The Artist’s Brush for 3D

To achieve our captivating wavy effect, we’ll turn to GLSL (OpenGL Shading Language). If React Three Fiber provides the canvas and the structure for our 3D scene, GLSL shaders are the artist’s brush, allowing us to paint and manipulate every pixel and vertex with incredible precision. Shaders are small programs that run directly on your computer’s graphics processing unit (GPU), making them incredibly fast for visual computations.

There are two primary types of shaders we’ll be concerned with:

  • Vertex Shaders: These run for each vertex (corner point) of your 3D model. They are responsible for determining the final position of each vertex in 3D space. This is where we’ll implement our “wavy” logic by programmatically altering the `y` or `z` position of each vertex based on time or other factors.
  • Fragment Shaders: These run for each “fragment” (essentially a pixel) that makes up the surface of your 3D model. They determine the final color of each pixel, taking into account lighting, textures, and other visual properties. For our wavy carousel, a simple fragment shader will suffice to give our elements a basic color.

By directly manipulating vertices with GLSL, we gain unparalleled control over the geometry, allowing us to create dynamic, procedural animations that would be challenging to achieve with traditional JavaScript alone.

Setting Up Your Project: The Foundation for Innovation

Before we write our first line of 3D code, let’s get our React project set up. We’ll assume you have Node.js and npm/yarn installed on your system. If not, make sure to install them first.

1. Create a New React Project

You can use Create React App or Vite. For this guide, we’ll use Vite for its speed:

npm create vite@latest my-wavy-carousel --template react-ts
cd my-wavy-carousel
npm install

2. Install React Three Fiber and Dependencies

Next, we need to install the core libraries:

npm install three @react-three/fiber @react-three/drei
  • three: The underlying Three.js library.
  • @react-three/fiber: The React renderer for Three.js.
  • @react-three/drei: A collection of useful helpers and abstractions for R3F, simplifying common tasks.

3. Basic Canvas Setup

Now, let’s set up our main 3D canvas. Replace the content of your src/App.tsx or a new component file:


import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';

function Scene() {
  return (
    <>
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      {/* Your carousel components will go here */}
      <OrbitControls />
    </>
  );
}

function App() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas camera={{ position: [0, 0, 5] }}>
        <Scene />
      </Canvas>
    </div>
  );
}

export default App;

This sets up a full-screen canvas, adds some basic lighting, and includes OrbitControls, which allows you to pan, zoom, and rotate the scene with your mouse – invaluable for development.

Crafting the Carousel Structure in React Three Fiber

Our carousel will consist of multiple items, each a 3D plane. We’ll arrange them in a linear fashion initially, and then use shader magic to make them wavy and infinite. Let’s create a simple component for a single carousel item.

Creating a Carousel Item Component

Create a file like src/CarouselItem.tsx:


import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';

interface CarouselItemProps {
  position: [number, number, number];
  index: number;
  totalItems: number;
}

function CarouselItem({ position, index, totalItems }: CarouselItemProps) {
  const meshRef = useRef<THREE.Mesh>(null);

  // We'll pass these uniforms to our shader later
  const uniforms = useRef({
    uTime: { value: 0 },
    uFrequency: { value: 5.0 },
    uAmplitude: { value: 0.2 },
  }).current;

  useFrame(({ clock }) => {
    if (meshRef.current) {
      uniforms.uTime.value = clock.getElapsedTime();
    }
  });

  return (
    <mesh ref={meshRef} position={position}>
      <planeGeometry args={[2, 2, 64, 64]} /> {"/* 64x64 segments for wavy detail */"}
      <shaderMaterial
        uniforms={uniforms}
        vertexShader={/* Our vertex shader will go here */`void main() { gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`}
        fragmentShader={/* Our fragment shader will go here */`void main() { gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0); }`}
        side={THREE.DoubleSide}
      />
    </mesh>
  );
}

export default CarouselItem;

Notice we’re using planeGeometry args={[2, 2, 64, 64]}. The last two arguments, 64, 64, are crucial. They define the number of width and height segments for the plane. A higher number of segments means more vertices, which in turn allows our vertex shader to create a smoother, more detailed wave effect.

Bringing the Waves: The Power of GLSL Shaders

Now for the exciting part! Let’s define our GLSL shaders within the CarouselItem.tsx component. We’ll replace the placeholder shader strings.

Vertex Shader (vertexShader)

This shader will manipulate the position of each vertex to create the wavy motion. We’ll use uTime to animate the wave, and uFrequency and uAmplitude to control its appearance.


const vertexShader = `
  uniform float uTime;
  uniform float uFrequency;
  uniform float uAmplitude;

  void main() {
    vec3 newPosition = position;
    float waveEffect = sin(newPosition.x * uFrequency + uTime) * uAmplitude;
    newPosition.z += waveEffect; // Modify Z-axis for a wave along X

    gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
  }
`;

Here’s what’s happening:

  • We get the original position of the vertex.
  • We calculate a waveEffect using the sine function, which creates a smooth, oscillating value. This value depends on the vertex’s x coordinate (for a wave across the plane’s width) and uTime for animation.
  • We add this waveEffect to the z coordinate of the newPosition. This moves vertices forward and backward along the Z-axis, creating the wave.
  • Finally, gl_Position is calculated, transforming our 3D vertex into its final screen position.

Fragment Shader (fragmentShader)

For now, our fragment shader will simply color the plane. You can expand this to add textures, lighting, or more complex effects later.


const fragmentShader = `
  void main() {
    gl_FragColor = vec4(0.2, 0.7, 1.0, 1.0); // A nice blue color
  }
`;

Now, update your CarouselItem component to use these shader strings:


// ... inside CarouselItem component ...

  return (
    <mesh ref={meshRef} position={position}>
      <planeGeometry args={[2, 2, 64, 64]} />
      <shaderMaterial
        uniforms={uniforms}
        vertexShader={vertexShader}
        fragmentShader={fragmentShader}
        side={THREE.DoubleSide}
      />
    </mesh>
  );

Making it Infinite: The Seamless Loop

An “infinite” carousel isn’t truly infinite; it just creates the illusion. We achieve this by dynamically repositioning items as they move off-screen. In our case, as an item scrolls past a certain point, we instantly move it to the other end of the carousel.

Managing Carousel Items in Scene

Let’s go back to our Scene component and render multiple CarouselItems. We’ll use the useFrame hook to update their positions and manage the infinite loop logic.


import { useRef, useState } from 'react';
import { Canvas, useFrame } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import CarouselItem from './CarouselItem'; // Import our item component

const ITEM_COUNT = 5;
const ITEM_SPACING = 3;
const CAROUSEL_SPEED = 0.02; // Adjust speed as needed

function Scene() {
  const [items, setItems] = useState(Array.from({ length: ITEM_COUNT }, (_, i) => i));

  // Logic for the infinite loop
  useFrame(() => {
    setItems(prevItems => {
      return prevItems.map((itemIndex, i) => {
        // Calculate current X position based on its original index and time
        let currentX = (i * ITEM_SPACING) - (CAROUSEL_SPEED * performance.now() / 100); // Simple time-based movement

        // Loop logic: if item moves too far left, move it to the right end
        if (currentX < -(ITEM_SPACING * (ITEM_COUNT / 2))) { // Adjust threshold as needed
          currentX += ITEM_SPACING * ITEM_COUNT;
        }

        // You might need a more robust looping logic depending on your design
        // This is a simplified example to show the concept.

        return itemIndex; // For this simple example, we just map and reposition directly in JSX
      });
    });
  });

  return (
    <>
      <ambientLight intensity={0.5} />
      <pointLight position={[10, 10, 10]} />
      {items.map((_, i) => {
        // Calculate initial position, then apply the dynamic movement
        const initialX = (i - Math.floor(ITEM_COUNT / 2)) * ITEM_SPACING;
        let currentX = initialX - (CAROUSEEL_SPEED * performance.now() / 100); // Global movement

        // Loop logic to wrap items
        if (currentX < -(ITEM_SPACING * ITEM_COUNT / 2)) {
            currentX += ITEM_SPACING * ITEM_COUNT;
        }
         if (currentX > (ITEM_SPACING * ITEM_COUNT / 2)) {
            currentX -= ITEM_SPACING * ITEM_COUNT; // Handle moving past the right edge too
        }

        return (
          <CarouselItem
            key={i}
            index={i}
            totalItems={ITEM_COUNT}
            position={[currentX, 0, 0]}
          />
        );
      })}
      <OrbitControls />
    </>
  );
}

// ... rest of App component ...

This simplified example provides a basic understanding. In a real-world scenario, you’d refine the looping logic to ensure seamless transitions and handle item data more robustly. The key is to calculate the item’s position relative to its neighbours and wrap it around when it goes out of view.

Adding Polish: Interactivity and Performance Considerations

While a continuously animating carousel is cool, adding user interaction makes it truly engaging. You could implement drag-to-scroll functionality by capturing mouse or touch events and adjusting the carousel’s global offset based on user input. React Three Fiber’s event system makes this quite straightforward.

Performance is also paramount in 3D web applications:

  • Instancing: If you have many identical carousel items, consider using Three.js instancing (often abstracted by @react-three/drei components) to render them efficiently, as it drastically reduces draw calls.
  • Shader Optimization: Keep your GLSL shaders as lean as possible. Avoid complex calculations if simpler ones achieve the desired visual effect.
  • Geometry Simplification: While we used 64×64 segments for our wavy plane, assess if fewer segments would still look good on smaller devices or for less prominent elements.
  • Culling: Ensure items outside the camera’s view are not rendered. Three.js handles this automatically to a large extent, but custom culling logic might be necessary for very complex scenes.

Conclusion: Unleash Your Creativity

You’ve now journeyed through the exciting process of creating a wavy infinite carousel using React Three Fiber and GLSL Shaders. From understanding the foundational tools to implementing complex vertex manipulations and crafting an infinite loop, you’ve gained valuable insights into modern 3D web development.

This technique opens up a world of possibilities for creating unique, dynamic interfaces that truly stand out. Whether it’s for product showcases, portfolio displays, or artistic installations, the combination of React’s component-based power and GLSL’s low-level control allows for unparalleled creative freedom. Continue experimenting with different wave patterns, colors, textures, and interactivity to build your next show-stopping web experience. The 3D web is waiting for your touch!

Subscribe

Join our community of 3 million people and get updated every week We have a lot more just for you! Lets join us now