Files
websperiments/standalone/goldberg-sphere/shaders/face.frag
mikael-lovqvists-claude-agent 9600a2bc2a Add Goldberg Polyhedron Paint experiment
Interactive WebGL Goldberg polyhedron viewer and painter with PBR
shading, adjustable environment lighting, paint tools (pen, brush,
circle, fill, line, pick), undo/redo, colour palettes, and mesh
relaxation. Added to the standalone experiments index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:00:19 +00:00

113 lines
3.8 KiB
GLSL

precision mediump float;
varying vec3 v_pos;
varying vec3 v_normal;
varying vec3 v_color;
varying vec2 v_pbr;
varying vec3 v_obj_pos;
varying vec4 v_noise; // (scale, strength, octaves, gain); lacunarity fixed at 2.0
uniform vec3 u_cam_pos;
uniform vec3 u_light1_dir;
uniform vec3 u_light1_color;
uniform vec3 u_light2_dir;
uniform vec3 u_light2_color;
uniform vec3 u_ambient;
const float PI = 3.14159265;
const float LACUNARITY = 2.0;
// ---------------------------------------------------------------------------
// 3-D value noise (input: object-space position → no rotation seam)
// ---------------------------------------------------------------------------
float hash3(vec3 p) {
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
p += dot(p, p.yxz + 33.33);
return fract((p.x + p.y) * p.z);
}
float vnoise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(mix(hash3(i), hash3(i+vec3(1,0,0)), u.x),
mix(hash3(i+vec3(0,1,0)), hash3(i+vec3(1,1,0)), u.x), u.y),
mix(mix(hash3(i+vec3(0,0,1)), hash3(i+vec3(1,0,1)), u.x),
mix(hash3(i+vec3(0,1,1)), hash3(i+vec3(1,1,1)), u.x), u.y), u.z);
}
// FBM: octaves is a float so partial octaves blend smoothly.
float fbm(vec3 p, float octaves, float gain) {
float val = 0.0, amp = 0.5, freq = 1.0, norm = 0.0;
for (int i = 0; i < 8; i++) {
float w = clamp(octaves - float(i), 0.0, 1.0);
float c = amp * w;
val += c * vnoise(p * freq);
norm += c;
freq *= LACUNARITY;
amp *= gain;
}
return norm > 0.001 ? val / norm : 0.5;
}
// Perturb n using the per-face noise params from v_noise.
// Uses object-space position so noise is fixed to the sphere, not world space.
// Strength is divided by scale for consistent visual amplitude across frequencies.
vec3 perturb_normal(vec3 n) {
float scale = v_noise.x;
float strength = v_noise.y;
float octaves = v_noise.z;
float gain = v_noise.w;
vec3 t1 = cross(n, vec3(0.0, 1.0, 0.0));
if (length(t1) < 0.01) { t1 = cross(n, vec3(1.0, 0.0, 0.0)); }
t1 = normalize(t1);
vec3 t2 = normalize(cross(n, t1));
float eps = 0.05;
vec3 sp = v_obj_pos * scale;
float n0 = fbm(sp, octaves, gain);
float dx = (fbm(sp + t1 * (eps * scale), octaves, gain) - n0) / eps;
float dy = (fbm(sp + t2 * (eps * scale), octaves, gain) - n0) / eps;
float str = strength / max(scale, 1.0);
return normalize(n + str * (dx * t1 + dy * t2));
}
// ---------------------------------------------------------------------------
// PBR (Cook-Torrance)
// ---------------------------------------------------------------------------
float D_GGX(float NdH, float a2) {
float d = NdH*NdH*(a2-1.0)+1.0;
return a2 / (PI*d*d + 0.0001);
}
float G_Smith(float NdX, float k) {
return NdX / (NdX*(1.0-k)+k+0.0001);
}
vec3 F_Schlick(float cosT, vec3 F0) {
return F0 + (1.0-F0)*pow(clamp(1.0-cosT,0.0,1.0),5.0);
}
vec3 pbr_light(vec3 n, vec3 v, vec3 l, vec3 lc, vec3 alb, float met, float rou) {
vec3 h = normalize(v+l);
float NdL = max(dot(n,l),0.0);
float NdV = max(dot(n,v),0.001);
float NdH = max(dot(n,h),0.0);
float VdH = max(dot(v,h),0.0);
float a2 = rou*rou*rou*rou;
float k = rou*rou/2.0;
vec3 F0 = mix(vec3(0.04), alb, met);
vec3 F = F_Schlick(VdH, F0);
float D = D_GGX(NdH, a2);
float G = G_Smith(NdL,k)*G_Smith(NdV,k);
vec3 spec = D*F*G / (4.0*NdL*NdV + 0.0001);
vec3 kd = (1.0-F)*(1.0-met);
return (kd*alb/PI + spec) * lc * NdL;
}
void main() {
float met = v_pbr.x;
float rou = v_pbr.y;
vec3 alb = v_color;
vec3 n = normalize(v_normal);
if (v_noise.y > 0.001 && v_noise.x > 0.001) {
n = perturb_normal(n);
}
vec3 v = normalize(u_cam_pos - v_pos);
vec3 col = pbr_light(n, v, u_light1_dir, u_light1_color, alb, met, rou)
+ pbr_light(n, v, u_light2_dir, u_light2_color, alb, met, rou);
col += alb * (1.0-met) * u_ambient;
gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}