One and Three Flags, 2024 (ongoing)

WebGL Animation • Interactive • Click and drag to rotate view

This ongoing project continues my exploration of flag symbolism as a volatile form. Using WebGL and fragment shaders, I create a digital representation of the Turkish flag that responds to both programmatic animation and user interaction. The flag exists in a liminal space between the digital and physical, reflecting on how national symbols transform in virtual environments.

The animation uses physically-based rendering techniques to simulate fabric properties, wind dynamics, and environmental lighting. By transforming a static national symbol into a dynamic, interactive digital artifact, the work investigates how our relationship with cultural symbols changes as they migrate into digital spaces.

This piece serves as a digital extension of my earlier work "Flag as a Volatile Form" (2019), continuing to examine how meaning is constructed, preserved, and transformed through digitization and algorithmic representation.

WebGL Fragment Shader Code GLSL • Read-only
// WebGL Fragment Shader for Turkish Flag Animation (Optimized & Detailed)
// Created by Yusuf Özcan, 2024-2026
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform vec4 iMouse;
uniform sampler2D iChannel0;

const float tau = 6.28318530717958647692;

vec2 gFragCoord;

// Common utility functions
vec3 toNormalizedRGB(float r, float g, float b) {
    return vec3(r/255.0, g/255.0, b/255.0);
}

// Flag colors - Turkish flag - exact official colors
const vec3 FLAG_RED = vec3(227.0/255.0, 10.0/255.0, 23.0/255.0);
const vec3 FLAG_WHITE = vec3(1.0, 1.0, 1.0);

// Tone Mapping - optimized for better performance
const float exposure = 1.0;
const vec3 gradient = vec3(1.4,1.5,1.6);
const vec3 whiteSoftness = vec3(.1);
const vec3 blackClip = vec3(.0);
const vec3 blackSoftness = vec3(.05);

vec3 LinearToSRGB(vec3 col) {
    return mix(col*12.92, 1.055*pow(col,vec3(1./2.4))-.055, step(.0031308,col));
}

vec3 HDRtoLDR(vec3 col) {
    col *= exposure;
    col = max(col-blackClip,0.);
    col = sqrt(col*col+blackSoftness*blackSoftness)-blackSoftness;
    col *= gradient;
    vec3 w2 = whiteSoftness*whiteSoftness;
    col += w2;
    col = (1.-col)*.5;
    col = 1. - (sqrt(col*col+w2) + col);
    return LinearToSRGB(col);
}

float linstep(float a, float b, float c) {
    return clamp((c-a)/(b-a),0.,1.);
}

// Set up a camera looking at the scene.
void CamPolar(out vec3 pos, out vec3 ray, in vec3 origin, in vec2 rotation, in float distance, in float zoom) {
    // get rotation coefficients
    vec2 c = vec2(cos(rotation.x),cos(rotation.y));
    vec4 s;
    s.xy = vec2(sin(rotation.x),sin(rotation.y));
    s.zw = -s.xy;
    
    // ray in view space
    ray.xy = gFragCoord.xy - iResolution.xy*.5;
    ray.z = iResolution.y*zoom;
    ray = normalize(ray);
    
    // rotate ray
    ray.yz = ray.yz*c.xx + ray.zy*s.zx;
    ray.xz = ray.xz*c.yy + ray.zx*s.yw;
    
    // position camera
    pos = origin - distance*vec3(c.x*s.y,s.z,c.x*c.y);
}

// Pre-computed orthogonal rotation basis matrix for cyclicNoise (reduces CPU/GPU overhead)
const mat3 rotMatrix = mat3(
    vec3(0.4472136, 0.0, 0.8944272),
    vec3(-0.7807200, 0.4879500, 0.3903600),
    vec3(-0.4364358, -0.8728716, 0.2182179)
);

