前置き
vertex shaderで出力した値は線形補間されてfragment shaderに渡されるという話をします。
以前のバージョンでvarying変数と呼ばれていたものの話です。
p5.jsを使った例で説明しますが、p5.jsを使っていない方にも役立つと思います。
これわかってないとシェーダープログラミングなんてできないだろってくらい基本的なことだと思うのですが、検索しても妙に資料が少なく、これだけのことを突き止めるのにも苦労したので記事にしておきます。
解決したい疑問点
vertex shaderは頂点ごとに実行される
fragment shaderはピクセルごとに実行される
ということは例えば、3つの頂点を持つ三角形を描画する場合、vertex shaderは3回、fragment shaderはその三角形内のピクセルの数だけ実行される(例えば1万回くらい)と考えられる。
そして、vertex shaderからfragment shaderへ値を渡すことができる。(これは、以前varying変数と呼ばれていたものですが、現状は名前がないので、この記事ではvarying変数と呼ぶことにします。)
ここで、もしvertex shaderが3回とも違う値をvarying変数として出力したら、fragment shaderには何が渡るのでしょうか?
fragment shaderは1万回実行されるされるわけですが、どの回でどの値を受け取るのか?
vertex shaderで出力した値は線形補間されてfragment shaderに渡される
結論はこれです。線形補間されます。Linear Interpolation、略してlerpとも言いますね。
1次元の例ですと、例えば頂点0はvertex shaderで値10.0を割り当てられ、頂点1は値20.0を割り当てられたとします。
すると、fragment shaderの側では、頂点0と頂点1のちょうど真ん中にあるピクセルを描画するときには値15.0を受け取ることになる。という感じです。
真ん中より頂点0に近いピクセルでは12.0あたりの値を受け取っているピクセルもあるでしょうね。
線形補間は2次元で行われる
実際の線形補間は、三角形内のピクセルに割り当てる値を、3頂点の情報をもとに計算します。
わかる人向けに数式で説明すると…
3頂点の位置ベクトルを順に$\vec{x_0},\vec{x_1},\vec{x_2}$とします。
で、これら3頂点に割り当てる値を順に$v_0,v_1,v_2$,とします。これらはスカラーでもベクトルでもいいです。
さて、この3頂点を含む平面内の任意の点の位置ベクトルは$s+t+u=1$を満たす$s,t,u$を用いて
$s\vec{x_0}+t\vec{x_1}+u\vec{x_2}$
と一意に表すことができます。そして、この点に割り当てられる値は、この$s,t,u$を用いて
$sv_0+tv_1+uv_2$
と決定されます。
コード例
コード例とその出力結果を上げておきます。
まずはhtml。htmlではp5.jsとscript.jsを呼んでいるだけで、あとはテンプレです。読み飛ばしてもOK。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.8.0/p5.js"></script>
</head>
<body>
<script src="script.js"></script>
</body>
</html>
続いてJavaScript。p5.jsを用いて頂点を3つ打ち、三角形を描画します。p5.jsはインスタンスモードで使ってます。
window.onload = function () {
const sketch = (p5) => {
let myShader; // p5.Shaderを格納する変数
p5.preload = () => {
// preload内でシェーダを読み込む
myShader = p5.loadShader('./shader.vert', './shader.frag');
}
p5.setup = () => {
p5.createCanvas(400, 400, p5.WEBGL);
p5.noStroke();
}
p5.draw = () => {
p5.background(0);
p5.shader(myShader); // 使うシェーダを指定
p5.beginShape(p5.TRIANGLES); // 三角形を描画
p5.vertex(0.0, 0.5); // gl_VertexID == 0
p5.vertex(-0.5, -0.5); // gl_VertexID == 1
p5.vertex(0.5, -0.5); // gl_VertexID == 2
p5.endShape();
}
}
new p5(sketch);
};
続いてvertex shader。3つの頂点に順に赤、緑、青を割り当ててvColorに代入し、fragment shaderに渡します。
色はベクトルとして表現されています。なので線形補間できますね。
#version 300 es
precision mediump float;
in vec3 aPosition;
out vec4 vColor;
void main() {
gl_Position = vec4(aPosition, 1.0);
int r = gl_VertexID; // gl_VertexIDは古いバージョンでは使えないので注意
// 3つの頂点に赤、緑、青の順に割り当ててvColorに代入し、
// フラグメントシェーダに渡します。
vColor =
r == 0 ? vec4(1.0, 0.0, 0.0, 1.0) :
r == 1 ? vec4(0.0, 1.0, 0.0, 1.0) :
vec4(0.0, 0.0, 1.0, 1.0);
}
最後にfragment shader。線形補間されて渡されたベクトルを、色としてそのまま出力しているだけです。
#version 300 es
precision mediump float;
// このvColorに渡ってくる値は線形補完されている!
in vec4 vColor;
out vec4 fragColor;
void main() {
fragColor = vColor; // 渡ってきた値をそのまま出力
}
描画結果
3頂点に赤、緑、青が割り当てられ、その間のピクセルは色が線形補間されていることがわかりますね!
色以外にも、座標を渡したり、いろいろできます。
