Popcorn ceiling

GLSL shader by scry · created 2026-06-27 · 10s loop · 2 passes

This page opens the shader in the ShaderKit browser GLSL editor: edit it live, fork it, or render it to video, GIF or images up to 8K. Also available as a full-screen view and an embeddable player.

Shader source (GLSL)

Common

#define pi acos(-1.)
#define deg pi/180.
#define time iTime*2.*pi/10.
#define R iResolution.xy
#define ar R.x/R.y
vec3 cs = vec3(1.,2.,3.);
mat2 r2d(float a) {
    return mat2(cos(a),sin(a),-sin(a),cos(a));
}

Buffer A (iChannel0)


float hash(vec2 p) {
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

float hash(float n) {
    return fract(sin(n) * 43758.5453);
}

// 3D value noise for smooth temporal evolution
float valueNoise3D(vec3 p) {
    vec3 i = floor(p);
    vec3 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    
    float n = i.x + i.y * 57.0 + 113.0 * i.z;
    return mix(
        mix(mix(hash(n), hash(n + 1.0), f.x),
            mix(hash(n + 57.0), hash(n + 58.0), f.x), f.y),
        mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
            mix(hash(n + 170.0), hash(n + 171.0), f.x), f.y),
        f.z
    );
}

float valueNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x), f.y);
}

// Cellular/Worley noise for popcorn blob shapes
float cellNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    float d1 = 1.0;
    float d2 = 1.0;
    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 cell = i + neighbor;
            vec2 pt = vec2(hash(cell), hash(cell * 1.73 + 0.5));
            // Randomize point size slightly for irregular blobs
            float d = length(neighbor + pt - f);
            if (d < d1) {
                d2 = d1;
                d1 = d;
            } else if (d < d2) {
                d2 = d;
            }
        }
    }
    // Return both distances packed: d1 for cell distance, d2-d1 for edge
    return d1;
}

// Second version returning edge factor (d2 - d1)
float cellEdge(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    float d1 = 1.0;
    float d2 = 1.0;
    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 cell = i + neighbor;
            vec2 pt = vec2(hash(cell), hash(cell * 1.73 + 0.5));
            float d = length(neighbor + pt - f);
            if (d < d1) {
                d2 = d1;
                d1 = d;
            } else if (d < d2) {
                d2 = d;
            }
        }
    }
    return d2 - d1;
}

float popcornHeight(vec2 p) {
    // Use 3D noise with time as third dimension for smooth evolution
    float t1 = iTime * 00.102;
    float t2 = iTime * 00.05;
    float t3 = iTime * 0.1018;
    
    // Large irregular blobs - the main popcorn splatter bumps
    vec2 offset1 = vec2(valueNoise3D(vec3(p * 0.5, t1)), valueNoise3D(vec3(p * 0.5 + 100.0, t1))) * 2.0;
    float c1 = cellNoise(p * 7.0 + offset1);
    float e1 = cellEdge(p * 1.0 + offset1);
    // Rounded blob profile: raised centers with soft falloff
    float blobs = 1.0 - smoothstep(0.0, 0.4, c1);
    blobs = pow(blobs, 1.2);
    // Add ridge detail at cell boundaries (dried paint edges)
    float ridges = smoothstep(0.0, 0.08, e1);
    blobs *= mix(0.7, 1.0, ridges);
    
    // Secondary smaller blobs - overlapping splatter
    vec2 offset2 = vec2(valueNoise3D(vec3(p * 0.5, t2)), valueNoise3D(vec3(p * 0.5 + 200.0, t2))) * 2.0;
    float c2 = cellNoise(p * 14.0 + 3.7 + offset2);
    float blobs2 = 1.0 - smoothstep(0.0, 0.35, c2);
    blobs2 = pow(blobs2, 1.8);
    
    // Tiny aggregate bumps - the gritty sand-like particles
    vec2 offset3 = vec2(valueNoise3D(vec3(p * 0.5, t3)), valueNoise3D(vec3(p * 0.5 + 300.0, t3))) * 2.0;
    float c3 = cellNoise(p * 39.0 + 7.1 + offset3);
    float grit = 1.0 - smoothstep(0.0, 0.45, c3);
    grit = pow(grit, 1.5);
    
    // Fine surface roughness - stipple texture
    float t4 = iTime * 0.01;
    float stipple = valueNoise3D(vec3(p * 80.0, t4)) * 0.5 + 
                    valueNoise3D(vec3(p * 160.0, t4 * 1.5)) * 0.25;
    // Modulate stipple by blob presence (more texture on raised areas)
    float onBlob = smoothstep(0.1, 0.5, blobs + blobs2 * 0.5);
    //stipple *= mix(0.3, 1.0, onBlob);
    
    // Domain-warped noise for organic variation in overall density
    float t5 = iTime * 0.012;
    vec2 wp = p + vec2(valueNoise3D(vec3(p * 3.0, t5)) * 0.15, 
                       valueNoise3D(vec3(p * 3.0 + 5.0, t5)) * 0.15);
    float density = valueNoise3D(vec3(wp * 8.0, t5));
    // Some areas have more buildup than others
    float densityMask = smoothstep(0.15, 0.75, density);
    
    // Combine layers
    float h = blobs * 0.20 + blobs2 * 0.25 + grit * 0.10 + stipple * 0.15;
    // Apply density variation
    h = mix(h * 0.5, h, densityMask);
    
    return h;
}

