【GLSL】画像の色の入れ替え、反転、座標をずらす、ゆっくり変化させる方法(デモ付き)

2023年12月6日WebGL & Three.js

WebGL(GLSL)のフラグメントシェーダで色を入れ替えたり、色を反転させたり、色の座標をずらしたり、ゆっくりと色を変化させたり、画像を繰り返したりする方法を紹介します。
元画像の表示から分かるように7つほどデモを用意したのでまずはデモを確認してみてください。

フラグメントシェーダで色を入れ替えたりゆっくり変化させるデモ







JavaScriptのソースコード

import * as THREE from "three";
import vertexShader from "./vertex.glsl";
import fragmentShader from "./fragment.glsl";
initializeThreeJS();
export default async 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(0x3d3d3d);
  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: {
      uTex: { value: await loadTex("/imgpath/img.jpg") },
      uTick: { value: 0 },
    },
    vertexShader,
    fragmentShader,
    transparent: true,
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);

  camera.position.z = 30;

  function animate() {
    requestAnimationFrame(animate);

    //  時間で変化する値
    material.uniforms.uTick.value++;
    renderer.render(scene, camera);
  }

  animate();
}

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

JavaScript部分のポイントを説明します。

時間経過を反映させられる変数uTickを追加

  const material = new THREE.ShaderMaterial({
    uniforms: {
      uTex: { value: await loadTex("/img/original/bld1.jpg") },
      uTick: { value: 0 },
    },
    vertexShader,
    fragmentShader,
    transparent: true,
  });

uTickのvalueの値を変化させることで時間経過を利用したアニメーションが可能になります。

  function animate() {
    ...
    //  時間で変化する値
    material.uniforms.uTick.value++;
    ...
  }

上のようにしてuTIckのvalueに対して値をセットしていけばOKです。

頂点シェーダーのソースコード

varying vec2 vUv;

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

頂点シェーダーのコードです。
こちらは共通でとくに真新しいことはしていません。

モデル(元画像を表示)のフラグメントシェーダーのソースコード

varying vec2 vUv;
uniform sampler2D uTex;
uniform float uTick;

// 画像を表示
void main() {
    vec4 texColor = texture(uTex, vUv);

    gl_FragColor = texColor;
}

単純に上のようにすることで画像を表示することができます。

このあたりの詳しい説明や画像をShaderMaterialを使って出力する方法については、【WebGL】Three.jsのShaderMaterialを使って画像や動画を出力する方法(デモ付き)で詳しく解説しているので確認してみてください。

モデルを表示(色をゆっくり変化させる)のfragment.glslのソースコード

...
    float speed = 0.009;
    float sine_wave_adjust = 0.5;
    float time = uTick * speed;

    gl_FragColor = vec4(0.0, sin(time) * sine_wave_adjust + sine_wave_adjust, 0.0, 1.0);
}

JavaScriptのrequestAnimationFrameを単に使ってしまうと1秒間に60回など数が変化して早すぎるため、
一般的にはスピードを調整します。
その色の変わるスピードがspeedの値で、ここは好みや制作の意図によって変更します。
sine_wave_adjustの値は、sineの曲線を0以上から始めることによって、色を0から1に変化させられるようにしています。
sineなどの三角関数については、必要な最低限の知識部分のみ【WebGL】GLSLで使う数学(三角関数、ベクトル)を文系エンジニアのために実務でいるところだけ解説で詳しく解説しているので確認してみてください。

モデル(色を入れ替える)のフラグメントシェーダーのソースコード

...
void main() {
    float speed = 0.01;
    vec4 color = vec4(0.0, 0.0, 0.0, 1.0);
    vec4 texColor = texture(uTex, vUv);

    color.r = texColor.b;
    color.g = texColor.r;
    color.b = texColor.g;
    gl_FragColor = color;
}

colorにまずデフォルト値をいれて定義しています。
そのcolorにUV座標ごとに色を取得するtexColorのrgbaを好きなように当てこみます。
rgbaの入れ替え部分(color.r = texColor.b;)はcolorのR(赤色)にtexColorのB(青色)の値を代入するということです。
それをgl_FragColorで表示すれば色を入れ替えた画像を出力することができます。

