注意事項:
- この記事では WebGL1.0 のみを扱います。
- この記事ではフラグメントシェーダーのみを扱います。
- この記事の主な参考資料:The Book of Shaders
- 一部の画像は open.gl から引用されています。
- 以下のすべての例は、この Playground に貼り付けて直接実行できます。
本質#
上記のパイプラインの図からもわかるように、フラグメントシェーダーの本質は各(ピクセル)点を処理することです。皆さんもご存知の通り、GPU は並列の単純な計算が得意であり、その目的はこの作業をうまく行うことです。逆に、CPU は複雑な計算が得意ですが、プロセッサの数は非常に少ないです。
The Book of Shaders は、わかりやすい比喩を示しています:
uniform#
uniform
が uniform と呼ばれるのは、それが統一されたものであるからです。CPU から GPU に渡され、各ピクセルを処理する際に、この値は常に統一されています。対照的なのは varying
で、これらの値はバーテックスシェーダーによって計算され、フラグメントシェーダーに渡され、各三角形に対して受け取る値は一貫していません。
入力と出力#
uniform vec2 u_resolution;
void main() {
// `gl_FragCoord.xy/u_resolution` は xy 値を 0 から 1 にマッピングします
vec2 st = gl_FragCoord.xy/u_resolution;
gl_FragColor = vec4(st.x,st.y,0.0,1.0);
}
gl_FragCoord
は組み込みの入力で、点の位置を表します;gl_FragColor
は固定の出力値で、この点がどの色で表示されるべきかを示します。
マッピング#
重要な入力は位置で、重要な出力は色です。したがって、フラグメントシェーダーを使用して描画する際の重要なポイントは、位置と色の関係を見つけることです。言い換えれば、GPU が行うべきことは、各点の色を計算することです。別の言い方をすれば、あなたの知恵を使って、位置 x と y の関係を利用して美しい画像を生成することです。
では、なぜ 0 から 1 へのマッピングがそれほど重要なのでしょうか。なぜなら、WebGL の RGB 出力は 0 から 1 の範囲だからです。これは位置と色の関係を構築する最も直接的な方法です。さらに、smoothstep
や fract
のような重要な組み込み関数は、0 から 1 の範囲の値を返します。
上記の例では、まず gl_FragCoord
を 0
から 1
にマッピングし、次に x と y の値をそれぞれ r と g に配置します(左下隅は 0,0
です)。そのため、出力される画像の左下隅は黒、左上隅は赤、右下隅は緑になります。
P.S. WebGL2 の出力方法は異なります。
よく使う関数#
さまざまな華やかな図形を描くためには、これらのよく使う関数を理解する必要があります:
角度と三角関数関連#
指数関連#
よく使う数学的手法#
- abs() 絶対値
- sign(x) x を 0、1、-1 にマッピング
- floor() 切り捨て
- ceil() 切り上げ
- fract() 小数部分のみを取得
- mod(x, y) x の剰余
- min(x, y) 2 つの値のうち小さい方を返す
- max(x, y) 2 つの値のうち大きい方を返す
- clamp(x, minVal, maxVal) 最大最小値の範囲内でのみ変化し、範囲外では境界値を出力
- mix(x, y, a) x から y へのグラデーション、a は変化の「進行状況」
- step(edge, x) 突然変化し、x を 0 から 1 にマッピング、小さい場合は edge を返し、そうでなければ 1 を返す
- smoothstep(edge0, edge1, x) edge0 と edge1 の範囲内で、x(ハーミテインターポレーションを使用)を滑らかに0 から 1 にマッピングします。edge0 と edge1 の順序を変更すると効果が異なることに注意してください。
幾何学的関数#
- length() ベクトルの長さ
- distance() 2 点間の距離、実際には length とほぼ同じもので、解釈の角度が異なるだけです。
- dot() ベクトルの内積
- cross() ベクトルの外積
- normalize() 単位ベクトルを返す
- facefoward() 参照ベクトルと同じ方向を指すベクトルを返す
- reflect() 反射計算
- refract() 屈折計算
描画を始められますか!#
はい!上記の一連のよく使う関数を理解すれば、あなたは驚くべき画像を描くことができます!例えば、以下のような短いコードで溶岩ランプのような効果を作り出すことができます(効果を見たい場合はこちらをクリックしてください):
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, // (3.0-sqrt(3.0))/6.0
0.366025403784439, // 0.5*(sqrt(3.0)-1.0)
-0.577350269189626, // -1.0 + 2.0 * C.x
0.024390243902439); // 1.0 / 41.0
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i); // Avoid truncation effects in permutation
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
st.x *= u_resolution.x/u_resolution.y;
vec3 color = vec3(0.0);
vec2 pos = vec2(st*3.);
float DF = 0.0;
// ランダムな位置を追加
float a = 0.0;
vec2 vel = vec2(u_time*.1);
DF += snoise(pos+vel)*.25+.25;
// ランダムな位置を追加
a = snoise(pos*vec2(cos(u_time*0.15),sin(u_time*0.1))*0.1)*3.1415;
vel = vec2(cos(a),sin(a));
DF += snoise(pos+vel)*.25+.25;
color = vec3( smoothstep(.7,.75,fract(DF)) );
gl_FragColor = vec4(1.0-color,1.0);
}
冗談です、独自にこのような画像を考え出すにはまだまだ距離があります 😂
0 から 1 へのいくつかの方法#
実際の使用では、線形関係だけが存在するわけではないので、さまざまな関数を利用して xy と 0 から 1 へのマッピングを調整することは、フラグメントを作成する際に避けられないステップです。
The Book of Shaders では、著者が plot
関数を使用してマッピング関係を視覚化し、x 値のマッピングを gl_FragColor
に割り当てることで、異なる曲線の実際の効果を比較的直感的に見ることができます:
// Author: Inigo Quiles
// Title: Pcurve
#ifdef GL_ES
precision mediump float;
#endif
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;
// Function from Iñigo Quiles
// www.iquilezles.org/www/articles/functions/functions.htm
float pcurve( float x, float a, float b ){
float k = pow(a+b,a+b) / (pow(a,a)*pow(b,b));
return k * pow( x, a ) * pow( 1.0-x, b );
}
float plot(vec2 st, float pct){
return smoothstep( pct-0.02, pct, st.y) -
smoothstep( pct, pct+0.02, st.y);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution;
float y = pcurve(st.x,3.0,1.0);
vec3 color = vec3(y);
float pct = plot(st,y);
color = (1.0-pct)*color+pct*vec3(0.0,1.0,0.0);
gl_FragColor = vec4(color,1.0);
}
レンダリング結果は直接こちらをクリックして確認できます。
四角形と円#
float rect (vec2 st,float width){
// 左下
vec2 bl = step(vec2(0.5-width/2.),st);
float pct = bl.x * bl.y;
// 右上
vec2 tr = step(vec2(0.5-width/2.),1.0-st);
pct *= tr.x * tr.y;
return pct;
}
float circle(in vec2 _st, in float _radius){
vec2 dist = _st-vec2(0.5);
return 1.-smoothstep(_radius-(_radius*0.01),
_radius+(_radius*0.01),
dot(dist,dist)*4.0);
}
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 rectcolor = vec3(rect(st,.5));
vec3 circlecolor = vec3(1) - vec3(circle(st,.5));
gl_FragColor = vec4(circlecolor + rectcolor,1.0);
}
四角形と円は最も理解しやすい図形であり、上記のコードにはいくつか注意すべき点があります:
- 四角形のコードは少し長いですが、実際には乗算を使用して交差を作成することに注意すれば大丈夫です。
- 例で使用されている点積を使用して円を描画します。点積は 2 つのベクトルの投影関係であるため、自分自身に投影することは長さを計算する方法の一つです。
- もちろん、
length
、distance
、または 2 点間の距離計算を使用して円を描くこともできます。 - 円形(または曲線)を描くために
step
を使用することもできますが、smoothstep
を使用するとアンチエイリアス効果があります。 - プログラマーの良き友である抽象化は、常により効率的で簡潔なコードを実現するのに役立ちます。関数を調整することで複雑な図形を簡単に描くことができます(たとえ描画の原理を一時的に理解していなくても)。
小さな星球#
一部のモバイル画像処理アプリには「小世界」というフィルターがありますが、これは画像をデカルト座標から極座標にマッピングするものです。フラグメントシェーダーを使用して雪の結晶や歯車などの図形を描く場合、同様の原理に基づいて対応する「波形」を極座標にマッピングできます。コードは以下の通りです:
void main(){
vec2 st = gl_FragCoord.xy/u_resolution.xy;
vec3 color = vec3(0.0);
vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);
float f = cos(a*3.); // 3 つの花びら
// f = abs(cos(a*3.)); // 6 つの花びら
// f = abs(cos(a*2.5))*.5+.3; // 5 つの花びら
// f = abs(cos(a*12.)*sin(a*3.))*.8+.1; // 雪の結晶
// f = smoothstep(-.5,1., cos(a*10.))*0.2+0.5; // 歯車
color = vec3( 1.-smoothstep(f,f+0.02,r) );
gl_FragColor = vec4(color, 1.0);
}
a
は逆正接を使用して点と中心を結ぶ線の角度を求めます。これは元の X 軸に相当し、返される値は -PI から PI です。r
は元の Y 軸と理解できます。f
は Y 軸の値であり、color はsmoothstep
を使用してf
を分割し、f
より小さい場合は黒、大きい場合は白を表示します。- したがって、順当な書き方は実際には
float y = cos(x*3.);
です。 - 異なる関数を極座標にマッピングすることで、さまざまな興味深い形状を生成できます。
パターン#
ここでのパターンはデザインパターンではなく、繰り返しの図案を指します。以前に説明したのは単一の図案でしたが、どうやって整然とした大量の図案を生成するのでしょうか?
答えは fract()
です。
void main() {
vec2 st = gl_FragCoord.xy/u_resolution;
vec3 color = vec3(0.0);
st *= 3.0; // 空間を3倍に拡大
st = fract(st); // 1.0でラップ
// これで0-1の範囲で9つの空間が得られます
color = vec3(st,0.0);
// color = vec3(circle(st,0.5));
gl_FragColor = vec4(color,1.0);
}
人間の本質はリピートマシンであり、fract()
は GLSL のリピートマシンと言えるでしょう 😅。fract()
を使用することで、数値を 0 から 1 の間で繰り返し循環させることができます。
- まず、xy 値を 0 から 1 にマッピングします。
- 次に、それを 0 から 3 にマッピングします。
fract()
の処理を経ることで、0 から 1 の間で 3 回繰り返されるようになります。効果が現れます。
擬似ランダム#
画像を生成する際には、金銭取引などは関与しないため、擬似ランダムで十分です。真のランダム性はハードウェアノイズなどの予測不可能な入力を必要とするため、ここでは議論しません。
y = fract(sin(x)*1.0);
は擬似ランダムの基本(の一つ)です。連続した画像から見ると非常に規則的ですが、x を整数として取り、y を取得すると、普通の人間にはこれが普通の sin
であることを反応できないかもしれません。JavaScript を使用して整数をパラメータとして出力する「ランダム値」を出力してみましょう:
function fract(num) {
return Math.abs(num - Math.trunc(num))
}
function random(x) {
return fract(Math.sin(x) * 1)
}
const nums = []
for (let i = 1; i < 20; i++) {
nums.push(random(i))
}
// output=> [0.8414709848078965, 0.9092974268256817, 0.1411200080598672, 0.7568024953079282, 0.9589242746631385, 0.27941549819892586, 0.6569865987187891, 0.9893582466233818, 0.4121184852417566, 0.5440211108893698, 0.9999902065507035, 0.5365729180004349, 0.4201670368266409, 0.9906073556948704, 0.6502878401571168, 0.2879033166650653, 0.9613974918795568, 0.7509872467716762, 0.14987720966295234]
この基礎の上に、sin(x)
の乗数が大きくなるほど、結果は見た目上よりランダムに見えます:
今、y = fract(sin(x)*10000.0);
を使用して x を擬似ランダムの 0 から 1 にマッピングできることがわかりました。次は非常に簡単です。ピクセル位置をランダム数にマッピングし、そのランダム数を出力色として設定すれば、ランダムな画像が見られます。
float random (vec2 st) {
return fract(sin(dot(st.xy,
vec2(12.9898,78.233)))*
10000.0);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
// st = st * 15.;
// st = floor(st);
float rnd = random( st );
gl_FragColor = vec4(vec3(rnd),1.0);
}
上記のコードは雪の結晶のような効果を描画できます。注意すべき点は、random 関数内の「x」は xy 値とベクトルとの点積(投影長)を使用して得られます。モザイクを作成するのも非常に簡単で、以前のリピート方法を使用してマッピングすれば完了です(2 つのコメントを外すと効果が見えます)!
その他の関数#
行列関連#
ベクトル関連#
マテリアル関連#
ツール#
原文リンク:https://ssshooter.com/2022-10-12-fragment-shader/