float sdPlane(vec3 p, vec3 n, float h) {
    return dot(p, n) + h;
}

vec4 map(vec3 p) {
    float sphere = length(p) - 0.5;
    // Displace the plane with popcorn texture for real 3D bumps
    float plane = p.y + 1.0 - popcornHeight(p.xz) * 0.055;
    
    float d = min(sphere, plane);
    
    return vec4(p, d);
}

vec2 RM(vec3 ro, vec3 rd) {
    float dO = 0.0;
    float ii = 0.0;
    for (int i=0; i<5; i++) {
        vec3 p = ro + rd*dO;
        float dS = map(p).w;
        dO += dS;
        ii += 1.0; // Can be used for effects based on steps
        if (dO > 20.0 || dS < 0.001) break;
    }
    return vec2(dO, ii);
}

vec3 calcNormal(vec3 p) {
    const vec2 e = vec2(0.0005, 0.0);
    return normalize(vec3(
        map(p + e.xyy).w - map(p - e.xyy).w,
        map(p + e.yxy).w - map(p - e.yxy).w,
        map(p + e.yyx).w - map(p - e.yyx).w
    ));
}

float softShadow(vec3 ro, vec3 rd, float mint, float tmax) {
    float res = 1.0;
    float t = mint;
    for (int i = 0; i < 32; i++) {
        float h = map(ro + rd * t).w;
        res = min(res, 8.0 * h / t);
        t += clamp(h, 0.02, 0.1);
        if (res < 0.001 || t > tmax) break;
    }
    return clamp(res, 0.0, 1.0);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;
    vec2 tv = uv;
    uv -= 0.5;
    uv.x *= ar;
    vec3 col = vec3(0.);
    vec3 ro = vec3(0.0, 4.0, 5.0); // Ray Origin
    vec3 rd = normalize(vec3(uv, -1.0)); // Ray Direction
    rd.yz *= r2d(deg*90.);
    vec2 d = RM(ro, rd);
    float t = d.x; // Distance to hit
    vec3 p = ro + rd * t; // Hit point in 3D space

    if (t < 20.0) {
        vec3 normal = calcNormal(p);
        vec3 lightPos = vec3(12.*sin(time), 23., 14.*cos(time));
        vec3 lightDir = normalize(lightPos - p);
        
        // Material color - slight warm off-white for popcorn ceiling
        vec3 baseCol = vec3(0.92, 0.90, 0.87);
        // Sphere gets a different color
        if (length(p) < 0.6) baseCol = vec3(0.8, 0.2, 0.2);
        
        float diff = max(dot(normal, lightDir), 0.0);
        float shadow = softShadow(p + normal * 0.01, lightDir, 0.02, length(lightPos - p));
        
        vec3 ambient = vec3(0.1, 0.1, 0.15);
        vec3 diffuse = diff * shadow * vec3(1.0, 0.95, 0.9);
        
        col = baseCol * (ambient + diffuse * 0.8);
    }
    
    fragColor = vec4(col, 1.0);
}

Image

Not used

More shaders by scry

Browse all public shaders · All shaders by scry · ShaderKit home

Vibe Mode BETA
💰 ~0 credits
Uniforms
FPS: 0
Time: 0
Resolution: 0 x 0

Account

FPS: -- Time: -- --×-- 1x