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>
113 lines
3.8 KiB
GLSL
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);
|
|
}
|