Skip to content

Commit b580ab9

Browse files
committed
hilbert rooms: Analytic Direct Lighting
1 parent dae5a9e commit b580ab9

File tree

2 files changed

+238
-119
lines changed

2 files changed

+238
-119
lines changed

shaders/hilbert.wgsl

Lines changed: 220 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,263 @@
11
// MIT License, Enes Altun, 2025
22
// resources for Skilling's algorithm
3-
// https://github.com/joshspeagle/dynesty
3+
// https://github.com/joshspeagle/dynesty
44
// https://doi.org/10.1063/1.1751381 and https://doi.org/10.1063/1.1751382
55
// http://www.inference.org.uk/bayesys/test/hilbert.c
66
// https://www.shadertoy.com/view/3tl3zl
7+
// lighting technique from @fad, (2023): Analytic Direct Lighting: https://www.shadertoy.com/view/dlcXR4
78
struct TimeUniform {
89
time: f32,
910
};
10-
1111
struct ResolutionUniform {
1212
dimensions: vec2<f32>,
1313
_padding: vec2<f32>,
1414
};
15-
1615
struct Params {
1716
iterations: u32,
18-
num_rays: u32,
17+
num_rays: f32,
1918
_pad1: vec2<f32>,
2019
scale: f32,
2120
time_scale: f32,
2221
vignette_radius: f32,
2322
vignette_softness: f32,
2423
color_offset: vec3<f32>,
25-
_pad2: f32,
24+
flanc: f32,
2625
};
27-
2826
@group(0) @binding(0) var<uniform> u_time: TimeUniform;
2927
@group(1) @binding(0) var<uniform> u_resolution: ResolutionUniform;
3028
@group(2) @binding(0) var<uniform> params: Params;
3129

