One and Three Flags, 2024 (ongoing)
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 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);
}