Skip to content

Instantly share code, notes, and snippets.

@kibotu
Created January 29, 2026 10:18
Show Gist options
  • Select an option

  • Save kibotu/214d1f8737afba0a3613aa457c5b3776 to your computer and use it in GitHub Desktop.

Select an option

Save kibotu/214d1f8737afba0a3613aa457c5b3776 to your computer and use it in GitHub Desktop.
website implementation of https://www.shadertoy.com/view/wdKBz1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shader Test - Train Scene</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
overflow: hidden;
font-family: monospace;
}
#shader-canvas {
display: block;
width: 100vw;
height: 100vh;
}
#info {
position: fixed;
top: 10px;
left: 10px;
color: white;
background: rgba(0, 0, 0, 0.7);
padding: 10px;
border-radius: 5px;
font-size: 12px;
z-index: 100;
}
#error {
position: fixed;
top: 10px;
right: 10px;
color: #ff4444;
background: rgba(0, 0, 0, 0.9);
padding: 10px;
border-radius: 5px;
font-size: 11px;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
z-index: 100;
display: none;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
}
#error.show {
display: block;
}
</style>
</head>
<body>
<canvas id="shader-canvas"></canvas>
<div id="info">
<div>WebGL Status: <span id="webgl-status">Checking...</span></div>
<div>Resolution: <span id="resolution">-</span></div>
<div>Time: <span id="time">0.0</span>s</div>
<div>FPS: <span id="fps">-</span></div>
<div>Iterations: <span id="iterations">64</span></div>
<button id="quality-btn" style="margin-top: 5px; padding: 5px 10px; cursor: pointer;">Toggle Quality</button>
</div>
<div id="error"></div>
<script>
(function() {
const canvas = document.getElementById('shader-canvas');
const infoDiv = document.getElementById('info');
const errorDiv = document.getElementById('error');
const webglStatus = document.getElementById('webgl-status');
const resolutionSpan = document.getElementById('resolution');
const timeSpan = document.getElementById('time');
const fpsSpan = document.getElementById('fps');
function showError(message) {
errorDiv.textContent = message;
errorDiv.classList.add('show');
console.error(message);
}
function hideError() {
errorDiv.classList.remove('show');
}
// Try to get WebGL2 first, then WebGL1
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
showError('WebGL not supported in this browser');
webglStatus.textContent = 'NOT SUPPORTED';
webglStatus.style.color = '#ff4444';
return;
}
const isWebGL2 = gl instanceof WebGL2RenderingContext;
webglStatus.textContent = isWebGL2 ? 'WebGL 2.0' : 'WebGL 1.0';
webglStatus.style.color = '#44ff44';
// Check for float texture support
if (!isWebGL2) {
const floatExt = gl.getExtension('OES_texture_float');
if (!floatExt) {
console.warn('Float textures not supported, some effects may be limited');
}
}
// Vertex shader
const vertexShaderSource = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
// Check if we have WebGL2 for uint support
const hasUintSupport = isWebGL2;
// Quality settings
let rayMarchIterations = 64;
const qualityBtn = document.getElementById('quality-btn');
const iterationsSpan = document.getElementById('iterations');
qualityBtn.addEventListener('click', () => {
if (rayMarchIterations === 64) {
rayMarchIterations = 32;
} else if (rayMarchIterations === 32) {
rayMarchIterations = 128;
} else {
rayMarchIterations = 64;
}
iterationsSpan.textContent = rayMarchIterations;
console.log('Quality changed to:', rayMarchIterations, 'iterations');
});
// Common shader code
const commonShaderCode = `
#define PI 3.14159265
#define saturate(x) clamp(x,0.,1.)
#define SUNDIR normalize(vec3(0.2,.3,2.))
#define FOGCOLOR vec3(1.,.2,.1)
float time;
float smin( float a, float b, float k ) {
float h = max(k-abs(a-b),0.0);
return min(a, b) - h*h*0.25/k;
}
float smax( float a, float b, float k ) {
k *= 1.4;
float h = max(k-abs(a-b),0.0);
return max(a, b) + h*h*h/(6.0*k*k);
}
float box( vec3 p, vec3 b, float r ) {
vec3 q = abs(p) - b;
return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r;
}
float capsule( vec3 p, float h, float r ) {
p.x -= clamp( p.x, 0.0, h );
return length( p ) - r;
}
// WebGL1 compatible hash function (no uint)
vec3 hash3( float n ) {
return fract(sin(vec3(n, n+1.0, n+2.0)) * vec3(43758.5453123, 22578.1459123, 19642.3490423));
}
float hash( float p ) {
return fract(sin(p)*43758.5453123);
}
mat2 rot(float v) {
float a = cos(v);
float b = sin(v);
return mat2(a,-b,b,a);
}
float train(vec3 p) {
vec3 op = p; // original position
// base
float d = abs(box(p-vec3(0., 0., 0.), vec3(100.,1.5,5.), 0.))-.1;
// windows - repeat along x axis
vec3 wp = p;
wp.x = mod(wp.x+1.0, 4.0)-2.0; // repeat every 4 units
d = smax(d, -box(wp-vec3(0.,0.25,5.), vec3(1.2,.5,0.0), .3), 0.03);
// window frames
wp.x = mod(op.x-.8, 2.0)-1.0; // repeat every 2 units (aligned with seats)
d = smin(d, box(wp-vec3(0.,0.57,5.), vec3(.05,.05,0.1), .0), 0.001);
// seats
p.x = mod(p.x-.8,2.)-1.;
p.z = abs(p.z-4.3)-.3;
d = smin(d, box(p-vec3(0.,-1., 0.), vec3(.3,.1-cos(p.z*PI*4.)*.01,.2),.05), 0.05);
d = smin(d, box(p-vec3(0.4+pow(p.y+1.,2.)*.1,-0.38, 0.), vec3(.1-cos(p.z*PI*4.)*.01,.7,.2),.05), 0.1);
d = smin(d, box(p-vec3(0.1,-1.3, 0.), vec3(.1,.2,.1),.05), 0.01);
return d;
}
float catenary(vec3 p) {
p.z -= 12.;
vec3 pp = p;
p.x = mod(p.x,100.)-50.;
// base
float d = box(p-vec3(0.,0.,0.), vec3(.0,3.,.0), .1);
d = smin(d, box(p-vec3(0.,2.,0.), vec3(.0,0.,1.), .1), 0.05);
p.z = abs(p.z-0.)-2.;
d = smin(d, box(p-vec3(0.,2.2,-1.), vec3(.0,0.2,0.), .1), 0.01);
// lines
pp.z = abs(pp.z-0.)-2.;
d = min(d, capsule(p-vec3(-50.,2.4-abs(cos(pp.x*.01*PI)),-1.),10000.,.02));
d = min(d, capsule(p-vec3(-50.,2.9-abs(cos(pp.x*.01*PI)),-2.),10000.,.02));
return d;
}
float city(vec3 p) {
vec3 pp = p;
vec2 pId = floor((p.xz)/30.);
vec3 rnd = hash3(pId.x + pId.y*1000.0);
p.xz = mod(p.xz, vec2(30.))-15.;
float h = 5.0+(pId.y-3.0)*5.0+rnd.x*20.0;
float offset = (rnd.z*2.0-1.0)*10.0;
float d = box(p-vec3(offset,-5.,0.), vec3(5.,h,5.), 0.1);
d = min(d, box(p-vec3(offset,-5.,0.), vec3(1.,h+pow(rnd.y,4.)*10.,1.), 0.1));
d = max(d,-pp.z+100.);
d = max(d,pp.z-300.);
return d*.6;
}
float map(vec3 p) {
float d = train(p);
// Faster acceleration: starts at 30% speed, reaches full speed in ~5 seconds
p.x -= mix(time*4.5, time*15., saturate(time*.2));
d = min(d, catenary(p));
d = min(d, city(p));
d = min(d, city(p+vec3(15.,0.,0.)));
return d;
}
`;
// Fragment shader - Simplified single-pass version
function createFragmentShader(iterations) {
return `
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
${commonShaderCode}
float trace(vec3 ro, vec3 rd, vec2 nearFar) {
float t = nearFar.x;
for(int i=0; i<${iterations}; i++) {
float d = map(ro+rd*t);
t += d;
if( abs(d) < 0.01 || t > nearFar.y )
break;
}
return t;
}
vec3 normal(vec3 p) {
vec2 eps = vec2(0.01, 0.);
float d = map(p);
vec3 n;
n.x = d - map(p-eps.xyy);
n.y = d - map(p-eps.yxy);
n.z = d - map(p-eps.yyx);
return normalize(n);
}
vec3 skyColor(vec3 rd) {
vec3 col = FOGCOLOR;
col += vec3(1.,.3,.1)*1. * pow(max(dot(rd,SUNDIR),0.),30.);
col += vec3(1.,.3,.1)*10. * pow(max(dot(rd,SUNDIR),0.),10000.);
return col;
}
void main() {
time = u_time;
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec2 v = -1.0+2.0*uv;
v.x *= u_resolution.x/u_resolution.y;
vec3 ro = vec3(-1.5,-.4,1.2);
vec3 rd = normalize(vec3(v, 2.5));
rd.xz = rot(.15)*rd.xz;
rd.yz = rot(.1)*rd.yz;
float t = trace(ro,rd, vec2(0.,300.));
vec3 p = ro + rd * t;
vec3 n = normal(p);
vec3 col = skyColor(rd);
if (t < 300.) {
vec3 diff = vec3(1.,.5,.3) * max(dot(n,SUNDIR),0.);
vec3 amb = vec3(0.1,.15,.2);
col = (diff*0.3 + amb*.3)*.02;
// Simple reflection for windows
if (p.z<6.) {
vec3 rrd = reflect(rd,n);
float fre = pow( saturate( 1.0 + dot(n,rd)), 8.0 );
vec3 rcol = skyColor(rrd);
col = mix(col, rcol, fre*.1);
}
col = mix(col, FOGCOLOR, smoothstep(100.,500.,t));
}
// Add godrays effect
float godray = pow(max(dot(rd,SUNDIR),0.),50.) * 0.3;
col += FOGCOLOR * godray * 0.01;
// Color correction
col = pow(col, vec3(1./2.2));
col = pow(col, vec3(.6,1.,.8*(uv.y*.2+.8)));
// Vignetting
float vignetting = pow(uv.x*uv.y*(1.-uv.x)*(1.-uv.y), .3)*2.5;
col *= vignetting;
// Fade in (instant - no fade)
// col *= smoothstep(0.,10.,u_time);
gl_FragColor = vec4(col, 1.0);
}
`;
}
const fragmentShaderSource = createFragmentShader(rayMarchIterations);
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const error = gl.getShaderInfoLog(shader);
showError('Shader compile error:\n' + error);
gl.deleteShader(shader);
return null;
}
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const error = gl.getProgramInfoLog(program);
showError('Program link error:\n' + error);
gl.deleteProgram(program);
return null;
}
return program;
}
console.log('Creating vertex shader...');
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
if (!vertexShader) {
// Try a simple fallback shader
console.log('Trying fallback shader...');
const fallbackFragmentShader = `
precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
vec3 col = vec3(1.0, 0.2, 0.1) * (0.5 + 0.5 * sin(u_time + uv.x * 3.0));
gl_FragColor = vec4(col, 1.0);
}
`;
const fallbackFS = createShader(gl, gl.FRAGMENT_SHADER, fallbackFragmentShader);
if (fallbackFS) {
const fallbackVS = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fallbackProgram = createProgram(gl, fallbackVS, fallbackFS);
if (fallbackProgram) {
showError('Main shader failed, using fallback gradient');
// Continue with fallback...
}
}
return;
}
console.log('Creating fragment shader...');
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
if (!fragmentShader) return;
console.log('Creating program...');
const program = createProgram(gl, vertexShader, fragmentShader);
if (!program) return;
console.log('Shader program created successfully!');
hideError();
// Set up geometry
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, 1,
]);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'position');
const timeLocation = gl.getUniformLocation(program, 'u_time');
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
// FPS counter
let lastTime = 0;
let frameCount = 0;
let fps = 0;
function resize() {
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
resolutionSpan.textContent = `${canvas.width} x ${canvas.height}`;
}
}
function render(time) {
resize();
// Calculate FPS
frameCount++;
if (time - lastTime >= 1000) {
fps = Math.round(frameCount * 1000 / (time - lastTime));
fpsSpan.textContent = fps;
frameCount = 0;
lastTime = time;
}
// Update time display
timeSpan.textContent = (time * 0.001).toFixed(1);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.enableVertexAttribArray(positionLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.uniform1f(timeLocation, time * 0.001);
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(render);
}
resize();
window.addEventListener('resize', resize);
requestAnimationFrame(render);
console.log('Render loop started!');
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment