Project_2025-12-29_14-46-59

GLSL shader by scry · created 2025-12-29 · updated 2025-12-30 · 10s loop · 5 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

// ============================================
// UTILITY MACROS & FUNCTIONS
// ============================================
#define pi acos(-1.)
#define deg (pi/180.)              // 1 degree in radians
#define time (iTime*2.*pi/10.)     // sin(time) loops every 10 seconds

// Shorthand accessors
#define R iResolution.xy
#define ar (R.x/R.y)               // aspect ratio
#define M iMouse
#define xm (M.xy/R)                // normalized mouse [0,1]
#define nm ((xm.xy-0.5)*vec2(ar,1.)+0.5)  // aspect-corrected mouse

// 2D rotation matrix
mat2 r2d(float a) {
    return mat2(cos(a),sin(a),-sin(a),cos(a));
}

// Hash function for pseudo-randomness
float hash(float n) {
    return fract(sin(n) * 43758.5453123);
}

// Color seed for barrier coloring
vec3 cs = vec3(1.,2.,3.);

// ============================================
// DATA BUFFER LAYOUT (iChannel0)
// ============================================
#define BALL_POS ivec2(0,0)
#define SCORE_POS ivec2(1,0)
#define BALL_RADIUS_POS ivec2(2,0)
#define BARRIER_START 2            // First pixel x-index for barrier data
#define NUM_PARTICLES 1            // Particles per row for explosions
#define PARTICLE_ROWS 1            // Number of rows for particle data

// ============================================
// BARRIER CONFIGURATION
// ============================================
#define BARRIER_SPACING 0.4
#define NUM_BARRIERS 200            // Number of concentric ring barriers
#define BARRIER_INITIAL_HEALTH 30.
#define BARRIER_ROTATION_SPEED -0.2  // radians per second

// Gap configuration
#define GAP_WIDTH_MODE 1.5         // 0.0=angular, 1.0=constant width
#define BASE_GAP_SIZE 0.08         // base gap size
#define GAP_DESTROYS_BARRIER 1.   // >0.5 means passing through gap destroys barrier
#define GAP_VELOCITY_MULTIPLIER 1.5  // velocity multiplier when passing through gap

// Returns the radius of barrier i (innermost = 0)
float barrierRadius(int i) {
    return 0.15 + iTime*-0.09 + float(i) * BARRIER_SPACING;
}

// ============================================
// BALL PHYSICS
// ============================================
#define BALL_ELASTICITY .88
#define GRAVITY 1.8                // Gravity acceleration (units per second^2)
#define BOUNCE_RANDOMNESS 0.5  // Amount of random angle deviation on bounce (0.0=none, 0.1=~6 degrees)

// ============================================
// WORLD BOUNDARIES & CAMERA
// ============================================
#define OUTER_BOUNDARY_RADIUS 2.85
#define CAMERA_FOLLOW_AMOUNT -0.7  // 0.0=static, 1.0=fully centered on ball

// ============================================
// DEBUG
// ============================================
#define DEBUG_VISUALIZATION 0.0

Buffer A (iChannel0)

