@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
|
@@ -50,6 +50,27 @@ export interface ParticleInstances {
|
|
|
50
50
|
count: number;
|
|
51
51
|
capacity: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
|
|
55
|
+
* order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
|
|
56
|
+
* sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
|
|
57
|
+
* own upload interleaves them. 64 bytes a particle.
|
|
58
|
+
*/
|
|
59
|
+
export declare const DEVICE_PARTICLE_FLOATS = 16;
|
|
60
|
+
/**
|
|
61
|
+
* Particles a caller's own compute shader wrote into a buffer of its own (`registerCompute`), drawn
|
|
62
|
+
* where they lie by `drawDeviceParticles` rather than read back and uploaded again. WebGPU only.
|
|
63
|
+
*
|
|
64
|
+
* **Not sorted**, as a pool's are where its batch asks: the device holds them, so an alpha-blended
|
|
65
|
+
* set that must be drawn far to near sorts itself. A slot with a half-width of 0 draws nothing, which
|
|
66
|
+
* is how a fixed-size buffer holds fewer live particles than it has room for.
|
|
67
|
+
*/
|
|
68
|
+
export interface DeviceParticles {
|
|
69
|
+
/** A buffer with `VERTEX` usage, `DEVICE_PARTICLE_FLOATS` floats a particle. */
|
|
70
|
+
readonly buffer: GPUBuffer;
|
|
71
|
+
/** How many particles from its start to draw; no more than the buffer holds. */
|
|
72
|
+
readonly count: number;
|
|
73
|
+
}
|
|
53
74
|
/**
|
|
54
75
|
* A pool of short-lived particles, simulated on the CPU and drawn as instances.
|
|
55
76
|
*
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { createInstanceData, writeInstance } from './instancedMesh.js';
|
|
2
|
+
/**
|
|
3
|
+
* Floats one particle takes in a buffer a caller's own compute writes (`DeviceParticles`), in this
|
|
4
|
+
* order: position 3, half-width 1, roll 1, colour 3, opacity 1, age 1, seed 1, velocity 3, a
|
|
5
|
+
* sprite's frame 1 and its half-height 1 — `ParticleInstances`' streams, interleaved as the pool's
|
|
6
|
+
* own upload interleaves them. 64 bytes a particle.
|
|
7
|
+
*/
|
|
8
|
+
export const DEVICE_PARTICLE_FLOATS = 16;
|
|
2
9
|
function createParticleInstances(capacity) {
|
|
3
10
|
return {
|
|
4
11
|
positions: new Float32Array(capacity * 3),
|
|
@@ -69,6 +69,14 @@ export interface PointLightSource {
|
|
|
69
69
|
* whose uniform budget has none to spare.
|
|
70
70
|
*/
|
|
71
71
|
inLightField?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* How the light fades with distance when it names its own way: `(1 - (d/R)^2)^n` with no distance
|
|
74
|
+
* term, `R` its radius and `n` this. Unreal's falloff for a light without inverse-square falloff,
|
|
75
|
+
* so a rig authored there — a fill of brightness 160 across 164 m, a key at exponent 8 — lights
|
|
76
|
+
* the space it was authored for rather than one that ends a few metres out. Absent or 0, the
|
|
77
|
+
* frame's own falloff (`pointLightFalloff`), which is every light before 4.8.7.
|
|
78
|
+
*/
|
|
79
|
+
falloffExponent?: number;
|
|
72
80
|
}
|
|
73
81
|
export interface PointLightBuffer {
|
|
74
82
|
readonly positions: Float32Array;
|
|
@@ -126,6 +134,11 @@ export interface PointLightBuffer {
|
|
|
126
134
|
readonly directions: Float32Array;
|
|
127
135
|
/** Two per slot: the cosine of the inner cone angle, then of the outer. */
|
|
128
136
|
readonly coneCos: Float32Array;
|
|
137
|
+
/**
|
|
138
|
+
* One per slot: the light's own falloff exponent, or 0 for the frame's falloff. See
|
|
139
|
+
* `PointLightSource.falloffExponent`.
|
|
140
|
+
*/
|
|
141
|
+
readonly falloffExponents: Float32Array;
|
|
129
142
|
/** One per slot: a row of the photometric atlas, or −1 for a light with no profile. */
|
|
130
143
|
readonly iesProfiles: Float32Array;
|
|
131
144
|
/** Internal insertion-sort storage; exposed only to keep one flat object. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Allocation-free selection and packing of point lights for a fixed shader budget. */
|
|
2
|
-
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER } from './clusteredLights.js';
|
|
2
|
+
import { POINT_LIGHT_COS_INNER, POINT_LIGHT_COS_OUTER, lightFalloffExponentOf, } from './clusteredLights.js';
|
|
3
3
|
import { MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from './lightBudget.js';
|
|
4
4
|
import { gatherLights } from './lightGrid.js';
|
|
5
5
|
export const DEFAULT_POINT_LIGHT_VIEW_RANGE = 180;
|
|
@@ -150,6 +150,7 @@ export function createPointLightBuffer(capacity = MAX_POINT_LIGHTS) {
|
|
|
150
150
|
weights: new Float32Array(shaded),
|
|
151
151
|
directions: new Float32Array(shaded * 3),
|
|
152
152
|
coneCos: new Float32Array(shaded * 2),
|
|
153
|
+
falloffExponents: new Float32Array(shaded),
|
|
153
154
|
/* −1 rather than 0, because row 0 is a real fixture and a light that asked for none must not
|
|
154
155
|
be shaped by whichever profile happened to load first. */
|
|
155
156
|
iesProfiles: new Float32Array(shaded).fill(-1),
|
|
@@ -382,6 +383,7 @@ function choose(sources, visit, visitCount, x, y, z, out, timeSeconds, viewRange
|
|
|
382
383
|
* Restricting it to spots would be a smaller feature for no saving.
|
|
383
384
|
*/
|
|
384
385
|
out.iesProfiles[slot] = light.iesProfile ?? -1;
|
|
386
|
+
out.falloffExponents[slot] = lightFalloffExponentOf(light.falloffExponent);
|
|
385
387
|
}
|
|
386
388
|
out.count = chosen;
|
|
387
389
|
/*
|
|
@@ -209,13 +209,14 @@ export interface RenderQuality {
|
|
|
209
209
|
* **`'pre-integrated'`, the default**: the lit stage's fit alone — a terminator softened and
|
|
210
210
|
* reddened by the surface's curvature, light through thin parts, a penumbra each channel crosses
|
|
211
211
|
* as far as it travels — at no cost beyond the skin pipeline itself. **`'screen-space'`** adds
|
|
212
|
-
* Burley's blur in the picture as well: a skin draw writes its diffuse
|
|
213
|
-
* is drawn
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
212
|
+
* Burley's blur in the picture as well: a skin draw writes the light its diffuse is made of to a
|
|
213
|
+
* target of its own and its colour to another (it is drawn three times to do so), and the light is
|
|
214
|
+
* spread across the pixels its scatter distance covers, multiplied by the colour and added back
|
|
215
|
+
* before anything blended is drawn, so light crosses a shadow's edge and a nostril's rim in the
|
|
216
|
+
* picture rather than in a fit. See `skinBlur.ts`.
|
|
217
|
+
*
|
|
218
|
+
* Construction-time: three half-float targets the size of the frame, and two multisampled ones
|
|
219
|
+
* under multisampling, made at the first skin draw. It needs the composite (`screenEffects`), which is
|
|
219
220
|
* where the frame is still linear light to add to; without one, skin is pre-integrated and the
|
|
220
221
|
* renderer says so once. What would make the default wrong is a close-up face that reads as wax
|
|
221
222
|
* at a shadow's edge — that is what the blur is for.
|
|
@@ -76,6 +76,9 @@ export function recordingGl(options = {}) {
|
|
|
76
76
|
ONE: 1,
|
|
77
77
|
ONE_MINUS_SRC_ALPHA: 0x0303,
|
|
78
78
|
TRIANGLES: 0x0004,
|
|
79
|
+
/* The winding a pass names, so a test can tell which one it chose. */
|
|
80
|
+
CW: 0x0900,
|
|
81
|
+
CCW: 0x0901,
|
|
79
82
|
TEXTURE0: 0x84c0,
|
|
80
83
|
DEPTH_BUFFER_BIT: 0x00000100,
|
|
81
84
|
DEPTH_TEST: 0x0b71,
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
16
|
+
export declare const RGB9E5_MAX: number;
|
|
17
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
18
|
+
export declare function packRgb9e5(red: number, green: number, blue: number): number;
|
|
19
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
20
|
+
export declare function unpackRgb9e5(bits: number, out: {
|
|
21
|
+
[index: number]: number;
|
|
22
|
+
}, at?: number): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const BIAS = 15;
|
|
16
|
+
const MANTISSA_BITS = 9;
|
|
17
|
+
const MANTISSA_VALUES = 1 << MANTISSA_BITS;
|
|
18
|
+
const MAX_EXPONENT = 31;
|
|
19
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
20
|
+
export const RGB9E5_MAX = ((MANTISSA_VALUES - 1) / MANTISSA_VALUES) * 2 ** (MAX_EXPONENT - BIAS);
|
|
21
|
+
const clampChannel = (v) => (v > 0 ? Math.min(v, RGB9E5_MAX) : 0);
|
|
22
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
23
|
+
export function packRgb9e5(red, green, blue) {
|
|
24
|
+
const r = clampChannel(red);
|
|
25
|
+
const g = clampChannel(green);
|
|
26
|
+
const b = clampChannel(blue);
|
|
27
|
+
const largest = Math.max(r, g, b);
|
|
28
|
+
let exponent = Math.max(-BIAS - 1, Math.floor(Math.log2(largest))) + 1 + BIAS;
|
|
29
|
+
let scale = 2 ** (exponent - BIAS - MANTISSA_BITS);
|
|
30
|
+
/* Rounding the largest channel up to 512 needs one more exponent, which the spec allows for. */
|
|
31
|
+
if (Math.floor(largest / scale + 0.5) === MANTISSA_VALUES) {
|
|
32
|
+
exponent += 1;
|
|
33
|
+
scale *= 2;
|
|
34
|
+
}
|
|
35
|
+
/* Inline rather than a helper: a page packs millions of texels, and a closure a call is one each. */
|
|
36
|
+
const mr = Math.min(MANTISSA_VALUES - 1, Math.floor(r / scale + 0.5));
|
|
37
|
+
const mg = Math.min(MANTISSA_VALUES - 1, Math.floor(g / scale + 0.5));
|
|
38
|
+
const mb = Math.min(MANTISSA_VALUES - 1, Math.floor(b / scale + 0.5));
|
|
39
|
+
return (mr | (mg << 9) | (mb << 18) | (exponent << 27)) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
42
|
+
export function unpackRgb9e5(bits, out, at = 0) {
|
|
43
|
+
const scale = 2 ** ((bits >>> 27) - BIAS - MANTISSA_BITS);
|
|
44
|
+
out[at] = (bits & 511) * scale;
|
|
45
|
+
out[at + 1] = ((bits >>> 9) & 511) * scale;
|
|
46
|
+
out[at + 2] = ((bits >>> 18) & 511) * scale;
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/** What a capture's texture is made from, in place of an image: its size and colour format. */
|
|
17
|
+
export interface SceneCaptureTexels {
|
|
18
|
+
readonly kind: 'scene-capture';
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
/** Half floats, which hold light past one; bytes otherwise. */
|
|
22
|
+
readonly float: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
25
|
+
export declare const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
26
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
27
|
+
export declare function sceneCaptureTexels(width: number, height: number, float: boolean): SceneCaptureTexels;
|
|
28
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
29
|
+
export declare function isSceneCaptureTexels(source: unknown): source is SceneCaptureTexels;
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
17
|
+
export const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
18
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
19
|
+
export function sceneCaptureTexels(width, height, float) {
|
|
20
|
+
for (const [name, side] of [
|
|
21
|
+
['width', width],
|
|
22
|
+
['height', height],
|
|
23
|
+
]) {
|
|
24
|
+
if (!Number.isInteger(side) || side < 1 || side > MAX_SCENE_CAPTURE_SIZE) {
|
|
25
|
+
throw new Error(`createSceneCapture: ${name} must be a whole number from 1 to ${MAX_SCENE_CAPTURE_SIZE}, ` +
|
|
26
|
+
`not ${String(side)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { kind: 'scene-capture', width, height, float };
|
|
30
|
+
}
|
|
31
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
32
|
+
export function isSceneCaptureTexels(source) {
|
|
33
|
+
return (typeof source === 'object' &&
|
|
34
|
+
source !== null &&
|
|
35
|
+
source.kind === 'scene-capture');
|
|
36
|
+
}
|
|
@@ -159,9 +159,30 @@ export declare function edgeFade(margin: number, band: number): number;
|
|
|
159
159
|
* nothing at one running directly into it.
|
|
160
160
|
*/
|
|
161
161
|
export declare function towardCameraFade(direction: ReadonlyVec3, viewDirection: ReadonlyVec3): number;
|
|
162
|
+
/**
|
|
163
|
+
* What a `ReflectiveSurface` is made with: its box, which way the surfaces inside it face, and how
|
|
164
|
+
* much and how far they reflect. Every number but the box has a default; see each field.
|
|
165
|
+
*/
|
|
162
166
|
export interface ReflectiveSurfaceOptions extends DecalBox {
|
|
163
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* How much of what the ray finds lands on the surface, 0 to 1: one share over the box, or with
|
|
169
|
+
* `fresnel` the surface's reflectance head-on.
|
|
170
|
+
*/
|
|
164
171
|
readonly strength?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Whether `strength` is the surface's reflectance head-on, F0, rather than one share over the
|
|
174
|
+
* box. Each pixel then returns `F0 · A + B` of what its ray finds, the split-sum environment BRDF
|
|
175
|
+
* at its own view and `roughness` — Karis's fit, the one the lit stage reflects its environment
|
|
176
|
+
* by — so a floor returns more toward the horizon than underfoot, and the share follows the
|
|
177
|
+
* camera as Unreal weighs its reflections. **False by default**, the one share as before.
|
|
178
|
+
*
|
|
179
|
+
* **What it gives up**: the pass has no material buffer, so the roughness and F0 are the box's,
|
|
180
|
+
* not each pixel's, and nothing occludes the reflection where the lit stage's ambient occlusion
|
|
181
|
+
* would. A floor of one material is what it is for.
|
|
182
|
+
*/
|
|
183
|
+
readonly fresnel?: boolean;
|
|
184
|
+
/** The surface's roughness, 0 to 1, that `fresnel` weighs by. 0, a polished surface, if absent. */
|
|
185
|
+
readonly roughness?: number;
|
|
165
186
|
/** How far a ray may travel, world metres. */
|
|
166
187
|
readonly reachM?: number;
|
|
167
188
|
/** How deep a depth sample is treated as being. See `ScreenSpaceMarch.thicknessM`. */
|
|
@@ -189,6 +210,9 @@ export declare class ReflectiveSurface {
|
|
|
189
210
|
readonly axis: Float32Array<ArrayBuffer>;
|
|
190
211
|
readonly tint: Float32Array<ArrayBuffer>;
|
|
191
212
|
strength: number;
|
|
213
|
+
/** See `ReflectiveSurfaceOptions.fresnel`. */
|
|
214
|
+
fresnel: boolean;
|
|
215
|
+
roughness: number;
|
|
192
216
|
reachM: number;
|
|
193
217
|
thicknessM: number;
|
|
194
218
|
steps: number;
|
|
@@ -226,6 +250,8 @@ export interface RecordedReflection {
|
|
|
226
250
|
readonly axis: Float32Array;
|
|
227
251
|
readonly tint: Float32Array;
|
|
228
252
|
strength: number;
|
|
253
|
+
fresnel: boolean;
|
|
254
|
+
roughness: number;
|
|
229
255
|
reachM: number;
|
|
230
256
|
thicknessM: number;
|
|
231
257
|
steps: number;
|
|
@@ -292,6 +292,9 @@ export class ReflectiveSurface {
|
|
|
292
292
|
axis = new Float32Array(3);
|
|
293
293
|
tint = new Float32Array(3);
|
|
294
294
|
strength;
|
|
295
|
+
/** See `ReflectiveSurfaceOptions.fresnel`. */
|
|
296
|
+
fresnel;
|
|
297
|
+
roughness;
|
|
295
298
|
reachM;
|
|
296
299
|
thicknessM;
|
|
297
300
|
steps;
|
|
@@ -308,6 +311,8 @@ export class ReflectiveSurface {
|
|
|
308
311
|
/* Half, because a mirror is the rare case: a wet floor, polished stone and a car bonnet all
|
|
309
312
|
return a fraction of what lands on them, and a consumer who wants a mirror asks for one. */
|
|
310
313
|
this.strength = options.strength ?? 0.5;
|
|
314
|
+
this.fresnel = options.fresnel ?? false;
|
|
315
|
+
this.roughness = options.roughness ?? 0;
|
|
311
316
|
/* Eight metres covers a room and a street corner. Further costs steps rather than accuracy,
|
|
312
317
|
since the step is the reach divided by them. */
|
|
313
318
|
this.reachM = options.reachM ?? 8;
|
|
@@ -366,6 +371,8 @@ function newRecord() {
|
|
|
366
371
|
axis: new Float32Array(3),
|
|
367
372
|
tint: new Float32Array(3),
|
|
368
373
|
strength: 0.5,
|
|
374
|
+
fresnel: false,
|
|
375
|
+
roughness: 0,
|
|
369
376
|
reachM: 8,
|
|
370
377
|
thicknessM: 0.25,
|
|
371
378
|
steps: 24,
|
|
@@ -414,6 +421,8 @@ export class ReflectionQueue {
|
|
|
414
421
|
record.axis.set(surface.axis);
|
|
415
422
|
record.tint.set(surface.tint);
|
|
416
423
|
record.strength = Math.min(1, Math.max(0, surface.strength));
|
|
424
|
+
record.fresnel = surface.fresnel;
|
|
425
|
+
record.roughness = Math.min(1, Math.max(0, surface.roughness));
|
|
417
426
|
record.reachM = Math.max(0, surface.reachM);
|
|
418
427
|
record.thicknessM = Math.max(0, surface.thicknessM);
|
|
419
428
|
record.steps = Math.min(32, Math.max(1, Math.round(surface.steps)));
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
* gives an arc its three-dimensionality is the path, and the path is world-space.
|
|
15
15
|
*/
|
|
16
16
|
export declare const BOLT_VERT = "#version 300 es\nlayout(location = 0) in vec3 aFrom;\nlayout(location = 1) in vec3 aTo;\n/** Along the segment (0 or 1), and which side of it (-1 or +1). */\nlayout(location = 2) in vec2 aCorner;\n/** Where this segment sits along its arc, the arc's envelope, its seed, its gain. */\nlayout(location = 3) in vec4 aArc;\n\nuniform mat4 uViewProj;\nuniform vec3 uCameraPos;\n/** Core half-width in metres, and how much of a pixel a far arc must still cover. */\nuniform float uWidth;\nuniform float uMinWidthPerMetre;\n\nout float vSide;\nout float vAlong;\nout float vFade;\nout float vSeed;\nout float vGain;\nout vec3 vWorldPos;\n\nvoid main() {\n vec3 mid = mix(aFrom, aTo, aCorner.x);\n vec3 dir = aTo - aFrom;\n float len = length(dir);\n dir = len > 1e-6 ? dir / len : vec3(0.0, 1.0, 0.0);\n\n vec3 view = mid - uCameraPos;\n float dist = length(view);\n view = dist > 1e-6 ? view / dist : vec3(0.0, 0.0, 1.0);\n\n /*\n * Across the segment and across the line of sight. Degenerate exactly when the\n * segment points at the viewer, where any perpendicular is as good as another \u2014\n * and where the segment covers almost no screen anyway.\n */\n vec3 across = cross(dir, view);\n float span = length(across);\n across = span > 1e-4 ? across / span : normalize(cross(dir, vec3(0.0, 1.0, 0.0)) + vec3(1e-3));\n\n /*\n * A floor on the world-space width that grows with distance, so a filament\n * never thins below a pixel. Under a pixel a bright thin line does not get\n * fainter, it *flickers* \u2014 the rasteriser catches it on some frames and not\n * others \u2014 and a strobing artefact reads as a bug, not as lightning.\n */\n float halfWidth = max(uWidth, dist * uMinWidthPerMetre);\n vec3 world = mid + across * aCorner.y * halfWidth;\n\n vSide = aCorner.y;\n vAlong = aArc.x;\n vFade = aArc.y;\n vSeed = aArc.z;\n vGain = aArc.w;\n vWorldPos = world;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
|
|
17
|
-
export declare const BOLT_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin float vAlong;\nin float vFade;\nin float vSeed;\nin float vGain;\nin vec3 vWorldPos;\n\nuniform float uTime;\n/** The saturating centre, and the glow it sits inside. */\nuniform vec3 uCoreColor;\nuniform vec3 uEdgeColor;\n/** How far past white the core is allowed to go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\n\nout vec4 outColor;\n\nfloat hash11(float p) {\n return fract(sin(p * 12.9898) * 43758.5453123);\n}\n\nvoid main() {\n /*\n * Across the filament: 1 at the centre line, 0 at the quad's edge. Two powers\n * of it are the entire look \u2014 a very tight one for the core and a loose one for\n * the glow. A single falloff gives either a hard line with no atmosphere or a\n * soft smudge with no filament.\n */\n float across = 1.0 - abs(vSide);\n float core = pow(across, 10.0);\n /*\n * A *wide* halo, deliberately. This exponent is the single biggest control over\n * whether the effect reads as lightning or as a drawn line: a tight falloff gives a\n * crisp filament with nothing around it, which is a wire, while a broad one bleeds\n * light into the air the way an over-exposed discharge does on film. It is the\n * closest thing to bloom available without a second pass.\n */\n float glow = pow(across, 1.1);\n\n /*\n * Stutter. An arc is not a steady light: it is re-established many times a\n * second, and the eye reads a *constant* bright line as neon. Two frequencies\n * so the pattern does not read as a sine, and per-arc phase from the seed so\n * several arcs never blink together.\n */\n /*\n * Stutter, but shallow. An arc is re-established many times a second and a perfectly\n * steady bright line reads as neon \u2014 but too *much* flicker reads as a broken effect\n * rather than as a discharge, and at the brief lifetime these now have, deep flicker\n * mostly hides the bolt. The path itself is already being redrawn (see 'BoltPool'),\n * which carries most of the crackle; this only keeps the brightness alive.\n */\n float t = uTime * 47.0 + vSeed * 17.0;\n float stutter = 0.78 + 0.22 * (hash11(floor(t)) * 0.6 + hash11(floor(t * 2.37)) * 0.4);\n\n /*\n * The tip tapers. An arc that ends in a squared-off quad reads as a drawn\n * segment; thinning toward the far end is what makes it read as a discharge\n * dissipating into the air.\n */\n float taper = 1.0 - vAlong * 0.55;\n\n float energy = vFade * stutter * taper * vGain;\n vec3 lit = uEdgeColor * glow + uCoreColor * core * uCoreGain;\n\n /*\n * Distance dims it rather than tinting it toward the fog. A discharge emits;\n * mixing it into the medium's colour is what a *surface* does, and doing that\n * here turned a far bolt grey instead of faint.\n */\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n /*\n * Alpha is 1, and the energy is already in the colour.\n *\n * The additive blend is 'SRC_ALPHA, ONE', so what reaches the framebuffer is\n * 'rgb * a'. Putting the energy in both \u2014 which the first version did \u2014 squares it,\n * and squaring a number below one is how an effect authored as blinding arrives as\n * faint: at a typical energy of 0.5 it delivered a quarter of what it asked for.\n * Every emissive material here has to pick one channel and mean it.\n */\n outColor = vec4(lit * energy * (1.0 - fog), 1.0);\n}\n";
|
|
17
|
+
export declare const BOLT_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin float vAlong;\nin float vFade;\nin float vSeed;\nin float vGain;\nin vec3 vWorldPos;\n\nuniform float uTime;\n/** The saturating centre, and the glow it sits inside. */\nuniform vec3 uCoreColor;\nuniform vec3 uEdgeColor;\n/** How far past white the core is allowed to go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\n\nout vec4 outColor;\n\nfloat hash11(float p) {\n return fract(sin(p * 12.9898) * 43758.5453123);\n}\n\nvoid main() {\n /*\n * Across the filament: 1 at the centre line, 0 at the quad's edge. Two powers\n * of it are the entire look \u2014 a very tight one for the core and a loose one for\n * the glow. A single falloff gives either a hard line with no atmosphere or a\n * soft smudge with no filament.\n */\n /*\n * Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which\n * may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a\n * negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc\n * the size of the blur. Found by a fight drawing short arcs at 4 samples.\n */\n float across = clamp(1.0 - abs(vSide), 0.0, 1.0);\n float core = pow(across, 10.0);\n /*\n * A *wide* halo, deliberately. This exponent is the single biggest control over\n * whether the effect reads as lightning or as a drawn line: a tight falloff gives a\n * crisp filament with nothing around it, which is a wire, while a broad one bleeds\n * light into the air the way an over-exposed discharge does on film. It is the\n * closest thing to bloom available without a second pass.\n */\n float glow = pow(across, 1.1);\n\n /*\n * Stutter. An arc is not a steady light: it is re-established many times a\n * second, and the eye reads a *constant* bright line as neon. Two frequencies\n * so the pattern does not read as a sine, and per-arc phase from the seed so\n * several arcs never blink together.\n */\n /*\n * Stutter, but shallow. An arc is re-established many times a second and a perfectly\n * steady bright line reads as neon \u2014 but too *much* flicker reads as a broken effect\n * rather than as a discharge, and at the brief lifetime these now have, deep flicker\n * mostly hides the bolt. The path itself is already being redrawn (see 'BoltPool'),\n * which carries most of the crackle; this only keeps the brightness alive.\n */\n float t = uTime * 47.0 + vSeed * 17.0;\n float stutter = 0.78 + 0.22 * (hash11(floor(t)) * 0.6 + hash11(floor(t * 2.37)) * 0.4);\n\n /*\n * The tip tapers. An arc that ends in a squared-off quad reads as a drawn\n * segment; thinning toward the far end is what makes it read as a discharge\n * dissipating into the air.\n */\n float taper = 1.0 - vAlong * 0.55;\n\n float energy = vFade * stutter * taper * vGain;\n vec3 lit = uEdgeColor * glow + uCoreColor * core * uCoreGain;\n\n /*\n * Distance dims it rather than tinting it toward the fog. A discharge emits;\n * mixing it into the medium's colour is what a *surface* does, and doing that\n * here turned a far bolt grey instead of faint.\n */\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n /*\n * Alpha is 1, and the energy is already in the colour.\n *\n * The additive blend is 'SRC_ALPHA, ONE', so what reaches the framebuffer is\n * 'rgb * a'. Putting the energy in both \u2014 which the first version did \u2014 squares it,\n * and squaring a number below one is how an effect authored as blinding arrives as\n * faint: at a typical energy of 0.5 it delivered a quarter of what it asked for.\n * Every emissive material here has to pick one channel and mean it.\n */\n outColor = vec4(lit * energy * (1.0 - fog), 1.0);\n}\n";
|
|
@@ -104,7 +104,13 @@ void main() {
|
|
|
104
104
|
* the glow. A single falloff gives either a hard line with no atmosphere or a
|
|
105
105
|
* soft smudge with no filament.
|
|
106
106
|
*/
|
|
107
|
-
|
|
107
|
+
/*
|
|
108
|
+
* Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which
|
|
109
|
+
* may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a
|
|
110
|
+
* negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc
|
|
111
|
+
* the size of the blur. Found by a fight drawing short arcs at 4 samples.
|
|
112
|
+
*/
|
|
113
|
+
float across = clamp(1.0 - abs(vSide), 0.0, 1.0);
|
|
108
114
|
float core = pow(across, 10.0);
|
|
109
115
|
/*
|
|
110
116
|
* A *wide* halo, deliberately. This exponent is the single biggest control over
|
|
@@ -100,6 +100,12 @@ void main() {
|
|
|
100
100
|
#endif
|
|
101
101
|
#if INSTANCED
|
|
102
102
|
mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
|
|
103
|
+
/* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
|
|
104
|
+
vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
|
|
105
|
+
model[0].w = 0.0;
|
|
106
|
+
model[1].w = 0.0;
|
|
107
|
+
model[2].w = 0.0;
|
|
108
|
+
model[3].w = 1.0;
|
|
103
109
|
#else
|
|
104
110
|
mat4 model = uModel;
|
|
105
111
|
#endif
|
|
@@ -124,7 +130,12 @@ void main() {
|
|
|
124
130
|
vLightPosition = uLightViewProj * vec4(bent, world.w);
|
|
125
131
|
gl_Position = vLightPosition;
|
|
126
132
|
#if CUTOUT
|
|
133
|
+
#if INSTANCED
|
|
134
|
+
/* A cutout instance's shadow is cut by its own cell, as its picture is. */
|
|
135
|
+
vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
|
|
136
|
+
#else
|
|
127
137
|
vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
|
|
138
|
+
#endif
|
|
128
139
|
vAlphaCutout = uAlphaCutout;
|
|
129
140
|
#endif
|
|
130
141
|
#if GLASS
|
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* Lamps widen both of the lobe's widths by the source's angle, as `sphereLobe` widens the standard
|
|
13
13
|
* lobe, with the energy kept; at strength 0 that is `sphereLobe` exactly.
|
|
14
14
|
*/
|
|
15
|
-
export declare const ANISOTROPIC_GLSL = "\n/* The stretch's frame and strength, gathered once with the surface. */\nvec3 aAlong;\nvec3 aAcross;\nfloat aStrength;\n\nvoid anisotropicSurface() {\n vec2 direction = vec2(1.0, 0.0);\n float strength = uModelParams[0].x;\n if (uModelParams[1].w > 0.0) {\n vec2 mapped = mMap.rg * 2.0 - 1.0;\n direction = dot(mapped, mapped) > 1e-8 ? normalize(mapped) : direction;\n strength *= mMap.b;\n }\n vec2 turned = vec2(\n direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,\n direction.x * uModelParams[0].z + direction.y * uModelParams[0].y\n );\n
|
|
15
|
+
export declare const ANISOTROPIC_GLSL = "\n/* The stretch's frame and strength, gathered once with the surface. */\nvec3 aAlong;\nvec3 aAcross;\nfloat aStrength;\n\nvoid anisotropicSurface() {\n vec2 direction = vec2(1.0, 0.0);\n float strength = uModelParams[0].x;\n if (uModelParams[1].w > 0.0) {\n vec2 mapped = mMap.rg * 2.0 - 1.0;\n direction = dot(mapped, mapped) > 1e-8 ? normalize(mapped) : direction;\n strength *= mMap.b;\n }\n vec2 turned = vec2(\n direction.x * uModelParams[0].y - direction.y * uModelParams[0].z,\n direction.x * uModelParams[0].z + direction.y * uModelParams[0].y\n );\n vec3 along = mTangent * turned.x + mBitangent * turned.y;\n /*\n * A mesh with neither tangents nor texture coordinates has no frame to take a direction from,\n * and normalising the zero it gives was a NaN that drew the whole surface black. Any direction in\n * the surface stands in, the one across the world's up, so the highlight is stretched somewhere.\n */\n if (dot(along, along) < 1e-12) {\n along = cross(abs(mNormal.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0), mNormal);\n }\n aAlong = normalize(along);\n aAcross = cross(mNormal, aAlong);\n aStrength = strength;\n}\n\n/** The stretched lobe at a half vector, 1 at the mirror direction: anisotropicLobe.ts. */\nfloat anisotropicLobe(vec3 h, float sourceRadius, float dist) {\n float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);\n float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;\n float grow = sourceRadius / max(2.0 * dist, 1e-3);\n float alongWide = clamp(alongWidth + grow, alongWidth, 1.0);\n float acrossWide = clamp(alpha + grow, alpha, 1.0);\n float energy = (alongWidth / alongWide) * (alpha / acrossWide);\n float x = dot(aAlong, h) / alongWide;\n float y = dot(aAcross, h) / acrossWide;\n float z = dot(mNormal, h);\n float d = x * x + y * y + z * z;\n return energy / max(d * d, 1e-8);\n}\n\n/**\n * The same stretch as GGX's own BRDF, `\u03C0 \u00B7 D \u00B7 Vis \u00B7 N\u00B7L`, for a surface whose highlight is\n * physical: the distribution normalised, and Smith's height-correlated masking stretched with it\n * (Heitz 2014, as Filament writes it). `anisotropicPhysicalLobe` in anisotropicLobe.ts.\n */\nfloat anisotropicPhysical(vec3 l, vec3 h, float ndl, float sourceRadius, float dist) {\n float alpha = max(mRoughness * mRoughness, MIN_LOBE_ALPHA);\n float alongWidth = alpha + (1.0 - alpha) * aStrength * aStrength;\n float grow = sourceRadius / max(2.0 * dist, 1e-3);\n float along = clamp(alongWidth + grow, alongWidth, 1.0);\n float across = clamp(alpha + grow, alpha, 1.0);\n float x = dot(aAlong, h) / along;\n float y = dot(aAcross, h) / across;\n float z = dot(mNormal, h);\n float d = x * x + y * y + z * z;\n float distribution = 1.0 / max(3.14159265 * along * across * d * d, 1e-8);\n float ndv = max(dot(mNormal, mToEye), 1e-4);\n float viewed = ndl * length(vec3(along * dot(aAlong, mToEye), across * dot(aAcross, mToEye), ndv));\n float lit = ndv * length(vec3(along * dot(aAlong, l), across * dot(aAcross, l), ndl));\n return 3.14159265 * distribution * (0.5 / max(viewed + lit, 1e-5)) * ndl;\n}\n\nvoid anisotropicLight(vec3 l, float sourceRadius, float dist) {\n float ndl = max(dot(mNormal, l), 0.0);\n vec3 h = normalize(l + mToEye);\n vec3 tint = highlightTint(mSpecColor, max(dot(mToEye, h), 0.0), mMetal);\n mDiffuse = mAlbedo * ndl * (1.0 - mMetal);\n float lobe = physicalSpecular()\n ? anisotropicPhysical(l, h, ndl, sourceRadius, dist)\n : anisotropicLobe(h, sourceRadius, dist);\n mSpecular = ndl > 0.0 ? tint * lobe : vec3(0.0);\n}\n\n/* The environment's normal, bent toward the stretch by its strength and the surface's roughness. */\nvec3 anisotropicReflectNormal(vec3 n) {\n vec3 across = cross(aAcross, mToEye);\n vec3 bent = cross(across, aAcross);\n float bend = aStrength * clamp(5.0 * mRoughness, 0.0, 1.0);\n return normalize(mix(n, bent, bend));\n}\n";
|
|
@@ -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. */
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Depth zero and after the models, whose `modelLight` it calls; see `flatFrag`.
|
|
9
9
|
*/
|
|
10
|
-
export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (vSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
|
|
10
|
+
export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's\n * positive edge: `p` the fragment's place along the axis, `height` its height above the\n * rectangle's plane, `halfSize` the rectangle's half extent, and the door's tip `tipOut` beyond\n * the edge and `tipUp` above the plane. Points of the rectangle past the answer are hidden.\n *\n * Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a\n * fragment beside the door and outside its line is hidden from all of it, and one inside the door's\n * reach is hidden from none of it.\n */\nfloat barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {\n float tipAt = halfSize + tipOut;\n if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);\n if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;\n return 1e9;\n}\n\n/**\n * The part of a rectangle a fragment sees past its barn doors, written into `centre` and\n * `halfSize` as the smaller rectangle that is left, or false where the doors hide all of it.\n * `doors` is the cosine of their angle from the normal and their length; a length of 0 is no doors.\n * Each axis is clipped by its own two doors, which is what makes this four projections and a min.\n */\nbool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {\n if (doors.y <= 0.0) return true;\n vec3 toFragment = vWorldPos - centre;\n float height = dot(toFragment, cross(right, up));\n /* Behind the emitting side: nothing to clip, and the form factor already answers it. */\n if (height <= 0.0) return true;\n float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));\n float tipUp = doors.y * doors.x;\n vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));\n vec2 hi = vec2(\n min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),\n min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))\n );\n vec2 lo = vec2(\n max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),\n max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))\n );\n if (hi.x <= lo.x || hi.y <= lo.y) return false;\n centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));\n halfSize = 0.5 * (hi - lo);\n return true;\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (vSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
|
|
@@ -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\`.
|
|
@@ -8,7 +8,7 @@ export declare function glassTintGlsl(glassShadows: boolean): string;
|
|
|
8
8
|
* The lit stage's switches, by the name of the constant each is: the decision both backends make
|
|
9
9
|
* about which optional code a lit program carries. See `litSwitchesGlsl`.
|
|
10
10
|
*/
|
|
11
|
-
export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT';
|
|
11
|
+
export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR';
|
|
12
12
|
/** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
|
|
13
13
|
export declare function noLitSwitches(): Record<LitSwitch, boolean>;
|
|
14
14
|
/** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
|
|
@@ -29,6 +29,7 @@ export interface LitSwitches {
|
|
|
29
29
|
* first uses them — a measured profile or a cookie, a material with an effects table, a DriftLight
|
|
30
30
|
* volume — and then on for good; see `PipelineCache.enable` and the WebGL2 renderer's rebuild.
|
|
31
31
|
* Measured on RADV against the lit shader with glass already out: 6,852 instructions and 120
|
|
32
|
-
* registers, 4,641 and 96 with all four out.
|
|
32
|
+
* registers, 4,641 and 96 with all four out. `PHYSICAL_SPECULAR` is a sixth, declared with the
|
|
33
|
+
* models so that theirs keep their ids (`modelsGlsl`).
|
|
33
34
|
*/
|
|
34
35
|
export declare function litSwitchesGlsl(switches: LitSwitches): string;
|