@driftengine/core 4.8.1 → 4.8.2
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 +86 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/input/actionMap.d.ts +6 -1
- package/dist/input/actionMap.js +20 -7
- package/dist/input/input.d.ts +28 -0
- package/dist/input/input.js +71 -9
- package/dist/input/inputHarness.d.ts +9 -0
- package/dist/input/inputHarness.js +34 -2
- package/dist/render/backend/webgl2/renderer.d.ts +52 -3
- package/dist/render/backend/webgl2/renderer.js +151 -5
- package/dist/render/backend/webgpu/buffers.d.ts +2 -0
- package/dist/render/backend/webgpu/buffers.js +2 -0
- package/dist/render/backend/webgpu/flatPass.js +16 -7
- package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
- package/dist/render/backend/webgpu/pipelineCache.js +76 -9
- package/dist/render/backend/webgpu/renderer.d.ts +56 -1
- package/dist/render/backend/webgpu/renderer.js +151 -26
- package/dist/render/mapsWithoutUvs.d.ts +24 -0
- package/dist/render/mapsWithoutUvs.js +24 -0
- package/dist/render/mesh.d.ts +2 -0
- package/dist/render/mesh.js +3 -0
- package/dist/render/particlePool.d.ts +15 -0
- package/dist/render/particlePool.js +72 -37
- package/dist/render/pass.d.ts +12 -0
- package/dist/render/renderQuality.d.ts +7 -6
- package/dist/render/shaders/flat/glassTint.d.ts +28 -0
- package/dist/render/shaders/flat/glassTint.js +31 -0
- package/dist/render/shaders/flat/index.d.ts +12 -0
- package/dist/render/shaders/flat/index.js +7 -1
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +60 -25
- package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
- package/dist/render/shaders/flat/surfaceEffects.js +6 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +64 -0
- package/dist/render/shaders/generated/flat.wgsl.js +102 -38
- package/dist/render/surfaceTexture.d.ts +2 -0
- package/dist/render/surfaceTexture.js +4 -0
- package/dist/render/viewModel.d.ts +29 -0
- package/dist/render/viewModel.js +35 -0
- package/package.json +6 -6
- package/src/index.ts +9 -1
- package/src/input/actionMap.ts +23 -8
- package/src/input/input.ts +79 -9
- package/src/input/inputHarness.ts +27 -2
- package/src/render/backend/webgl2/renderer.ts +166 -13
- package/src/render/backend/webgpu/buffers.ts +5 -0
- package/src/render/backend/webgpu/flatPass.ts +41 -25
- package/src/render/backend/webgpu/pipelineCache.ts +82 -8
- package/src/render/backend/webgpu/renderer.ts +151 -26
- package/src/render/mapsWithoutUvs.ts +43 -0
- package/src/render/mesh.ts +3 -0
- package/src/render/particlePool.ts +101 -54
- package/src/render/pass.ts +12 -0
- package/src/render/renderQuality.ts +7 -6
- package/src/render/shaders/flat/glassTint.ts +48 -0
- package/src/render/shaders/flat/index.ts +19 -1
- package/src/render/shaders/flat/main.ts +60 -25
- package/src/render/shaders/flat/surfaceEffects.ts +6 -2
- package/src/render/shaders/generated/flat.wgsl.ts +102 -38
- package/src/render/surfaceTexture.ts +5 -0
- package/src/render/viewModel.ts +41 -0
|
@@ -257,6 +257,9 @@ import type { InstancedOptions, MeshInstances } from '../../instances.ts';
|
|
|
257
257
|
import { cutoutOf } from '../../cutoutCaster.ts';
|
|
258
258
|
import type { CutoutCaster } from '../../cutoutCaster.ts';
|
|
259
259
|
import type { SurfaceTextureHandle } from '../api.ts';
|
|
260
|
+
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
|
|
261
|
+
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
|
|
262
|
+
import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
|
|
260
263
|
|
|
261
264
|
/**
|
|
262
265
|
* How many frames `firstFrameSettled` will ask before it stops asking.
|
|
@@ -1087,8 +1090,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1087
1090
|
|
|
1088
1091
|
private presentedFrameCount = 0;
|
|
1089
1092
|
|
|
1090
|
-
private
|
|
1091
|
-
private
|
|
1093
|
+
private flatProgram: WebGLProgram;
|
|
1094
|
+
private flatUniforms: Record<string, WebGLUniformLocation>;
|
|
1092
1095
|
/**
|
|
1093
1096
|
* The lit fragment source every flat program is built from, and the budget it was built at.
|
|
1094
1097
|
*
|
|
@@ -1099,7 +1102,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1099
1102
|
* for a lazily compiled variant to end up at a different light budget from the one the renderer
|
|
1100
1103
|
* uploads for, which is a picture nobody could explain.
|
|
1101
1104
|
*/
|
|
1102
|
-
private
|
|
1105
|
+
private flatFragSource: string;
|
|
1103
1106
|
/**
|
|
1104
1107
|
* How many lights this renderer's shader has room for, after the device had its say.
|
|
1105
1108
|
*
|
|
@@ -1683,6 +1686,33 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1683
1686
|
* each such draw and handed back, where the other backend bakes it into a pipeline.
|
|
1684
1687
|
*/
|
|
1685
1688
|
private materialDoubleSided = false;
|
|
1689
|
+
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
1690
|
+
private materialHasMaps = false;
|
|
1691
|
+
/** The material last set, so a lit program rebuilt for glass can be given it. */
|
|
1692
|
+
private currentMaterial: SurfaceMaterial | null = null;
|
|
1693
|
+
/**
|
|
1694
|
+
* The lit stage's switches: what the lit programs were compiled with (`litOn`), and what the
|
|
1695
|
+
* profile and the content have asked for since (`litWanted`).
|
|
1696
|
+
*
|
|
1697
|
+
* **Each is off until it is used** — glass until a pane is offered, a fixture until one is loaded,
|
|
1698
|
+
* the effects until a material carries a table, DriftLight until a volume is set — and then on for
|
|
1699
|
+
* good, rebuilt at the next `beginFrame`. Clustering is the profile's. The code costs the lit pass
|
|
1700
|
+
* registers on every surface whether it finds anything or not, and a world without the feature
|
|
1701
|
+
* draws the same picture without it. `PipelineCache.litSwitches` makes the same decision on the
|
|
1702
|
+
* other backend. The frame a feature is first used in draws without it; the rebuild is a
|
|
1703
|
+
* synchronous compile, and the next frame waits for it.
|
|
1704
|
+
*/
|
|
1705
|
+
private readonly litOn = noLitSwitches();
|
|
1706
|
+
private readonly litWanted = noLitSwitches();
|
|
1707
|
+
private litAsked = false;
|
|
1708
|
+
|
|
1709
|
+
/** Ask for a switch, which the next `beginFrame` compiles in. Never glass with it turned off. */
|
|
1710
|
+
private askLit(feature: LitSwitch): void {
|
|
1711
|
+
if (this.litWanted[feature]) return;
|
|
1712
|
+
if (feature === 'GLASS_SHADOWS' && this.quality.glassShadows === 'off') return;
|
|
1713
|
+
this.litWanted[feature] = true;
|
|
1714
|
+
this.litAsked = true;
|
|
1715
|
+
}
|
|
1686
1716
|
|
|
1687
1717
|
/**
|
|
1688
1718
|
* The one sink handed to every caster enumeration.
|
|
@@ -1715,6 +1745,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1715
1745
|
/* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
|
|
1716
1746
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
1717
1747
|
this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
|
|
1748
|
+
this.askLit('GLASS_SHADOWS');
|
|
1718
1749
|
}
|
|
1719
1750
|
return;
|
|
1720
1751
|
}
|
|
@@ -1756,6 +1787,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1756
1787
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
1757
1788
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
1758
1789
|
this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
|
|
1790
|
+
this.askLit('GLASS_SHADOWS');
|
|
1759
1791
|
}
|
|
1760
1792
|
return;
|
|
1761
1793
|
}
|
|
@@ -1789,6 +1821,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
1789
1821
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
1790
1822
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
1791
1823
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
1824
|
+
this.askLit('GLASS_SHADOWS');
|
|
1792
1825
|
}
|
|
1793
1826
|
return;
|
|
1794
1827
|
}
|
|
@@ -2297,6 +2330,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
2297
2330
|
this.iesRows = this.iesAtlas.height;
|
|
2298
2331
|
this.iesPlanes = this.iesAtlas.planes;
|
|
2299
2332
|
this.cookieTiles = this.cookieImages.length;
|
|
2333
|
+
/* The first profile or cookie is when the lit programs start reading them. See `litOn`. */
|
|
2334
|
+
if (this.iesRows > 1 || this.cookieTiles > 0) this.askLit('LIGHT_FIXTURES');
|
|
2300
2335
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
2301
2336
|
gl.activeTexture(gl.TEXTURE0);
|
|
2302
2337
|
}
|
|
@@ -2714,6 +2749,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
2714
2749
|
* Built for the profile this renderer resolved, not for every profile at once — so a
|
|
2715
2750
|
* world with shadows off compiles none of the shadow path. See `shaders/flat.ts`.
|
|
2716
2751
|
*/
|
|
2752
|
+
/* The one switch the profile decides; the rest wait for their content. See `litOn`. */
|
|
2753
|
+
this.litOn.CLUSTERED_LIGHTS = this.quality.clusteredLights;
|
|
2754
|
+
this.litWanted.CLUSTERED_LIGHTS = this.quality.clusteredLights;
|
|
2717
2755
|
const fitted = this.fitFlatProgram(gl);
|
|
2718
2756
|
this.flatProgram = fitted.program;
|
|
2719
2757
|
this.flatFragSource = fitted.source;
|
|
@@ -3306,6 +3344,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3306
3344
|
this.materials.dirty();
|
|
3307
3345
|
this.currentSurfaceTexture = material?.albedo ?? null;
|
|
3308
3346
|
this.materialDoubleSided = material?.doubleSided === true;
|
|
3347
|
+
this.materialHasMaps = materialHasMaps(material);
|
|
3348
|
+
this.currentMaterial = material;
|
|
3309
3349
|
/*
|
|
3310
3350
|
* Written to every flat program, because material state persists across draws and a skinned
|
|
3311
3351
|
* draw is entitled to the material the caller set before it. A second program holding none of
|
|
@@ -3422,6 +3462,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3422
3462
|
gl.bindTexture(gl.TEXTURE_2D, this.emptyTexture2D);
|
|
3423
3463
|
} else {
|
|
3424
3464
|
albedo.bindEffects(gl, SURFACE_EFFECTS_TEXTURE_UNIT, this.emptyTexture2D);
|
|
3465
|
+
/* The first table bound is when the lit programs start reading one. See `litOn`. */
|
|
3466
|
+
if (albedo.hasEffects) this.askLit('SURFACE_EFFECTS');
|
|
3425
3467
|
}
|
|
3426
3468
|
gl.uniform1i(u['uSurfaceEffects'] ?? null, SURFACE_EFFECTS_TEXTURE_UNIT);
|
|
3427
3469
|
if (albedo === null) {
|
|
@@ -5317,21 +5359,91 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
5317
5359
|
* rather than propagating. Only a shader that will not link at *any* budget throws, and it
|
|
5318
5360
|
* throws a sentence with the device's numbers in it rather than a bare driver string.
|
|
5319
5361
|
*/
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
budget: LightBudget;
|
|
5324
|
-
} {
|
|
5325
|
-
const variant = {
|
|
5362
|
+
/** The lit fragment source this renderer's profile compiles, at a light budget. */
|
|
5363
|
+
private flatFragAt(budget: LightBudget): string {
|
|
5364
|
+
return flatFrag({
|
|
5326
5365
|
pointShadows: this.quality.pointShadows,
|
|
5327
5366
|
directionalShadows: this.quality.directionalShadows,
|
|
5328
5367
|
environmentProbe: this.probeCapture !== null,
|
|
5329
5368
|
nightEmissive: this.quality.nightEmissive,
|
|
5330
|
-
/*
|
|
5331
|
-
glassShadows: this.
|
|
5369
|
+
/* Each off until it is used: see `litOn`. */
|
|
5370
|
+
glassShadows: this.litOn.GLASS_SHADOWS,
|
|
5371
|
+
clusteredLights: this.litOn.CLUSTERED_LIGHTS,
|
|
5372
|
+
lightFixtures: this.litOn.LIGHT_FIXTURES,
|
|
5373
|
+
surfaceEffects: this.litOn.SURFACE_EFFECTS,
|
|
5374
|
+
driftLight: this.litOn.DRIFT_LIGHT,
|
|
5375
|
+
maxLights: budget.maxLights,
|
|
5376
|
+
maxAreaLights: budget.maxAreaLights,
|
|
5377
|
+
});
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
/**
|
|
5381
|
+
* Every lit program that exists, compiled again with the switches asked for since, at the budget
|
|
5382
|
+
* the renderer already uploads for, and given the pass and the material the old one held.
|
|
5383
|
+
*
|
|
5384
|
+
* At `beginFrame`, not where a feature was first used: a pane is offered inside a shadow pass
|
|
5385
|
+
* with the depth program bound, and a program swapped under it would draw the next caster with
|
|
5386
|
+
* the lit one.
|
|
5387
|
+
*/
|
|
5388
|
+
private rebuildLit(): void {
|
|
5389
|
+
const { gl } = this;
|
|
5390
|
+
Object.assign(this.litOn, this.litWanted);
|
|
5391
|
+
this.flatFragSource = this.flatFragAt(this.lightBudget);
|
|
5392
|
+
const recompile = (
|
|
5393
|
+
old: WebGLProgram | null,
|
|
5394
|
+
skinned: boolean,
|
|
5395
|
+
morphed: boolean,
|
|
5396
|
+
instanced: boolean,
|
|
5397
|
+
label: string,
|
|
5398
|
+
): { program: WebGLProgram; uniforms: Record<string, WebGLUniformLocation> } | null => {
|
|
5399
|
+
if (old === null) return null;
|
|
5400
|
+
const program = compileProgram(
|
|
5401
|
+
gl,
|
|
5402
|
+
flatVert({ skinned, morphed, instanced }),
|
|
5403
|
+
this.flatFragSource,
|
|
5404
|
+
label,
|
|
5405
|
+
);
|
|
5406
|
+
gl.deleteProgram(old);
|
|
5407
|
+
return { program, uniforms: uniformLocations(gl, program, label) };
|
|
5332
5408
|
};
|
|
5333
|
-
const
|
|
5334
|
-
|
|
5409
|
+
const plain = recompile(this.flatProgram, false, false, false, 'flat');
|
|
5410
|
+
if (plain !== null) {
|
|
5411
|
+
this.flatProgram = plain.program;
|
|
5412
|
+
this.flatUniforms = plain.uniforms;
|
|
5413
|
+
}
|
|
5414
|
+
const skinned = recompile(this.flatSkinnedProgram, true, false, false, 'flat.skinned');
|
|
5415
|
+
this.flatSkinnedProgram = skinned?.program ?? null;
|
|
5416
|
+
this.flatSkinnedUniforms = skinned?.uniforms ?? null;
|
|
5417
|
+
const morphed = recompile(this.flatMorphedProgram, false, true, false, 'flat.morphed');
|
|
5418
|
+
this.flatMorphedProgram = morphed?.program ?? null;
|
|
5419
|
+
this.flatMorphedUniforms = morphed?.uniforms ?? null;
|
|
5420
|
+
const both = recompile(this.flatBothProgram, true, true, false, 'flat.morphedskinned');
|
|
5421
|
+
this.flatBothProgram = both?.program ?? null;
|
|
5422
|
+
this.flatBothUniforms = both?.uniforms ?? null;
|
|
5423
|
+
const instanced = recompile(this.flatInstancedProgram, false, false, true, 'flat.instanced');
|
|
5424
|
+
this.flatInstancedProgram = instanced?.program ?? null;
|
|
5425
|
+
this.flatInstancedUniforms = instanced?.uniforms ?? null;
|
|
5426
|
+
|
|
5427
|
+
this.flatTargets.length = 0;
|
|
5428
|
+
for (const target of [plain, skinned, morphed, both, instanced]) {
|
|
5429
|
+
if (target !== null) this.flatTargets.push(target);
|
|
5430
|
+
}
|
|
5431
|
+
const camera = this.lastPassCamera;
|
|
5432
|
+
const env = this.lastPassEnv;
|
|
5433
|
+
for (const target of this.flatTargets) {
|
|
5434
|
+
gl.useProgram(target.program);
|
|
5435
|
+
if (camera !== null && env !== null) this.writeMeshPassState(target.uniforms, camera, env);
|
|
5436
|
+
this.writeMaterialState(target.uniforms, this.currentMaterial);
|
|
5437
|
+
}
|
|
5438
|
+
this.useFlatProgram();
|
|
5439
|
+
}
|
|
5440
|
+
|
|
5441
|
+
private fitFlatProgram(gl: WebGL2RenderingContext): {
|
|
5442
|
+
program: WebGLProgram;
|
|
5443
|
+
source: string;
|
|
5444
|
+
budget: LightBudget;
|
|
5445
|
+
} {
|
|
5446
|
+
const build = (budget: LightBudget): string => this.flatFragAt(budget);
|
|
5335
5447
|
const vert = flatVert({ skinned: false, morphed: false, instanced: false });
|
|
5336
5448
|
const limit = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS) as number;
|
|
5337
5449
|
const ceiling: LightBudget = {
|
|
@@ -5989,6 +6101,35 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
5989
6101
|
this.restoreViewport();
|
|
5990
6102
|
}
|
|
5991
6103
|
|
|
6104
|
+
/**
|
|
6105
|
+
* Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
|
|
6106
|
+
*
|
|
6107
|
+
* A first-person view model — arms and a held weapon — drawn with the frame's camera, or with one
|
|
6108
|
+
* of its own through `bindMeshPass`, lands in front of anything past the near plane without the
|
|
6109
|
+
* world's depth being cleared, so every pass that reads depth afterwards still sees the world.
|
|
6110
|
+
* `share` is how much of the range it takes, 1% by default. See `viewModel.ts` for what it gives
|
|
6111
|
+
* up, which includes temporal reconstruction: draw a view model with multisampling.
|
|
6112
|
+
*
|
|
6113
|
+
* Not inside an inset: an inset has its own depth, cleared for it, and needs no squeeze.
|
|
6114
|
+
*/
|
|
6115
|
+
beginViewModel(share = VIEW_MODEL_DEPTH_SHARE): void {
|
|
6116
|
+
if (this.contextLost) return;
|
|
6117
|
+
viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
|
|
6118
|
+
this.gl.depthRange(this.viewModelRange[0], this.viewModelRange[1]);
|
|
6119
|
+
this.viewModelActive = true;
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
/** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
|
|
6123
|
+
endViewModel(): void {
|
|
6124
|
+
this.viewModelActive = false;
|
|
6125
|
+
if (this.contextLost) return;
|
|
6126
|
+
this.gl.depthRange(0, 1);
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
/** Whether a view model is being drawn, so `endFrame` can close one the caller left open. */
|
|
6130
|
+
private viewModelActive = false;
|
|
6131
|
+
private readonly viewModelRange: [number, number] = [0, 1];
|
|
6132
|
+
|
|
5992
6133
|
/**
|
|
5993
6134
|
* How much speed blur the frame about to be drawn should resolve with, 0 to 1.
|
|
5994
6135
|
*
|
|
@@ -6508,6 +6649,10 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
6508
6649
|
|
|
6509
6650
|
beginFrame(clearColor: Vec3): void {
|
|
6510
6651
|
if (this.contextLost) return;
|
|
6652
|
+
if (this.litAsked) {
|
|
6653
|
+
this.litAsked = false;
|
|
6654
|
+
this.rebuildLit();
|
|
6655
|
+
}
|
|
6511
6656
|
this.budget.reset();
|
|
6512
6657
|
this.materials.dirty();
|
|
6513
6658
|
/*
|
|
@@ -6584,6 +6729,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
6584
6729
|
*/
|
|
6585
6730
|
endFrame(): void {
|
|
6586
6731
|
if (this.contextLost) return;
|
|
6732
|
+
/* A view model left open would squeeze every pass after the world, the composite included. */
|
|
6733
|
+
if (this.viewModelActive) this.endViewModel();
|
|
6587
6734
|
|
|
6588
6735
|
/*
|
|
6589
6736
|
* Camera motion blur, built from the camera the frame was *rendered* with.
|
|
@@ -7240,6 +7387,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7240
7387
|
gl.bindTexture(gl.TEXTURE_3D, null);
|
|
7241
7388
|
this.driftIndex = index;
|
|
7242
7389
|
this.driftAtlas = atlas;
|
|
7390
|
+
/* The first volume is when the lit programs start reading one. See `litOn`. */
|
|
7391
|
+
this.askLit('DRIFT_LIGHT');
|
|
7243
7392
|
}
|
|
7244
7393
|
|
|
7245
7394
|
/** The field's uniforms and volumes for this pass, or the stand-ins that switch it off. */
|
|
@@ -7563,6 +7712,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7563
7712
|
gl.uniform1f(u['uAlbedoCutout'] ?? null, 0);
|
|
7564
7713
|
gl.uniform1i(u['uDoubleSided'] ?? null, 0);
|
|
7565
7714
|
this.materialDoubleSided = false;
|
|
7715
|
+
this.materialHasMaps = false;
|
|
7716
|
+
this.currentMaterial = null;
|
|
7566
7717
|
this.currentSurfaceTexture = null;
|
|
7567
7718
|
|
|
7568
7719
|
/*
|
|
@@ -7689,6 +7840,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7689
7840
|
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
7690
7841
|
and `Mesh.indexCount`. */
|
|
7691
7842
|
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
7843
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
7692
7844
|
|
|
7693
7845
|
/*
|
|
7694
7846
|
* Skipped where the flag asks and the bounds say so.
|
|
@@ -7877,6 +8029,7 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7877
8029
|
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
7878
8030
|
and `Mesh.indexCount`. */
|
|
7879
8031
|
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8032
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
7880
8033
|
|
|
7881
8034
|
if (opacity <= 0) return;
|
|
7882
8035
|
|
|
@@ -200,6 +200,8 @@ export interface GpuMesh {
|
|
|
200
200
|
* the key is built before the present map is consulted.
|
|
201
201
|
*/
|
|
202
202
|
readonly isSkinned: boolean;
|
|
203
|
+
/** Whether it has texture coordinates, without which a material's maps read one texel. */
|
|
204
|
+
readonly hasUvs: boolean;
|
|
203
205
|
/**
|
|
204
206
|
* Whether this mesh carries the per-vertex channel, which an instanced draw cannot.
|
|
205
207
|
*
|
|
@@ -425,6 +427,7 @@ export function createGpuMeshIncremental(
|
|
|
425
427
|
progress,
|
|
426
428
|
hasTangents: data.tangents !== undefined,
|
|
427
429
|
isSkinned: data.joints !== undefined,
|
|
430
|
+
hasUvs: data.uvs !== undefined,
|
|
428
431
|
skinOffsets: skinOffsetsOf(supplied),
|
|
429
432
|
hasChannel: data.channel !== undefined,
|
|
430
433
|
morph:
|
|
@@ -595,6 +598,7 @@ function gpuMeshOf(parts: {
|
|
|
595
598
|
readonly progress: { readonly uploaded: boolean };
|
|
596
599
|
readonly hasTangents: boolean;
|
|
597
600
|
readonly isSkinned: boolean;
|
|
601
|
+
readonly hasUvs: boolean;
|
|
598
602
|
readonly skinOffsets: GpuMesh['skinOffsets'];
|
|
599
603
|
readonly hasChannel: boolean;
|
|
600
604
|
readonly morph: MorphTexture | null;
|
|
@@ -614,6 +618,7 @@ function gpuMeshOf(parts: {
|
|
|
614
618
|
},
|
|
615
619
|
hasTangents: parts.hasTangents,
|
|
616
620
|
isSkinned: parts.isSkinned,
|
|
621
|
+
hasUvs: parts.hasUvs,
|
|
617
622
|
hasChannel: parts.hasChannel,
|
|
618
623
|
morph: parts.morph,
|
|
619
624
|
update: parts.update,
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
FLAT_VERT_WGSL,
|
|
6
6
|
} from '../../shaders/generated/flat.wgsl.ts';
|
|
7
7
|
import { vertexBufferLayouts } from './buffers.ts';
|
|
8
|
-
import type { PipelineCache } from './pipelineCache.ts';
|
|
8
|
+
import type { LitSwitch, PipelineCache } from './pipelineCache.ts';
|
|
9
9
|
import { shaderModule } from './shaderModules.ts';
|
|
10
10
|
|
|
11
11
|
/** The material maps the flat pass reads: `2d-array` colour, filtered, not shadow maps. */
|
|
@@ -199,17 +199,27 @@ export function flatFragmentBindings(variant: FlatVariant): StageBindings {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* The lit fragment stage's constants, set from the cache
|
|
202
|
+
* The lit fragment stage's constants, set from the cache's switches, keyed by each one's **id**.
|
|
203
203
|
*
|
|
204
204
|
* Not by its name, because an override declared with `@id` answers to the id alone — WebGPU's
|
|
205
205
|
* "pipeline-overridable constant identifier string" — and naga writes one on every specialisation
|
|
206
206
|
* constant. Keyed by name, Chrome refused every lit pipeline: *Pipeline overridable constant
|
|
207
|
-
* "GLASS_SHADOWS" not found*, and the frame drew nothing.
|
|
207
|
+
* "GLASS_SHADOWS" not found*, and the frame drew nothing. Every override the variant declares is
|
|
208
|
+
* set, so a switch added to the shader and not to the cache is refused here rather than defaulted.
|
|
208
209
|
*/
|
|
209
210
|
function flatFragmentConstants(variant: FlatVariant, cache: PipelineCache): Record<string, number> {
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
212
|
-
|
|
211
|
+
const overrides = flatFragmentBindings(variant).overrides;
|
|
212
|
+
if (overrides?.['GLASS_SHADOWS'] === undefined) {
|
|
213
|
+
throw new Error(`flatPass: variant "${variant}" has no glass switch`);
|
|
214
|
+
}
|
|
215
|
+
const constants: Record<string, number> = {};
|
|
216
|
+
for (const [name, id] of Object.entries(overrides)) {
|
|
217
|
+
if (!(name in cache.litSwitches)) {
|
|
218
|
+
throw new Error(`flatPass: variant "${variant}" declares a switch "${name}" no cache sets`);
|
|
219
|
+
}
|
|
220
|
+
constants[String(id)] = cache.litSwitches[name as LitSwitch] ? 1 : 0;
|
|
221
|
+
}
|
|
222
|
+
return constants;
|
|
213
223
|
}
|
|
214
224
|
|
|
215
225
|
/** The depth format both backends compare with. */
|
|
@@ -507,23 +517,26 @@ export function flatPipeline(
|
|
|
507
517
|
/** Culls nothing, for a surface seen from both faces. The key must carry it: `|2s`. */
|
|
508
518
|
doubleSided = false,
|
|
509
519
|
): GPURenderPipeline {
|
|
510
|
-
return cache.get(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
520
|
+
return cache.get(
|
|
521
|
+
key,
|
|
522
|
+
() =>
|
|
523
|
+
flatDescriptor(
|
|
524
|
+
cache,
|
|
525
|
+
device,
|
|
526
|
+
layout,
|
|
527
|
+
variant,
|
|
528
|
+
key,
|
|
529
|
+
present,
|
|
530
|
+
translucent,
|
|
531
|
+
skinned,
|
|
532
|
+
morphed,
|
|
533
|
+
depthWrite,
|
|
534
|
+
depthLayer,
|
|
535
|
+
instanced,
|
|
536
|
+
oit,
|
|
537
|
+
doubleSided,
|
|
538
|
+
),
|
|
539
|
+
true,
|
|
527
540
|
);
|
|
528
541
|
}
|
|
529
542
|
|
|
@@ -549,8 +562,11 @@ export function flatPipelineAsync(
|
|
|
549
562
|
/** Whether this pipeline reads morph deltas. See `FlatVertexOptions`. */
|
|
550
563
|
morphed = false,
|
|
551
564
|
): Promise<GPURenderPipeline> {
|
|
552
|
-
return cache.getAsync(
|
|
553
|
-
|
|
565
|
+
return cache.getAsync(
|
|
566
|
+
key,
|
|
567
|
+
() =>
|
|
568
|
+
flatDescriptor(cache, device, layout, variant, key, present, translucent, skinned, morphed),
|
|
569
|
+
true,
|
|
554
570
|
);
|
|
555
571
|
}
|
|
556
572
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { noLitSwitches, type LitSwitch } from '../../shaders/flat/glassTint.ts';
|
|
2
|
+
|
|
3
|
+
export type { LitSwitch };
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Render pipelines, built once and looked up by name.
|
|
3
7
|
*
|
|
@@ -35,15 +39,30 @@ export class PipelineCache {
|
|
|
35
39
|
readonly sampleCount: number;
|
|
36
40
|
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
42
|
+
* Which of the lit stage's switches the pipelines built now have on: the pipeline-overridable
|
|
43
|
+
* constants the generated lit shader branches on (`GLASS_SHADOWS` and `litSwitchesGlsl`), which
|
|
44
|
+
* the device compiles away where they are off. Read by `flatPass` when it describes a pipeline.
|
|
39
45
|
*
|
|
40
|
-
* **
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
46
|
+
* **Each is code a scene pays for in every lit pixel's registers whether it uses it or not**, so
|
|
47
|
+
* each is off until it is used: glass until the renderer is offered a pane, a fixture until one is
|
|
48
|
+
* loaded, the effects until a material carries a table, DriftLight until a volume is set — and
|
|
49
|
+
* then on for good, through `enable`. Clustering is the profile's, fixed at construction. Every
|
|
50
|
+
* scene paid for all of it from 4.5.0 to 4.8.1, and on a phone that was the lit pass: about 30%
|
|
51
|
+
* of the lit shader's instructions for glass alone, measured on RADV. **What it costs** is the
|
|
52
|
+
* frames between a feature's first use and its recompiled pipelines, which draw without it.
|
|
44
53
|
*/
|
|
54
|
+
readonly litSwitches: Record<LitSwitch, boolean> = noLitSwitches();
|
|
55
|
+
|
|
56
|
+
/** Whether glass may ever be switched on here: `glassShadows` other than `'off'`. */
|
|
45
57
|
readonly glassShadows: boolean;
|
|
46
58
|
|
|
59
|
+
/** How to describe each lit pipeline again, so `enable` can rebuild every one. */
|
|
60
|
+
private readonly litDescribers = new Map<string, () => GPURenderPipelineDescriptor>();
|
|
61
|
+
/** Bumped by every switch, so a lit build started before one is not kept after it. */
|
|
62
|
+
private litGeneration = 0;
|
|
63
|
+
/** The last rebuild, which the next one waits for so they land in the order they were asked. */
|
|
64
|
+
private litRebuild: Promise<void> = Promise.resolve();
|
|
65
|
+
|
|
47
66
|
private readonly device: GPUDevice;
|
|
48
67
|
private readonly pipelines = new Map<string, GPURenderPipeline>();
|
|
49
68
|
/**
|
|
@@ -62,11 +81,19 @@ export class PipelineCache {
|
|
|
62
81
|
*/
|
|
63
82
|
private readonly compiling = new Map<string, Promise<GPURenderPipeline>>();
|
|
64
83
|
|
|
65
|
-
constructor(
|
|
84
|
+
constructor(
|
|
85
|
+
device: GPUDevice,
|
|
86
|
+
format: GPUTextureFormat,
|
|
87
|
+
sampleCount = 1,
|
|
88
|
+
glassShadows = true,
|
|
89
|
+
/** The profile's `clusteredLights`, which compiles the clustered arm in or leaves it out. */
|
|
90
|
+
clusteredLights = false,
|
|
91
|
+
) {
|
|
66
92
|
this.device = device;
|
|
67
93
|
this.format = format;
|
|
68
94
|
this.sampleCount = sampleCount;
|
|
69
95
|
this.glassShadows = glassShadows;
|
|
96
|
+
this.litSwitches.CLUSTERED_LIGHTS = clusteredLights;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
/** How many distinct pipelines have been built. Read by tests and diagnostics. */
|
|
@@ -85,11 +112,15 @@ export class PipelineCache {
|
|
|
85
112
|
async getAsync(
|
|
86
113
|
key: string,
|
|
87
114
|
describe: () => GPURenderPipelineDescriptor,
|
|
115
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
116
|
+
lit = false,
|
|
88
117
|
): Promise<GPURenderPipeline> {
|
|
118
|
+
if (lit) this.litDescribers.set(key, describe);
|
|
89
119
|
const existing = this.pipelines.get(key);
|
|
90
120
|
if (existing !== undefined) return existing;
|
|
91
121
|
const inFlight = this.compiling.get(key);
|
|
92
122
|
if (inFlight !== undefined) return inFlight;
|
|
123
|
+
const generation = this.litGeneration;
|
|
93
124
|
|
|
94
125
|
/*
|
|
95
126
|
* **Falls back where the async form is missing.** It is part of the standard, but a device
|
|
@@ -103,7 +134,8 @@ export class PipelineCache {
|
|
|
103
134
|
? this.device.createRenderPipelineAsync(describe())
|
|
104
135
|
: Promise.resolve(this.device.createRenderPipeline(describe()));
|
|
105
136
|
const promise = build.then((built) => {
|
|
106
|
-
|
|
137
|
+
/* A lit pipeline started before a switch is not kept: the switch rebuilds it. */
|
|
138
|
+
if (!lit || generation === this.litGeneration) this.pipelines.set(key, built);
|
|
107
139
|
this.compiling.delete(key);
|
|
108
140
|
return built;
|
|
109
141
|
});
|
|
@@ -142,7 +174,13 @@ export class PipelineCache {
|
|
|
142
174
|
* too**: a nested object with vertex buffer layouts and blend state, constructed to be
|
|
143
175
|
* thrown away on every hit. Passing a function means a hit costs one `Map` lookup.
|
|
144
176
|
*/
|
|
145
|
-
get(
|
|
177
|
+
get(
|
|
178
|
+
key: string,
|
|
179
|
+
describe: () => GPURenderPipelineDescriptor,
|
|
180
|
+
/** Whether this is a lit pipeline, which `enable` rebuilds. See `litSwitches`. */
|
|
181
|
+
lit = false,
|
|
182
|
+
): GPURenderPipeline {
|
|
183
|
+
if (lit) this.litDescribers.set(key, describe);
|
|
146
184
|
const existing = this.pipelines.get(key);
|
|
147
185
|
if (existing !== undefined) return existing;
|
|
148
186
|
const built = this.device.createRenderPipeline(describe());
|
|
@@ -150,6 +188,42 @@ export class PipelineCache {
|
|
|
150
188
|
return built;
|
|
151
189
|
}
|
|
152
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Turn one of the lit stage's switches on, for every lit pipeline built from now on and every one
|
|
193
|
+
* already built. See `litSwitches`.
|
|
194
|
+
*
|
|
195
|
+
* **The rebuilt set lands at once**, when every one of them has compiled, so no frame draws some
|
|
196
|
+
* surfaces with a feature and others without. Until then the pipelines already built go on drawing
|
|
197
|
+
* as they were. Once only: a switch is not turned off again when the last user of it leaves,
|
|
198
|
+
* because a second switch would cost a second compile for a saving the first use showed was not
|
|
199
|
+
* needed. Glass is never turned on where the profile asked for no glass shadows.
|
|
200
|
+
*/
|
|
201
|
+
enable(feature: LitSwitch): Promise<void> {
|
|
202
|
+
if (this.litSwitches[feature]) return this.litRebuild;
|
|
203
|
+
if (feature === 'GLASS_SHADOWS' && !this.glassShadows) return this.litRebuild;
|
|
204
|
+
this.litSwitches[feature] = true;
|
|
205
|
+
this.litGeneration += 1;
|
|
206
|
+
const generation = this.litGeneration;
|
|
207
|
+
this.litRebuild = this.litRebuild.then(async () => {
|
|
208
|
+
/* A later switch has its own rebuild, which describes every pipeline with this one on too. */
|
|
209
|
+
if (generation !== this.litGeneration) return;
|
|
210
|
+
const rebuilt = [...this.litDescribers].map(([key, describe]) => {
|
|
211
|
+
const descriptor = describe();
|
|
212
|
+
const build =
|
|
213
|
+
typeof this.device.createRenderPipelineAsync === 'function'
|
|
214
|
+
? this.device.createRenderPipelineAsync(descriptor)
|
|
215
|
+
: Promise.resolve(this.device.createRenderPipeline(descriptor));
|
|
216
|
+
return build.then((pipeline) => [key, pipeline] as const);
|
|
217
|
+
});
|
|
218
|
+
const results = await Promise.allSettled(rebuilt);
|
|
219
|
+
if (generation !== this.litGeneration) return;
|
|
220
|
+
for (const result of results) {
|
|
221
|
+
if (result.status === 'fulfilled') this.pipelines.set(result.value[0], result.value[1]);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return this.litRebuild;
|
|
225
|
+
}
|
|
226
|
+
|
|
153
227
|
/**
|
|
154
228
|
* Drop every pipeline.
|
|
155
229
|
*
|