【GLSL】画像を切り替えエフェクト:遷移時にノイズをかけたり、歪ませたりする方法(デモあり)

WebGL & Three.js

WebGL(GLSL)を使って画像を切り替えエフェクト、遷移時にノイズをかける方法を紹介します。
またノイズを応用することで画像を歪ませながら遷移させることもできます。
合わせてデモをたくさん用意したので、まずは確認してみてください。

【GLSL】画像を切り替えエフェクト: 遷移時にノイズをかけるデモ








モーダルが開きます。その中の左上「画像反転」ボタンをクリックしてノイズを確認してください。

頂点シェーダーのコード

precision mediump float;

varying vec2 vUv;

void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

とくに特別なことはしませんので詳細は割愛します。
Three.jsやWebGLについてまったく知らない場合は、【WebGL&Three.js入門】Three.jsの一番シンプルなサンプルコードのチュートリアルの解説にて基本のコードを詳しく解説しているので参考にしてみてください。

JavaScriptのコード

import * as THREE from "three";
import vertexShader from "./vertex.glsl";
import fragmentShader from "./fragment.glsl";
import { gsap } from "gsap";

function initializeThreeJS() {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
  );

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setClearColor(0xffffff);
  document.body.appendChild(renderer.domElement);


  async function loadTex(url) {
    const texLoader = new THREE.TextureLoader();
    const texture = await texLoader.loadAsync(url);
    return texture;
  }

  const geometry = new THREE.PlaneGeometry(20, 20);
  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTexFirst: { value: await loadTex("/imgpath/1.jpg") },
      uTexSecond: { value: await loadTex("/imgpath/2.jpg") },
      uProgress: { value: 0 },
      uNoiseScale: { value: new THREE.Vector2(2, 2) },
    },
    vertexShader,
    fragmentShader,
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  camera.position.z = 30;

  document.querySelector(".js-image-flip").addEventListener("click", () => {
    gsap.to(material.uniforms.uProgress, {
      value: !Boolean(material.uniforms.uProgress.value),
      duration: 1.0,
      ease: "Power2.inOut",
    });
  });

  function animate() {
    requestAnimationFrame(animate);

    renderer.render(scene, camera);
  }

  animate();
}

ポイントを解説します。

uniformsに画像をセットする

  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTexFirst: { value: await loadTex("/imgpath/1.jpg") },
      uTexSecond: { value: await loadTex("/imgpath/2.jpg") },
      uProgress: { value: 0 },
      uNoiseScale: { value: new THREE.Vector2(2, 2) },
    },
    vertexShader,
    fragmentShader,
  });

今回使う画像が2枚になるので、それぞれuTexFirst, uTexSecondとしてvalueに値をセットします。
またuProgressで画像の切り替えを行い、uNoiseScaleでノイズの大きさを調整します。

ノイズの大きさを指定する

      uNoiseScale: { value: new THREE.Vector2(2, 2) },

上の値なら下くらいの大きなノイズになります。

      uNoiseScale: { value: new THREE.Vector2(2, 50) },

ならX軸に大きくなります。



      uNoiseScale: { value: new THREE.Vector2(50, 2) },

上ならY軸に大きくなります。

もっとノイズを細かくしたい場合は以下のように大きな値を設定します。

      uNoiseScale: { value: new THREE.Vector2(70, 70) },

画像を反転させる

  document.querySelector(".js-image-flip").addEventListener("click", () => {
    gsap.to(material.uniforms.uProgress, {
      value: !Boolean(material.uniforms.uProgress.value),
      duration: 1.0,
      ease: "Power2.inOut",
    });
  });

uProgressの値を0か1にすることで画像を反転させることができます。
その理屈はフラグメントシェーダー部分で詳しくみるとして、ここではその方法として、gsapを使っています。
gsapについては、gsap.toメソッドと fromメソッドの使い方などいろいろな記事があるので参考にしてみてください。
valueの値をBooleanにすることで0か1になり1枚目と2枚目の画像をボタンクリックで反転させることができます。

フラグメントシェーダーのコード

precision mediump float;

#pragma glslify: noise2 = require(glsl-noise/simplex/2d);

