OCEAN 2 - 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>Ocean</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { width: 100% !important; height: 100% !important; display: block; }
#stage {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
filter: grayscale(1) sepia(1) hue-rotate(80deg) saturate(5) brightness(1.15) contrast(1.2);
z-index: 2;
}
#hint {
position: fixed;
bottom: 16px;
right: 20px;
font-family: Georgia, serif;
font-size: 13px;
color: rgba(255,255,255,0.4);
letter-spacing: 0.04em;
pointer-events: none;
z-index: 3;
}
/* 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>
</head>
<body>
<div id="scope">
<div class="glow"></div>
<div class="scan"></div>
<div class="vig"></div>
<div class="mask"></div>
</div>
<div id="stage">
<canvas id="c"></canvas>
</div>
<div id="hint">drag to orbit</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(function () {
const canvas = document.getElementById('c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;
renderer.outputEncoding = THREE.sRGBEncoding;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 20000);
// Orbit controls
let isDragging = false, lastX = 0, lastY = 0;
let spherical = { theta: 0.3, phi: 1.1, radius: 110 };
function updateCamera() {
camera.position.set(
spherical.radius * Math.sin(spherical.phi) * Math.sin(spherical.theta),
spherical.radius * Math.cos(spherical.phi),
spherical.radius * Math.sin(spherical.phi) * Math.cos(spherical.theta)
);
camera.position.y = Math.max(camera.position.y, 5);
camera.lookAt(0, 10, 0);
}
updateCamera();
canvas.addEventListener('mousedown', e => { isDragging = true; lastX = e.clientX; lastY = e.clientY; });
canvas.addEventListener('touchstart', e => { isDragging = true; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; });
window.addEventListener('mouseup', () => isDragging = false);
window.addEventListener('touchend', () => isDragging = false);
window.addEventListener('mousemove', e => {
if (!isDragging) return;
spherical.theta -= (e.clientX - lastX) * 0.005;
spherical.phi = Math.max(0.1, Math.min(Math.PI * 0.48, spherical.phi + (e.clientY - lastY) * 0.005));
lastX = e.clientX; lastY = e.clientY;
updateCamera();
});
window.addEventListener('touchmove', e => {
if (!isDragging) return;
spherical.theta -= (e.touches[0].clientX - lastX) * 0.005;
spherical.phi = Math.max(0.1, Math.min(Math.PI * 0.48, spherical.phi + (e.touches[0].clientY - lastY) * 0.005));
lastX = e.touches[0].clientX; lastY = e.touches[0].clientY;
updateCamera();
});
// Sky
const skyMat = new THREE.ShaderMaterial({
uniforms: { sunDirection: { value: new THREE.Vector3() } },
vertexShader: `
varying vec3 vWorldPos;
void main() {
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 sunDirection;
varying vec3 vWorldPos;
void main() {
vec3 dir = normalize(vWorldPos);
float sunDot = clamp(dot(dir, normalize(sunDirection)), 0.0, 1.0);
float sundisk = pow(sunDot, 256.0) * 2.0;
float haze = pow(clamp(1.0 - dir.y, 0.0, 1.0), 4.0) * 0.6;
vec3 skyColor = mix(vec3(0.1, 0.2, 0.5), vec3(0.6, 0.35, 0.2), haze);
skyColor += vec3(1.0, 0.85, 0.4) * sundisk;
gl_FragColor = vec4(skyColor, 1.0);
}`,
side: THREE.BackSide,
depthWrite: false
});
scene.add(new THREE.Mesh(new THREE.SphereGeometry(8000, 32, 16), skyMat));
const theta = Math.PI * (0.49 - 0.5);
const phi = 2 * Math.PI * (0.205 - 0.5);
const sunDir = new THREE.Vector3(
Math.cos(phi),
Math.sin(phi) * Math.sin(theta),
Math.sin(phi) * Math.cos(theta)
);
skyMat.uniforms.sunDirection.value.copy(sunDir);
const sunLight = new THREE.DirectionalLight(0xffcc66, 2.5);
sunLight.position.copy(sunDir).multiplyScalar(100);
scene.add(sunLight);
scene.add(new THREE.AmbientLight(0x334466, 0.8));
// Water — all wave math in the fragment shader to avoid polygon grid artifacts
const waterGeo = new THREE.PlaneGeometry(10000, 10000, 1, 1);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
sunDirection: { value: sunDir },
sunColor: { value: new THREE.Color(0xffcc44) },
waterColor: { value: new THREE.Color(0x001e0f) },
cameraPos: { value: new THREE.Vector3() },
},
vertexShader: `
varying vec3 vWorldPos;
varying vec2 vUv;
void main() {
vUv = uv;
vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform float time;
uniform vec3 sunDirection;
uniform vec3 sunColor;
uniform vec3 waterColor;
uniform vec3 cameraPos;
varying vec3 vWorldPos;
varying vec2 vUv;
float waveH(float x, float z, float t) {
float y = 0.0;
y += 1.20 * sin(0.08 * x + 0.90 * t);
y += 0.70 * sin(0.13 * z + 1.30 * t + 0.50);
y += 0.50 * sin(0.10 * x + 0.08 * z + 1.70 * t + 1.20);
y += 0.30 * sin(0.20 * x - 0.14 * z + 2.30 * t + 2.10);
y += 0.15 * sin(0.40 * z + 3.10 * t + 0.30);
y += 0.20 * sin(0.30 * x + 0.15 * z - 2.50 * t + 1.80);
y += 0.08 * sin(0.60 * x + 0.50 * z + 4.00 * t + 0.70);
y += 0.05 * sin(0.90 * z - 0.40 * x + 3.50 * t + 2.40);
return y;
}
vec3 waveNormal(float x, float z, float t) {
float eps = 0.5;
float h = waveH(x, z, t);
float hx = waveH(x + eps, z, t);
float hz = waveH(x, z + eps, t);
return normalize(vec3(-(hx - h) / eps, 1.0, -(hz - h) / eps));
}
void main() {
float x = vWorldPos.x;
float z = vWorldPos.z;
vec3 N = waveNormal(x, z, time);
vec3 V = normalize(cameraPos - vWorldPos);
vec3 L = normalize(sunDirection);
float fresnel = pow(1.0 - max(dot(N, V), 0.0), 4.0) * 0.85 + 0.04;
float spec = pow(max(dot(reflect(-L, N), V), 0.0), 512.0) * 3.0;
vec3 shallowColor = vec3(0.0, 0.25, 0.35);
float depth = clamp(N.y, 0.0, 1.0);
vec3 waterC = mix(waterColor, shallowColor, depth * 0.4);
vec3 skyRefl = mix(vec3(0.55, 0.32, 0.12), vec3(0.1, 0.2, 0.5), clamp(N.y * 1.5, 0.0, 1.0));
vec3 col = mix(waterC, skyRefl, fresnel);
col += sunColor * spec;
// Subtle foam on wave crests
float waveTop = clamp((N.y - 0.85) * 8.0, 0.0, 1.0);
col = mix(col, vec3(1.0), waveTop * 0.18);
gl_FragColor = vec4(col, 0.95);
}`,
transparent: true,
side: THREE.FrontSide,
});
scene.add(new THREE.Mesh(waterGeo, waterMat));
function animate() {
requestAnimationFrame(animate);
const t = performance.now() * 0.001;
waterMat.uniforms.time.value = t;
waterMat.uniforms.cameraPos.value.copy(camera.position);
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
})();
</script>
</body>
</html>