تظليل الماء

Starter pack

الصيف قادم (على الأقل عند كتابة هذا الدرس)، حان وقت تنظيم حفلة في المسبح! 🩳

في هذا الدرس، سنقوم بإنشاء تأثير الماء التالي باستخدام React Three Fiber و GLSL:

الرغوة أكثر كثافة حول حواف المسبح وحول البطة.

لإنشاء هذا التأثير، سنكتشف مكتبة تظليل Lygia لتبسيط عملية إنشاء التظليلات وسنقوم بتطبيق تقنية الهدف التقديمي لإنشاء تأثير الرغوة.

الحزمة الابتدائية

تتضمن الحزمة الابتدائية لهذا الدرس الأصول التالية:

الباقي هو إعداد إضاءة وكاميرا بسيط.

الحزمة الابتدائية

يوم مشمس في المسبح 🏊

تظليل المياه

الماء هنا هو مجرد سطح مستوٍ تم تطبيق <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.

Lygia Shader library

ضمن قسم التوليدي، يمكنك العثور على قائمة الوظائف المتاحة.

عند فتح واحدة من الوظائف، يمكنك رؤية مقتطف الكود لاستخدامه في الـ shader ومعاينة للتأثير.

Lygia Shader library function

صفحة وظيفة 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 مع الأسود بناءً على قيمة الضوضاء لإنشاء اللون النهائي.

Lygia Shader library pnoise

يمكننا رؤية تأثير الضوضاء المطبق على الماء.

⚠️ يبدو أن مكتبة 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>
}

لدينا الآن تأثير رغوة جميل على الماء.

Three.js logoReact logo

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
Unlock the Full Course – Just $85

One-time payment. Lifetime updates included.