Beating heart

Beating heart

하트 곡선 (x²+y²-1)³ = x²y³입니다. 부호로 안쪽을 채우고 fwidth로 가장자리를 다듬었으며, 뾰족하게 만든 사인파 박동에 맞춰 뛰고 박자에 맞춰 빛납니다.

TypeScript로 쓰고 TypeShade가 컴파일한 Beating heart 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — beating heart (implicit curve) ═══
//
// The sextic heart curve (x² + y² − 1)³ − x²y³ = 0, filled where the implicit
// function goes negative, anti-aliased with `fwidth` on the field itself, and
// scaled by a sharpened-sine heartbeat. A different shading recipe from the
// SDF examples: implicit-function sign fill + field-driven inner gradient +
// exponential outer glow synced to the beat. WGSL (WebGPU) + GLSL ES 3.00.
import {
fn,
module,
f32,
vec3,
vec4,
sin,
abs,
pow,
max,
mix,
exp,
clamp,
smoothstep,
fwidth,
Let,
f32T,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ beat: f32T });
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const t = U.field.time;
const res = U.field.resolution;
const p = screenCoords(vo.uv, res);
// heartbeat: |sin|⁸ sharpens the sine into a thump per half-period
const beat = Let(pow(abs(sin(t.mul(3.14159).mul(U.field.beat))), 8).mul(0.12));
const s = Let(f32(0.72).add(beat));
const qx = Let(p.x.mul(1.3).div(s));
const qy = Let(p.y.add(0.08).mul(1.3).div(s));
// the sextic heart: (x² + y² − 1)³ − x²·y³
const xx = Let(qx.mul(qx));
const k = Let(xx.add(qy.mul(qy)).sub(1));
const f = Let(k.mul(k).mul(k).sub(xx.mul(qy).mul(qy).mul(qy)));
// fill where f < 0, anti-aliased on the field's own screen gradient
const aa = Let(fwidth(f).mul(1.6));
const fill = f32(1).sub(smoothstep(aa.neg(), aa, f));
// inside: brighter toward the centre (f is most negative there)
const heart = mix(vec3(0.55, 0.03, 0.1), vec3(0.95, 0.15, 0.25), clamp(f.neg(), 0, 1));
// outside: a soft glow that swells with the beat
const glow = exp(max(f, 0).mul(2.2).neg()).mul(beat.mul(3).add(0.35));
const bg = vec3(0.05, 0.04, 0.07).mul(f32(1).sub(vo.uv.y.mul(0.35)));
const col = mix(bg.add(vec3(0.7, 0.08, 0.16).mul(glow)), heart, fill);
return vec4(col, 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const heartModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const heart: ShaderExample = {
id: 'heart',
title: 'Beating heart',
blurb:
'The sextic heart curve (x²+y²−1)³ = x²y³, sign-filled and fwidth-anti-aliased, thumping to a sharpened-sine heartbeat with a beat-synced glow. Beat rate is live.',
category: 'generic',
file: 'heart.ts',
module: heartModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
beat: { kind: 'slider', label: 'Beat rate', min: 0.2, max: 2, step: 0.1, value: 1 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
beat: f32,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
@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 = (pow(abs(sin(((U.time * 3.14159) * U.beat))), 8.0) * 0.12);
let _v1 = (0.72 + _v0);
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 _v2 = ((_cse0.x * 1.3) / _v1);
let _v3 = (((_cse0.y + 0.08) * 1.3) / _v1);
let _v4 = (_v2 * _v2);
let _v5 = ((_v4 + (_v3 * _v3)) - 1.0);
let _v6 = (((_v5 * _v5) * _v5) - (((_v4 * _v3) * _v3) * _v3));
let _v7 = (fwidth(_v6) * 1.6);
return vec4<f32>(mix(((vec3<f32>(0.05, 0.04, 0.07) * (1.0 - (vo.uv.y * 0.35))) + (vec3<f32>(0.7, 0.08, 0.16) * (exp((-(max(_v6, 0.0) * 2.2))) * ((_v0 * 3.0) + 0.35)))), mix(vec3<f32>(0.55, 0.03, 0.1), vec3<f32>(0.95, 0.15, 0.25), clamp((-_v6), 0.0, 1.0)), (1.0 - smoothstep((-_v7), _v7, _v6))), 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 beat;
} U;
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _v0 = (pow(abs(sin(((U.time * 3.14159) * U.beat))), 8.0) * 0.12);
float _v1 = (0.72 + _v0);
vec2 _cse0 = vec2((((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((uv.y * 2.0) - 1.0));
float _v2 = ((_cse0.x * 1.3) / _v1);
float _v3 = (((_cse0.y + 0.08) * 1.3) / _v1);
float _v4 = (_v2 * _v2);
float _v5 = ((_v4 + (_v3 * _v3)) - 1.0);
float _v6 = (((_v5 * _v5) * _v5) - (((_v4 * _v3) * _v3) * _v3));
float _v7 = (fwidth(_v6) * 1.6);
_ret = vec4(mix(((vec3(0.05, 0.04, 0.07) * (1.0 - (uv.y * 0.35))) + (vec3(0.7, 0.08, 0.16) * (exp((-(max(_v6, 0.0) * 2.2))) * ((_v0 * 3.0) + 0.35)))), mix(vec3(0.55, 0.03, 0.1), vec3(0.95, 0.15, 0.25), clamp((-_v6), 0.0, 1.0)), (1.0 - smoothstep((-_v7), _v7, _v6))), 1.0);
}

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

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

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

GitHub의 파일

이 페이지 편집 문제 보고