@driftengine/core 4.6.1 → 4.7.1
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 +58 -1
- package/README.md +166 -5
- package/dist/render/backend/budget.d.ts +6 -0
- package/dist/render/backend/budget.js +3 -0
- package/dist/render/backend/webgl2/renderer.d.ts +10 -13
- package/dist/render/backend/webgl2/renderer.js +21 -18
- package/dist/render/backend/webgpu/renderer.d.ts +30 -1
- package/dist/render/backend/webgpu/renderer.js +46 -42
- package/dist/render/pass.d.ts +4 -2
- package/dist/render/renderQuality.d.ts +39 -4
- package/dist/render/renderQuality.js +19 -0
- package/dist/render/rendererHarness.js +9 -1
- package/dist/world/freeze.d.ts +1 -1
- package/package.json +13 -9
- package/scripts/png.d.mts +1 -1
- package/scripts/png.mjs +99 -25
- package/src/render/backend/budget.ts +11 -1
- package/src/render/backend/webgl2/renderer.ts +21 -18
- package/src/render/backend/webgpu/renderer.ts +46 -43
- package/src/render/pass.ts +4 -2
- package/src/render/renderQuality.ts +59 -4
- package/src/render/rendererHarness.ts +8 -1
- package/src/world/freeze.ts +1 -1
package/dist/render/pass.d.ts
CHANGED
|
@@ -34,7 +34,8 @@ import type { FrameResource } from './frame/index.ts';
|
|
|
34
34
|
*/
|
|
35
35
|
export type PassDevice = {
|
|
36
36
|
/**
|
|
37
|
-
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**.
|
|
37
|
+
* Pre-multiply a projection by this **if your WGSL was generated from GLSL**. On WebGL2 it is
|
|
38
|
+
* the depth remap while that context draws reversed depth, and the identity where it cannot.
|
|
38
39
|
*
|
|
39
40
|
* A contributed pass takes its camera from its caller, so it never sees the corrected matrix the
|
|
40
41
|
* renderer built for its own verbs. Without this it would have to copy four numbers — and a
|
|
@@ -55,7 +56,8 @@ export type PassDevice = {
|
|
|
55
56
|
*/
|
|
56
57
|
readonly clipCorrection: Float32Array;
|
|
57
58
|
/**
|
|
58
|
-
* Pre-multiply a projection by this **if you wrote your WGSL by hand**.
|
|
59
|
+
* Pre-multiply a projection by this **if you wrote your WGSL by hand**. On WebGL2 it is the same
|
|
60
|
+
* matrix as `clipCorrection`.
|
|
59
61
|
*
|
|
60
62
|
* The same matrix without the Y negation, because there is no generated negation to cancel.
|
|
61
63
|
* Taking `clipCorrection` in a hand-written shader mirrors the picture vertically — and mirrors
|
|
@@ -88,6 +88,37 @@ export interface RenderQuality {
|
|
|
88
88
|
* halved frame rate rather than a slightly slower one.
|
|
89
89
|
*/
|
|
90
90
|
readonly maxDrawingBufferPixels: number;
|
|
91
|
+
/**
|
|
92
|
+
* How many times a frame may change material, on a backend that keeps a slot for each.
|
|
93
|
+
*
|
|
94
|
+
* **WebGPU spends a slot of a uniform ring per material change and skips every draw past it**,
|
|
95
|
+
* because a buffer rewritten between two draws would hand both of them its last contents. So
|
|
96
|
+
* this is a ceiling on what a frame can show, not a performance hint: a scene that needs more
|
|
97
|
+
* loses whatever it asks for last — measured on a bought city of 1,139 materials, whose street
|
|
98
|
+
* spends some nine hundred and whose river's planar mirror spends the rest twice over.
|
|
99
|
+
*
|
|
100
|
+
* **What it costs is memory, paid at construction**: a slot is the fragment block twice over,
|
|
101
|
+
* staging and buffer, so 7 to 13 KB depending on the permutation built — 1,024 is 7 to 13 MB
|
|
102
|
+
* and 2,048 twice that. Hence a default that suits most scenes and an option for the one that
|
|
103
|
+
* measures past it, rather than a number every consumer pays for. WebGL2 writes each uniform
|
|
104
|
+
* into the command stream and keeps no ring, so there it is read and does nothing, and its
|
|
105
|
+
* budget line reports no ceiling. What would make it wrong is a scene that switches material
|
|
106
|
+
* every draw, which no ring answers: many copies of one mesh want one instanced draw.
|
|
107
|
+
*/
|
|
108
|
+
readonly materialChangesPerFrame: number;
|
|
109
|
+
/**
|
|
110
|
+
* How many draws a frame may make, on a backend that keeps a uniform slot for each — the main
|
|
111
|
+
* pass and the shadow passes each hold this many.
|
|
112
|
+
*
|
|
113
|
+
* **The same shape as `materialChangesPerFrame` and the same failure**: WebGPU skips every draw
|
|
114
|
+
* past the ring. A frame that draws two hundred pays for two hundred whatever this says, so
|
|
115
|
+
* raising it costs no bandwidth; it costs memory at construction, 1,540 bytes a slot — 6 MB at
|
|
116
|
+
* the default 4,096. A city whose street spends three and a half thousand and whose river
|
|
117
|
+
* mirrors that street spends more than the default, and a scene in the ordinary band of two to
|
|
118
|
+
* ten thousand draws can meet it. WebGL2 keeps no ring and reads this without effect. What would
|
|
119
|
+
* make it wrong is a frame that approaches it on the CPU first, which four thousand draws can.
|
|
120
|
+
*/
|
|
121
|
+
readonly drawsPerFrame: number;
|
|
91
122
|
readonly directionalShadows: boolean;
|
|
92
123
|
readonly pointShadows: boolean;
|
|
93
124
|
/**
|
|
@@ -564,10 +595,10 @@ export interface RenderQuality {
|
|
|
564
595
|
* the output size with no upscaling, which is a temporal antialiaser with a better neighbourhood
|
|
565
596
|
* rule than `temporalAa`'s. A sentinel that collided with it would make that unaskable.
|
|
566
597
|
*
|
|
567
|
-
* The range is 1.3 to 2 and a value
|
|
568
|
-
* render saves less than a third of the fragment work and the resolve's own cost
|
|
569
|
-
* the render is a quarter of the output and no reconstruction holds an edge
|
|
570
|
-
* negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
598
|
+
* The range is 1.3 to 2 and a positive value outside it is clamped to the end it is nearest.
|
|
599
|
+
* Below 1.3 the render saves less than a third of the fragment work and the resolve's own cost
|
|
600
|
+
* eats it; above 2 the render is a quarter of the output and no reconstruction holds an edge
|
|
601
|
+
* through that. A negative number is off, not a clamp to the bottom: it cannot mean "a little".
|
|
571
602
|
*
|
|
572
603
|
* **What it needs from you.** A `createMover()` per moving object, passed with that object's
|
|
573
604
|
* draws where `drawMesh` takes last frame's matrix — the renderer keeps last frame's model and,
|
|
@@ -810,6 +841,10 @@ export interface RenderQuality {
|
|
|
810
841
|
/** Callers override only the levers selected by a future settings profile. */
|
|
811
842
|
export type RenderQualityOptions = Partial<RenderQuality>;
|
|
812
843
|
export declare const MAX_SHADOW_FILTER_TAPS = 12;
|
|
844
|
+
/** The most material changes a frame may be built to hold. See `materialChangesPerFrame`. */
|
|
845
|
+
export declare const MAX_MATERIAL_CHANGES_PER_FRAME = 8192;
|
|
846
|
+
/** The most draws a frame may be built to hold. See `drawsPerFrame`. */
|
|
847
|
+
export declare const MAX_DRAWS_PER_FRAME = 65536;
|
|
813
848
|
export declare const MAX_WATER_REFLECTION_FILTER_TAPS = 9;
|
|
814
849
|
export declare const POINT_SHADOW_FADE_START = 0.35;
|
|
815
850
|
export declare const DIRECTIONAL_SHADOW_FADE_START = 0.65;
|
|
@@ -4,6 +4,10 @@ import { MAX_GLOBAL_MEDIUM_STEPS } from './globalMedium.js';
|
|
|
4
4
|
import { MAX_POINT_LIGHTS } from './lightBudget.js';
|
|
5
5
|
import { PREFILTER_SAMPLE_COUNTS } from './prefilterEnvMap.js';
|
|
6
6
|
export const MAX_SHADOW_FILTER_TAPS = 12;
|
|
7
|
+
/** The most material changes a frame may be built to hold. See `materialChangesPerFrame`. */
|
|
8
|
+
export const MAX_MATERIAL_CHANGES_PER_FRAME = 8192;
|
|
9
|
+
/** The most draws a frame may be built to hold. See `drawsPerFrame`. */
|
|
10
|
+
export const MAX_DRAWS_PER_FRAME = 65536;
|
|
7
11
|
export const MAX_WATER_REFLECTION_FILTER_TAPS = 9;
|
|
8
12
|
export const POINT_SHADOW_FADE_START = 0.35;
|
|
9
13
|
export const DIRECTIONAL_SHADOW_FADE_START = 0.65;
|
|
@@ -46,6 +50,8 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
|
|
|
46
50
|
* if it is above where people actually are. Consumers wanting the pixels set it to 0.
|
|
47
51
|
*/
|
|
48
52
|
maxDrawingBufferPixels: 12_000_000,
|
|
53
|
+
materialChangesPerFrame: 1024,
|
|
54
|
+
drawsPerFrame: 4096,
|
|
49
55
|
directionalShadows: true,
|
|
50
56
|
pointShadows: true,
|
|
51
57
|
/* The engine's own budgets, which the renderer lowers per device when it has to. */
|
|
@@ -194,6 +200,8 @@ export function resolveRenderQuality(options = {}) {
|
|
|
194
200
|
pointLightFalloff: options.pointLightFalloff ?? DEFAULT_RENDER_QUALITY.pointLightFalloff,
|
|
195
201
|
maxDevicePixelRatio: options.maxDevicePixelRatio ?? DEFAULT_RENDER_QUALITY.maxDevicePixelRatio,
|
|
196
202
|
maxDrawingBufferPixels: options.maxDrawingBufferPixels ?? DEFAULT_RENDER_QUALITY.maxDrawingBufferPixels,
|
|
203
|
+
materialChangesPerFrame: options.materialChangesPerFrame ?? DEFAULT_RENDER_QUALITY.materialChangesPerFrame,
|
|
204
|
+
drawsPerFrame: options.drawsPerFrame ?? DEFAULT_RENDER_QUALITY.drawsPerFrame,
|
|
197
205
|
directionalShadows: options.directionalShadows ?? DEFAULT_RENDER_QUALITY.directionalShadows,
|
|
198
206
|
pointShadows: options.pointShadows ?? DEFAULT_RENDER_QUALITY.pointShadows,
|
|
199
207
|
/* Whole slots, at least one, never above what the shader is written for: these two are
|
|
@@ -260,6 +268,17 @@ export function resolveRenderQuality(options = {}) {
|
|
|
260
268
|
// Zero is legal and means uncapped, so this is the non-negative check rather than
|
|
261
269
|
// the positive one.
|
|
262
270
|
nonNegative('maxDrawingBufferPixels', quality.maxDrawingBufferPixels);
|
|
271
|
+
positiveInteger('materialChangesPerFrame', quality.materialChangesPerFrame);
|
|
272
|
+
if (quality.materialChangesPerFrame > MAX_MATERIAL_CHANGES_PER_FRAME) {
|
|
273
|
+
throw new Error(`materialChangesPerFrame is ${quality.materialChangesPerFrame}, and at most ` +
|
|
274
|
+
`${MAX_MATERIAL_CHANGES_PER_FRAME} are kept: past that the ring is a hundred megabytes, ` +
|
|
275
|
+
'and a frame changing material that often wants its draws batched instead.');
|
|
276
|
+
}
|
|
277
|
+
positiveInteger('drawsPerFrame', quality.drawsPerFrame);
|
|
278
|
+
if (quality.drawsPerFrame > MAX_DRAWS_PER_FRAME) {
|
|
279
|
+
throw new Error(`drawsPerFrame is ${quality.drawsPerFrame}, and at most ${MAX_DRAWS_PER_FRAME} are kept: ` +
|
|
280
|
+
'past that the rings are a hundred megabytes, and a frame drawing that often wants instancing.');
|
|
281
|
+
}
|
|
263
282
|
positiveInteger('directionalShadowMapSize', quality.directionalShadowMapSize);
|
|
264
283
|
positiveInteger('pointShadowFaceSize', quality.pointShadowFaceSize);
|
|
265
284
|
positiveInteger('shadowFilterTaps', quality.shadowFilterTaps);
|
|
@@ -123,7 +123,15 @@ export function recordingGl(options = {}) {
|
|
|
123
123
|
and a small answer is read as a device that cannot do the thing. */
|
|
124
124
|
return 4096;
|
|
125
125
|
},
|
|
126
|
-
getExtension: (name) =>
|
|
126
|
+
getExtension: (name) => {
|
|
127
|
+
if (!extensions.has(String(name)))
|
|
128
|
+
return null;
|
|
129
|
+
/* Clip control is called as well as asked for, so its object has to carry the call. */
|
|
130
|
+
if (name === 'EXT_clip_control') {
|
|
131
|
+
return { LOWER_LEFT_EXT: 0x8ca1, ZERO_TO_ONE_EXT: 0x935f, clipControlEXT: () => undefined };
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
},
|
|
127
135
|
getShaderParameter: () => true,
|
|
128
136
|
/*
|
|
129
137
|
* A shader remembers its own source, and a program remembers its fragment one, so a link can
|
package/dist/world/freeze.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* What freezing needs of a world.
|
|
24
24
|
*
|
|
25
25
|
* Taken as a parameter rather than imported, on `AGENTS.md`'s rule: core has no entity store and a
|
|
26
|
-
* consumer may be holding one of several.
|
|
26
|
+
* consumer may be holding one of several. Five methods, all of which any store can answer.
|
|
27
27
|
*/
|
|
28
28
|
export interface FreezableWorld {
|
|
29
29
|
/** Every entity whose home is this cell, live or not. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "DriftEngine
|
|
3
|
+
"version": "4.7.1",
|
|
4
|
+
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.7.1",
|
|
33
|
+
"@driftengine/physics": "4.7.1",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "git+https://github.com/drftrun/driftengine.git",
|
|
40
40
|
"directory": "packages/core"
|
|
41
41
|
},
|
|
42
|
-
"homepage": "https://
|
|
42
|
+
"homepage": "https://driftengine.dev/docs",
|
|
43
43
|
"bugs": "https://github.com/drftrun/driftengine/issues",
|
|
44
44
|
"keywords": [
|
|
45
45
|
"driftengine",
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
"webgl2",
|
|
52
52
|
"scene-graph",
|
|
53
53
|
"game-engine",
|
|
54
|
-
"game-loop"
|
|
54
|
+
"game-loop",
|
|
55
|
+
"gamedev",
|
|
56
|
+
"webgpu-game-engine",
|
|
57
|
+
"physics",
|
|
58
|
+
"rigid-body"
|
|
55
59
|
],
|
|
56
60
|
"engines": {
|
|
57
61
|
"node": ">=22.12.0"
|
|
@@ -63,8 +67,8 @@
|
|
|
63
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
|
-
"@driftengine/nav": "4.
|
|
67
|
-
"@driftengine/network": "4.
|
|
68
|
-
"@driftengine/texture": "4.
|
|
70
|
+
"@driftengine/nav": "4.7.1",
|
|
71
|
+
"@driftengine/network": "4.7.1",
|
|
72
|
+
"@driftengine/texture": "4.7.1"
|
|
69
73
|
}
|
|
70
74
|
}
|
package/scripts/png.d.mts
CHANGED
package/scripts/png.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Enough of a PNG reader to compare two frames, with no dependency.
|
|
3
3
|
*
|
|
4
4
|
* A screenshot arrives as 8-bit RGBA, filter method 0, not interlaced, which is what every
|
|
5
|
-
* browser produces. A bought model's maps
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* browser produces, and is read on a path that copies nothing. A bought model's maps arrive as
|
|
6
|
+
* every other non-interlaced kind — grey, grey with alpha, RGB and RGBA at eight or sixteen bits,
|
|
7
|
+
* palettes and greys at one to eight — and the baker and the native host read those through here
|
|
8
|
+
* too, widened to 8-bit samples. Interlacing is refused by name rather than guessed at, because a
|
|
9
|
+
* reader that quietly mis-decodes produces pixel differences that look like a rendering change.
|
|
9
10
|
*
|
|
10
11
|
* `zlib` is Node's own, so this costs nothing to have.
|
|
11
12
|
*/
|
|
@@ -14,14 +15,28 @@ import { readFileSync } from 'node:fs';
|
|
|
14
15
|
|
|
15
16
|
const SIGNATURE = 0x89504e47;
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
+
/** Samples a pixel, by colour type. */
|
|
19
|
+
const SAMPLES = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
20
|
+
/** The bit depths the specification allows for each colour type. */
|
|
21
|
+
const DEPTHS = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `{ width, height, channels, pixels }`, pixels being row-major 8-bit samples.
|
|
25
|
+
*
|
|
26
|
+
* A palette widens to RGB, or to RGBA where it carries `tRNS`; a grey or RGB image with a `tRNS`
|
|
27
|
+
* key gains an alpha channel, nought at that one value; sixteen bits keep the high byte; a grey
|
|
28
|
+
* below eight bits is scaled to the full byte, so one bit is 0 or 255.
|
|
29
|
+
*/
|
|
18
30
|
export function decodePng(bytes) {
|
|
19
31
|
if (bytes.length < 8 || bytes.readUInt32BE(0) !== SIGNATURE) throw new Error('not a PNG');
|
|
20
32
|
|
|
21
33
|
let at = 8;
|
|
22
34
|
let width = 0;
|
|
23
35
|
let height = 0;
|
|
24
|
-
let
|
|
36
|
+
let depth = 0;
|
|
37
|
+
let colorType = 0;
|
|
38
|
+
let palette = null;
|
|
39
|
+
let transparency = null;
|
|
25
40
|
const parts = [];
|
|
26
41
|
|
|
27
42
|
while (at + 8 <= bytes.length) {
|
|
@@ -31,20 +46,22 @@ export function decodePng(bytes) {
|
|
|
31
46
|
if (type === 'IHDR') {
|
|
32
47
|
width = body.readUInt32BE(0);
|
|
33
48
|
height = body.readUInt32BE(4);
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
depth = body[8];
|
|
50
|
+
colorType = body[9];
|
|
36
51
|
const interlace = body[12];
|
|
37
|
-
if (depth !== 8) throw new Error(`bit depth ${depth}, only 8 is handled`);
|
|
38
52
|
if (interlace !== 0) throw new Error('interlaced PNG, not handled');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* single-channel maps are greyscale, and refusing them left a baker's texture cap unable to
|
|
42
|
-
* shrink a third of one scene's maps. A palette needs `PLTE` read, and nothing has asked.
|
|
43
|
-
*/
|
|
44
|
-
channels = { 0: 1, 4: 2, 2: 3, 6: 4 }[colorType] ?? 0;
|
|
45
|
-
if (channels === 0) {
|
|
46
|
-
throw new Error(`colour type ${colorType}, only 0, 2, 4 and 6 are handled`);
|
|
53
|
+
if (SAMPLES[colorType] === undefined) {
|
|
54
|
+
throw new Error(`colour type ${colorType}, which is not a PNG colour type`);
|
|
47
55
|
}
|
|
56
|
+
if (!DEPTHS[colorType].includes(depth)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`bit depth ${depth} for colour type ${colorType}, which PNG does not allow`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
} else if (type === 'PLTE') {
|
|
62
|
+
palette = body;
|
|
63
|
+
} else if (type === 'tRNS') {
|
|
64
|
+
transparency = body;
|
|
48
65
|
} else if (type === 'IDAT') {
|
|
49
66
|
parts.push(body);
|
|
50
67
|
} else if (type === 'IEND') {
|
|
@@ -54,15 +71,21 @@ export function decodePng(bytes) {
|
|
|
54
71
|
at += 12 + length;
|
|
55
72
|
}
|
|
56
73
|
if (width === 0 || height === 0) throw new Error('no IHDR');
|
|
74
|
+
if (colorType === 3 && palette === null) throw new Error('a palette PNG with no PLTE');
|
|
57
75
|
|
|
76
|
+
const samples = SAMPLES[colorType];
|
|
77
|
+
const bits = samples * depth;
|
|
78
|
+
/* What a filter steps back by: a whole pixel, or one byte where a pixel is smaller than one. */
|
|
79
|
+
const step = Math.max(1, bits >> 3);
|
|
80
|
+
const stride = (width * bits + 7) >> 3;
|
|
58
81
|
const raw = inflateSync(Buffer.concat(parts));
|
|
59
|
-
const
|
|
60
|
-
const pixels = Buffer.alloc(stride * height);
|
|
82
|
+
const scan = Buffer.alloc(stride * height);
|
|
61
83
|
|
|
62
84
|
/*
|
|
63
85
|
* Un-filtering, which is the whole of the format that is not zlib. Each row states one of five
|
|
64
|
-
* predictors and the residual against it; `a` is the
|
|
65
|
-
* `c` the one above and left, and all three are zero outside the image rather than wrapped.
|
|
86
|
+
* predictors and the residual against it; `a` is the byte one pixel left, `b` the one above,
|
|
87
|
+
* `c` the one above and left, and all three are zero outside the image rather than wrapped. It
|
|
88
|
+
* runs on the packed bytes, before a sub-byte pixel is unpacked, which is what the format says.
|
|
66
89
|
*/
|
|
67
90
|
for (let y = 0; y < height; y++) {
|
|
68
91
|
const filter = raw[y * (stride + 1)];
|
|
@@ -70,9 +93,9 @@ export function decodePng(bytes) {
|
|
|
70
93
|
const to = y * stride;
|
|
71
94
|
for (let i = 0; i < stride; i++) {
|
|
72
95
|
const value = raw[from + i];
|
|
73
|
-
const a = i >=
|
|
74
|
-
const b = y > 0 ?
|
|
75
|
-
const c = i >=
|
|
96
|
+
const a = i >= step ? scan[to + i - step] : 0;
|
|
97
|
+
const b = y > 0 ? scan[to - stride + i] : 0;
|
|
98
|
+
const c = i >= step && y > 0 ? scan[to - stride + i - step] : 0;
|
|
76
99
|
let restored;
|
|
77
100
|
if (filter === 0) restored = value;
|
|
78
101
|
else if (filter === 1) restored = value + a;
|
|
@@ -80,10 +103,61 @@ export function decodePng(bytes) {
|
|
|
80
103
|
else if (filter === 3) restored = value + ((a + b) >> 1);
|
|
81
104
|
else if (filter === 4) restored = value + paeth(a, b, c);
|
|
82
105
|
else throw new Error(`row ${y} states filter ${filter}, which is not a PNG filter`);
|
|
83
|
-
|
|
106
|
+
scan[to + i] = restored & 0xff;
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
|
|
110
|
+
/* Eight bits of grey, grey and alpha, RGB or RGBA with no key: the scan is the pixels. */
|
|
111
|
+
if (depth === 8 && colorType !== 3 && transparency === null) {
|
|
112
|
+
return { width, height, channels: samples, pixels: scan };
|
|
113
|
+
}
|
|
114
|
+
return widen(scan, width, height, depth, colorType, stride, palette, transparency);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A sample at its own precision: pixel `x`, sample `s` of row `y`. */
|
|
118
|
+
function sampleAt(scan, stride, depth, samples, y, x, s) {
|
|
119
|
+
const index = x * samples + s;
|
|
120
|
+
if (depth === 16) return scan.readUInt16BE(y * stride + index * 2);
|
|
121
|
+
if (depth === 8) return scan[y * stride + index];
|
|
122
|
+
const bit = index * depth;
|
|
123
|
+
return (scan[y * stride + (bit >> 3)] >> (8 - depth - (bit & 7))) & ((1 << depth) - 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Everything the 8-bit path does not take, widened to 8-bit samples. */
|
|
127
|
+
function widen(scan, width, height, depth, colorType, stride, palette, transparency) {
|
|
128
|
+
const samples = SAMPLES[colorType];
|
|
129
|
+
const paletted = colorType === 3;
|
|
130
|
+
/* A key in tRNS is one sample value a channel, at the image's own depth. */
|
|
131
|
+
const keyed = !paletted && transparency !== null && (colorType === 0 || colorType === 2);
|
|
132
|
+
const channels = paletted ? (transparency === null ? 3 : 4) : samples + (keyed ? 1 : 0);
|
|
133
|
+
const key = keyed
|
|
134
|
+
? [0, 1, 2].slice(0, samples).map((s) => transparency.readUInt16BE(s * 2))
|
|
135
|
+
: null;
|
|
136
|
+
const to8 =
|
|
137
|
+
depth === 16 ? (v) => v >> 8 : depth === 8 ? (v) => v : (v) => (v * 255) / ((1 << depth) - 1);
|
|
138
|
+
const entries = paletted ? palette.length / 3 : 0;
|
|
139
|
+
const pixels = Buffer.alloc(width * height * channels);
|
|
140
|
+
let out = 0;
|
|
141
|
+
for (let y = 0; y < height; y++) {
|
|
142
|
+
for (let x = 0; x < width; x++) {
|
|
143
|
+
if (paletted) {
|
|
144
|
+
const index = sampleAt(scan, stride, depth, 1, y, x, 0);
|
|
145
|
+
if (index >= entries) throw new Error(`palette index ${index} past a PLTE of ${entries}`);
|
|
146
|
+
pixels[out++] = palette[index * 3];
|
|
147
|
+
pixels[out++] = palette[index * 3 + 1];
|
|
148
|
+
pixels[out++] = palette[index * 3 + 2];
|
|
149
|
+
if (channels === 4) pixels[out++] = index < transparency.length ? transparency[index] : 255;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
let matches = key !== null;
|
|
153
|
+
for (let s = 0; s < samples; s++) {
|
|
154
|
+
const value = sampleAt(scan, stride, depth, samples, y, x, s);
|
|
155
|
+
if (key !== null && value !== key[s]) matches = false;
|
|
156
|
+
pixels[out++] = to8(value);
|
|
157
|
+
}
|
|
158
|
+
if (keyed) pixels[out++] = matches ? 0 : 255;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
87
161
|
return { width, height, channels, pixels };
|
|
88
162
|
}
|
|
89
163
|
|
|
@@ -49,6 +49,12 @@ export interface BudgetCounter extends BudgetLine {
|
|
|
49
49
|
ask(): void;
|
|
50
50
|
/** One of them was refused, and the frame is marked. */
|
|
51
51
|
drop(): void;
|
|
52
|
+
/**
|
|
53
|
+
* The ceiling, where it is a construction-time option and not known when the line is declared.
|
|
54
|
+
* Called at construction, once; a line keeps the place it was declared in, which is the order a
|
|
55
|
+
* report reads in.
|
|
56
|
+
*/
|
|
57
|
+
limit(ceiling: number): void;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
class Line implements BudgetCounter {
|
|
@@ -57,10 +63,14 @@ class Line implements BudgetCounter {
|
|
|
57
63
|
|
|
58
64
|
constructor(
|
|
59
65
|
readonly name: string,
|
|
60
|
-
|
|
66
|
+
public ceiling: number | null,
|
|
61
67
|
private readonly budget: FrameBudget,
|
|
62
68
|
) {}
|
|
63
69
|
|
|
70
|
+
limit(ceiling: number): void {
|
|
71
|
+
this.ceiling = ceiling;
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/** One more of this thing was asked for, whether or not it will fit. */
|
|
65
75
|
ask(): void {
|
|
66
76
|
this.used += 1;
|
|
@@ -2093,14 +2093,20 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
2093
2093
|
*/
|
|
2094
2094
|
registerPass(definition: PassDefinition): PassHandle {
|
|
2095
2095
|
const handle = registerIn(this.passes, definition);
|
|
2096
|
-
/*
|
|
2097
|
-
|
|
2098
|
-
|
|
2096
|
+
/*
|
|
2097
|
+
* Both the same here: this backend runs the GLSL natively, so no generator has flipped
|
|
2098
|
+
* anything. **What they carry is the depth remap whenever this context draws reversed**, and
|
|
2099
|
+
* they were the identity until a splat capture was drawn among meshes on this backend: the
|
|
2100
|
+
* renderer's own draws go through `GL_DEPTH_REMAP` (`sceneMatrix`), a pass's projection did
|
|
2101
|
+
* not, so a pass wrote conventional depth into a reversed buffer and a splat behind a post was
|
|
2102
|
+
* drawn over it. Identity where clip control is missing and depth stays conventional.
|
|
2103
|
+
*/
|
|
2104
|
+
const correction = this.reversedDepth ? GL_DEPTH_REMAP : IDENTITY_CLIP;
|
|
2099
2105
|
this.passDevice ??= {
|
|
2100
2106
|
backend: 'webgl2',
|
|
2101
2107
|
gl: this.gl,
|
|
2102
|
-
clipCorrection:
|
|
2103
|
-
depthCorrection:
|
|
2108
|
+
clipCorrection: correction,
|
|
2109
|
+
depthCorrection: correction,
|
|
2104
2110
|
};
|
|
2105
2111
|
if (definition.prepare !== undefined) this.preparingPasses++;
|
|
2106
2112
|
definition.init?.(this.passDevice);
|
|
@@ -4746,17 +4752,14 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
4746
4752
|
* `endPlanarReflection`; the next water draw samples the completed target.
|
|
4747
4753
|
*/
|
|
4748
4754
|
/**
|
|
4749
|
-
* **
|
|
4750
|
-
*
|
|
4751
|
-
*
|
|
4752
|
-
*
|
|
4753
|
-
*
|
|
4754
|
-
* into it however it calls this. Reported from outside after being attempted, which is the
|
|
4755
|
-
* cost of a name that promises more than the wiring does.
|
|
4755
|
+
* **Two things sample the target: water, and a wet film.** It is allocated when `water` and
|
|
4756
|
+
* `waterReflections` are both on, or when `RenderQuality.planarReflections` asks for it in a
|
|
4757
|
+
* scene with no water, and `drawFilm` reads it through `FilmOptions.reflectionStrength` for a
|
|
4758
|
+
* film on the plane it was rendered for. This said the pass was water-only after both of those
|
|
4759
|
+
* had landed, which is how a stale limitation outlives its cause: it read as a decision.
|
|
4756
4760
|
*
|
|
4757
|
-
*
|
|
4758
|
-
*
|
|
4759
|
-
* Neither is a substitute for a mirrored pass and this comment is not pretending otherwise.
|
|
4761
|
+
* A surface on any other plane, or that is not a film, reflects a probe
|
|
4762
|
+
* (`bakeReflectionProbe`), which is a room rather than a mirror.
|
|
4760
4763
|
*/
|
|
4761
4764
|
/* Like `bakeReflectionProbe`, this re-enters the **mesh pass only**: anything a scene draws
|
|
4762
4765
|
after the world, particles, light volumes, the sky, is missing from the mirror unless it is
|
|
@@ -8447,9 +8450,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
8447
8450
|
* tarmac at night by bisection twice, at 0.7 and again at 0.24, and landed on **0.15 to
|
|
8448
8451
|
* 0.18**. Start there rather than repeating the search.
|
|
8449
8452
|
*
|
|
8450
|
-
*
|
|
8451
|
-
*
|
|
8452
|
-
*
|
|
8453
|
+
* The mirror is a separate dial: `FilmOptions.reflectionStrength` shows the planar reflection
|
|
8454
|
+
* a frame rendered for the film's plane, weighted by the viewing angle. See
|
|
8455
|
+
* `beginPlanarReflection` for when that target exists.
|
|
8453
8456
|
*/
|
|
8454
8457
|
drawFilm(
|
|
8455
8458
|
mesh: Mesh,
|