تظليل الماء
الصيف قادم (على الأقل عند كتابة هذا الدرس)، حان وقت تنظيم حفلة في المسبح! 🩳
في هذا الدرس، سنقوم بإنشاء تأثير الماء التالي باستخدام React Three Fiber و GLSL:
الرغوة أكثر كثافة حول حواف المسبح وحول البطة.
لإنشاء هذا التأثير، سنكتشف مكتبة تظليل Lygia لتبسيط عملية إنشاء التظليلات وسنقوم بتطبيق تقنية الهدف التقديمي لإنشاء تأثير الرغوة.
الحزمة الابتدائية
تتضمن الحزمة الابتدائية لهذا الدرس الأصول التالية:
- نموذج المسبح بواسطة Poly by Google CC-BY عبر Poly Pizza
- نموذج البطة من سوق Pmndrs
- خط Inter من خطوط جوجل
الباقي هو إعداد إضاءة وكاميرا بسيط.
يوم مشمس في المسبح 🏊
تظليل المياه
الماء هنا هو مجرد سطح مستوٍ تم تطبيق <meshBasicMaterial />
عليه. سنقوم باستبدال هذا الـ material بمادة تظليل مخصصة.
لنقم بإنشاء ملف جديد WaterMaterial.jsx
مع قالب لـ 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> }` );
المادة لدينا تحتوي على اثنين من uniforms: uColor
و uOpacity
.
لتكون قادرًا على استخدام المادة المخصصة لدينا بشكل تعبيري، لنعتمد على وظيفة extend
من @react-three/fiber
في ملف main.jsx
:
// ... import { extend } from "@react-three/fiber"; import { WaterMaterial } from "./components/WaterMaterial.jsx"; extend({ WaterMaterial }); // ...
نحتاج إلى تنفيذ استدعاء
extend
من ملف يتم استيراده قبل المكون الذي يستخدم المادة المخصصة. بهذه الطريقة، سنكون قادرين على استخدامWaterMaterial
بشكل تعبيري في مكوناتنا.لذلك نقوم بذلك في ملف
main.jsx
بدلاً من ملفWaterMaterial.jsx
.
الآن في Water.jsx
، يمكننا استبدال <meshBasicMaterial />
بالمادة المخصصة لدينا وضبط الخصائص مع الـ uniforms المقابلة:
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> ); };
لقد قمنا بنجاح باستبدال الـ material الأساسي بـ shader material المخصص لدينا.
مكتبة Lygia Shader
لإنشاء تأثير رغوة متحرك، سنستخدم مكتبة Lygia Shader. تسهل هذه المكتبة إنشاء الـ shaders من خلال توفير مجموعة من الأدوات والوظائف لإنشاء الـ shaders بطريقة أكثر وصفية.
القسم الذي سيهمنا هو التوليدي. يحتوي على مجموعة من الوظائف المفيدة لإنشاء التأثيرات التوليدية مثل noise وcurl وfbm.
ضمن قسم التوليدي، يمكنك العثور على قائمة الوظائف المتاحة.
عند فتح واحدة من الوظائف، يمكنك رؤية مقتطف الكود لاستخدامه في الـ shader ومعاينة للتأثير.
صفحة وظيفة pnoise
هذا هو التأثير الذي نريد استخدامه. يمكنك أن ترى في المثال أنه لاستخدام وظيفة pnoise
، يقومون بتضمين ملف pnoise
shader. سنقوم بنفس الشيء.
حل Lygia
لاستخدام مكتبة Lygia Shader في مشروعنا، لدينا خياران:
- نسخ محتوى المكتبة إلى مشروعنا واستيراد الملفات التي نحتاجها. (لقد رأينا كيفية استيراد ملفات GLSL في درس مقدمة عن الـ shaders)
- استخدام مكتبة باسم
resolve-lygia
التي ستقوم بحل مكتبة Lygia Shader من الويب واستبدال التوجيهات#include
المتعلقة بالـ lygia تلقائيًا بمحتوى الملفات.
اعتمادًا على مشروعك، وكمية التأثيرات التي ترغب في استخدامها، وإذا كنت تستخدم مكتبات shaders أخرى، قد تفضل حلاً عن الآخر.
في هذا الدرس، سنستخدم مكتبة resolve-lygia
. لتثبيتها، قم بتشغيل الأمر التالي:
yarn add resolve-lygia
بعد ذلك، لاستخدامها، نحتاج ببساطة إلى تغليف كود fragment و/أو vertex shader لدينا بوظيفة 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> }`) );
سنحتاج إلى استخدام وظيفة resolveLygia
فقط لـ fragment shader في هذا الدرس.
يمكننا الآن استخدام وظيفة pnoise
في الـ shader لدينا:
#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> }
نضرب vUv
بـ 10.0
لجعل الضوضاء أكثر وضوحًا ونستخدم وظيفة pnoise
لإنشاء تأثير الضوضاء. نمزج بين uColor
مع الأسود بناءً على قيمة الضوضاء لإنشاء اللون النهائي.
يمكننا رؤية تأثير الضوضاء المطبق على الماء.
⚠️ يبدو أن مكتبة Resolve Lygia تواجه بعض مشاكل التوقف من وقت لآخر. إذا واجهت أي مشاكل، يمكنك استخدام مكتبة Glslify أو نسخ الملفات من مكتبة Lygia Shader التي تحتاجها واستخدام ملفات glsl كما فعلنا في درس مقدمة عن الـ shaders.
تأثير الرغوة
تأثير الضوضاء هو أساس تأثير الرغوة لدينا. قبل ضبط التأثير جيدًا، دعونا ننشئ المتغيرات التي سنحتاجها للحصول على تحكم كامل في تأثير الرغوة.
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> }`) );
لقد أضفنا المتغيرات التالية:
uTime
: لتحريك تأثير الرغوةuSpeed
: للتحكم بسرعة حركة التأثيرuRepeat
: لتغيير مقياس تأثير الضوضاءuNoiseType
: للتبديل بين وظائف الضوضاء المختلفةuFoam
: للتحكم في العتبة عندما يبدأ تأثير الرغوةuFoamTop
: للتحكم في العتبة عند زيادة كثافة الرغوة
الآن نحن بحاجة إلى تطبيق هذه المتغيرات على الـ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> ); };
يمكننا الآن إنشاء منطق الرغوة في الشيدر.
نقوم بحساب الوقت المعدل بواسطة ضرب الـ uTime
في uSpeed
:
float adjustedTime = uTime * uSpeed;
ثم نقوم بتوليد تأثير الضوضاء باستخدام الدالة pnoise
:
float noise = pnoise(vec3(vUv * uRepeat, adjustedTime * 0.5), vec3(100.0, 24.0, 112.0));
نطبق تأثير الرغوة باستخدام دالة smoothstep
:
noise = smoothstep(uFoam, uFoamTop, noise);
ثم نقوم بإنشاء ألوان أكثر إشراقًا لتمثيل الرغوة. نقوم بإنشاء intermediateColor
و topColor
:
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));
عندما تكون الضوضاء بين 0.01
و 1.0
، سيكون اللون هو اللون الوسيط. وعندما تكون الضوضاء أعلى أو تساوي 1.0
، سيكون اللون هو اللون العلوي.
إليك الشيفرة النهائية للشيدر:
#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> }
لدينا الآن تأثير رغوة جميل على الماء.
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.