モデル(色を入れ替えつつ、ゆっくり変化させる)のフラグメントシェーダーのソースコード

...
void main() {
    float speed = 0.01;
    float sine_wave_adjust = 0.5;
    float time = uTick * speed;
    vec4 color = vec4(0.0, 0.0, 0.0, 1.0);
    vec4 texColor = texture(uTex, vUv);

    color.r = texColor.b * sin(time) * sine_wave_adjust + sine_wave_adjust;
    color.g = texColor.r * cos(time) * sine_wave_adjust + sine_wave_adjust;
    color.b = texColor.g * cos(time) * sine_wave_adjust + sine_wave_adjust;
    gl_FragColor = color;
}

上で説明している色を入れ替えとゆっくり変化させるを合わせたバージョンがこちらです。
sinやcosなどの三角関数のカーブを掛け合わせることで色をいろんな形で変化させることが可能になります。

モデル(元画像の色反転)のフラグメントシェーダーのソースコード

...
void main() {
    float speed = 0.01;
    float sine_wave_adjust = 0.5;
    float time = uTick * speed;
    vec4 color = vec4(0.0, 0.0, 0.0, 1.0);
    vec4 texColor = texture(uTex, vUv);

    // color.r = 1.0 - texColor.r;
    // color.g = 1.0 - texColor.g;
    // color.b = 1.0 - texColor.b;

    // 上を省略した書き方
    color.rgb = 1.0 - texColor.rgb;
    gl_FragColor = color;
}

色を反転させたい場合は、0から1未満の値がtexColorのrgbaに入っているので、1から引いてあげることで反転した数値を取得することができます。
その値をcolorに代入すればOKです。

    color.r = 1.0 - texColor.r;
    color.g = 1.0 - texColor.g;
    color.b = 1.0 - texColor.b;

上のようにかいてもいいですし、sinやcosなど掛け算とかせずにrgbaのまま記述する場合は、以下のように省略して書くことができます。

color.rgb = 1.0 - texColor.rgb;

上のようにまとめて記述することができます。

モデル(色の座標をずらす)のフラグメントシェーダーのソースコード

...
void main() {
    float speed = 0.02;
    float time = uTick * speed;
    const float AMPLITUDE = 0.02; // 振れ幅
    vec4 color = vec4(0.0, 0.0, 0.0, 1.0);

    // color.g = texture(uTex, vUv).g;

    // color.r = texture(uTex, vUv + vec2(0.2, 0.2)).r;
    // color.g = texture(uTex, vUv + vec2(-0.2, 0.0)).g;
    // color.b = texture(uTex, vUv + vec2(0.2, 0.2)).b;

    color.r = texture(uTex, vUv + vec2(sin(time) * AMPLITUDE, -sin(time) * AMPLITUDE)).r;
    color.g = texture(uTex, vUv + vec2(-sin(time) * AMPLITUDE, 0.0)).g;
    color.b = texture(uTex, vUv + vec2(sin(time) * AMPLITUDE, sin(time) * AMPLITUDE)).b;

    gl_FragColor = color;
}

rgbaの特定の色だけを制御する

colorの初期化後、たとえばrgbaのgだけ色を濃くしたい場合は以下のようにします。

vec4 color = vec4(0.0, 0.0, 0.0, 1.0);
color.g = texture(uTex, vUv).g;

そうすると以下のようなみどりっぽい画像になります。

textureは色情報のrgbaを返すので本来なら4次元ですが、末尾にほしい色情報を指定することで、1次元などにすることができます。

色の座標をずらす

続いて、色の座標をずらしたい場合は以下のようにします。

    color.r = texture(uTex, vUv + vec2(0.2, 0.2)).r;
    color.g = texture(uTex, vUv + vec2(-0.2, 0.0)).g;
    color.b = texture(uTex, vUv + vec2(0.2, 0.2)).b;

メッシュの位置をずらすのではなく、vUVで取得する色情報をずらして取得することができます。
以下が画像の色の座標をずらして取得した画像です。

