diff --git a/shader/playground.glsl b/shader/playground.glsl index 8ec93da..a00191f 100644 --- a/shader/playground.glsl +++ b/shader/playground.glsl @@ -7,16 +7,176 @@ uniform float uTime; // 起動してからの秒数 uniform vec2 uResolution; // 画面の大きさ(ピクセル) uniform vec2 uMouse; // マウスの位置(ピクセル、左下が原点) +const int MAX_STEPS = 128; // レイ1本あたりの最大ステップ数 +const float MAX_DIST = 40.0; // 此れより遠くは「何もない」 +const float SURF_DIST = 0.001; + +//------------------------------------------ +// 距離関数(SDF) +//------------------------------------------ + +float sdSphere(vec3 p, float r) { + return length(p) - r; +} + +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 sdTorus(vec3 p, float R, float r) { + vec2 q = vec2(length(p.xz) - R, p.y); + return length(q) - r; +} + +// 滑らかな和集合(第6章のsminと同じ) +float smin(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); + return mix(b, a, h) - k * h * (1.0 - h); +} + +mat2 rot(float a) { + float c = cos(a), s = sin(a); + return mat2(c, s, -s, c); +} + +// シーン全体:x=距離、y=マテリアル番号 +vec2 map(vec3 p) { + // 床(y = 0の平面) + vec2 res = vec2(p.y, 0.0); + + // 中央:二つの球を滑らかにくっつけた物 + float bob = 0.35 * sin(uTime * 1.5); + float a = sdSphere(p - vec3(0.0, 0.7, 0.0), 0.6); + float b = sdSphere(p - vec3(0.45, 1.3 + bob, 0.1), 0.35); + float blob = smin(a, b, 0.3); + if (blob < res.x) res = vec2(blob, 1.0); + + // 左:回転する角の丸い箱 + vec3 q = p - vec3(-1.9, 0.6, 0.3); + q.xz = rot(uTime * 0.6) * q.xz; + float box = sdBox(q, vec3(0.45)) - 0.08; + if (box < res.x) res = vec2(box, 2.0); + + // 右:傾いたトーラス + q = p - vec3(1.9, 0.75, 0.0); + q.yz = rot(0.9) * q.yz; + float torus = sdTorus(q, 0.5, 0.18); + if (torus < res.x) res = vec2(torus, 3.0); + + // 奥:modで無限に並ぶ柱 + q = p - vec3(0.0, 0.0, -8.0); + q.x = mod(q.x + 1.25, 2.5) - 1.25; + float pillar = sdBox(q, vec3(0.25, 2.5, 0.25)); + if (pillar < res.x) res = vec2(pillar, 4.0); + + return res; +} + +//------------------------------------------ +// レイマーチング +//------------------------------------------ + +// 戻り値:x=当たった距離t、y=マテリアル番号(何にも当たらなかったら-1) +vec2 raymarch(vec3 ro, vec3 rd) { + float t = 0.0; + for (int i = 0; i < MAX_STEPS; i++) { + vec2 h = map(ro + rd * t); + if (h.x < SURF_DIST * t) return vec2(t, h.y); // 十分に近い:当たり + t += h.x; // 安全な距離だけ進む + if (t > MAX_DIST) break; // 遠過ぎ:外れ + } + return vec2(t, -1.0); +} + +// 法線=距離場の勾配(中心差分) +vec3 calcNormal(vec3 p) { + const vec2 e = vec2(0.001, 0.0); + return normalize(vec3( + map(p + e.xyy).x - map(p - e.xyy).x, + map(p + e.yxy).x - map(p - e.yxy).x, + map(p + e.yyx).x - map(p - e.yyx).x)); +} + +// 柔らかい影:光へ向かうレイが物体の近くを通る程暗くする +float softShadow(vec3 ro, vec3 rd, float k) { + float res = 1.0; + float t = 0.02; + for (int i = 0; i < 64; i++) { + float h = map(ro + rd * t).x; + if (h < 0.0005) return 0.0; + res = min(res, k * h / t); + t += clamp(h, 0.02, 0.4); + if (t > 12.0) break; + } + return clamp(res, 0.0, 1.0); +} + +// アンビエントオクルージョン:法線方向に何回か距離場を調べる +float ambientOcclusion(vec3 p, vec3 n) { + float occ = 0.0; + float weight = 1.0; + for (int i = 1; i <= 5; i++) { + float h = 0.03 + 0.12 * float(i); // 表面からの距離 + float d = map(p + n * h).x; // 実際に空いている距離 + occ += (h - d) * weight; + weight *= 0.6; + } + return clamp(1.0 - 2.0 * occ, 0.0, 1.0); +} + +vec3 materialColor(float id, vec3 p) { + if (id < 0.5) { + float checker = mod(floor(p.x) + floor(p.z), 2.0); + return mix(vec3(0.08), vec3(0.35), checker); + } + if (id < 1.5) return vec3(0.9, 0.25, 0.35); + if (id < 2.5) return vec3(0.95, 0.55, 0.1); + if (id < 3.5) return vec3(0.1, 0.7, 0.6); + return vec3(0.55, 0.55, 0.6); +} + void main() { - // ピクセル座標を0〜1に正規化 - vec2 uv = gl_FragCoord.xy / uResolution; + // 中心が原点、縦が-1〜1の座標(第5章) + vec2 uv = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y; + + // カメラ:原点の周りを回る(マウスを左右に動かすと角度がずれる) + float angle = 0.25 * uTime + 6.2831853 * (uMouse.x / uResolution.x - 0.5); + vec3 ro = vec3(5.0 * sin(angle), 2.2, 5.0 * cos(angle)); // レイの原点=カメラの位置 + vec3 target = vec3(0.0, 0.6, 0.0); + + // lookAtと同じ正規直交基底(線型代数の第11章) + vec3 forward = normalize(target - ro); + vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0))); + vec3 up = cross(right, forward); + const float FOCAL = 1.8; // 焦点距離:大きい程視野が狭い + vec3 rd = normalize(uv.x * right + uv.y * up + FOCAL * forward); + + vec3 skyColor = vec3(0.45, 0.6, 0.85) - 0.4 * rd.y; + vec3 color = skyColor; + + vec2 hit = raymarch(ro, rd); + if (hit.y >= 0.0) { + vec3 p = ro + rd * hit.x; + vec3 n = calcNormal(p); + vec3 albedo = materialColor(hit.y, p); + + vec3 l = normalize(vec3(0.8, 0.6, 0.1)); // 光の方へ向かう方向 + vec3 h = normalize(l - rd); // ハーフベクトル(視線は-rd) + float diffuse = max(dot(n, l), 0.0); + float shadow = softShadow(p + n * 0.002, l, 8.0); + float specular = pow(max(dot(n, h), 0.0), 32.0) * diffuse; + float ao = ambientOcclusion(p, n); + float sky = 0.5 + 0.5 * n.y; - // 時間で動くグラデーション - vec3 color = 0.5 + 0.5 * cos(uTime + uv.xyx + vec3(0.0, 2.0, 4.0)); + vec3 lit = albedo * vec3(1.3, 1.2, 1.0) * diffuse * shadow; + lit += albedo * vec3(0.12, 0.15, 0.22) * sky * ao; + lit += vec3(0.8) * specular * shadow; - // マウスの周りに白い円 - float d = length(gl_FragCoord.xy - uMouse); - color = mix(color, vec3(1.0), 1.0 - smoothstep(18.0, 20.0, d)); + // 霧:遠い程空の色に近づける + float fog = 1.0 - exp(-0.004 * hit.x * hit.x); + color = mix(lit, skyColor, fog); + } - out_color = vec4(color, 1.0); + out_color = vec4(pow(color, vec3(1.0 / 2.2)), 1.0); // ガンマ補正 }