varying vec2 vUv;
uniform sampler2D uTexFirst;
uniform sampler2D uTexSecond;
uniform float uProgress;
uniform vec2 uNoiseScale;

void main() {
  // nの取りえる範囲を調べる => -1 ~ 1
   float n = noise2(vec2(vUv.x * uNoiseScale.x, vUv.y * uNoiseScale.y));
  //  step()で利用しやすい -1 ~ 0 の値に加工する
  n = n * 0.5 - 0.5;
  // uProgressを足すと、uProgress=1のとき0 ~ 1になる
  n = n + uProgress;
  //  0か1のどちらかを返したい場合はstep()を使う
  //
  n = step(0.0, n);

  vec4 texFirst = texture(uTexFirst, vUv);
  vec4 texSecond = texture(uTexSecond, vUv);

  // gl_FragColor = texFirst;
  gl_FragColor = mix(texFirst, texSecond, n);
  // gl_FragColor = vec4(n, n, n, 1.0);
}

uniformで値を使えるようにする

uniform sampler2D uTexFirst;
uniform sampler2D uTexSecond;
uniform float uProgress;
uniform vec2 uNoiseScale;

uniformsに登録したものをuniformを使ってフラグメントシェーダーで使えるようにします。

画像をそれぞれ設定する

  vec4 texFirst = texture(uTexFirst, vUv);
  vec4 texSecond = texture(uTexSecond, vUv);

描画する値をmix関数で切り分ける

gl_FragColor = mix(texFirst, texSecond, n);

nを基準にして1枚目、それ以外を2枚目の部分を表示したいので、まずはmix関数を使って1枚目以外のところが2枚目を表示できるようにします。
nは次に説明します。

0か1などの決まった二つの値を返すようにstep関数を使う

  // nの取りえる範囲を調べる => -1 ~ 1
   float n = noise2(vec2(vUv.x * uNoiseScale.x, vUv.y * uNoiseScale.y));
  //  step()で利用しやすい -1 ~ 0 の値に加工する
  n = n * 0.5 - 0.5;
  // uProgressを足すと、uProgress=1のとき0 ~ 1になる
  n = n + uProgress;
  //  0か1のどちらかを返したい場合はstep()を使う
  //
  n = step(0.0, n);

nが0か1などの決まった二つの値を返すようにstep関数します。

glslifyのnoise2という関数はとりえる範囲が-1から1のため、その値を扱いやすい形式に変更します。
そして、uProgressが0から1なので、増加を1にしたいので現状-1から1と2の幅があるため半分にします。

n = n * 0.5;

stepの値を0.0で管理したい場合は、上のようにさらに-0.5してずらしてあげるといいです。
stepの値を0.5で管理する場合は以下のようになります。

  // nの取りえる範囲を調べる => -1 ~ 1
   float n = noise2(vec2(vUv.x * uNoiseScale.x, vUv.y * uNoiseScale.y));
  //  step()で利用しやすい -1 ~ 0 の値に加工する
  n = n * 0.5;
  // uProgressを足すと、uProgress=1のとき0 ~ 1になる
  n = n + uProgress;
  //  0か1のどちらかを返したい場合はstep()を使う
  //
  n = step(0.5, n);

こちらでもどうように機能します。
これでstepによって0か1かをnが返すようになります。

遷移時に画像を歪ませるJavaScriptのコード

  const material = new THREE.ShaderMaterial({
    uniforms: {
      ...
      uTexMid: { value: await loadTex('/imagepath/img.jpg') },
      ...
    },
    vertexShader,
    fragmentShader,
  });

遷移時に歪ませるためには、歪みを加える必要があります。
フラグメントシェーダーで色の情報を足すことができるので、
色の情報を持つ白黒画像などを用意して、それを遷移中の間に色情報を足してあげることで画像を歪ませることができます。
そのため、上部のJavaScriptに上のJavaScriptを加えます。
新しい画像を設定しています。
こちらに設定する画像次第でいろいろな表情の歪みを実現することができます。
建物
上の画像を使用すると以下のような見た目になります。

