頂点シェーダ(Vertex Shader)
各頂点について 1 回呼ばれ、最終的なクリップ空間座標 gl_Positionを計算するのが仕事。
ついでに各頂点ごとの値をfragment へ varying(out)で渡すのも担当する。
役割
- 入力: 頂点1つ分の
position/normal/uv等 - 処理: model行列 × view行列 × projection行列でクリップ空間に変換
- 出力1:
gl_Positionに書き込む(必須) - 出力2: fragment 用の値を
outで書く(任意、必要なら)
最小コード
Three.js の ShaderMaterial で動く最小例:
#version 300 es
in vec3 position;
in vec3 normal;
in vec2 uv;
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
out vec2 vUv;
out vec3 vNormal;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Three.js はこれらの uniforms / attributes を自動で渡すので、
実際にコードに書くのは main() 中身と out の宣言だけ。
varying vec2 vUv;
varying vec3 vNormal;
void main() {
vUv = uv;
vNormal = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
4つの座標空間
頂点シェーダの世界では位置が4つの段階を経る。混乱しがちなので意識すると整理しやすい。
| 空間 | 意味 | 得る方法 |
|---|---|---|
| 1. ローカル空間 | そのメッシュ内の座標 | position(attribute) |
| 2. ワールド空間 | シーン全体の座標 | modelMatrix * vec4(position, 1.0) |
| 3. ビュー空間 | カメラから見た座標 | viewMatrix * worldPos または modelViewMatrix * vec4(position, 1.0) |
| 4. クリップ空間 | 正規化された出力空間 | projectionMatrix * viewPos |
多くのライティング計算はワールド空間 or ビュー空間で行う。 どちらでやってもいいが、混在させると壊れるので統一する。
ワールド座標を得る
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPos = worldPos.xyz;
gl_Position = projectionMatrix * viewMatrix * worldPos;
ビュー座標を得る
vec4 viewPos = modelViewMatrix * vec4(position, 1.0);
vViewPos = viewPos.xyz;
gl_Position = projectionMatrix * viewPos;
法線の変換
法線は位置と同じ行列で変換してはいけない。スケールが入ると法線が崩れる。
normalMatrix(Three.js が自動で渡す mat3)を使う。
vec3 worldNormal = normalize(mat3(modelMatrix) * normal);
vec3 viewNormal = normalize(normalMatrix * normal); // Three.js
normalMatrix は transpose(inverse(modelViewMatrix)) の上 3×3。
非等方スケールがある時に正しく法線を変換できる。
varying(fragment へ渡す値)
fragment shader で使う値を頂点シェーダで書き出す。三角形の中で線形補間されて届く。
// vertex
out vec2 vUv;
out vec3 vNormal;
out vec3 vWorldPos;
void main() {
vUv = uv;
vNormal = normalize(mat3(modelMatrix) * normal);
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// fragment
in vec2 vUv;
in vec3 vNormal;
in vec3 vWorldPos;
名前と型を一致させるのが必須。typo するとリンクエラー。
補間の注意
varying は三角形の中で線形補間される。だが法線などは補間で「縮む」。
fragment 側で再度 normalize() するのが定石。
// fragment 側
in vec3 vNormal;
void main() {
vec3 N = normalize(vNormal); // 補間で縮んだのを戻す
...
}
flat / smooth / centroid(ES 3.00+)
補間方法を制御できる:
smooth out vec3 v;(既定) — 線形補間flat out int v;— 補間しない(最後の頂点の値が使われる)。整数を渡す時に必須centroid out vec2 v;— マルチサンプル時の重心補間
頂点を変形する
頂点シェーダで頂点位置を加工すると、波・歪み・揺らぎが作れる。
uniform float uTime;
void main() {
vec3 pos = position;
pos.z += sin(pos.x * 4.0 + uTime) * 0.2;
pos.z += sin(pos.y * 4.0 + uTime * 0.7) * 0.1;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
ジオメトリのセグメント数が頂点の細かさを決める。PlaneGeometry(2, 2, 64, 64) で 64×64 のグリッドになる。
頂点が少ないと滑らかな波にならない。
パーティクル的なバラ撒き
in float aRandom; // 頂点ごとのランダム値(attribute で渡す)
void main() {
vec3 pos = position;
pos.y += sin(uTime + aRandom * 6.28) * 0.5;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
頂点ごとの色(Vertex Color)
頂点に色 attribute を持たせて、fragment で受ける:
// vertex
in vec3 color; // attribute(geometry に setAttribute で設定)
out vec3 vColor;
void main() {
vColor = color;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
// fragment
in vec3 vColor;
out vec4 outColor;
void main() {
outColor = vec4(vColor, 1.0);
}
InstancedMesh / Instancing
InstancedMesh では各インスタンスに行列・色・データを持たせられる。gl_InstanceID も使える。
in vec3 position;
in mat4 instanceMatrix; // Three.js の InstancedMesh が渡す
void main() {
vec4 worldPos = modelMatrix * instanceMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewMatrix * worldPos;
}
gl_PointSize(Points 描画時)
THREE.Points で点群を描く時、頂点シェーダで点のピクセルサイズを指定する。
void main() {
vec4 mvPos = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPos;
gl_PointSize = 200.0 / -mvPos.z; // カメラに近いほど大きく
}
Skinning(スキンメッシュ)
ボーンのある GLTF モデルでは、各頂点が複数のボーンに重みで紐づく。
Three.js の SkinnedMesh は専用の chunkでスキニング計算を提供する(自前で全部書くのは大変)。
詳しくは onBeforeCompile パターンで必要な chunk を #include する。
頂点シェーダの典型パターン集
position をそのままワールドに(モデル行列なし)
gl_Position = projectionMatrix * viewMatrix * vec4(position, 1.0);
カメラと同じ向きを維持(ビルボード)
// modelViewMatrix の回転成分を消す
mat4 mv = modelViewMatrix;
mv[0].xyz = vec3(1.0, 0.0, 0.0) * length(mv[0].xyz);
mv[1].xyz = vec3(0.0, 1.0, 0.0) * length(mv[1].xyz);
mv[2].xyz = vec3(0.0, 0.0, 1.0) * length(mv[2].xyz);
gl_Position = projectionMatrix * mv * vec4(position, 1.0);
変形してから法線を再計算(高度)
頂点を動かしたら法線も合わせないと正しいライティングにならない。簡易的には2方向の差分から法線を求める:
vec3 displaced(vec3 p) {
return p + vec3(0.0, sin(p.x * 4.0 + uTime) * 0.2, 0.0);
}
void main() {
vec3 p = displaced(position);
vec3 px = displaced(position + vec3(0.001, 0.0, 0.0));
vec3 py = displaced(position + vec3(0.0, 0.0, 0.001));
vNormal = normalize(cross(px - p, py - p));
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
よくあるバグ
gl_Positionのwを 1.0 にしない —vec4(position, 1.0)を忘れて 0 になり消える- 変形しすぎてカメラのフラスタムから外れる —
frustumCulledをオフ、または手動でバウンディングを更新 - 法線を
modelMatrixで変換 — スケールが入ると崩れる。normalMatrixを使う - varying の名前が vertex/fragment で違う — リンクエラーで真っ黒
- セグメント数が足りない頂点変形 — 板を波打たせるなら
PlaneGeometry(w, h, 64, 64)等の細かさが必要
GPU は頂点ごとの並列計算が得意だが、頂点間で連携する処理は不得意。 隣接頂点を見て何かする、頂点同士で値を共有する、といった処理は無理(compute shader / FBO 経由なら可能だが大がかり)。