Metaballs

Metaballs

음함수로 그린 방울.

TypeScript로 쓰고 TypeShade가 컴파일한 Metaballs 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — metaballs (implicit blob field) ═══
//
// The classic 2D metaballs: each ball contributes an inverse-square field
// w = k/d², the fields SUM, and the iso-contour of the summed field is the
// blobby silhouette that merges and splits as the balls orbit. Colour is the
// field-weighted blob index through a cosine palette, so blobs keep their hue
// while blending where they touch. Showcases a Loop accumulator over a
// uniform-driven ball count. WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import {
fn,
module,
u32,
f32,
toF32,
vec2,
vec3,
vec4,
sin,
cos,
dot,
max,
mix,
smoothstep,
Loop,
Var,
Let,
f32T,
vec4fT,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ count: f32T, mouse: vec4fT });
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 t = U.field.time;
const res = U.field.resolution;
const p = screenCoords(vo.uv, res);
// once the pointer has entered (m.w = 1), ball 0 leaves its orbit and
// follows the cursor — mapped to the same isotropic space as p
const m = U.field.mouse;
const cursor = Let(screenCoords(vec2(m.x.div(res.x), m.y.div(res.y)), res));
// sum the inverse-square fields; hue accumulates field-weighted so merged
// blobs blend their colours in proportion to who dominates the pixel
const field = Var(f32(0));
const hue = Var(f32(0));
Loop(
u32(0),
(i) => toF32(i).lt(U.field.count),
(i) => {
const fi = toF32(i);
const orbit = vec2(
sin(t.mul(fi.mul(0.13).add(0.5)).add(fi.mul(2.4))).mul(0.55),
cos(t.mul(fi.mul(0.11).add(0.4)).add(fi.mul(1.7))).mul(0.42),
);
// ball 0 → cursor when interacted: step(fi, 0.5) selects i == 0
const follow = f32(1)
.sub(smoothstep(0.4, 0.6, fi))
.mul(m.w);
const ctr = mix(orbit, cursor, follow);
const d = Let(p.sub(ctr));
const w = Let(f32(0.055).div(dot(d, d).add(0.003)));
field.assign(field.add(w));
hue.assign(hue.add(w.mul(fi)));
},
);
// iso-contour of the summed field = the blob silhouette
const iso = smoothstep(0.95, 1.15, field);
const blob = palette({ t: hue.div(max(field, 0.0001)).mul(0.15).add(t.mul(0.03)) });
// outside: dark bg lit by a faint field glow; inside: the full blob colour
const bg = vec3(0.03, 0.04, 0.08).add(blob.mul(field.mul(0.08)));
return vec4(mix(bg, blob, iso), 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const metaballsModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const metaballs: ShaderExample = {
id: 'metaballs',
title: 'Metaballs',
blurb:
'Implicit blobs — each ball adds an inverse-square field, the fields sum, and the iso-contour merges and splits as they orbit. Move the pointer and one ball follows it into the swarm. Ball count is live.',
category: 'generic',
file: 'metaballs.ts',
module: metaballsModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
count: { kind: 'slider', label: 'Balls', min: 2, max: 6, step: 1, value: 5 },
mouse: { kind: 'mouse' },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
count: 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 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 _licm0 = U.count;
let _licm1 = ((vo.uv.x * 2.0) - 1.0);
let _licm2 = ((vo.uv.y * 2.0) - 1.0);
let _licm3 = U.time;
let _licm4 = U.mouse.w;
let _cse0 = vec2<f32>((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y));
let _cse1 = (U.resolution.x / U.resolution.y);
let _v0 = vec2<f32>((((_cse0.x * 2.0) - 1.0) * _cse1), ((_cse0.y * 2.0) - 1.0));
var _v1: f32 = 0.0;
var _v2: f32 = 0.0;
for (var _v3: u32 = 0u; (f32(_v3) < _licm0); _v3 = (_v3 + 1u)) {
let _gv0 = f32(_v3);
let _v4 = (vec2<f32>((_licm1 * _cse1), _licm2) - mix(vec2<f32>((sin(((_licm3 * ((_gv0 * 0.13) + 0.5)) + (_gv0 * 2.4))) * 0.55), (cos(((_licm3 * ((_gv0 * 0.11) + 0.4)) + (_gv0 * 1.7))) * 0.42)), _v0, ((1.0 - smoothstep(0.4, 0.6, _gv0)) * _licm4)));
let _v5 = (0.055 / (dot(_v4, _v4) + 0.003));
_v1 = (_v1 + _v5);
_v2 = (_v2 + (_v5 * _gv0));
}
let _cse2 = (_licm3 * 0.03);
let _lc1 = palette((((_v2 / max(_v1, 0.0001)) * 0.15) + _cse2));
return vec4<f32>(mix((vec3<f32>(0.03, 0.04, 0.08) + (_lc1 * (_v1 * 0.08))), _lc1, smoothstep(0.95, 1.15, _v1)), 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 count;
vec4 mouse;
} U;
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() {
float _licm0 = U.count;
float _licm1 = ((uv.x * 2.0) - 1.0);
float _licm2 = ((uv.y * 2.0) - 1.0);
float _licm3 = U.time;
float _licm4 = U.mouse.w;
vec2 _cse0 = vec2((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y));
float _cse1 = (U.resolution.x / U.resolution.y);
vec2 _v0 = vec2((((_cse0.x * 2.0) - 1.0) * _cse1), ((_cse0.y * 2.0) - 1.0));
float _v1 = 0.0;
float _v2 = 0.0;
for (uint _v3 = 0u; (float(_v3) < _licm0); _v3 = (_v3 + 1u)) {
float _gv0 = float(_v3);
vec2 _v4 = (vec2((_licm1 * _cse1), _licm2) - mix(vec2((sin(((_licm3 * ((_gv0 * 0.13) + 0.5)) + (_gv0 * 2.4))) * 0.55), (cos(((_licm3 * ((_gv0 * 0.11) + 0.4)) + (_gv0 * 1.7))) * 0.42)), _v0, ((1.0 - smoothstep(0.4, 0.6, _gv0)) * _licm4)));
float _v5 = (0.055 / (dot(_v4, _v4) + 0.003));
_v1 = (_v1 + _v5);
_v2 = (_v2 + (_v5 * _gv0));
}
float _cse2 = (_licm3 * 0.03);
vec3 _lc1 = palette((((_v2 / max(_v1, 0.0001)) * 0.15) + _cse2));
_ret = vec4(mix((vec3(0.03, 0.04, 0.08) + (_lc1 * (_v1 * 0.08))), _lc1, smoothstep(0.95, 1.15, _v1)), 1.0);
}

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

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

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

GitHub의 파일

이 페이지 편집 문제 보고