// iChannel0: Data storage buffer
// Pixel (0,0): ball position (xy), velocity (zw)
// Pixel (1,0): score (x), unused (yzw)
// Pixel (2,0): ball radius (x), unused (yzw)
// Pixels (2-9,0): barrier health (x), gap angle (y), destroyed flag (z)
//                 gap passage flag (w) - set to 1.0 when ball passes through
// Pixels (0-63,1-4): particle data (pos.xy, vel.xy) per particle

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    ivec2 px = ivec2(fragCoord);
    vec4 data = texelFetch(iChannel0, px, 0);
    
    float dt = 1.0 / 60.0;
    
    // Initialize on first frame
    if (iFrame < 2) {
        if (px == BALL_POS) {
            fragColor = vec4(0.0, 0.0, 0.3, 0.4); // pos, vel
        } else if (px == SCORE_POS) {
            fragColor = vec4(0.0, 0.0, 0.0, 0.0); // score starts at 0
        } else if (px == BALL_RADIUS_POS) {
            fragColor = vec4(0.02, 0.0, 0.0, 0.0); // ball radius
        } else if (px.y == 0 && px.x >= BARRIER_START && px.x < BARRIER_START + NUM_BARRIERS) {
            int idx = px.x - BARRIER_START;
            float gapAngle = float(idx)*time*0.1 + 0.; // all gaps same angle for now
            fragColor = vec4(BARRIER_INITIAL_HEALTH, gapAngle, 0.0, 0.0); // health, gap angle, destroyed, gap_passed
        } else if (px.y >= 1 && px.y <= PARTICLE_ROWS && px.x < NUM_PARTICLES) {
            fragColor = vec4(0.0); // inactive particles
        } else {
            fragColor = vec4(0.0);
        }
        return;
    }
    
    // Ball physics (pixel 0,0)
    if (px == BALL_POS) {
        vec2 pos = data.xy;
        vec2 vel = data.zw;
        
        // Check if any barrier flagged a gap passage last frame, apply velocity reduction
        for (int i = 0; i < NUM_BARRIERS; i++) {
            vec4 barrier = texelFetch(iChannel0, ivec2(BARRIER_START + i, 0), 0);
            // w component stores gap passage flag from last frame
            if (barrier.w > 0.5) {
                vel *= GAP_VELOCITY_MULTIPLIER;
                break; // Only apply once per frame even if multiple gaps passed
            }
        }
        
        float originalSpeed = length(vel);
        
        float ballRadius = texelFetch(iChannel0, ivec2(2, 0), 0).x;
        if (ballRadius < 0.001) ballRadius = 0.02; // Fallback
        
        vel.y -= GRAVITY * dt; // gravity
        
        // Iterative collision - handle multiple bounces per frame
        float remainingDt = dt;
        
        for (int bounce = 0; bounce < 10 && remainingDt > 0.0001; bounce++) {
            vec2 rayOrigin = pos;
            vec2 rayDir = vel * remainingDt;
            
            bool collided = false;
            float closestT = 2.0;
            int closestBarrier = -1;
            vec2 closestNormal = vec2(0.0);
            
            bool hitGapEdge = false;
            
            for (int i = 0; i < NUM_BARRIERS; i++) {
                vec4 barrier = texelFetch(iChannel0, ivec2(BARRIER_START + i, 0), 0);
                if (barrier.z > 0.5) continue;
                
                float r = barrierRadius(i);
                float gapAngle = barrier.y + iTime * BARRIER_ROTATION_SPEED;
                float gapSize = mix(BASE_GAP_SIZE, BASE_GAP_SIZE / r, GAP_WIDTH_MODE);
                
                // Check collision with expanded radius (barrier +/- ball radius)
                
                // First check gap edge collisions (radial walls at gap boundaries)
                // Gap edges are at angles: gapAngle - gapSize and gapAngle + gapSize
                for (int edgeIdx = 0; edgeIdx < 2; edgeIdx++) {
                    float edgeAngle = gapAngle + (edgeIdx == 0 ? -gapSize : gapSize);
                    vec2 edgeDir = vec2(cos(edgeAngle), sin(edgeAngle));
                    vec2 edgeNormal = vec2(-sin(edgeAngle), cos(edgeAngle)) * (edgeIdx == 0 ? 1.0 : -1.0);
                    
                    // Ray-line intersection: check if ray crosses the radial edge line
                    // Edge line goes from origin outward along edgeDir
                    // We only care about the segment from inner to outer radius of the ring
                    
                    // Parametric line: P = rayOrigin + t * rayDir
                    // Edge constraint: dot(P - origin, edgeNormal) = 0 (on the edge plane)
                    // Radial constraint: length(P) between (r - ballRadius) and (r + ballRadius)
                    
                    float denom = dot(rayDir, edgeNormal);
                    if (abs(denom) > 0.0001) {
                        float t = -dot(rayOrigin, edgeNormal) / denom;
                        
                        if (t > 0.00001 && t < 1.0 && t < closestT) {
                            vec2 hitPos = rayOrigin + rayDir * t;
                            float hitDist = length(hitPos);
                            
                            // Check if hit point is within the ring thickness
                            if (hitDist > r - ballRadius * 0.5 && hitDist < r + ballRadius * 0.5) {
                                // Check if we're hitting from the correct side (inside the gap)
                                float hitAngle = atan(hitPos.y, hitPos.x);
                                float angleFromGapCenter = mod(hitAngle - gapAngle + 3.0*pi, 2.0*pi) - pi;
                                
                                // Only register hit if coming from inside the gap
                                bool comingFromGap = abs(angleFromGapCenter) < gapSize * 1.2;
                                
                                if (comingFromGap) {
                                    closestT = t;
                                    closestBarrier = i;
                                    closestNormal = edgeNormal;
                                    collided = true;
                                    hitGapEdge = true;
                                }
                            }
                        }
                    }
                }
                
                // We need to check both inner and outer edges
                float rOuter = r + ballRadius * 0.5;
                float rInner = max(r - ballRadius * 0.5, 0.001);
                
                // Ray-circle intersection: |rayOrigin + t*rayDir|^2 = r^2
                float a = dot(rayDir, rayDir);
                if (a < 0.000001) continue; // No movement
                
                float b = 2.0 * dot(rayOrigin, rayDir);
                
                // Check both the outer and inner collision surfaces
                for (int surface = 0; surface < 2; surface++) {
                    float checkR = (surface == 0) ? rOuter : rInner;
                    float c = dot(rayOrigin, rayOrigin) - checkR * checkR;
                    float discriminant = b * b - 4.0 * a * c;
                    
                    if (discriminant >= 0.0) {
                        float sqrtDisc = sqrt(discriminant);
                        float t1 = (-b - sqrtDisc) / (2.0 * a);
                        float t2 = (-b + sqrtDisc) / (2.0 * a);
                        
                        // Check both intersections
                        for (int j = 0; j < 2; j++) {
                            float t = (j == 0) ? t1 : t2;
                            if (t > 0.00001 && t < 1.0 && t < closestT) {
                                vec2 hitPos = rayOrigin + rayDir * t;
                                float hitDist = length(hitPos);
                                
                                // Get the actual barrier contact point (on the barrier circle, not expanded)
                                vec2 contactPoint = normalize(hitPos) * r;
                            float hitAngle = atan(hitPos.y, hitPos.x);
                                
                                // Calculate gap angle at the exact collision time
                                float collisionTime = iTime - remainingDt + t * remainingDt;
                                float adjustedGap = barrier.y + collisionTime * BARRIER_ROTATION_SPEED;
                                
                                // Check if in gap with some tolerance
                                float angleDiff = mod(hitAngle - adjustedGap + 3.0*pi, 2.0*pi) - pi;
                                bool inGap = abs(angleDiff) < gapSize * 1.1; // Slight tolerance
                            
                            if (!inGap) {
                                closestT = t;
                                closestBarrier = i;
                                    closestNormal = normalize(contactPoint);
                                collided = true;
                            }
                        }
                        }
                    }
                }
            }
            
            if (collided) {
                // Store collision info for barrier damage
                vec2 hitPos = rayOrigin + rayDir * closestT;
                
                // Gap edges use the stored normal directly
                if (hitGapEdge) {
                    vec2 n = closestNormal;
                    
                    // Reflect velocity off gap edge
                    vel = reflect(vel, n) * BALL_ELASTICITY;
                    
                    // Add random perturbation
                    float randAngle = (hash(iTime * 1000.0 + float(closestBarrier) * 13.7) - 0.5) * BOUNCE_RANDOMNESS;
                    vel = r2d(randAngle) * vel;
                    
                    // Push ball away from edge
                    pos = hitPos + n * 0.005;
                    remainingDt *= (1.0 - closestT);
                } else {
                    // Original ring collision code
                    float r = barrierRadius(closestBarrier);
                    bool fromInside = length(rayOrigin) < r;
                    vec2 n = fromInside ? -closestNormal : closestNormal;
                    
                    // Account for barrier motion (moving inward)
                    vec2 barrierVelocity = normalize(closestNormal) * (-0.1);
                    vec2 relativeVel = vel - barrierVelocity;
                    vec2 reflectedRelativeVel = reflect(relativeVel, n);
                    vel = reflectedRelativeVel + barrierVelocity;
                    vel *= BALL_ELASTICITY;
                    
                    // Add random perturbation
                    float randAngle = (hash(iTime * 1000.0 + float(closestBarrier) * 13.7) - 0.5) * BOUNCE_RANDOMNESS;
                    vel = r2d(randAngle) * vel;
                    
                    // Push ball away from barrier surface
                    if (fromInside) {
                        pos = normalize(hitPos) * (r - ballRadius - 0.002);
                    } else {
                        pos = normalize(hitPos) * (r + ballRadius + 0.002);
                    }
                    remainingDt *= (1.0 - closestT);
                }
            } else {
                pos = rayOrigin + rayDir;
                break;
            }
        }
        
        // Final safety check: ensure ball isn't stuck inside any barrier
        for (int i = 0; i < NUM_BARRIERS; i++) {
            vec4 barrier = texelFetch(iChannel0, ivec2(BARRIER_START + i, 0), 0);
            if (barrier.z > 0.5) continue;
            
            float br = barrierRadius(i);
            float distFromCenter = length(pos);
            float gapAngle = barrier.y + iTime * BARRIER_ROTATION_SPEED;
            float gapSize = mix(BASE_GAP_SIZE, BASE_GAP_SIZE / br, GAP_WIDTH_MODE);
            float posAngle = atan(pos.y, pos.x);
            float angleDiff = mod(posAngle - gapAngle + 3.0*pi, 2.0*pi) - pi;
            bool inGap = abs(angleDiff) < gapSize;
            
            // If ball center is very close to the barrier ring and not in gap, push out
            // Only push outward relative to THIS barrier, not to the outermost one
            if (abs(distFromCenter - br) < ballRadius * 0.8 && !inGap) {
                // Push to whichever side (inside or outside) is closer
                float pushDist = (distFromCenter > br) ? (br + ballRadius + 0.005) : (br - ballRadius - 0.005);
                pos = normalize(pos) * pushDist;
                vel = reflect(vel, normalize(pos)) * BALL_ELASTICITY;
            }
        }
        
        // Outer boundary check
        float dist = length(pos);
        if (dist > OUTER_BOUNDARY_RADIUS - ballRadius) {
            pos = normalize(pos) * (OUTER_BOUNDARY_RADIUS - ballRadius - 0.005);
            vel = reflect(vel, -normalize(pos)) * BALL_ELASTICITY;
        }
        
        fragColor = vec4(pos, vel);
    }
    // Score (pixel 1,0)
    else if (px == SCORE_POS) {
        // Count destroyed barriers as score
        float score = 0.0;
        for (int i = 0; i < NUM_BARRIERS; i++) {
            vec4 barrier = texelFetch(iChannel0, ivec2(BARRIER_START + i, 0), 0);
            if (barrier.z > 0.5) score += 1.0; // Count destroyed barriers
        }
        fragColor = vec4(score, 0.0, 0.0, 0.0);
    }
    // Barrier data
    else if (px.y == 0 && px.x >= BARRIER_START && px.x < BARRIER_START + NUM_BARRIERS) {
        int idx = px.x - BARRIER_START;
        float r = barrierRadius(idx);
        vec4 prevBallData = texelFetch(iChannel0, BALL_POS, 0);
        vec4 ballData = texelFetch(iChannel0, BALL_POS, 0);
        vec2 ballPos = ballData.xy;
        vec2 ballVel = ballData.zw;
        float ballDist = length(ballPos);
        
        float gapAngle = data.y;
        float ballRadius = texelFetch(iChannel0, ivec2(2, 0), 0).x;
        if (ballRadius < 0.001) ballRadius = 0.02;
        
        float angularGap = BASE_GAP_SIZE;
        float constantWidthGap = BASE_GAP_SIZE / r;
        float gapSize = mix(angularGap, constantWidthGap, GAP_WIDTH_MODE);
        
        float health = data.x;
        float destroyed = data.z;
        float gapPassed = 0.0; // Reset gap passage flag each frame
        
        // Check if gap is larger than the entire ring circumference
        float ringCircumference = 2.0 * pi * r;
        float gapArcLength = gapSize * r; // Convert angular gap to arc length
        if (gapArcLength >= ringCircumference) {
            health = 0.0;
            destroyed = 1.0;
        }
        
        // Only process if not already destroyed
        if (data.z < 0.5) {
            // Detect if ball passed through this barrier's gap OR hit the solid part
            
            // Get previous frame's ball data to detect crossing
            vec4 prevBallDataRaw = texelFetch(iChannel0, BALL_POS, 0);
            // We need to check the ball's actual trajectory
            // Since ball physics already ran, we reconstruct previous position
            // But we also check current position relative to barrier
            
            float currDist = length(ballPos);
            
            bool hitBarrier = false;
            bool passedThroughGap = false;
            
            // More robust gap passage detection:
            // Check if ball is very close to the barrier radius AND in the gap region
            // This catches cases where the ball just passed through
            float distToBarrier = abs(currDist - r);
            
            // Calculate current gap angle
            float currentGapAngle = data.y + iTime * BARRIER_ROTATION_SPEED;
            float ballAngle = atan(ballPos.y, ballPos.x);
            float angleDiffCurr = mod(ballAngle - currentGapAngle + 3.0*pi, 2.0*pi) - pi;
            bool ballInGapRegion = abs(angleDiffCurr) < gapSize * 1.3;
            
            // Check if ball recently crossed this barrier by looking at velocity direction
            // If ball is moving outward and is just outside, or moving inward and just inside
            float radialVel = dot(normalize(ballPos), ballVel);
            bool movingOutward = radialVel > 0.0;
            bool justOutside = currDist > r && currDist < r + ballRadius * 2.0 + length(ballVel) * dt;
            bool justInside = currDist < r && currDist > r - ballRadius * 2.0 - length(ballVel) * dt;
            
            // Detect gap passage: ball crossed through gap region recently
            if (ballInGapRegion) {
                if ((movingOutward && justOutside) || (!movingOutward && justInside)) {
                    passedThroughGap = true;
                    gapPassed = 1.0;
                }
            }
            
            // Also check with ray-based method for redundancy
            vec2 prevBallPos = ballPos - ballVel * dt;
            float prevDist = length(prevBallPos - ballVel * dt * 0.5); // Look further back to catch slow collisions
            bool crossedOutward = (prevDist < r - ballRadius * 0.5 && currDist >= r + ballRadius * 0.5);
            bool crossedInward = (prevDist > r + ballRadius * 0.5 && currDist <= r - ballRadius * 0.5);
            
            if ((crossedOutward || crossedInward) && ballInGapRegion) {
                // Verify the crossing happened through the gap
                float crossAngle = atan(ballPos.y, ballPos.x);
                
                float angleDiff = mod(crossAngle - currentGapAngle + 3.0*pi, 2.0*pi) - pi;
                bool inGap = abs(angleDiff) < gapSize * 1.5; // More generous for gap passage
                
                if (inGap) {
                    passedThroughGap = true;
                    gapPassed = 1.0;
                }
            }
            
            // Check both inner and outer collision surfaces
            vec2 rayOrigin = prevBallPos;
            vec2 rayDir = ballVel * dt;
            
            for (int surface = 0; surface < 2; surface++) {
                float checkR = (surface == 0) ? (r + ballRadius) : max(r - ballRadius, 0.001);
                
                float a = dot(rayDir, rayDir);
                if (a < 0.000001) continue;
                
                float b = 2.0 * dot(rayOrigin, rayDir);
                float c = dot(rayOrigin, rayOrigin) - checkR * checkR;
                float discriminant = b * b - 4.0 * a * c;
                
                if (discriminant >= 0.0) {
                    float sqrtDisc = sqrt(discriminant);
                    float t1 = (-b - sqrtDisc) / (2.0 * a);
                    float t2 = (-b + sqrtDisc) / (2.0 * a);
                    
                    for (int j = 0; j < 2; j++) {
                        float t = (j == 0) ? t1 : t2;
                        if (t >= 0.0 && t <= 1.0) {
                            vec2 hitPos = rayOrigin + rayDir * t;
                            float hitAngle = atan(hitPos.y, hitPos.x);
                            
                            // Calculate gap angle at collision time
                            float collisionTime = iTime - dt + t * dt;
                            float adjustedGap = data.y + collisionTime * BARRIER_ROTATION_SPEED;
                            
                            float angleDiff = mod(hitAngle - adjustedGap + 3.0*pi, 2.0*pi) - pi;
                            bool inGap = abs(angleDiff) < gapSize * 1.1;
                            
                            // Only register hit if NOT in gap (ball bounced off solid part)
                            if (!inGap && !passedThroughGap) {
                                hitBarrier = true;
                                break;
                            }
                        }
                    }
                }
                if (hitBarrier) break;
            }
            
            // If ball passed through the gap and GAP_DESTROYS_BARRIER is enabled, destroy the barrier
            if (passedThroughGap && GAP_DESTROYS_BARRIER > 0.5) {
                health = 0.0;
                destroyed = 1.0;
            }
            // If ball hit the solid part, reduce health
            if (hitBarrier) {
                health -= 1.0;
                if (health <= 0.0) {
                    health = 0.0;
                    destroyed = 1.0;
                }
            }
        }
        
        fragColor = vec4(health, data.y, destroyed, gapPassed);
    }
    // Particle data
    else if (px.y >= 1 && px.y <= PARTICLE_ROWS && px.x < NUM_PARTICLES) {
        vec2 ppos = data.xy;
        vec2 pvel = data.zw;
        if (length(pvel) > 0.01) {
            pvel.y -= 0.3 * dt;
            pvel *= 0.98;
            ppos += pvel * dt;
        }
        fragColor = vec4(ppos, pvel);
    }
    else {
        fragColor = data;
    }
}

