湯を沸かすほどの熱いGLSL

More than 1 year has passed since last update.

この記事は、早稲田大学 AdventCalender 2016 14日目です。

とあるGLSLおじさんがフラグメントシェーダで温泉を掘る話をします:hotsprings:

最終的に掘れた温泉はこちらです。PCでの閲覧を推奨。

アウトプット

(湯に見えなくても、「これは湯だ...」と言い聞かせましょう)


はじめに


  • 本記事は、約1ヶ月のGLSL学習をまとめることが目的です。

  • 簡易的にコードを掲載するために、The Book of Shadersのオンラインエディタを使用しています。解説等は、こちらにコメントで掲載します。


GLSLとは

C言語をベースとしたシェーディング言語です。

詳しくは偉大な先人たちの書物をお読みください。

- wgld.org

- GPU で暖を取りたい人のための GLSL Advent Calendar 2016


本編


あらすじ

おじいさんは山へ柴刈りに、おばあさんは川へ洗濯に行きました。

そして、GLSLおじさんは火山地帯へ温泉を掘りに行きましたとさ...:peach:


掘りやすそうな土壌を探す

GLSLおじさんが火山地帯へ到着すると、そこは一面、砂利(灰色)ばかり。彼は、温泉を掘るためにスコップを入れやすそうな土(茶色)の地面を探し始めました:mag:

precision mediump float;

void main(){
vec3 gravel = vec3(0.3, 0.3, 0.3); // 砂利のRGB
vec3 soil = vec3(0.7, 0.5, 0.3); // 土のRGB

//-------------------------
// 代入値をsoilに変更する
//-------------------------
vec3 color = gravel;

gl_FragColor = vec4(color, 1.0); // 出力する色
}

実行結果はこちら

soil


[ 解説 ]

gl_FragColorに、最終的に出力される色を4次元ベクトルとして代入します。4次元である理由は、線形代数の平行移動処理云々が関わってくるらしいのですが、割愛します。ちなみに今回は1面ベタ塗りですが、フラグメントシェーダでは1つ1つのピクセルごとに描画処理が走ります。


目からビームで穴を掘る

GLSLおじさんは、目からビーム(ray)を放つことができます。いやぁ、すごいですね! それでは、地面にビームを当てて、穴を掘りましょう。(砂利の地面でも良かったのでは...?):cyclone:

precision mediump float;

uniform vec2 u_resolution;

//===================================
// ・レイマーチングの参考
// https://wgld.org/d/glsl/g008.html

// ・距離関数
// http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
//===================================

// 距離関数(sphere)
float dHole(vec3 p) {
return length(p) - 1.0;
}

//===================================
// ・メイン関数
//===================================

void main() {
// 色の宣言
vec3 sand = vec3(0.7, 0.5, 0.3);
vec3 hole = vec3(0.4, 0.2, 0.0);

// colorの初期化
vec3 color = vec3(0.0);

// 座標の正規化(range: -1.0 ~ 1.0)
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

// カメラのセッティング
vec3 cPos = vec3(0.0, 0.0, 2.0); // カメラの位置(GLSLおじさんの立ち位置)
vec3 cDir = vec3(0.0, 0.0, -1.0); // カメラの注視点(GLSLおじさんの視点)
vec3 cUp = vec3(0.0, 1.0, 0.0); // カメラの上方向(GLSLおじさんの頭上)
vec3 cSide = cross(cDir, cUp); // 外積を使って横方向を算出
float targetDepth = 1.0; // 深度

// レイの生成
vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);

// レイマーチングループ
float dist = 0.0; // レイと穴の距離
float rLen = 0.0; // レイの長さ
vec3 rPos = cPos; // レイの先端
for (int i = 0; i < 8; i++) {
dist = dHole(rPos);
rLen += dist;
rPos = cPos + ray * rLen;
}

// 衝突判定で色の出しわけ
if (abs(dist) < 0.001) {
color = hole;
} else {
color = sand;
}

// 最終的に該当ピクセルに出力する色
gl_FragColor = vec4(color, 1.0);
}

hole


[ 解説 ]

円を書くだけであれば、length関数やstep関数を使えば実装できます。しかし今回は、後ろの項目で「光(ray)」を使うため、レイマーチングで実装しています。レイマーチングとは、その名の通り、光を徐々に進めて、光と物体の衝突判定を行う手法です。これにより、光が物体に衝突したか否かで処理を書き分けることが可能になります。

