Water Shader

Starter pack

여름이 다가오고 있습니다 (적어도 이 레슨을 작성할 때는요), 수영장 파티를 열 시간이에요! 🩳

이 레슨에서는 React Three Fiber와 GLSL을 사용하여 다음과 같은 물 효과를 만들 것입니다:

거품은 수영장 가장자리와 오리 주변에 더 조밀합니다.

이 효과를 만들기 위해 셰이더 생성 작업을 단순화할 수 있는 Lygia Shader library를 발견하게 되며, 거품 효과를 생성하기 위해 render target 기법을 실습할 것입니다.

스타터 팩

이 레슨의 스타터 팩에는 다음과 같은 자산이 포함되어 있습니다:

나머지는 간단한 조명과 카메라 설정으로 이루어져 있습니다.

Starter pack

수영장의 화창한 날 🏊

Water shader

Water는 <meshBasicMaterial />을 적용한 단순한 평면입니다. 이 재료를 커스텀 shader로 대체하겠습니다.

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

우리의 material은 두 개의 uniforms: uColoruOpacity를 갖습니다.

커스텀 material을 선언적으로 사용하려면, main.jsx 파일에서 @react-three/fiberextend 기능을 사용합니다:

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

extend({ WaterMaterial });

// ...

커스텀 material을 사용하는 컴포넌트보다 먼저 import된 파일에서 extend 호출을 해야 합니다. 이렇게 하면 우리의 컴포넌트에서 WaterMaterial을 선언적으로 사용할 수 있게 됩니다.

그래서 WaterMaterial.jsx 파일 대신 main.jsx 파일에서 이를 수행합니다.

이제 Water.jsx에서 <meshBasicMaterial />을 우리의 커스텀 material로 교체하고 관련된 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 라이브러리를 사용할 것입니다. 이 라이브러리는 셰이더를 더 선언적으로 생성할 수 있게 하는 유틸리티와 함수 세트를 제공하여 셰이더 생성 과정을 단순화합니다.

우리가 관심을 가질 부분은 generative 섹션입니다. 이 섹션에는 노이즈, curl, fbm과 같은 생성 효과를 만드는 데 유용한 함수 세트가 포함되어 있습니다.

Lygia Shader library

generative 섹션 내에서 사용할 수 있는 함수 목록을 찾을 수 있습니다.

하나의 함수를 열면 셰이더 내에서 사용할 수 있는 코드 조각과 효과의 미리보기를 볼 수 있습니다.

Lygia Shader library function

pnoise 함수 페이지

이것이 우리가 사용하고자 하는 효과입니다. 예제에서 pnoise 함수를 사용하기 위해 pnoise 셰이더 파일을 포함해야 함을 볼 수 있습니다. 우리도 동일하게 할 것입니다.

Lygia 해결

프로젝트에서 Lygia Shader 라이브러리를 사용하기 위해 두 가지 옵션이 있습니다:

  • 라이브러리의 내용을 프로젝트로 복사하여 필요한 파일을 가져옵니다. (GLSL 파일을 가져오는 방법은 셰이더 소개 레슨에서 보았습니다.)
  • 웹에서 Lygia Shader 라이브러리를 해결하고 lygia와 관련된 #include 지시문을 파일의 내용으로 자동으로 대체하는 resolve-lygia라는 라이브러리를 사용합니다.

프로젝트에 따라, 얼마나 많은 효과를 사용할 것인지, 다른 셰이더 라이브러리를 사용하는지에 따라 한 가지 해결 방법을 선호할 수 있습니다.

본 레슨에서는 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>
    }`)
);

이 레슨에서는 fragment 셰이더에만 resolveLygia 함수를 사용하면 됩니다.

이제 우리의 셰이더에서 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 library를 사용하거나 필요한 Lygia Shader 라이브러리의 파일을 복사하여 셰이더 소개 레슨에서 했던 것처럼 glsl 파일을 사용할 수 있습니다.

Foam effect

noise 효과는 우리의 폼 효과의 기초입니다. 효과를 미세 조정하기 전에 폼 효과에 대한 완전한 제어를 위해 필요한 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: noise 효과의 스케일링
  • uNoiseType: 다른 noise 함수 간 전환을 위해
  • 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>
  );
};

이제 셰이더에 폼 로직을 생성할 수 있습니다.

uTimeuSpeed로 곱하여 조정된 시간을 계산합니다:

float adjustedTime = uTime * uSpeed;

그런 다음 pnoise 함수를 사용하여 noise 효과를 생성합니다:

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;

noise 값에 따라 색상을 조정합니다:

vec3 finalColor = uColor;
finalColor = mix(uColor, intermediateColor, step(0.01, noise));
finalColor = mix(finalColor, topColor, step(1.0, noise));

noise가 0.011.0 사이에 있을 때, 색상은 중간 색상이 됩니다. noise가 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.