{"id":12863,"date":"2025-07-26T10:14:30","date_gmt":"2025-07-26T10:14:30","guid":{"rendered":"https:\/\/binus.ac.id\/binus-digital\/?p=12863"},"modified":"2025-12-04T02:20:42","modified_gmt":"2025-12-04T02:20:42","slug":"membuat-wavy-infinite-carousel-di-react-three-fiber-dengan-glsl-panduan-lengkap","status":"publish","type":"post","link":"https:\/\/binus.ac.id\/binus-digital\/2025\/07\/26\/membuat-wavy-infinite-carousel-di-react-three-fiber-dengan-glsl-panduan-lengkap\/","title":{"rendered":"Membuat Wavy Infinite Carousel di React Three Fiber dengan GLSL: Panduan Lengkap"},"content":{"rendered":"<h2>Ringkasan<\/h2>\n<p>\u201cWavy infinite carousel\u201d menggabungkan <strong>React Three Fiber<\/strong> (R3F) untuk render 3D\/WebGL dengan <strong>GLSL<\/strong> untuk mendistorsi kartu\/gambar secara dinamis sehingga terasa hidup saat bergerak tak-berujung. Inti tekniknya: <em>instancing<\/em> untuk performa, <em>vertex shader<\/em> untuk gelombang, <em>fragment shader<\/em> untuk pewarnaan\/tekstur, dan <em>loop wrap<\/em> untuk ilusi tak-berujung.<\/p>\n<h2>Konsep Dasar<\/h2>\n<ul>\n<li><strong>Carousel tak-berujung:<\/strong> Deretan panel bergerak saling menyambung; saat panel melewati batas, posisinya \u201cdi-teleport\u201d ke sisi lain.<\/li>\n<li><strong>Wavy (gelombang):<\/strong> Posisi vertex dimodifikasi sinus\/cosinus menggunakan <code>uTime<\/code>, <code>uAmplitude<\/code>, dan <code>uFrequency<\/code>.<\/li>\n<li><strong>Instancing:<\/strong> Satu <em>mesh<\/em> direplikasi ribuan kali secara efisien; atribut kustom seperti <code>aOffset<\/code> memberi variasi fase gelombang.<\/li>\n<li><strong>Interaksi:<\/strong> Pointer\/drag menambah modulasi (misalnya kecepatan atau amplitudo sesaat).<\/li>\n<\/ul>\n<h2>Struktur Arsitektur Mini<\/h2>\n<ol>\n<li><strong>Assets:<\/strong> Siapkan tekstur (atlas untuk irit drawcall) dan aktifkan mipmaps\/anisotropy.<\/li>\n<li><strong>R3F Canvas:<\/strong> Render scene; <code>useFrame<\/code> untuk animasi posisi dan waktu shader.<\/li>\n<li><strong>InstancedMesh:<\/strong> Susun barisan panel; <code>setMatrixAt<\/code> + <code>InstancedBufferAttribute<\/code> untuk offset\/fase.<\/li>\n<li><strong>ShaderMaterial:<\/strong> Vertex shader = gelombang; fragment shader = sampling tekstur + efek ringan.<\/li>\n<li><strong>Loop:<\/strong> Logika wrap di CPU (JS) atau di shader (modulus).<\/li>\n<\/ol>\n<h2>Contoh Shader (Ilustratif)<\/h2>\n<p>Shader berikut bersifat ilustratif (bukan salinan artikel sumber). Ubah sesuai kebutuhan produksi.<\/p>\n<h3>Vertex Shader (gelombang)<\/h3>\n<pre><code class=\"language-glsl\">uniform float uTime;\r\nuniform float uAmplitude;   \/\/ tinggi gelombang\r\nuniform float uFrequency;   \/\/ kerapatan gelombang\r\nuniform float uSpeed;       \/\/ kecepatan fase\r\nattribute float aOffset;    \/\/ variasi per-instance\r\nvarying vec2 vUv;\r\n\r\nvoid main() {\r\n  vec3 p = position;\r\n  float phase = (aOffset + p.x) * uFrequency + uTime * uSpeed;\r\n  p.y += sin(phase) * uAmplitude;\r\n  vUv = uv;\r\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);\r\n}\r\n<\/code><\/pre>\n<h3>Fragment Shader (sampling &amp; toning)<\/h3>\n<pre><code class=\"language-glsl\">uniform sampler2D uMap;\r\nuniform float uTint; \/\/ 0.0..1.0 untuk efek toning ringan\r\nvarying vec2 vUv;\r\n\r\nvoid main() {\r\n  vec4 tex = texture2D(uMap, vUv);\r\n  vec3 tone = mix(tex.rgb, tex.rgb * vec3(0.95, 1.05, 1.10), uTint);\r\n  gl_FragColor = vec4(tone, tex.a);\r\n}\r\n<\/code><\/pre>\n<h2>Contoh R3F (Ilustratif)<\/h2>\n<p>Contoh singkat: membuat baris panel instanced yang bergeser dan di-wrap. Gunakan loader tekstur sesuai kebutuhan (atlas\/array).<\/p>\n<pre><code class=\"language-tsx\">\/\/ Carousel.tsx (ilustratif)\r\nimport * as THREE from \"three\";\r\nimport { Canvas, useFrame, useThree } from \"@react-three\/fiber\";\r\nimport { useMemo, useRef } from \"react\";\r\n\r\nfunction WavyCarousel({ count = 20, spacing = 1.2 }) {\r\n  const meshRef = useRef&lt;THREE.InstancedMesh&gt;(null);\r\n  const matRef = useRef&lt;THREE.ShaderMaterial&gt;(null);\r\n  const { clock } = useThree();\r\n\r\n  \/\/ Posisi awal per panel dan offset fase\r\n  const { matrices, offsets } = useMemo(() =&gt; {\r\n    const dummy = new THREE.Object3D();\r\n    const m = new Array(count);\r\n    const o = new Float32Array(count);\r\n    for (let i = 0; i &lt; count; i++) {\r\n      dummy.position.set(i * spacing, 0, 0);\r\n      dummy.rotation.set(0, 0, 0);\r\n      dummy.updateMatrix();\r\n      m[i] = dummy.matrix.clone();\r\n      o[i] = i * 0.37; \/\/ fase unik agar gelombang tidak seragam\r\n    }\r\n    return { matrices: m, offsets: o };\r\n  }, [count, spacing]);\r\n\r\n  \/\/ Buat atribut offset untuk instancing\r\n  const offsetAttr = useMemo(() =&gt; new THREE.InstancedBufferAttribute(offsets, 1), [offsets]);\r\n\r\n  \/\/ Parameter loop\r\n  const totalWidth = (count - 1) * spacing;\r\n  const speed = 0.6; \/\/ unit per detik\r\n\r\n  useFrame((state, dt) =&gt; {\r\n    const t = clock.getElapsedTime();\r\n    if (matRef.current) {\r\n      matRef.current.uniforms.uTime.value = t;\r\n    }\r\n    \/\/ Geser semua panel ke kiri, wrap saat melewati batas\r\n    const dummy = new THREE.Object3D();\r\n    for (let i = 0; i &lt; count; i++) {\r\n      dummy.matrix.copy(matrices[i]);\r\n      dummy.position.set(\r\n        ((dummy.position.x - speed * dt) % (totalWidth + spacing) + (totalWidth + spacing)) % (totalWidth + spacing),\r\n        0,\r\n        0\r\n      );\r\n      dummy.updateMatrix();\r\n      meshRef.current!.setMatrixAt(i, dummy.matrix);\r\n    }\r\n    meshRef.current!.instanceMatrix.needsUpdate = true;\r\n  });\r\n\r\n  return (\r\n    &lt;instancedMesh ref={meshRef} args={[undefined as any, undefined as any, count]}&gt;\r\n      &lt;planeGeometry args={[1, 0.6, 16, 16]}&gt;\r\n        &lt;instancedBufferAttribute attach='attributes-aOffset' args={[offsetAttr.array, 1]} \/&gt;\r\n      &lt;\/planeGeometry&gt;\r\n      &lt;shaderMaterial\r\n        ref={matRef}\r\n        uniforms={{\r\n          uTime: { value: 0 },\r\n          uAmplitude: { value: 0.08 },\r\n          uFrequency: { value: 2.5 },\r\n          uSpeed: { value: 2.0 },\r\n          uTint: { value: 0.15 },\r\n          uMap: { value: new THREE.TextureLoader().load('\/path\/to\/texture.jpg') }\r\n        }}\r\n        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);}`}\r\n        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);}`}\r\n        transparent\r\n      \/&gt;\r\n    &lt;\/instancedMesh&gt;\r\n  );\r\n}\r\n\r\nexport default function Scene() {\r\n  return (\r\n    &lt;Canvas camera={{ position: [0, 0, 3], fov: 50 }}&gt;\r\n      &lt;ambientLight intensity={0.6} \/&gt;\r\n      &lt;WavyCarousel count={24} spacing={1.1} \/&gt;\r\n    &lt;\/Canvas&gt;\r\n  );\r\n}\r\n<\/code><\/pre>\n<h2>Kontrol Interaksi<\/h2>\n<ul>\n<li><strong>Hover\/drag:<\/strong> Saat drag, tambah kecepatan atau ubah <code>uAmplitude<\/code> sementara lalu lerp kembali.<\/li>\n<li><strong>Autoplay:<\/strong> Gunakan kecepatan dasar + easing agar transisi halus saat interaksi berakhir.<\/li>\n<li><strong>Pointer easing:<\/strong> Mapping posisi pointer ke parameter shader (misal frekuensi lokal).<\/li>\n<\/ul>\n<h2>Performa &amp; Kualitas<\/h2>\n<ul>\n<li><strong>Instancing:<\/strong> Minimalkan draw calls. Pakai <code>InstancedMesh<\/code> alih-alih banyak mesh terpisah.<\/li>\n<li><strong>Tekstur:<\/strong> Gunakan atlas atau <em>texture array<\/em> untuk beberapa gambar.<\/li>\n<li><strong>Mipmaps\/anisotropy:<\/strong> Kurangi shimmer saat carousel bergerak cepat.<\/li>\n<li><strong>Ukuran geometry:<\/strong> Segmen secukupnya (mis. 16\u201332) untuk wave halus tanpa berat.<\/li>\n<li><strong>Mobile fallback:<\/strong> Turunkan jumlah instance, amplitude, atau matikan efek berat.<\/li>\n<\/ul>\n<h2>Aksesibilitas &amp; UX<\/h2>\n<ul>\n<li><strong>Reduced motion:<\/strong> Hormati <code>prefers-reduced-motion<\/code> dengan menurunkan <code>uAmplitude<\/code>\/<code>speed<\/code>.<\/li>\n<li><strong>Keyboard:<\/strong> Sediakan kontrol alternatif (prev\/next) di luar kanvas.<\/li>\n<li><strong>SEO\/Fallback:<\/strong> Tampilkan daftar gambar statis bila WebGL tidak tersedia.<\/li>\n<\/ul>\n<p>&lt;img src=&quot;data:image\/svg+xml;utf8,Kontrol Shader\u2022 uAmplitude (tinggi gelombang)\u2022 uFrequency (kerapatan)\u2022 uSpeed (kecepatan fase)Guardrails Performa\u2022 Instancing \u2022 atlas \u2022 mipmaps\u2022 Batasi segmen geometry\u2022 Fallback mobileUX &amp; A11y\u2022 Respek prefers-reduced-motion \u2022 Kontrol keyboard\u2022 SEO fallback (daftar gambar statis)&#8221; alt=&#8221;Panel parameter shader, guardrails performa, dan rekomendasi UX\/A11y&#8221; \/&gt;<\/p>\n<h2>Checklist Implementasi Cepat<\/h2>\n<ol>\n<li>Siapkan aset tekstur (optimasi ukuran, aktifkan mipmaps\/anisotropy).<\/li>\n<li>Buat <code>InstancedMesh<\/code> panel (plane) + atribut <code>aOffset<\/code>.<\/li>\n<li>Terapkan vertex\/fragment shader (uniform: <code>uTime<\/code>, <code>uAmplitude<\/code>, <code>uFrequency<\/code>, <code>uSpeed<\/code>).<\/li>\n<li>Tambahkan loop wrap di <code>useFrame<\/code> dan kontrol interaksi (drag\/hover).<\/li>\n<li>Uji performa (desktop\/mobile) dan sediakan fallback.<\/li>\n<\/ol>\n<h2>Sumber &amp; Kredit<\/h2>\n<ul>\n<li>Artikel inspirasi: <a href=\"https:\/\/tympanus.net\/codrops\/2025\/11\/26\/creating-wavy-infinite-carousels-in-react-three-fiber-with-glsl-shaders\/\" rel=\"nofollow noopener\" target=\"_blank\">Codrops<\/a><\/li>\n<li>React Three Fiber: <a href=\"https:\/\/docs.pmnd.rs\/react-three-fiber\/getting-started\/introduction\" rel=\"nofollow noopener\" target=\"_blank\">docs.pmnd.rs<\/a><\/li>\n<li>GLSL dasar: <a href=\"https:\/\/thebookofshaders.com\/\" rel=\"nofollow noopener\" target=\"_blank\">The Book of Shaders<\/a><\/li>\n<\/ul>\n<p><em>Terbit: 2 Desember 2025 \u2022 Sumber inspirasi: <a href=\"https:\/\/tympanus.net\/codrops\/2025\/11\/26\/creating-wavy-infinite-carousels-in-react-three-fiber-with-glsl-shaders\/\" rel=\"nofollow noopener\" target=\"_blank\">Codrops<\/a><\/em><\/p>\n<p><strong>Catatan gambar:<\/strong> Kedua ilustrasi dibuat khusus untuk artikel ini dan dirilis sebagai CC0 (domain publik) \u2014 bebas digunakan tanpa atribusi. Jika situs Anda memblokir data URI, beri tahu saya agar saya kirimkan file PNG\/SVG untuk diunggah ke Media Library.<\/p>\n<p><em>Catatan:<\/em> Teks dan kode merupakan ringkasan serta pengembangan orisinal, bukan salinan langsung dari sumber.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ringkasan \u201cWavy infinite carousel\u201d 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 [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":12885,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[77],"tags":[],"class_list":["post-12863","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-articles"],"_links":{"self":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12863","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/comments?post=12863"}],"version-history":[{"count":1,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12863\/revisions"}],"predecessor-version":[{"id":12864,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/posts\/12863\/revisions\/12864"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/media\/12885"}],"wp:attachment":[{"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/media?parent=12863"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/categories?post=12863"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/binus.ac.id\/binus-digital\/wp-json\/wp\/v2\/tags?post=12863"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}