(注)

ちなみに今回は「穴」と称していますが、描かれているのは立体的な「球体」です。そのため、法線情報を使えば、陰を落として立体的に見せることもできます。


お湯が湧き出てきた

GLSLおじさんが穴を掘り続けること数十分、ようやく湯が湧き出てきました。季節は冬ですから、早く浸かりたいですね。:hotsprings:

precision mediump float;

uniform vec2 u_resolution;
uniform float u_time;

//===================================
// ・ランダム / ノイズ / 非整数ブラウン運動
// https://thebookofshaders.com/13/?lan=jp
//===================================

float random (in vec2 p) {
return fract(sin(dot(p.xy,vec2(12.9898,78.233)))*43758.5453123);
}

float noise (in vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

#define OCTAVES 4
float fbm (in vec2 p) {
float value = 0.0;
float amplitud = 0.1;
float frequency = 0.0;
for (int i = 0; i < OCTAVES; i++) {
value += amplitud * noise(p);
p *= 2.0;
amplitud *= 0.8;
}
return value;
}

//===================================
// ・距離関数
// http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
//===================================

float dSpring(vec3 p) {
vec3 n = normalize(vec3(0.0, 1.0, 0.0));
float gush = fbm(vec2(p.x, length(p * 2.5) - u_time * 3.0) * 0.15); // 湧き出る動きを実装
return dot(p, n) + gush;
}

//===================================
// ・法線の取得
// https://wgld.org/d/glsl/g010.html
//===================================

vec3 genNormal(vec3 p) {
float d = 0.001;
return normalize(vec3(
dSpring(p + vec3( d, 0.0, 0.0)) - dSpring(p + vec3( -d, 0.0, 0.0)),
dSpring(p + vec3(0.0, d, 0.0)) - dSpring(p + vec3(0.0, -d, 0.0)),
dSpring(p + vec3(0.0, 0.0, d)) - dSpring(p + vec3(0.0, 0.0, -d))
));
}

//===================================
// ・メイン関数
//===================================

void main() {
// 色の宣言
vec3 spring = vec3(0.9, 0.9, 0.6);

// colorの初期化
vec3 color = vec3(0.0);

// 座標の正規化(range: -1.0 ~ 1.0)
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

// カメラのセッティング
vec3 cPos = vec3(0.0, 4.0, 3.0); // カメラの位置(GLSLおじさんの立ち位置)
vec3 cDir = vec3(0.0, -1.0, -1.0); // カメラの注視点(GLSLおじさんの視点)
vec3 cUp = vec3(0.0, 0.5, 1.0); // カメラの上方向(GLSLおじさんの頭上)
vec3 cSide = cross(cDir, cUp); // 外積を使って横方向を算出
float targetDepth = 1.0; // 深度

// レイの生成
vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);

// レイマーチングループ
float dist = 0.0; // レイと穴の距離
float rLen = 0.0; // レイの長さ
vec3 rPos = cPos; // レイの先端
for (int i = 0; i < 32; i++) {
dist = dSpring(rPos);
rLen += dist;
rPos = cPos + ray * rLen;
}

// 衝突判定で色の出しわけ
if (abs(dist) < 0.001) {
vec3 normal = genNormal(rPos); // 物体の法線情報を取得
vec3 light = normalize(vec3(1.0, 1.0, 0.0)); // ライトの位置
float diff = max(dot(normal, light), 0.1); // 拡散光を内積で算出
color = spring * diff;
} else {
color = vec3(0.0);
}

// 最終的に該当ピクセルに出力する色
gl_FragColor = vec4(color, 1.0);
}

実行結果はこちら / PCでの閲覧を推奨

spring


[ 解説 ]

ここで主に行なっていることは、主に3つです。

- 距離関数をsphereからplaneに変更

- 法線情報とライトを取り入れて、拡散光を実装

- 非整数ブラウン運動を使って、物体に揺らぎを作る

こちらのサイトでは、様々な物体の距離関数を示してくれているので、とても便利ですね!


照明をつける

GLSLおじさんは、温泉を掘り当てたからには一攫千金を狙おう、と考えました。そこで、照明器具を使って、いい感じな雰囲気の温泉を作り始めました。:hotsprings:

precision mediump float;

uniform vec2 u_resolution;
uniform float u_time;

//===================================
// ・ランダム / ノイズ / 非整数ブラウン運動
// http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
//===================================