3230
const PI: f32 = 3.14159265359;
33-
const FLT_MAX: f32 = 33333.0;
34-
//adapted from: https://www.shadertoy.com/view/3tl3zl tpfto, 2019.
35-
fn hilbert(k: u32, s: u32) -> vec2<f32> {
36-
let bb = 1u << s;
37-
var b = bb;
38-
let t = vec2<u32>(k ^ (k >> 1));
39-
var hp = vec2<u32>(0u);
40-
41-
for(var j: i32 = i32(s) - 1; j >= 0; j--) {
42-
b = b >> 1u;
43-
hp += (t >> vec2<u32>(u32(j + 1), u32(j))) & vec2<u32>(b);
31+
const MAX_SEGMENTS: u32 = 64u;
32+
const MAX_ANGLES: u32 = 128u;
33+
fn atan2(y: f32, x: f32) -> f32 {
34+
if (x > 0.0) {
35+
return atan(y / x);
36+
} else if (x < 0.0) {
37+
return atan(y / x) + select(-PI, PI, y >= 0.0);
38+
} else {
39+
return select(-PI / 2.0, PI / 2.0, y >= 0.0);
4440
}
45-
for(var p = 2u; p < bb; p = p << 1u) {
46-
let q = p - 1u;
47-
if((hp.y & p) != 0u) {
48-
hp.x = hp.x ^ q;
49-
} else {
50-
let temp = (hp.x ^ hp.y) & q;
51-
hp.x = hp.x ^ temp;
52-
hp.y = hp.y ^ temp;
53-
}
54-
if((hp.x & p) != 0u) {
55-
hp.x = hp.x ^ q;
41+
}
42+
fn fmod(x: f32, y: f32) -> f32 {
43+
return x - y * floor(x / y);
44+
}
45+
fn hilbert(idx: u32) -> vec2<u32> {
46+
var res = vec2<u32>(0u, 0u);
47+
var i = idx;
48+
for (var k = 0u; k < params.iterations; k++) {
49+
let r = vec2<u32>((i >> 1u) & 1u, (i ^ (i >> 1u)) & 1u);
50+
if (r.y == 0u) {
51+
if (r.x == 1u) {
52+
res = ((1u << k) - 1u) - res;
53+
}
54+
let temp = res.x;
55+
res.x = res.y;
56+
res.y = temp;
5657
}
58+
res += vec2<u32>(r.x << k, r.y << k);
59+
i >>= 2u;
5760
}
58-
59-
return 2.0 * (vec2<f32>(hp) / f32(bb - 1u)) - 1.0;
61+
return res;
6062
}
61-
fn intersect_line(ro: vec2<f32>, rd: vec2<f32>, line: vec4<f32>, t: ptr<function, f32>) -> bool {
62-
let A = ro;
63-
let B = ro + rd;
64-
let C = line.xy;
65-
let D = line.zw;
63+
// Generate Hilbert curve point for a given index: chp: calculate hilbert point
64+
fn CHP(i: u32) -> vec2<f32> {
65+
let hpos = hilbert(i);
66+
let size = 1u << params.iterations;
67+
let np = vec2<f32>(hpos) / f32(size);
68+
let scale = 0.7 * min(u_resolution.dimensions.x, u_resolution.dimensions.y);
69+
let screen_pos = np * scale + (u_resolution.dimensions - vec2<f32>(scale)) * 0.5;
6670

67-
let AmC = A - C;
68-
let DmC = D - C;
69-
let BmA = B - A;
70-
71-
let denom = (BmA.x * DmC.y) - (BmA.y * DmC.x);
72-
73-
if(abs(denom) > 0.0001) {
74-
let r = ((AmC.y * DmC.x) - (AmC.x * DmC.y)) / denom;
75-
let s = ((AmC.y * BmA.x) - (AmC.x * BmA.y)) / denom;
76-
77-
if((r > 0.0 && r < *t) && (s > 0.0 && s < 1.0)) {
78-
*t = r;
79-
return true;
71+
return screen_pos;
72+
}
73+
fn get_point(i: u32) -> vec2<f32> {
74+
let num_points = 1u << (2u * params.iterations);
75+
if (i >= num_points) {
76+
return vec2<f32>(0.0);
77+
}
78+
return CHP(i);
79+
}
80+
struct LineSegment {
81+
p0: vec2<f32>,
82+
p1: vec2<f32>,
83+
emissive_color: vec3<f32>,
84+
};
85+
fn sdf(l: LineSegment, p: vec2<f32>) -> f32 {
86+
let pa = p - l.p0;
87+
let ba = l.p1 - l.p0;
88+
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
89+
return length(pa - ba * h);
90+
}
91+
fn blend_over(top: vec4<f32>, bottom: vec4<f32>) -> vec4<f32> {
92+
let a = top.a + bottom.a * (1.0 - top.a);
93+
if (a < 0.0001) {
94+
return vec4<f32>(0.0);
95+
}
96+
return vec4<f32>((top.rgb * top.a + bottom.rgb * bottom.a * (1.0 - top.a)) / a, a);
97+
}
98+
fn draw_sdf(dst: vec4<f32>, src: vec4<f32>, dist: f32) -> vec4<f32> {
99+
return blend_over(
100+
vec4<f32>(src.rgb, src.a * clamp(1.5 - abs(dist), 0.0, 1.0)),
101+
dst
102+
);
103+
}
104+
// Matrix inverse 2x2
105+
fn inverse_2x2(m: mat2x2<f32>) -> mat2x2<f32> {
106+
let det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
107+
if (abs(det) < 0.0001) {
108+
return mat2x2<f32>(1.0, 0.0, 0.0, 1.0);
109+
}
110+
let inv_det = 1.0 / det;
111+
return mat2x2<f32>(
112+
m[1][1] * inv_det, -m[0][1] * inv_det,
113+
-m[1][0] * inv_det, m[0][0] * inv_det
114+
);
115+
}
116+
117+
// insertion sort
118+
fn sort_angles(segments: ptr<function, array<LineSegment, MAX_SEGMENTS>>, angles: ptr<function, array<f32, MAX_ANGLES>>, num_segments: u32) {
119+
let num_angles = 2u * num_segments;
120+
for (var i = 0u; i < num_segments; i++) {
121+
for (var j = 0u; j < 2u; j++) {
122+
let k = 2u * i + j;
123+
let p = select((*segments)[i].p0, (*segments)[i].p1, j == 1u);
124+
let angle = fmod(atan2(p.y, p.x), 2.0 * PI);
125+
var l = i32(k) - 1;
126+
while (l >= 0 && angle < (*angles)[u32(l)]) {
127+
(*angles)[u32(l) + 1u] = (*angles)[u32(l)];
128+
l -= 1;
129+
}
130+
(*angles)[u32(l) + 1u] = angle;
80131
}
81132
}
82-
return false;
133+
}
134+
// add radiance from a line segment
135+
fn integrate_radiance(segment: LineSegment, angle: vec2<f32>) -> vec3<f32> {
136+
return (angle[1] - angle[0]) * segment.emissive_color;
83137
}
84138

85-
fn intersect_scene(ro: vec2<f32>, rd: vec2<f32>, t: ptr<function, f32>, colour: ptr<function, vec3<f32>>) -> bool {
86-
var intersect = false;
87-
var minDist = *t;
139+
// add sky radiance for a basic angle range
140+
fn isb(angle: vec2<f32>) -> vec3<f32> {
141+
let sky_color = vec3<f32>(0.01, 0.02, 0.04);
142+
return sky_color * (angle[1] - angle[0]);
143+
}
88144

89-
let s = params.iterations;
90-
let NUM = (1u << (2u * s)) - 1u;
91-
92-
var a = hilbert(0u, s);
93-
var b: vec2<f32>;
94-
95-
let scale = params.scale;
96-
let offset = vec2<f32>(0.0);
97-
let scaletime = u_time.time * params.time_scale;
98-
for(var i = 0u; i < NUM; i++) {
99-
b = hilbert(i + 1u, s);
100-
let line = vec4<f32>((a * scale) + offset, (b * scale) + offset);
101-
let lineDir = normalize(line.zw - line.xy);
102-
let normal = vec2<f32>(-lineDir.y, lineDir.x);
103-
if(intersect_line(ro, rd, line, &minDist)) {
104-
let hue = f32(i) / f32(NUM) + scaletime;
105-
*colour = 0.5 + 0.5 * cos(3.28318 * (hue + params.color_offset));
106-
let lighting = abs(dot(-rd, normal));
107-
*colour = *colour * lighting;
108-
intersect = true;
109-
}
110-
111-
a = b;
145+
// Integrate sky radiance with wrap-around support
146+
fn isr(angle: vec2<f32>) -> vec3<f32> {
147+
if (angle[1] < 2.0 * PI) {
148+
return isb(angle);
112149
}
113-
114-
*t = minDist;
115-
return intersect;
150+
return isb(vec2<f32>(angle[0], 2.0 * PI)) +
151+
isb(vec2<f32>(0.0, angle[1] - 2.0 * PI));
116152
}
117153

154+
// Find which segment is intersected by a ray at a specific angle
155+
fn find_index(segments: ptr<function, array<LineSegment, MAX_SEGMENTS>>, angle: f32, num_segments: u32) -> i32 {
156+
var m = mat2x2<f32>(0.0, 0.0, 0.0, 0.0);
157+
m[1] = vec2<f32>(cos(angle), sin(angle));
158+
var best_index = -1;
159+
var best_u = 1e10;
160+
for (var i = 0u; i < num_segments; i++) {
161+
m[0] = (*segments)[i].p0 - (*segments)[i].p1;
162+
// Calculate the inverse and multiply manually in case of singular matrices
163+
let inv_m = inverse_2x2(m);
164+
let tu = inv_m * (*segments)[i].p0;
165+
// Check if valid intersection (0 <= t <= 1 and 0 <= u <= best_u)
166+
if (tu.x >= 0.0 && tu.x <= 1.0 && tu.y >= 0.0 && tu.y <= best_u) {
167+
best_u = tu.y;
168+
best_index = i32(i);
169+
}
170+
}
171+
return best_index;
172+
}
173+
// total radiance from all directions
174+
fn calculate_fluence(segments: ptr<function, array<LineSegment, MAX_SEGMENTS>>, angles: ptr<function, array<f32, MAX_ANGLES>>, num_segments: u32) -> vec3<f32> {
175+
var fluence = vec3<f32>(0.0);
176+
let num_angles = 2u * num_segments;
177+
for (var i = 0u; i < num_angles; i++) {
178+
var a = vec2<f32>((*angles)[i], 0.0);
179+
if (i + 1u < num_angles) {
180+
a[1] = (*angles)[i + 1u];
181+
} else {
182+
a[1] = (*angles)[0] + 2.0 * PI;
183+
}
184+
if (abs(a[0] - a[1]) < 0.0001) {
185+
continue;
186+
}
187+
let mid_angle = (a[0] + a[1]) / 2.0;
188+
let j = find_index(segments, mid_angle, num_segments);
189+
if (j == -1) {
190+
fluence += isr(a);
191+
} else {
192+
fluence += integrate_radiance((*segments)[u32(j)], a);
193+
}
194+
}
195+
return fluence;
196+
}
118197
@fragment
119-
fn fs_main(@builtin(position) FragCoord: vec4<f32>) -> @location(0) vec4<f32> {
120-
let ro = -1.0 + 2.0 * (vec2<f32>(FragCoord.x, u_resolution.dimensions.y - FragCoord.y) / u_resolution.dimensions);
121-
122-
var total = vec3<f32>(0.0);
123-
124-
for(var i = 0u; i < params.num_rays; i++) {
125-
let angle = PI * (f32(i) / f32(params.num_rays));
126-
let rd = vec2<f32>(cos(angle), sin(angle));
127-
128-
var t = FLT_MAX;
129-
var colour = vec3<f32>(1.0);
130-
131-
if(intersect_scene(ro, rd, &t, &colour)) {
132-
total += colour;
198+
fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
199+
let num_points = 1u << (2u * params.iterations);
200+
let num_segments = min(num_points - 1u, MAX_SEGMENTS);
201+
var segments: array<LineSegment, MAX_SEGMENTS>;
202+
var angles: array<f32, MAX_ANGLES>;
203+
for (var i = 0u; i < MAX_ANGLES; i++) {
204+
angles[i] = 0.0;
205+
}
206+
let pixel_pos = vec2<f32>(frag_coord.x, u_resolution.dimensions.y - frag_coord.y);
207+
for (var i = 0u; i < num_segments; i++) {
208+
segments[i].p0 = get_point(i) - pixel_pos;
209+
segments[i].p1 = get_point(i + 1u) - pixel_pos;
210+
segments[i].emissive_color = vec3<f32>(0.0);
211+
}
212+
let t = fmod(u_time.time * params.time_scale * 0.1, 1.0);
213+
let exact_pos = t * f32(num_segments);
214+
let light_length: f32 = params.num_rays;
215+
for (var i = 0u; i < num_segments; i++) {
216+
// Calculate distance along the curve from this segment to the light center
217+
let fi = f32(i);
218+
let dcp = min(
219+
abs(fi - exact_pos),
220+
min(
221+
abs(fi + f32(num_segments) - exact_pos),
222+
abs(fi - f32(num_segments) - exact_pos)
223+
)
224+
);
225+
// Only light up segments within the light's range,
226+
// dcp: distance to curve position
227+
if (dcp < light_length) {
228+
let intensity = exp(-dcp * dcp / (light_length * 0.5));
229+
let color_pos = dcp / light_length;
230+
let base_color1 = vec3<f32>(1.0, 0.3, 0.05) + params.color_offset;
231+
let base_color2 = vec3<f32>(0.1, 0.7, 1.0) + params.color_offset;
232+
let light_color = mix(
233+
base_color1,
234+
base_color2,
235+
smoothstep(0.0, 1.0, color_pos)
236+
);
237+
segments[i].emissive_color = light_color * intensity * 2.0;
133238
}
134239
}
135-
136-
total = total / f32(params.num_rays);
137-
138-
let dist = length(ro);
139-
let radius = params.vignette_radius;
140-
let softness = params.vignette_softness;
141-
let vignette = smoothstep(radius, radius + softness, dist);
142-
total *= 1.0 - vignette * 0.95;
143-
144-
let exposure = 1.5;
145-
return vec4<f32>(pow(total * exposure, vec3<f32>(1.0/1.1)), 1.0);
240+
sort_angles(&segments, &angles, num_segments);
241+
let fluence = calculate_fluence(&segments, &angles, num_segments);
242+
var frag_color = vec4<f32>(1.0 - params.flanc / pow(1.0 + fluence, vec3<f32>(params.vignette_softness)), 1.0);
243+
for (var i = 0u; i < num_segments; i++) {
244+
let base_curve_color = vec3<f32>(0.15);
245+
let has_emission = length(segments[i].emissive_color) > 0.01;
246+
let segment_color = select(
247+
base_curve_color,
248+
2.0 * segments[i].emissive_color,
249+
has_emission
250+
);
251+
let thickness = select(1.0, params.scale, has_emission);
252+
frag_color = draw_sdf(
253+
frag_color,
254+
vec4<f32>(segment_color, 1.0),
255+
sdf(segments[i], vec2<f32>(0.0)) / thickness
256+
);
257+
}
258+
frag_color = vec4<f32>(gamma(frag_color.rgb, params.vignette_radius), frag_color.a);
259+
return frag_color;
146260
}
261+
fn gamma(color: vec3<f32>, gamma: f32) -> vec3<f32> {
262+
return pow(max(color, vec3<f32>(0.0)), vec3<f32>(1.0 / gamma));
263+
}

0 commit comments

Comments
 (0)