水着着色器

Starter pack

夏天要来了 (至少在我写这篇课程的时候),是时候举办泳池派对了!🩳

在本课程中,我们将使用 React Three Fiber 和 GLSL 创建以下水效果:

泡沫在泳池边缘和鸭子周围更为浓密。

为了实现这种效果,我们将探索 Lygia Shader 库 来简化着色器的创建,并将实践一种 render target 技术 来创建泡沫效果。

启动包

本课程的启动包中包含以下资源:

其余部分为简单的灯光和相机设置。

启动包

阳光明媚的泳池日 🏊

水着色器

水面只是一个简单的平面,上应用了 <meshBasicMaterial />。我们将用自定义着色器替换这个 material。

让我们创建一个新文件 WaterMaterial.jsx,为着色器 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 有两个 uniforms: uColoruOpacity

为了能够声明式地使用我们的自定义 material,在 main.jsx 文件中使用 @react-three/fiberextend 函数:

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

extend({ WaterMaterial });

// ...

我们需要从一个先于使用自定义 material 的组件导入的文件中进行 extend 调用。这样,我们才能在组件中声明式地使用 WaterMaterial

这就是为什么我们在 main.jsx 文件中,而不是 WaterMaterial.jsx 文件中进行这个操作。

现在在 Water.jsx 中,我们可以用自定义 material 替换 <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 替换了基本 material。

Lygia Shader 库

为了创建动态泡沫效果,我们将使用 Lygia Shader 库。这个库通过提供一组实用程序和函数以更具声明性的方式创建 shaders 从而简化了 shaders 的创建。

我们感兴趣的部分是 generative。它包含了一组有用的函数,用于创建生成效果,如噪声、curl、fbm。

Lygia Shader 库

在 generative 部分,你可以找到可用函数的列表。

通过打开其中一个函数,你可以看到代码片段以在你的 shader 中使用它,并预览效果。

Lygia Shader 库函数

pnoise 函数页面

这是我们想要使用的效果。你可以在示例中看到,为了能够使用 pnoise 函数,他们包括了 pnoise shader 文件。我们也将这样做。

Resolve Lygia

为了在我们的项目中使用 Lygia Shader 库,我们有两个选择:

  • 将库的内容复制到我们的项目中并导入我们需要的文件。(我们已经在 shaders 入门课程 见过如何导入 GLSL 文件)
  • 使用一个名为 resolve-lygia 的库,它将从网络中解析 Lygia Shader 库,并自动用文件内容替换与 lygia 相关的 #include 指令。

根据你的项目、需要使用多少效果以及是否在使用其他 shader 库,你可能会偏好某个解决方案。

在本课程中,我们将使用 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>
    }`)
);

在本课程中,我们只需在 fragment shader 中使用 resolveLygia 函数。

现在我们可以在 shader 中使用 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>
}

我们将 vUv 乘以 10.0 以使噪声更明显,并使用 pnoise 函数创建噪声效果。我们根据噪声值将 uColor 与黑色混合以创建最终颜色。

Lygia Shader 库 pnoise

我们可以看到应用于水的噪声效果。

⚠️ Resolve Lygia 似乎有时会出现停机问题。如果你遇到任何问题,可以使用 Glslify 库 或从 Lygia Shader 库中复制你需要的文件,并像我们在 shaders 入门课程 中一样使用 glsl 文件

泡沫效果

噪声效果是我们泡沫效果的基础。在微调效果之前,让我们创建我们所需的 uniforms,以便完全控制泡沫效果。

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

我们添加了以下 uniforms:

  • uTime: 动画泡沫效果
  • uSpeed: 控制效果动画的速度
  • uRepeat: 缩放噪声效果
  • uNoiseType: 切换不同的噪声函数
  • uFoam: 控制泡沫效果开始的阈值
  • uFoamTop: 控制泡沫变得更浓密的阈值

现在我们需要将这些 uniforms 应用于材质。 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>
  );
};

我们现在可以在 shader 中创建泡沫逻辑。

通过 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.011.0 之间时,颜色将是中间颜色。当噪声大于或等于 1.0 时,颜色将是顶部颜色。

这是最终的 shader 代码:

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