ThreejsとGLSLを使用したノイズ画像スライド

はじめに

この記事では、Three.jsを使用してノイズ画像をスライドさせる方法について詳しく説明します。

Three.jsは、WebGLを簡単に扱うためのJavaScriptライブラリであり、複雑な3Dシーンを簡単に作成することができます。

ここでは、GLSL(OpenGL Shading Language)を使用してカスタムシェーダーを作成し、ノイズを使った画像スライド効果を実現します。

必要なパッケージ

各必要なパッケージをインストールします。

詳細はnpm docs hogehogeで確認してください。

  "three": "^0.165.0",
  "gsap": "^3.12.5",
  "lil-gui": "^0.19.2",
  "glsl-easings": "^1.0.0",
  "glsl-noise": "^0.0.0",
  "glslify": "^7.1.1",

詳細解説

シェーダーについて

Three.jsの設定

Three.jsのシーン、カメラ、レンダラーを設定し、リサイズイベントに対応しています。

ShaderMaterialを使用してカスタムシェーダーを設定し、シェーダーに必要なユニフォーム変数を渡しています!

アニメーション

gsapを使用して、進行状況をアニメートし、requestAnimationFrameを使って毎フレームシェーダーのノイズが変化するようにしています!

サンプルコード

HTML
<canvas id="webgl"></canvas>
TypeScript
import gsap from "gsap";
import GUI from "lil-gui";
import * as THREE from "three";
import fragmentShader from "./shaders/fragmentShader.glsl";
import vertexShader from "./shaders/vertexShader.glsl";
import { loadTexture } from "./utils";

(async () => {
/** Three.jsの3Dシーンを作成します*/
const scene: THREE.Scene = new THREE.Scene();

/** カメラを設定します */
const camera = new THREE.PerspectiveCamera(
  40, // 視野角
  window.innerWidth / window.innerHeight, // アスペクト比
  0.1, // カメラの視点の最短距離
  1000, // カメラの視点の最長距離
);

/** リサイズした時画面幅を合わせる */
window.addEventListener(
  "resize",
  () => {
    renderer?.setSize(window.innerWidth, window.innerHeight);
    if (camera) {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    }
  },
  false,
);

/** WebGLレンダラーを作成し、アンチエイリアシングを有効にします */
const renderer = new THREE.WebGLRenderer({
  antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);

/** エレメントの取得 */
const containerElement: HTMLElement | null = document.querySelector("#webgl");
if (!containerElement) return; // #webgl要素が存在しない場合は処理を終了します
containerElement.appendChild(renderer.domElement);
renderer.setSize(window.innerWidth, window.innerHeight);

/** Geometry を作成します */
const geometry = new THREE.PlaneGeometry(8, 4);

console.log(geometry.attributes);

/** ShaderMaterialを作成します */
const material = new THREE.ShaderMaterial({
  uniforms: {
    uTexCurrent: {
      value: await loadTexture("../../images/sample1.jpg"),
    },
    uTexNext: {
      value: await loadTexture("../../images/sample2.jpg"),
    },
    uTexDisplay: {
      value: await loadTexture(
        "https://static.not-equal.dev/ja_webgl_basic/img/displacement/3.png",
      ),
    },
    uTick: { value: 0 },
    uProgress: { value: 0 },
    // ノイズスケールを設定する
    uNoiseScale: { value: new THREE.Vector2(10, 10) },
  },
  vertexShader,
  fragmentShader,
});

console.log(material.uniforms.uNoiseScale.value);

/** Mesh を作成します */
const cube = new THREE.Mesh(geometry, material);

/** シーンに追加します */
scene.add(cube);

camera.position.z = 5;
// lil gui
const gui = new GUI();
const folder1 = gui.addFolder("Noise");
const datObj = { next: !!material.uniforms.uProgress.value };
folder1.open();

folder1.add(material.uniforms.uNoiseScale.value, "x", 0, 10, 1);
folder1.add(material.uniforms.uNoiseScale.value, "y", 0, 10, 1);
folder1
  .add(material.uniforms.uProgress, "value", 0, 1, 0.1)
  .name("progress")
  .listen();
folder1
  .add(datObj, "next")
  .name("Animate")
  .onChange(function () {
    gsap.to(material.uniforms.uProgress, {
      value: +datObj.next,
      duration: 3,
      ease: "none",
    });
  });
/** アニメーションループ*/
function animate() {
  requestAnimationFrame(animate);
  material.uniforms.uTick.value++;

  renderer.render(scene, camera);
}

animate();
})();

fragmentShader.glsl
varying vec2 vUv;
uniform float uProgress;
void main() {
float progress = uProgress;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

vertexShader.glsl
varying vec2 vUv; // 頂点シェーダから受け取る変数
#pragma glslify: noise2 = require(glsl-noise/simplex/2d);
#pragma glslify: noise3 = require(glsl-noise/simplex/3d);
#pragma glslify: exponential = require(glsl-easings/exponential-out);

uniform sampler2D uTexCurrent;
uniform sampler2D uTexNext;
uniform float uProgress;
uniform float uTick;
uniform vec2 uNoiseScale;

void main() {

float speed = exponential(uProgress);

// uNoiseScaleは2次元のベクトルのため、uNoiseScale.xとuNoiseScale.yをそれぞれ設定します。
float n = noise3(vec3(vUv.x * uNoiseScale.x, vUv.y * uNoiseScale.y, uTick * 0.01)) * (1.0 - speed);

vec4 texCurrent = texture(uTexCurrent, vUv + n);
vec4 texNext = texture(uTexNext, vUv + n);

gl_FragColor = mix(texCurrent, texNext, uProgress);
}

まとめ

この記事では、Three.jsとGLSLを使用してノイズ画像スライド効果を実現する方法を紹介しました。

カスタムシェーダーを使用することで、自由自在にエフェクトをコントロールすることができます!