Buffer B (iChannel1)

// iChannel1: Collision detection buffer (currently handled in iChannel0)

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    fragColor = texelFetch(iChannel0, ivec2(fragCoord), 0);
}

Buffer C (iChannel2)

// iChannel2: Visualization buffer

// Simple digit rendering
float digit(vec2 p, int d) {
    p = p * 2.0 - 1.0;
    if (abs(p.x) > 0.4 || abs(p.y) > 0.5) return 0.0;
    // 7-segment display patterns for digits 0-9
    // Segments: top=1, top-right=2, bottom-right=4, bottom=8, bottom-left=16, top-left=32, middle=64
    int segs = 0;
    if (d == 0) segs = 63;       // 0: all but middle
    else if (d == 1) segs = 6;   // 1: right side only
    else if (d == 2) segs = 91;  // 2: top, top-right, middle, bottom-left, bottom
    else if (d == 3) segs = 79;  // 3: top, top-right, middle, bottom-right, bottom
    else if (d == 4) segs = 102; // 4: top-left, middle, top-right, bottom-right
    else if (d == 5) segs = 109; // 5: top, top-left, middle, bottom-right, bottom
    else if (d == 6) segs = 125; // 6: top, top-left, middle, bottom-left, bottom-right, bottom
    else if (d == 7) segs = 7;   // 7: top, top-right, bottom-right
    else if (d == 8) segs = 127; // 8: all segments
    else if (d == 9) segs = 111; // 9: top, top-left, top-right, middle, bottom-right, bottom
    float s = 0.0;
    if ((segs & 1) > 0) s += step(abs(p.y - 0.4), 0.08) * step(abs(p.x), 0.3);      // top
    if ((segs & 2) > 0) s += step(abs(p.x - 0.3), 0.08) * step(abs(p.y - 0.2), 0.2); // top-right
    if ((segs & 4) > 0) s += step(abs(p.x - 0.3), 0.08) * step(abs(p.y + 0.2), 0.2); // bottom-right
    if ((segs & 8) > 0) s += step(abs(p.y + 0.4), 0.08) * step(abs(p.x), 0.3);       // bottom
    if ((segs & 16) > 0) s += step(abs(p.x + 0.3), 0.08) * step(abs(p.y + 0.2), 0.2); // bottom-left
    if ((segs & 32) > 0) s += step(abs(p.x + 0.3), 0.08) * step(abs(p.y - 0.2), 0.2); // top-left
    if ((segs & 64) > 0) s += step(abs(p.y), 0.08) * step(abs(p.x), 0.3);            // middle
    return clamp(s, 0.0, 1.0);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;
    
    // Camera offset
    vec4 ballDataCam = texelFetch(iChannel0, BALL_POS, 0);
    vec2 cameraOffset = ballDataCam.xy * CAMERA_FOLLOW_AMOUNT;
    
    // World position: subtract camera offset to center ball
    vec2 p = (uv - 0.5) * vec2(ar, 1.0) * 1.8 - cameraOffset;
    
    vec3 col = texture(iChannel2,uv).rgb*0.87;
    
    vec2 ballPos = ballDataCam.xy;
    vec2 ballVel = ballDataCam.zw;
    
    // Draw particles
    for (int row = 1; row <= PARTICLE_ROWS; row++) {
        for (int i = 0; i < NUM_PARTICLES; i++) {
            vec4 part = texelFetch(iChannel0, ivec2(i, row), 0);
            if (length(part.zw) > 0.01) {
                float d = length(p - part.xy);
                float brightness = length(part.zw) * 2.0;
                col += vec3(1.0, 0.6, 0.2) * smoothstep(0.015, 0.005, d) * brightness;
            }
        }
    }
    
    // Draw ball
    float ballD = length(p - ballPos);
    col += vec3(0.9, 0.9, 1.0) * smoothstep(0.025, 0.015, ballD);
    col += vec3(0.3, 0.5, 1.0) * smoothstep(0.04, 0.02, ballD);
    
    // DEBUG: Draw a line showing the ball's velocity ray
    if (DEBUG_VISUALIZATION > 0.5) {
        vec2 rayEnd = ballPos + ballVel * (1.0/60.0);
        float rayDist = length(p - ballPos);
        vec2 toPoint = p - ballPos;
        vec2 rayNorm = normalize(ballVel);
        float alongRay = dot(toPoint, rayNorm);
        float perpDist = length(toPoint - rayNorm * alongRay);
        if (alongRay > 0.0 && alongRay < length(ballVel) * (9./60.0) && perpDist < 0.005) {
            col += vec3(1.0, 1.0, 0.0); // Yellow ray
        }
    }
    
    // Draw outer boundary
    float boundary = smoothstep(0.015, 0.005, abs(length(p) - OUTER_BOUNDARY_RADIUS));
    col += vec3(0.3, 0.3, 0.4) * boundary;
    
    // Get score
    float score = texelFetch(iChannel0, SCORE_POS, 0).x;
    
    // Draw score
    vec2 scoreUV = uv - vec2(0.05, 0.9);
    if (scoreUV.x > 0.0 && scoreUV.x < 0.15 && scoreUV.y > 0.0 && scoreUV.y < 0.08) {
        int sc = int(score);
        vec2 d1uv = (scoreUV - vec2(0.0, 0.0)) / vec2(0.05, 0.08);
        vec2 d2uv = (scoreUV - vec2(0.05, 0.0)) / vec2(0.05, 0.08);
        vec2 d3uv = (scoreUV - vec2(0.1, 0.0)) / vec2(0.05, 0.08);
        col += vec3(1.0) * digit(d1uv, (sc / 100) % 10);
        col += vec3(1.0) * digit(d2uv, (sc / 10) % 10);
        col += vec3(1.0) * digit(d3uv, sc % 10);
    }
    
    fragColor = vec4(col, 1.0);
}