// Cyclic Noise algorithm (originally by nimitz) for smooth organic fabric folds
float cyclicNoise(vec3 p, float time) {
    float noise = 0.0;
    float amp = 1.0;
    const float gain = 0.55;
    const float lacunarity = 1.4;
    
    const float warp = 0.45;    
    float warpTrk = 1.4;
    const float warpTrkGain = 1.25;
    
    for (int i = 0; i < 4; i++) {
        // Domain warping
        p += sin((p.zxy + vec3(-sin(time)*0.1, time*0.25, time*0.4)) * warpTrk - 2.0 * warpTrk) * warp; 
        
        // Sine wave noise calculation
        float f = sin(dot(cos(p), sin(p.zyx)));
        noise += f * amp;
        
        // Rotate and scale
        p *= rotMatrix;
        p *= lacunarity;
        
        warpTrk *= warpTrkGain;
        amp *= gain;
    }
    
    return (noise * 0.25 + 0.5);
}

// Noise function for clouds - optimized skewing matrix
vec4 Noise(in vec2 x) {
    x = mat2(0.86602540378, -0.5, 0.5, 0.86602540378) * x;
    vec2 p = floor(x);
    vec2 f = fract(x);
    f = f*f*(3.0-2.0*f);
    vec2 uv = (p + f)/256.0;
    return texture2D(iChannel0, uv);
}

// Star shape for Turkish flag
float sdStar5(in vec2 p, in float r, in float rf) {
    float angle = 3.141592653589793 / 10.0;
    float c = cos(angle);
    float s = sin(angle);
    p = mat2(c, -s, s, c) * p;
    
    const vec2 k1 = vec2(0.809016994375, -0.587785252292);
    const vec2 k2 = vec2(-k1.x, k1.y);
    p.x = abs(p.x);
    p -= 2.0*max(dot(k1,p), 0.0)*k1;
    p -= 2.0*max(dot(k2,p), 0.0)*k2;
    p.x = abs(p.x);
    p.y -= r;
    vec2 ba = rf*vec2(-k1.y, k1.x) - vec2(0,1);
    float h = clamp(dot(p,ba)/dot(ba,ba), 0.0, r);
    return length(p-ba*h) * sign(p.y*ba.x-p.x*ba.y);
}

// Water ripple effect - shifts noise horizontally over time to propagate folds from left to right
float RippleHeight(vec2 pos) {
    // Monotonic phase function: prevents waves from slowing to a halt or moving backward.
    // By keeping the derivative of the phase offset strictly positive, waves always travel forward.
    float time = iTime * 1.8 + sin(iTime * 0.5) * 0.5 + cos(iTime * 0.23) * 0.4;
    
    // Moving frame of reference (left to right propagation)
    float travelX = pos.x - time * 1.2;
    vec2 wavePos = vec2(travelX * 1.4, pos.y * 1.5);
    
    // Apply a slight diagonal angle (15 degrees rotation) for natural folds
    wavePos = mat2(0.966, -0.259, 0.259, 0.966) * wavePos;
    
    // Slow-evolving time for gentle morphing of waves as they travel
    float slowTime = time * 0.08;
    float n = cyclicNoise(vec3(wavePos, slowTime), slowTime);
    float f = n - 0.5;
    
    // Damping near the flagpole (x = 0)
    f = f * (1.0 - exp2(-abs(pos.x) * 1.5));
    return f * 0.95;
}

// Distance field for flag surface - optimized
float DistanceField(vec3 pos) {
    return (RippleHeight(pos.xy)-pos.z)*.5;
}

// Calculate normal of flag surface using heightfield gradient (only 3 calls to RippleHeight)
vec3 Normal(vec3 pos) {
    vec2 e = vec2(0.02, 0.0);
    float h = RippleHeight(pos.xy);
    float hx = RippleHeight(pos.xy + e.xy);
    float hy = RippleHeight(pos.xy + e.yx);
    return normalize(vec3(hx - h, hy - h, -e.x));
}

// Map UV coordinates onto distorted flag with improved performance
vec2 UVMapping(vec2 target) {
    // bow the left edge so it's just mounted at 2 points
    float bow = cos(target.y*6.283185/4.)*.1;
    target.x -= bow;
    
    float droop = 0.8;
    target.y += droop;
    
    vec2 uv = vec2(0);
    
    const int n = 4; // Optimized integration steps
    vec2 d = target/float(n);
    vec2 l;
    l.x = RippleHeight(vec2(0,target.y));
    l.y = RippleHeight(vec2(target.x,0));
    
    for (int i=0; i < n; i++) {
        vec2 s;
        s.x = RippleHeight(vec2(d.x*float(i),target.y));
        s.y = RippleHeight(vec2(target.x,d.y*float(i)));
        
        uv += sign(d)*sqrt(pow(s-l,vec2(2.0))+d*d);
        l = s;
    }
    
    uv.y -= droop;
    
    return (uv+vec2(0,1))/vec2(3.0,2.0);
}

