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); }