GLSL GRID - RETROSCOPIC CRT SHADER SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GLSL Grid</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; background: #000; overflow: hidden; }
canvas {
display: block;
width: 100vw;
height: 100vh;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
position: relative;
z-index: 2;
}
/* Retroscopic Overlay */
#scope {
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
display: block;
}
#scope>div {
position: absolute;
inset: 0;
}
#scope .glow {
background: radial-gradient(circle at 50% 46%, rgba(30, 255, 130, .08), rgba(0, 0, 0, 0) 62%);
animation: nvflick 3.6s steps(24) infinite;
}
#scope .scan {
background: repeating-linear-gradient(to bottom, rgba(0, 0, 0, 0) 0 2px, rgba(0, 25, 8, .45) 2px 3px);
animation: nvscan 7s linear infinite;
}
#scope .vig {
box-shadow: inset 0 0 200px 50px rgba(0, 0, 0, .8);
}
#scope .mask {
background: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0) 54%, rgba(0, 0, 0, .5) 72%, rgba(0, 0, 0, .94) 100%);
}
@keyframes nvscan {
from { background-position: 0 0; }
to { background-position: 0 240px; }
}
@keyframes nvflick {
0%, 100% { opacity: .55; }
40% { opacity: .9; }
62% { opacity: .4; }
80% { opacity: .75; }
}
</style>
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.165.0/build/three.module.js" } }
</script>
</head>
<body>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<script type="module">
import * as THREE from 'three';
const U = v => ({ value: v });
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const mat = new THREE.ShaderMaterial({
uniforms: {
time: U(0.0),
res: U(new THREE.Vector2(window.innerWidth, window.innerHeight)),
fov: U(1.2),
maxSteps: U(80),
maxDist: U(30.0),
hitEps: U(0.002),
cellSize: U(2.0),
wallThick: U(0.07),
sphereR: U(0.28),
pillarR: U(0.10),
rotX: U(0.0),
rotY: U(0.0),
camZ: U(0.0),
hue1: U(0.60),
hue2: U(0.55),
edgePow: U(4.0),
},
vertexShader: `void main(){ gl_Position=vec4(position.xy,0.,1.); }`,
fragmentShader: `
precision highp float;
uniform float time;
uniform vec2 res;
uniform float fov;
uniform int maxSteps;
uniform float maxDist;
uniform float hitEps;
uniform float cellSize;
uniform float wallThick;
uniform float sphereR;
uniform float pillarR;
uniform float rotX;
uniform float rotY;
uniform float camZ;
uniform float hue1;
uniform float hue2;
uniform float edgePow;
mat3 Rx(float a){ float c=cos(a),s=sin(a); return mat3(1,0,0, 0,c,-s, 0,s,c); }
mat3 Ry(float a){ float c=cos(a),s=sin(a); return mat3(c,0,s, 0,1,0,-s,0,c); }
float smin(float a,float b,float k){
float h=max(k-abs(a-b),0.)/k;
return min(a,b)-h*h*k*.25;
}
float sdSphere(vec3 p, float r){ return length(p)-r; }
float sdCapsule(vec3 p, vec3 a, vec3 b, float r){
vec3 ab=b-a, ap=p-a;
float t=clamp(dot(ap,ab)/dot(ab,ab),0.,1.);
return length(ap-t*ab)-r;
}
float sdLattice(vec3 p){
float C = cellSize;
float W = wallThick * C * 0.5;
float SR = sphereR * C * 0.5;
float PR = pillarR * C * 0.5;
vec3 q = mod(p, C) - C*0.5;
float wx = abs(q.x) - W;
float wy = abs(q.y) - W;
float wz = abs(q.z) - W;
float pillarX = length(q.yz) - PR;
float pillarY = length(q.xz) - PR;
float pillarZ = length(q.xy) - PR;
vec3 corner = sign(q) * C * 0.5;
float sphere = length(q - corner) - SR;
float d = min(sphere, min(min(pillarX, pillarY), pillarZ));
float faceXY = max(wz, max(-abs(q.x)+C*0.5, -abs(q.y)+C*0.5));
float faceYZ = max(wx, max(-abs(q.y)+C*0.5, -abs(q.z)+C*0.5));
float faceXZ = max(wy, max(-abs(q.x)+C*0.5, -abs(q.z)+C*0.5));
d = min(d, min(min(faceXY, faceYZ), faceXZ));
return d;
}
vec3 normal(vec3 p){
float e = 0.001;
return normalize(vec3(
sdLattice(p+vec3(e,0,0)) - sdLattice(p-vec3(e,0,0)),
sdLattice(p+vec3(0,e,0)) - sdLattice(p-vec3(0,e,0)),
sdLattice(p+vec3(0,0,e)) - sdLattice(p-vec3(0,0,e))
));
}
vec3 hsv(float h,float s,float v){
vec4 K=vec4(1.,2./3.,1./3.,3.);
vec3 p=abs(fract(vec3(h)+K.xyz)*6.-K.www);
return v*mix(K.xxx,clamp(p-K.xxx,0.,1.),s);
}
void main(){
vec2 uv = (gl_FragCoord.xy*2.-res) / res.y;
vec3 ro = vec3(0., 0., -8.0 + camZ);
vec3 rd = normalize(vec3(uv * fov, 1.0));
mat3 R = Ry(rotY) * Rx(rotX);
ro = R * ro;
rd = R * rd;
float t = 0.0;
float dist = 0.0;
bool hit = false;
for(int i=0; i<256; i++){
if(i >= maxSteps) break;
dist = sdLattice(ro + rd*t);
if(dist < hitEps){ hit=true; break; }
if(t > maxDist) break;
t += dist * 0.7;
}
if(!hit){
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
vec3 pos = ro + rd*t;
vec3 nor = normal(pos);
vec3 lDir = normalize(vec3(sin(time*0.3), 0.6, cos(time*0.3)));
float diff = max(dot(nor, lDir), 0.0);
float rim = pow(1.0 - abs(dot(nor, -rd)), edgePow);
float fres = pow(1.0 - abs(dot(nor, -rd)), 2.0);
vec3 faceCol = hsv(hue1, 0.9, 0.15 + diff * 0.55);
vec3 rimCol = hsv(hue2, 0.6, 1.0);
vec3 refl = reflect(rd, nor);
float spec = pow(max(dot(refl, lDir), 0.0), 32.0);
vec3 specCol = vec3(0.8, 0.95, 1.0) * spec * 0.8;
vec3 col = faceCol + rimCol * rim * 0.9 + specCol;
float fog = exp(-max(t - 4.0, 0.0) * 0.18);
col *= fog;
col = pow(max(col, vec3(0.0)), vec3(0.45));
gl_FragColor = vec4(col, 1.0);
}
`,
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), mat));
let dragX = 0.0, dragY = 0.0;
let velX = 0.0, velY = 0.0;
let autoX = 0.0, autoY = 0.0;
let isDragging = false;
let lastMX = 0, lastMY = 0;
const DRAG_SENS = 0.005;
const INERTIA = 0.92;
const AUTO_SPEED = { x: 0.13, y: 0.09 };
let autoFrozen = false;
let idleTimer = 0;
let zoomDist = 8.0;
let zoomVel = 0.0;
const ZOOM_MIN = 2.5;
const ZOOM_MAX = 20.0;
const ZOOM_SENS_WHEEL = 0.0012;
const ZOOM_SENS_PINCH = 0.018;
const ZOOM_INERTIA = 0.88;
const canvas = renderer.domElement;
canvas.style.cursor = 'grab';
canvas.addEventListener('mousedown', e => {
isDragging = true;
autoFrozen = true;
idleTimer = 0;
velX = velY = 0;
lastMX = e.clientX;
lastMY = e.clientY;
canvas.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', e => {
if (!isDragging) return;
const dx = e.clientX - lastMX;
const dy = e.clientY - lastMY;
velY = dx * DRAG_SENS;
velX = dy * DRAG_SENS;
dragY += velY;
dragX += velX;
lastMX = e.clientX;
lastMY = e.clientY;
});
window.addEventListener('mouseup', () => {
isDragging = false;
canvas.style.cursor = 'grab';
});
let lastTX = 0, lastTY = 0;
canvas.addEventListener('touchstart', e => {
e.preventDefault();
isDragging = true;
autoFrozen = true;
idleTimer = 0;
velX = velY = 0;
lastTX = e.touches[0].clientX;
lastTY = e.touches[0].clientY;
}, { passive: false });
canvas.addEventListener('touchmove', e => {
e.preventDefault();
if (!isDragging) return;
const dx = e.touches[0].clientX - lastTX;
const dy = e.touches[0].clientY - lastTY;
velY = dx * DRAG_SENS;
velX = dy * DRAG_SENS;
dragY += velY;
dragX += velX;
lastTX = e.touches[0].clientX;
lastTY = e.touches[0].clientY;
}, { passive: false });
canvas.addEventListener('touchend', e => {
if (e.touches.length < 2) lastPinchDist = null;
if (e.touches.length === 0) isDragging = false;
});
canvas.addEventListener('wheel', e => {
e.preventDefault();
zoomVel += e.deltaY * ZOOM_SENS_WHEEL * zoomDist * 0.3;
}, { passive: false });
let lastPinchDist = null;
canvas.addEventListener('touchmove', e => {
if (e.touches.length !== 2) return;
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const d = Math.sqrt(dx*dx + dy*dy);
if (lastPinchDist !== null) {
zoomVel += (lastPinchDist - d) * ZOOM_SENS_PINCH;
}
lastPinchDist = d;
}, { passive: false });
const clock = new THREE.Clock();
let prevElapsed = 0;
(function animate(){
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
const dt = elapsed - prevElapsed;
prevElapsed = elapsed;
if (!isDragging) {
dragX += velX;
dragY += velY;
velX *= INERTIA;
velY *= INERTIA;
const speed = Math.abs(velX) + Math.abs(velY);
if (speed < 0.0001) {
idleTimer++;
if (idleTimer > 30) autoFrozen = false;
} else {
idleTimer = 0;
}
}
if (!autoFrozen) {
autoX += dt * AUTO_SPEED.x;
autoY += dt * AUTO_SPEED.y;
}
const rotX = dragX + (autoFrozen ? 0 : Math.sin(autoX) * 0.55);
const rotY = dragY + (autoFrozen ? 0 : autoY);
mat.uniforms.time.value = elapsed;
mat.uniforms.rotX.value = rotX;
mat.uniforms.rotY.value = rotY;
zoomDist = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoomDist + zoomVel));
zoomVel *= ZOOM_INERTIA;
mat.uniforms.camZ.value = -(zoomDist - 8.0) + Math.sin(elapsed * 0.07) * 0.6;
mat.uniforms.wallThick.value = 0.04 + 0.03 * Math.sin(elapsed * 0.19);
mat.uniforms.pillarR.value = 0.08 + 0.04 * Math.sin(elapsed * 0.27 + 1.0);
mat.uniforms.sphereR.value = 0.22 + 0.08 * Math.sin(elapsed * 0.17 + 2.0);
mat.uniforms.hue1.value = 0.55 + 0.12 * Math.sin(elapsed * 0.05);
mat.uniforms.hue2.value = 0.50 + 0.15 * Math.cos(elapsed * 0.04);
mat.uniforms.edgePow.value = 3.0 + 2.0 * Math.sin(elapsed * 0.11);
renderer.render(scene, camera);
})();
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
mat.uniforms.res.value.set(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>