@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
|
@@ -337,6 +337,8 @@ import {
|
|
|
337
337
|
import type { GlobalMediumOptions } from '../../globalMedium.ts';
|
|
338
338
|
import type { MeshData } from '../../mesh.ts';
|
|
339
339
|
import { castsDepth, glassOf } from '../../shadowCasters.ts';
|
|
340
|
+
import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.ts';
|
|
341
|
+
import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.ts';
|
|
340
342
|
import { GlassCasterList, type GlassReplaySink } from '../../glassCasters.ts';
|
|
341
343
|
import { GpuGlassTint } from './glassTintPass.ts';
|
|
342
344
|
import {
|
|
@@ -379,7 +381,7 @@ import {
|
|
|
379
381
|
createDepthBindGroupLayout,
|
|
380
382
|
depthPipeline,
|
|
381
383
|
} from './depthPass.ts';
|
|
382
|
-
import { PipelineCache } from './pipelineCache.ts';
|
|
384
|
+
import { PipelineCache, type LitSwitch } from './pipelineCache.ts';
|
|
383
385
|
import {
|
|
384
386
|
SKY_FIELDS,
|
|
385
387
|
SKY_UNIFORM_SIZE,
|
|
@@ -2805,6 +2807,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2805
2807
|
private readonly uvScale = new Float32Array([1, 1]);
|
|
2806
2808
|
/** Whether the bound material is seen from both faces. See `SurfaceMaterial.doubleSided`. */
|
|
2807
2809
|
private materialDoubleSided = false;
|
|
2810
|
+
/** Whether the bound material has a map, for the warning a mesh with no coordinates earns. */
|
|
2811
|
+
private materialHasMaps = false;
|
|
2808
2812
|
|
|
2809
2813
|
createSurfaceTexture(
|
|
2810
2814
|
source: TexImageSource,
|
|
@@ -2937,6 +2941,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2937
2941
|
this.uvScale[0] = material?.uScale ?? 1;
|
|
2938
2942
|
this.uvScale[1] = material?.vScale ?? 1;
|
|
2939
2943
|
this.materialDoubleSided = material?.doubleSided === true;
|
|
2944
|
+
this.materialHasMaps = materialHasMaps(material);
|
|
2940
2945
|
const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
|
|
2941
2946
|
const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
|
|
2942
2947
|
const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
|
|
@@ -5450,6 +5455,27 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5450
5455
|
*/
|
|
5451
5456
|
/** The glass the open shadow pass was offered, kept for its own passes: `glassCasters.ts`. */
|
|
5452
5457
|
private readonly glassCasters = new GlassCasterList();
|
|
5458
|
+
/** Whether a glass caster has ever been offered, which is when lit pipelines start reading it. */
|
|
5459
|
+
private glassSeen = false;
|
|
5460
|
+
|
|
5461
|
+
/**
|
|
5462
|
+
* The first glass caster switches every lit pipeline to reading glass. See
|
|
5463
|
+
* `PipelineCache.litSwitches`: until a pane exists the lookups would find nothing, and they cost
|
|
5464
|
+
* the lit pass on every surface whether they find anything or not.
|
|
5465
|
+
*/
|
|
5466
|
+
private noteGlassCaster(): void {
|
|
5467
|
+
if (this.glassSeen) return;
|
|
5468
|
+
this.glassSeen = true;
|
|
5469
|
+
this.enableLit('GLASS_SHADOWS');
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
/** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
|
|
5473
|
+
private enableLit(feature: LitSwitch): void {
|
|
5474
|
+
if (this.pipelines.litSwitches[feature]) return;
|
|
5475
|
+
void this.pipelines.enable(feature);
|
|
5476
|
+
if (this.overlayPipelines !== this.pipelines) void this.overlayPipelines.enable(feature);
|
|
5477
|
+
if (this.latePipelines !== this.pipelines) void this.latePipelines.enable(feature);
|
|
5478
|
+
}
|
|
5453
5479
|
/** One resolution every glass caster is read through, so recording one allocates nothing. */
|
|
5454
5480
|
private readonly glassCasterScratch: ResolvedGlass = {
|
|
5455
5481
|
transmission: 0,
|
|
@@ -5462,6 +5488,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5462
5488
|
/* Glass: kept aside for its own two passes rather than dropped. See `glassCasters.ts`. */
|
|
5463
5489
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
5464
5490
|
this.glassCasters.recordMesh(mesh, model, material, this.glassCasterScratch);
|
|
5491
|
+
this.noteGlassCaster();
|
|
5465
5492
|
}
|
|
5466
5493
|
return;
|
|
5467
5494
|
}
|
|
@@ -5517,6 +5544,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5517
5544
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
5518
5545
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
5519
5546
|
this.glassCasters.recordInstanced(batch, data, material, this.glassCasterScratch);
|
|
5547
|
+
this.noteGlassCaster();
|
|
5520
5548
|
}
|
|
5521
5549
|
return;
|
|
5522
5550
|
}
|
|
@@ -5572,6 +5600,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5572
5600
|
if (!this.drawingGlassDepth && !castsDepth(material)) {
|
|
5573
5601
|
if (this.quality.glassShadows !== 'off' && glassOf(material, this.glassCasterScratch)) {
|
|
5574
5602
|
this.glassCasters.recordSkinned(mesh, model, palette, material, this.glassCasterScratch);
|
|
5603
|
+
this.noteGlassCaster();
|
|
5575
5604
|
}
|
|
5576
5605
|
return;
|
|
5577
5606
|
}
|
|
@@ -5968,6 +5997,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5968
5997
|
quality.screenEffects ? sceneColorFormat(quality.hdrScene) : surface.format,
|
|
5969
5998
|
this.samples,
|
|
5970
5999
|
glassShadows,
|
|
6000
|
+
quality.clusteredLights,
|
|
5971
6001
|
);
|
|
5972
6002
|
/*
|
|
5973
6003
|
* The canvas' own state, for the pass `ensurePass` reopens after `endFrame`.
|
|
@@ -5980,11 +6010,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5980
6010
|
this.overlayPipelines =
|
|
5981
6011
|
this.pipelines.format === surface.format && this.samples === 1
|
|
5982
6012
|
? this.pipelines
|
|
5983
|
-
: new PipelineCache(device, surface.format, 1, glassShadows);
|
|
6013
|
+
: new PipelineCache(device, surface.format, 1, glassShadows, quality.clusteredLights);
|
|
5984
6014
|
this.latePipelines =
|
|
5985
6015
|
this.pipelines.format === RECON_HISTORY_FORMAT && this.samples === 1
|
|
5986
6016
|
? this.pipelines
|
|
5987
|
-
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows);
|
|
6017
|
+
: new PipelineCache(device, RECON_HISTORY_FORMAT, 1, glassShadows, quality.clusteredLights);
|
|
5988
6018
|
this.issueBound = this.issueCommand.bind(this);
|
|
5989
6019
|
this.pipelineTargets =
|
|
5990
6020
|
this.overlayPipelines === this.pipelines
|
|
@@ -6288,7 +6318,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6288
6318
|
/* The albedo's effects table, or a one-texel stand-in the shader's size test reads as none.
|
|
6289
6319
|
The albedo is the group's key, so its table needs no key of its own. */
|
|
6290
6320
|
if (name === 'uSurfaceEffects') {
|
|
6291
|
-
|
|
6321
|
+
const effects = this.albedo?.effectsView ?? null;
|
|
6322
|
+
/* The first table bound is when the lit pipelines start reading one. See `litSwitches`. */
|
|
6323
|
+
if (effects !== null) this.enableLit('SURFACE_EFFECTS');
|
|
6324
|
+
return { view: effects ?? blankView, sampler: blankSampler };
|
|
6292
6325
|
}
|
|
6293
6326
|
/*
|
|
6294
6327
|
* The room, once a bake has filled it. Unbaked reads as the blank cube: nothing to mirror.
|
|
@@ -7752,6 +7785,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7752
7785
|
i[at('uLightingEnabled')] = 1;
|
|
7753
7786
|
i[at('uFogEnabled')] = 1;
|
|
7754
7787
|
i[at('uAlbedoEnabled')] = 0;
|
|
7788
|
+
this.materialHasMaps = false;
|
|
7755
7789
|
|
|
7756
7790
|
/*
|
|
7757
7791
|
* **The camera medium, which this pass did not upload until 2026-08-29.**
|
|
@@ -8284,6 +8318,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8284
8318
|
* where it happened to pass, write motion for a surface no pixel of the frame belongs to.
|
|
8285
8319
|
*/
|
|
8286
8320
|
if (this.tracksMotionNow()) this.recordDrawMotion(mesh, model, previousModel);
|
|
8321
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
8287
8322
|
this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
|
|
8288
8323
|
}
|
|
8289
8324
|
|
|
@@ -8389,6 +8424,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8389
8424
|
return;
|
|
8390
8425
|
}
|
|
8391
8426
|
|
|
8427
|
+
noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
|
|
8392
8428
|
/* The tint travels in the options here, where `drawMesh` takes it positionally — see
|
|
8393
8429
|
`TranslucentMeshOptions.tint` for why, and for the fade it was added for. */
|
|
8394
8430
|
this.submitMesh(
|
|
@@ -12598,6 +12634,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12598
12634
|
command.instances = 1;
|
|
12599
12635
|
command.indirect = null;
|
|
12600
12636
|
recordNode(this.arena, VERB_DRAW, reads, writes, at, 0);
|
|
12637
|
+
if (this.framePresented) this.queueOverlayFlush();
|
|
12601
12638
|
return command;
|
|
12602
12639
|
}
|
|
12603
12640
|
|
|
@@ -12613,11 +12650,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12613
12650
|
if (verb === VERB_PASS) {
|
|
12614
12651
|
const definition = passAt(this.passes, nodeState(this.arena, at));
|
|
12615
12652
|
if (definition === undefined) return;
|
|
12616
|
-
this.
|
|
12617
|
-
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
12618
|
-
this.passContext.outputTransform = this.gradeCode();
|
|
12619
|
-
this.passContext.outputExposure = this.gradeExposure();
|
|
12620
|
-
definition.draw(this.passContext as PassContext);
|
|
12653
|
+
definition.draw(this.fillPassContext(pass));
|
|
12621
12654
|
return;
|
|
12622
12655
|
}
|
|
12623
12656
|
if (verb !== VERB_DRAW) return;
|
|
@@ -12704,6 +12737,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12704
12737
|
}
|
|
12705
12738
|
|
|
12706
12739
|
private openPass(): GPURenderPassEncoder | null {
|
|
12740
|
+
const was = this.pass;
|
|
12741
|
+
const pass = this.openAnyPass();
|
|
12742
|
+
/* A pass opened while a view model is drawn starts with the whole depth range: squeeze it. */
|
|
12743
|
+
if (this.viewModelActive && pass !== null && pass !== was && !this.reflectionPassActive) {
|
|
12744
|
+
this.squeezeViewport(pass);
|
|
12745
|
+
}
|
|
12746
|
+
return pass;
|
|
12747
|
+
}
|
|
12748
|
+
|
|
12749
|
+
private openAnyPass(): GPURenderPassEncoder | null {
|
|
12707
12750
|
/* While the replay is running this is the only pass there is. See `oitEncoder`. */
|
|
12708
12751
|
if (this.oitEncoder !== null) return this.oitEncoder;
|
|
12709
12752
|
/*
|
|
@@ -12797,19 +12840,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12797
12840
|
});
|
|
12798
12841
|
this.overlayActive = true;
|
|
12799
12842
|
|
|
12800
|
-
|
|
12801
|
-
* **Submitted on a microtask, which runs before the browser presents.** There is no second
|
|
12802
|
-
* `endFrame` to hang this on — a consumer thinks the frame is over — and presentation happens
|
|
12803
|
-
* when the task ends, after the microtask checkpoint. Anything that needs the pixels sooner
|
|
12804
|
-
* flushes synchronously; `copyRegionTo` is the one that does.
|
|
12805
|
-
*/
|
|
12806
|
-
if (!this.overlayFlushQueued) {
|
|
12807
|
-
this.overlayFlushQueued = true;
|
|
12808
|
-
queueMicrotask(() => this.flushOverlay());
|
|
12809
|
-
}
|
|
12843
|
+
this.queueOverlayFlush();
|
|
12810
12844
|
return this.pass;
|
|
12811
12845
|
}
|
|
12812
12846
|
|
|
12847
|
+
/**
|
|
12848
|
+
* Submit what is drawn after the present on a microtask, which runs before the browser presents.
|
|
12849
|
+
*
|
|
12850
|
+
* There is no second `endFrame` to hang this on — a consumer thinks the frame is over — and
|
|
12851
|
+
* presentation happens when the task ends, after the microtask checkpoint. Anything that needs
|
|
12852
|
+
* the pixels sooner flushes synchronously; `copyRegionTo` is the one that does.
|
|
12853
|
+
*
|
|
12854
|
+
* **Queued wherever such work begins: where the overlay pass opens, and where a verb records.**
|
|
12855
|
+
* With the graph on a verb records rather than opening a pass, and until 4.8.2 only an opening
|
|
12856
|
+
* queued this, so `fillPanel` and a contributed pass drawn after `endFrame` waited for the next
|
|
12857
|
+
* `beginFrame` — whose replay drew onto a canvas texture already presented and destroyed.
|
|
12858
|
+
*/
|
|
12859
|
+
private queueOverlayFlush(): void {
|
|
12860
|
+
if (this.overlayFlushQueued) return;
|
|
12861
|
+
this.overlayFlushQueued = true;
|
|
12862
|
+
queueMicrotask(this.flushOverlayBound);
|
|
12863
|
+
}
|
|
12864
|
+
|
|
12865
|
+
/** `flushOverlay` bound once, so queueing it each frame allocates nothing. */
|
|
12866
|
+
private readonly flushOverlayBound = (): void => this.flushOverlay();
|
|
12867
|
+
|
|
12813
12868
|
/**
|
|
12814
12869
|
* The cache whose pipelines match the pass this draw will land in.
|
|
12815
12870
|
*
|
|
@@ -12873,6 +12928,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12873
12928
|
}
|
|
12874
12929
|
|
|
12875
12930
|
endFrame(): void {
|
|
12931
|
+
/* A view model left open would squeeze every pass after the world, the composite included. */
|
|
12932
|
+
if (this.viewModelActive) this.endViewModel();
|
|
12876
12933
|
this.inFrame = false;
|
|
12877
12934
|
if (this.surface.lost || this.encoder === null) return;
|
|
12878
12935
|
/*
|
|
@@ -13341,14 +13398,40 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13341
13398
|
outputTransform: number;
|
|
13342
13399
|
outputExposure: number;
|
|
13343
13400
|
jitter: Float32Array;
|
|
13401
|
+
format: GPUTextureFormat;
|
|
13402
|
+
depthFormat: GPUTextureFormat;
|
|
13403
|
+
samples: number;
|
|
13344
13404
|
} = {
|
|
13345
13405
|
backend: 'webgpu',
|
|
13346
13406
|
pass: null,
|
|
13347
13407
|
outputTransform: 0,
|
|
13348
13408
|
outputExposure: 1,
|
|
13349
13409
|
jitter: this.passJitter,
|
|
13410
|
+
format: 'rgba8unorm',
|
|
13411
|
+
depthFormat: DEPTH_FORMAT,
|
|
13412
|
+
samples: 1,
|
|
13350
13413
|
};
|
|
13351
13414
|
|
|
13415
|
+
/**
|
|
13416
|
+
* Fill the context a contributed pass draws with, for the pass it is about to draw into.
|
|
13417
|
+
*
|
|
13418
|
+
* **The attachments as well as the encoder**: a pass drawn after `endFrame` lands on the canvas
|
|
13419
|
+
* at one sample where the frame's own is the scene target at the scene's samples, and a pipeline
|
|
13420
|
+
* built for the one is refused by the other. The cache a mesh drawn here would use is the answer,
|
|
13421
|
+
* so the two cannot disagree.
|
|
13422
|
+
*/
|
|
13423
|
+
private fillPassContext(pass: GPURenderPassEncoder): PassContext {
|
|
13424
|
+
const target = this.targetPipelines();
|
|
13425
|
+
const context = this.passContext;
|
|
13426
|
+
context.pass = pass;
|
|
13427
|
+
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
13428
|
+
context.outputTransform = this.gradeCode();
|
|
13429
|
+
context.outputExposure = this.gradeExposure();
|
|
13430
|
+
context.format = target.format;
|
|
13431
|
+
context.samples = target.sampleCount;
|
|
13432
|
+
return context as PassContext;
|
|
13433
|
+
}
|
|
13434
|
+
|
|
13352
13435
|
/**
|
|
13353
13436
|
* Let something outside this file draw into the frame. See `Renderer.registerPass`.
|
|
13354
13437
|
*
|
|
@@ -13502,11 +13585,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13502
13585
|
if (!this.quality.frameGraph) {
|
|
13503
13586
|
const pass = this.openPass();
|
|
13504
13587
|
if (pass === null) return;
|
|
13505
|
-
this.
|
|
13506
|
-
/* The frame's grade, so a contributed pass can obey the 2026-08-17 rule. See `PassContext`. */
|
|
13507
|
-
this.passContext.outputTransform = this.gradeCode();
|
|
13508
|
-
this.passContext.outputExposure = this.gradeExposure();
|
|
13509
|
-
definition.draw(this.passContext as PassContext);
|
|
13588
|
+
definition.draw(this.fillPassContext(pass));
|
|
13510
13589
|
return;
|
|
13511
13590
|
}
|
|
13512
13591
|
recordNode(
|
|
@@ -13517,6 +13596,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13517
13596
|
handle,
|
|
13518
13597
|
0,
|
|
13519
13598
|
);
|
|
13599
|
+
if (this.framePresented) this.queueOverlayFlush();
|
|
13520
13600
|
}
|
|
13521
13601
|
|
|
13522
13602
|
/** Let go of a pass. A handle kept past this draws nothing; see `PassHandle`. */
|
|
@@ -13728,7 +13808,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13728
13808
|
* **The clear is a drawn quad rather than a clear**, because WebGPU clears whole attachments
|
|
13729
13809
|
* and cannot confine one to a scissor. See `insetPass.ts`.
|
|
13730
13810
|
*
|
|
13731
|
-
* Returns the
|
|
13811
|
+
* Returns the **aspect** of the inset, width over height in the pixels its viewport got, as the
|
|
13812
|
+
* other backend does — for `camera.updateMatrices`. With no pass to draw into, a lost device,
|
|
13813
|
+
* it is the aspect of the box asked for: there are no pixels to measure, and a zero would be a
|
|
13814
|
+
* projection of infinities in the caller's camera.
|
|
13732
13815
|
*/
|
|
13733
13816
|
beginInset(rect: InsetRect, clearColor: Vec3 | null): number {
|
|
13734
13817
|
/*
|
|
@@ -13738,7 +13821,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13738
13821
|
*/
|
|
13739
13822
|
if (this.quality.frameGraph) this.flushGraph();
|
|
13740
13823
|
const pass = this.openPass();
|
|
13741
|
-
if (this.surface.lost || pass === null)
|
|
13824
|
+
if (this.surface.lost || pass === null)
|
|
13825
|
+
return Math.max(rect.width, 1) / Math.max(rect.height, 1);
|
|
13742
13826
|
const canvas = this.surface.context.canvas as HTMLCanvasElement;
|
|
13743
13827
|
const cssWidth = Math.max(canvas.clientWidth, 1);
|
|
13744
13828
|
const cssHeight = Math.max(canvas.clientHeight, 1);
|
|
@@ -13843,6 +13927,43 @@ export class WebGPURenderer implements RendererApi {
|
|
|
13843
13927
|
return w / h;
|
|
13844
13928
|
}
|
|
13845
13929
|
|
|
13930
|
+
/**
|
|
13931
|
+
* Draw what follows as a **view model**, in the nearest sliver of depth, until `endViewModel`.
|
|
13932
|
+
*
|
|
13933
|
+
* The twin of the WebGL2 method, which carries the reasons. Here the squeeze is the viewport's
|
|
13934
|
+
* depth bounds, which are pass state applied when a draw is issued, so both ends flush the graph
|
|
13935
|
+
* as an inset's do, and a pass opened in between is given the bounds again in `openPass`.
|
|
13936
|
+
*/
|
|
13937
|
+
beginViewModel(share = VIEW_MODEL_DEPTH_SHARE): void {
|
|
13938
|
+
if (this.quality.frameGraph) this.flushGraph();
|
|
13939
|
+
viewModelDepthRange(this.reversedDepth, share, this.viewModelRange);
|
|
13940
|
+
this.viewModelActive = true;
|
|
13941
|
+
const pass = this.openPass();
|
|
13942
|
+
if (this.surface.lost || pass === null) return;
|
|
13943
|
+
this.squeezeViewport(pass);
|
|
13944
|
+
}
|
|
13945
|
+
|
|
13946
|
+
/** Give the whole depth range back. Safe to call without a matching `beginViewModel`. */
|
|
13947
|
+
endViewModel(): void {
|
|
13948
|
+
if (!this.viewModelActive) return;
|
|
13949
|
+
if (this.quality.frameGraph) this.flushGraph();
|
|
13950
|
+
this.viewModelActive = false;
|
|
13951
|
+
const pass = this.pass;
|
|
13952
|
+
if (this.surface.lost || pass === null) return;
|
|
13953
|
+
const [width, height] = this.passSize();
|
|
13954
|
+
pass.setViewport(0, 0, width, height, 0, 1);
|
|
13955
|
+
}
|
|
13956
|
+
|
|
13957
|
+
/** The view model's depth bounds on the whole of the pass. */
|
|
13958
|
+
private squeezeViewport(pass: GPURenderPassEncoder): void {
|
|
13959
|
+
const [width, height] = this.passSize();
|
|
13960
|
+
pass.setViewport(0, 0, width, height, this.viewModelRange[0], this.viewModelRange[1]);
|
|
13961
|
+
}
|
|
13962
|
+
|
|
13963
|
+
/** Whether a view model is being drawn: `openPass` re-applies it, `endFrame` closes it. */
|
|
13964
|
+
private viewModelActive = false;
|
|
13965
|
+
private readonly viewModelRange: [number, number] = [0, 1];
|
|
13966
|
+
|
|
13846
13967
|
/** Give the whole canvas back. Safe to call without a matching `beginInset`. */
|
|
13847
13968
|
endInset(): void {
|
|
13848
13969
|
/*
|
|
@@ -15030,6 +15151,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15030
15151
|
this.iesRows = this.iesAtlas.height;
|
|
15031
15152
|
this.iesPlanes = this.iesAtlas.planes;
|
|
15032
15153
|
this.cookieTiles = this.cookieImages.length;
|
|
15154
|
+
/* The first profile or cookie is when the lit pipelines start reading them. See `litSwitches`. */
|
|
15155
|
+
if (this.iesRows > 1 || this.cookieTiles > 0) this.enableLit('LIGHT_FIXTURES');
|
|
15033
15156
|
return this.fixtureView;
|
|
15034
15157
|
}
|
|
15035
15158
|
|
|
@@ -15125,6 +15248,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
15125
15248
|
);
|
|
15126
15249
|
this.driftIndex = index;
|
|
15127
15250
|
this.driftAtlas = atlas;
|
|
15251
|
+
/* The first volume is when the lit pipelines start reading one. See `litSwitches`. */
|
|
15252
|
+
this.enableLit('DRIFT_LIGHT');
|
|
15128
15253
|
this.driftIndexView = index.createView({ dimension: '3d' });
|
|
15129
15254
|
this.driftAtlasView = atlas.createView({ dimension: '3d' });
|
|
15130
15255
|
this.bakeLightField = {
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
const told = new WeakSet<object>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a material binds a map that is a picture: albedo, normal or emissive.
|
|
20
|
+
*
|
|
21
|
+
* **Not ORM**, because a one-colour ORM map is how a draw sets a constant roughness and metalness,
|
|
22
|
+
* and read at one texel it is exactly that constant — the materials example draws its spheres so.
|
|
23
|
+
* The three counted here are pictures nobody binds to be read at one point.
|
|
24
|
+
*/
|
|
25
|
+
export function materialHasMaps(material: SurfaceMaterial<unknown> | null): boolean {
|
|
26
|
+
if (material === null) return false;
|
|
27
|
+
return (
|
|
28
|
+
(material.albedo ?? null) !== null ||
|
|
29
|
+
(material.normal ?? null) !== null ||
|
|
30
|
+
(material.emissive ?? null) !== null
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Warn the first time `mesh`, which has no coordinates, is drawn with a picture map bound. */
|
|
35
|
+
export function noteMapsWithoutUvs(mesh: object, hasUvs: boolean, materialHasMaps: boolean): void {
|
|
36
|
+
if (hasUvs || !materialHasMaps || told.has(mesh)) return;
|
|
37
|
+
told.add(mesh);
|
|
38
|
+
console.warn(
|
|
39
|
+
'[driftengine] a mesh with no texture coordinates is drawn with an albedo, normal or emissive ' +
|
|
40
|
+
'map, which it cannot read: every map samples one texel. Give the mesh `uvs` — ' +
|
|
41
|
+
'`MeshBuilder.build({ planarUvs: true })` writes them — or draw it without the maps.',
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/render/mesh.ts
CHANGED
|
@@ -136,6 +136,8 @@ export class Mesh {
|
|
|
136
136
|
readonly bounds: Bounds = createBounds();
|
|
137
137
|
/** Whether this mesh carries a rig, which decides which flat program draws it. */
|
|
138
138
|
readonly isSkinned: boolean;
|
|
139
|
+
/** Whether it has texture coordinates, without which a material's maps read one texel. */
|
|
140
|
+
readonly hasUvs: boolean;
|
|
139
141
|
|
|
140
142
|
/**
|
|
141
143
|
* This mesh's morph deltas, or null for geometry that does not deform.
|
|
@@ -190,6 +192,7 @@ export class Mesh {
|
|
|
190
192
|
constructor(gl: WebGL2RenderingContext, data: MeshData, dynamic = false, spread = false) {
|
|
191
193
|
validateMeshData(data);
|
|
192
194
|
this.spread = spread;
|
|
195
|
+
this.hasUvs = data.uvs !== undefined;
|
|
193
196
|
boundsOfPositions(data.positions, this.bounds);
|
|
194
197
|
this.positionFloats = data.positions.length;
|
|
195
198
|
const vao = gl.createVertexArray();
|
|
@@ -117,6 +117,15 @@ export class ParticlePool {
|
|
|
117
117
|
* together.
|
|
118
118
|
*/
|
|
119
119
|
private readonly seedOf: Float32Array;
|
|
120
|
+
/**
|
|
121
|
+
* Where each slot's particle sits in the drawn streams, or -1 where it sits nowhere.
|
|
122
|
+
*
|
|
123
|
+
* **What lets `emit` draw a particle the frame it is born.** The streams are compacted, so a
|
|
124
|
+
* slot's entry is wherever `update` last put it; an emit into a slot that has one overwrites
|
|
125
|
+
* that entry, and an emit into one that has none appends. Without it a new particle waited for
|
|
126
|
+
* the next `update` to be drawn at all.
|
|
127
|
+
*/
|
|
128
|
+
private readonly drawnAt: Int32Array;
|
|
120
129
|
private next = 0;
|
|
121
130
|
|
|
122
131
|
constructor(private readonly options: ParticlePoolOptions) {
|
|
@@ -138,6 +147,7 @@ export class ParticlePool {
|
|
|
138
147
|
this.spin = new Float32Array(capacity);
|
|
139
148
|
this.scale = new Float32Array(capacity).fill(1);
|
|
140
149
|
this.seedOf = new Float32Array(capacity);
|
|
150
|
+
this.drawnAt = new Int32Array(capacity).fill(-1);
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
/** How many particles are alive. */
|
|
@@ -184,6 +194,18 @@ export class ParticlePool {
|
|
|
184
194
|
this.spin[i] = (hash - Math.floor(hash)) * Math.PI * 2;
|
|
185
195
|
this.scale[i] = sizeScale;
|
|
186
196
|
this.seedOf[i] = seed;
|
|
197
|
+
/*
|
|
198
|
+
* Drawn now, at birth, rather than at the next `update`: into the entry this slot already
|
|
199
|
+
* has when it is taking over a particle that was drawn, and onto the end otherwise.
|
|
200
|
+
*/
|
|
201
|
+
const held = this.drawnAt[i] as number;
|
|
202
|
+
const at = held >= 0 ? held : this.particles.count;
|
|
203
|
+
this.writeDrawn(i, at, 0, x, y, z, vx, vy, vz);
|
|
204
|
+
if (held < 0) {
|
|
205
|
+
this.drawnAt[i] = at;
|
|
206
|
+
this.instances.count = at + 1;
|
|
207
|
+
this.particles.count = at + 1;
|
|
208
|
+
}
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
/**
|
|
@@ -198,11 +220,15 @@ export class ParticlePool {
|
|
|
198
220
|
let count = 0;
|
|
199
221
|
for (let i = 0; i < this.age.length; i++) {
|
|
200
222
|
const age = this.age[i] as number;
|
|
201
|
-
if (age === Infinity)
|
|
223
|
+
if (age === Infinity) {
|
|
224
|
+
this.drawnAt[i] = -1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
202
227
|
const life = this.life[i] as number;
|
|
203
228
|
const next = age + dt;
|
|
204
229
|
if (next >= life) {
|
|
205
230
|
this.age[i] = Infinity;
|
|
231
|
+
this.drawnAt[i] = -1;
|
|
206
232
|
continue;
|
|
207
233
|
}
|
|
208
234
|
this.age[i] = next;
|
|
@@ -222,68 +248,89 @@ export class ParticlePool {
|
|
|
222
248
|
this.y[i] = py;
|
|
223
249
|
this.z[i] = pz;
|
|
224
250
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const r =
|
|
228
|
-
(o.colorStart[0] as number) + ((o.colorEnd[0] as number) - (o.colorStart[0] as number)) * t;
|
|
229
|
-
const g =
|
|
230
|
-
(o.colorStart[1] as number) + ((o.colorEnd[1] as number) - (o.colorStart[1] as number)) * t;
|
|
231
|
-
const b =
|
|
232
|
-
(o.colorStart[2] as number) + ((o.colorEnd[2] as number) - (o.colorStart[2] as number)) * t;
|
|
233
|
-
/*
|
|
234
|
-
* Colour carries the fade rather than an alpha channel: the instanced
|
|
235
|
-
* path multiplies a tint into the base mesh and has no per-instance
|
|
236
|
-
* opacity, and for an additive puff darkening to nothing *is* fading out.
|
|
237
|
-
* It also means the effect needs no blend-state change.
|
|
238
|
-
*/
|
|
239
|
-
const fade = 1 - t * t;
|
|
240
|
-
writeInstance(
|
|
241
|
-
this.instances,
|
|
242
|
-
count,
|
|
243
|
-
px,
|
|
244
|
-
py,
|
|
245
|
-
pz,
|
|
246
|
-
size,
|
|
247
|
-
this.spin[i] as number,
|
|
248
|
-
r * fade,
|
|
249
|
-
g * fade,
|
|
250
|
-
b * fade,
|
|
251
|
-
0,
|
|
252
|
-
0,
|
|
253
|
-
0,
|
|
254
|
-
);
|
|
255
|
-
/*
|
|
256
|
-
* The same particle for a material that has a real opacity channel. Colour
|
|
257
|
-
* is written *unfaded* here, because dimming it is the scatter path's
|
|
258
|
-
* workaround and doing both would fade twice — a puff that vanished at half
|
|
259
|
-
* its life while still occluding what was behind it.
|
|
260
|
-
*/
|
|
261
|
-
const p = this.particles;
|
|
262
|
-
const alphaStart = o.alphaStart ?? 1;
|
|
263
|
-
const alphaEnd = o.alphaEnd ?? 0;
|
|
264
|
-
p.positions[count * 3] = px;
|
|
265
|
-
p.positions[count * 3 + 1] = py;
|
|
266
|
-
p.positions[count * 3 + 2] = pz;
|
|
267
|
-
p.sizes[count] = size;
|
|
268
|
-
p.spins[count] = this.spin[i] as number;
|
|
269
|
-
p.colors[count * 3] = r;
|
|
270
|
-
p.colors[count * 3 + 1] = g;
|
|
271
|
-
p.colors[count * 3 + 2] = b;
|
|
272
|
-
p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
|
|
273
|
-
p.ages[count] = t;
|
|
274
|
-
p.seeds[count] = this.seedOf[i] as number;
|
|
275
|
-
p.velocities[count * 3] = vx;
|
|
276
|
-
p.velocities[count * 3 + 1] = vy;
|
|
277
|
-
p.velocities[count * 3 + 2] = vz;
|
|
251
|
+
this.drawnAt[i] = count;
|
|
252
|
+
this.writeDrawn(i, count, next / life, px, py, pz, vx, vy, vz);
|
|
278
253
|
count++;
|
|
279
254
|
}
|
|
280
255
|
this.instances.count = count;
|
|
281
256
|
this.particles.count = count;
|
|
282
257
|
}
|
|
283
258
|
|
|
259
|
+
/**
|
|
260
|
+
* One particle's entry in both drawn streams, at `t` of its life: what `update` writes for every
|
|
261
|
+
* live particle and `emit` for a new one, so the two cannot disagree about what a particle looks
|
|
262
|
+
* like.
|
|
263
|
+
*/
|
|
264
|
+
private writeDrawn(
|
|
265
|
+
i: number,
|
|
266
|
+
count: number,
|
|
267
|
+
t: number,
|
|
268
|
+
px: number,
|
|
269
|
+
py: number,
|
|
270
|
+
pz: number,
|
|
271
|
+
vx: number,
|
|
272
|
+
vy: number,
|
|
273
|
+
vz: number,
|
|
274
|
+
): void {
|
|
275
|
+
const o = this.options;
|
|
276
|
+
const size = (o.sizeStart + (o.sizeEnd - o.sizeStart) * t) * (this.scale[i] as number);
|
|
277
|
+
const r =
|
|
278
|
+
(o.colorStart[0] as number) + ((o.colorEnd[0] as number) - (o.colorStart[0] as number)) * t;
|
|
279
|
+
const g =
|
|
280
|
+
(o.colorStart[1] as number) + ((o.colorEnd[1] as number) - (o.colorStart[1] as number)) * t;
|
|
281
|
+
const b =
|
|
282
|
+
(o.colorStart[2] as number) + ((o.colorEnd[2] as number) - (o.colorStart[2] as number)) * t;
|
|
283
|
+
/*
|
|
284
|
+
* Colour carries the fade rather than an alpha channel: the instanced
|
|
285
|
+
* path multiplies a tint into the base mesh and has no per-instance
|
|
286
|
+
* opacity, and for an additive puff darkening to nothing *is* fading out.
|
|
287
|
+
* It also means the effect needs no blend-state change.
|
|
288
|
+
*/
|
|
289
|
+
const fade = 1 - t * t;
|
|
290
|
+
writeInstance(
|
|
291
|
+
this.instances,
|
|
292
|
+
count,
|
|
293
|
+
px,
|
|
294
|
+
py,
|
|
295
|
+
pz,
|
|
296
|
+
size,
|
|
297
|
+
this.spin[i] as number,
|
|
298
|
+
r * fade,
|
|
299
|
+
g * fade,
|
|
300
|
+
b * fade,
|
|
301
|
+
0,
|
|
302
|
+
0,
|
|
303
|
+
0,
|
|
304
|
+
);
|
|
305
|
+
/*
|
|
306
|
+
* The same particle for a material that has a real opacity channel. Colour
|
|
307
|
+
* is written *unfaded* here, because dimming it is the scatter path's
|
|
308
|
+
* workaround and doing both would fade twice — a puff that vanished at half
|
|
309
|
+
* its life while still occluding what was behind it.
|
|
310
|
+
*/
|
|
311
|
+
const p = this.particles;
|
|
312
|
+
const alphaStart = o.alphaStart ?? 1;
|
|
313
|
+
const alphaEnd = o.alphaEnd ?? 0;
|
|
314
|
+
p.positions[count * 3] = px;
|
|
315
|
+
p.positions[count * 3 + 1] = py;
|
|
316
|
+
p.positions[count * 3 + 2] = pz;
|
|
317
|
+
p.sizes[count] = size;
|
|
318
|
+
p.spins[count] = this.spin[i] as number;
|
|
319
|
+
p.colors[count * 3] = r;
|
|
320
|
+
p.colors[count * 3 + 1] = g;
|
|
321
|
+
p.colors[count * 3 + 2] = b;
|
|
322
|
+
p.alphas[count] = alphaStart + (alphaEnd - alphaStart) * t;
|
|
323
|
+
p.ages[count] = t;
|
|
324
|
+
p.seeds[count] = this.seedOf[i] as number;
|
|
325
|
+
p.velocities[count * 3] = vx;
|
|
326
|
+
p.velocities[count * 3 + 1] = vy;
|
|
327
|
+
p.velocities[count * 3 + 2] = vz;
|
|
328
|
+
}
|
|
329
|
+
|
|
284
330
|
/** Drop everything, for a respawn or a scene change. */
|
|
285
331
|
clear(): void {
|
|
286
332
|
this.age.fill(Infinity);
|
|
333
|
+
this.drawnAt.fill(-1);
|
|
287
334
|
this.instances.count = 0;
|
|
288
335
|
this.particles.count = 0;
|
|
289
336
|
this.next = 0;
|
package/src/render/pass.ts
CHANGED
|
@@ -125,6 +125,18 @@ export type PassContext = {
|
|
|
125
125
|
readonly pass: GPURenderPassEncoder;
|
|
126
126
|
/** The frame's jitter, the same array `PrepareContext.jitter` held. */
|
|
127
127
|
readonly jitter: Float32Array;
|
|
128
|
+
/**
|
|
129
|
+
* The colour format, depth format and sample count of the pass this draw lands in, which a
|
|
130
|
+
* pipeline set on it must match exactly.
|
|
131
|
+
*
|
|
132
|
+
* **Not always the device's.** `PassDevice` says what the frame is; a pass drawn after
|
|
133
|
+
* `endFrame` lands on the canvas, at the canvas's format and one sample, where the frame is
|
|
134
|
+
* the scene target at the scene's samples. A pass that is drawn in both places keeps a
|
|
135
|
+
* pipeline for each and chooses here; one built only from the device is refused by the other.
|
|
136
|
+
*/
|
|
137
|
+
readonly format: GPUTextureFormat;
|
|
138
|
+
readonly depthFormat: GPUTextureFormat;
|
|
139
|
+
readonly samples: number;
|
|
128
140
|
}
|
|
129
141
|
);
|
|
130
142
|
|
|
@@ -194,12 +194,13 @@ export interface RenderQuality {
|
|
|
194
194
|
* existed: a pane casts nothing at all.
|
|
195
195
|
*
|
|
196
196
|
* Nothing is allocated until a glass caster first reaches a shadow, so a scene with no glass pays
|
|
197
|
-
* no memory and no pass whatever this says. **
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K
|
|
201
|
-
*
|
|
202
|
-
*
|
|
197
|
+
* no memory and no pass whatever this says. **Nor any shader, since 4.8.2**: the lit shader is
|
|
198
|
+
* built without the glass lookup and rebuilt with it when the first glass caster reaches a shadow.
|
|
199
|
+
* A lookup that finds no glass still cost the lit pass its registers — measured on the courtyard at
|
|
200
|
+
* night, 0.27 ms at 720p on WebGPU, 0.42 ms on WebGL2 and 1.2 ms at 4K — and from 4.5.0 to 4.8.1
|
|
201
|
+
* every scene paid it, glass or none: about 30% of the lit shader's instructions, which on a phone
|
|
202
|
+
* was the lit pass. `'off'` never rebuilds. What would make `'full'` wrong is a phone budget with a
|
|
203
|
+
* lot of glass; that is what `'half'` is for. See `glassShadow.ts`.
|
|
203
204
|
*/
|
|
204
205
|
readonly glassShadows: GlassShadows;
|
|
205
206
|
/** Horizontal world-space reach over which a directional shadow dissolves. */
|