Raymarched box field

Raymarched box field

도메인 반복.

TypeScript로 쓰고 TypeShade가 컴파일한 Raymarched box field 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — raymarched box field (domain repetition) ═══
//
// One rounded-box SDF + floor-mod domain repetition = an infinite lattice of
// boxes, flown through forever. The step up from raymarch-sphere.ts: a reusable
// scene-SDF helper fn (called by the march AND six more times for the
// finite-difference normal), per-cell hashing for colour, and exponential fog.
// Domain repetition uses `mod` (#839) — the portable FLOOR-mod; WGSL `%` is
// trunc-mod and GLSL `%` is integer-only, so both would break on negatives.
// WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import {
fn,
module,
u32,
f32,
vec3,
vec4,
sin,
cos,
floor,
fract,
dot,
max,
mix,
abs,
exp,
mod,
normalize,
length,
Loop,
If,
Break,
Let,
f32T,
vec3fT,
vec4fT,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ speed: f32T, mouse: vec4fT });
// scene SDF: a rounded box repeated every 2.6 units in all three axes —
// mod's floor-mod keeps WGSL and GLSL agreeing on negative coordinates.
const scene = fn('scene', { p: vec3fT }, ({ p }) => {
const q = Let(mod(p, 2.6).sub(vec3(1.3)));
const b = abs(q).sub(vec3(0.62));
return length(max(b, vec3(0, 0, 0))).sub(0.14);
});
const palette = fn('palette', { t: f32T }, ({ t }) => {
const ph = vec3(0.0, 0.33, 0.67);
return vec3(0.5).add(cos(t.add(ph).mul(6.283)).mul(0.5));
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const time = U.field.time;
const res = U.field.resolution;
const ndc = screenCoords(vo.uv, res);
// camera: weaving gently down the corridor BETWEEN the boxes. Boxes are
// centred at 1.3 + 2.6k per axis and reach ±0.76, so the free corridor is
// ±0.54 around the axis — a ±0.4 wobble never enters a box.
const ro = Let(
vec3(
sin(time.mul(0.23)).mul(0.4),
cos(time.mul(0.2)).mul(0.4),
time.mul(U.field.speed).neg(),
),
);
const rd0 = Let(normalize(vec3(ndc.x, ndc.y, f32(1.6).neg())));
// pointer looks around: yaw (about y) from pointer x, pitch (about x) from
// pointer y. m.w = 0 (never touched) makes both angles exactly 0, so gates
// and thumbnails see the straight-down-the-corridor view.
const m = U.field.mouse;
const yaw = Let(m.x.div(res.x).sub(0.5).mul(1.6).mul(m.w));
const pit = Let(m.y.div(res.y).sub(0.5).mul(m.w));
const cy = Let(cos(yaw));
const sy = Let(sin(yaw));
const cp = Let(cos(pit));
const sp = Let(sin(pit));
// rotY(yaw) then rotX(pitch)
const rx = Let(rd0.x.mul(cy).add(rd0.z.mul(sy)));
const rz1 = Let(rd0.z.mul(cy).sub(rd0.x.mul(sy)));
const ry = Let(rd0.y.mul(cp).sub(rz1.mul(sp)));
const rz = Let(rz1.mul(cp).add(rd0.y.mul(sp)));
const rd = Let(vec3(rx, ry, rz));
const t = f32(0);
const hit = f32(0);
Loop(
u32(0),
(i) => i.lt(u32(90)),
() => {
const p = ro.add(rd.mul(t));
const d = Let(scene({ p }));
If(d.lt(0.002), () => {
hit.assign(1);
Break();
});
t.assign(t.add(d.mul(0.9)));
If(t.gt(30), () => {
Break();
});
},
);
const bg = Let(vec3(0.04, 0.05, 0.09).add(vec3(0.02, 0.04, 0.08).mul(vo.uv.y)));
const col = bg.add(vec3(0, 0, 0));
If(hit.gt(0.5), () => {
const p = Let(ro.add(rd.mul(t)));
// finite-difference normal — six more SDF taps through the helper
const e = 0.0015;
const n = Let(
normalize(
vec3(
scene({ p: p.add(vec3(e, 0, 0)) }).sub(scene({ p: p.sub(vec3(e, 0, 0)) })),
scene({ p: p.add(vec3(0, e, 0)) }).sub(scene({ p: p.sub(vec3(0, e, 0)) })),
scene({ p: p.add(vec3(0, 0, e)) }).sub(scene({ p: p.sub(vec3(0, 0, e)) })),
),
),
);
const ld = normalize(vec3(0.5, 0.8, 0.3));
const diff = max(dot(n, ld), 0);
// per-cell tint from the cell id
const id = floor(p.div(2.6));
const hcell = fract(
sin(id.x.mul(12.9898).add(id.y.mul(78.233)).add(id.z.mul(37.719))).mul(43758.5453),
);
const surf = palette({ t: hcell }).mul(diff.mul(0.8).add(0.16));
const fog = exp(t.mul(0.09).neg());
col.assign(mix(bg, surf, fog));
});
return vec4(col, 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const boxesModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const raymarchBoxes: ShaderExample = {
id: 'raymarch-boxes',
title: 'Raymarched box field',
blurb:
'Domain repetition — one rounded-box SDF floor-modded into an infinite lattice, flown through forever. Move the pointer to look around the corridor; a reusable scene() helper serves the march and the six-tap normal. Fly speed is live.',
category: 'generic',
file: 'raymarch-boxes.ts',
module: boxesModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
speed: { kind: 'slider', label: 'Fly speed', min: 0, max: 3, step: 0.1, value: 1.4 },
mouse: { kind: 'mouse' },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
speed: f32,
mouse: vec4<f32>,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn scene(p: vec3<f32>) -> f32 {
let _v0 = ((p - 2.6 * floor(p / 2.6)) - vec3<f32>(1.3));
return (length(max((abs(_v0) - vec3<f32>(0.62)), vec3<f32>(0.0, 0.0, 0.0))) - 0.14);
}
fn palette(t: f32) -> vec3<f32> {
return (vec3<f32>(0.5) + (cos(((t + vec3<f32>(0.0, 0.33, 0.67)) * 6.283)) * 0.5));
}
@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 _v0 = vec3<f32>((sin((U.time * 0.23)) * 0.4), (cos((U.time * 0.2)) * 0.4), (-(U.time * U.speed)));
let _cse0 = vec2<f32>((((vo.uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((vo.uv.y * 2.0) - 1.0));
let _v1 = normalize(vec3<f32>(_cse0.x, _cse0.y, -1.6));
let _v2 = ((((U.mouse.x / U.resolution.x) - 0.5) * 1.6) * U.mouse.w);
let _v3 = (((U.mouse.y / U.resolution.y) - 0.5) * U.mouse.w);
let _v4 = cos(_v2);
let _v5 = sin(_v2);
let _v6 = cos(_v3);
let _v7 = sin(_v3);
let _v8 = ((_v1.x * _v4) + (_v1.z * _v5));
let _v9 = ((_v1.z * _v4) - (_v1.x * _v5));
let _v10 = ((_v1.y * _v6) - (_v9 * _v7));
let _v11 = ((_v9 * _v6) + (_v1.y * _v7));
let _v12 = vec3<f32>(_v8, _v10, _v11);
var _av0: f32 = 0.0;
var _av1: f32 = 0.0;
for (var _v13: u32 = 0u; (_v13 < 90u); _v13 = (_v13 + 1u)) {
let _v14 = scene((_v0 + (_v12 * _av1)));
if ((_v14 < 0.002)) {
_av0 = 1.0;
break;
}
_av1 = (_av1 + (_v14 * 0.9));
if ((_av1 > 30.0)) {
break;
}
}
let _v15 = (vec3<f32>(0.04, 0.05, 0.09) + (vec3<f32>(0.02, 0.04, 0.08) * vo.uv.y));
var _av2: vec3<f32> = (_v15 + vec3<f32>(0.0, 0.0, 0.0));
if ((_av0 > 0.5)) {
let _v16 = (_v0 + (_v12 * _av1));
let _cse1 = vec3<f32>(0.0015, 0.0, 0.0);
let _cse2 = vec3<f32>(0.0, 0.0015, 0.0);
let _cse3 = vec3<f32>(0.0, 0.0, 0.0015);
let _v17 = normalize(vec3<f32>((scene((_v16 + _cse1)) - scene((_v16 - _cse1))), (scene((_v16 + _cse2)) - scene((_v16 - _cse2))), (scene((_v16 + _cse3)) - scene((_v16 - _cse3)))));
let _lc0 = floor((_v16 / 2.6));
_av2 = mix(_v15, (palette(fract((sin((((_lc0.x * 12.9898) + (_lc0.y * 78.233)) + (_lc0.z * 37.719))) * 43758.5453))) * ((max(dot(_v17, normalize(vec3<f32>(0.5, 0.8, 0.3))), 0.0) * 0.8) + 0.16)), exp((-(_av1 * 0.09))));
}
return vec4<f32>(_av2, 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 speed;
vec4 mouse;
} U;
float scene(vec3 p) {
vec3 _v0 = (mod(p, 2.6) - vec3(1.3));
return (length(max((abs(_v0) - vec3(0.62)), vec3(0.0, 0.0, 0.0))) - 0.14);
}
vec3 palette(float t) {
return (vec3(0.5) + (cos(((t + vec3(0.0, 0.33, 0.67)) * 6.283)) * 0.5));
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
vec3 _v0 = vec3((sin((U.time * 0.23)) * 0.4), (cos((U.time * 0.2)) * 0.4), (-(U.time * U.speed)));
vec2 _cse0 = vec2((((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((uv.y * 2.0) - 1.0));
vec3 _v1 = normalize(vec3(_cse0.x, _cse0.y, -1.6));
float _v2 = ((((U.mouse.x / U.resolution.x) - 0.5) * 1.6) * U.mouse.w);
float _v3 = (((U.mouse.y / U.resolution.y) - 0.5) * U.mouse.w);
float _v4 = cos(_v2);
float _v5 = sin(_v2);
float _v6 = cos(_v3);
float _v7 = sin(_v3);
float _v8 = ((_v1.x * _v4) + (_v1.z * _v5));
float _v9 = ((_v1.z * _v4) - (_v1.x * _v5));
float _v10 = ((_v1.y * _v6) - (_v9 * _v7));
float _v11 = ((_v9 * _v6) + (_v1.y * _v7));
vec3 _v12 = vec3(_v8, _v10, _v11);
float _av0 = 0.0;
float _av1 = 0.0;
for (uint _v13 = 0u; (_v13 < 90u); _v13 = (_v13 + 1u)) {
float _v14 = scene((_v0 + (_v12 * _av1)));
if ((_v14 < 0.002)) {
_av0 = 1.0;
break;
}
_av1 = (_av1 + (_v14 * 0.9));
if ((_av1 > 30.0)) {
break;
}
}
vec3 _v15 = (vec3(0.04, 0.05, 0.09) + (vec3(0.02, 0.04, 0.08) * uv.y));
vec3 _av2 = (_v15 + vec3(0.0, 0.0, 0.0));
if ((_av0 > 0.5)) {
vec3 _v16 = (_v0 + (_v12 * _av1));
vec3 _cse1 = vec3(0.0015, 0.0, 0.0);
vec3 _cse2 = vec3(0.0, 0.0015, 0.0);
vec3 _cse3 = vec3(0.0, 0.0, 0.0015);
vec3 _v17 = normalize(vec3((scene((_v16 + _cse1)) - scene((_v16 - _cse1))), (scene((_v16 + _cse2)) - scene((_v16 - _cse2))), (scene((_v16 + _cse3)) - scene((_v16 - _cse3)))));
vec3 _lc0 = floor((_v16 / 2.6));
_av2 = mix(_v15, (palette(fract((sin((((_lc0.x * 12.9898) + (_lc0.y * 78.233)) + (_lc0.z * 37.719))) * 43758.5453))) * ((max(dot(_v17, normalize(vec3(0.5, 0.8, 0.3))), 0.0) * 0.8) + 0.16)), exp((-(_av1 * 0.09))));
}
_ret = vec4(_av2, 1.0);
}

Raymarched box field. 빌드할 때 그린 화면입니다.

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

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

GitHub의 파일

이 페이지 편집 문제 보고