<canvas id="seascape"></canvas>
<div id="infos">
Seascape<br/>
Optimized<br/>
<a href="https://www.shadertoy.com/view/Ms2SD1">Credits</a> 🌊
</div>
#seascape {
display: block;
width: 100vw;
height: 100vh;
user-select: none;
}
#infos {
position: fixed;
bottom: 0;
left: 0;
font-family: 'Courier New', Courier, monospace;
padding: .3rem .4rem;
color: #FFF;
background: rgba(0, 0, 0, 0.3);
}
#infos a {
color: inherit;
border-bottom: solid 0.5px var(--link-color);
}
/**
* @class SeascapeShader
*/
class SeascapeShader {
// original shader : https://www.shadertoy.com/view/Ms2SD1
constructor(canvas) {
this.canvas = canvas;
this.gl = canvas.getContext(
"webgl",
{
antialias: false,
depth: false,
alpha: false,
powerPreference: "high-performance"
}
);
if(!this.gl) {
console.error("WebGL not supported");
return;
}
// Disable unnecessary features
this.gl.disable(this.gl.DEPTH_TEST);
this.gl.disable(this.gl.BLEND);
this.gl.clearColor(
0.0,
0.0,
0.0,
1.0
);
// Camera state
this.offset = 0;
this.timeOffset = 0;
this.pointing = false;
this.initShaders();
this.initBuffers();
this.initEvents();
this.resize();
// Cache bound render method
this.boundRender = this.render.bind(this);
requestAnimationFrame(this.boundRender);
}
initShaders() {
// Vertex shader
const vsSource = `
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
`;
// Fragment shader
const fsSource = `
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float iOffset;
const int NUM_STEPS = 8;
const float PI = 3.141592;
const float EPSILON = 1e-3;
#define EPSILON_NRM (0.1 / iResolution.x)
// Sea constants
const int ITER_GEOMETRY = 3;
const int ITER_FRAGMENT = 4;
const float SEA_HEIGHT = 0.8;
const float SEA_CHOPPY = 4.0;
const float SEA_SPEED = 0.9;
const float SEA_FREQ = 0.16;
const vec3 SEA_BASE = vec3(0.0, 0.09, 0.18);
const vec3 SEA_WATER_COLOR = vec3(0.8, 0.9, 0.6) * 0.6;
#define SEA_TIME (iTime * SEA_SPEED)
const mat2 octave_m = mat2(1.6, 1.2, -1.2, 1.6);
// Hash function
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
// Noise function
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return -1.0 + 2.0 * (a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y);
}
// Rotation matrix calculation
mat3 fromEuler(vec3 ang) {
vec2 a1 = vec2(sin(ang.x), cos(ang.x));
vec2 a2 = vec2(sin(ang.y), cos(ang.y));
vec2 a3 = vec2(sin(ang.z), cos(ang.z));
mat3 m;
m[0] = vec3(a1.y * a3.y + a1.x * a2.x * a3.x, a1.y * a2.x * a3.x + a3.y * a1.x, -a2.y * a3.x);
m[1] = vec3(-a2.y * a1.x, a1.y * a2.y, a2.x);
m[2] = vec3(a3.y * a1.x * a2.x + a1.y * a3.x, a1.x * a3.x - a1.y * a3.y * a2.x, a2.y * a3.y);
return m;
}
// Lighting functions
float diffuse(vec3 n, vec3 l, float p) {
return pow(dot(n, l) * 0.4 + 0.61, p);
}
float specular(vec3 n, vec3 l, vec3 e, float s) {
float nrm = (s + 8.0) / (PI * 8.0);
return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
}
// Sky color
vec3 getSkyColor(vec3 e) {
e.y = (max(e.y, 0.0) * 0.8 + 0.2) * 0.8;
return vec3(pow(1.0 - e.y, 2.0), 1.0 - e.y, 0.6 + (1.0 - e.y) * 0.4) * 1.1;
}
// Sea octave
float sea_octave(vec2 uv, float choppy) {
uv += noise(uv);
vec2 wv = 1.0 - abs(sin(uv));
vec2 swv = abs(cos(uv));
wv = mix(wv, swv, wv);
return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}
// Map functions
float map(vec3 p) {
float freq = SEA_FREQ;
float amp = SEA_HEIGHT;
float choppy = SEA_CHOPPY;
vec2 uv = p.xz;
uv.x *= 0.75;
float h = 0.0;
for(int i = 0; i < ITER_GEOMETRY; i++) {
float d = sea_octave((uv + SEA_TIME) * freq, choppy);
d += sea_octave((uv - SEA_TIME) * freq, choppy);
h += d * amp;
uv *= octave_m;
freq *= 1.9;
amp *= 0.22;
choppy = mix(choppy, 1.0, 0.2);
}
return p.y - h;
}
float map_detailed(vec3 p) {
float freq = SEA_FREQ;
float amp = SEA_HEIGHT;
float choppy = SEA_CHOPPY;
vec2 uv = p.xz;
uv.x *= 0.75;
float h = 0.0;
for(int i = 0; i < ITER_FRAGMENT; i++) {
float d = sea_octave((uv + SEA_TIME) * freq, choppy);
d += sea_octave((uv - SEA_TIME) * freq, choppy);
h += d * amp;
uv *= octave_m;
freq *= 1.9;
amp *= 0.22;
choppy = mix(choppy, 1.0, 0.2);
}
return p.y - h;
}
// Sea color calculation
vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) {
float fresnel = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
fresnel = pow(fresnel, 3.0) * 0.5;
vec3 reflected = getSkyColor(reflect(eye, n));
vec3 refracted = SEA_BASE + diffuse(n, l, 80.0) * SEA_WATER_COLOR * 0.15;
vec3 color = mix(refracted, reflected, fresnel);
float atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
color += SEA_WATER_COLOR * (p.y - SEA_HEIGHT) * 0.18 * atten;
color += specular(n, l, eye, 60.0);
return color;
}
// Normal calculation
vec3 getNormal(vec3 p, float eps) {
vec3 n;
n.y = map_detailed(p);
n.x = map_detailed(vec3(p.x + eps, p.y, p.z)) - n.y;
n.z = map_detailed(vec3(p.x, p.y, p.z + eps)) - n.y;
n.y = eps;
return normalize(n);
}
// Ray tracing
float heightMapTracing(vec3 ori, vec3 dir, out vec3 p) {
float tm = 0.0;
float tx = 1000.0;
float hx = map(ori + dir * tx);
if(hx > 0.0) {
p = ori + dir * tx;
return tx;
}
float hm = map(ori);
float tmid = 0.0;
for(int i = 0; i < NUM_STEPS; i++) {
tmid = mix(tm, tx, hm / (hm - hx));
p = ori + dir * tmid;
float hmid = map(p);
if (hmid < 0.0) {
tx = tmid;
hx = hmid;
}
else {
tm = tmid;
hm = hmid;
}
if (abs(hmid) < EPSILON)
break;
}
return tmid;
}
// Main pixel calculation
vec3 getPixel(vec2 coord, float time) {
vec2 uv = coord / iResolution.xy;
uv = uv * 2.0 - 1.0;
uv.x *= iResolution.x / iResolution.y;
// Camera
vec3 ang = vec3(sin(time * 1.0) * 0.05, sin(time) * 0.1, time * 0.07);
vec3 ori = vec3(0.0, 3.5, time * 5.0);
vec3 dir = normalize(vec3(uv.xy, -2.0));
dir.z += length(uv) * 0.19;
dir = normalize(dir) * fromEuler(ang);
// Tracing
vec3 p;
heightMapTracing(ori, dir, p);
vec3 dist = p - ori;
vec3 n = getNormal(p, dot(dist, dist) * EPSILON_NRM);
vec3 light = normalize(vec3(0.0, 1.0, 0.8));
// Calculate final color
vec3 sea = getSeaColor(p, n, light, dir, dist);
vec3 sky = getSkyColor(dir);
float skyFactor = smoothstep(0.0, -0.05, dir.y);
return mix(sky, sea, pow(skyFactor, 0.2));
}
void main() {
// Combined time with offset
float time = iTime + iOffset;
// Get color and apply gamma correction
vec3 color = getPixel(gl_FragCoord.xy, time);
gl_FragColor = vec4(pow(color, vec3(0.65)), 1.0);
}
`;
// Create shader program
this.shaderProgram = this.createShaderProgram(
vsSource,
fsSource
);
// Store locations
this.programInfo = {
attribLocations: {
vertexPosition: this.gl.getAttribLocation(
this.shaderProgram,
"aVertexPosition"
)
},
uniformLocations: {
resolution: this.gl.getUniformLocation(
this.shaderProgram,
"iResolution"
),
time: this.gl.getUniformLocation(
this.shaderProgram,
"iTime"
),
offset: this.gl.getUniformLocation(
this.shaderProgram,
"iOffset"
)
}
};
}
createShaderProgram(vsSource, fsSource) {
const vertexShader = this.loadShader(
this.gl.VERTEX_SHADER,
vsSource
);
const fragmentShader = this.loadShader(
this.gl.FRAGMENT_SHADER,
fsSource
);
const program = this.gl.createProgram();
this.gl.attachShader(
program,
vertexShader
);
this.gl.attachShader(
program,
fragmentShader
);
this.gl.linkProgram(program);
if(!this.gl.getProgramParameter(
program,
this.gl.LINK_STATUS
)) {
console.error("Shader program link fail");
return null;
}
// Clean up shaders after linking
this.gl.deleteShader(vertexShader);
this.gl.deleteShader(fragmentShader);
return program;
}
loadShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(
shader,
source
);
this.gl.compileShader(shader);
if(!this.gl.getShaderParameter(
shader,
this.gl.COMPILE_STATUS
)) {
console.error(
"Shader compilation error",
this.gl.getShaderInfoLog(shader)
);
this.gl.deleteShader(shader);
return null;
}
return shader;
}
initBuffers() {
// Single quad buffer creation
const positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(
this.gl.ARRAY_BUFFER,
positionBuffer
);
// Quad coordinates
const positions = new Float32Array([
-1.0, -1.0,
1.0, -1.0,
-1.0, 1.0,
1.0, 1.0
]);
this.gl.bufferData(
this.gl.ARRAY_BUFFER,
positions,
this.gl.STATIC_DRAW
);
this.buffers = {
position: positionBuffer
};
}
initEvents() {
Object.entries({
"pointerdown": this.onPointerDown,
"pointermove": this.onPointerMove,
"pointerup": this.onPointerUp,
"pointerleave": this.onPointerUp
})
.forEach(([evtName, evtHandler]) =>
this.canvas.addEventListener(
evtName,
evtHandler.bind(this)
));
this.canvas.addEventListener(
"touchstart",
this.doNotTouch.bind(this)
);
window.addEventListener(
"resize",
this.resize.bind(this)
);
}
onPointerDown(evt) {
this.pointing = true;
this.startX = evt.clientX;
}
onPointerMove(evt) {
if(this.pointing)
this.offset = (this.startX - evt.clientX) * 0.001;
}
onPointerUp() {
if(this.pointing) {
this.timeOffset += this.offset;
this.offset = 0;
this.pointing = false;
}
}
doNotTouch(evt) {
evt.preventDefault();
}
resize() {
// Set canvas to display size
const displayWidth = window.innerWidth;
const displayHeight = window.innerHeight;
this.canvas.width = displayWidth;
this.canvas.height = displayHeight;
this.gl.viewport(
0,
0,
displayWidth,
displayHeight
);
}
render(timestamp) {
// Convert to seconds and slow down
const time = timestamp * 0.001;
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
this.gl.useProgram(this.shaderProgram);
// Set uniforms
this.gl.uniform2f(
this.programInfo.uniformLocations.resolution,
this.canvas.width,
this.canvas.height
);
this.gl.uniform1f(
this.programInfo.uniformLocations.time,
time
);
this.gl.uniform1f(
this.programInfo.uniformLocations.offset,
this.offset + this.timeOffset
);
// Draw setup
this.gl.bindBuffer(
this.gl.ARRAY_BUFFER,
this.buffers.position
);
this.gl.vertexAttribPointer(
this.programInfo.attribLocations.vertexPosition,
2,
this.gl.FLOAT,
false,
0,
0
);
this.gl.enableVertexAttribArray(this.programInfo.attribLocations.vertexPosition);
// Draw the quad
this.gl.drawArrays(
this.gl.TRIANGLE_STRIP,
0,
4
);
// Frame loop
requestAnimationFrame(this.boundRender);
}
}
new SeascapeShader(document.getElementById("seascape"))