Created
January 29, 2026 10:18
-
-
Save kibotu/214d1f8737afba0a3613aa457c5b3776 to your computer and use it in GitHub Desktop.
website implementation of https://www.shadertoy.com/view/wdKBz1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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