Ringkasan

“Wavy infinite carousel” menggabungkan React Three Fiber (R3F) untuk render 3D/WebGL dengan GLSL untuk mendistorsi kartu/gambar secara dinamis sehingga terasa hidup saat bergerak tak-berujung. Inti tekniknya: instancing untuk performa, vertex shader untuk gelombang, fragment shader untuk pewarnaan/tekstur, dan loop wrap untuk ilusi tak-berujung.

Konsep Dasar

  • Carousel tak-berujung: Deretan panel bergerak saling menyambung; saat panel melewati batas, posisinya “di-teleport” ke sisi lain.
  • Wavy (gelombang): Posisi vertex dimodifikasi sinus/cosinus menggunakan uTime, uAmplitude, dan uFrequency.
  • Instancing: Satu mesh direplikasi ribuan kali secara efisien; atribut kustom seperti aOffset memberi variasi fase gelombang.
  • Interaksi: Pointer/drag menambah modulasi (misalnya kecepatan atau amplitudo sesaat).

Struktur Arsitektur Mini

  1. Assets: Siapkan tekstur (atlas untuk irit drawcall) dan aktifkan mipmaps/anisotropy.
  2. R3F Canvas: Render scene; useFrame untuk animasi posisi dan waktu shader.
  3. InstancedMesh: Susun barisan panel; setMatrixAt + InstancedBufferAttribute untuk offset/fase.
  4. ShaderMaterial: Vertex shader = gelombang; fragment shader = sampling tekstur + efek ringan.
  5. Loop: Logika wrap di CPU (JS) atau di shader (modulus).

Contoh Shader (Ilustratif)

Shader berikut bersifat ilustratif (bukan salinan artikel sumber). Ubah sesuai kebutuhan produksi.

Vertex Shader (gelombang)

uniform float uTime;
uniform float uAmplitude;   // tinggi gelombang
uniform float uFrequency;   // kerapatan gelombang
uniform float uSpeed;       // kecepatan fase
attribute float aOffset;    // variasi per-instance
varying vec2 vUv;

void main() {
  vec3 p = position;
  float phase = (aOffset + p.x) * uFrequency + uTime * uSpeed;
  p.y += sin(phase) * uAmplitude;
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}

Fragment Shader (sampling & toning)

uniform sampler2D uMap;
uniform float uTint; // 0.0..1.0 untuk efek toning ringan
varying vec2 vUv;

void main() {
  vec4 tex = texture2D(uMap, vUv);
  vec3 tone = mix(tex.rgb, tex.rgb * vec3(0.95, 1.05, 1.10), uTint);
  gl_FragColor = vec4(tone, tex.a);
}

Contoh R3F (Ilustratif)

Contoh singkat: membuat baris panel instanced yang bergeser dan di-wrap. Gunakan loader tekstur sesuai kebutuhan (atlas/array).

// Carousel.tsx (ilustratif)
import * as THREE from "three";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { useMemo, useRef } from "react";

function WavyCarousel({ count = 20, spacing = 1.2 }) {
  const meshRef = useRef<THREE.InstancedMesh>(null);
  const matRef = useRef<THREE.ShaderMaterial>(null);
  const { clock } = useThree();

  // Posisi awal per panel dan offset fase
  const { matrices, offsets } = useMemo(() => {
    const dummy = new THREE.Object3D();
    const m = new Array(count);
    const o = new Float32Array(count);
    for (let i = 0; i < count; i++) {
      dummy.position.set(i * spacing, 0, 0);
      dummy.rotation.set(0, 0, 0);
      dummy.updateMatrix();
      m[i] = dummy.matrix.clone();
      o[i] = i * 0.37; // fase unik agar gelombang tidak seragam
    }
    return { matrices: m, offsets: o };
  }, [count, spacing]);

  // Buat atribut offset untuk instancing
  const offsetAttr = useMemo(() => new THREE.InstancedBufferAttribute(offsets, 1), [offsets]);

  // Parameter loop
  const totalWidth = (count - 1) * spacing;
  const speed = 0.6; // unit per detik

  useFrame((state, dt) => {
    const t = clock.getElapsedTime();
    if (matRef.current) {
      matRef.current.uniforms.uTime.value = t;
    }
    // Geser semua panel ke kiri, wrap saat melewati batas
    const dummy = new THREE.Object3D();
    for (let i = 0; i < count; i++) {
      dummy.matrix.copy(matrices[i]);
      dummy.position.set(
        ((dummy.position.x - speed * dt) % (totalWidth + spacing) + (totalWidth + spacing)) % (totalWidth + spacing),
        0,
        0
      );
      dummy.updateMatrix();
      meshRef.current!.setMatrixAt(i, dummy.matrix);
    }
    meshRef.current!.instanceMatrix.needsUpdate = true;
  });

  return (
    <instancedMesh ref={meshRef} args={[undefined as any, undefined as any, count]}>
      <planeGeometry args={[1, 0.6, 16, 16]}>
        <instancedBufferAttribute attach='attributes-aOffset' args={[offsetAttr.array, 1]} />
      </planeGeometry>
      <shaderMaterial
        ref={matRef}
        uniforms={{
          uTime: { value: 0 },
          uAmplitude: { value: 0.08 },
          uFrequency: { value: 2.5 },
          uSpeed: { value: 2.0 },
          uTint: { value: 0.15 },
          uMap: { value: new THREE.TextureLoader().load('/path/to/texture.jpg') }
        }}
        vertexShader={`uniform float uTime;uniform float uAmplitude;uniform float uFrequency;uniform float uSpeed;attribute float aOffset;varying vec2 vUv;void main(){vec3 p=position;float phase=(aOffset+p.x)*uFrequency+uTime*uSpeed;p.y+=sin(phase)*uAmplitude;vUv=uv;gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.0);}`}
        fragmentShader={`uniform sampler2D uMap;uniform float uTint;varying vec2 vUv;void main(){vec4 tex=texture2D(uMap,vUv);vec3 tone=mix(tex.rgb,tex.rgb*vec3(0.95,1.05,1.10),uTint);gl_FragColor=vec4(tone,tex.a);}`}
        transparent
      />
    </instancedMesh>
  );
}