雲
上の画像を使用すると以下のような見た目になります。

歪ませるためのフラグメントシェーダーのコード

precision mediump float;

#pragma glslify: noise2 = require(glsl-noise/simplex/2d);

varying vec2 vUv;
uniform sampler2D uTexFirst;
uniform sampler2D uTexSecond;
uniform sampler2D uTexMid;
uniform float uProgress;
uniform vec2 uNoiseScale;
float parabola( float x, float k ) {
  return pow( 4. * x * ( 1. - x ), k );
}

void main() {
  // nの取りえる範囲を調べる => -1 ~ 1
   float n = noise2(vec2(vUv.x * uNoiseScale.x, vUv.y * uNoiseScale.y));
  //  step()で利用しやすい -1 ~ 0 の値に加工する
  n = n * 0.5 - 0.5;
  // uProgressを足すと、uProgress=1のとき0 ~ 1になる
  n = n + uProgress;
  //  0か1のどちらかを返したい場合はstep()を使う
  //
  n = step(0.0, n);

  // 歪ませる画像
  vec4 texMid = texture(uTexMid, vUv);
  // 白を使う場合
  float distortion = texMid.r;
  // 黒を使う場合
  // float distortion = 1.0 - texMid.r;

  distortion = distortion * parabola(uProgress, 1.0);

  vec4 texFirst = texture(uTexFirst, vUv + distortion);
  vec4 texSecond = texture(uTexSecond, vUv + distortion);

  gl_FragColor = mix(texFirst, texSecond, uProgress);
}

歪ませる画像を設定

まずフラグメントシェーダで扱えるように受け取ります。

uniform sampler2D uTexMid;
  // 歪ませる画像
  vec4 texMid = texture(uTexMid, vUv);

こちらでテクスチャーにします。

色情報を利用して白か黒を取り出す

  // 白を使う場合
  float distortion = texMid.r;
  // 黒を使う場合
  float distortion = 1.0 - texMid.r;

上のようにすると、真ん中の画像の白(1の値)もしくは黒(0の値)の情報を取得することができます。
白黒画像ならrgbで白はどれも1ですのでrである必要はありませんが、上のような形でrなどの情報をとって歪みに利用するという手法があります。

遷移中の真ん中の画像を表示するparabola関数

float parabola( float x, float k ) {
  return pow( 4. * x * ( 1. - x ), k );
}

parabola関数は山のような形の描く関数です。

  distortion = distortion * parabola(uProgress, 1.0);

上のように使用することで、0.5のときにちょうど真ん中の画像が表示されるようになります。

歪みを画像に混ぜ込む

  vec4 texFirst = texture(uTexFirst, vUv + distortion);
  vec4 texSecond = texture(uTexSecond, vUv + distortion);

  gl_FragColor = mix(texFirst, texSecond, uProgress);

上で作成した歪み(distortion)をそれぞれの画像に足し合わせたり引いたりして希望の歪みを実現します。

歪みのレベルを調整したい場合

// 歪みを調整
  float distortionLevel = 0.1;
  // y軸にゆがかせたい
  vec2 distortionUv = vec2(vUv.x, vUv.y + distortion * distortionLevel);
  vec2 distortionUv2 = vec2(vUv.x, vUv.y - distortion * distortionLevel);

上のようにしてvUVの値に掛け合わせてあげることで歪みを小さくしたり大きくしたりできます。

X軸やY軸方向にだけ歪ませたい場合


  // 歪みを調整
  float distortionLevel = 0.1;
  // y軸にゆがかせたい
  vec2 distortionUv = vec2(vUv.x, vUv.y + distortion * distortionLevel);
  vec2 distortionUv2 = vec2(vUv.x, vUv.y - distortion * distortionLevel);
  // x軸にゆがかせたい
  vec2 distortionUv = vec2(vUv.x - distortion, vUv.y);
  vec2 distortionUv2 = vec2(vUv.x - distortion, vUv.y);

X軸だけやY軸だけに歪みを付け加えたい場合は、vUvのx軸とy軸に分けて歪みを加えたい方にdistortion(歪み)を足したり引いたりしてあげることで実現できます。