// Turkish flag pattern - using exact specifications from reference file
vec3 TurkishFlag(vec2 uv) {
    // Set the flag aspect ratio to 1.5:1 (width:height) as per Turkish standards
    vec2 flagSize = vec2(1.5, 1.0);
    vec2 center = vec2(0.5 * flagSize.x, 0.5 * flagSize.y);
    
    // Background - Official Turkish flag red
    vec3 rectangleColor = toNormalizedRGB(227.0, 10.0, 23.0);
    vec3 circleColor = FLAG_WHITE;
    
    // Create flag with exact dimensions from reference file
    float firstCircleRadius = flagSize.y * 0.25; // Moon outer radius
    vec2 firstCirclePos = vec2(center.x - 0.250, center.y);
    
    float secondCircleRadius = flagSize.y * 0.20; // Moon inner radius
    vec2 secondCirclePos = vec2(center.x - 0.1875, center.y);
    
    // Calculate moon crescent with precise values from reference
    float outerDist = length(uv * flagSize - firstCirclePos) - firstCircleRadius;
    float innerDist = length(uv * flagSize - secondCirclePos) - secondCircleRadius;
    
    // Improve anti-aliasing for sharper edges
    float crescentEdge = smoothstep(-0.001, 0.001, outerDist);
    float crescentHole = smoothstep(-0.001, 0.001, innerDist);
    
    // Star positioning based on exact reference coordinates
    vec2 starCenterPos = vec2(center.x + 0.0708333333333, center.y);
    float starRadius = flagSize.y * 0.125; // Star size
    float starInnerRadius = 0.38; // Inner radius ratio
    
    // Calculate star shape using the reference SDF function
    float starDist = sdStar5((uv * flagSize) - starCenterPos, starRadius, starInnerRadius);
    float starEdge = smoothstep(-0.001, 0.001, starDist);
    
    // Combine elements with correct layering
    vec3 color = rectangleColor;
    
    // Apply moon crescent - white where outer circle is visible but inner circle is not
    if (crescentEdge < 1.0) {
        color = mix(color, circleColor, 1.0 - crescentEdge);
    }
    
    if (crescentHole < 1.0) {
        color = mix(color, rectangleColor, 1.0 - crescentHole);
    }
    
    // Apply star - white where star is visible
    if (starEdge < 1.0) {
        color = mix(color, circleColor, 1.0 - starEdge);
    }
    
    return color;
}

// Flag boundary mask
float Mask(vec2 uv) {
    float xMask = (uv.x < 0.01 || uv.x > 0.99) ? 1.0 : 0.0;
    float yMask = (uv.y < 0.01 || uv.y > 0.99) ? 1.0 : 0.0;
    return max(xMask, yMask);
}

// Checks if a point is within flag bounds
bool isInFlag(vec2 uv) {
    return (uv.x >= -0.001 && uv.x <= 1.001 && uv.y >= -0.001 && uv.y <= 1.001);
}

// Fabric weave texture - optimized for performance
float Weave(vec2 uv) {
    vec2 a = uv*vec2(3.0,2.0)*300.0;
    float f = (sin(a.x)+sin(a.y))*.25+.5;
    f = mix(f, .5, min(1., .2*max(fwidth(a.x), fwidth(a.y))));
    return f;
}

// Seam around flag edge
float Seam(vec2 uv) {
    return smoothstep(.5, .48, abs(uv.y-.5))
        * smoothstep(1., .985, uv.x)
        * smoothstep(.02, .03, uv.x);
}

// Sky color with improved gradient and horizon
vec3 airColourLog2 = vec3(.07,.2,.4);
const vec3 lightDir = normalize(vec3(-3,.7,-.6));

