@driftengine/core 4.6.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +11 -0
- package/README.md +1 -1
- package/dist/render/backend/webgl2/renderer.js +11 -7
- package/dist/render/backend/webgpu/buffers.d.ts +7 -0
- package/dist/render/backend/webgpu/motionPass.js +2 -1
- package/dist/render/backend/webgpu/renderer.js +16 -13
- package/dist/render/mesh.d.ts +6 -1
- package/dist/render/mesh.js +6 -1
- package/package.json +6 -6
- package/src/render/backend/webgl2/renderer.ts +11 -7
- package/src/render/backend/webgpu/buffers.ts +7 -0
- package/src/render/backend/webgpu/motionPass.ts +2 -1
- package/src/render/backend/webgpu/renderer.ts +16 -13
- package/src/render/mesh.ts +6 -1
package/CHANGELOG.json
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "4.6.1",
|
|
4
|
+
"date": "2026-10-01",
|
|
5
|
+
"entries": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "fixed",
|
|
8
|
+
"short": "A mesh with no triangles is skipped by every pass instead of drawn as nothing, so a game's console no longer fills with WebGPU warnings about empty draws.",
|
|
9
|
+
"text": "**A mesh with no triangles draws nothing, in every pass, on both backends.** A game's world can hold an empty mesh, for a level with none of that kind of geometry, and every pass it reached issued a draw of zero indices: WebGPU answers each with a warning, five on one game's first screen (the frame, its reflection, the sun's static and peeled maps and a lamp's shadow faces), and WebGL2 counted draws that drew nothing. `GpuMesh.indexCount` and `Mesh.indexCount` carry the rule, and every place a mesh enters a pass skips an empty one beside its other checks: meshes, translucent meshes, instanced batches, light volumes, shadow casters, glass depth and the motion pass. A game that already skipped its empty meshes itself can keep doing so."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
2
13
|
{
|
|
3
14
|
"version": "4.6.0",
|
|
4
15
|
"date": "2026-10-01",
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The runtime everything needs: renderer, loop, camera, input, geometry. Collision and dynamics live in [`@driftengine/physics`](../physics/README.md), which this package depends on and re-exports whole, so reaching for them through this barrel works as it always did.
|
|
4
4
|
|
|
5
|
-
**Cost:
|
|
5
|
+
**Cost: 699.0 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
|
|
6
6
|
3% — the number is a fact about the build rather than a claim in a document.
|
|
7
7
|
|
|
8
8
|
Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
|
|
@@ -2844,7 +2844,8 @@ export class WebGL2Renderer {
|
|
|
2844
2844
|
if (this.contextLost)
|
|
2845
2845
|
return;
|
|
2846
2846
|
const mesh = batch.mesh;
|
|
2847
|
-
|
|
2847
|
+
/* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
|
|
2848
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
2848
2849
|
return;
|
|
2849
2850
|
const count = Math.min(data.count, batch.capacity);
|
|
2850
2851
|
if (count === 0)
|
|
@@ -6215,8 +6216,9 @@ export class WebGL2Renderer {
|
|
|
6215
6216
|
_previousModel = null) {
|
|
6216
6217
|
if (this.contextLost)
|
|
6217
6218
|
return;
|
|
6218
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
6219
|
-
|
|
6219
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
6220
|
+
and `Mesh.indexCount`. */
|
|
6221
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
6220
6222
|
return;
|
|
6221
6223
|
/*
|
|
6222
6224
|
* Skipped where the flag asks and the bounds say so.
|
|
@@ -6406,8 +6408,9 @@ export class WebGL2Renderer {
|
|
|
6406
6408
|
drawTranslucentMesh(mesh, model, opacity, options = {}) {
|
|
6407
6409
|
if (this.contextLost)
|
|
6408
6410
|
return;
|
|
6409
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
6410
|
-
|
|
6411
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
6412
|
+
and `Mesh.indexCount`. */
|
|
6413
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
6411
6414
|
return;
|
|
6412
6415
|
if (opacity <= 0)
|
|
6413
6416
|
return;
|
|
@@ -6803,8 +6806,9 @@ export class WebGL2Renderer {
|
|
|
6803
6806
|
drawLightVolume(mesh, model, camera, strength, length, spread, options = {}) {
|
|
6804
6807
|
if (this.contextLost)
|
|
6805
6808
|
return;
|
|
6806
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
6807
|
-
|
|
6809
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
6810
|
+
and `Mesh.indexCount`. */
|
|
6811
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
6808
6812
|
return;
|
|
6809
6813
|
const shown = options.medium === undefined
|
|
6810
6814
|
? strength
|
|
@@ -68,6 +68,13 @@ export interface GpuMesh {
|
|
|
68
68
|
readonly weights: number;
|
|
69
69
|
} | null;
|
|
70
70
|
readonly indexBuffer: GPUBuffer;
|
|
71
|
+
/**
|
|
72
|
+
* How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
|
|
73
|
+
* a level can hold none of some kind of geometry, and a draw of zero indices does nothing but
|
|
74
|
+
* earn a warning from the device in every pass that issues it. Every place a mesh enters a pass
|
|
75
|
+
* checks this beside `complete` or its vertex buffers, and WebGL2 skips the same meshes, so the
|
|
76
|
+
* two backends count the same draws.
|
|
77
|
+
*/
|
|
71
78
|
readonly indexCount: number;
|
|
72
79
|
/**
|
|
73
80
|
* How big this mesh is, in its own space.
|
|
@@ -465,7 +465,8 @@ export class MotionPass {
|
|
|
465
465
|
else {
|
|
466
466
|
pipeline = this.pipelineFor(mesh.vertexStride);
|
|
467
467
|
}
|
|
468
|
-
|
|
468
|
+
/* A mesh with nothing in it draws nothing: see `GpuMesh.indexCount`. */
|
|
469
|
+
if (pipeline === null || mesh.indexCount === 0)
|
|
469
470
|
continue;
|
|
470
471
|
if (pipeline !== bound) {
|
|
471
472
|
pass.setPipeline(pipeline);
|
|
@@ -1982,7 +1982,7 @@ export class WebGPURenderer {
|
|
|
1982
1982
|
if (!this.canDraw())
|
|
1983
1983
|
return;
|
|
1984
1984
|
const geometry = mesh;
|
|
1985
|
-
if (geometry.vertexBuffers === undefined)
|
|
1985
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
1986
1986
|
return;
|
|
1987
1987
|
const vertexSlot = this.filmVerts.allocate();
|
|
1988
1988
|
const fragmentSlot = this.filmFrags.allocate();
|
|
@@ -3465,8 +3465,9 @@ export class WebGPURenderer {
|
|
|
3465
3465
|
return;
|
|
3466
3466
|
/* Geometry that has not all arrived is not drawn, which is the surface's contract and what
|
|
3467
3467
|
every other mesh verb here and the other backend's volume already kept. This one drew a
|
|
3468
|
-
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`.
|
|
3469
|
-
|
|
3468
|
+
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. A
|
|
3469
|
+
hull with no triangles draws nothing either: see `GpuMesh.indexCount`. */
|
|
3470
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
3470
3471
|
return;
|
|
3471
3472
|
/* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
|
|
3472
3473
|
now covers a clear night too: a beam with no medium to light is not drawn at all. */
|
|
@@ -4426,7 +4427,7 @@ export class WebGPURenderer {
|
|
|
4426
4427
|
if (cutout !== null && cutoutGroup !== null)
|
|
4427
4428
|
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
4428
4429
|
const geometry = mesh;
|
|
4429
|
-
if (geometry.vertexBuffers === undefined)
|
|
4430
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
4430
4431
|
return;
|
|
4431
4432
|
const cuts = cutoutGroup !== null;
|
|
4432
4433
|
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
@@ -4455,7 +4456,7 @@ export class WebGPURenderer {
|
|
|
4455
4456
|
return;
|
|
4456
4457
|
const gpuBatch = batch;
|
|
4457
4458
|
const geometry = gpuBatch.mesh;
|
|
4458
|
-
if (geometry.vertexBuffers === undefined)
|
|
4459
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
4459
4460
|
return;
|
|
4460
4461
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
4461
4462
|
if (count === 0)
|
|
@@ -4510,7 +4511,7 @@ export class WebGPURenderer {
|
|
|
4510
4511
|
this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uModel.offset, model);
|
|
4511
4512
|
this.writeShadowWind(slot);
|
|
4512
4513
|
const geometry = mesh;
|
|
4513
|
-
if (geometry.vertexBuffers === undefined)
|
|
4514
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
4514
4515
|
return;
|
|
4515
4516
|
pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, this.shadowSkinnedLayout, `depth-skinned|${this.depthCullMode}|${geometry.key ?? ''}`, this.presentOf(geometry), this.depthCullMode, true));
|
|
4516
4517
|
pass.setBindGroup(0, this.shadowSkinnedGroup(view, paletteSlot), [slot]);
|
|
@@ -4532,7 +4533,7 @@ export class WebGPURenderer {
|
|
|
4532
4533
|
if (pass === null || data.count === 0)
|
|
4533
4534
|
return;
|
|
4534
4535
|
const geometry = scatter;
|
|
4535
|
-
if (geometry.vertexBuffers === undefined)
|
|
4536
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
4536
4537
|
return;
|
|
4537
4538
|
const v = this.scatterDepthFloats;
|
|
4538
4539
|
const at = (name) => (SCATTER_DEPTH_FIELDS[name]?.offset ?? -4) / 4;
|
|
@@ -6823,8 +6824,9 @@ export class WebGPURenderer {
|
|
|
6823
6824
|
submitMesh(mesh, model, tint, opacity, blend, options = {}) {
|
|
6824
6825
|
if (!this.canDraw() || this.viewProj === null)
|
|
6825
6826
|
return;
|
|
6826
|
-
/* Geometry that has not all arrived is not drawn. See `GpuMesh.complete
|
|
6827
|
-
|
|
6827
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `GpuMesh.complete`
|
|
6828
|
+
and `GpuMesh.indexCount`. */
|
|
6829
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
6828
6830
|
return;
|
|
6829
6831
|
const slot = this.perDraw.allocate();
|
|
6830
6832
|
if (slot === null) {
|
|
@@ -7212,7 +7214,8 @@ export class WebGPURenderer {
|
|
|
7212
7214
|
if (!this.canDraw() || this.viewProj === null)
|
|
7213
7215
|
return;
|
|
7214
7216
|
const mesh = batch.mesh;
|
|
7215
|
-
|
|
7217
|
+
/* Nor a batch of a mesh with nothing in it: see `GpuMesh.indexCount`. */
|
|
7218
|
+
if (!mesh.complete || mesh.indexCount === 0)
|
|
7216
7219
|
return;
|
|
7217
7220
|
const count = Math.min(data.count, batch.capacity);
|
|
7218
7221
|
if (count === 0)
|
|
@@ -7803,7 +7806,7 @@ export class WebGPURenderer {
|
|
|
7803
7806
|
if (pass === null || tint === null)
|
|
7804
7807
|
return;
|
|
7805
7808
|
const geometry = mesh;
|
|
7806
|
-
if (geometry.vertexBuffers === undefined)
|
|
7809
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
7807
7810
|
return;
|
|
7808
7811
|
const cutout = cutoutOf(material);
|
|
7809
7812
|
const albedo = cutout === null ? null : cutout.albedo;
|
|
@@ -7834,7 +7837,7 @@ export class WebGPURenderer {
|
|
|
7834
7837
|
return;
|
|
7835
7838
|
const gpuBatch = batch;
|
|
7836
7839
|
const geometry = gpuBatch.mesh;
|
|
7837
|
-
if (geometry.vertexBuffers === undefined)
|
|
7840
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
7838
7841
|
return;
|
|
7839
7842
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
7840
7843
|
if (count === 0)
|
|
@@ -7867,7 +7870,7 @@ export class WebGPURenderer {
|
|
|
7867
7870
|
if (pass === null || tint === null)
|
|
7868
7871
|
return;
|
|
7869
7872
|
const geometry = mesh;
|
|
7870
|
-
if (geometry.vertexBuffers === undefined)
|
|
7873
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0)
|
|
7871
7874
|
return;
|
|
7872
7875
|
const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
|
|
7873
7876
|
const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
|
package/dist/render/mesh.d.ts
CHANGED
|
@@ -19,7 +19,12 @@ export declare class Mesh {
|
|
|
19
19
|
private readonly deformable;
|
|
20
20
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
21
21
|
private readonly positionFloats;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Indices one draw of it issues; the batch-size rule reads it (`cullsInstances`). **At zero the
|
|
24
|
+
* mesh has nothing to draw and no verb draws it**, as on WebGPU (`GpuMesh.indexCount`): a draw of
|
|
25
|
+
* nothing is silent here but is still a call and a counted draw, and the two backends count the
|
|
26
|
+
* same frame.
|
|
27
|
+
*/
|
|
23
28
|
readonly indexCount: number;
|
|
24
29
|
/**
|
|
25
30
|
* The constant values this mesh's *absent* attributes are shaded with, re-applied on
|
package/dist/render/mesh.js
CHANGED
|
@@ -88,7 +88,12 @@ export class Mesh {
|
|
|
88
88
|
deformable = null;
|
|
89
89
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
90
90
|
positionFloats;
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Indices one draw of it issues; the batch-size rule reads it (`cullsInstances`). **At zero the
|
|
93
|
+
* mesh has nothing to draw and no verb draws it**, as on WebGPU (`GpuMesh.indexCount`): a draw of
|
|
94
|
+
* nothing is silent here but is still a call and a counted draw, and the two backends count the
|
|
95
|
+
* same frame.
|
|
96
|
+
*/
|
|
92
97
|
indexCount;
|
|
93
98
|
/**
|
|
94
99
|
* The constant values this mesh's *absent* attributes are shaded with, re-applied on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.1",
|
|
4
4
|
"description": "DriftEngine reusable browser 3D runtime",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.6.
|
|
33
|
-
"@driftengine/physics": "4.6.
|
|
32
|
+
"@driftengine/drft": "4.6.1",
|
|
33
|
+
"@driftengine/physics": "4.6.1",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@driftengine/nav": "4.6.
|
|
67
|
-
"@driftengine/network": "4.6.
|
|
68
|
-
"@driftengine/texture": "4.6.
|
|
66
|
+
"@driftengine/nav": "4.6.1",
|
|
67
|
+
"@driftengine/network": "4.6.1",
|
|
68
|
+
"@driftengine/texture": "4.6.1"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -3794,7 +3794,8 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
3794
3794
|
): void {
|
|
3795
3795
|
if (this.contextLost) return;
|
|
3796
3796
|
const mesh = batch.mesh;
|
|
3797
|
-
|
|
3797
|
+
/* Nor a batch of a mesh with nothing in it: see `Mesh.indexCount`. */
|
|
3798
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
3798
3799
|
const count = Math.min(data.count, batch.capacity);
|
|
3799
3800
|
if (count === 0) return;
|
|
3800
3801
|
/* A culling batch's camera draw: the whole batch against the view and the occluders first,
|
|
@@ -7698,8 +7699,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7698
7699
|
_previousModel: ReadonlyMat4 | Mover | null = null,
|
|
7699
7700
|
): void {
|
|
7700
7701
|
if (this.contextLost) return;
|
|
7701
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
7702
|
-
|
|
7702
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
7703
|
+
and `Mesh.indexCount`. */
|
|
7704
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
7703
7705
|
|
|
7704
7706
|
/*
|
|
7705
7707
|
* Skipped where the flag asks and the bounds say so.
|
|
@@ -7885,8 +7887,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
7885
7887
|
options: TranslucentMeshOptions = {},
|
|
7886
7888
|
): void {
|
|
7887
7889
|
if (this.contextLost) return;
|
|
7888
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
7889
|
-
|
|
7890
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
7891
|
+
and `Mesh.indexCount`. */
|
|
7892
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
7890
7893
|
|
|
7891
7894
|
if (opacity <= 0) return;
|
|
7892
7895
|
|
|
@@ -8312,8 +8315,9 @@ export class WebGL2Renderer implements RendererApi {
|
|
|
8312
8315
|
options: LightVolumeDrawOptions = {},
|
|
8313
8316
|
): void {
|
|
8314
8317
|
if (this.contextLost) return;
|
|
8315
|
-
/* Geometry that has not all arrived is not drawn. See `Mesh.complete
|
|
8316
|
-
|
|
8318
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `Mesh.complete`
|
|
8319
|
+
and `Mesh.indexCount`. */
|
|
8320
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8317
8321
|
const shown =
|
|
8318
8322
|
options.medium === undefined
|
|
8319
8323
|
? strength
|
|
@@ -154,6 +154,13 @@ export interface GpuMesh {
|
|
|
154
154
|
*/
|
|
155
155
|
readonly skinOffsets: { readonly joints: number; readonly weights: number } | null;
|
|
156
156
|
readonly indexBuffer: GPUBuffer;
|
|
157
|
+
/**
|
|
158
|
+
* How many indices the mesh draws. **Zero is a mesh with nothing to draw, and no pass draws it**:
|
|
159
|
+
* a level can hold none of some kind of geometry, and a draw of zero indices does nothing but
|
|
160
|
+
* earn a warning from the device in every pass that issues it. Every place a mesh enters a pass
|
|
161
|
+
* checks this beside `complete` or its vertex buffers, and WebGL2 skips the same meshes, so the
|
|
162
|
+
* two backends count the same draws.
|
|
163
|
+
*/
|
|
157
164
|
readonly indexCount: number;
|
|
158
165
|
/**
|
|
159
166
|
* How big this mesh is, in its own space.
|
|
@@ -509,7 +509,8 @@ export class MotionPass {
|
|
|
509
509
|
} else {
|
|
510
510
|
pipeline = this.pipelineFor(mesh.vertexStride);
|
|
511
511
|
}
|
|
512
|
-
|
|
512
|
+
/* A mesh with nothing in it draws nothing: see `GpuMesh.indexCount`. */
|
|
513
|
+
if (pipeline === null || mesh.indexCount === 0) continue;
|
|
513
514
|
if (pipeline !== bound) {
|
|
514
515
|
pass.setPipeline(pipeline);
|
|
515
516
|
bound = pipeline;
|
|
@@ -2624,7 +2624,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2624
2624
|
): void {
|
|
2625
2625
|
if (!this.canDraw()) return;
|
|
2626
2626
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
2627
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
2627
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
2628
2628
|
const vertexSlot = this.filmVerts.allocate();
|
|
2629
2629
|
const fragmentSlot = this.filmFrags.allocate();
|
|
2630
2630
|
if (vertexSlot === null || fragmentSlot === null) return;
|
|
@@ -4329,8 +4329,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
4329
4329
|
if (!this.canDraw()) return;
|
|
4330
4330
|
/* Geometry that has not all arrived is not drawn, which is the surface's contract and what
|
|
4331
4331
|
every other mesh verb here and the other backend's volume already kept. This one drew a
|
|
4332
|
-
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`.
|
|
4333
|
-
|
|
4332
|
+
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. A
|
|
4333
|
+
hull with no triangles draws nothing either: see `GpuMesh.indexCount`. */
|
|
4334
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
4334
4335
|
/* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
|
|
4335
4336
|
now covers a clear night too: a beam with no medium to light is not drawn at all. */
|
|
4336
4337
|
const shown =
|
|
@@ -5478,7 +5479,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5478
5479
|
this.writeCutout(slot, cutout, DEPTH_CUTOUT_VERT_FIELDS);
|
|
5479
5480
|
|
|
5480
5481
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5481
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5482
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5482
5483
|
const cuts = cutoutGroup !== null;
|
|
5483
5484
|
/* A two-sided caster casts from whichever face the light sees: nothing culled. */
|
|
5484
5485
|
const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
|
|
@@ -5517,7 +5518,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5517
5518
|
if (pass === null) return;
|
|
5518
5519
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
5519
5520
|
const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
|
|
5520
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5521
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5521
5522
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
5522
5523
|
if (count === 0) return;
|
|
5523
5524
|
const cutout = cutoutOf(material);
|
|
@@ -5591,7 +5592,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5591
5592
|
this.writeShadowWind(slot);
|
|
5592
5593
|
|
|
5593
5594
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
5594
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5595
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5595
5596
|
pass.setPipeline(
|
|
5596
5597
|
depthPipeline(
|
|
5597
5598
|
this.pipelines,
|
|
@@ -5621,7 +5622,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5621
5622
|
const pass = this.shadowPass;
|
|
5622
5623
|
if (pass === null || data.count === 0) return;
|
|
5623
5624
|
const geometry = scatter as GpuScatter;
|
|
5624
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
5625
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
5625
5626
|
|
|
5626
5627
|
const v = this.scatterDepthFloats;
|
|
5627
5628
|
const at = (name: string): number => (SCATTER_DEPTH_FIELDS[name]?.offset ?? -4) / 4;
|
|
@@ -8422,8 +8423,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8422
8423
|
options: TranslucentMeshOptions = {},
|
|
8423
8424
|
): void {
|
|
8424
8425
|
if (!this.canDraw() || this.viewProj === null) return;
|
|
8425
|
-
/* Geometry that has not all arrived is not drawn. See `GpuMesh.complete
|
|
8426
|
-
|
|
8426
|
+
/* Geometry that has not all arrived is not drawn, nor a mesh with none. See `GpuMesh.complete`
|
|
8427
|
+
and `GpuMesh.indexCount`. */
|
|
8428
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8427
8429
|
|
|
8428
8430
|
const slot = this.perDraw.allocate();
|
|
8429
8431
|
if (slot === null) {
|
|
@@ -8868,7 +8870,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8868
8870
|
): void {
|
|
8869
8871
|
if (!this.canDraw() || this.viewProj === null) return;
|
|
8870
8872
|
const mesh = batch.mesh;
|
|
8871
|
-
|
|
8873
|
+
/* Nor a batch of a mesh with nothing in it: see `GpuMesh.indexCount`. */
|
|
8874
|
+
if (!mesh.complete || mesh.indexCount === 0) return;
|
|
8872
8875
|
const count = Math.min(data.count, batch.capacity);
|
|
8873
8876
|
if (count === 0) return;
|
|
8874
8877
|
/* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
|
|
@@ -9522,7 +9525,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9522
9525
|
const tint = this.glassTint;
|
|
9523
9526
|
if (pass === null || tint === null) return;
|
|
9524
9527
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
9525
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9528
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9526
9529
|
const cutout = cutoutOf(material);
|
|
9527
9530
|
const albedo = cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
|
|
9528
9531
|
const slot = this.shadowDraws.allocate();
|
|
@@ -9565,7 +9568,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9565
9568
|
if (pass === null || tint === null) return;
|
|
9566
9569
|
const gpuBatch = batch as GpuInstancedBatch;
|
|
9567
9570
|
const geometry = gpuBatch.mesh as GpuMesh & { key?: string };
|
|
9568
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9571
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9569
9572
|
const count = Math.min(data.count, gpuBatch.capacity);
|
|
9570
9573
|
if (count === 0) return;
|
|
9571
9574
|
const cutout = cutoutOf(material);
|
|
@@ -9609,7 +9612,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9609
9612
|
const tint = this.glassTint;
|
|
9610
9613
|
if (pass === null || tint === null) return;
|
|
9611
9614
|
const geometry = mesh as GpuMesh & { key?: string };
|
|
9612
|
-
if (geometry.vertexBuffers === undefined) return;
|
|
9615
|
+
if (geometry.vertexBuffers === undefined || geometry.indexCount === 0) return;
|
|
9613
9616
|
const paletteSlot = this.skinPalettes.take(this.surface.device, palette);
|
|
9614
9617
|
const view = paletteSlot === null ? null : this.skinPalettes.view(paletteSlot);
|
|
9615
9618
|
if (view === null) return;
|
package/src/render/mesh.ts
CHANGED
|
@@ -99,7 +99,12 @@ export class Mesh {
|
|
|
99
99
|
private readonly deformable: { positions: WebGLBuffer; normals: WebGLBuffer } | null = null;
|
|
100
100
|
/** How many floats the position buffer holds, so an update of the wrong size is refused. */
|
|
101
101
|
private readonly positionFloats: number;
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Indices one draw of it issues; the batch-size rule reads it (`cullsInstances`). **At zero the
|
|
104
|
+
* mesh has nothing to draw and no verb draws it**, as on WebGPU (`GpuMesh.indexCount`): a draw of
|
|
105
|
+
* nothing is silent here but is still a call and a counted draw, and the two backends count the
|
|
106
|
+
* same frame.
|
|
107
|
+
*/
|
|
103
108
|
readonly indexCount: number;
|
|
104
109
|
/**
|
|
105
110
|
* The constant values this mesh's *absent* attributes are shaded with, re-applied on
|