Ocean horizon

Ocean horizon

원리부터 계산해 그린 바다 풍경.

TypeScript로 쓰고 TypeShade가 컴파일한 Ocean horizon 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — ocean horizon (fBm water + sun glitter) ═══
//
// A Seascape-style seascape from first principles: a perspective-divided water
// plane below the horizon sampled with octaves of value noise, a warm sky
// gradient with a sun disc above it, and a glitter path where wave crests
// align under the sun. Everything is one fragment — sky and sea are both
// evaluated and blended by a horizon step, no branches on uniform state.
// WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import {
fn,
module,
u32,
f32,
vec2,
vec3,
vec4,
floor,
fract,
mix,
max,
pow,
exp,
abs,
step,
distance,
smoothstep,
clamp,
Loop,
Let,
f32T,
vec2fT,
i32,
u32T,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ swell: f32T });
// An exact integer hash (lowbias32 with xxHash's primes): every target and the CPU oracle
// agree on every bit of it. `fract(sin(x) * 43758.5453)` did not, since WGSL bounds `sin`
// only to 2^-11 and the multiply puts that error above the fraction (#184).
const hash32 = fn('hash32', { x: u32T }, ({ x }) => {
const a = Let(x.bitXor(x.shr(16)).mul(0x85ebca77));
const b = Let(a.bitXor(a.shr(13)).mul(0xc2b2ae3d));
return b.bitXor(b.shr(16));
});
// scalar hash of a lattice point → [0,1): 24 bits, which f32 holds exactly
const hash = fn('hash', { p: vec2fT }, ({ p }) => {
const h = Let(hash32({ x: u32(i32(p.x)).bitXor(hash32({ x: u32(i32(p.y)) })) }));
return f32(h.shr(8)).mul(5.9604644775390625e-8); // 2^-24, exact: WGSL lets `/` round
});
// bilinear value noise with smootherstep weights
const noise = fn('noise', { p: vec2fT }, ({ p }) => {
const i = Let(floor(p));
const f = Let(fract(p));
const u = f.mul(f).mul(vec2(3).sub(f.mul(2)));
return mix(
mix(hash({ p: i }), hash({ p: i.add(vec2(1, 0)) }), u.x),
mix(hash({ p: i.add(vec2(0, 1)) }), hash({ p: i.add(vec2(1, 1)) }), u.x),
u.y,
);
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const t = U.field.time;
const res = U.field.resolution;
const asp = res.x.div(res.y);
const x = vo.uv.x.mul(2).sub(1).mul(asp);
const y = vo.uv.y;
const horizon = f32(0.58);
const sunX = f32(0.42);
// ── sky: warm haze at the horizon rising to a deep zenith, plus the sun
const sky = mix(vec3(0.83, 0.58, 0.38), vec3(0.12, 0.28, 0.48), smoothstep(horizon, 1, y));
// sun distance in ISOTROPIC coordinates — x spans 2·aspect over the width
// and y·2−1 spans 2 over the height, so units match and the disc stays round
const y2 = y.mul(2).sub(1);
const dSun = Let(distance(vec2(x, y2), vec2(sunX, 0.56)));
const sun = f32(1).sub(smoothstep(0.035, 0.06, dSun));
const halo = exp(dSun.mul(4).neg()).mul(0.35);
const skyCol = sky.add(vec3(1.0, 0.85, 0.6).mul(sun.add(halo)));
// ── sea: perspective-divide the rows below the horizon into a plane,
// then sum fBm octaves of value noise drifting with time
const dpt = Let(max(horizon.sub(y), 0.0008)); // 0 at the horizon
const wz = Let(f32(0.06).div(dpt)); // world distance along the plane
const sp = vec2(x.mul(wz).mul(0.6), wz.add(t.mul(0.6))).mul(3);
// fBm accumulator — 4 octaves, frequency ×2, amplitude ×½
const h = f32(0);
const amp = f32(0.5);
const freq = f32(1);
Loop(
u32(0),
(i) => i.lt(u32(4)),
() => {
h.assign(h.add(amp.mul(noise({ p: sp.mul(freq).add(vec2(t.mul(0.12), 0)) }))));
freq.assign(freq.mul(2.03));
amp.assign(amp.mul(0.5));
},
);
// fade the wave texture right at the horizon (sub-pixel noise reads as static)
const wave = h.mul(U.field.swell).mul(smoothstep(0, 0.05, dpt));
// deep near water lifting to the hazy horizon colour in the distance
const sea = mix(vec3(0.05, 0.18, 0.28), vec3(0.55, 0.5, 0.45), exp(dpt.mul(7).neg())).add(
vec3(0.3, 0.38, 0.36).mul(wave),
);
// sun-glitter path: bright crests, inside a column under the sun, fading out
const glint = pow(max(wave.sub(0.32), 0).mul(2.6), 3)
.mul(exp(abs(x.sub(sunX)).mul(2.2).neg()))
.mul(exp(dpt.mul(2.5).neg()));
const seaCol = sea.add(vec3(1.0, 0.85, 0.6).mul(clamp(glint, 0, 1.2)));
const col = mix(seaCol, skyCol, step(horizon, y));
return vec4(col, 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const oceanModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const ocean: ShaderExample = {
id: 'ocean',
title: 'Ocean horizon',
blurb:
'A seascape from first principles — the rows below the horizon perspective-divided into a water plane, waved by fBm value noise, with a sun disc and a glitter path where crests align beneath it. Swell height is live.',
category: 'generic',
file: 'ocean.ts',
module: oceanModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
swell: { kind: 'slider', label: 'Swell', min: 0, max: 1.5, step: 0.05, value: 0.8 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
swell: f32,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn hash32(x: u32) -> u32 {
let _v0 = ((x ^ (x >> 16u)) * 2246822519u);
let _v1 = ((_v0 ^ (_v0 >> 13u)) * 3266489917u);
return (_v1 ^ (_v1 >> 16u));
}
fn hash(p: vec2<f32>) -> f32 {
let _v0 = hash32((u32(i32(p.x)) ^ hash32(u32(i32(p.y)))));
return (f32((_v0 >> 8u)) * 5.960464477539063e-8);
}
fn noise(p: vec2<f32>) -> f32 {
let _v0 = floor(p);
let _v1 = fract(p);
let _cse0 = vec2<f32>(3.0);
let _gv0 = ((_v1 * _v1) * (_cse0 - (_v1 * 2.0)));
let _lc0 = _gv0.x;
return mix(mix(hash(_v0), hash((_v0 + vec2<f32>(1.0, 0.0))), _lc0), mix(hash((_v0 + vec2<f32>(0.0, 1.0))), hash((_v0 + vec2<f32>(1.0, 1.0))), _lc0), _gv0.y);
}
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0);
let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0);
return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));
}
@fragment
fn fs(vo: VsOut) -> @location(0) vec4<f32> {
let _licm0 = (U.time * 0.6);
let _licm1 = vec2<f32>((U.time * 0.12), 0.0);
let _cse0 = (((vo.uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y));
let _v0 = distance(vec2<f32>(_cse0, ((vo.uv.y * 2.0) - 1.0)), vec2<f32>(0.42, 0.56));
let _v1 = max((0.58 - vo.uv.y), 0.0008);
let _v2 = (0.06 / _v1);
var _av0: f32 = 0.0;
var _av1: f32 = 1.0;
var _av2: f32 = 0.5;
for (var _v3: u32 = 0u; (_v3 < 4u); _v3 = (_v3 + 1u)) {
_av0 = (_av0 + (_av2 * noise((((vec2<f32>(((_cse0 * _v2) * 0.6), (_v2 + _licm0)) * 3.0) * _av1) + _licm1))));
_av1 = (_av1 * 2.03);
_av2 = (_av2 * 0.5);
}
let _cse1 = vec3<f32>(1.0, 0.85, 0.6);
let _lc0 = ((_av0 * U.swell) * smoothstep(0.0, 0.05, _v1));
return vec4<f32>(mix(((mix(vec3<f32>(0.05, 0.18, 0.28), vec3<f32>(0.55, 0.5, 0.45), exp((-(_v1 * 7.0)))) + (vec3<f32>(0.3, 0.38, 0.36) * _lc0)) + (_cse1 * clamp(((pow((max((_lc0 - 0.32), 0.0) * 2.6), 3.0) * exp((-(abs((_cse0 - 0.42)) * 2.2)))) * exp((-(_v1 * 2.5)))), 0.0, 1.2))), (mix(vec3<f32>(0.83, 0.58, 0.38), vec3<f32>(0.12, 0.28, 0.48), smoothstep(0.58, 1.0, vo.uv.y)) + (_cse1 * ((1.0 - smoothstep(0.035, 0.06, _v0)) + (exp((-(_v0 * 4.0))) * 0.35)))), step(0.58, vo.uv.y)), 1.0);
}
#version 300 es
precision highp float;
precision highp int;
out vec2 uv;
void main() {
uint vi = uint(gl_VertexID);
float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0);
float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0);
gl_Position = vec4(_cse0, _cse1, 0.0, 1.0);
uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));
}
#version 300 es
precision highp float;
precision highp int;
layout(std140) uniform Uniforms {
float time;
vec2 resolution;
float swell;
} U;
int _f2i(float x) {
return int(mix(clamp(x, -2147483648.0, 2147483520.0), 0.0, isnan(x)));
}
uint hash32(uint x) {
uint _v0 = ((x ^ (x >> 16u)) * 2246822519u);
uint _v1 = ((_v0 ^ (_v0 >> 13u)) * 3266489917u);
return (_v1 ^ (_v1 >> 16u));
}
float hash(vec2 p) {
uint _v0 = hash32((uint(_f2i(p.x)) ^ hash32(uint(_f2i(p.y)))));
return (float((_v0 >> 8u)) * 5.960464477539063e-8);
}
float noise(vec2 p) {
vec2 _v0 = floor(p);
vec2 _v1 = fract(p);
vec2 _cse0 = vec2(3.0);
vec2 _gv0 = ((_v1 * _v1) * (_cse0 - (_v1 * 2.0)));
float _lc0 = _gv0.x;
return mix(mix(hash(_v0), hash((_v0 + vec2(1.0, 0.0))), _lc0), mix(hash((_v0 + vec2(0.0, 1.0))), hash((_v0 + vec2(1.0, 1.0))), _lc0), _gv0.y);
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _licm0 = (U.time * 0.6);
vec2 _licm1 = vec2((U.time * 0.12), 0.0);
float _cse0 = (((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y));
float _v0 = distance(vec2(_cse0, ((uv.y * 2.0) - 1.0)), vec2(0.42, 0.56));
float _v1 = max((0.58 - uv.y), 0.0008);
float _v2 = (0.06 / _v1);
float _av0 = 0.0;
float _av1 = 1.0;
float _av2 = 0.5;
for (uint _v3 = 0u; (_v3 < 4u); _v3 = (_v3 + 1u)) {
_av0 = (_av0 + (_av2 * noise((((vec2(((_cse0 * _v2) * 0.6), (_v2 + _licm0)) * 3.0) * _av1) + _licm1))));
_av1 = (_av1 * 2.03);
_av2 = (_av2 * 0.5);
}
vec3 _cse1 = vec3(1.0, 0.85, 0.6);
float _lc0 = ((_av0 * U.swell) * smoothstep(0.0, 0.05, _v1));
_ret = vec4(mix(((mix(vec3(0.05, 0.18, 0.28), vec3(0.55, 0.5, 0.45), exp((-(_v1 * 7.0)))) + (vec3(0.3, 0.38, 0.36) * _lc0)) + (_cse1 * clamp(((pow((max((_lc0 - 0.32), 0.0) * 2.6), 3.0) * exp((-(abs((_cse0 - 0.42)) * 2.2)))) * exp((-(_v1 * 2.5)))), 0.0, 1.2))), (mix(vec3(0.83, 0.58, 0.38), vec3(0.12, 0.28, 0.48), smoothstep(0.58, 1.0, uv.y)) + (_cse1 * ((1.0 - smoothstep(0.035, 0.06, _v0)) + (exp((-(_v0 * 4.0))) * 0.35)))), step(0.58, uv.y)), 1.0);
}

Ocean horizon. 빌드할 때 그린 화면입니다.

WGSL과 GLSL 탭은 커밋 c66579bf의 컴파일러가 직접 낸 출력입니다. 컴파일러의 출력 검사가 구워 둔 골든 파일에서 그대로 읽어 왔습니다(emit-goldens.test.ts).

이 예제는 fn() 빌더 API로 작성해서 Playground의 편집기가 받지 않습니다.

GitHub의 파일

이 페이지 편집 문제 보고