export default function Scene() {
  return (
    <Canvas camera={{ position: [0, 0, 3], fov: 50 }}>
      <ambientLight intensity={0.6} />
      <WavyCarousel count={24} spacing={1.1} />
    </Canvas>
  );
}

Kontrol Interaksi

  • Hover/drag: Saat drag, tambah kecepatan atau ubah uAmplitude sementara lalu lerp kembali.
  • Autoplay: Gunakan kecepatan dasar + easing agar transisi halus saat interaksi berakhir.
  • Pointer easing: Mapping posisi pointer ke parameter shader (misal frekuensi lokal).

Performa & Kualitas

  • Instancing: Minimalkan draw calls. Pakai InstancedMesh alih-alih banyak mesh terpisah.
  • Tekstur: Gunakan atlas atau texture array untuk beberapa gambar.
  • Mipmaps/anisotropy: Kurangi shimmer saat carousel bergerak cepat.
  • Ukuran geometry: Segmen secukupnya (mis. 16–32) untuk wave halus tanpa berat.
  • Mobile fallback: Turunkan jumlah instance, amplitude, atau matikan efek berat.

Aksesibilitas & UX

  • Reduced motion: Hormati prefers-reduced-motion dengan menurunkan uAmplitude/speed.
  • Keyboard: Sediakan kontrol alternatif (prev/next) di luar kanvas.
  • SEO/Fallback: Tampilkan daftar gambar statis bila WebGL tidak tersedia.

<img src="data:image/svg+xml;utf8,Kontrol Shader• uAmplitude (tinggi gelombang)• uFrequency (kerapatan)• uSpeed (kecepatan fase)Guardrails Performa• Instancing • atlas • mipmaps• Batasi segmen geometry• Fallback mobileUX & A11y• Respek prefers-reduced-motion • Kontrol keyboard• SEO fallback (daftar gambar statis)” alt=”Panel parameter shader, guardrails performa, dan rekomendasi UX/A11y” />

Checklist Implementasi Cepat

  1. Siapkan aset tekstur (optimasi ukuran, aktifkan mipmaps/anisotropy).
  2. Buat InstancedMesh panel (plane) + atribut aOffset.
  3. Terapkan vertex/fragment shader (uniform: uTime, uAmplitude, uFrequency, uSpeed).
  4. Tambahkan loop wrap di useFrame dan kontrol interaksi (drag/hover).
  5. Uji performa (desktop/mobile) dan sediakan fallback.

Sumber & Kredit

Terbit: 2 Desember 2025 • Sumber inspirasi: Codrops

Catatan gambar: Kedua ilustrasi dibuat khusus untuk artikel ini dan dirilis sebagai CC0 (domain publik) — bebas digunakan tanpa atribusi. Jika situs Anda memblokir data URI, beri tahu saya agar saya kirimkan file PNG/SVG untuk diunggah ke Media Library.

Catatan: Teks dan kode merupakan ringkasan serta pengembangan orisinal, bukan salinan langsung dari sumber.