@tinypoly/gpu-renderer 0.1.1 → 0.2.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 +31 -3
- package/README.md +11 -3
- package/dist/denoise.d.ts +10 -1
- package/dist/denoise.d.ts.map +1 -1
- package/dist/denoise.js +4 -0
- package/dist/denoise.js.map +1 -1
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +8 -0
- package/dist/device.js.map +1 -1
- package/dist/frameLayout.d.ts.map +1 -1
- package/dist/frameLayout.js +4 -1
- package/dist/frameLayout.js.map +1 -1
- package/dist/imageResolve.d.ts +2 -1
- package/dist/imageResolve.d.ts.map +1 -1
- package/dist/imageResolve.js +1 -0
- package/dist/imageResolve.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lensPass.d.ts +4 -0
- package/dist/lensPass.d.ts.map +1 -1
- package/dist/lensPass.js +14 -1
- package/dist/lensPass.js.map +1 -1
- package/dist/lights.d.ts +18 -0
- package/dist/lights.d.ts.map +1 -1
- package/dist/lights.js +45 -0
- package/dist/lights.js.map +1 -1
- package/dist/oidnPass.d.ts +48 -0
- package/dist/oidnPass.d.ts.map +1 -0
- package/dist/oidnPass.js +255 -0
- package/dist/oidnPass.js.map +1 -0
- package/dist/pathTracePass.d.ts +2 -2
- package/dist/pathTracePass.d.ts.map +1 -1
- package/dist/pathTracePass.js +9 -9
- package/dist/pathTracePass.js.map +1 -1
- package/dist/presentPass.d.ts +2 -0
- package/dist/presentPass.d.ts.map +1 -1
- package/dist/presentPass.js +1 -0
- package/dist/presentPass.js.map +1 -1
- package/dist/renderer.d.ts +12 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +100 -14
- package/dist/renderer.js.map +1 -1
- package/dist/sceneUpload.d.ts +4 -0
- package/dist/sceneUpload.d.ts.map +1 -1
- package/dist/sceneUpload.js +3 -0
- package/dist/sceneUpload.js.map +1 -1
- package/dist/settings.d.ts +23 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +13 -4
- package/dist/settings.js.map +1 -1
- package/dist/wgsl/lens.wgsl.d.ts +1 -1
- package/dist/wgsl/lens.wgsl.d.ts.map +1 -1
- package/dist/wgsl/lens.wgsl.js +108 -1
- package/dist/wgsl/lens.wgsl.js.map +1 -1
- 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 +147 -17
- 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 +123 -91
- package/dist/wgsl/pathTrace.wgsl.js.map +1 -1
- package/dist/wgsl/present.wgsl.d.ts +1 -1
- package/dist/wgsl/present.wgsl.d.ts.map +1 -1
- package/dist/wgsl/present.wgsl.js +11 -1
- package/dist/wgsl/present.wgsl.js.map +1 -1
- package/package.json +4 -2
- package/src/denoise.ts +15 -1
- package/src/device.ts +9 -0
- package/src/frameLayout.ts +4 -1
- package/src/imageResolve.ts +2 -0
- package/src/index.ts +2 -1
- package/src/lensPass.ts +17 -1
- package/src/lights.ts +60 -0
- package/src/oidnPass.ts +284 -0
- package/src/oidnWebgpu.d.ts +5 -0
- package/src/pathTracePass.ts +11 -9
- package/src/presentPass.ts +3 -0
- package/src/renderer.ts +118 -15
- package/src/sceneUpload.ts +7 -0
- package/src/settings.ts +32 -4
- package/src/wgsl/lens.wgsl.ts +108 -1
- package/src/wgsl/lights.wgsl.ts +147 -17
- package/src/wgsl/pathTrace.wgsl.ts +123 -91
- package/src/wgsl/present.wgsl.ts +11 -1
- package/weights/LICENSE.txt +202 -0
- package/weights/rt_hdr_alb_nrm.tza +0 -0
package/src/sceneUpload.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface SceneBuffers {
|
|
|
53
53
|
lightTree: number;
|
|
54
54
|
lightLeaves: number;
|
|
55
55
|
lightList: number;
|
|
56
|
+
/** The windows the sky is sampled through: see lights.ts. */
|
|
57
|
+
portals: number;
|
|
58
|
+
portalCount: number;
|
|
59
|
+
portalList: number;
|
|
56
60
|
/** Radiance an ambient or hemisphere light adds, which no shadow ray can reach. */
|
|
57
61
|
ambient: PackedLights["ambient"];
|
|
58
62
|
/** Where the surrounding image and its distributions ended up, for the frame uniform. */
|
|
@@ -208,6 +212,9 @@ export function uploadScene(
|
|
|
208
212
|
lightTree: lights.treeAt,
|
|
209
213
|
lightLeaves: lights.leavesAt,
|
|
210
214
|
lightList: lights.listAt,
|
|
215
|
+
portals: lights.portalTreeAt,
|
|
216
|
+
portalCount: lights.portalCount,
|
|
217
|
+
portalList: lights.portalListAt,
|
|
211
218
|
ambient: lights.ambient,
|
|
212
219
|
environment: {
|
|
213
220
|
texture: environment ? textures.indexOf.get(environment.texture.id) ?? NO_TEXTURE : NO_TEXTURE,
|
package/src/settings.ts
CHANGED
|
@@ -20,6 +20,12 @@ export interface GpuRenderSettings extends Omit<RenderSettings, "globalIlluminat
|
|
|
20
20
|
* gets the samples a large panel far away would have taken.
|
|
21
21
|
*/
|
|
22
22
|
lightTree: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Inside a room, aims at the sky through its windows (every thin transmissive material) as well as by its
|
|
25
|
+
* brightness, as Cycles' portals do, so daylight that enters through glass is found instead of stumbled upon.
|
|
26
|
+
* A point counts as inside when something is above it; elsewhere the sky is sampled as before.
|
|
27
|
+
*/
|
|
28
|
+
portals: boolean;
|
|
23
29
|
};
|
|
24
30
|
/**
|
|
25
31
|
* The light a real lens and sensor scatter around anything bright: a soft halo that fades with distance. Energy
|
|
@@ -60,11 +66,24 @@ export interface GpuRenderSettings extends Omit<RenderSettings, "globalIlluminat
|
|
|
60
66
|
};
|
|
61
67
|
/** A camera's exposure triangle and white balance. */
|
|
62
68
|
camera: {
|
|
69
|
+
/**
|
|
70
|
+
* The contrast and saturation of AgX's look, `0` to `1`: `0` is its base curve, flat on purpose, and `1` a look
|
|
71
|
+
* like Blender's Punchy, pivoted on middle grey so the image keeps its brightness. The other tone mappings ignore it.
|
|
72
|
+
*/
|
|
73
|
+
contrast: number;
|
|
63
74
|
/**
|
|
64
75
|
* Exposes from `iso`, `shutterSpeed` and `depthOfField.aperture` as a camera would, with `exposure` as a
|
|
65
76
|
* compensation on top. The scene's lights then have to be in physical units (lux, candela, nits).
|
|
66
77
|
*/
|
|
67
78
|
physicalExposure: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Meters the image as a camera in automatic does and exposes it to middle grey, from the mean of the logarithms of
|
|
81
|
+
* its luminance, weighed towards the centre: a bright window or sky cannot darken the rest. It takes the place of
|
|
82
|
+
* `exposure` and of the physical exposure, with `exposureCompensation` on top.
|
|
83
|
+
*/
|
|
84
|
+
autoExposure: boolean;
|
|
85
|
+
/** Stops added to the metered exposure: positive brightens, as on a camera's dial. */
|
|
86
|
+
exposureCompensation: number;
|
|
68
87
|
iso: number;
|
|
69
88
|
/** Seconds. */
|
|
70
89
|
shutterSpeed: number;
|
|
@@ -130,11 +149,14 @@ export const DEFAULT_GPU_RENDER_SETTINGS: Readonly<GpuRenderSettings> = Object.f
|
|
|
130
149
|
// AgX rolls highlights off as film does, where ACES pushes saturated colours to the gamut's edge.
|
|
131
150
|
toneMapping: "agx",
|
|
132
151
|
globalIllumination: { ...DEFAULT_RENDER_SETTINGS.globalIllumination, russianRoulette: true, rouletteStart: 1,
|
|
133
|
-
lightTree: true },
|
|
152
|
+
lightTree: true, portals: true },
|
|
134
153
|
bloom: { enabled: true, intensity: 0.04, radius: 0.65 },
|
|
135
154
|
glare: { enabled: true, intensity: 0.06, blades: 6, length: 0.25, rotation: 15 },
|
|
136
|
-
lens: { chromaticAberration: 0.
|
|
137
|
-
camera: {
|
|
155
|
+
lens: { chromaticAberration: 0.05, distortion: 0 },
|
|
156
|
+
camera: {
|
|
157
|
+
contrast: 1, physicalExposure: false, autoExposure: false, exposureCompensation: 0, iso: 100,
|
|
158
|
+
shutterSpeed: 1 / 125, whiteBalance: 6500,
|
|
159
|
+
},
|
|
138
160
|
subsurface: { enabled: true, depth: 2 },
|
|
139
161
|
atmosphere: { enabled: true, visibility: 20000, haze: 0.6, color: "" },
|
|
140
162
|
volumes: { enabled: true },
|
|
@@ -160,10 +182,16 @@ export function physicalExposure(aperture: number, shutterSpeed: number, iso: nu
|
|
|
160
182
|
return 1 / (1.2 * 2 ** ev100);
|
|
161
183
|
}
|
|
162
184
|
|
|
163
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* The exposure the image is multiplied by: the setting itself, or a camera's on top of it as compensation, or with
|
|
187
|
+
* the automatic exposure only its compensation, the meter's own exposure being applied on the device.
|
|
188
|
+
*/
|
|
164
189
|
export function effectiveExposure(settings: GpuRenderSettings): number {
|
|
165
190
|
const { camera, depthOfField, exposure } = settings;
|
|
166
191
|
|
|
192
|
+
// The meter's exposure is applied on the device, by the lens pass; what is left here is the compensation.
|
|
193
|
+
if (camera.autoExposure) return 2 ** (Number(camera.exposureCompensation) || 0);
|
|
194
|
+
|
|
167
195
|
return camera.physicalExposure
|
|
168
196
|
? exposure * physicalExposure(depthOfField.aperture, camera.shutterSpeed, camera.iso)
|
|
169
197
|
: exposure;
|
package/src/wgsl/lens.wgsl.ts
CHANGED
|
@@ -46,7 +46,8 @@ struct Lens {
|
|
|
46
46
|
// The size of the level the rays are gathered at, and the angle of the first ray, in radians.
|
|
47
47
|
streakSize: vec2<u32>,
|
|
48
48
|
glareAngle: f32,
|
|
49
|
-
|
|
49
|
+
// Whether the camera meters the image and exposes it to middle grey, which the meter pass works out.
|
|
50
|
+
autoExposure: f32,
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
const PI: f32 = 3.14159265358979;
|
|
@@ -61,6 +62,108 @@ const STREAK_TAPS: i32 = 24;
|
|
|
61
62
|
@group(0) @binding(4) var<storage, read_write> up: array<vec4<f32>>;
|
|
62
63
|
@group(0) @binding(5) var<storage, read_write> graded: array<vec4<f32>>;
|
|
63
64
|
@group(0) @binding(6) var<storage, read_write> streaked: array<vec4<f32>>;
|
|
65
|
+
// What the camera's meter read: the exposure that brings the image's average to middle grey, in x.
|
|
66
|
+
@group(0) @binding(7) var<storage, read_write> metered: array<vec4<f32>>;
|
|
67
|
+
|
|
68
|
+
/** What a light meter calibrates to: an average scene reflects about eighteen percent. */
|
|
69
|
+
const MIDDLE_GREY: f32 = 0.18;
|
|
70
|
+
const METER_GRID: vec2<u32> = vec2<u32>(64u, 36u);
|
|
71
|
+
const METER_THREADS: u32 = 256u;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* How far above middle grey the meter lets the bright end of the image land: three stops, where AgX still shows
|
|
75
|
+
* detail, three and a half under where it reaches white.
|
|
76
|
+
*/
|
|
77
|
+
const HIGHLIGHT_CEILING: f32 = 8.0;
|
|
78
|
+
/** The share of the metered points that counts as the bright end: the light through a window, not a lamp's own pixels. */
|
|
79
|
+
const HIGHLIGHT_SHARE: f32 = 0.03;
|
|
80
|
+
/** Half stops of luminance, from 2^-16 to 2^16, which the bright end is found in. */
|
|
81
|
+
const METER_BINS: u32 = 64u;
|
|
82
|
+
|
|
83
|
+
var<workgroup> meterSums: array<vec2<f32>, 256>;
|
|
84
|
+
var<workgroup> meterHistogram: array<atomic<u32>, 64>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The camera's meter: the image's average luminance over a grid of points, taken as the mean of the logarithms so that
|
|
88
|
+
* a bright window or a patch of sky cannot darken everything else, weighed towards the centre as a camera's
|
|
89
|
+
* centre-weighted metering is. What no path has reached yet, or is black, does not count. One workgroup reads the
|
|
90
|
+
* grid and sums it in shared memory.
|
|
91
|
+
*
|
|
92
|
+
* Then it guards the highlights, as a camera's matrix metering does: the average alone takes a dim attic with a shaft
|
|
93
|
+
* of sun in it to middle grey and burns the shaft out, where a photographer keeps the room dim. The brightest few
|
|
94
|
+
* percent of the points, found in a histogram of half stops, may land no more than {@link HIGHLIGHT_CEILING} times
|
|
95
|
+
* brighter; an even scene, whose bright end is close to its average, is left as the average says.
|
|
96
|
+
*/
|
|
97
|
+
@compute @workgroup_size(256, 1, 1)
|
|
98
|
+
fn meter(@builtin(local_invocation_index) thread: u32) {
|
|
99
|
+
var sum = vec2<f32>(0.0);
|
|
100
|
+
let count = METER_GRID.x * METER_GRID.y;
|
|
101
|
+
|
|
102
|
+
if (thread < METER_BINS) {
|
|
103
|
+
atomicStore(&meterHistogram[thread], 0u);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
workgroupBarrier();
|
|
107
|
+
|
|
108
|
+
for (var k = thread; k < count; k = k + METER_THREADS) {
|
|
109
|
+
let cell = vec2<f32>(f32(k % METER_GRID.x), f32(k / METER_GRID.x)) + 0.5;
|
|
110
|
+
let uv = cell / vec2<f32>(METER_GRID);
|
|
111
|
+
let luminance = dot(imageTexel(i32(uv.x * f32(lens.resolution.x)), i32(uv.y * f32(lens.resolution.y))),
|
|
112
|
+
vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
113
|
+
let away = (uv - 0.5) * 2.0;
|
|
114
|
+
let weight = select(0.0, 1.0 - 0.6 * min(dot(away, away) * 0.5, 1.0), luminance > 0.0);
|
|
115
|
+
|
|
116
|
+
sum = sum + vec2<f32>(log(max(luminance, 1e-6)) * weight, weight);
|
|
117
|
+
|
|
118
|
+
if (luminance > 0.0) {
|
|
119
|
+
let bin = u32(clamp((log2(luminance) + 16.0) * 2.0, 0.0, f32(METER_BINS - 1u)));
|
|
120
|
+
|
|
121
|
+
atomicAdd(&meterHistogram[bin], 1u);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
meterSums[thread] = sum;
|
|
126
|
+
workgroupBarrier();
|
|
127
|
+
|
|
128
|
+
for (var stride = METER_THREADS / 2u; stride > 0u; stride = stride / 2u) {
|
|
129
|
+
if (thread < stride) {
|
|
130
|
+
meterSums[thread] = meterSums[thread] + meterSums[thread + stride];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
workgroupBarrier();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (thread == 0u) {
|
|
137
|
+
let total = meterSums[0];
|
|
138
|
+
let average = select(MIDDLE_GREY, exp(total.x / max(total.y, 1e-6)), total.y > 0.0);
|
|
139
|
+
var exposure = MIDDLE_GREY / average;
|
|
140
|
+
|
|
141
|
+
// From the top down, the first half stop by which the bright share is reached; its upper edge is that end.
|
|
142
|
+
var points = 0u;
|
|
143
|
+
|
|
144
|
+
for (var bin = 0u; bin < METER_BINS; bin = bin + 1u) {
|
|
145
|
+
points = points + atomicLoad(&meterHistogram[bin]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var above = 0u;
|
|
149
|
+
var bright = 0.0;
|
|
150
|
+
|
|
151
|
+
for (var bin = i32(METER_BINS) - 1; bin >= 0 && points > 0u; bin = bin - 1) {
|
|
152
|
+
above = above + atomicLoad(&meterHistogram[u32(bin)]);
|
|
153
|
+
|
|
154
|
+
if (f32(above) >= HIGHLIGHT_SHARE * f32(points)) {
|
|
155
|
+
bright = exp2(f32(bin + 1) * 0.5 - 16.0);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (bright > 0.0) {
|
|
161
|
+
exposure = min(exposure, HIGHLIGHT_CEILING * MIDDLE_GREY / bright);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
metered[0] = vec4<f32>(clamp(exposure, 1e-4, 1e4), average, bright, 0.0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
64
167
|
|
|
65
168
|
/** The image's own texel: its mean so far, or nothing where no path has landed yet. */
|
|
66
169
|
fn imageTexel(x: i32, y: i32) -> vec3<f32> {
|
|
@@ -293,6 +396,10 @@ fn composite(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
293
396
|
color = mix(color, streakBilinear(position * vec2<f32>(lens.streakSize) / size), lens.glare);
|
|
294
397
|
}
|
|
295
398
|
|
|
399
|
+
if (lens.autoExposure > 0.0) {
|
|
400
|
+
color = color * metered[0].x;
|
|
401
|
+
}
|
|
402
|
+
|
|
296
403
|
let balanced = vec3<f32>(dot(lens.white0.xyz, color), dot(lens.white1.xyz, color), dot(lens.white2.xyz, color));
|
|
297
404
|
|
|
298
405
|
graded[id.y * lens.resolution.x + id.x] = vec4<f32>(max(balanced, vec3<f32>(0.0)), 1.0);
|
package/src/wgsl/lights.wgsl.ts
CHANGED
|
@@ -64,8 +64,8 @@ const NO_LIGHT: u32 = 0xffffffffu;
|
|
|
64
64
|
* angle the node's box takes from the point (Conty Estevez and Kulla's importance). A point inside the box rules
|
|
65
65
|
* nothing out. A zero normal is a receiver that takes light from everywhere, such as a point in a volume.
|
|
66
66
|
*/
|
|
67
|
-
fn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
68
|
-
let at =
|
|
67
|
+
fn lightNodeImportance(tree: u32, node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
68
|
+
let at = tree + node * TREE_NODE_WORDS;
|
|
69
69
|
let low = extraVec4(at);
|
|
70
70
|
let high = extraVec4(at + 4u);
|
|
71
71
|
|
|
@@ -125,13 +125,13 @@ fn lightNodeImportance(node: u32, position: vec3<f32>, normal: vec3<f32>) -> f32
|
|
|
125
125
|
return low.w * orientation * facing / distance2;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
/** The chance of stepping from a node of
|
|
129
|
-
fn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
|
|
128
|
+
/** The chance of stepping from a node of a tree (the lights' or the windows') to each of its children, from here. */
|
|
129
|
+
fn lightChildOdds(tree: u32, left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
|
|
130
130
|
var importance = vec2<f32>(0.0);
|
|
131
131
|
|
|
132
132
|
// One loop over the two children, so the importance is written into the kernel once per walk.
|
|
133
133
|
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
134
|
-
importance[k] = lightNodeImportance(select(right, left, k == 0u), position, normal);
|
|
134
|
+
importance[k] = lightNodeImportance(tree, select(right, left, k == 0u), position, normal);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
let total = importance.x + importance.y;
|
|
@@ -139,20 +139,23 @@ fn lightChildOdds(left: u32, right: u32, position: vec3<f32>, normal: vec3<f32>)
|
|
|
139
139
|
return select(importance / total, vec2<f32>(0.0), total <= 0.0);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
142
|
+
/**
|
|
143
|
+
* An entry picked down a tree by importance from this point, with the chance of picking it; NO_LIGHT for none. The
|
|
144
|
+
* tree is the lights' or the windows', given by where its nodes and its leaves' list start.
|
|
145
|
+
*/
|
|
146
|
+
fn sampleLightTree(tree: u32, list: u32, position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<function, f32>,
|
|
144
147
|
density: ptr<function, f32>) -> u32 {
|
|
145
148
|
var node = 0u;
|
|
146
149
|
var chance = 1.0;
|
|
147
150
|
var x = u;
|
|
148
151
|
|
|
149
152
|
for (var depth = 0u; depth < 64u; depth = depth + 1u) {
|
|
150
|
-
let at =
|
|
153
|
+
let at = tree + node * TREE_NODE_WORDS;
|
|
151
154
|
let first = textures[at + 12u];
|
|
152
155
|
|
|
153
156
|
// A leaf lists its lights, too close together to tell apart from here: one is picked by its share of the power.
|
|
154
157
|
if ((first & TREE_LEAF) != 0u) {
|
|
155
|
-
let start =
|
|
158
|
+
let start = list + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;
|
|
156
159
|
let count = textures[at + 13u];
|
|
157
160
|
var slot = start + (count - 1u) * TREE_ENTRY_WORDS;
|
|
158
161
|
|
|
@@ -170,7 +173,7 @@ fn sampleLightTree(position: vec3<f32>, normal: vec3<f32>, u: f32, pdf: ptr<func
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
let second = textures[at + 13u];
|
|
173
|
-
let odds = lightChildOdds(first, second, position, normal);
|
|
176
|
+
let odds = lightChildOdds(tree, first, second, position, normal);
|
|
174
177
|
|
|
175
178
|
if (odds.x + odds.y <= 0.0) {
|
|
176
179
|
return NO_LIGHT;
|
|
@@ -208,7 +211,7 @@ fn lightTreePdf(index: u32, position: vec3<f32>, normal: vec3<f32>) -> f32 {
|
|
|
208
211
|
|
|
209
212
|
let at = frame.lightTree + parent * TREE_NODE_WORDS;
|
|
210
213
|
let first = textures[at + 12u];
|
|
211
|
-
let odds = lightChildOdds(first, textures[at + 13u], position, normal);
|
|
214
|
+
let odds = lightChildOdds(frame.lightTree, first, textures[at + 13u], position, normal);
|
|
212
215
|
|
|
213
216
|
chance = chance * select(odds.y, odds.x, node == first);
|
|
214
217
|
node = parent;
|
|
@@ -252,6 +255,119 @@ fn lightPdfOfTriangle(triangle: u32, position: vec3<f32>, normal: vec3<f32>) ->
|
|
|
252
255
|
return 0.0;
|
|
253
256
|
}
|
|
254
257
|
|
|
258
|
+
/** Share of the sky's samples aimed through the windows, from a point inside a room. */
|
|
259
|
+
const PORTAL_SHARE: f32 = 0.5;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* How often the sky is aimed at through the windows from this point: from inside a room, where something stands above
|
|
263
|
+
* it, half the time; outside never, where the open sky is better found by its brightness. A function of the point alone,
|
|
264
|
+
* so the heuristic can ask it again when a bounce ray escapes.
|
|
265
|
+
*/
|
|
266
|
+
fn portalChance(position: vec3<f32>) -> f32 {
|
|
267
|
+
if (!PORTALS || frame.portalCount == 0u) {
|
|
268
|
+
return 0.0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return select(0.0, PORTAL_SHARE, traceAny(position, vec3<f32>(0.0, 1.0, 0.0), 1e30) != NO_HIT);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Window subtrees a ray's walk through the windows' tree keeps waiting. */
|
|
275
|
+
const PORTAL_STACK: u32 = 32u;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The solid angle density with which aiming through the windows produces this direction: over every window triangle
|
|
279
|
+
* it crosses, the chance the tree picks that window from here times the area density of a point on it, over the area
|
|
280
|
+
* it shows from here. The ray walks the tree as it would a hierarchy, by the nodes' boxes, carrying the chance of
|
|
281
|
+
* reaching each node, so the windows it misses cost a box test and the ones it crosses no walk back up.
|
|
282
|
+
*/
|
|
283
|
+
fn portalPdf(position: vec3<f32>, direction: vec3<f32>) -> f32 {
|
|
284
|
+
var nodes: array<u32, PORTAL_STACK>;
|
|
285
|
+
var chances: array<f32, PORTAL_STACK>;
|
|
286
|
+
var top = 1u;
|
|
287
|
+
let inverse = 1.0 / direction;
|
|
288
|
+
var pdf = 0.0;
|
|
289
|
+
|
|
290
|
+
nodes[0] = 0u;
|
|
291
|
+
chances[0] = 1.0;
|
|
292
|
+
|
|
293
|
+
loop {
|
|
294
|
+
if (top == 0u) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
top = top - 1u;
|
|
299
|
+
|
|
300
|
+
let node = nodes[top];
|
|
301
|
+
let chance = chances[top];
|
|
302
|
+
let at = frame.portals + node * TREE_NODE_WORDS;
|
|
303
|
+
|
|
304
|
+
if (enterBox(extraVec4(at).xyz, extraVec4(at + 4u).xyz, position, inverse, 1e30) >= 1e30) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let first = textures[at + 12u];
|
|
309
|
+
let second = textures[at + 13u];
|
|
310
|
+
|
|
311
|
+
if ((first & TREE_LEAF) != 0u) {
|
|
312
|
+
let start = frame.portalList + (first & ~TREE_LEAF) * TREE_ENTRY_WORDS;
|
|
313
|
+
|
|
314
|
+
for (var j = 0u; j < second; j = j + 1u) {
|
|
315
|
+
let slot = start + j * TREE_ENTRY_WORDS;
|
|
316
|
+
let triangle = textures[slot];
|
|
317
|
+
let t = intersectTriangle(leafOf(triangle), position, direction).x;
|
|
318
|
+
|
|
319
|
+
if (t > 0.0) {
|
|
320
|
+
let cosine = abs(dot(triangleNormal(triangle), direction));
|
|
321
|
+
|
|
322
|
+
pdf = pdf + chance * extraFloat(slot + 1u) * extraFloat(slot + 2u) * t * t / max(cosine, 1e-6);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let odds = lightChildOdds(frame.portals, first, second, position, vec3<f32>(0.0));
|
|
330
|
+
|
|
331
|
+
for (var k = 0u; k < 2u; k = k + 1u) {
|
|
332
|
+
if (odds[k] > 0.0 && top < PORTAL_STACK) {
|
|
333
|
+
nodes[top] = select(second, first, k == 0u);
|
|
334
|
+
chances[top] = chance * odds[k];
|
|
335
|
+
top = top + 1u;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return pdf;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** A direction through a point on a window the tree picks from here, or none from a point on the window itself. */
|
|
344
|
+
fn samplePortal(position: vec3<f32>, u: vec2<f32>) -> vec3<f32> {
|
|
345
|
+
var chance = 0.0;
|
|
346
|
+
var density = 0.0;
|
|
347
|
+
let triangle = sampleLightTree(frame.portals, frame.portalList, position, vec3<f32>(0.0), u.x, &chance, &density);
|
|
348
|
+
|
|
349
|
+
if (triangle == NO_LIGHT) {
|
|
350
|
+
return vec3<f32>(0.0);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The tree spent the sample walking down; a fresh one places the point, which the pick does not depend on.
|
|
354
|
+
let point = trianglePointAt(triangle, triangleBarycentric(vec2<f32>(fract(u.x * 4096.0), u.y)));
|
|
355
|
+
let offset = point - position;
|
|
356
|
+
|
|
357
|
+
return select(vec3<f32>(0.0), normalize(offset), dot(offset, offset) > 1e-12);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The density of the sky's sampling from this point for this direction: by brightness, through windows, or both. */
|
|
361
|
+
fn skyPdf(position: vec3<f32>, direction: vec3<f32>, share: f32) -> f32 {
|
|
362
|
+
var pdf = (1.0 - share) * environmentPdf(direction);
|
|
363
|
+
|
|
364
|
+
if (share > 0.0) {
|
|
365
|
+
pdf = pdf + share * portalPdf(position, direction);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return pdf;
|
|
369
|
+
}
|
|
370
|
+
|
|
255
371
|
/** The power heuristic: how much of a contribution the strategy that produced it keeps, against the other one. */
|
|
256
372
|
fn misWeight(pdf: f32, other: f32) -> f32 {
|
|
257
373
|
let a = pdf * pdf;
|
|
@@ -307,7 +423,7 @@ fn sampleLight(position: vec3<f32>, normal: vec3<f32>, pick: f32, u: vec2<f32>)
|
|
|
307
423
|
if (frame.lightTree != 0u && pick < frame.localLightPick) {
|
|
308
424
|
var chance = 0.0;
|
|
309
425
|
var density = 1.0;
|
|
310
|
-
let index = sampleLightTree(position, normal, pick / frame.localLightPick, &chance, &density);
|
|
426
|
+
let index = sampleLightTree(frame.lightTree, frame.lightList, position, normal, pick / frame.localLightPick, &chance, &density);
|
|
311
427
|
|
|
312
428
|
if (index == NO_LIGHT || chance <= 0.0) {
|
|
313
429
|
return noLight();
|
|
@@ -356,17 +472,31 @@ fn sampleLightEntry(light: Light, pdf: f32, position: vec3<f32>, u: vec2<f32>) -
|
|
|
356
472
|
sample.misPdf = 0.0;
|
|
357
473
|
|
|
358
474
|
if (light.kind == LIGHT_ENVIRONMENT) {
|
|
359
|
-
|
|
475
|
+
// From inside a room, part of the time through a window: daylight comes in there or not at all.
|
|
476
|
+
let share = portalChance(position);
|
|
477
|
+
var direction = vec3<f32>(0.0);
|
|
478
|
+
|
|
479
|
+
if (u.x < share) {
|
|
480
|
+
direction = samplePortal(position, vec2<f32>(u.x / share, u.y));
|
|
481
|
+
} else {
|
|
482
|
+
let drawn = sampleEnvironmentDirection(vec2<f32>(min((u.x - share) / (1.0 - share), 0.99999994), u.y));
|
|
483
|
+
|
|
484
|
+
if (drawn.pdf > 0.0) {
|
|
485
|
+
direction = drawn.direction;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
let mixed = skyPdf(position, direction, share);
|
|
360
490
|
|
|
361
|
-
if (
|
|
491
|
+
if (all(direction == vec3<f32>(0.0)) || mixed <= 0.0) {
|
|
362
492
|
return noLight();
|
|
363
493
|
}
|
|
364
494
|
|
|
365
|
-
let density = pdf *
|
|
495
|
+
let density = pdf * mixed;
|
|
366
496
|
|
|
367
|
-
sample.direction =
|
|
497
|
+
sample.direction = direction;
|
|
368
498
|
sample.distance = 1e30;
|
|
369
|
-
sample.value = environmentRadiance(
|
|
499
|
+
sample.value = environmentRadiance(direction) / density;
|
|
370
500
|
// A bounce ray that escapes finds the same light, so the two strategies share it.
|
|
371
501
|
sample.misPdf = density;
|
|
372
502
|
|