Water Shader
Musim panas akan datang (setidaknya saat saya menulis pelajaran ini), saatnya membuat pesta di kolam! 🩳
Dalam pelajaran ini, kita akan membuat efek air berikut menggunakan React Three Fiber dan GLSL:
Busa lebih padat di sekitar tepi kolam dan di sekitar bebek.
Untuk membuat efek ini kita akan mempelajari Lygia Shader library untuk menyederhanakan pembuatan shader dan kita akan mempraktikkan render target technique untuk menciptakan efek busa.
Paket Awal
Paket awal untuk pelajaran ini mencakup aset-aset berikut:
- Model kolam renang oleh Poly by Google CC-BY melalui Poly Pizza
- Model bebek dari Pmndrs marketplace
- Font Inter dari Google Fonts
Sisanya adalah pencahayaan sederhana dan pengaturan kamera.
Hari yang cerah di kolam 🏊
Shader air
Air hanya berupa bidang datar sederhana dengan <meshBasicMaterial />
yang diterapkan padanya. Kita akan mengganti material ini dengan custom shader.
Mari kita buat file baru WaterMaterial.jsx
dengan boilerplate untuk shader material:
import { shaderMaterial } from "@react-three/drei"; import { Color } from "three"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, /*glsl*/ ` varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { gl_FragColor = vec4(uColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }` );
Material kita memiliki dua uniforms: uColor
dan uOpacity
.
Agar kita bisa menggunakan material kustom kita secara deklaratif, mari gunakan fungsi extend
dari @react-three/fiber
di file main.jsx
:
// ... import { extend } from "@react-three/fiber"; import { WaterMaterial } from "./components/WaterMaterial.jsx"; extend({ WaterMaterial }); // ...
Kita perlu melakukan pemanggilan
extend
dari file yang diimpor sebelum komponen yang menggunakan material kustom. Dengan cara ini, kita dapat menggunakanWaterMaterial
secara deklaratif di dalam komponen kita.Itulah sebabnya kita melakukannya di file
main.jsx
dan bukan di fileWaterMaterial.jsx
.
Sekarang di Water.jsx
, kita dapat mengganti <meshBasicMaterial />
dengan material kustom kita dan menyesuaikan properti dengan uniforms yang sesuai:
import { useControls } from "leva"; import { Color } from "three"; export const Water = ({ ...props }) => { const { waterColor, waterOpacity } = useControls({ waterOpacity: { value: 0.8, min: 0, max: 1 }, waterColor: "#00c3ff", }); return ( <mesh {...props}> <planeGeometry args={[15, 32, 22, 22]} /> <waterMaterial uColor={new Color(waterColor)} transparent uOpacity={waterOpacity} /> </mesh> ); };
Kita berhasil mengganti material dasar dengan shader material kustom kita.
Lygia Shader Library
Untuk membuat efek busa yang animasi, kita akan menggunakan Lygia Shader library. Library ini mempermudah pembuatan shaders dengan menyediakan serangkaian utilitas dan fungsi untuk membuat shaders dengan cara yang lebih deklaratif.
Bagian yang akan menarik bagi kita adalah bagian generative. Bagian ini berisi serangkaian fungsi berguna untuk membuat efek generatif seperti noise, curl, fbm.
Di dalam bagian generative, Anda dapat menemukan daftar fungsi yang tersedia.
Dengan membuka salah satu fungsinya, Anda dapat melihat potongan kode untuk menggunakannya dalam shader Anda dan pratinjau efeknya.
Halaman fungsi pnoise
Ini adalah efek yang ingin kita gunakan. Anda dapat melihat dalam contoh bahwa untuk bisa menggunakan fungsi pnoise
, mereka menyertakan file shader pnoise
. Kita akan melakukan hal yang sama.
Resolve Lygia
Agar dapat menggunakan Lygia Shader library dalam proyek kita, kita memiliki dua opsi:
- Menyalin konten dari library ke dalam proyek kita dan mengimpor file yang kita butuhkan. (Kita telah melihat cara mengimpor file GLSL di pelajaran pengantar shaders)
- Menggunakan library bernama
resolve-lygia
yang akan mengambil Lygia Shader library dari web dan secara otomatis menggantikan direktif#include
terkait lygia dengan konten file tersebut.
Bergantung pada proyek Anda, seberapa banyak efek yang ingin Anda gunakan, dan jika Anda menggunakan library shader lainnya, Anda mungkin lebih memilih satu solusi daripada yang lainnya.
Dalam pelajaran ini, kita akan menggunakan library resolve-lygia
. Untuk menginstalnya, jalankan perintah berikut:
yarn add resolve-lygia
Kemudian, untuk menggunakannya kita hanya perlu membungkus kode fragment dan/atau vertex shader kita dengan fungsi resolveLygia
:
// ... import { resolveLygia } from "resolve-lygia"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, resolveLygia(/*glsl*/ ` varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { gl_FragColor = vec4(uColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }`) );
Kita hanya perlu menggunakan fungsi resolveLygia
untuk fragment shader dalam pelajaran ini.
Kita sekarang dapat menggunakan fungsi pnoise
dalam shader kita:
#include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; void main() { float noise = pnoise(vec3(vUv * 10.0, 1.0), vec3(100.0, 24.0, 112.0)); vec3 black = vec3(0.0); vec3 finalColor = mix(uColor, black, noise); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }
Kita mengalikan vUv
dengan 10.0
untuk membuat noise lebih terlihat dan kita menggunakan fungsi pnoise
untuk menciptakan efek noise. Kita mencampur uColor
dengan hitam berdasarkan nilai noise untuk menciptakan warna akhir.
Kita dapat melihat efek noise diterapkan pada air.
⚠️ Resolve Lygia tampaknya mengalami beberapa masalah downtime dari waktu ke waktu. Jika Anda mengalami masalah, Anda dapat menggunakan Glslify library atau menyalin file dari Lygia Shader library yang Anda butuhkan dan menggunakan file glsl seperti yang kita lakukan di pelajaran pengantar shaders.
Efek Busa
Efek noise adalah dasar dari efek busa kita. Sebelum menyempurnakan efek ini, mari kita buat beberapa uniform yang akan kita butuhkan untuk memiliki kendali penuh atas efek busa tersebut.
WaterMaterial.jsx
:
import { shaderMaterial } from "@react-three/drei"; import { resolveLygia } from "resolve-lygia"; import { Color } from "three"; export const WaterMaterial = shaderMaterial( { uColor: new Color("skyblue"), uOpacity: 0.8, uTime: 0, uSpeed: 0.5, uRepeat: 20.0, uNoiseType: 0, uFoam: 0.4, uFoamTop: 0.7, }, /*glsl*/ ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`, resolveLygia(/*glsl*/ ` #include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; uniform float uTime; uniform float uSpeed; uniform float uRepeat; uniform int uNoiseType; uniform float uFoam; uniform float uFoamTop; void main() { float noise = pnoise(vec3(vUv * 10.0, 1.0), vec3(100.0, 24.0, 112.0)); vec3 black = vec3(0.0); vec3 finalColor = mix(uColor, black, noise); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }`) );
Kami menambahkan uniform berikut:
uTime
: untuk menganimasikan efek busauSpeed
: untuk mengendalikan kecepatan animasi efekuRepeat
: untuk menskalakan efek noiseuNoiseType
: untuk beralih antara berbagai fungsi noiseuFoam
: untuk mengontrol ambang saat efek busa dimulaiuFoamTop
: untuk mengontrol ambang di mana busa menjadi lebih pekat
Sekarang kita perlu menerapkan uniform ini pada material.
Water.jsx
:
import { useFrame } from "@react-three/fiber"; import { useControls } from "leva"; import { useRef } from "react"; import { Color } from "three"; export const Water = ({ ...props }) => { const waterMaterialRef = useRef(); const { waterColor, waterOpacity, speed, noiseType, foam, foamTop, repeat } = useControls({ waterOpacity: { value: 0.8, min: 0, max: 1 }, waterColor: "#00c3ff", speed: { value: 0.5, min: 0, max: 5 }, repeat: { value: 30, min: 1, max: 100, }, foam: { value: 0.4, min: 0, max: 1, }, foamTop: { value: 0.7, min: 0, max: 1, }, noiseType: { value: 0, options: { Perlin: 0, Voronoi: 1, }, }, }); useFrame(({ clock }) => { if (waterMaterialRef.current) { waterMaterialRef.current.uniforms.uTime.value = clock.getElapsedTime(); } }); return ( <mesh {...props}> <planeGeometry args={[15, 32, 22, 22]} /> <waterMaterial ref={waterMaterialRef} uColor={new Color(waterColor)} transparent uOpacity={waterOpacity} uNoiseType={noiseType} uSpeed={speed} uRepeat={repeat} uFoam={foam} uFoamTop={foamTop} /> </mesh> ); };
Sekarang kita dapat membuat logika busa dalam shader.
Kita menghitung waktu yang disesuaikan dengan mengalikan uTime
dengan uSpeed
:
float adjustedTime = uTime * uSpeed;
Kemudian kita menghasilkan efek noise menggunakan fungsi pnoise
:
float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0));
Kita menerapkan efek busa menggunakan fungsi smoothstep
:
noise = smoothstep(uFoam, uFoamTop, noise);
Kemudian, kita menciptakan warna yang lebih terang untuk merepresentasikan busa. Kita menciptakan intermediateColor
dan topColor
:
vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0;
Kita menyesuaikan warna berdasarkan nilai noise:
vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise));
Ketika noise berada di antara 0.01
dan 1.0
, warnanya akan menjadi warna perantara. Ketika noise berada di atas atau sama dengan 1.0
, warnanya akan menjadi warna puncak.
Berikut adalah kode shader akhirnya:
#include "lygia/generative/pnoise.glsl" varying vec2 vUv; uniform vec3 uColor; uniform float uOpacity; uniform float uTime; uniform float uSpeed; uniform float uRepeat; uniform int uNoiseType; uniform float uFoam; uniform float uFoamTop; void main() { float adjustedTime = uTime * uSpeed; // NOISE GENERATION float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0)); // FOAM noise = smoothstep(uFoam, uFoamTop, noise); // COLOR vec3 intermediateColor = uColor * 1.8; vec3 topColor = intermediateColor * 2.0; vec3 finalColor = uColor; finalColor = mix(uColor, intermediateColor, step(0.01, noise)); finalColor = mix(finalColor, topColor, step(1.0, noise)); gl_FragColor = vec4(finalColor, uOpacity); #include <tonemapping_fragment> #include <encodings_fragment> }
Kita sekarang memiliki efek busa yang indah di air.
Voronoi noise
Kita dapat beralih antara fungsi noise yang berbeda dengan mengubah uniform uNoiseType
. Mari tambahkan fungsi Voronoi noise ke shader kita:
#include "lygia/generative/pnoise.glsl" #include "lygia/generative/voronoise.glsl" // ...
Untuk mendapatkan contoh yang berfungsi dari fungsi Voronoi noise, anda dapat mengunjungi halaman Voronoise di situs web perpustakaan Lygia Shader. Kemudian sesuaikan kode dengan uniform dan logika Anda.
Mari terapkan secara kondisional salah satu fungsi noise berdasarkan uniform uNoiseType
:
// ... // NOISE GENERATION float noise = 0.0; if (uNoiseType == 0) { noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0)); } else if (uNoiseType == 1) { vec2 p = 0.5 - 0.5*cos(adjustedTime *vec2(1.0,0.5)); p = p*p*(3.0-2.0*p); p = p*p*(3.0-2.0*p); p = p*p*(3.0-2.0*p); noise = voronoise(vec3(vUv * uRepeat, adjustedTime), p.x, 1.0); } // ...
Kita sekarang memiliki efek busa alternatif yang bagus menggunakan fungsi Voronoi noise. Berkat kontrol kita dapat menyesuaikan efek ini secara real-time.
Kepadatan Busa di Sekitar Tepi
Untuk membuatnya lebih realistis, kita akan membuat busa lebih padat di sekitar tepi kolam dan benda lain yang berpotongan dengan air.
Untuk mencapai ini, kita akan menggunakan render target untuk menghitung jarak suatu objek ke kamera kita. Kemudian kita akan menggunakan jarak ini untuk menyesuaikan kepadatan busa.
Tekstur Kedalaman
Untuk menyimpan informasi kedalaman, kita perlu membuat MeshDepthMaterial
dan me-render scene ke dalam render target.
Mari deklarasikan material kedalaman kita dalam Water.jsx
:
// ... import { // ... FloatType, MeshDepthMaterial, NoBlending, RGBADepthPacking, } from "three"; const depthMaterial = new MeshDepthMaterial(); depthMaterial.depthPacking = RGBADepthPacking; depthMaterial.blending = NoBlending; // ...
Kami mengatur depthPacking
ke RGBADepthPacking
untuk menyimpan kedalaman dalam saluran RGB
dalam tekstur. Kami mengatur blending
ke NoBlending
untuk menghindari penggabungan nilai kedalaman.
Kemudian kita perlu membuat render target, referensi dari mesh air kita untuk dapat menyembunyikannya saat menghitung kedalaman, dan kontrol maxDepth
untuk menyesuaikan hingga kedalaman mana kita ingin menghitung jaraknya:
// ... export const Water = ({ ...props }) => { // ... const { // ... maxDepth, } = useControls({ // ... maxDepth: { value: 2, min: 0, max: 5 }, }); const renderTarget = useFBO({ depth: true, type: FloatType, }); const waterRef = useRef(); // ... return ( <mesh {...props} ref={waterRef}> <planeGeometry args={[15, 32, 22, 22]} /> <waterCellShading // ... uMaxDepth={maxDepth} /> </mesh> ); };
Dan di dalam useFrame
kita akan me-render scene ke render target dan mengatur uniform yang kita butuhkan dalam shader kita:
// ... export const Water = ({ ...props }) => { // ... useFrame(({ gl, scene, camera, clock }) => { // Kita sembunyikan mesh air dan me-render scene ke render target waterRef.current.visible = false; gl.setRenderTarget(renderTarget); scene.overrideMaterial = depthMaterial; // Ini menggantikan material semua mesh dalam scene untuk menyimpan nilai kedalaman di dalam render target gl.render(scene, camera); // Kita reset scene dan menampilkan mesh air scene.overrideMaterial = null; // Komentari baris ini jika Anda ingin melihat apa yang terjadi di render target waterRef.current.visible = true; gl.setRenderTarget(null); // Kita atur uniform if (waterMaterialRef.current) { // ... waterMaterialRef.current.uniforms.uDepth.value = renderTarget.texture; const pixelRatio = gl.getPixelRatio(); waterMaterialRef.current.uniforms.uResolution.value = [ window.innerWidth * pixelRatio, window.innerHeight * pixelRatio, ]; waterMaterialRef.current.uniforms.uCameraNear.value = camera.near; waterMaterialRef.current.uniforms.uCameraFar.value = camera.far; } }); // ... };
Kita mengatur uniform uDepth
, uMaxDepth
, uResolution
, uCameraNear
, dan uCameraFar
dalam shader yang akan kita gunakan untuk menghitung jaraknya. Mari deklarasikan uniform ini dalam shader:
WaterMaterial.jsx
:
// ... export const WaterMaterial = shaderMaterial( { // ... uDepth: null, uMaxDepth: 1.0, uResolution: [0, 0], uCameraNear: 0, uCameraFar: 0, }, /*glsl*/ ` // ... `, resolveLygia(/*glsl*/ ` // ... uniform sampler2D uDepth; uniform float uMaxDepth; uniform vec2 uResolution; uniform float uCameraNear; uniform float uCameraFar; // ... `) );
Sempurna, kita memiliki semua yang kita butuhkan untuk mendapatkan nilai kedalaman dari render target.
Dalam fragment shader kita, kita akan menghitung jarak dari fragmen saat ini ke kamera:
// ... #include <packing> float getViewZ(const in float depth) { return perspectiveDepthToViewZ(depth, uCameraNear, uCameraFar); } float getDepth(const in vec2 screenPosition ) { return unpackRGBAToDepth(texture2D(uDepth, screenPosition)); } void main() { float adjustedTime = uTime * uSpeed; // PEMBUATAN NOISE // ... // KEDALAMAN vec2 screenUV = gl_FragCoord.xy / uResolution; float fragmentLinearEyeDepth = getViewZ(gl_FragCoord.z); float linearEyeDepth = getViewZ(getDepth(screenUV)); float depth = fragmentLinearEyeDepth - linearEyeDepth; // ... if (depth > uMaxDepth) { finalColor = vec3(1.0, 0.0, 0.0); } else { finalColor = vec3(depth); } gl_FragColor = vec4(finalColor, uOpacity); }
Dalam contoh ini, kita mewarnai fragmen merah jika kedalaman di atas uMaxDepth
, sebaliknya, kita mewarnai fragmennya berdasarkan nilai kedalaman untuk memvisualisasikan nilainya.
Kita dapat melihat nilai kedalaman divisualisasikan dalam air.
Ketika kedalaman melebihi uMaxDepth
, kita akan mengabaikan nilai-nilai tersebut tetapi ketika kita berada antara 0
(hitam) dan uMaxDepth
, kita akan menyesuaikan kepadatan busa dari sangat tinggi ke halus:
// KEDALAMAN // ... float depth = fragmentLinearEyeDepth - linearEyeDepth; noise += smoothstep(uMaxDepth, 0.0, depth);
Kita dapat menghapus visualisasi warna kedalaman dan kita sudah siap:
// if (depth > uMaxDepth) { // finalColor = vec3(1.0, 0.0, 0.0); // } else { // finalColor = vec3(depth); // }
Sekarang kita memiliki efek kepadatan busa yang bagus di sekitar tepi bebek dan kolam 🎉
Kesimpulan
Kita telah mempelajari cara membuat shader air canggih menggunakan React Three Fiber dan GLSL.
Kami menggunakan perpustakaan Lygia Shader untuk memudahkan pembuatan shader dan mempraktikkan teknik render target untuk menciptakan efek yang disesuaikan dengan baik.
React Three Fiber: The Ultimate Guide to 3D Web Development
✨ You have reached the end of the preview ✨
Go to the next level with Three.js and React Three Fiber!
Get full access to this lesson and the complete course when you enroll:
- 🔓 Full lesson videos with no limits
- 💻 Access to the final source code
- 🎓 Course progress tracking & completion
- 💬 Invite to our private Discord community
One-time payment. Lifetime updates included.