Buffer D (iChannel3)

// iChannel3: Barrier visualization buffer

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;
    
    // Camera offset
    vec4 ballDataCam = texelFetch(iChannel0, BALL_POS, 0);
    vec2 cameraOffset = ballDataCam.xy * CAMERA_FOLLOW_AMOUNT;
    
    // World position: subtract camera offset to center ball
    vec2 p = (uv - 0.5) * vec2(ar, 1.0) * 1.8 - cameraOffset;
    
    // Background color
    vec3 col = vec3(0.02, 0.02, 0.05);
    
    // Draw barriers
    for (int i = 0; i < NUM_BARRIERS; i++) {
        vec4 barrier = texelFetch(iChannel0, ivec2(BARRIER_START + i, 0), 0);
        if (barrier.z > 0.5) continue; // destroyed
        
        float r = barrierRadius(i);
        float dist = length(p);
        float angle = atan(p.y, p.x);
        float gapAngle = barrier.y + iTime * BARRIER_ROTATION_SPEED;
        float angularGap = BASE_GAP_SIZE;
        float constantWidthGap = BASE_GAP_SIZE / r;
        float gapSize = mix(angularGap, constantWidthGap, GAP_WIDTH_MODE);
        bool inGap = abs(mod(angle - gapAngle + pi, 2.0*pi) - pi) < gapSize;
        
        float ring = smoothstep(0.015, 0.01, abs(dist - r));
        float health = barrier.x / BARRIER_INITIAL_HEALTH;
            //vec3 barrierCol = mix(vec3(1.0, 0.2, 0.1), vec3(0.2, 1.0, 0.3), health);
            vec3 scol = (sin(float(i)/1.+cs*1.7)*0.5+0.6);
            vec3 barrierCol = mix(vec3(1.,0.,0.),scol,health);
        if (!inGap) {
            col += ring * barrierCol;
            col += smoothstep(0.08, 0.01, abs(dist - r))*vec3(1.,0.,0.)*(1.-health);
        } else {
            col += ring * barrierCol*0.25;
        }
    }
    
    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