経緯
シェーダーを勉強しようと思い、色々調べていたらレイマーチングというものを知った。
下記のサイトでレイマーチングについて学んだ後、
取り敢えず何か作りたかったので、簡単そうな鉄火巻きを作ることにした。
使用したもの
目標
1.複数オブジェクトの表示
2.回転や平行移動の使用
3.陰影の表示(ソフトシャドウ)
を実現することを目標とした。
コード
precision mediump float;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
//カメラ
const float PI = 3.14159265;
const float angle = 60.0;
const float fov = angle * 0.5 * PI / 180.0;
const vec3 cPos = vec3(0.0, 2.0, 5.2);
const vec3 cDir = vec3(0.0, -1.0, -2.0);
//光
const vec3 lightPos = vec3(0.0, 5.0, 5.0);
const vec3 lightDir = vec3(0.0, 1.0, 1.0);
//色
vec3 noriColor = vec3(0.2, 0.2, 0.2);
vec3 riceColor = vec3(1.0, 1.0, 1.0);
vec3 tunaColor = vec3(1.0, 0.0, 0.0);
vec3 wPlainColor = vec3(0.7, 0.7, 0.7);
vec3 bPlainColor = vec3(0.3, 0.3, 0.3);
//平行移動
vec3 translate(vec3 p, vec3 t) {
mat4 m = mat4(vec4(1.0, 0.0, 0.0, 0.0),
vec4(0.0, 1.0, 0.0, 0.0),
vec4(0.0, 0.0, 1.0, 0.0),
vec4(-t.x, -t.y, -t.z, 1.0));
return (m * vec4(p, 1.0)).xyz;
}
//任意軸回転
vec3 rotate(vec3 p, float angle, vec3 axis){
vec3 a = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float r = 1.0 - c;
mat3 m = mat3(
a.x * a.x * r + c,
a.y * a.x * r + a.z * s,
a.z * a.x * r - a.y * s,
a.x * a.y * r - a.z * s,
a.y * a.y * r + c,
a.z * a.y * r + a.x * s,
a.x * a.z * r + a.y * s,
a.y * a.z * r - a.x * s,
a.z * a.z * r + c
);
return m * p;
}
//立方体の距離関数
float sdBox( vec3 p, vec3 b )
{
vec3 q = abs(p) - b;
return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0);
}
//円柱の距離関数
float sdCappedCylinder( vec3 p, float h, float r )
{
vec2 d = abs(vec2(length(p.xz),p.y)) - vec2(h,r);
return min(max(d.x,d.y),0.0) + length(max(d,0.0));
}
//平面の距離関数
float sdPlane( vec3 p, vec4 n )
{
return dot(p,n.xyz) + n.w;
}
//描画関数
float distanceFunc(vec3 p) {
//平行移動と回転
vec3 q = translate(p, vec3(0.0, 0.5, 0.0));
vec3 r = rotate(q, radians(20.0 * time), vec3(0.0, 1.0, 0.0));
vec3 s = rotate(r, radians(70.0), vec3(1.5, 0.5, 0.5));
//オブジェクトの描画
float tuna = sdBox(s, vec3(0.05, 0.53, 0.05));
float rice = sdCappedCylinder(s, 0.15, 0.50);
float nori = sdCappedCylinder(s, 0.17, 0.50);
//床の描画
float plain = sdPlane(p, vec4(0.0, 1.0, 0.0, 1.0));
//結合
return min(min(min(nori, rice), tuna), plain);
}
//色の計算
vec3 calcColor(vec3 p) {
//上と同様
vec3 q = translate(p, vec3(0.0, 0.5, 0.0));
vec3 r = rotate(q, radians(20.0 * time), vec3(0.0, 1.0, 0.0));
vec3 s = rotate(r, radians(70.0), vec3(1.5, 0.5, 0.5));
float tuna = sdBox(s, vec3(0.05, 0.55, 0.05));
float rice = sdCappedCylinder(s, 0.15, 0.50);
float nori = sdCappedCylinder(s, 0.17, 0.50);
float plain = sdPlane(p, vec4(0.0, 1.0, 0.0, 1.0));
//距離関数が最小のものの色を描画
float minDist = tuna;
vec3 color = tunaColor;
if(rice < minDist) {
minDist = rice;
color = riceColor;
}
if(nori < minDist) {
minDist = nori;
color = noriColor;
}
if(plain < minDist) {
minDist = plain;
color = bPlainColor;
//チェック柄にする
float u = 1.0 - floor(mod(p.x, 2.0));
float v = 1.0 - floor(mod(p.z, 2.0));
if((u == 1.0 && v < 1.0) || (u < 1.0 && v == 1.0)){
color = wPlainColor;
}
}
return color;
}
//法線
vec3 getNormal(vec3 p){
float d = 0.0001;
return normalize(vec3(
distanceFunc(p + vec3( d, 0.0, 0.0)) - distanceFunc(p + vec3( -d, 0.0, 0.0)),
distanceFunc(p + vec3(0.0, d, 0.0)) - distanceFunc(p + vec3(0.0, -d, 0.0)),
distanceFunc(p + vec3(0.0, 0.0, d)) - distanceFunc(p + vec3(0.0, 0.0, -d))
));
}
//影
float genShadow(vec3 ro, vec3 rd){
float h = 0.0;
float c = 0.001;
float r = 1.0;
float shadowCoef = 0.5;
for(float t = 0.0; t < 50.0; t++){
h = distanceFunc(ro + rd * c);
if(h < 0.001){
return shadowCoef;
}
r = min(r, h * 16.0 / c);
c += h;
}
return 1.0 - shadowCoef + r * shadowCoef;
}
void main(void){
//位置
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
//レイの方向
float targetDepth = 1.0;
vec3 ray = normalize(vec3(sin(fov) * p.x, sin(fov) * p.y, -cos(fov)) + cDir * targetDepth);
//レイマーチング
float distance = 0.0; // レイとオブジェクト間の最短距離
float rLen = 0.0; // レイに継ぎ足す長さ
vec3 rPos = cPos; // レイの先端位置
for(int i = 0; i < 64; i++){
distance = distanceFunc(rPos);
if(distance < 0.001) break;
rLen += distance;
rPos = cPos + ray * rLen;
}
//光
vec3 light = normalize(lightDir);
//影
float shadow = 1.0;
//出力色
vec3 outColor;
//レイの当たり判定
if(abs(distance) < 0.001){
//法線
vec3 normal = getNormal(rPos);
//光
vec3 halfLE = normalize(light - ray);
float diff = clamp(dot(light, normal), 0.1, 1.0);
float spec = pow(clamp(dot(halfLE, normal), 0.0, 1.0), 30.0);
//影
shadow = genShadow(rPos + normal * 0.001, light);
//色
vec3 objColor = calcColor(rPos);
outColor = objColor * diff + vec3(spec);
}else{
outColor = vec3(0.0);
}
//最終的な出力
gl_FragColor = vec4(outColor * max(0.5, shadow), 1.0);
}
出力結果

感想
作ったものはシンプルだがこれだけでも結構悩まされた。
距離関数を理解するのが難しい。
もっと勉強して色々できるようになったら楽しそう。
参考にしたサイト
・[2019年度GLSLお役立ちシート]
(https://qiita.com/7CIT/items/6cfac5849dce678f86d3)
・[wgld.org]
(https://wgld.org/d/glsl/)
・WebGL と GLSL で始める、はじめてのシェーダコーディング(1)
・[The Book of Shaders]
(https://thebookofshaders.com/?lan=jp)