@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
|
@@ -1115,6 +1115,8 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
1115
1115
|
private readonly uvScale;
|
|
1116
1116
|
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
1117
1117
|
private materialDoubleSided;
|
|
1118
|
+
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
1119
|
+
private materialHasMaps;
|
|
1118
1120
|
createSurfaceTexture(source: TexImageSource, options?: SurfaceTextureOptions): GpuSurfaceTexture;
|
|
1119
1121
|
/** Several images of one size as an array's layers. See `RendererApi.createSurfaceTextureArray`. */
|
|
1120
1122
|
createSurfaceTextureArray(sources: readonly TexImageSource[], options?: SurfaceTextureOptions): GpuSurfaceTexture;
|
|
@@ -1834,6 +1836,16 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
1834
1836
|
*/
|
|
1835
1837
|
/** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
|
|
1836
1838
|
private readonly glassCasters;
|
|
1839
|
+
/** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
|
|
1840
|
+
private glassSeen;
|
|
1841
|
+
/**
|
|
1842
|
+
* The first glass caster switches every lit pipeline to reading glass. See
|
|
1843
|
+
* `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
|
|
1844
|
+
* the lit pass on every surface whether they find anything or not.
|
|
1845
|
+
*/
|
|
1846
|
+
private noteGlassCaster;
|
|
1847
|
+
/** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
|
|
1848
|
+
private enableLit;
|
|
1837
1849
|
/** One resolution every glass caster is read through, so recording one allocates nothing. */
|
|
1838
1850
|
private readonly glassCasterScratch;
|
|
1839
1851
|
private readonly casterSink;
|
|
@@ -2944,6 +2956,22 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
2944
2956
|
*/
|
|
2945
2957
|
private canDraw;
|
|
2946
2958
|
private openPass;
|
|
2959
|
+
private openAnyPass;
|
|
2960
|
+
/**
|
|
2961
|
+
* Submit what is drawn after the present on a microtask, which runs before the browser presents.
|
|
2962
|
+
*
|
|
2963
|
+
* There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
|
|
2964
|
+
* presentation happens when the task ends, after the microtask checkpoint. Anything that needs
|
|
2965
|
+
* the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
|
|
2966
|
+
*
|
|
2967
|
+
* **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
|
|
2968
|
+
* With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
|
|
2969
|
+
* queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
|
|
2970
|
+
* `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
|
|
2971
|
+
*/
|
|
2972
|
+
private queueOverlayFlush;
|
|
2973
|
+
/** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
|
|
2974
|
+
private readonly flushOverlayBound;
|
|
2947
2975
|
/**
|
|
2948
2976
|
* The cache whose pipelines match the pass this draw will land in.
|
|
2949
2977
|
*
|
|
@@ -3159,6 +3187,15 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
3159
3187
|
* context is valid for the duration of the call for exactly this reason.
|
|
3160
3188
|
*/
|
|
3161
3189
|
private readonly passContext;
|
|
3190
|
+
/**
|
|
3191
|
+
* Fill the context a contributed pass draws with, for the pass it is about to draw into.
|
|
3192
|
+
*
|
|
3193
|
+
* **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
|
|
3194
|
+
* at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
|
|
3195
|
+
* built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
|
|
3196
|
+
* so the two cannot disagree.
|
|
3197
|
+
*/
|
|
3198
|
+
private fillPassContext;
|
|
3162
3199
|
/**
|
|
3163
3200
|
* Let something outside this file draw into the frame. See `Renderer.registerPass`.
|
|
3164
3201
|
*
|
|
@@ -3313,9 +3350,27 @@ export declare class WebGPURenderer implements RendererApi {
|
|
|
3313
3350
|
* **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
|
|
3314
3351
|
* and cannot confine one to a scissor. See `insetPass.ts`.
|
|
3315
3352
|
*
|
|
3316
|
-
* Returns the
|
|
3353
|
+
* Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
|
|
3354
|
+
* other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
|
|
3355
|
+
* it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
|
|
3356
|
+
* projection of infinities in the caller's camera.
|
|
3317
3357
|
*/
|
|
3318
3358
|
beginInset(rect: InsetRect, clearColor: Vec3 | null): number;
|
|
3359
|
+
/**
|
|
3360
|
+
* Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
|
|
3361
|
+
*
|
|
3362
|
+
* The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
|
|
3363
|
+
* depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
|
|
3364
|
+
* as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
|
|
3365
|
+
*/
|
|
3366
|
+
beginViewModel(share?: number): void;
|
|
3367
|
+
/** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
|
|
3368
|
+
endViewModel(): void;
|
|
3369
|
+
/** The view model's depth bounds on the whole of the pass. */
|
|
3370
|
+
private squeezeViewport;
|
|
3371
|
+
/** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
|
|
3372
|
+
private viewModelActive;
|
|
3373
|
+
private readonly viewModelRange;
|
|
3319
3374
|
/** Give the whole canvas back. Safe to call without a matching `beginInset`. */
|
|
3320
3375
|
endInset(): void;
|
|
3321
3376
|
/**
|
|
@@ -77,6 +77,8 @@ import { resolvePointLights } from '../../lightBudget.js';
|
|
|
77
77
|
import { resolveRenderQuality } from '../../renderQuality.js';
|
|
78
78
|
import { DEFAULT_GLOBAL_MEDIUM, mediumActive, mediumTargetSize, WEAK_GPU_MEDIUM_STEPS, resolveGlobalMedium, } from '../../globalMedium.js';
|
|
79
79
|
import { castsDepth, glassOf } from '../../shadowCasters.js';
|
|
80
|
+
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
|
|
81
|
+
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
|
|
80
82
|
import { GlassCasterList } from '../../glassCasters.js';
|
|
81
83
|
import { GpuGlassTint } from './glassTintPass.js';
|
|
82
84
|
import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
|
|
@@ -2131,6 +2133,8 @@ export class WebGPURenderer {
|
|
|
2131
2133
|
uvScale = new Float32Array([1, 1]);
|
|
2132
2134
|
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2133
2135
|
materialDoubleSided = false;
|
|
2136
|
+
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
2137
|
+
materialHasMaps = false;
|
|
2134
2138
|
createSurfaceTexture(source, options = {}) {
|
|
2135
2139
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
2136
2140
|
}
|
|
@@ -2257,6 +2261,7 @@ export class WebGPURenderer {
|
|
|
2257
2261
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2258
2262
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2259
2263
|
this.materialDoubleSided = material?.doubleSided === true;
|
|
2264
|
+
this.materialHasMaps = materialHasMaps(material);
|
|
2260
2265
|
const normal = (material?.normal ?? null);
|
|
2261
2266
|
const orm = (material?.orm ?? null);
|
|
2262
2267
|
const emissiveMap = (material?.emissive ?? null);
|
|
@@ -4400,6 +4405,29 @@ export class WebGPURenderer {
|
|
|
4400
4405
|
*/
|
|
4401
4406
|
/** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
|
|
4402
4407
|
glassCasters = new GlassCasterList();
|
|
4408
|
+
/** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
|
|
4409
|
+
glassSeen = false;
|
|
4410
|
+
/**
|
|
4411
|
+
* The first glass caster switches every lit pipeline to reading glass. See
|
|
4412
|
+
* `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
|
|
4413
|
+
* the lit pass on every surface whether they find anything or not.
|
|
4414
|
+
*/
|
|
4415
|
+
noteGlassCaster() {
|
|
4416
|
+
if (this.glassSeen)
|
|
4417
|
+
return;
|
|
4418
|
+
this.glassSeen = true;
|
|
4419
|
+
this.enableLit('GLASS_SHADOWS');
|
|
4420
|
+
}
|
|
4421
|
+
/** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
|
|
4422
|
+
enableLit(feature) {
|
|
4423
|
+
if (this.pipelines.litSwitches[feature])
|
|
4424
|
+
return;
|
|
4425
|
+
void this.pipelines.enable(feature);
|
|
4426
|
+
if (this.overlayPipelines !== this.pipelines)
|
|
4427
|
+
void this.overlayPipelines.enable(feature);
|
|
4428
|
+
if (this.latePipelines !== this.pipelines)
|
|
4429
|
+
void this.latePipelines.enable(feature);
|
|
4430
|
+
}
|
|
4403
4431
|
/** One resolution every glass caster is read through, so recording one allocates nothing. */
|
|
4404
4432
|
glassCasterScratch = {
|
|
4405
4433
|
transmission: 0,
|
|
@@ -4412,6 +4440,7 @@ export class WebGPURenderer {
|
|
|
4412
4440
|
/* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
|
|
4413
4441
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
4414
4442
|
this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
|
|
4443
|
+
this.noteGlassCaster();
|
|
4415
4444
|
}
|
|
4416
4445
|
return;
|
|
4417
4446
|
}
|
|
@@ -4451,6 +4480,7 @@ export class WebGPURenderer {
|
|
|
4451
4480
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
4452
4481
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
4453
4482
|
this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
|
|
4483
|
+
this.noteGlassCaster();
|
|
4454
4484
|
}
|
|
4455
4485
|
return;
|
|
4456
4486
|
}
|
|
@@ -4491,6 +4521,7 @@ export class WebGPURenderer {
|
|
|
4491
4521
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
4492
4522
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
4493
4523
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
4524
|
+
this.noteGlassCaster();
|
|
4494
4525
|
}
|
|
4495
4526
|
return;
|
|
4496
4527
|
}
|
|
@@ -4833,7 +4864,7 @@ export class WebGPURenderer {
|
|
|
4833
4864
|
*/
|
|
4834
4865
|
/* Off compiles none of the glass lookup into a lit pipeline: see `PipelineCache.glassShadows`. */
|
|
4835
4866
|
const glassShadows = quality.glassShadows !== 'off';
|
|
4836
|
-
this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples, glassShadows);
|
|
4867
|
+
this.pipelines = new PipelineCache(device, quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format, this.samples, glassShadows, quality.clusteredLights);
|
|
4837
4868
|
/*
|
|
4838
4869
|
* The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
|
|
4839
4870
|
*
|
|
@@ -4845,11 +4876,11 @@ export class WebGPURenderer {
|
|
|
4845
4876
|
this.overlayPipelines =
|
|
4846
4877
|
this.pipelines.format === surface.format && this.samples === 1
|
|
4847
4878
|
? this.pipelines
|
|
4848
|
-
: new PipelineCache(device, surface.format, 1, glassShadows);
|
|
4879
|
+
: new PipelineCache(device, surface.format, 1, glassShadows, quality.clusteredLights);
|
|
4849
4880
|
this.latePipelines =
|
|
4850
4881
|
this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
|
|
4851
4882
|
? this.pipelines
|
|
4852
|
-
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
|
|
4883
|
+
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
|
|
4853
4884
|
this.issueBound = this.issueCommand.bind(this);
|
|
4854
4885
|
this.pipelineTargets =
|
|
4855
4886
|
this.overlayPipelines === this.pipelines
|
|
@@ -5123,7 +5154,11 @@ export class WebGPURenderer {
|
|
|
5123
5154
|
/* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
|
|
5124
5155
|
The albedo is the group's key, so its table needs no key of its own. */
|
|
5125
5156
|
if (name === 'uSurfaceEffects') {
|
|
5126
|
-
|
|
5157
|
+
const effects = this.albedo?.effectsView ?? null;
|
|
5158
|
+
/* The first table bound is when the lit pipelines start reading one. See `litSwitches`. */
|
|
5159
|
+
if (effects !== null)
|
|
5160
|
+
this.enableLit('SURFACE_EFFECTS');
|
|
5161
|
+
return { view: effects ?? blankView, sampler: blankSampler };
|
|
5127
5162
|
}
|
|
5128
5163
|
/*
|
|
5129
5164
|
* The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
|
|
@@ -6215,6 +6250,7 @@ export class WebGPURenderer {
|
|
|
6215
6250
|
i[at('uLightingEnabled')] = 1;
|
|
6216
6251
|
i[at('uFogEnabled')] = 1;
|
|
6217
6252
|
i[at('uAlbedoEnabled')] = 0;
|
|
6253
|
+
this.materialHasMaps = false;
|
|
6218
6254
|
/*
|
|
6219
6255
|
* **The camera medium, which this pass did not upload until 2026-08-29.**
|
|
6220
6256
|
*
|
|
@@ -6707,6 +6743,7 @@ export class WebGPURenderer {
|
|
|
6707
6743
|
*/
|
|
6708
6744
|
if (this.tracksMotionNow())
|
|
6709
6745
|
this.recordDrawMotion(mesh, model, previousModel);
|
|
6746
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
6710
6747
|
this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
|
|
6711
6748
|
}
|
|
6712
6749
|
/**
|
|
@@ -6801,6 +6838,7 @@ export class WebGPURenderer {
|
|
|
6801
6838
|
this.translucentQueue.record(mesh, model, opacity, options);
|
|
6802
6839
|
return;
|
|
6803
6840
|
}
|
|
6841
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
6804
6842
|
/* The tint travels in the options here, where `drawMesh` takes it positionally — see
|
|
6805
6843
|
`TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
|
|
6806
6844
|
this.submitMesh(mesh, model, options.tint ?? null, Math.min(opacity, 1), true, options);
|
|
@@ -10442,6 +10480,8 @@ export class WebGPURenderer {
|
|
|
10442
10480
|
command.instances = 1;
|
|
10443
10481
|
command.indirect = null;
|
|
10444
10482
|
recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
|
|
10483
|
+
if (this.framePresented)
|
|
10484
|
+
this.queueOverlayFlush();
|
|
10445
10485
|
return command;
|
|
10446
10486
|
}
|
|
10447
10487
|
/** Replay one recorded draw into an open pass. */
|
|
@@ -10457,11 +10497,7 @@ export class WebGPURenderer {
|
|
|
10457
10497
|
const definition = passAt(this.passes, nodeState(this.arena, at));
|
|
10458
10498
|
if (definition === undefined)
|
|
10459
10499
|
return;
|
|
10460
|
-
this.
|
|
10461
|
-
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
10462
|
-
this.passContext.outputTransform = this.gradeCode();
|
|
10463
|
-
this.passContext.outputExposure = this.gradeExposure();
|
|
10464
|
-
definition.draw(this.passContext);
|
|
10500
|
+
definition.draw(this.fillPassContext(pass));
|
|
10465
10501
|
return;
|
|
10466
10502
|
}
|
|
10467
10503
|
if (verb !== VERB_DRAW)
|
|
@@ -10558,6 +10594,15 @@ export class WebGPURenderer {
|
|
|
10558
10594
|
return this.framePresented;
|
|
10559
10595
|
}
|
|
10560
10596
|
openPass() {
|
|
10597
|
+
const was = this.pass;
|
|
10598
|
+
const pass = this.openAnyPass();
|
|
10599
|
+
/* A pass opened while a view model is drawn starts with the whole depth range: squeeze it. */
|
|
10600
|
+
if (this.viewModelActive && pass !== null && pass !== was && !this.reflectionPassActive) {
|
|
10601
|
+
this.squeezeViewport(pass);
|
|
10602
|
+
}
|
|
10603
|
+
return pass;
|
|
10604
|
+
}
|
|
10605
|
+
openAnyPass() {
|
|
10561
10606
|
/* While the replay is running this is the only pass there is. See `oitEncoder`. */
|
|
10562
10607
|
if (this.oitEncoder !== null)
|
|
10563
10608
|
return this.oitEncoder;
|
|
@@ -10649,18 +10694,29 @@ export class WebGPURenderer {
|
|
|
10649
10694
|
},
|
|
10650
10695
|
});
|
|
10651
10696
|
this.overlayActive = true;
|
|
10652
|
-
|
|
10653
|
-
* **Submitted on a microtask, which runs before the browser presents.** There is no second
|
|
10654
|
-
* `endFrame` to hang this on — a consumer thinks the frame is over — and presentation happens
|
|
10655
|
-
* when the task ends, after the microtask checkpoint. Anything that needs the pixels sooner
|
|
10656
|
-
* flushes synchronously; `copyRegionTo` is the one that does.
|
|
10657
|
-
*/
|
|
10658
|
-
if (!this.overlayFlushQueued) {
|
|
10659
|
-
this.overlayFlushQueued = true;
|
|
10660
|
-
queueMicrotask(() => this.flushOverlay());
|
|
10661
|
-
}
|
|
10697
|
+
this.queueOverlayFlush();
|
|
10662
10698
|
return this.pass;
|
|
10663
10699
|
}
|
|
10700
|
+
/**
|
|
10701
|
+
* Submit what is drawn after the present on a microtask, which runs before the browser presents.
|
|
10702
|
+
*
|
|
10703
|
+
* There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
|
|
10704
|
+
* presentation happens when the task ends, after the microtask checkpoint. Anything that needs
|
|
10705
|
+
* the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
|
|
10706
|
+
*
|
|
10707
|
+
* **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
|
|
10708
|
+
* With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
|
|
10709
|
+
* queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
|
|
10710
|
+
* `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
|
|
10711
|
+
*/
|
|
10712
|
+
queueOverlayFlush() {
|
|
10713
|
+
if (this.overlayFlushQueued)
|
|
10714
|
+
return;
|
|
10715
|
+
this.overlayFlushQueued = true;
|
|
10716
|
+
queueMicrotask(this.flushOverlayBound);
|
|
10717
|
+
}
|
|
10718
|
+
/** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
|
|
10719
|
+
flushOverlayBound = () => this.flushOverlay();
|
|
10664
10720
|
/**
|
|
10665
10721
|
* The cache whose pipelines match the pass this draw will land in.
|
|
10666
10722
|
*
|
|
@@ -10725,6 +10781,9 @@ export class WebGPURenderer {
|
|
|
10725
10781
|
this.encoder = null;
|
|
10726
10782
|
}
|
|
10727
10783
|
endFrame() {
|
|
10784
|
+
/* A view model left open would squeeze every pass after the world, the composite included. */
|
|
10785
|
+
if (this.viewModelActive)
|
|
10786
|
+
this.endViewModel();
|
|
10728
10787
|
this.inFrame = false;
|
|
10729
10788
|
if (this.surface.lost || this.encoder === null)
|
|
10730
10789
|
return;
|
|
@@ -11148,7 +11207,29 @@ export class WebGPURenderer {
|
|
|
11148
11207
|
outputTransform: 0,
|
|
11149
11208
|
outputExposure: 1,
|
|
11150
11209
|
jitter: this.passJitter,
|
|
11210
|
+
format: 'rgba8unorm',
|
|
11211
|
+
depthFormat: DEPTH_FORMAT,
|
|
11212
|
+
samples: 1,
|
|
11151
11213
|
};
|
|
11214
|
+
/**
|
|
11215
|
+
* Fill the context a contributed pass draws with, for the pass it is about to draw into.
|
|
11216
|
+
*
|
|
11217
|
+
* **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
|
|
11218
|
+
* at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
|
|
11219
|
+
* built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
|
|
11220
|
+
* so the two cannot disagree.
|
|
11221
|
+
*/
|
|
11222
|
+
fillPassContext(pass) {
|
|
11223
|
+
const target = this.targetPipelines();
|
|
11224
|
+
const context = this.passContext;
|
|
11225
|
+
context.pass = pass;
|
|
11226
|
+
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
11227
|
+
context.outputTransform = this.gradeCode();
|
|
11228
|
+
context.outputExposure = this.gradeExposure();
|
|
11229
|
+
context.format = target.format;
|
|
11230
|
+
context.samples = target.sampleCount;
|
|
11231
|
+
return context;
|
|
11232
|
+
}
|
|
11152
11233
|
/**
|
|
11153
11234
|
* Let something outside this file draw into the frame. See `Renderer.registerPass`.
|
|
11154
11235
|
*
|
|
@@ -11286,14 +11367,12 @@ export class WebGPURenderer {
|
|
|
11286
11367
|
const pass = this.openPass();
|
|
11287
11368
|
if (pass === null)
|
|
11288
11369
|
return;
|
|
11289
|
-
this.
|
|
11290
|
-
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
11291
|
-
this.passContext.outputTransform = this.gradeCode();
|
|
11292
|
-
this.passContext.outputExposure = this.gradeExposure();
|
|
11293
|
-
definition.draw(this.passContext);
|
|
11370
|
+
definition.draw(this.fillPassContext(pass));
|
|
11294
11371
|
return;
|
|
11295
11372
|
}
|
|
11296
11373
|
recordNode(this.arena, VERB_PASS, this.passReads.get(handle) ?? 0, this.currentTarget(), handle, 0);
|
|
11374
|
+
if (this.framePresented)
|
|
11375
|
+
this.queueOverlayFlush();
|
|
11297
11376
|
}
|
|
11298
11377
|
/** Let go of a pass. A handle kept past this draws nothing; see `PassHandle`. */
|
|
11299
11378
|
unregisterPass(handle) {
|
|
@@ -11485,7 +11564,10 @@ export class WebGPURenderer {
|
|
|
11485
11564
|
* **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
|
|
11486
11565
|
* and cannot confine one to a scissor. See `insetPass.ts`.
|
|
11487
11566
|
*
|
|
11488
|
-
* Returns the
|
|
11567
|
+
* Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
|
|
11568
|
+
* other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
|
|
11569
|
+
* it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
|
|
11570
|
+
* projection of infinities in the caller's camera.
|
|
11489
11571
|
*/
|
|
11490
11572
|
beginInset(rect, clearColor) {
|
|
11491
11573
|
/*
|
|
@@ -11497,7 +11579,7 @@ export class WebGPURenderer {
|
|
|
11497
11579
|
this.flushGraph();
|
|
11498
11580
|
const pass = this.openPass();
|
|
11499
11581
|
if (this.surface.lost || pass === null)
|
|
11500
|
-
return
|
|
11582
|
+
return Math.max(rect.width, 1) / Math.max(rect.height, 1);
|
|
11501
11583
|
const canvas = this.surface.context.canvas;
|
|
11502
11584
|
const cssWidth = Math.max(canvas.clientWidth, 1);
|
|
11503
11585
|
const cssHeight = Math.max(canvas.clientHeight, 1);
|
|
@@ -11594,6 +11676,44 @@ export class WebGPURenderer {
|
|
|
11594
11676
|
*/
|
|
11595
11677
|
return w / h;
|
|
11596
11678
|
}
|
|
11679
|
+
/**
|
|
11680
|
+
* Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
|
|
11681
|
+
*
|
|
11682
|
+
* The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
|
|
11683
|
+
* depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
|
|
11684
|
+
* as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
|
|
11685
|
+
*/
|
|
11686
|
+
beginViewModel(share = VIEW_MODEL_DEPTH_SHARE) {
|
|
11687
|
+
if (this.quality.frameGraph)
|
|
11688
|
+
this.flushGraph();
|
|
11689
|
+
viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
|
|
11690
|
+
this.viewModelActive = true;
|
|
11691
|
+
const pass = this.openPass();
|
|
11692
|
+
if (this.surface.lost || pass === null)
|
|
11693
|
+
return;
|
|
11694
|
+
this.squeezeViewport(pass);
|
|
11695
|
+
}
|
|
11696
|
+
/** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
|
|
11697
|
+
endViewModel() {
|
|
11698
|
+
if (!this.viewModelActive)
|
|
11699
|
+
return;
|
|
11700
|
+
if (this.quality.frameGraph)
|
|
11701
|
+
this.flushGraph();
|
|
11702
|
+
this.viewModelActive = false;
|
|
11703
|
+
const pass = this.pass;
|
|
11704
|
+
if (this.surface.lost || pass === null)
|
|
11705
|
+
return;
|
|
11706
|
+
const [width, height] = this.passSize();
|
|
11707
|
+
pass.setViewport(0, 0, width, height, 0, 1);
|
|
11708
|
+
}
|
|
11709
|
+
/** The view model's depth bounds on the whole of the pass. */
|
|
11710
|
+
squeezeViewport(pass) {
|
|
11711
|
+
const [width, height] = this.passSize();
|
|
11712
|
+
pass.setViewport(0, 0, width, height, this.viewModelRange[0], this.viewModelRange[1]);
|
|
11713
|
+
}
|
|
11714
|
+
/** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
|
|
11715
|
+
viewModelActive = false;
|
|
11716
|
+
viewModelRange = [0, 1];
|
|
11597
11717
|
/** Give the whole canvas back. Safe to call without a matching `beginInset`. */
|
|
11598
11718
|
endInset() {
|
|
11599
11719
|
/*
|
|
@@ -12663,6 +12783,9 @@ export class WebGPURenderer {
|
|
|
12663
12783
|
this.iesRows = this.iesAtlas.height;
|
|
12664
12784
|
this.iesPlanes = this.iesAtlas.planes;
|
|
12665
12785
|
this.cookieTiles = this.cookieImages.length;
|
|
12786
|
+
/* The first profile or cookie is when the lit pipelines start reading them. See `litSwitches`. */
|
|
12787
|
+
if (this.iesRows > 1 || this.cookieTiles > 0)
|
|
12788
|
+
this.enableLit('LIGHT_FIXTURES');
|
|
12666
12789
|
return this.fixtureView;
|
|
12667
12790
|
}
|
|
12668
12791
|
/**
|
|
@@ -12739,6 +12862,8 @@ export class WebGPURenderer {
|
|
|
12739
12862
|
device.queue.writeTexture({ texture: atlas }, field.atlas, { bytesPerRow: aw * 8, rowsPerImage: ah }, [aw, ah, ad]);
|
|
12740
12863
|
this.driftIndex = index;
|
|
12741
12864
|
this.driftAtlas = atlas;
|
|
12865
|
+
/* The first volume is when the lit pipelines start reading one. See `litSwitches`. */
|
|
12866
|
+
this.enableLit('DRIFT_LIGHT');
|
|
12742
12867
|
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
12743
12868
|
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
12744
12869
|
this.bakeLightField = {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saying, once a mesh, that a material's maps cannot be read on it.
|
|
3
|
+
*
|
|
4
|
+
* **A mesh with no texture coordinates reads one constant coordinate**, so every map bound to it
|
|
5
|
+
* samples a single texel: an emissive map makes nothing glow, a normal map tilts nothing, and the
|
|
6
|
+
* frame is identical pixel for pixel with and without the material's images. Found when an example's
|
|
7
|
+
* burning logs never glowed — `MeshBuilder` writes no coordinates unless `build({ planarUvs: true })`
|
|
8
|
+
* asks — and nothing said why. One decision for both backends, each calling it from its draw.
|
|
9
|
+
*
|
|
10
|
+
* Once per mesh, held weakly: a mesh drawn every frame is said once, and a disposed one is
|
|
11
|
+
* forgotten. **What it gives up** is a material shared between a textured mesh and an untextured one
|
|
12
|
+
* on purpose, which is said once for the untextured one and is then quiet.
|
|
13
|
+
*/
|
|
14
|
+
import type { SurfaceMaterial } from './surfaceTexture.ts';
|
|
15
|
+
/**
|
|
16
|
+
* Whether a material binds a map that is a picture: albedo, normal or emissive.
|
|
17
|
+
*
|
|
18
|
+
* **Not ORM**, because a one-colour ORM map is how a draw sets a constant roughness and metalness,
|
|
19
|
+
* and read at one texel it is exactly that constant — the materials example draws its spheres so.
|
|
20
|
+
* The three counted here are pictures nobody binds to be read at one point.
|
|
21
|
+
*/
|
|
22
|
+
export declare function materialHasMaps(material: SurfaceMaterial<unknown> | null): boolean;
|
|
23
|
+
/** Warn the first time `mesh`, which has no coordinates, is drawn with a picture map bound. */
|
|
24
|
+
export declare function noteMapsWithoutUvs(mesh: object, hasUvs: boolean, materialHasMaps: boolean): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const told = new WeakSet();
|
|
2
|
+
/**
|
|
3
|
+
* Whether a material binds a map that is a picture: albedo, normal or emissive.
|
|
4
|
+
*
|
|
5
|
+
* **Not ORM**, because a one-colour ORM map is how a draw sets a constant roughness and metalness,
|
|
6
|
+
* and read at one texel it is exactly that constant — the materials example draws its spheres so.
|
|
7
|
+
* The three counted here are pictures nobody binds to be read at one point.
|
|
8
|
+
*/
|
|
9
|
+
export function materialHasMaps(material) {
|
|
10
|
+
if (material === null)
|
|
11
|
+
return false;
|
|
12
|
+
return ((material.albedo ?? null) !== null ||
|
|
13
|
+
(material.normal ?? null) !== null ||
|
|
14
|
+
(material.emissive ?? null) !== null);
|
|
15
|
+
}
|
|
16
|
+
/** Warn the first time `mesh`, which has no coordinates, is drawn with a picture map bound. */
|
|
17
|
+
export function noteMapsWithoutUvs(mesh, hasUvs, materialHasMaps) {
|
|
18
|
+
if (hasUvs || !materialHasMaps || told.has(mesh))
|
|
19
|
+
return;
|
|
20
|
+
told.add(mesh);
|
|
21
|
+
console.warn('[driftengine] a mesh with no texture coordinates is drawn with an albedo, normal or emissive ' +
|
|
22
|
+
'map, which it cannot read: every map samples one texel. Give the mesh `uvs` — ' +
|
|
23
|
+
'`MeshBuilder.build({ planarUvs: true })` writes them — or draw it without the maps.');
|
|
24
|
+
}
|
package/dist/render/mesh.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare class Mesh {
|
|
|
56
56
|
readonly bounds: Bounds;
|
|
57
57
|
/** Whether this mesh carries a rig, which decides which flat program draws it. */
|
|
58
58
|
readonly isSkinned: boolean;
|
|
59
|
+
/** Whether it has texture coordinates, without which a material's maps read one texel. */
|
|
60
|
+
readonly hasUvs: boolean;
|
|
59
61
|
/**
|
|
60
62
|
* This mesh's morph deltas, or null for geometry that does not deform.
|
|
61
63
|
*
|
package/dist/render/mesh.js
CHANGED
|
@@ -123,6 +123,8 @@ export class Mesh {
|
|
|
123
123
|
bounds = createBounds();
|
|
124
124
|
/** Whether this mesh carries a rig, which decides which flat program draws it. */
|
|
125
125
|
isSkinned;
|
|
126
|
+
/** Whether it has texture coordinates, without which a material's maps read one texel. */
|
|
127
|
+
hasUvs;
|
|
126
128
|
/**
|
|
127
129
|
* This mesh's morph deltas, or null for geometry that does not deform.
|
|
128
130
|
*
|
|
@@ -171,6 +173,7 @@ export class Mesh {
|
|
|
171
173
|
constructor(gl, data, dynamic = false, spread = false) {
|
|
172
174
|
validateMeshData(data);
|
|
173
175
|
this.spread = spread;
|
|
176
|
+
this.hasUvs = data.uvs !== undefined;
|
|
174
177
|
boundsOfPositions(data.positions, this.bounds);
|
|
175
178
|
this.positionFloats = data.positions.length;
|
|
176
179
|
const vao = gl.createVertexArray();
|
|
@@ -98,6 +98,15 @@ export declare class ParticlePool {
|
|
|
98
98
|
* together.
|
|
99
99
|
*/
|
|
100
100
|
private readonly seedOf;
|
|
101
|
+
/**
|
|
102
|
+
* Where each slot's particle sits in the drawn streams, or -1 where it sits nowhere.
|
|
103
|
+
*
|
|
104
|
+
* **What lets `emit` draw a particle the frame it is born.** The streams are compacted, so a
|
|
105
|
+
* slot's entry is wherever `update` last put it; an emit into a slot that has one overwrites
|
|
106
|
+
* that entry, and an emit into one that has none appends. Without it a new particle waited for
|
|
107
|
+
* the next `update` to be drawn at all.
|
|
108
|
+
*/
|
|
109
|
+
private readonly drawnAt;
|
|
101
110
|
private next;
|
|
102
111
|
constructor(options: ParticlePoolOptions);
|
|
103
112
|
/** How many particles are alive. */
|
|
@@ -119,6 +128,12 @@ export declare class ParticlePool {
|
|
|
119
128
|
* particles cost nothing but the loop iteration.
|
|
120
129
|
*/
|
|
121
130
|
update(dt: number): void;
|
|
131
|
+
/**
|
|
132
|
+
* One particle's entry in both drawn streams, at `t` of its life: what `update` writes for every
|
|
133
|
+
* live particle and `emit` for a new one, so the two cannot disagree about what a particle looks
|
|
134
|
+
* like.
|
|
135
|
+
*/
|
|
136
|
+
private writeDrawn;
|
|
122
137
|
/** Drop everything, for a respawn or a scene change. */
|
|
123
138
|
clear(): void;
|
|
124
139
|
}
|