ずらしたら画像の範囲外の色はどうやって決まるの?という点ですが、それは後述します。

色の座標をずらしながら、ゆっくり変化させる

    ...
    const float AMPLITUDE = 0.02; // 振れ幅
    ...
    color.r = texture(uTex, vUv + vec2(sin(time) * AMPLITUDE, -sin(time) * AMPLITUDE)).r;
    color.g = texture(uTex, vUv + vec2(-sin(time) * AMPLITUDE, 0.0)).g;
    color.b = texture(uTex, vUv + vec2(sin(time) * AMPLITUDE, sin(time) * AMPLITUDE)).b;

このように振れ幅を設定して、上で説明したようにsineカーブやcosカーブを使って反復させることができます。

実際の動きや見た目はデモをぜひ参照してみてください。

ずらしたら画像の範囲外の色はどうやって決まるの?(ClampToEdgeWrapping・RepeatWrapping・MirroredRepeatWrapping)

Three.jsでは画像の色情報がないときに、どのようにその部分を出力するかを設定することができます。
JavaScriptの以下の部分をみてみましょう。

  async function loadTex(url) {
    const texLoader = new THREE.TextureLoader();
    const texture = await texLoader.loadAsync(url);
    // X軸(wrapS)
    // Y軸(wrapT)
    // ClampToEdgeWrapping: デフォルトはこれ。端の色をリピート
    // RepeatWrapping: 左端から同じ画像をリピート
    // MirroredRepeatWrapping: 反転して左端から同じ画像をリピート
    texture.wrapS = THREE.ClampToEdgeWrapping;
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapS = THREE.MirroredRepeatWrapping;
    return texture;
  }

画像の色情報がないときに、どのようにその部分を出力するかはテクスチャで設定することができます。
X軸(wrapS)かY軸(wrapT)を選択して、それに以下の3種類のどれかを代入します。

  • ClampToEdgeWrapping: デフォルトはこれ。端の色をリピート
  • RepeatWrapping: 左端から同じ画像をリピート
  • MirroredRepeatWrapping: 反転して左端から同じ画像をリピート

何もしてしていない状態なら、端の色をリピートするClampToEdgeWrappingが適用されるため、
上の色の座標をずらすアニメーションではずらした部分が端の色をずっとリピートして適用されている状態でした。

画像をリピートする(fract関数)フラグメントシェーダーのソースコード

...
void main() {
    float speed = 0.02;
    float time = uTick * speed;
    const float AMPLITUDE = 0.02; // 振れ幅
    const float REPEAT_NUM = 5.0;
    vec4 color = vec4(0.0, 0.0, 0.0, 1.0);

    // vUvは二次元(x,y)なのでx,y軸ともに繰り返される
    color = texture(uTex, fract(vUv * REPEAT_NUM));

    // リピート数を分けたいとき
    // vec2 xUv = vUv;
    // xUv.x = fract(xUv.x * REPEAT_NUM);
    // xUv.y = fract(xUv.y * 3.0);
    // color = texture(uTex, xUv);

    gl_FragColor = color;
}

fract関数は、小数点の値のみを返す関数です。
そのため、整数になるまで増加して、整数値つまり小数点が0になると0を返します。
これを利用することで、画像を繰り返し描写することができます。

    // vUvは二次元(x,y)なのでx,y軸ともに繰り返される
    color = texture(uTex, fract(vUv * REPEAT_NUM));

上のようにvUvは二次元のため、X軸、Y軸ともに繰り返しになります。

X軸とY軸で別々の数を繰り返したい


もしX軸とY軸で別々の数を繰り返したい場合は、以下のようにします。

    // リピート数を分けたいとき
    vec2 xUv = vUv;
    xUv.x = fract(xUv.x * REPEAT_NUM);
    xUv.y = fract(xUv.y * 3.0);
    color = texture(uTex, xUv);

vUvを別の変数に代入して、その変数のxとyにそれぞれ別の値を掛け合わせれば、それぞれの数繰り返すことができます。