vec3 SkyColour(vec3 ray) {
    vec3 col = exp2(-ray.y/airColourLog2);
    
    // Clouds
    vec2 cloudUV = ray.xz/(ray.y+.1) + iTime*vec2(-.02,0);
    
    // Cloud noise uses the cheaper 2D Noise
    float cloudDetail1 = Noise(2.*cloudUV).x;
    float cloudDetail2 = Noise(4.*cloudUV).x * 0.5;
    float cloudDetail3 = Noise(8.*cloudUV).x * 0.25;
    float cloudDetail4 = Noise(16.*cloudUV).x * 0.125;
    
    float cloudValue = (cloudDetail1 + cloudDetail2 + cloudDetail3 + cloudDetail4) / 1.875;
    float cloudDensity = smoothstep(0.4, 0.6, cloudValue);
    col = mix(col, vec3(0.95, 0.95, 1.0), cloudDensity * max(0., ray.y) * 0.8);
    
    vec3 darkGreenEarthTone = vec3(0.0, 0.01, 0.0);
    
    float horizonSDF = ray.y - .05 - .04*Noise(ray.xz*12.).x;
    col = mix(col, darkGreenEarthTone, smoothstep(.002, -.002, horizonSDF));
    
    vec3 oppLightDir = -lightDir;
    float dotProduct = dot(normalize(ray.xz), normalize(oppLightDir.xz));
    float brightnessFactor = smoothstep(-0.5, 1.0, dotProduct) * 0.3;
    col = mix(col, vec3(0.7, 0.8, 1.0), brightnessFactor * max(0.0, ray.y * 4.0));
    
    return col;
}

vec3 Ambient(vec3 normal) {
    return mix(vec3(.1,.07,.05), vec3(.15,.2,.25), normal.y*.3+.7);
}

// Ray-AABB (Axis-Aligned Bounding Box) intersection check to skip empty space
bool RayAABB(vec3 ro, vec3 rd, vec3 boxMin, vec3 boxMax, out float tMin, out float tMax) {
    vec3 invDir = 1.0 / (rd + vec3(1e-6));
    vec3 t0 = (boxMin - ro) * invDir;
    vec3 t1 = (boxMax - ro) * invDir;
    vec3 tmin = min(t0, t1);
    vec3 tmax = max(t0, t1);
    float tNear = max(max(tmin.x, tmin.y), tmin.z);
    float tFar = min(min(tmax.x, tmax.y), tmax.z);
    tMin = tNear;
    tMax = tFar;
    return tNear < tFar && tFar > 0.0;
}

