Julia set

Julia set

탈출 시간으로 그리는 줄리아 프랙탈.

TypeScript로 쓰고 TypeShade가 컴파일한 Julia set 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — animated Julia set (escape-time fractal) ═══
//
// Iterate z ← z² + c per pixel until |z| escapes, then colour by the (smoothed)
// iteration count through a cosine palette. `c` orbits slowly so the fractal morphs.
// Showcases the DSL's `Loop` + early `Break` + a mutable `var` accumulator, typed
// ioStruct fn-params, and scalar×vector broadcast (the palette), all emitted to
// both WGSL (WebGPU) and GLSL ES 3.00 (WebGL2).
import {
fn,
module,
u32,
f32,
vec2,
vec3,
vec4,
sin,
cos,
dot,
mix,
Loop,
If,
Break,
Var,
Let,
f32T,
vec4fT,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ zoom: f32T, mouse: vec4fT });
// Iridescent cosine palette: 0.5 + 0.5·cos(2π(t + phase)).
const palette = fn('palette', { t: f32T }, ({ t }) => {
const ph = vec3(0.0, 0.33, 0.67);
// scalar t broadcasts over the phase vector; cos() is component-wise.
return vec3(0.5).add(cos(t.add(ph).mul(6.283)).mul(0.5));
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const uv = vo.uv;
// centre the plane, scale by the zoom uniform
const z = Var(vec2(uv.x.mul(2).sub(1), uv.y.mul(2).sub(1)).mul(U.field.zoom));
// the Julia constant: orbits on autopilot; once the pointer has entered
// (m.w = 1) it maps to the pointer instead — sweep the cursor to morph the
// set by hand. The pointer is normalised to c-space ≈ [−0.8, 0.8]².
const m = U.field.mouse;
const res = U.field.resolution;
const orbit = vec2(
cos(U.field.time.mul(0.31)).mul(0.39).sub(0.4),
sin(U.field.time.mul(0.41)).mul(0.39),
);
const held = vec2(m.x.div(res.x).mul(2).sub(1).mul(0.8), m.y.div(res.y).mul(2).sub(1).mul(0.8));
const c = Let(mix(orbit, held, m.w));
const it = Var(f32(0));
Loop(
u32(0),
(i) => i.lt(u32(96)),
() => {
If(dot(z, z).gt(4), () => {
Break();
}); // escaped
z.assign(vec2(z.x.mul(z.x).sub(z.y.mul(z.y)).add(c.x), z.x.mul(z.y).mul(2).add(c.y)));
it.assign(it.add(1));
},
);
// outside points (never escaped) → black core; escaped → palette by iteration
const col = palette({ t: it.div(96).add(U.field.time.mul(0.05)) });
return vec4(col.mul(it.div(96)), 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
// `palette` is called via its handle in `fs`, so module() collects it transitively — funcs lists only the entry points.
const juliaModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const julia: ShaderExample = {
id: 'julia',
title: 'Julia set',
blurb:
'Escape-time Julia fractal — z ← z² + c iterated per pixel, coloured by iteration count through a cosine palette. The constant c orbits on autopilot; move the pointer over the canvas to steer it by hand.',
category: 'generic',
file: 'julia.ts',
module: juliaModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
zoom: { kind: 'slider', label: 'Zoom', min: 0.4, max: 2.0, step: 0.05, value: 1.4 },
mouse: { kind: 'mouse' },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
zoom: 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> {
var _v0: vec2<f32> = (vec2<f32>(((vo.uv.x * 2.0) - 1.0), ((vo.uv.y * 2.0) - 1.0)) * U.zoom);
let _v1 = mix(vec2<f32>(((cos((U.time * 0.31)) * 0.39) - 0.4), (sin((U.time * 0.41)) * 0.39)), vec2<f32>(((((U.mouse.x / U.resolution.x) * 2.0) - 1.0) * 0.8), ((((U.mouse.y / U.resolution.y) * 2.0) - 1.0) * 0.8)), U.mouse.w);
var _v2: f32 = 0.0;
for (var _v3: u32 = 0u; (_v3 < 96u); _v3 = (_v3 + 1u)) {
if ((dot(_v0, _v0) > 4.0)) {
break;
}
_v0 = vec2<f32>((((_v0.x * _v0.x) - (_v0.y * _v0.y)) + _v1.x), (((_v0.x * _v0.y) * 2.0) + _v1.y));
_v2 = (_v2 + 1.0);
}
let _lc0 = (_v2 / 96.0);
return vec4<f32>((palette((_lc0 + (U.time * 0.05))) * _lc0), 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 zoom;
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() {
vec2 _v0 = (vec2(((uv.x * 2.0) - 1.0), ((uv.y * 2.0) - 1.0)) * U.zoom);
vec2 _v1 = mix(vec2(((cos((U.time * 0.31)) * 0.39) - 0.4), (sin((U.time * 0.41)) * 0.39)), vec2(((((U.mouse.x / U.resolution.x) * 2.0) - 1.0) * 0.8), ((((U.mouse.y / U.resolution.y) * 2.0) - 1.0) * 0.8)), U.mouse.w);
float _v2 = 0.0;
for (uint _v3 = 0u; (_v3 < 96u); _v3 = (_v3 + 1u)) {
if ((dot(_v0, _v0) > 4.0)) {
break;
}
_v0 = vec2((((_v0.x * _v0.x) - (_v0.y * _v0.y)) + _v1.x), (((_v0.x * _v0.y) * 2.0) + _v1.y));
_v2 = (_v2 + 1.0);
}
float _lc0 = (_v2 / 96.0);
_ret = vec4((palette((_lc0 + (U.time * 0.05))) * _lc0), 1.0);
}

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

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

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

GitHub의 파일

이 페이지 편집 문제 보고