@tinypoly/gpu-renderer 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +59 -3
- package/README.md +52 -4
- package/dist/atmosphere.d.ts +21 -0
- package/dist/atmosphere.d.ts.map +1 -0
- package/dist/atmosphere.js +72 -0
- package/dist/atmosphere.js.map +1 -0
- package/dist/denoise.d.ts +11 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +34 -6
- package/dist/denoise.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +15 -0
- package/dist/frameLayout.js.map +1 -1
- package/dist/geometry.d.ts.map +1 -1
- package/dist/geometry.js +6 -3
- package/dist/geometry.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +60 -0
- package/dist/lensPass.d.ts.map +1 -0
- package/dist/lensPass.js +235 -0
- package/dist/lensPass.js.map +1 -0
- package/dist/lightTree.d.ts +43 -0
- package/dist/lightTree.d.ts.map +1 -0
- package/dist/lightTree.js +107 -0
- package/dist/lightTree.js.map +1 -0
- package/dist/lights.d.ts +10 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +54 -1
- package/dist/lights.js.map +1 -1
- package/dist/materialLayout.d.ts +24 -0
- package/dist/materialLayout.d.ts.map +1 -1
- package/dist/materialLayout.js +36 -1
- package/dist/materialLayout.js.map +1 -1
- package/dist/pathTracePass.d.ts +6 -3
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +16 -10
- package/dist/pathTracePass.js.map +1 -1
- package/dist/renderer.d.ts +19 -7
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +118 -14
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +5 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +4 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +136 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +100 -0
- package/dist/settings.js.map +1 -0
- package/dist/subsurface.d.ts +76 -0
- package/dist/subsurface.d.ts.map +1 -0
- package/dist/subsurface.js +70 -0
- package/dist/subsurface.js.map +1 -0
- package/dist/volumeContainment.d.ts +17 -0
- package/dist/volumeContainment.d.ts.map +1 -0
- package/dist/volumeContainment.js +61 -0
- package/dist/volumeContainment.js.map +1 -0
- package/dist/wgsl/bsdf.wgsl.d.ts +1 -1
- package/dist/wgsl/bsdf.wgsl.d.ts.map +1 -1
- package/dist/wgsl/bsdf.wgsl.js +35 -3
- package/dist/wgsl/bsdf.wgsl.js.map +1 -1
- package/dist/wgsl/common.wgsl.d.ts.map +1 -1
- package/dist/wgsl/common.wgsl.js +4 -0
- package/dist/wgsl/common.wgsl.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +17 -0
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -0
- package/dist/wgsl/lens.wgsl.js +315 -0
- package/dist/wgsl/lens.wgsl.js.map +1 -0
- package/dist/wgsl/lights.wgsl.d.ts +1 -1
- package/dist/wgsl/lights.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lights.wgsl.js +203 -31
- package/dist/wgsl/lights.wgsl.js.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts +1 -1
- package/dist/wgsl/pathTrace.wgsl.d.ts.map +1 -1
- package/dist/wgsl/pathTrace.wgsl.js +396 -25
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts +1 -1
- package/dist/wgsl/sampling.wgsl.d.ts.map +1 -1
- package/dist/wgsl/sampling.wgsl.js +25 -0
- package/dist/wgsl/sampling.wgsl.js.map +1 -1
- package/dist/wgsl/surface.wgsl.d.ts +1 -1
- package/dist/wgsl/surface.wgsl.d.ts.map +1 -1
- package/dist/wgsl/surface.wgsl.js +21 -1
- package/dist/wgsl/surface.wgsl.js.map +1 -1
- package/package.json +1 -1
- package/src/atmosphere.ts +95 -0
- package/src/denoise.ts +44 -6
- package/src/frameLayout.ts +15 -0
- package/src/geometry.ts +6 -2
- package/src/index.ts +13 -0
- package/src/lensPass.ts +299 -0
- package/src/lightTree.ts +172 -0
- package/src/lights.ts +71 -1
- package/src/materialLayout.ts +52 -1
- package/src/pathTracePass.ts +19 -10
- package/src/renderer.ts +166 -22
- package/src/sceneUpload.ts +9 -0
- package/src/settings.ts +234 -0
- package/src/subsurface.ts +153 -0
- package/src/volumeContainment.ts +81 -0
- package/src/wgsl/bsdf.wgsl.ts +35 -3
- package/src/wgsl/common.wgsl.ts +4 -0
- package/src/wgsl/lens.wgsl.ts +314 -0
- package/src/wgsl/lights.wgsl.ts +203 -31
- package/src/wgsl/pathTrace.wgsl.ts +396 -25
- package/src/wgsl/sampling.wgsl.ts +25 -0
- package/src/wgsl/surface.wgsl.ts +21 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lens and the sensor do to the light before it is exposed: a halo around bright light (bloom), colour
|
|
3
|
+
* fringes towards the corners (lateral chromatic aberration) and the camera's white balance. Everything here is linear
|
|
4
|
+
* light, between the traced (or denoised) radiance and the presentation pass, which exposes and tone maps the result.
|
|
5
|
+
*
|
|
6
|
+
* The halo is built over a pyramid of half sized copies of the image, as Jimenez's "Next generation post processing in
|
|
7
|
+
* Call of Duty" (2014) builds it: each level is a 13 tap filter of the one above, and the levels are folded back from
|
|
8
|
+
* the smallest up with a tent filter, each mixed with the one below by the radius. The result is a wide, smooth spread
|
|
9
|
+
* whose weights sum to one, so a flat image is left as it was and the halo takes light from where it is rather than
|
|
10
|
+
* adding any: the image keeps its exact mean.
|
|
11
|
+
*
|
|
12
|
+
* The rays a bright light throws through the aperture are gathered the same way, from a quarter sized level: each
|
|
13
|
+
* texel takes the light of the texels behind it along every ray's direction, fading with the distance, with weights
|
|
14
|
+
* that again sum to one.
|
|
15
|
+
*/
|
|
16
|
+
export const LENS_WGSL = /* wgsl */ `
|
|
17
|
+
struct Lens {
|
|
18
|
+
resolution: vec2<u32>,
|
|
19
|
+
// Whether the colour comes from the denoiser's output rather than straight from the accumulation.
|
|
20
|
+
denoised: u32,
|
|
21
|
+
// Upsampling only: the level read from sits in the downsampled pyramid (the smallest one) instead of the folded one.
|
|
22
|
+
fromDown: u32,
|
|
23
|
+
sourceSize: vec2<u32>,
|
|
24
|
+
// Where the level read from starts in its pyramid; a size of the full resolution reads the image itself.
|
|
25
|
+
sourceOffset: u32,
|
|
26
|
+
// The first downsample reads the image itself, and averages its taps by luminance against fireflies.
|
|
27
|
+
fromImage: u32,
|
|
28
|
+
targetSize: vec2<u32>,
|
|
29
|
+
targetOffset: u32,
|
|
30
|
+
// Where the largest level of the folded pyramid starts, which the composite reads the halo from.
|
|
31
|
+
haloOffset: u32,
|
|
32
|
+
haloSize: vec2<u32>,
|
|
33
|
+
// How much of the light the halo takes, and how much each level takes from the one below it.
|
|
34
|
+
intensity: f32,
|
|
35
|
+
radius: f32,
|
|
36
|
+
// How far red and blue move apart at the corners, as a share of the distance from the centre.
|
|
37
|
+
aberration: f32,
|
|
38
|
+
// How much of the light the aperture's rays take, how many there are, and how far they reach in streak texels.
|
|
39
|
+
glare: f32,
|
|
40
|
+
rays: u32,
|
|
41
|
+
glareLength: f32,
|
|
42
|
+
// The white balance, a linear sRGB matrix, row by row.
|
|
43
|
+
white0: vec4<f32>,
|
|
44
|
+
white1: vec4<f32>,
|
|
45
|
+
white2: vec4<f32>,
|
|
46
|
+
// The size of the level the rays are gathered at, and the angle of the first ray, in radians.
|
|
47
|
+
streakSize: vec2<u32>,
|
|
48
|
+
glareAngle: f32,
|
|
49
|
+
pad0: f32,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const PI: f32 = 3.14159265358979;
|
|
53
|
+
|
|
54
|
+
/** Taps along each of the aperture's rays. */
|
|
55
|
+
const STREAK_TAPS: i32 = 24;
|
|
56
|
+
|
|
57
|
+
@group(0) @binding(0) var<uniform> lens: Lens;
|
|
58
|
+
@group(0) @binding(1) var<storage, read> accumulation: array<vec4<f32>>;
|
|
59
|
+
@group(0) @binding(2) var<storage, read> denoised: array<vec4<f32>>;
|
|
60
|
+
@group(0) @binding(3) var<storage, read_write> down: array<vec4<f32>>;
|
|
61
|
+
@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;
|
|
62
|
+
@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;
|
|
63
|
+
@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;
|
|
64
|
+
|
|
65
|
+
/** The image's own texel: its mean so far, or nothing where no path has landed yet. */
|
|
66
|
+
fn imageTexel(x: i32, y: i32) -> vec3<f32> {
|
|
67
|
+
let size = vec2<i32>(lens.resolution);
|
|
68
|
+
let index = u32(clamp(y, 0, size.y - 1) * size.x + clamp(x, 0, size.x - 1));
|
|
69
|
+
|
|
70
|
+
if (lens.denoised != 0u) {
|
|
71
|
+
return denoised[index].rgb;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let stored = accumulation[index];
|
|
75
|
+
|
|
76
|
+
return select(stored.rgb / stored.w, vec3<f32>(0.0), stored.w <= 0.0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn downTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {
|
|
80
|
+
let s = vec2<i32>(size);
|
|
81
|
+
|
|
82
|
+
return down[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn upTexel(offset: u32, size: vec2<u32>, x: i32, y: i32) -> vec3<f32> {
|
|
86
|
+
let s = vec2<i32>(size);
|
|
87
|
+
|
|
88
|
+
return up[offset + u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Bilinear weights and the texel below and left of a point given in texels, edges clamped. */
|
|
92
|
+
fn bilinearAt(position: vec2<f32>) -> vec4<f32> {
|
|
93
|
+
let p = position - 0.5;
|
|
94
|
+
let base = floor(p);
|
|
95
|
+
|
|
96
|
+
return vec4<f32>(base, p - base);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn imageBilinear(position: vec2<f32>) -> vec3<f32> {
|
|
100
|
+
let b = bilinearAt(position);
|
|
101
|
+
let x = i32(b.x);
|
|
102
|
+
let y = i32(b.y);
|
|
103
|
+
|
|
104
|
+
return mix(
|
|
105
|
+
mix(imageTexel(x, y), imageTexel(x + 1, y), b.z),
|
|
106
|
+
mix(imageTexel(x, y + 1), imageTexel(x + 1, y + 1), b.z), b.w);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn downBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {
|
|
110
|
+
let b = bilinearAt(position);
|
|
111
|
+
let x = i32(b.x);
|
|
112
|
+
let y = i32(b.y);
|
|
113
|
+
|
|
114
|
+
return mix(
|
|
115
|
+
mix(downTexel(offset, size, x, y), downTexel(offset, size, x + 1, y), b.z),
|
|
116
|
+
mix(downTexel(offset, size, x, y + 1), downTexel(offset, size, x + 1, y + 1), b.z), b.w);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn upBilinear(offset: u32, size: vec2<u32>, position: vec2<f32>) -> vec3<f32> {
|
|
120
|
+
let b = bilinearAt(position);
|
|
121
|
+
let x = i32(b.x);
|
|
122
|
+
let y = i32(b.y);
|
|
123
|
+
|
|
124
|
+
return mix(
|
|
125
|
+
mix(upTexel(offset, size, x, y), upTexel(offset, size, x + 1, y), b.z),
|
|
126
|
+
mix(upTexel(offset, size, x, y + 1), upTexel(offset, size, x + 1, y + 1), b.z), b.w);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The level being filtered, read bilinearly at a point given in its own texels. */
|
|
130
|
+
fn sourceAt(position: vec2<f32>) -> vec3<f32> {
|
|
131
|
+
if (lens.fromImage != 0u) {
|
|
132
|
+
return imageBilinear(position);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return downBilinear(lens.sourceOffset, lens.sourceSize, position);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn luminance(color: vec3<f32>) -> f32 {
|
|
139
|
+
return dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Karis' average: a group of taps weighed by one over one plus its luminance, so a lone firefly cannot flare. */
|
|
143
|
+
fn karis(sum: vec3<f32>) -> vec4<f32> {
|
|
144
|
+
let weight = 1.0 / (1.0 + luminance(sum));
|
|
145
|
+
|
|
146
|
+
return vec4<f32>(sum * weight, weight);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One level of the pyramid from the one above it: Jimenez's 13 taps, four overlapping boxes and a centre one. */
|
|
150
|
+
@compute @workgroup_size(8, 8, 1)
|
|
151
|
+
fn downsample(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
152
|
+
if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);
|
|
157
|
+
let centre = (vec2<f32>(id.xy) + 0.5) * scale;
|
|
158
|
+
let a = sourceAt(centre + vec2<f32>(-2.0, -2.0));
|
|
159
|
+
let b = sourceAt(centre + vec2<f32>(0.0, -2.0));
|
|
160
|
+
let c = sourceAt(centre + vec2<f32>(2.0, -2.0));
|
|
161
|
+
let d = sourceAt(centre + vec2<f32>(-2.0, 0.0));
|
|
162
|
+
let e = sourceAt(centre);
|
|
163
|
+
let f = sourceAt(centre + vec2<f32>(2.0, 0.0));
|
|
164
|
+
let g = sourceAt(centre + vec2<f32>(-2.0, 2.0));
|
|
165
|
+
let h = sourceAt(centre + vec2<f32>(0.0, 2.0));
|
|
166
|
+
let i = sourceAt(centre + vec2<f32>(2.0, 2.0));
|
|
167
|
+
let j = sourceAt(centre + vec2<f32>(-1.0, -1.0));
|
|
168
|
+
let k = sourceAt(centre + vec2<f32>(1.0, -1.0));
|
|
169
|
+
let l = sourceAt(centre + vec2<f32>(-1.0, 1.0));
|
|
170
|
+
let m = sourceAt(centre + vec2<f32>(1.0, 1.0));
|
|
171
|
+
var result: vec3<f32>;
|
|
172
|
+
|
|
173
|
+
if (lens.fromImage != 0u) {
|
|
174
|
+
let groups = karis((j + k + l + m) * 0.25) * 0.5
|
|
175
|
+
+ karis((a + b + d + e) * 0.25) * 0.125
|
|
176
|
+
+ karis((b + c + e + f) * 0.25) * 0.125
|
|
177
|
+
+ karis((d + e + g + h) * 0.25) * 0.125
|
|
178
|
+
+ karis((e + f + h + i) * 0.25) * 0.125;
|
|
179
|
+
|
|
180
|
+
result = groups.rgb / max(groups.w, 1e-6);
|
|
181
|
+
} else {
|
|
182
|
+
result = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
down[lens.targetOffset + id.y * lens.targetSize.x + id.x] = vec4<f32>(max(result, vec3<f32>(0.0)), 1.0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A 3 x 3 tent over the smaller level, read at a point given in its texels. */
|
|
189
|
+
fn tent(position: vec2<f32>) -> vec3<f32> {
|
|
190
|
+
var sum = vec3<f32>(0.0);
|
|
191
|
+
|
|
192
|
+
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
193
|
+
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
194
|
+
let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;
|
|
195
|
+
let at = position + vec2<f32>(f32(dx), f32(dy));
|
|
196
|
+
|
|
197
|
+
if (lens.fromDown != 0u) {
|
|
198
|
+
sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;
|
|
199
|
+
} else {
|
|
200
|
+
sum = sum + upBilinear(lens.sourceOffset, lens.sourceSize, at) * weight;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return sum;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One level folded back: its own light, mixed by the radius with the wider spread of every level below it. */
|
|
209
|
+
@compute @workgroup_size(8, 8, 1)
|
|
210
|
+
fn upsample(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
211
|
+
if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let scale = vec2<f32>(lens.sourceSize) / vec2<f32>(lens.targetSize);
|
|
216
|
+
let wider = tent((vec2<f32>(id.xy) + 0.5) * scale);
|
|
217
|
+
let index = lens.targetOffset + id.y * lens.targetSize.x + id.x;
|
|
218
|
+
|
|
219
|
+
up[index] = vec4<f32>(mix(down[index].rgb, wider, lens.radius), 1.0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The aperture's rays at one texel of the streak level: the light behind it along each ray, fading with distance. */
|
|
223
|
+
@compute @workgroup_size(8, 8, 1)
|
|
224
|
+
fn streaks(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
225
|
+
if (id.x >= lens.targetSize.x || id.y >= lens.targetSize.y) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let centre = vec2<f32>(id.xy) + 0.5;
|
|
230
|
+
var sum = vec3<f32>(0.0);
|
|
231
|
+
var total = 0.0;
|
|
232
|
+
|
|
233
|
+
for (var r = 0u; r < lens.rays; r = r + 1u) {
|
|
234
|
+
let angle = lens.glareAngle + f32(r) * 2.0 * PI / f32(lens.rays);
|
|
235
|
+
let along = vec2<f32>(cos(angle), sin(angle));
|
|
236
|
+
|
|
237
|
+
for (var j = 1; j <= STREAK_TAPS; j = j + 1) {
|
|
238
|
+
let t = f32(j) / f32(STREAK_TAPS);
|
|
239
|
+
let weight = exp(-4.0 * t);
|
|
240
|
+
|
|
241
|
+
sum = sum + downBilinear(lens.sourceOffset, lens.sourceSize, centre - along * (t * lens.glareLength)) * weight;
|
|
242
|
+
total = total + weight;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
streaked[id.y * lens.targetSize.x + id.x] = vec4<f32>(sum / max(total, 1e-6), 1.0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn streakTexel(x: i32, y: i32) -> vec3<f32> {
|
|
250
|
+
let s = vec2<i32>(lens.streakSize);
|
|
251
|
+
|
|
252
|
+
return streaked[u32(clamp(y, 0, s.y - 1) * s.x + clamp(x, 0, s.x - 1))].rgb;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn streakBilinear(position: vec2<f32>) -> vec3<f32> {
|
|
256
|
+
let b = bilinearAt(position);
|
|
257
|
+
let x = i32(b.x);
|
|
258
|
+
let y = i32(b.y);
|
|
259
|
+
|
|
260
|
+
return mix(mix(streakTexel(x, y), streakTexel(x + 1, y), b.z), mix(streakTexel(x, y + 1), streakTexel(x + 1, y + 1), b.z), b.w);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** The folded halo at full resolution: a tent over its largest level, which is half the image's size. */
|
|
264
|
+
fn tentHalo(position: vec2<f32>) -> vec3<f32> {
|
|
265
|
+
var sum = vec3<f32>(0.0);
|
|
266
|
+
|
|
267
|
+
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
|
268
|
+
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
|
269
|
+
let weight = f32((2 - abs(dx)) * (2 - abs(dy))) / 16.0;
|
|
270
|
+
|
|
271
|
+
sum = sum + upBilinear(lens.haloOffset, lens.haloSize, position + vec2<f32>(f32(dx), f32(dy)) * 0.5) * weight;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return sum;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The image as the lens delivers it: fringed towards the corners, with its halo, balanced for the camera's white. */
|
|
279
|
+
@compute @workgroup_size(8, 8, 1)
|
|
280
|
+
fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
281
|
+
if (id.x >= lens.resolution.x || id.y >= lens.resolution.y) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let size = vec2<f32>(lens.resolution);
|
|
286
|
+
let position = vec2<f32>(id.xy) + 0.5;
|
|
287
|
+
var color = imageTexel(i32(id.x), i32(id.y));
|
|
288
|
+
|
|
289
|
+
// Lateral chromatic aberration: each colour is magnified a little differently, red outwards and blue inwards,
|
|
290
|
+
// in proportion to the distance from the centre, which is how a lens's dispersion moves them.
|
|
291
|
+
if (lens.aberration > 0.0) {
|
|
292
|
+
let centre = size * 0.5;
|
|
293
|
+
let away = position - centre;
|
|
294
|
+
let red = imageBilinear(centre + away * (1.0 - lens.aberration));
|
|
295
|
+
let blue = imageBilinear(centre + away * (1.0 + lens.aberration));
|
|
296
|
+
|
|
297
|
+
color = vec3<f32>(red.r, color.g, blue.b);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (lens.intensity > 0.0) {
|
|
301
|
+
let halo = tentHalo(position * vec2<f32>(lens.haloSize) / size);
|
|
302
|
+
|
|
303
|
+
color = mix(color, halo, lens.intensity);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (lens.glare > 0.0) {
|
|
307
|
+
color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));
|
|
311
|
+
|
|
312
|
+
graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);
|
|
313
|
+
}
|
|
314
|
+
`;
|
|
315
|
+
//# sourceMappingURL=lens.wgsl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lens.wgsl.js","sourceRoot":"","sources":["../../src/wgsl/lens.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0SnC,CAAC"}
|
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* The falloff, the spot cone and the rectangle's projected area follow `material.js` in the CPU renderer, so direct
|
|
7
7
|
* light from an analytic light agrees with it.
|
|
8
8
|
*/
|
|
9
|
-
export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n // The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.\n if (extraFloat(frame.lightCdf + middle) > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\n/**\n * The area density with which direct sampling would have picked a point on this triangle, or zero if it never would.\n * Only the leading entries are emissive triangles, and they are in triangle order, so one binary search answers it.\n */\nfn lightPdfOfTriangle(triangle: u32) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = textures[frame.lightTriangles + middle];\n\n if (candidate == triangle) {\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n return sampleLightEntry(lightByProbability(pick), position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\nfn sampleLightEntry(light: Light, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n let drawn = sampleEnvironmentDirection(u);\n\n if (drawn.pdf <= 0.0) {\n return noLight();\n }\n\n let density = light.pdf * drawn.pdf;\n\n sample.direction = drawn.direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(drawn.direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / light.pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = light.pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / light.pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted.\n */\nfn lightTransmittance(\n position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32) -> vec3<f32> {\n if (!chosen.occlude) {\n return vec3<f32>(1.0);\n }\n\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the first\n // one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(1.0);\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through;\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n // A shadow catcher catches shadows rather than casting them, and a surface that adds light blocks none.\n if (m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through;\n }\n }\n\n return vec3<f32>(0.0);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
|
|
9
|
+
export declare const LIGHTS_WGSL = "\nstruct LightSample {\n // Unit vector from the shaded point towards the light.\n direction: vec3<f32>,\n // How far an occlusion ray may travel before it is past the light.\n distance: f32,\n // What the light delivers, already divided by the density it was sampled with.\n value: vec3<f32>,\n // Solid angle density of this direction, for the power heuristic. Zero when no bounce ray could find this light,\n // which is every light that occupies no geometry, and then the light strategy keeps the whole contribution.\n misPdf: f32,\n valid: bool,\n occlude: bool,\n kind: u32,\n};\n\nfn noLight() -> LightSample {\n var sample: LightSample;\n sample.valid = false;\n\n return sample;\n}\n\n/** The light whose cumulative probability range contains u. */\nfn lightByProbability(u: f32) -> Light {\n var low = 0u;\n var high = frame.lightCount - 1u;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n\n // The probabilities packed tight in the texture buffer: the same floats as lights[].cdf, a word apart.\n if (extraFloat(frame.lightCdf + middle) > u) {\n high = middle;\n } else {\n low = middle + 1u;\n }\n }\n\n return lights[low];\n}\n\nconst TREE_NODE_WORDS: u32 = 16u;\nconst TREE_LEAF: u32 = 0x80000000u;\nconst TREE_ROOT_PARENT: u32 = 0xffffffffu;\nconst NO_LIGHT: u32 = 0xffffffffu;\n\n/**\n * How much the lights under a node of the light tree can deliver to a point: their power over the square of the\n * distance, times how well their cone of directions and the receiving surface face each other, each widened by the\n * angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules\n * nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.\n */\nfn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n let at = frame.lightTree + node * TREE_NODE_WORDS;\n let low = extraVec4(at);\n let high = extraVec4(at + 4u);\n let cone = extraVec4(at + 8u);\n\n if (low.w <= 0.0) {\n return 0.0;\n }\n\n let centre = (low.xyz + high.xyz) * 0.5;\n let radius = length(high.xyz - low.xyz) * 0.5;\n let offset = position - centre;\n let distance2 = dot(offset, offset);\n\n if (distance2 <= radius * radius) {\n return low.w / max(radius * radius, 1e-12);\n }\n\n let distance = sqrt(distance2);\n let spread = asin(clamp(radius / distance, 0.0, 1.0));\n var orientation = 1.0;\n\n // A cone of minus one is the whole sphere: such lights shine towards every point alike.\n if (high.w > -1.0) {\n let theta = acos(clamp(dot(cone.xyz, offset / distance), -1.0, 1.0));\n let past = max(0.0, theta - acos(high.w) - spread);\n\n orientation = select(0.0, cos(past), past <= acos(clamp(cone.w, -1.0, 1.0)));\n }\n\n var facing = 1.0;\n\n if (any(normal != vec3<f32>(0.0))) {\n let past = max(0.0, acos(clamp(dot(normal, -offset / distance), -1.0, 1.0)) - spread);\n\n facing = select(0.0, cos(past), past < 0.5 * PI);\n }\n\n return low.w * orientation * facing / distance2;\n}\n\n/** The chance of stepping from a node of the tree to each of its children, from this point. */\nfn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {\n var importance = vec2<f32>(0.0);\n\n // One loop over the two children, so the importance is written into the kernel once per walk.\n for (var k = 0u; k < 2u; k = k + 1u) {\n importance[k] = lightNodeImportance(select(right, left, k == 0u), position, normal);\n }\n\n let total = importance.x + importance.y;\n\n return select(importance / total, vec2<f32>(0.0), total <= 0.0);\n}\n\n/** A light picked down the tree by importance from this point, with the chance of picking it; NO_LIGHT for none. */\nfn sampleLightTree(position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,\n density: ptr<function, f32>) -> u32 {\n var node = 0u;\n var chance = 1.0;\n var x = u;\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let at = frame.lightTree + node * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n\n if ((first & TREE_LEAF) != 0u) {\n *pdf = chance;\n *density = extraFloat(at + 13u);\n\n return first & ~TREE_LEAF;\n }\n\n let second = textures[at + 13u];\n let odds = lightChildOdds(first, second, position, normal);\n\n if (odds.x + odds.y <= 0.0) {\n return NO_LIGHT;\n }\n\n if (x < odds.x) {\n node = first;\n chance = chance * odds.x;\n x = x / odds.x;\n } else {\n node = second;\n chance = chance * odds.y;\n x = (x - odds.x) / max(odds.y, 1e-20);\n }\n\n x = min(x, 0.99999994);\n }\n\n return NO_LIGHT;\n}\n\n/** The chance the tree would have picked the local light at this index from this point, walked up from its leaf. */\nfn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var node = textures[frame.lightLeaves + index];\n var chance = extraFloat(frame.lightTree + node * TREE_NODE_WORDS + 13u);\n\n for (var depth = 0u; depth < 64u; depth = depth + 1u) {\n let parent = textures[frame.lightTree + node * TREE_NODE_WORDS + 14u];\n\n if (parent == TREE_ROOT_PARENT) {\n return chance;\n }\n\n let at = frame.lightTree + parent * TREE_NODE_WORDS;\n let first = textures[at + 12u];\n let odds = lightChildOdds(first, textures[at + 13u], position, normal);\n\n chance = chance * select(odds.y, odds.x, node == first);\n node = parent;\n }\n\n return 0.0;\n}\n\n/**\n * The area density with which direct sampling from this point would have picked a point on this triangle, or zero if\n * it never would. Only the leading entries are emissive triangles, and they are in triangle order, so one binary search\n * finds the entry; the tree, when there is one, says how likely it was from here.\n */\nfn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {\n var low = 0u;\n var high = frame.areaLightCount;\n\n loop {\n if (low >= high) {\n break;\n }\n\n let middle = (low + high) / 2u;\n let candidate = textures[frame.lightTriangles + middle];\n\n if (candidate == triangle) {\n if (frame.lightTree != 0u) {\n return frame.localLightPick * lightTreePdf(middle, position, normal);\n }\n\n return lights[middle].pdf;\n }\n\n if (candidate < triangle) {\n low = middle + 1u;\n } else {\n high = middle;\n }\n }\n\n return 0.0;\n}\n\n/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */\nfn misWeight(pdf: f32, other: f32) -> f32 {\n let a = pdf * pdf;\n let b = other * other;\n\n return select(a / (a + b), 0.0, a + b <= 0.0);\n}\n\n/**\n * Spreads a direction over a cone, which is how a light of some apparent size softens its own shadow. The settings give\n * the angular diameter; a rasterizer would blur the shadow map by the same amount.\n */\nfn spreadDirection(direction: vec3<f32>, u: vec2<f32>) -> vec3<f32> {\n if (frame.shadowSpread <= 0.0) {\n return direction;\n }\n\n let basis = orthonormalBasis(direction);\n let radius = sqrt(u.x) * frame.shadowSpread;\n let angle = 2.0 * PI * u.y;\n\n return normalize(direction + basis[0] * (radius * cos(angle)) + basis[1] * (radius * sin(angle)));\n}\n\n/** Three's getDistanceAttenuation: the inverse power law, cut off smoothly at the light's range. */\nfn distanceFalloff(light: Light, distance: f32) -> f32 {\n var falloff = 1.0 / max(pow(distance, light.decay), 0.01);\n\n if (light.cutoff > 0.0) {\n let edge = clamp(1.0 - pow(distance / light.cutoff, 4.0), 0.0, 1.0);\n\n falloff = falloff * edge * edge;\n }\n\n return falloff;\n}\n\n/** A point uniformly distributed over a rectangular light, and the axis it emits along. */\nfn rectanglePoint(light: Light, u: vec2<f32>) -> vec3<f32> {\n return light.position + light.tangent * (u.x * 2.0 - 1.0) + light.bitangent * (u.y * 2.0 - 1.0);\n}\n\n/**\n * Where the light that pick selects arrives from, and how much of it. u places the point on a surface light or spreads a\n * point light's shadow, so the same two dimensions serve every kind.\n */\nfn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>) -> LightSample {\n if (frame.lightCount == 0u) {\n return noLight();\n }\n\n // The lights in the scene go through the tree, by what they can deliver here; what lies outside it by its weight.\n if (frame.lightTree != 0u && pick < frame.localLightPick) {\n var chance = 0.0;\n var density = 1.0;\n let index = sampleLightTree(position, normal, pick / frame.localLightPick, &chance, &density);\n\n if (index == NO_LIGHT || chance <= 0.0) {\n return noLight();\n }\n\n return sampleLightEntry(lights[index], frame.localLightPick * chance * density, position, u);\n }\n\n let light = lightByProbability(pick);\n\n return sampleLightEntry(light, light.pdf, position, u);\n}\n\n/** Three's spotLightMap: inside the projection's frustum the texture tints the light; outside it leaves it alone. */\nfn spotLightTint(light: Light, position: vec3<f32>) -> vec3<f32> {\n if ((light.flags & LIGHT_HAS_MAP) == 0u) {\n return vec3<f32>(1.0);\n }\n\n let at = light.triangle;\n let map = textures[at];\n let matrix = mat4x4<f32>(extraVec4(at + 1u), extraVec4(at + 5u), extraVec4(at + 9u), extraVec4(at + 13u));\n let coordinate = matrix * vec4<f32>(position, 1.0);\n\n if (coordinate.w <= 0.0) {\n return vec3<f32>(1.0);\n }\n\n let projected = coordinate.xyz / coordinate.w;\n\n if (any(projected <= vec3<f32>(0.0)) || any(projected >= vec3<f32>(1.0))) {\n return vec3<f32>(1.0);\n }\n\n return sampleTextureRaw(map, projected.xy).rgb;\n}\n\n/** pdf is the chance of picking this light, times the area density of a point on it where it has an area. */\nfn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -> LightSample {\n var sample: LightSample;\n sample.valid = true;\n sample.kind = light.kind;\n // Emissive geometry is always tested: a panel that lit the room through its own wall would not be a path tracer.\n sample.occlude = light.kind == LIGHT_AREA\n || ((light.flags & LIGHT_CASTS_SHADOW) != 0u && frame.shadowsEnabled != 0u);\n sample.misPdf = 0.0;\n\n if (light.kind == LIGHT_ENVIRONMENT) {\n let drawn = sampleEnvironmentDirection(u);\n\n if (drawn.pdf <= 0.0) {\n return noLight();\n }\n\n let density = pdf * drawn.pdf;\n\n sample.direction = drawn.direction;\n sample.distance = 1e30;\n sample.value = environmentRadiance(drawn.direction) / density;\n // A bounce ray that escapes finds the same light, so the two strategies share it.\n sample.misPdf = density;\n\n return sample;\n }\n\n if (light.kind == LIGHT_DIRECTIONAL) {\n sample.direction = spreadDirection(light.direction, u);\n sample.distance = 1e30;\n // The colour already carries the intensity, which for a directional light is the irradiance it delivers.\n sample.value = light.color / pdf;\n\n return sample;\n }\n\n if (light.kind == LIGHT_AREA || light.kind == LIGHT_RECT) {\n let area = light.kind == LIGHT_AREA;\n let bary = triangleBarycentric(u);\n let point = select(rectanglePoint(light, u), trianglePointAt(light.triangle, bary), area);\n let axis = select(normalize(cross(light.tangent, light.bitangent)), triangleNormal(light.triangle), area);\n let offset = point - position;\n let distanceSquared = dot(offset, offset);\n let distance = sqrt(distanceSquared);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n sample.direction = offset / distance;\n // An emissive triangle radiates from both sides in this milestone; a rectangular light only from the side it faces.\n let cosine = dot(axis, sample.direction);\n let cosLight = select(max(cosine, 0.0), abs(cosine), area);\n\n if (cosLight <= 1e-6) {\n return noLight();\n }\n\n // Stop just short of the light so its own surface is not reported as the blocker.\n sample.distance = distance * (1.0 - 1e-3);\n let density = pdf * distanceSquared / cosLight;\n // The emissive map is read at the very point that was sampled, so this agrees with what a bounce ray would find.\n let emission = select(light.color, emissionAt(light.triangle, bary), area);\n\n sample.value = emission / density;\n // Only a triangle of the scene's geometry can also be found by a bounce ray, so only it needs the heuristic.\n sample.misPdf = select(0.0, density, area);\n\n return sample;\n }\n\n // A point or a spot light: both fall off with distance and are sampled at their centre.\n let offset = light.position - position;\n let distance = length(offset);\n\n if (distance <= RAY_EPSILON) {\n return noLight();\n }\n\n let towards = offset / distance;\n var power = distanceFalloff(light, distance);\n\n if (light.kind == LIGHT_SPOT) {\n let cosine = dot(light.direction, towards);\n\n if (light.innerCos <= light.outerCos) {\n power = power * select(0.0, 1.0, cosine >= light.outerCos);\n } else {\n let t = clamp((cosine - light.outerCos) / (light.innerCos - light.outerCos), 0.0, 1.0);\n\n power = power * t * t * (3.0 - 2.0 * t);\n }\n\n if (power <= 0.0) {\n return noLight();\n }\n }\n\n sample.direction = spreadDirection(towards, u);\n sample.distance = distance;\n sample.value = light.color * (power / pdf);\n\n if (light.kind == LIGHT_SPOT) {\n sample.value = sample.value * spotLightTint(light, position);\n }\n\n return sample;\n}\n\n/**\n * Whether a material stops every shadow ray that meets it, wherever: opaque, no cutout, no transmission, no clipping\n * or wireframe, no shader that might discard, and normal blending. Only then can the first triangle found decide.\n */\nfn blocksAll(m: u32) -> bool {\n let flags = materials[m].flags;\n let blending = materials[m].blending;\n\n return (flags & (MATERIAL_TRANSPARENT | MATERIAL_ALPHA_HASH | MATERIAL_WIREFRAME | MATERIAL_VOLUME)) == 0u\n && materials[m].alphaTest <= 0.0\n && materials[m].transmission <= 0.0\n && materials[m].clipCount == 0u\n && materials[m].program == NO_PROGRAM\n && materials[m].kind != KIND_SHADOW\n && (blending == BLEND_NORMAL || blending == BLEND_NONE);\n}\n\n/** How many surfaces a shadow ray may pass through before it gives up and calls the light blocked. */\nconst MAX_SHADOW_SKIPS: u32 = 8u;\n\n/** No material to skip: every blocker counts. */\nconst NO_MATERIAL: u32 = 0xffffffffu;\n\n/**\n * What a shadow ray keeps of a thin transmissive wall it meets: the share Three lets through, tinted by the base colour,\n * less what the interface reflects. It is the expectation of the straight crossing in sampleDielectric, so a light\n * seen through a window is found the same by both estimators. A volume bends the ray it lets through, so it blocks.\n */\nfn thinCrossing(triangle: u32, bary: vec2<f32>, direction: vec3<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n\n if (materials[m].transmission <= 0.0) {\n return vec3<f32>(0.0);\n }\n\n let metalness = materials[m].metalness * sampleMap(materials[m].metalnessMap, triangle, bary).b;\n let transmission = transmissionAt(m, triangle, bary, metalness);\n let thickness = materials[m].thickness * sampleMap(materials[m].thicknessMap, triangle, bary).g;\n\n if (transmission <= 0.0 || thickness > 0.0) {\n return vec3<f32>(0.0);\n }\n\n let albedo = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n let reflectance = fresnelDielectric(abs(dot(authoredNormal(triangle, bary), direction)), 1.0 / max(materials[m].ior, 1.0));\n\n return albedo * (transmission * (1.0 - reflectance));\n}\n\n/** The colour a blended surface filters what is behind it by: its base colour, with the map and vertex colours. */\nfn blendColor(triangle: u32, bary: vec2<f32>) -> vec3<f32> {\n let m = materialIndexOf(triangle);\n var color = materials[m].color.rgb * sampleMap(materials[m].map, triangle, bary).rgb;\n\n if ((materials[m].flags & MATERIAL_VERTEX_COLORS) != 0u) {\n color = color * triangleColor(triangle, bary).rgb;\n }\n\n return color;\n}\n\n/**\n * How much of the light this sample came from reaches the shaded point. A surface that only partly covers its triangle\n * blocks the ray only that often, which is what makes a cutout leaf cast a cutout shadow; a thin transmissive wall\n * lets its own share through, tinted. A volume's boundary lets the ray in or out, and the medium inside takes its\n * share over the distance between; medium is the density at the shaded point itself, zero outside any volume.\n */\nfn lightTransmittance(position: vec3<f32>, chosen: LightSample, rng: ptr<function, PathSampler>, skipMaterial: u32,\n medium: f32) -> vec3<f32> {\n // A light that casts no shadow is blocked by nothing, but a medium on the way still takes its share: then the walk\n // below only looks for volumes and passes every other surface.\n if (!chosen.occlude && !VOLUMES) {\n return vec3<f32>(1.0);\n }\n\n if (chosen.occlude) {\n // Most blockers are plainly opaque, and for those any triangle on the way settles it: the search stops at the\n // first one found instead of looking for the closest. Anything that lets light through goes the long way below.\n let first = traceAny(position, chosen.direction, chosen.distance);\n\n if (first == NO_HIT) {\n return vec3<f32>(exp(-medium * chosen.distance));\n }\n\n let firstMaterial = materialIndexOf(first);\n\n if (firstMaterial != skipMaterial && blocksAll(firstMaterial)) {\n return vec3<f32>(0.0);\n }\n }\n\n var origin = position;\n var remaining = chosen.distance;\n var through = vec3<f32>(1.0);\n // The medium the ray is in, and how far along the ray it entered it.\n var density = medium;\n var travelled = 0.0;\n var enteredAt = 0.0;\n\n for (var skip = 0u; skip < MAX_SHADOW_SKIPS; skip = skip + 1u) {\n let blocker = traceScene(origin, chosen.direction, remaining);\n\n if (blocker.triangle == NO_HIT) {\n return through * exp(-density * (travelled + remaining - enteredAt));\n }\n\n let bary = vec2<f32>(blocker.u, blocker.v);\n let m = materialIndexOf(blocker.triangle);\n let blending = materials[m].blending;\n\n if (VOLUMES && (materials[m].flags & MATERIAL_VOLUME) != 0u) {\n let at = travelled + blocker.distance;\n\n if (dot(authoredNormal(blocker.triangle, bary), chosen.direction) < 0.0) {\n density = select(0.0, materials[m].volume.w, frame.volumesEnabled != 0u);\n enteredAt = at;\n } else {\n through = through * exp(-density * (at - enteredAt));\n density = 0.0;\n }\n } else if (chosen.occlude && m != skipMaterial && materials[m].kind != KIND_SHADOW && blending != BLEND_ADDITIVE) {\n let coverage = coverageAt(blocker.triangle, bary, chosen.direction);\n\n if (blending == BLEND_MULTIPLY) {\n through = through * mix(vec3<f32>(1.0), blendColor(blocker.triangle, bary), coverage);\n } else if (blending == BLEND_SUBTRACTIVE) {\n through = through * (vec3<f32>(1.0) - blendColor(blocker.triangle, bary) * coverage);\n } else if (coverage >= 1.0 || sample2D(rng).x < coverage) {\n through = through * thinCrossing(blocker.triangle, bary, chosen.direction);\n }\n\n if (all(through <= vec3<f32>(0.0))) {\n return vec3<f32>(0.0);\n }\n }\n\n // The ray carries on from just past the surface it saw through.\n origin = origin + chosen.direction * (blocker.distance + RAY_EPSILON);\n remaining = remaining - blocker.distance - RAY_EPSILON;\n travelled = travelled + blocker.distance + RAY_EPSILON;\n\n if (remaining <= RAY_EPSILON) {\n return through * exp(-density * (travelled - enteredAt));\n }\n }\n\n // Too many surfaces to see through: a shadow ray gives up dark, a ray that only counts the media keeps what it has.\n return select(vec3<f32>(0.0), through, !chosen.occlude);\n}\n\n/**\n * How shadowed a point is, as Three's ShadowMaterial reads it: for every light that casts a shadow, whether something\n * stands between it and the point, the darkest of them winning as in the CPU renderer. A rectangular light casts no\n * shadow in Three, so it takes no part.\n */\nfn shadowMaskAt(position: vec3<f32>, rng: ptr<function, PathSampler>) -> f32 {\n if (frame.shadowsEnabled == 0u) {\n return 0.0;\n }\n\n var shadow = 0.0;\n\n for (var i = frame.areaLightCount; i < frame.lightCount; i = i + 1u) {\n let light = lights[i];\n\n if (light.kind == LIGHT_ENVIRONMENT || light.kind == LIGHT_RECT || (light.flags & LIGHT_CASTS_SHADOW) == 0u) {\n continue;\n }\n\n let chosen = sampleLightEntry(light, light.pdf, position, sample2D(rng));\n\n if (!chosen.valid) {\n continue;\n }\n\n let visible = luminanceOf(lightTransmittance(position, chosen, rng, NO_MATERIAL, 0.0));\n\n shadow = max(shadow, 1.0 - clamp(visible, 0.0, 1.0));\n }\n\n return shadow;\n}\n";
|
|
10
10
|
//# sourceMappingURL=lights.wgsl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"lights.wgsl.d.ts","sourceRoot":"","sources":["../../src/wgsl/lights.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,2upBAqlBvB,CAAC"}
|