void mainImage(out vec4 fragColour, in vec2 fragCoord) {
    gFragCoord = fragCoord;
    
    vec3 camPos, ray;
    vec2 mousePos = iMouse.xy/iResolution.xy;
    if (dot(iMouse.xy,vec2(1)) <= 0. || iMouse.z < 0.5) {
        // Auto camera movement
        float autoTime = iTime * 0.5;
        mousePos = vec2(
            0.5 + 0.15 * sin(autoTime),
            0.5 + 0.1 * cos(autoTime * 1.3)
        );
    }
    
    CamPolar(camPos, ray, vec3(1.5,-1.0,0), vec2(-0.2,-0.1)+vec2(0.2,1.0)*mousePos.yx, 12.0, 2.5);

    camPos.y -= 2.0;
    if (camPos.y < 0.1) camPos.y = -1.2;
    
    // Bounding box for flag surface to restrict raymarching
    float tMin = 0.0;
    float tMax = 100.0;
    bool hitBox = RayAABB(camPos, ray, vec3(-0.2, -1.2, -0.6), vec3(3.2, 1.2, 0.6), tMin, tMax);
    
    float t = max(0.0, tMin);
    float h = 1.0;
    bool hitFlag = false;
    
    if (hitBox) {
        const int maxIterations = 32; // Optimized iteration count (higher to prevent transparency at grazing angles)
        for (int i=0; i < maxIterations; i++) {
            if (h < .004 || t > tMax)
                break;
            h = DistanceField(camPos+t*ray);
            t += h * 0.8;
        }
        if (h < .004) {
            hitFlag = true;
        }
    }
    
    vec3 pos = camPos + t*ray;
    vec2 uv = UVMapping(pos.xy);
    
    vec3 albedo = TurkishFlag(uv);
    
    float mask = Mask(uv);
    bool inFlagBounds = isInFlag(uv);
    
    float weave = Weave(uv);
    float seam = Seam(uv);
    
    vec3 normal = Normal(pos);
    
    const vec3 lightCol = vec3(1.8,1.6,1.3);
    
    float nl = dot(normal,lightDir);
    float l = max(nl, .0);
    vec3 scatteredLight = pow(albedo,vec3(2)) * smoothstep(.7, -1., nl);
    
    // Height-based ambient occlusion (darkens folds/valleys)
    float ao = smoothstep(-0.5, 0.5, pos.z) * 0.4 + 0.6;
    vec3 ambient = Ambient(normal) * .3 * ao;
    
    scatteredLight *= mix(.3, .7, weave);
    ambient *= mix(1.7, .3, weave);
    l *= mix(1.15, .85, weave);
    
    scatteredLight *= mix(.5, 1., seam);
    
    vec3 col = albedo;
    col *= (l + scatteredLight)*lightCol + ambient;
    
    // Satin specular sheen (dual specular highlight for premium fabric texture)
    vec3 H = normalize(lightDir - ray);
    float dotNH = max(0.0, dot(normal, H));
    float specSharp = pow(dotNH, 120.0) * 0.4;
    float specBroad = pow(dotNH, 12.0) * 0.15;
    col += lightCol * (specSharp + specBroad) * weave;
    
    // Subsurface Scattering (backlight glow for translucent fabric)
    float transmission = max(0.0, dot(ray, lightDir));
    float sss = pow(transmission, 4.0) * 0.35;
    col += albedo * sss * lightCol;
    
    // Rim lighting
    float rim = pow(1.0 - max(0.0, dot(normal, -ray)), 3.0);
    col += rim * lightCol * 0.2;
    
    vec3 flagCol = col;
    
    // Start with sky background
    fragColour.rgb = SkyColour(ray);
    fragColour.a = 1.0;

    // --- Flagpole Rendering ---
    const float poleThickness = 0.05;
    vec3 poleOrigin = vec3(-poleThickness, 1.1, 0);
    vec3 poleRelativePos = poleOrigin - camPos;

    float distanceOfPoleAlongRayXZ = dot(ray.xz, poleRelativePos.xz)/dot(ray.xz,ray.xz);

    vec2 closestPointXZ = ray.xz * distanceOfPoleAlongRayXZ - poleRelativePos.xz;
    float distToPoleCenterXZ = length(closestPointXZ);

    float poleT = -1.0;
    bool intersection = false;

    if (distToPoleCenterXZ < poleThickness) {
        float travelInside = sqrt(poleThickness*poleThickness - distToPoleCenterXZ*distToPoleCenterXZ);
        poleT = distanceOfPoleAlongRayXZ - travelInside / length(ray.xz);

        vec3 intersectionPoint = camPos + poleT * ray;
        if (poleT > 0.0 && intersectionPoint.y < poleOrigin.y && intersectionPoint.y > -5.0) {
            intersection = true;
        }
    }

    // Apply atmospheric fog
    if (!hitFlag || !inFlagBounds) {
        fragColour.rgb = mix(SkyColour(ray), fragColour.rgb, exp2(-t * vec3(0.012, 0.015, 0.02)));
    }

    // --- Compositing ---
    if (hitFlag && inFlagBounds) {
        fragColour.rgb = flagCol;
    }

    if (intersection && (poleT < t || !hitFlag || !inFlagBounds)) {
        vec3 polePos = (camPos + poleT * ray) - poleOrigin;
        vec3 poleNorm = normalize(vec3(polePos.x, 0.0, polePos.z));
        vec3 poleCol = vec3(0.6) * (lightCol * max(dot(poleNorm, lightDir), 0.0) + Ambient(poleNorm) * 0.5);
        poleCol = mix(SkyColour(ray), poleCol, exp2(-poleT * vec3(0.012, 0.015, 0.02)));
        fragColour.rgb = poleCol;
    }

    // Apply tone mapping
    fragColour.rgb = HDRtoLDR(fragColour.rgb);
    fragColour.a = 1.0;
}

void main() {
    mainImage(gl_FragColor, gl_FragCoord.xy);
}
                    
Press ESC to exit fullscreen