テクスチャ

GPU から画像にアクセスする方法。sampler2D 型の uniformに JS 側がテクスチャを渡し、 シェーダ内で texture(sampler, uv) を呼ぶ。

サンプラ型

用途
sampler2D2D 画像
samplerCubeキューブマップ(6面)
sampler3D3D ボリュームテクスチャ(ES 3.00+)
sampler2DArray2D 配列(ES 3.00+)
sampler2DShadow影マップ(深度比較)

基本サンプリング

GLSL ES 3.00
uniform sampler2D uTex;
in vec2 vUv;
out vec4 outColor;

void main() {
  outColor = texture(uTex, vUv);
}
GLSL ES 1.00(Three.js でもこの記法は通る)
uniform sampler2D uTex;
varying vec2 vUv;
void main() {
  gl_FragColor = texture2D(uTex, vUv);
}

texture()vec4 を返す(RGBA)。

UV 座標の基本

UV を加工する

// 繰り返し
vec2 uv = vUv * 4.0;     // 4 タイル
outColor = texture(uTex, uv);

// スクロール
vec2 uv = vUv + vec2(uTime * 0.1, 0.0);

// 中心を基点にスケール
vec2 uv = (vUv - 0.5) * 0.5 + 0.5;

// Y 反転
vec2 uv = vec2(vUv.x, 1.0 - vUv.y);

// 回転
vec2 p = vUv - 0.5;
mat2 r = mat2(cos(uTime), -sin(uTime), sin(uTime), cos(uTime));
vec2 uv = p * r + 0.5;

JS から渡す(Three.js の場合)

Three.js の ShaderMaterial の uniforms{ value: texture } として渡す。

import { useTexture } from "@react-three/drei"

function Wall() {
  const tex = useTexture("/wall.jpg")
  return (
    <mesh>
      <planeGeometry args={[2, 2]} />
      <shaderMaterial
        uniforms={{ uTex: { value: tex } }}
        vertexShader={vs}
        fragmentShader={fs}
      />
    </mesh>
  )
}

カラースペース(重要)

Three.js r152+ はsRGB がデフォルト。テクスチャの種類で colorSpace を分ける必要がある。

この設定により、texture() の戻り値はリニア空間になる(自動でデガンマ)。 シェーダ内では普通にリニアで計算し、最終的な出力も Three.js が自動で sRGB に変換する。

wrap モード(GLSL では指定できない)

wrap モードは GLSL ではなくテクスチャ側のプロパティ。Three.js なら:

tex.wrapS = THREE.RepeatWrapping
tex.wrapT = THREE.RepeatWrapping
tex.repeat.set(4, 4)

ただしシェーダ内で自分で wrap したいなら fract() を使う:

vec2 uv = fract(vUv * 4.0);    // 自前で繰り返し
outColor = texture(uTex, uv);

mipmap

遠くの物体には縮小されたテクスチャを使うと、モアレが防げて速い。 Three.js は自動で mipmap を生成する(POT・NPOT 両対応)。

シェーダで明示的に LOD を指定したい時は textureLod:

// LOD 0 = 元解像度、上がるほど縮小
vec4 c = textureLod(uTex, vUv, 2.0);

ぼかしマップとして使う

textureLod(tex, uv, level)大きなぼかし画像が安く取れる。 環境マップのプレフィルタや、Bloom の擬似実装に使う。

典型パターン

普通に画像を貼る

outColor = texture(uTex, vUv);

UV スクロール

vec2 uv = vUv + vec2(uTime * 0.1, 0.0);
outColor = texture(uTex, uv);

波打つ歪み

vec2 uv = vUv;
uv.x += sin(vUv.y * 20.0 + uTime) * 0.02;
outColor = texture(uTex, uv);

2 枚をブレンド

vec4 a = texture(uTexA, vUv);
vec4 b = texture(uTexB, vUv);
outColor = mix(a, b, uMix);   // uMix は 0..1 の uniform

マスクで合成

vec4 base = texture(uBase, vUv);
vec4 over = texture(uOver, vUv);
float mask = texture(uMask, vUv).r;
outColor = mix(base, over, mask);

ノーマルマップを読む

ノーマルマップは RGB に法線の XYZ0..1 にエンコードしている。 サンプリング後に -1..1 に戻す:

vec3 nm = texture(uNormalMap, vUv).rgb;
vec3 normal = normalize(nm * 2.0 - 1.0);
// 多くの場合 TBN 行列でワールド/ビュー空間に変換

マットキャップ

// ビュー空間の法線を 0..1 の UV にマッピング
vec3 viewN = normalize(normalMatrix * vNormal);
vec2 mcUv = viewN.xy * 0.5 + 0.5;
outColor = texture(uMatcap, mcUv);

キューブマップ(環境マップ)

uniform samplerCube uEnv;

vec3 R = reflect(-V, N);          // 反射ベクトル
outColor = texture(uEnv, R);

Three.js では scene.environment に HDR を入れると、Standard / Physical Material が自動で参照する。 自前 ShaderMaterial で使うなら uniform samplerCube で受ける。

テクスチャサイズを取る

ピクセル単位の処理(畳み込みフィルタ等)には1 ピクセル分の UV オフセットが必要。

// GLSL ES 3.00
ivec2 size = textureSize(uTex, 0);
vec2 texel = 1.0 / vec2(size);

vec4 c = (
  texture(uTex, vUv + vec2(-texel.x, 0.0)) +
  texture(uTex, vUv + vec2( texel.x, 0.0)) +
  texture(uTex, vUv + vec2(0.0, -texel.y)) +
  texture(uTex, vUv + vec2(0.0,  texel.y))
) * 0.25;

畳み込み(ブラー等)

シンプルな box blur

vec3 blur(sampler2D t, vec2 uv, vec2 texel) {
  vec3 sum = vec3(0.0);
  for (int x = -2; x <= 2; x++) {
    for (int y = -2; y <= 2; y++) {
      sum += texture(t, uv + vec2(float(x), float(y)) * texel).rgb;
    }
  }
  return sum / 25.0;
}

分離可能なガウシアン(高速)

水平方向と垂直方向の2パスに分けると、N×N が 2N で済む。実用ではこれを使う。

動画テクスチャ

VideoTexturesampler2D として扱える。シェーダ側はただの画像。 Three.js 側で video.play() されていれば自動で更新される。

RenderTarget(FBO)から読む

前パスでレンダリングした結果をテクスチャとして読み、後段のパスで使う。

// fragment
uniform sampler2D tDiffuse;     // postprocessing で前パスの出力
varying vec2 vUv;

void main() {
  vec4 c = texture(tDiffuse, vUv);
  // 後処理...
  gl_FragColor = c;
}

整数テクスチャ・データテクスチャ

DataTexture任意のピクセル値を作って GPU に渡せる。 シェーダ側は sampler2D で読むだけ。LUT(Look-Up Table)や、CPU で計算した値を渡す時に。

よくあるバグ

テクスチャ取得のコスト

texture()ピクセルごとに毎回 GPU メモリにアクセスする。 1 fragment で 10 回も 20 回もサンプリングすると重い(特にモバイル)。 サンプリング数を減らすピクセル数を減らす(解像度を下げる)が基本の最適化。