Water Shader

Starter pack

夏が近づいていますね (少なくともこのレッスンを書いている時点では), そろそろプールパーティーを開く時です! 🩳

このレッスンでは、React Three FiberとGLSLを使用して次のような水の効果を作成します:

フォームはプールの端やアヒルの周りでより濃くなります。

この効果を作成するために、シェーダーの作成を簡単にするためのLygia Shader libraryを紹介し、フォーム効果を作成するためのレンダーターゲット技法を実践します。

スターターパック

このレッスン用のスターターパックには以下のアセットが含まれています:

それ以外は簡単なライティングとカメラの設定です。

スターターパック

プールでの晴れた日🏊

Water shader

この水面は単なる平面に<meshBasicMaterial />を適用したものです。これをカスタムシェーダーで置き換えます。

新しいファイル WaterMaterial.jsx を作成し、シェーダーマテリアル用のボイラープレートを追加します:

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>
    }`
);

このマテリアルには二つのユニフォーム: uColoruOpacity が含まれています。

カスタムマテリアルを宣言的に使用できるようにするために、main.jsxファイルで@react-three/fiberからextend関数を使用しましょう:

// ...
import { extend } from "@react-three/fiber";
import { WaterMaterial } from "./components/WaterMaterial.jsx";

extend({ WaterMaterial });

// ...

カスタムマテリアルを使用するコンポーネントよりも前にインポートされるファイルからextendを呼び出す必要があります。こうすることで、コンポーネント内で宣言的にWaterMaterialを使用できるようになります。

そのため、この処理をWaterMaterial.jsxファイルではなく、main.jsxファイルで行います。

次にWater.jsx<meshBasicMaterial />をカスタムマテリアルに置き換え、対応するユニフォームでプロパティを調整します:

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>
  );
};

基本的なマテリアルをカスタムシェーダーマテリアルで成功裏に置き換えることができました。

Lygia Shader ライブラリ

アニメーションの泡効果を作成するために、Lygia Shader ライブラリを使用します。このライブラリは、宣言的な方法でシェーダーを作成するためのユーティリティと関数のセットを提供することで、シェーダーの作成を簡素化します。

私たちが興味を持つセクションは、generativeセクションです。これには、ノイズ、カール、fbmなどの生成効果を作成するための便利な関数が含まれています。

Lygia Shader library

生成セクション内には、利用可能な関数のリストがあります。

関数の1つを開くと、シェーダーで使用するためのコードスニペットと効果のプレビューを見ることができます。

Lygia Shader library function

pnoise関数ページ

これが使用したい効果です。例を見ると、pnoise関数を使用するためには、pnoiseシェーダーファイルを含める必要があることがわかります。私たちも同じことを行います。

Resolve Lygia

プロジェクトでLygia Shaderライブラリを使用するために、2つのオプションがあります:

  • ライブラリの内容をプロジェクトにコピーし、必要なファイルをインポートする方法。(GLSLファイルのインポート方法についてはシェーダー入門レッスンで確認しました。)
  • resolve-lygiaというライブラリを使用して、WebからLygia Shaderライブラリを解決し、lygiaに関連する#includeディレクティブを自動的にファイルの内容で置き換える方法。

プロジェクトの規模や使用したい効果の数、また他のシェーダーライブラリを使用しているかどうかに応じて、どちらの方法を選択するかが決まります。

このレッスンでは、resolve-lygiaライブラリを使用します。インストールするには、以下のコマンドを実行します:

yarn add resolve-lygia

その後、使用するためには、fragmentおよび/またはvertexシェーダーコードを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シェーダーにのみ使用します。

これでシェーダーでpnoise関数を使用できるようになりました:

#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>
}

vUv10.0倍してノイズをより目立たせ、pnoise関数を使用してノイズ効果を作成します。ノイズ値に基づいてuColorを黒色と混合して最終的な色を作成します。

Lygia Shader library pnoise

水に適用されたノイズ効果が見えます。

⚠️ Resolve Lygiaは時折ダウンタイムの問題があります。問題が発生した場合は、Glslifyライブラリを使用するか、Lygia Shaderライブラリから必要なファイルをコピーし、glslファイルシェーダー入門レッスンのように使用してください。

泡のエフェクト

ノイズエフェクトは、泡のエフェクトの基礎です。エフェクトを微調整する前に、泡のエフェクトを完全に制御するために必要なuniformを作成しましょう。

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>
    }`)
);

以下のuniformを追加しました:

  • uTime: 泡エフェクトをアニメートするため
  • uSpeed: エフェクトアニメーションの速度を制御するため
  • uRepeat: ノイズエフェクトをスケールするため
  • uNoiseType: 異なるノイズ関数の切り替えのため
  • uFoam: 泡エフェクトが始まる閾値を制御するため
  • uFoamTop: 泡が濃くなる閾値を制御するため

次に、これらのuniformを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>
  );
};

これでシェーダ内で泡のロジックを作成できます。

uTimeuSpeed で乗じて、調整された時間を計算します:

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);

その後、泡を表現するためにより明るい色を作成します。intermediateColortopColor を作成します:

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.011.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>
}

これで水にきれいな泡のエフェクトが加わりました。

End of lesson preview

To get access to the entire lesson, you need to purchase the course.