float random (in vec2 p) {
return fract(sin(dot(p.xy,vec2(12.9898,78.233)))*43758.5453123);
}

float noise (in vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

#define OCTAVES 4
float fbm (in vec2 p) {
float value = 0.0;
float amplitud = 0.2;
float frequency = .9;
for (int i = 0; i < OCTAVES; i++) {
value += amplitud * noise(p);
p *= 2.0;
amplitud *= 0.8;
}
return value;
}

//===================================
// ・距離関数
// http://iquilezles.org/www/articles/distfunctions/distfunctions.htm
//===================================

float dSpring(vec3 p) {
vec3 n = normalize(vec3(0.0, 1.0, 0.0));

// 複数の非整数ブラウン運動を重ね合わせて、不規則っぽい揺らぎにする
float s = fbm(vec2(p.x, length(p * 1.5) - u_time * 1.25) * .075);
float t = fbm(vec2(p.x, length(p * 2.5) - u_time * 2.75) * .125);
float u = fbm(vec2(p.x, length(p * 3.5) - u_time * 0.75) * .075);
float v = s + t + u;
return dot(p, n) + v;
}

//===================================
// ・法線の取得
// https://wgld.org/d/glsl/g010.html
//===================================

vec3 genNormal(vec3 p) {
float d = 0.001;
return normalize(vec3(
dSpring(p + vec3( d, 0.0, 0.0)) - dSpring(p + vec3( -d, 0.0, 0.0)),
dSpring(p + vec3(0.0, d, 0.0)) - dSpring(p + vec3(0.0, -d, 0.0)),
dSpring(p + vec3(0.0, 0.0, d)) - dSpring(p + vec3(0.0, 0.0, -d))
));
}

//===================================
// ・メイン関数
//===================================

void main() {
// 色の宣言
vec3 spring = vec3(0.9, 0.9, 0.6);

// colorの初期化
vec3 color = vec3(0.0);

// 座標の正規化(range: -1.0 ~ 1.0)
vec2 p = (gl_FragCoord.xy * 2.0 - u_resolution) / min(u_resolution.x, u_resolution.y);

// カメラのセッティング
vec3 cPos = vec3(0.0, 3.0, 6.0); // カメラの位置(GLSLおじさんの立ち位置)
vec3 cDir = vec3(0.0, -1.0, -1.0); // カメラの注視点(GLSLおじさんの視点)
vec3 cUp = vec3(0.0, 0.5, 1.0); // カメラの上方向(GLSLおじさんの頭上)
vec3 cSide = cross(cDir, cUp); // 外積を使って横方向を算出
float targetDepth = 1.0; // 深度

// レイの生成
vec3 ray = normalize(cSide * p.x + cUp * p.y + cDir * targetDepth);

// レイマーチングループ
float dist = 0.0; // レイと穴の距離
float rLen = 0.0; // レイの長さ
vec3 rPos = cPos; // レイの先端
for (int i = 0; i < 32; i++) {
dist = dSpring(rPos);
rLen += dist;
rPos = cPos + ray * rLen;
}

// 衝突判定で色の出しわけ
if (abs(dist) < 0.001) {
vec3 normal = genNormal(rPos); // 物体の法線情報を取得
vec3 light = normalize(vec3(sin(u_time), 1.2, 0.0)); // ライトの位置
float diff = max(dot(normal, light), 0.1); // 拡散反射光を内積で算出

vec3 eye = reflect(normalize(rPos - cPos), normal);
float speculer = pow(clamp(dot(eye, light), 0.0, 1.0) * 1.025, 30.0); // 反射光を算出

color = (spring + speculer) * diff;
} else {
color = vec3(0.0);
}

// 最終的に該当ピクセルに出力する色
gl_FragColor = vec4(color, 1.0);
}

実行結果はこちら / PCでの閲覧を推奨

アウトプット


[ 解説 ]

ここで主に行なっていることは、主に2つです。

- 反射光の実装

- 非整数ブラウン運動を複数使って、不規則っぽい揺らぎを作る


まとめ

GLSLで温泉(のようなもの)を描画することができました :clap:

岩や湯気も描画して、露天風呂を作りたくなってしまいます。

シェーダはとっつきにくい印象があるかもしれませんが、偉大な先人たちがドキュメントや関数を用意してくれていますので、むしろ学び易いはずです。

「そろそろWebGL案件を抱え始めそう...」という方は、是非、シェーダにもトライしてみてください!