@driftengine/core 4.8.6 → 4.8.7
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/CHANGELOG.json +71 -0
- package/README.md +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +35 -0
- package/dist/render/areaLights.js +35 -0
- package/dist/render/backend/api.d.ts +33 -3
- package/dist/render/backend/webgl2/renderer.d.ts +50 -14
- package/dist/render/backend/webgl2/renderer.js +141 -30
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/renderer.d.ts +35 -1
- package/dist/render/backend/webgpu/renderer.js +155 -16
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/clusteredLights.d.ts +23 -5
- package/dist/render/clusteredLights.js +35 -8
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +5 -0
- package/dist/render/instances.d.ts +19 -5
- package/dist/render/instances.js +9 -3
- package/dist/render/lightBudget.d.ts +12 -1
- package/dist/render/lightBudget.js +23 -3
- package/dist/render/lightmap.d.ts +23 -9
- package/dist/render/lightmap.js +40 -18
- package/dist/render/particlePool.d.ts +21 -0
- package/dist/render/particlePool.js +7 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/renderQuality.d.ts +8 -7
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/screenSpaceReflection.d.ts +27 -1
- package/dist/render/screenSpaceReflection.js +9 -0
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +46 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +3 -1
- package/dist/render/shaders/flat/index.d.ts +5 -0
- package/dist/render/shaders/flat/index.js +16 -2
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +57 -22
- package/dist/render/shaders/flat/models.d.ts +5 -2
- package/dist/render/shaders/flat/models.js +100 -16
- package/dist/render/shaders/flat/preamble.js +10 -3
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
- package/dist/render/shaders/generated/flat.wgsl.js +54 -38
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/ssrTrace.js +20 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.js +1 -0
- package/dist/render/surfaceModel.d.ts +29 -8
- package/dist/render/surfaceModel.js +33 -9
- package/dist/render/surfaceTexture.d.ts +25 -1
- package/dist/render/surfaceTexture.js +27 -5
- package/package.json +6 -6
- package/src/index.ts +6 -2
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +58 -0
- package/src/render/backend/api.ts +49 -2
- package/src/render/backend/webgl2/renderer.ts +192 -38
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +194 -19
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
- package/src/render/clusteredLights.ts +41 -8
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +4 -0
- package/src/render/instances.ts +26 -6
- package/src/render/lightBudget.ts +40 -2
- package/src/render/lightmap.ts +64 -23
- package/src/render/particlePool.ts +23 -0
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/renderQuality.ts +8 -7
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/screenSpaceReflection.ts +33 -1
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +46 -0
- package/src/render/shaders/flat/glassTint.ts +9 -2
- package/src/render/shaders/flat/index.ts +21 -2
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +57 -22
- package/src/render/shaders/flat/models.ts +100 -16
- package/src/render/shaders/flat/preamble.ts +10 -3
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/flat.wgsl.ts +54 -38
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/ssrTrace.ts +20 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +1 -0
- package/src/render/surfaceModel.ts +47 -10
- package/src/render/surfaceTexture.ts +69 -9
|
@@ -53,6 +53,29 @@ export interface ParticleInstances {
|
|
|
53
53
|
capacity: number;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
|
|
58
|
+
* order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
|
|
59
|
+
* sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
|
|
60
|
+
* own upload interleaves them. 64 bytes a particle.
|
|
61
|
+
*/
|
|
62
|
+
export const DEVICE_PARTICLE_FLOATS = 16;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Particles a caller's own compute shader wrote into a buffer of its own (`registerCompute`), drawn
|
|
66
|
+
* where they lie by `drawDeviceParticles` rather than read back and uploaded again. WebGPU only.
|
|
67
|
+
*
|
|
68
|
+
* **Not sorted**, as a pool's are where its batch asks: the device holds them, so an alpha-blended
|
|
69
|
+
* set that must be drawn far to near sorts itself. A slot with a half-width of 0 draws nothing, which
|
|
70
|
+
* is how a fixed-size buffer holds fewer live particles than it has room for.
|
|
71
|
+
*/
|
|
72
|
+
export interface DeviceParticles {
|
|
73
|
+
/** A buffer with `VERTEX` usage, `DEVICE_PARTICLE_FLOATS` floats a particle. */
|
|
74
|
+
readonly buffer: GPUBuffer;
|
|
75
|
+
/** How many particles from its start to draw; no more than the buffer holds. */
|
|
76
|
+
readonly count: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
function createParticleInstances(capacity: number): ParticleInstances {
|
|
57
80
|
return {
|
|
58
81
|
positions: new Float32Array(capacity * 3),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** Allocation-free selection and packing of point lights for a fixed shader budget. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
POINT_LIGHT_COS_INNER,
|
|
5
|
+
POINT_LIGHT_COS_OUTER,
|
|
6
|
+
lightFalloffExponentOf,
|
|
7
|
+
} from './clusteredLights.ts';
|
|
4
8
|
import { MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from './lightBudget.ts';
|
|
5
9
|
import { gatherLights } from './lightGrid.ts';
|
|
6
10
|
import type { LightGrid } from './lightGrid.ts';
|
|
@@ -198,6 +202,14 @@ export interface PointLightSource {
|
|
|
198
202
|
* whose uniform budget has none to spare.
|
|
199
203
|
*/
|
|
200
204
|
inLightField?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* How the light fades with distance when it names its own way: `(1 - (d/R)^2)^n` with no distance
|
|
207
|
+
* term, `R` its radius and `n` this. Unreal's falloff for a light without inverse-square falloff,
|
|
208
|
+
* so a rig authored there — a fill of brightness 160 across 164 m, a key at exponent 8 — lights
|
|
209
|
+
* the space it was authored for rather than one that ends a few metres out. Absent or 0, the
|
|
210
|
+
* frame's own falloff (`pointLightFalloff`), which is every light before 4.8.7.
|
|
211
|
+
*/
|
|
212
|
+
falloffExponent?: number;
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
export interface PointLightBuffer {
|
|
@@ -256,6 +268,11 @@ export interface PointLightBuffer {
|
|
|
256
268
|
readonly directions: Float32Array;
|
|
257
269
|
/** Two per slot: the cosine of the inner cone angle, then of the outer. */
|
|
258
270
|
readonly coneCos: Float32Array;
|
|
271
|
+
/**
|
|
272
|
+
* One per slot: the light's own falloff exponent, or 0 for the frame's falloff. See
|
|
273
|
+
* `PointLightSource.falloffExponent`.
|
|
274
|
+
*/
|
|
275
|
+
readonly falloffExponents: Float32Array;
|
|
259
276
|
/** One per slot: a row of the photometric atlas, or −1 for a light with no profile. */
|
|
260
277
|
readonly iesProfiles: Float32Array;
|
|
261
278
|
/** Internal insertion-sort storage; exposed only to keep one flat object. */
|
|
@@ -307,6 +324,7 @@ export function createPointLightBuffer(capacity: number = MAX_POINT_LIGHTS): Poi
|
|
|
307
324
|
weights: new Float32Array(shaded),
|
|
308
325
|
directions: new Float32Array(shaded * 3),
|
|
309
326
|
coneCos: new Float32Array(shaded * 2),
|
|
327
|
+
falloffExponents: new Float32Array(shaded),
|
|
310
328
|
/* −1 rather than 0, because row 0 is a real fixture and a light that asked for none must not
|
|
311
329
|
be shaped by whichever profile happened to load first. */
|
|
312
330
|
iesProfiles: new Float32Array(shaded).fill(-1),
|
|
@@ -587,6 +605,7 @@ function choose(
|
|
|
587
605
|
* Restricting it to spots would be a smaller feature for no saving.
|
|
588
606
|
*/
|
|
589
607
|
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
608
|
+
out.falloffExponents[slot] = lightFalloffExponentOf(light.falloffExponent);
|
|
590
609
|
}
|
|
591
610
|
out.count = chosen;
|
|
592
611
|
|
|
@@ -219,13 +219,14 @@ export interface RenderQuality {
|
|
|
219
219
|
* **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
|
|
220
220
|
* reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
|
|
221
221
|
* as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
|
|
222
|
-
* Burley's blur in the picture as well: a skin draw writes its diffuse
|
|
223
|
-
* is drawn
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
222
|
+
* Burley's blur in the picture as well: a skin draw writes the light its diffuse is made of to a
|
|
223
|
+
* target of its own and its colour to another (it is drawn three times to do so), and the light is
|
|
224
|
+
* spread across the pixels its scatter distance covers, multiplied by the colour and added back
|
|
225
|
+
* before anything blended is drawn, so light crosses a shadow's edge and a nostril's rim in the
|
|
226
|
+
* picture rather than in a fit. See `skinBlur.ts`.
|
|
227
|
+
*
|
|
228
|
+
* Construction-time: three half-float targets the size of the frame, and two multisampled ones
|
|
229
|
+
* under multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
|
|
229
230
|
* where the frame is still linear light to add to; without one, skin is pre-integrated and the
|
|
230
231
|
* renderer says so once. What would make the default wrong is a close-up face that reads as wax
|
|
231
232
|
* at a shadow's edge — that is what the blur is for.
|
|
@@ -119,6 +119,9 @@ export function recordingGl(
|
|
|
119
119
|
ONE: 1,
|
|
120
120
|
ONE_MINUS_SRC_ALPHA: 0x0303,
|
|
121
121
|
TRIANGLES: 0x0004,
|
|
122
|
+
/* The winding a pass names, so a test can tell which one it chose. */
|
|
123
|
+
CW: 0x0900,
|
|
124
|
+
CCW: 0x0901,
|
|
122
125
|
TEXTURE0: 0x84c0,
|
|
123
126
|
DEPTH_BUFFER_BIT: 0x00000100,
|
|
124
127
|
DEPTH_TEST: 0x0b71,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rgb9e5: three 9-bit mantissas over one shared 5-bit exponent, an unsigned float colour in four
|
|
3
|
+
* bytes. `EXT_texture_shared_exponent`'s encoding, the one a GPU's `rgb9e5ufloat` reads, written
|
|
4
|
+
* out so a lightmap page can hold its irradiance in four bytes a texel against a half float's eight.
|
|
5
|
+
*
|
|
6
|
+
* **What it gives up**: precision in the smaller channels, which share the largest one's exponent —
|
|
7
|
+
* a channel a thousandth of its neighbour keeps about one bit — and anything negative or past
|
|
8
|
+
* 65,408, which it clamps. Light is neither, and a bake's colours rarely span more than a few
|
|
9
|
+
* octaves within one texel, so a lightmap loses nothing visible; a value that needs each channel
|
|
10
|
+
* exact wants a format of its own.
|
|
11
|
+
*
|
|
12
|
+
* The bits, low to high: red in 0–8, green in 9–17, blue in 18–26, the exponent in 27–31, biased by
|
|
13
|
+
* 15, and a value is `mantissa * 2^(exponent - 15 - 9)`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BIAS = 15;
|
|
17
|
+
const MANTISSA_BITS = 9;
|
|
18
|
+
const MANTISSA_VALUES = 1 << MANTISSA_BITS;
|
|
19
|
+
const MAX_EXPONENT = 31;
|
|
20
|
+
|
|
21
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
22
|
+
export const RGB9E5_MAX = ((MANTISSA_VALUES - 1) / MANTISSA_VALUES) * 2 ** (MAX_EXPONENT - BIAS);
|
|
23
|
+
|
|
24
|
+
const clampChannel = (v: number): number => (v > 0 ? Math.min(v, RGB9E5_MAX) : 0);
|
|
25
|
+
|
|
26
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
27
|
+
export function packRgb9e5(red: number, green: number, blue: number): number {
|
|
28
|
+
const r = clampChannel(red);
|
|
29
|
+
const g = clampChannel(green);
|
|
30
|
+
const b = clampChannel(blue);
|
|
31
|
+
const largest = Math.max(r, g, b);
|
|
32
|
+
let exponent = Math.max(-BIAS - 1, Math.floor(Math.log2(largest))) + 1 + BIAS;
|
|
33
|
+
let scale = 2 ** (exponent - BIAS - MANTISSA_BITS);
|
|
34
|
+
/* Rounding the largest channel up to 512 needs one more exponent, which the spec allows for. */
|
|
35
|
+
if (Math.floor(largest / scale + 0.5) === MANTISSA_VALUES) {
|
|
36
|
+
exponent += 1;
|
|
37
|
+
scale *= 2;
|
|
38
|
+
}
|
|
39
|
+
/* Inline rather than a helper: a page packs millions of texels, and a closure a call is one each. */
|
|
40
|
+
const mr = Math.min(MANTISSA_VALUES - 1, Math.floor(r / scale + 0.5));
|
|
41
|
+
const mg = Math.min(MANTISSA_VALUES - 1, Math.floor(g / scale + 0.5));
|
|
42
|
+
const mb = Math.min(MANTISSA_VALUES - 1, Math.floor(b / scale + 0.5));
|
|
43
|
+
return (mr | (mg << 9) | (mb << 18) | (exponent << 27)) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
47
|
+
export function unpackRgb9e5(bits: number, out: { [index: number]: number }, at = 0): void {
|
|
48
|
+
const scale = 2 ** ((bits >>> 27) - BIAS - MANTISSA_BITS);
|
|
49
|
+
out[at] = (bits & 511) * scale;
|
|
50
|
+
out[at + 1] = ((bits >>> 9) & 511) * scale;
|
|
51
|
+
out[at + 2] = ((bits >>> 18) & 511) * scale;
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene capture: a texture the world is drawn into from a camera of the caller's, which a material
|
|
3
|
+
* then shows like any image — a screen in the world showing what a camera elsewhere sees, a monitor,
|
|
4
|
+
* a security feed. The planar reflection's pass, for a caller's camera rather than a mirrored one.
|
|
5
|
+
*
|
|
6
|
+
* **It holds radiance, not display pixels**, as a probe does: drawn with the output transform held
|
|
7
|
+
* off, at the colour format a probe takes — half floats under `hdrScene`, bytes otherwise — and
|
|
8
|
+
* sampled linear, so a material showing it as its `emissive` emits the light the capture saw and the
|
|
9
|
+
* frame's grade then applies once, to the screen as to everything around it.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: it is the mesh pass alone, as a probe bake is — the sky, water, particles and
|
|
12
|
+
* anything else a scene draws after its world are in the capture only if the callback draws them;
|
|
13
|
+
* no temporal resolve, bloom or other effect after the world runs on it; and it has no mip chain, so
|
|
14
|
+
* a screen seen small and far shimmers where a mipmapped image would not.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** What a capture's texture is made from, in place of an image: its size and colour format. */
|
|
18
|
+
export interface SceneCaptureTexels {
|
|
19
|
+
readonly kind: 'scene-capture';
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
/** Half floats, which hold light past one; bytes otherwise. */
|
|
23
|
+
readonly float: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
27
|
+
export const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
28
|
+
|
|
29
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
30
|
+
export function sceneCaptureTexels(
|
|
31
|
+
width: number,
|
|
32
|
+
height: number,
|
|
33
|
+
float: boolean,
|
|
34
|
+
): SceneCaptureTexels {
|
|
35
|
+
for (const [name, side] of [
|
|
36
|
+
['width', width],
|
|
37
|
+
['height', height],
|
|
38
|
+
] as const) {
|
|
39
|
+
if (!Number.isInteger(side) || side < 1 || side > MAX_SCENE_CAPTURE_SIZE) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`createSceneCapture: ${name} must be a whole number from 1 to ${MAX_SCENE_CAPTURE_SIZE}, ` +
|
|
42
|
+
`not ${String(side)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { kind: 'scene-capture', width, height, float };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
50
|
+
export function isSceneCaptureTexels(source: unknown): source is SceneCaptureTexels {
|
|
51
|
+
return (
|
|
52
|
+
typeof source === 'object' &&
|
|
53
|
+
source !== null &&
|
|
54
|
+
(source as { kind?: unknown }).kind === 'scene-capture'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -350,9 +350,30 @@ export function towardCameraFade(direction: ReadonlyVec3, viewDirection: Readonl
|
|
|
350
350
|
return Math.min(1, Math.max(0, (along + 1) / 2));
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
/**
|
|
354
|
+
* What a `ReflectiveSurface` is made with: its box, which way the surfaces inside it face, and how
|
|
355
|
+
* much and how far they reflect. Every number but the box has a default; see each field.
|
|
356
|
+
*/
|
|
353
357
|
export interface ReflectiveSurfaceOptions extends DecalBox {
|
|
354
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* How much of what the ray finds lands on the surface, 0 to 1: one share over the box, or with
|
|
360
|
+
* `fresnel` the surface's reflectance head-on.
|
|
361
|
+
*/
|
|
355
362
|
readonly strength?: number;
|
|
363
|
+
/**
|
|
364
|
+
* Whether `strength` is the surface's reflectance head-on, F0, rather than one share over the
|
|
365
|
+
* box. Each pixel then returns `F0 · A + B` of what its ray finds, the split-sum environment BRDF
|
|
366
|
+
* at its own view and `roughness` — Karis's fit, the one the lit stage reflects its environment
|
|
367
|
+
* by — so a floor returns more toward the horizon than underfoot, and the share follows the
|
|
368
|
+
* camera as Unreal weighs its reflections. **False by default**, the one share as before.
|
|
369
|
+
*
|
|
370
|
+
* **What it gives up**: the pass has no material buffer, so the roughness and F0 are the box's,
|
|
371
|
+
* not each pixel's, and nothing occludes the reflection where the lit stage's ambient occlusion
|
|
372
|
+
* would. A floor of one material is what it is for.
|
|
373
|
+
*/
|
|
374
|
+
readonly fresnel?: boolean;
|
|
375
|
+
/** The surface's roughness, 0 to 1, that `fresnel` weighs by. 0, a polished surface, if absent. */
|
|
376
|
+
readonly roughness?: number;
|
|
356
377
|
/** How far a ray may travel, world metres. */
|
|
357
378
|
readonly reachM?: number;
|
|
358
379
|
/** How deep a depth sample is treated as being. See `ScreenSpaceMarch.thicknessM`. */
|
|
@@ -382,6 +403,9 @@ export class ReflectiveSurface {
|
|
|
382
403
|
readonly tint = new Float32Array(3);
|
|
383
404
|
|
|
384
405
|
strength: number;
|
|
406
|
+
/** See `ReflectiveSurfaceOptions.fresnel`. */
|
|
407
|
+
fresnel: boolean;
|
|
408
|
+
roughness: number;
|
|
385
409
|
reachM: number;
|
|
386
410
|
thicknessM: number;
|
|
387
411
|
steps: number;
|
|
@@ -405,6 +429,8 @@ export class ReflectiveSurface {
|
|
|
405
429
|
/* Half, because a mirror is the rare case: a wet floor, polished stone and a car bonnet all
|
|
406
430
|
return a fraction of what lands on them, and a consumer who wants a mirror asks for one. */
|
|
407
431
|
this.strength = options.strength ?? 0.5;
|
|
432
|
+
this.fresnel = options.fresnel ?? false;
|
|
433
|
+
this.roughness = options.roughness ?? 0;
|
|
408
434
|
/* Eight metres covers a room and a street corner. Further costs steps rather than accuracy,
|
|
409
435
|
since the step is the reach divided by them. */
|
|
410
436
|
this.reachM = options.reachM ?? 8;
|
|
@@ -469,6 +495,8 @@ export interface RecordedReflection {
|
|
|
469
495
|
readonly axis: Float32Array;
|
|
470
496
|
readonly tint: Float32Array;
|
|
471
497
|
strength: number;
|
|
498
|
+
fresnel: boolean;
|
|
499
|
+
roughness: number;
|
|
472
500
|
reachM: number;
|
|
473
501
|
thicknessM: number;
|
|
474
502
|
steps: number;
|
|
@@ -482,6 +510,8 @@ function newRecord(): RecordedReflection {
|
|
|
482
510
|
axis: new Float32Array(3),
|
|
483
511
|
tint: new Float32Array(3),
|
|
484
512
|
strength: 0.5,
|
|
513
|
+
fresnel: false,
|
|
514
|
+
roughness: 0,
|
|
485
515
|
reachM: 8,
|
|
486
516
|
thicknessM: 0.25,
|
|
487
517
|
steps: 24,
|
|
@@ -537,6 +567,8 @@ export class ReflectionQueue {
|
|
|
537
567
|
record.axis.set(surface.axis);
|
|
538
568
|
record.tint.set(surface.tint);
|
|
539
569
|
record.strength = Math.min(1, Math.max(0, surface.strength));
|
|
570
|
+
record.fresnel = surface.fresnel;
|
|
571
|
+
record.roughness = Math.min(1, Math.max(0, surface.roughness));
|
|
540
572
|
record.reachM = Math.max(0, surface.reachM);
|
|
541
573
|
record.thicknessM = Math.max(0, surface.thicknessM);
|
|
542
574
|
record.steps = Math.min(32, Math.max(1, Math.round(surface.steps)));
|
|
@@ -107,7 +107,13 @@ void main() {
|
|
|
107
107
|
* the glow. A single falloff gives either a hard line with no atmosphere or a
|
|
108
108
|
* soft smudge with no filament.
|
|
109
109
|
*/
|
|
110
|
-
|
|
110
|
+
/*
|
|
111
|
+
* Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which
|
|
112
|
+
* may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a
|
|
113
|
+
* negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc
|
|
114
|
+
* the size of the blur. Found by a fight drawing short arcs at 4 samples.
|
|
115
|
+
*/
|
|
116
|
+
float across = clamp(1.0 - abs(vSide), 0.0, 1.0);
|
|
111
117
|
float core = pow(across, 10.0);
|
|
112
118
|
/*
|
|
113
119
|
* A *wide* halo, deliberately. This exponent is the single biggest control over
|
|
@@ -101,6 +101,12 @@ void main() {
|
|
|
101
101
|
#endif
|
|
102
102
|
#if INSTANCED
|
|
103
103
|
mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
|
|
104
|
+
/* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
|
|
105
|
+
vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
|
|
106
|
+
model[0].w = 0.0;
|
|
107
|
+
model[1].w = 0.0;
|
|
108
|
+
model[2].w = 0.0;
|
|
109
|
+
model[3].w = 1.0;
|
|
104
110
|
#else
|
|
105
111
|
mat4 model = uModel;
|
|
106
112
|
#endif
|
|
@@ -125,7 +131,12 @@ void main() {
|
|
|
125
131
|
vLightPosition = uLightViewProj * vec4(bent, world.w);
|
|
126
132
|
gl_Position = vLightPosition;
|
|
127
133
|
#if CUTOUT
|
|
134
|
+
#if INSTANCED
|
|
135
|
+
/* A cutout instance's shadow is cut by its own cell, as its picture is. */
|
|
136
|
+
vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
|
|
137
|
+
#else
|
|
128
138
|
vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
|
|
139
|
+
#endif
|
|
129
140
|
vAlphaCutout = uAlphaCutout;
|
|
130
141
|
#endif
|
|
131
142
|
#if GLASS
|
|
@@ -30,7 +30,16 @@ void anisotropicSurface() {
|
|
|
30
30
|
direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,
|
|
31
31
|
direction.x * uModelParams[0].z + direction.y * uModelParams[0].y
|
|
32
32
|
);
|
|
33
|
-
|
|
33
|
+
vec3 along = mTangent * turned.x + mBitangent * turned.y;
|
|
34
|
+
/*
|
|
35
|
+
* A mesh with neither tangents nor texture coordinates has no frame to take a direction from,
|
|
36
|
+
* and normalising the zero it gives was a NaN that drew the whole surface black. Any direction in
|
|
37
|
+
* the surface stands in, the one across the world's up, so the highlight is stretched somewhere.
|
|
38
|
+
*/
|
|
39
|
+
if (dot(along, along) < 1e-12) {
|
|
40
|
+
along = cross(abs(mNormal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0), mNormal);
|
|
41
|
+
}
|
|
42
|
+
aAlong = normalize(along);
|
|
34
43
|
aAcross = cross(mNormal, aAlong);
|
|
35
44
|
aStrength = strength;
|
|
36
45
|
}
|
|
@@ -50,12 +59,37 @@ float anisotropicLobe(vec3 h, float sourceRadius, float dist) {
|
|
|
50
59
|
return energy / max(d * d, 1e-8);
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The same stretch as GGX's own BRDF, \`π · D · Vis · N·L\`, for a surface whose highlight is
|
|
64
|
+
* physical: the distribution normalised, and Smith's height-correlated masking stretched with it
|
|
65
|
+
* (Heitz 2014, as Filament writes it). \`anisotropicPhysicalLobe\` in anisotropicLobe.ts.
|
|
66
|
+
*/
|
|
67
|
+
float anisotropicPhysical(vec3 l, vec3 h, float ndl, float sourceRadius, float dist) {
|
|
68
|
+
float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);
|
|
69
|
+
float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;
|
|
70
|
+
float grow = sourceRadius / max(2.0 * dist, 1e-3);
|
|
71
|
+
float along = clamp(alongWidth + grow, alongWidth, 1.0);
|
|
72
|
+
float across = clamp(alpha + grow, alpha, 1.0);
|
|
73
|
+
float x = dot(aAlong, h) / along;
|
|
74
|
+
float y = dot(aAcross, h) / across;
|
|
75
|
+
float z = dot(mNormal, h);
|
|
76
|
+
float d = x * x + y * y + z * z;
|
|
77
|
+
float distribution = 1.0 / max(3.14159265 * along * across * d * d, 1e-8);
|
|
78
|
+
float ndv = max(dot(mNormal, mToEye), 1e-4);
|
|
79
|
+
float viewed = ndl * length(vec3(along * dot(aAlong, mToEye), across * dot(aAcross, mToEye), ndv));
|
|
80
|
+
float lit = ndv * length(vec3(along * dot(aAlong, l), across * dot(aAcross, l), ndl));
|
|
81
|
+
return 3.14159265 * distribution * (0.5 / max(viewed + lit, 1e-5)) * ndl;
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
void anisotropicLight(vec3 l, float sourceRadius, float dist) {
|
|
54
85
|
float ndl = max(dot(mNormal, l), 0.0);
|
|
55
86
|
vec3 h = normalize(l + mToEye);
|
|
56
|
-
vec3 tint =
|
|
87
|
+
vec3 tint = highlightTint(mSpecColor, max(dot(mToEye, h), 0.0), mMetal);
|
|
57
88
|
mDiffuse = mAlbedo * ndl * (1.0 - mMetal);
|
|
58
|
-
|
|
89
|
+
float lobe = physicalSpecular()
|
|
90
|
+
? anisotropicPhysical(l, h, ndl, sourceRadius, dist)
|
|
91
|
+
: anisotropicLobe(h, sourceRadius, dist);
|
|
92
|
+
mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
/* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */
|
|
@@ -23,6 +23,52 @@ float areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {
|
|
|
23
23
|
return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's
|
|
28
|
+
* positive edge: \`p\` the fragment's place along the axis, \`height\` its height above the
|
|
29
|
+
* rectangle's plane, \`halfSize\` the rectangle's half extent, and the door's tip \`tipOut\` beyond
|
|
30
|
+
* the edge and \`tipUp\` above the plane. Points of the rectangle past the answer are hidden.
|
|
31
|
+
*
|
|
32
|
+
* Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a
|
|
33
|
+
* fragment beside the door and outside its line is hidden from all of it, and one inside the door's
|
|
34
|
+
* reach is hidden from none of it.
|
|
35
|
+
*/
|
|
36
|
+
float barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {
|
|
37
|
+
float tipAt = halfSize + tipOut;
|
|
38
|
+
if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);
|
|
39
|
+
if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;
|
|
40
|
+
return 1e9;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The part of a rectangle a fragment sees past its barn doors, written into \`centre\` and
|
|
45
|
+
* \`halfSize\` as the smaller rectangle that is left, or false where the doors hide all of it.
|
|
46
|
+
* \`doors\` is the cosine of their angle from the normal and their length; a length of 0 is no doors.
|
|
47
|
+
* Each axis is clipped by its own two doors, which is what makes this four projections and a min.
|
|
48
|
+
*/
|
|
49
|
+
bool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {
|
|
50
|
+
if (doors.y <= 0.0) return true;
|
|
51
|
+
vec3 toFragment = vWorldPos - centre;
|
|
52
|
+
float height = dot(toFragment, cross(right, up));
|
|
53
|
+
/* Behind the emitting side: nothing to clip, and the form factor already answers it. */
|
|
54
|
+
if (height <= 0.0) return true;
|
|
55
|
+
float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));
|
|
56
|
+
float tipUp = doors.y * doors.x;
|
|
57
|
+
vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));
|
|
58
|
+
vec2 hi = vec2(
|
|
59
|
+
min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),
|
|
60
|
+
min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))
|
|
61
|
+
);
|
|
62
|
+
vec2 lo = vec2(
|
|
63
|
+
max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),
|
|
64
|
+
max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))
|
|
65
|
+
);
|
|
66
|
+
if (hi.x <= lo.x || hi.y <= lo.y) return false;
|
|
67
|
+
centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));
|
|
68
|
+
halfSize = 0.5 * (hi - lo);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
26
72
|
/**
|
|
27
73
|
* What one rectangle of colour \`color\` and form factor \`form\` adds to the lamps' two sums, open
|
|
28
74
|
* and shadowed by \`occl\` and \`glass\`.
|
|
@@ -29,7 +29,12 @@ vec4 glassUnmix(vec4 mip, float centreClarity) {
|
|
|
29
29
|
* about which optional code a lit program carries. See `litSwitchesGlsl`.
|
|
30
30
|
*/
|
|
31
31
|
export type LitSwitch =
|
|
32
|
-
|
|
32
|
+
| 'GLASS_SHADOWS'
|
|
33
|
+
| 'CLUSTERED_LIGHTS'
|
|
34
|
+
| 'LIGHT_FIXTURES'
|
|
35
|
+
| 'SURFACE_EFFECTS'
|
|
36
|
+
| 'DRIFT_LIGHT'
|
|
37
|
+
| 'PHYSICAL_SPECULAR';
|
|
33
38
|
|
|
34
39
|
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
35
40
|
export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
@@ -39,6 +44,7 @@ export function noLitSwitches(): Record<LitSwitch, boolean> {
|
|
|
39
44
|
LIGHT_FIXTURES: false,
|
|
40
45
|
SURFACE_EFFECTS: false,
|
|
41
46
|
DRIFT_LIGHT: false,
|
|
47
|
+
PHYSICAL_SPECULAR: false,
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -61,7 +67,8 @@ export interface LitSwitches {
|
|
|
61
67
|
* first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
|
|
62
68
|
* volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
|
|
63
69
|
* Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
|
|
64
|
-
* registers, 4,641 and 96 with all four out.
|
|
70
|
+
* registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
|
|
71
|
+
* models so that theirs keep their ids (`modelsGlsl`).
|
|
65
72
|
*/
|
|
66
73
|
export function litSwitchesGlsl(switches: LitSwitches): string {
|
|
67
74
|
return /* glsl */ `
|
|
@@ -14,7 +14,7 @@ import { CLOTH_BINDING_GLSL } from '../clothBinding.ts';
|
|
|
14
14
|
import { SKINNING_GLSL } from '../skinning.ts';
|
|
15
15
|
import { SURFACE_GLSL } from './surface.ts';
|
|
16
16
|
import { MAIN_GLSL } from './main.ts';
|
|
17
|
-
import {
|
|
17
|
+
import { modelsGlsl } from './models.ts';
|
|
18
18
|
import { AREA_LIGHT_GLSL } from './areaLight.ts';
|
|
19
19
|
import { NORMAL_MAP_GLSL } from './normalMap.ts';
|
|
20
20
|
import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
|
|
@@ -245,6 +245,15 @@ void main() {
|
|
|
245
245
|
#endif
|
|
246
246
|
#if INSTANCED
|
|
247
247
|
mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
|
|
248
|
+
/*
|
|
249
|
+
* **The bottom row is the instance's texture cell** (\`MeshInstances.uvRegions\`), which an
|
|
250
|
+
* affine placement leaves free: read, and rebuilt as the \`[0, 0, 0, 1]\` it stands for.
|
|
251
|
+
*/
|
|
252
|
+
vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
|
|
253
|
+
model[0].w = 0.0;
|
|
254
|
+
model[1].w = 0.0;
|
|
255
|
+
model[2].w = 0.0;
|
|
256
|
+
model[3].w = 1.0;
|
|
248
257
|
vec3 tint = aInstanceTint.rgb;
|
|
249
258
|
if (LIGHTMAP_REGIONS) tint = vec3(1.0);
|
|
250
259
|
#else
|
|
@@ -293,7 +302,12 @@ void main() {
|
|
|
293
302
|
vColor = aColor * tint;
|
|
294
303
|
vEmissive = aEmissive;
|
|
295
304
|
vSpecular = aSpecular;
|
|
305
|
+
#if INSTANCED
|
|
306
|
+
/* The instance's cell first, then the material's scale and offset. */
|
|
307
|
+
vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
|
|
308
|
+
#else
|
|
296
309
|
vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
|
|
310
|
+
#endif
|
|
297
311
|
vEmissiveColor = aEmissiveColor;
|
|
298
312
|
vRoughness = aRoughness;
|
|
299
313
|
vGrain = aGrain;
|
|
@@ -449,6 +463,11 @@ export interface FlatShaderOptions {
|
|
|
449
463
|
readonly surfaceEffects?: boolean;
|
|
450
464
|
/** Whether it reads a DriftLight volume. True when absent. See `litSwitchesGlsl`. */
|
|
451
465
|
readonly driftLight?: boolean;
|
|
466
|
+
/**
|
|
467
|
+
* Whether it carries GGX's own highlight for a material that asks (`physicalSpecular`). True
|
|
468
|
+
* when absent. See `modelsGlsl`.
|
|
469
|
+
*/
|
|
470
|
+
readonly physicalSpecular?: boolean;
|
|
452
471
|
/**
|
|
453
472
|
* How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
|
|
454
473
|
*
|
|
@@ -593,7 +612,7 @@ export function flatFrag(options: FlatShaderOptions): string {
|
|
|
593
612
|
DRIFT_LIGHT_GLSL,
|
|
594
613
|
SURFACE_EFFECTS_GLSL,
|
|
595
614
|
/* Depth zero and after the tangent frame and the lobes it calls; see models.ts. */
|
|
596
|
-
|
|
615
|
+
modelsGlsl(options.physicalSpecular ?? true),
|
|
597
616
|
/* After the models, whose answer to a rectangle it asks for. See areaLight.ts. */
|
|
598
617
|
AREA_LIGHT_GLSL,
|
|
599
618
|
MAIN_GLSL,
|
|
@@ -30,6 +30,55 @@ export const EMISSIVE_SHADOW_SHARE = 0.55;
|
|
|
30
30
|
*/
|
|
31
31
|
export const MIN_LOBE_ALPHA = 0.01;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Karis's fit to the split-sum environment BRDF, as GLSL: the lit stage's, and the screen-space
|
|
35
|
+
* reflection's when a surface asks for its Fresnel (`ReflectiveSurface.fresnel`). One text, so the
|
|
36
|
+
* two cannot drift.
|
|
37
|
+
*/
|
|
38
|
+
export const ENV_BRDF_GLSL = `/**
|
|
39
|
+
* The other half of the split-sum approximation: what fraction of an environment a surface returns.
|
|
40
|
+
*
|
|
41
|
+
* The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
|
|
42
|
+
* Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
|
|
43
|
+
* standing in for an integral.
|
|
44
|
+
*
|
|
45
|
+
* Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
|
|
46
|
+
* unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
|
|
47
|
+
* reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
|
|
48
|
+
* f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
|
|
49
|
+
* does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
|
|
50
|
+
* is the substantive difference: a fully rough dielectric does reflect its surroundings.
|
|
51
|
+
*
|
|
52
|
+
* **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
|
|
53
|
+
* angular dependence; multiplying by a separate Fresnel term would apply it twice.
|
|
54
|
+
*
|
|
55
|
+
* **How much this is worth was measured, and it is worth less than it looks on a dark room.**
|
|
56
|
+
* On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
|
|
57
|
+
* replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
|
|
58
|
+
* of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
|
|
59
|
+
* weight three times larger against a near-zero radiance is still near zero. It is worth much more
|
|
60
|
+
* against a bright environment, which is what a loaded sky is and what no captured room here is.
|
|
61
|
+
*
|
|
62
|
+
* **A warning about reading that page, paid for once.** The ladder falls about five-fold from
|
|
63
|
+
* mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
|
|
64
|
+
* bright wall into a sharp image and a rough surface averages it against five dark ones, so the
|
|
65
|
+
* falloff is the room's own content. The normalised falloff also happens to track one-minus-
|
|
66
|
+
* roughness closely, which made the old term look like the cause — and removing that term entirely
|
|
67
|
+
* changed nothing, which is what settled it. Perturb before believing a match.
|
|
68
|
+
*
|
|
69
|
+
* **What it costs** is a fit rather than the integral: the published error is around one per cent
|
|
70
|
+
* of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
|
|
71
|
+
* is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
|
|
72
|
+
* and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
|
|
73
|
+
*/
|
|
74
|
+
vec2 envBrdfApprox(float ndv, float roughness) {
|
|
75
|
+
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
76
|
+
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
77
|
+
vec4 r = roughness * c0 + c1;
|
|
78
|
+
float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
|
|
79
|
+
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
80
|
+
}`;
|
|
81
|
+
|
|
33
82
|
export const LOBES_GLSL = `float shadowReach(float distance, float maxDistance) {
|
|
34
83
|
return 1.0 - smoothstep(
|
|
35
84
|
maxDistance * ${POINT_SHADOW_FADE_START.toFixed(2)},
|
|
@@ -156,49 +205,7 @@ float sphereLobe(float ndh, float roughness, float sourceRadius, float dist) {
|
|
|
156
205
|
return energy * (w2 * w2) / max(d * d, 1e-8);
|
|
157
206
|
}
|
|
158
207
|
|
|
159
|
-
|
|
160
|
-
* The other half of the split-sum approximation: what fraction of an environment a surface returns.
|
|
161
|
-
*
|
|
162
|
-
* The convolution decides *what* a surface reflects at a given roughness; this decides *how much*.
|
|
163
|
-
* Before it the amount was fresnel times one-minus-roughness, which is a curve somebody chose
|
|
164
|
-
* standing in for an integral.
|
|
165
|
-
*
|
|
166
|
-
* Karis's analytic fit to the environment BRDF, so there is **no lookup texture and no texture
|
|
167
|
-
* unit**: a few multiply-adds against roughness and the view angle. x scales the surface's own
|
|
168
|
-
* reflectance at normal incidence and y is the additive grazing term, so a smooth dielectric at
|
|
169
|
-
* f0 0.04 returns about 0.046 head-on and approaches 1 at the edge, which is what a dielectric
|
|
170
|
-
* does. At roughness 1 it returns about 0.016 rather than the zero one-minus-roughness gave, which
|
|
171
|
-
* is the substantive difference: a fully rough dielectric does reflect its surroundings.
|
|
172
|
-
*
|
|
173
|
-
* **It replaces the Fresnel factor as well as the thinning**, because the fit already contains the
|
|
174
|
-
* angular dependence; multiplying by a separate Fresnel term would apply it twice.
|
|
175
|
-
*
|
|
176
|
-
* **How much this is worth was measured, and it is worth less than it looks on a dark room.**
|
|
177
|
-
* On demo/dev/ibl.html?ladder=1 the whole ladder moved by at most 0.2 of 255 when the old term was
|
|
178
|
-
* replaced. That is not the term failing: at high roughness the surface reads the coarsest levels
|
|
179
|
-
* of the chain, and in a room of five dark walls and one bright one those levels are dark, so a
|
|
180
|
-
* weight three times larger against a near-zero radiance is still near zero. It is worth much more
|
|
181
|
-
* against a bright environment, which is what a loaded sky is and what no captured room here is.
|
|
182
|
-
*
|
|
183
|
-
* **A warning about reading that page, paid for once.** The ladder falls about five-fold from
|
|
184
|
-
* mirror to rough, and that reads as energy being lost. It is not: a mirror concentrates the one
|
|
185
|
-
* bright wall into a sharp image and a rough surface averages it against five dark ones, so the
|
|
186
|
-
* falloff is the room's own content. The normalised falloff also happens to track one-minus-
|
|
187
|
-
* roughness closely, which made the old term look like the cause — and removing that term entirely
|
|
188
|
-
* changed nothing, which is what settled it. Perturb before believing a match.
|
|
189
|
-
*
|
|
190
|
-
* **What it costs** is a fit rather than the integral: the published error is around one per cent
|
|
191
|
-
* of the tabulated term, far below what an eight-bit frame can show. **What would make it wrong**
|
|
192
|
-
* is a material model this shader does not have — a clearcoat or a sheen lobe carries its own BRDF
|
|
193
|
-
* and its own integral, and this fit is Trowbridge-Reitz with Smith masking and nothing else.
|
|
194
|
-
*/
|
|
195
|
-
vec2 envBrdfApprox(float ndv, float roughness) {
|
|
196
|
-
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
|
|
197
|
-
const vec4 c1 = vec4(1.0, 0.0425, 1.04, -0.04);
|
|
198
|
-
vec4 r = roughness * c0 + c1;
|
|
199
|
-
float a004 = min(r.x * r.x, exp2(-9.28 * ndv)) * r.x + r.y;
|
|
200
|
-
return vec2(-1.04, 1.04) * a004 + r.zw;
|
|
201
|
-
}
|
|
208
|
+
${ENV_BRDF_GLSL}
|
|
202
209
|
|
|
203
210
|
/**
|
|
204
211
|
* How much of a prefiltered environment a surface returns, with the light that bounces more than
|