Skip to content

Instantly share code, notes, and snippets.

@gszauer
Created September 2, 2025 20:17
Show Gist options
  • Select an option

  • Save gszauer/1d86a1df16a34ac94c24d1e895dcdaad to your computer and use it in GitHub Desktop.

Select an option

Save gszauer/1d86a1df16a34ac94c24d1e895dcdaad to your computer and use it in GitHub Desktop.
rainbowroad.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rainbow Road</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Scene setup
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x000033, 10, 100);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
camera.lookAt(0, 0, -5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// Rainbow Road Shader
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
uniform float time;
uniform vec2 resolution;
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
// Mario Kart 8 specific rainbow colors
vec3 getRainbowColor(float t) {
float segment = mod(t * 6.0, 6.0);
vec3 colors[6];
colors[0] = vec3(1.0, 0.4, 0.7); // Light pink
colors[1] = vec3(1.0, 0.95, 0.3); // Yellow
colors[2] = vec3(0.3, 1.0, 0.6); // Turquoise
colors[3] = vec3(0.3, 0.8, 1.0); // Cyan
colors[4] = vec3(0.4, 0.4, 1.0); // Blue
colors[5] = vec3(0.8, 0.3, 1.0); // Purple
int idx = int(segment);
float frac = fract(segment);
vec3 col1 = colors[idx];
vec3 col2 = colors[int(mod(float(idx + 1), 6.0))];
return mix(col1, col2, smoothstep(0.0, 1.0, frac));
}
const float MotionSpeed = 2.0;
void main() {
vec2 fragCoord = vUv * resolution;
vec2 uv = (fragCoord - 0.5 * resolution) / resolution.y;
vec3 ro = vec3(0.0, 1.8, time * MotionSpeed);
vec3 rd = normalize(vec3(uv, 1.0));
vec3 col = vec3(0.0);
float d = 0.0;
float mask = 0.0;
for (int i = 0; i < 120; i++) {
vec3 p = ro + rd * d;
float h = max(abs(p.y), abs(p.x) - 6.0);
if (h < 0.01) {
float tileSizeX = 1.0; // Twice as wide
float tileSizeZ = 0.25; // Half as tall
float row = floor(p.z / tileSizeZ);
float rowOffset = (fract(sin(row * 47.823) * 34567.8) - 0.5) * 0.2; // -0.1 to 0.1
// Grid coordinates for solar panels with row offset
float tileX = floor((p.x + rowOffset) / tileSizeX);
float tileZ = row;
float rainbowFlow = p.z * 0.15 + time * 0.3;
vec3 baseColor = getRainbowColor(rainbowFlow);
// Dynamic color shifting within spectrum
float colorShift = sin(time * 2.0 + p.x * 0.5) * 0.1;
vec3 panelColor = getRainbowColor(rainbowFlow + colorShift);
// Solar panel grid lines (with row offset applied)
float localX = fract((p.x + rowOffset) / tileSizeX);
float localZ = fract(p.z / tileSizeZ);
// Subtle parallax effect
float gridWidth = 0.02; // Very thin gaps
// Create smooth height map for tiles
float tileHeight = smoothstep(gridWidth, gridWidth * 4.0, localX) *
smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX) *
smoothstep(gridWidth, gridWidth * 4.0, localZ) *
smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ);
// Add per-tile variation
float tileVariation = fract(sin(tileX * 12.9898 + tileZ * 78.233) * 43758.5453);
tileHeight *= 0.8 + tileVariation * 0.2;
// Calculate surface normal from height gradient
float eps = 0.01;
float heightX1 = smoothstep(gridWidth, gridWidth * 4.0, localX + eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX - eps);
float heightX2 = smoothstep(gridWidth, gridWidth * 4.0, localX - eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localX + eps);
float heightZ1 = smoothstep(gridWidth, gridWidth * 4.0, localZ + eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ - eps);
float heightZ2 = smoothstep(gridWidth, gridWidth * 4.0, localZ - eps) * smoothstep(gridWidth, gridWidth * 4.0, 1.0 - localZ + eps);
vec2 gradient = vec2(heightX1 - heightX2, heightZ1 - heightZ2) * 5.0;
vec3 normal = normalize(vec3(-gradient.x, 1.0, -gradient.y));
// Randomize light direction per tile
float lightAngle = fract(sin(tileX * 45.23 + tileZ * 98.54) * 65432.1) * 6.28318;
float lightTilt = fract(sin(tileX * 23.45 + tileZ * 67.89) * 54321.9) * 0.3 + 0.7;
vec3 lightDir = normalize(vec3(cos(lightAngle) * 0.4, lightTilt, sin(lightAngle) * 0.4));
vec3 viewDir = normalize(-rd);
vec3 halfDir = normalize(lightDir + viewDir);
float NdotL = max(dot(normal, lightDir), 0.0);
float NdotH = max(dot(normal, halfDir), 0.0);
// Ambient + diffuse lighting
float lighting = 0.6 + NdotL * 0.4;
// per-tile intensity variation for visual breakup
float tileIntensity = 0.7 + tileVariation * 0.4; // 70% to 110% brightness per tile
col = panelColor * lighting * tileIntensity;
// Sharp specular highlight only on raised portions
float spec = pow(NdotH, 128.0) * tileHeight;
col += vec3(1.0) * spec * 0.5;
// Edge highlight - tiles catch light on their beveled edges
float edgeFactor = 1.0 - tileHeight;
float edgeSpec = pow(NdotH, 32.0) * edgeFactor * 0.3;
col += panelColor * edgeSpec;
// rim lighting to define tile shape
float rim = 1.0 - max(0.0, dot(viewDir, normal));
rim = pow(rim, 2.0) * tileHeight;
col += panelColor * rim * 0.2;
// Clear shadows in the gaps between tiles (back to original)
float gap = 1.0 - tileHeight;
col *= 1.0 - gap * 0.4;
// Create a rotated/shifted pattern for the underlayer
float underlayX = p.x * 0.7 - p.z * 0.3;
float underlayZ = p.x * 0.3 + p.z * 0.7;
float underlayFlow = underlayZ * 0.2 + time * 0.15;
// Different color offset for contrast
vec3 underlayColor = getRainbowColor(underlayFlow + underlayX * 0.3 + 2.0);
// Make underlayer more visible through semi-transparent tiles
// Stronger in tile centers, weaker at edges
float transparency = tileHeight * 0.4;
col = col * (1.0 - transparency) + underlayColor * transparency;
// Add shimmer where layers interact
float layerInteraction = sin(underlayX * 5.0) * sin(p.z * 5.0) * 0.1;
col += underlayColor * layerInteraction * tileHeight;
// Edge glow for rainbow road effect
float tileEdgeGlow = pow(1.0 - abs(localX - 0.5) * 2.0, 3.0) +
pow(1.0 - abs(localZ - 0.5) * 2.0, 3.0);
col += panelColor * tileEdgeGlow * 0.1;
// Boost colors for vibrancy
col *= 1.4;
// Add bloom-like glow for bright areas
float brightness = dot(col, vec3(0.299, 0.587, 0.114));
vec3 bloom = panelColor * smoothstep(0.5, 1.0, brightness) * 0.5;
col += bloom;
// Distance fade
col /= max(d * 0.025, 1.0);
// Edge transparency and glow
float edgeDist = abs(p.x) / 6.0;
mask = 1.0 - smoothstep(0.8, 1.0, edgeDist);
// Edge rainbow glow
float edgeGlow = exp(-edgeDist * 2.0) * 0.5;
col += getRainbowColor(p.z * 0.1 + time) * edgeGlow * (1.0 - mask);
break;
}
d += h * 0.4;
if (d > 60.0) break;
}
// Space glow effect
vec3 spaceGlow = getRainbowColor(uv.x * 2.0 + time * 0.2);
col += spaceGlow * 0.1 * (1.0 - mask);
col *= mask;
// Tonemapping
col = pow(col, vec3(1.2));
col = col / (1.0 + col);
col = pow(col, vec3(1.0 / 1.5));
// Color enhancement
col = mix(col, col * col * (3.0 - 2.0 * col), vec3(0.5));
gl_FragColor = vec4(col, 1.0);
}
`;
// Create road material
const roadMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
transparent: true,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
// Create road geometry
const roadGeometry = new THREE.PlaneGeometry(8, 200, 32, 128);
const road = new THREE.Mesh(roadGeometry, roadMaterial);
road.rotation.x = -Math.PI / 2;
road.position.y = -1;
road.position.z = -50;
scene.add(road);
// Add stars in background
const starsGeometry = new THREE.BufferGeometry();
const starsCount = 5000;
const positions = new Float32Array(starsCount * 3);
for (let i = 0; i < starsCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 200;
positions[i + 1] = Math.random() * 50 - 10;
positions[i + 2] = (Math.random() - 0.5) * 200;
}
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const starsMaterial = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
transparent: true,
opacity: 0.8
});
const stars = new THREE.Points(starsGeometry, starsMaterial);
scene.add(stars);
// Add ambient light
const ambientLight = new THREE.AmbientLight(0x222244, 0.5);
scene.add(ambientLight);
// Animation
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
// Update shader uniforms
roadMaterial.uniforms.time.value = elapsedTime;
// Rotate stars slowly
stars.rotation.y = elapsedTime * 0.02;
// Slight camera movement for dynamic feel
camera.position.x = Math.sin(elapsedTime * 0.5) * 0.5;
camera.position.y = 2 + Math.sin(elapsedTime * 0.3) * 0.2;
renderer.render(scene, camera);
}
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
roadMaterial.uniforms.resolution.value.set(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment