テクスチャ
GPU から画像にアクセスする方法。sampler2D 型の uniformに JS 側がテクスチャを渡し、
シェーダ内で texture(sampler, uv) を呼ぶ。
サンプラ型
| 型 | 用途 |
|---|---|
sampler2D | 2D 画像 |
samplerCube | キューブマップ(6面) |
sampler3D | 3D ボリュームテクスチャ(ES 3.00+) |
sampler2DArray | 2D 配列(ES 3.00+) |
sampler2DShadow | 影マップ(深度比較) |
基本サンプリング
uniform sampler2D uTex;
in vec2 vUv;
out vec4 outColor;
void main() {
outColor = texture(uTex, vUv);
}
uniform sampler2D uTex;
varying vec2 vUv;
void main() {
gl_FragColor = texture2D(uTex, vUv);
}
texture() は vec4 を返す(RGBA)。
UV 座標の基本
- 0..1 の範囲。左下が
(0, 0)、右上が(1, 1) - 頂点に attribute として持たせ、varying で fragment に渡す
- 0..1 を超えた値はwrap モード(CSS で言う
background-repeat相当)に従う
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 を分ける必要がある。
- カラー画像(
map,emissiveMap等):texture.colorSpace = THREE.SRGBColorSpace - データ画像(
normalMap,roughnessMap等):texture.colorSpace = THREE.NoColorSpace(既定)
この設定により、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 に法線の XYZ を 0..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 で済む。実用ではこれを使う。
動画テクスチャ
VideoTexture も sampler2D として扱える。シェーダ側はただの画像。
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 で計算した値を渡す時に。
よくあるバグ
- テクスチャが真っ黒 — UV が範囲外、または colorSpace がおかしい、またはテクスチャがロードされる前にレンダリングされた
- 色が薄い・濃い — sRGB / リニアの混乱(
texture.colorSpace = THREE.SRGBColorSpaceを確認) - 境界がぼやける — 必要なら
texture.minFilter = THREE.NearestFilter - NPOT で wrap=Repeat が効かない(WebGL1) — WebGL1 は POT 限定。WebGL2 ならOK
- 動画テクスチャが更新されない —
video.play()されているか、autoplay muted playsinlineが付いているか - Y が反転 —
texture.flipYの設定。glTF / KTX2 は flipY=false 前提
texture() はピクセルごとに毎回 GPU メモリにアクセスする。
1 fragment で 10 回も 20 回もサンプリングすると重い(特にモバイル)。
サンプリング数を減らす、ピクセル数を減らす(解像度を下げる)が基本の最適化。