Starfield

Starfield

텍스처 없이 그린 밤하늘.

TypeScript로 쓰고 TypeShade가 컴파일한 Starfield 셰이더. 빌드할 때 렌더링함.
// ═══ typeshade example — starfield (hash grid, parallax layers) ═══
//
// The classic star field without a single texture: three grid layers, each
// cell hashing whether it holds a star, where inside the cell it sits, how it
// twinkles — nearer layers drift faster for parallax. A faint diagonal band
// stands in for the Milky Way. Showcases procedural hashing as a data source
// (the same lattice-hash the noise examples build on, used directly).
// WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import {
fn,
module,
u32,
f32,
toF32,
vec2,
vec3,
vec4,
sin,
floor,
fract,
mix,
exp,
step,
distance,
smoothstep,
Loop,
Var,
Let,
f32T,
vec2fT,
i32,
u32T,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ density: 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
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const t = U.field.time;
const res = U.field.resolution;
const p = screenCoords(vo.uv, res);
const col = Var(vec3(0, 0, 0));
// three parallax layers — nearer (coarser) layers drift faster
Loop(
u32(0),
(i) => i.lt(u32(3)),
(i) => {
const fi = toF32(i);
const scale = fi.mul(14).add(18);
const drift = fi.mul(0.014).add(0.01);
const q = vec2(p.x.add(t.mul(drift)), p.y)
.mul(scale)
.add(fi.mul(37.7));
const cell = Let(floor(q));
const f = fract(q);
const h = Let(hash({ p: cell }));
// does this cell hold a star? density raises the hash gate
const gate = step(f32(0.92).sub(U.field.density.mul(0.25)), h);
// star position inside the cell (kept off the cell edges)
const sp = vec2(
hash({ p: cell.add(vec2(12.3, 45.6)) }),
hash({ p: cell.add(vec2(78.9, 1.2)) }),
)
.mul(0.7)
.add(0.15);
const d = distance(f, sp);
const rad = f32(0.06).sub(fi.mul(0.012)); // far layers are smaller
const core = Let(f32(1).sub(smoothstep(0, rad, d)));
const twinkle = sin(t.mul(h.mul(4).add(2)).add(h.mul(40)))
.mul(0.4)
.add(0.6);
const b = core
.mul(core)
.mul(twinkle)
.mul(gate)
.mul(f32(1).sub(fi.mul(0.25)));
// colour temperature from the hash: blue-white ↔ warm
const tint = mix(vec3(0.75, 0.85, 1.0), vec3(1.0, 0.9, 0.75), h);
col.assign(col.add(tint.mul(b)));
},
);
// a faint diagonal Milky-Way band
const s = Let(p.y.add(p.x.mul(0.35)));
const band = exp(s.mul(s).mul(6).neg());
return vec4(col.add(vec3(0.09, 0.11, 0.16).mul(band)), 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
const starfieldModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const starfield: ShaderExample = {
id: 'starfield',
title: 'Starfield',
blurb:
'A textureless night sky — three hash-grid layers decide per cell whether a star exists, where it sits, and how it twinkles; nearer layers drift faster for parallax. Density is live.',
category: 'generic',
file: 'starfield.ts',
module: starfieldModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
density: { kind: 'slider', label: 'Density', min: 0, max: 1, step: 0.05, value: 0.5 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
density: 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);
}
@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;
let _licm1 = vec2<f32>(12.3, 45.6);
let _licm2 = vec2<f32>(78.9, 1.2);
let _licm3 = vec3<f32>(0.75, 0.85, 1.0);
let _licm4 = vec3<f32>(1.0, 0.9, 0.75);
let _licm5 = (0.92 - (U.density * 0.25));
var _v0: vec3<f32> = vec3<f32>(0.0, 0.0, 0.0);
let _cse2 = vec2<f32>((((vo.uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((vo.uv.y * 2.0) - 1.0));
let _cse0 = _cse2.x;
let _cse1 = _cse2.y;
for (var _v1: u32 = 0u; (_v1 < 3u); _v1 = (_v1 + 1u)) {
let _gv0 = f32(_v1);
let _gv1 = ((vec2<f32>((_cse0 + (_licm0 * ((_gv0 * 0.014) + 0.01))), _cse1) * ((_gv0 * 14.0) + 18.0)) + (_gv0 * 37.7));
let _v2 = floor(_gv1);
let _v3 = hash(_v2);
let _v4 = (1.0 - smoothstep(0.0, (0.06 - (_gv0 * 0.012)), distance(fract(_gv1), ((vec2<f32>(hash((_v2 + _licm1)), hash((_v2 + _licm2))) * 0.7) + 0.15))));
_v0 = (_v0 + (mix(_licm3, _licm4, _v3) * ((((_v4 * _v4) * ((sin(((_licm0 * ((_v3 * 4.0) + 2.0)) + (_v3 * 40.0))) * 0.4) + 0.6)) * step(_licm5, _v3)) * (1.0 - (_gv0 * 0.25)))));
}
let _v5 = (_cse1 + (_cse0 * 0.35));
return vec4<f32>((_v0 + (vec3<f32>(0.09, 0.11, 0.16) * exp((-((_v5 * _v5) * 6.0))))), 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 density;
} 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);
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _licm0 = U.time;
vec2 _licm1 = vec2(12.3, 45.6);
vec2 _licm2 = vec2(78.9, 1.2);
vec3 _licm3 = vec3(0.75, 0.85, 1.0);
vec3 _licm4 = vec3(1.0, 0.9, 0.75);
float _licm5 = (0.92 - (U.density * 0.25));
vec3 _v0 = vec3(0.0, 0.0, 0.0);
vec2 _cse2 = vec2((((uv.x * 2.0) - 1.0) * (U.resolution.x / U.resolution.y)), ((uv.y * 2.0) - 1.0));
float _cse0 = _cse2.x;
float _cse1 = _cse2.y;
for (uint _v1 = 0u; (_v1 < 3u); _v1 = (_v1 + 1u)) {
float _gv0 = float(_v1);
vec2 _gv1 = ((vec2((_cse0 + (_licm0 * ((_gv0 * 0.014) + 0.01))), _cse1) * ((_gv0 * 14.0) + 18.0)) + (_gv0 * 37.7));
vec2 _v2 = floor(_gv1);
float _v3 = hash(_v2);
float _v4 = (1.0 - smoothstep(0.0, (0.06 - (_gv0 * 0.012)), distance(fract(_gv1), ((vec2(hash((_v2 + _licm1)), hash((_v2 + _licm2))) * 0.7) + 0.15))));
_v0 = (_v0 + (mix(_licm3, _licm4, _v3) * ((((_v4 * _v4) * ((sin(((_licm0 * ((_v3 * 4.0) + 2.0)) + (_v3 * 40.0))) * 0.4) + 0.6)) * step(_licm5, _v3)) * (1.0 - (_gv0 * 0.25)))));
}
float _v5 = (_cse1 + (_cse0 * 0.35));
_ret = vec4((_v0 + (vec3(0.09, 0.11, 0.16) * exp((-((_v5 * _v5) * 6.0))))), 1.0);
}

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

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

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

GitHub의 파일

이 페이지 편집 문제 보고