@aethervtt/shard-render 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/assets.d.ts +104 -0
- package/dist/assets.js +213 -0
- package/dist/atmosphere-model.d.ts +96 -0
- package/dist/atmosphere-model.js +370 -0
- package/dist/atmosphere-nodes.d.ts +55 -0
- package/dist/atmosphere-nodes.js +780 -0
- package/dist/atmosphere-plugin.d.ts +5 -0
- package/dist/atmosphere-plugin.js +42 -0
- package/dist/atmosphere-shaders.d.ts +7 -0
- package/dist/atmosphere-shaders.js +595 -0
- package/dist/atmosphere-state.d.ts +72 -0
- package/dist/atmosphere-state.js +18 -0
- package/dist/atmosphere.d.ts +191 -0
- package/dist/atmosphere.js +950 -0
- package/dist/camera.d.ts +83 -0
- package/dist/camera.js +163 -0
- package/dist/clusters.d.ts +68 -0
- package/dist/clusters.js +288 -0
- package/dist/culling-shaders.d.ts +2 -0
- package/dist/culling-shaders.js +203 -0
- package/dist/culling.d.ts +84 -0
- package/dist/culling.js +498 -0
- package/dist/debug-shaders.d.ts +3 -0
- package/dist/debug-shaders.js +207 -0
- package/dist/debug-views.d.ts +60 -0
- package/dist/debug-views.js +197 -0
- package/dist/deferred-plugin.d.ts +5 -0
- package/dist/deferred-plugin.js +20 -0
- package/dist/deferred-shaders.d.ts +2 -0
- package/dist/deferred-shaders.js +151 -0
- package/dist/deferred.d.ts +22 -0
- package/dist/deferred.js +296 -0
- package/dist/deform.d.ts +80 -0
- package/dist/deform.js +213 -0
- package/dist/describe-plugin.d.ts +5 -0
- package/dist/describe-plugin.js +19 -0
- package/dist/display-nodes.d.ts +5 -0
- package/dist/display-nodes.js +189 -0
- package/dist/dynamic-resolution.d.ts +47 -0
- package/dist/dynamic-resolution.js +191 -0
- package/dist/environment-plugin.d.ts +6 -0
- package/dist/environment-plugin.js +44 -0
- package/dist/environment-shaders.d.ts +2 -0
- package/dist/environment-shaders.js +260 -0
- package/dist/environment-state.d.ts +71 -0
- package/dist/environment-state.js +144 -0
- package/dist/environment.d.ts +70 -0
- package/dist/environment.js +529 -0
- package/dist/forward.d.ts +123 -0
- package/dist/forward.js +1216 -0
- package/dist/fxaa.d.ts +2 -0
- package/dist/fxaa.js +65 -0
- package/dist/gizmo-font.d.ts +21 -0
- package/dist/gizmo-font.js +132 -0
- package/dist/gizmos-plugin.d.ts +5 -0
- package/dist/gizmos-plugin.js +34 -0
- package/dist/gizmos.d.ts +156 -0
- package/dist/gizmos.js +772 -0
- package/dist/gpu-assets.d.ts +114 -0
- package/dist/gpu-assets.js +494 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.js +417 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +540 -0
- package/dist/instances.d.ts +349 -0
- package/dist/instances.js +1335 -0
- package/dist/large-world.d.ts +13 -0
- package/dist/large-world.js +20 -0
- package/dist/lights.d.ts +160 -0
- package/dist/lights.js +468 -0
- package/dist/material-noise.d.ts +19 -0
- package/dist/material-noise.js +7 -0
- package/dist/material-pipelines.d.ts +46 -0
- package/dist/material-pipelines.js +166 -0
- package/dist/materials.d.ts +108 -0
- package/dist/materials.js +219 -0
- package/dist/noise.d.ts +76 -0
- package/dist/noise.js +286 -0
- package/dist/overlay-registry.d.ts +18 -0
- package/dist/overlay-registry.js +30 -0
- package/dist/overlays.d.ts +35 -0
- package/dist/overlays.js +344 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +46 -0
- package/dist/picking-plugin.d.ts +2 -0
- package/dist/picking-plugin.js +20 -0
- package/dist/picking.d.ts +94 -0
- package/dist/picking.js +456 -0
- package/dist/pixel-perfect-plugin.d.ts +9 -0
- package/dist/pixel-perfect-plugin.js +135 -0
- package/dist/pixel-perfect.d.ts +27 -0
- package/dist/pixel-perfect.js +70 -0
- package/dist/plugin.d.ts +111 -0
- package/dist/plugin.js +182 -0
- package/dist/pool.d.ts +19 -0
- package/dist/pool.js +43 -0
- package/dist/post-common.d.ts +51 -0
- package/dist/post-common.js +148 -0
- package/dist/post-nodes.d.ts +39 -0
- package/dist/post-nodes.js +849 -0
- package/dist/post-plugin.d.ts +5 -0
- package/dist/post-plugin.js +26 -0
- package/dist/post-shaders.d.ts +8 -0
- package/dist/post-shaders.js +698 -0
- package/dist/post.d.ts +175 -0
- package/dist/post.js +453 -0
- package/dist/readback.d.ts +4 -0
- package/dist/readback.js +95 -0
- package/dist/render-scale.d.ts +53 -0
- package/dist/render-scale.js +63 -0
- package/dist/shaders.d.ts +122 -0
- package/dist/shaders.js +1168 -0
- package/dist/shadows.d.ts +119 -0
- package/dist/shadows.js +562 -0
- package/dist/skin-asset.d.ts +26 -0
- package/dist/skin-asset.js +25 -0
- package/dist/skinning-plugin.d.ts +5 -0
- package/dist/skinning-plugin.js +16 -0
- package/dist/skinning.d.ts +19 -0
- package/dist/skinning.js +343 -0
- package/dist/standard-fields.d.ts +67 -0
- package/dist/standard-fields.js +73 -0
- package/dist/standard.d.ts +7 -0
- package/dist/standard.js +37 -0
- package/dist/stats.d.ts +25 -0
- package/dist/stats.js +18 -0
- package/dist/target.d.ts +55 -0
- package/dist/target.js +70 -0
- package/dist/testing.d.ts +34 -0
- package/dist/testing.js +126 -0
- package/dist/timer.d.ts +28 -0
- package/dist/timer.js +136 -0
- package/dist/view.d.ts +117 -0
- package/dist/view.js +389 -0
- package/dist/visibility.d.ts +10 -0
- package/dist/visibility.js +84 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1335 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
defineResource,
|
|
4
|
+
defineSystem,
|
|
5
|
+
onRemove,
|
|
6
|
+
t
|
|
7
|
+
} from "@aethervtt/shard-core";
|
|
8
|
+
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
9
|
+
import { LogResource } from "@aethervtt/shard-runtime";
|
|
10
|
+
import { toHalf } from "@aethervtt/shard-texture";
|
|
11
|
+
import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
|
|
12
|
+
import { MaterialAsset, Materials, Meshes } from "./assets.js";
|
|
13
|
+
import { DEFORM_WORDS, DeformStore } from "./deform.js";
|
|
14
|
+
import { GpuAssetsResource } from "./gpu-assets.js";
|
|
15
|
+
import { isTransparent } from "./materials.js";
|
|
16
|
+
import { ComputedVisibility, Visibility } from "./visibility.js";
|
|
17
|
+
const InstanceSlot = defineComponent(
|
|
18
|
+
"render/InstanceSlot",
|
|
19
|
+
{
|
|
20
|
+
slot: t.u32({
|
|
21
|
+
readonly: true,
|
|
22
|
+
description: "GPU instance slot + 1 (0 = none yet). Managed by the renderer."
|
|
23
|
+
})
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
description: "The entity's slot in the renderer's persistent instance buffer. Do not write.",
|
|
27
|
+
serialize: false
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
const NotShadowCaster = defineComponent(
|
|
31
|
+
"render/NotShadowCaster",
|
|
32
|
+
{},
|
|
33
|
+
{ description: "Tag: this mesh casts no shadows." }
|
|
34
|
+
);
|
|
35
|
+
const NotShadowReceiver = defineComponent(
|
|
36
|
+
"render/NotShadowReceiver",
|
|
37
|
+
{},
|
|
38
|
+
{ description: "Tag: shadows are not applied to this mesh." }
|
|
39
|
+
);
|
|
40
|
+
const Mesh3d = defineComponent(
|
|
41
|
+
"render/Mesh3d",
|
|
42
|
+
{ mesh: t.handle("Mesh", { description: "The mesh to draw." }) },
|
|
43
|
+
{
|
|
44
|
+
description: "Draws a mesh at this entity's transform.",
|
|
45
|
+
requires: [Transform, Visibility, InstanceSlot]
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
const MeshMaterial = defineComponent(
|
|
49
|
+
"render/MeshMaterial",
|
|
50
|
+
{
|
|
51
|
+
material: t.handle("Material", {
|
|
52
|
+
description: "Standard material; a neutral gray when absent."
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
{ description: "The material a Mesh3d is drawn with." }
|
|
56
|
+
);
|
|
57
|
+
const MAX_LOD_LEVELS = 8;
|
|
58
|
+
const Lod = defineComponent(
|
|
59
|
+
"render/Lod",
|
|
60
|
+
{
|
|
61
|
+
levels: t.list(
|
|
62
|
+
t.struct({
|
|
63
|
+
mesh: t.handle("Mesh", { description: "The mesh for this level." }),
|
|
64
|
+
screenSize: t.f32({
|
|
65
|
+
min: 0,
|
|
66
|
+
description: "Projected bounding-sphere diameter, as a fraction of viewport height, above which this level draws."
|
|
67
|
+
})
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
description: "From most to least detailed, with decreasing screenSize. Smaller than the last level: not drawn."
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
hysteresis: t.f32({
|
|
74
|
+
default: 0.1,
|
|
75
|
+
min: 0,
|
|
76
|
+
max: 0.9,
|
|
77
|
+
description: "A level changes only once the size crosses a threshold by this fraction."
|
|
78
|
+
}),
|
|
79
|
+
bias: t.f32({
|
|
80
|
+
description: "Scales every size by 2^bias: positive keeps detail longer, negative drops it sooner."
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
description: "Level of detail: the drawn mesh follows the entity's size on screen, chosen per camera (shadows use the camera's choice). Replaces Mesh3d.mesh.",
|
|
85
|
+
requires: [Mesh3d]
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const SkinnedMesh = defineComponent(
|
|
89
|
+
"render/SkinnedMesh",
|
|
90
|
+
{
|
|
91
|
+
skin: t.handle("Skin", {
|
|
92
|
+
description: "The skin: joint paths and inverse bind matrices (imported from glTF)."
|
|
93
|
+
}),
|
|
94
|
+
joints: t.list(t.entity, {
|
|
95
|
+
description: "The joint entities, in the skin's order. Resolved from the skin's joint paths when empty (the model's instance spawns them); set it to drive a mesh with other entities."
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
description: "Deforms the mesh by its joints' transforms (linear blend skinning, 4 influences), in every pass that draws it. Culling bounds follow the pose.",
|
|
100
|
+
requires: [Mesh3d]
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
const MorphWeights = defineComponent(
|
|
104
|
+
"render/MorphWeights",
|
|
105
|
+
{
|
|
106
|
+
weights: t.list(t.f32, {
|
|
107
|
+
description: "One weight per morph target of the mesh (0 = none, 1 = the full target). The 8 heaviest are applied. Animation clips' weights channels write it."
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
description: "Blends the mesh's morph targets (blend shapes) by weight.",
|
|
112
|
+
requires: [Mesh3d]
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
const VisibilityRange = defineComponent(
|
|
116
|
+
"render/VisibilityRange",
|
|
117
|
+
{
|
|
118
|
+
start: t.f32({ min: 0, unit: "m", description: "Hidden when the camera is closer than this." }),
|
|
119
|
+
end: t.f32({
|
|
120
|
+
default: 100,
|
|
121
|
+
min: 0,
|
|
122
|
+
unit: "m",
|
|
123
|
+
description: "Hidden at this distance from the camera and beyond (shadows too)."
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
description: "Distance culling from the camera, for small props. Applies to the camera's shadow views too.",
|
|
128
|
+
requires: [Mesh3d]
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
const InstanceData = defineComponent(
|
|
132
|
+
"render/InstanceData",
|
|
133
|
+
{
|
|
134
|
+
x: t.f32({ description: "First value (stored as a half float)." }),
|
|
135
|
+
y: t.f32({
|
|
136
|
+
description: "Second value (stored as a half float; integers up to 2048 are exact)."
|
|
137
|
+
})
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
description: "Two numbers this instance's material reads in its vertex stage (vertex_instance_data() in shard::mesh), e.g. a fade or per-edge flags. They share the slot word VisibilityRange uses, so an entity has one or the other.",
|
|
141
|
+
requires: [Mesh3d]
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
const INSTANCE_FLOATS = 16;
|
|
145
|
+
const INSTANCE_BYTES = INSTANCE_FLOATS * 4;
|
|
146
|
+
const PREV_BYTES = 48;
|
|
147
|
+
const InstanceFlags = {
|
|
148
|
+
Visible: 1,
|
|
149
|
+
Caster: 2,
|
|
150
|
+
Receiver: 4,
|
|
151
|
+
Range: 8,
|
|
152
|
+
Lod: 16,
|
|
153
|
+
/** Skinned: the vertex stage reads joint matrices; culling reads the slot's own sphere. */
|
|
154
|
+
Skinned: 32,
|
|
155
|
+
/** Morphed: the vertex stage adds morph target deltas. */
|
|
156
|
+
Morph: 64
|
|
157
|
+
};
|
|
158
|
+
const NO_BATCH = 4294967295;
|
|
159
|
+
const LOD_BIT = 2147483648;
|
|
160
|
+
const SLOT_MASK = 268435455;
|
|
161
|
+
const LOD_UNSET = 255;
|
|
162
|
+
function createDrawList() {
|
|
163
|
+
return { items: [], length: 0, visible: 0, culled: 0, hidden: 0, pending: 0, cullView: -1 };
|
|
164
|
+
}
|
|
165
|
+
const halfToFloat = (h) => {
|
|
166
|
+
const s = h & 32768 ? -1 : 1;
|
|
167
|
+
const e = h >> 10 & 31;
|
|
168
|
+
const f = h & 1023;
|
|
169
|
+
if (e === 0) return s * 2 ** -14 * (f / 1024);
|
|
170
|
+
if (e === 31) return f ? Number.NaN : s * Number.POSITIVE_INFINITY;
|
|
171
|
+
return s * 2 ** (e - 15) * (1 + f / 1024);
|
|
172
|
+
};
|
|
173
|
+
const f32 = Math.fround;
|
|
174
|
+
const scratchSphere = new Float32Array(4);
|
|
175
|
+
const halfScratch = new Float32Array(2);
|
|
176
|
+
function slotSphere(out, f, o, bounds) {
|
|
177
|
+
const cx = f32((bounds[0] + bounds[3]) * 0.5);
|
|
178
|
+
const cy = f32((bounds[1] + bounds[4]) * 0.5);
|
|
179
|
+
const cz = f32((bounds[2] + bounds[5]) * 0.5);
|
|
180
|
+
const ex = f32((bounds[3] - bounds[0]) * 0.5);
|
|
181
|
+
const ey = f32((bounds[4] - bounds[1]) * 0.5);
|
|
182
|
+
const ez = f32((bounds[5] - bounds[2]) * 0.5);
|
|
183
|
+
const r = f32(Math.sqrt(ex * ex + ey * ey + ez * ez));
|
|
184
|
+
out[0] = f32(f[o] * cx + f[o + 1] * cy + f[o + 2] * cz + f[o + 3]);
|
|
185
|
+
out[1] = f32(f[o + 4] * cx + f[o + 5] * cy + f[o + 6] * cz + f[o + 7]);
|
|
186
|
+
out[2] = f32(f[o + 8] * cx + f[o + 9] * cy + f[o + 10] * cz + f[o + 11]);
|
|
187
|
+
const sx = f[o] * f[o] + f[o + 4] * f[o + 4] + f[o + 8] * f[o + 8];
|
|
188
|
+
const sy = f[o + 1] * f[o + 1] + f[o + 5] * f[o + 5] + f[o + 9] * f[o + 9];
|
|
189
|
+
const sz = f[o + 2] * f[o + 2] + f[o + 6] * f[o + 6] + f[o + 10] * f[o + 10];
|
|
190
|
+
out[3] = f32(r * Math.sqrt(Math.max(sx, sy, sz)));
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
function sphereOfSlot(out, store, slot, flags, bounds) {
|
|
194
|
+
if (flags & InstanceFlags.Skinned) {
|
|
195
|
+
const r = store.deform.recordF32;
|
|
196
|
+
const o = slot * DEFORM_WORDS;
|
|
197
|
+
if (r[o + 3] > 0) {
|
|
198
|
+
out[0] = r[o];
|
|
199
|
+
out[1] = r[o + 1];
|
|
200
|
+
out[2] = r[o + 2];
|
|
201
|
+
out[3] = r[o + 3];
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return slotSphere(out, store.f32, slot * INSTANCE_FLOATS, bounds);
|
|
206
|
+
}
|
|
207
|
+
function sphereInFrustum(planes, s) {
|
|
208
|
+
for (let p = 0; p < 24; p += 4) {
|
|
209
|
+
if (planes[p] * s[0] + planes[p + 1] * s[1] + planes[p + 2] * s[2] + planes[p + 3] < -s[3])
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
function selectLod(size, thresholds, count, hysteresis, previous) {
|
|
215
|
+
let level = previous;
|
|
216
|
+
if (previous === LOD_UNSET || previous > count) {
|
|
217
|
+
level = count;
|
|
218
|
+
for (let k = 0; k < count; k++) {
|
|
219
|
+
if (size >= thresholds[k]) {
|
|
220
|
+
level = k;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return level;
|
|
225
|
+
}
|
|
226
|
+
while (level > 0 && size > thresholds[level - 1] * (1 + hysteresis)) level--;
|
|
227
|
+
while (level < count && size < thresholds[level] * (1 - hysteresis)) level++;
|
|
228
|
+
return level;
|
|
229
|
+
}
|
|
230
|
+
class InstanceStore {
|
|
231
|
+
capacity = 0;
|
|
232
|
+
/** Slots in use or freed; slots >= high were never used. */
|
|
233
|
+
high = 0;
|
|
234
|
+
f32 = new Float32Array(0);
|
|
235
|
+
u32 = new Uint32Array(0);
|
|
236
|
+
/**
|
|
237
|
+
* Each slot's InstanceFlags, mirrored from its record: prepare compares against these every
|
|
238
|
+
* frame, and reading them from the 64-byte records would touch a cache line per instance.
|
|
239
|
+
*/
|
|
240
|
+
flags = new Uint8Array(0);
|
|
241
|
+
/** Hidden instances per ECS table id, from the last time prepare visited the table. */
|
|
242
|
+
tableHidden = [];
|
|
243
|
+
/** Batch index (>= 0), LOD set (-2 - index), or -1 (none), per slot. */
|
|
244
|
+
batchOf = new Int32Array(0);
|
|
245
|
+
/** Index of the slot in its batch's member list, or -1. */
|
|
246
|
+
memberIndex = new Int32Array(0);
|
|
247
|
+
/** Visibility range per slot, as the GPU sees it (f16-rounded start, end). */
|
|
248
|
+
ranges = new Float32Array(0);
|
|
249
|
+
meshRefs = [];
|
|
250
|
+
materialRefs = [];
|
|
251
|
+
/** Lod component value per slot, or null. */
|
|
252
|
+
lods = [];
|
|
253
|
+
dirty = new Uint8Array(0);
|
|
254
|
+
dirtyLo = Number.POSITIVE_INFINITY;
|
|
255
|
+
dirtyHi = -1;
|
|
256
|
+
/**
|
|
257
|
+
* Last frame's transform per slot (12 floats), for motion vectors. A slot that moved last frame
|
|
258
|
+
* but not this one catches up, so a stopped object has no velocity.
|
|
259
|
+
*/
|
|
260
|
+
prev = new Float32Array(0);
|
|
261
|
+
prevDirty = new Uint8Array(0);
|
|
262
|
+
prevLo = Number.POSITIVE_INFINITY;
|
|
263
|
+
prevHi = -1;
|
|
264
|
+
movedFrame = new Uint32Array(0);
|
|
265
|
+
moved = new Uint32Array(64);
|
|
266
|
+
movedCount = 0;
|
|
267
|
+
lastMoved = new Uint32Array(64);
|
|
268
|
+
lastMovedCount = 0;
|
|
269
|
+
frame = 1;
|
|
270
|
+
prevBuffer;
|
|
271
|
+
free = [];
|
|
272
|
+
/** Slots waiting for their mesh or material to load. */
|
|
273
|
+
pending = /* @__PURE__ */ new Set();
|
|
274
|
+
batches = [];
|
|
275
|
+
/** Batches in draw order (sorted by pipeline, then material, then mesh). */
|
|
276
|
+
sorted = [];
|
|
277
|
+
byMesh = /* @__PURE__ */ new Map();
|
|
278
|
+
lodSets = [];
|
|
279
|
+
lodByKey = /* @__PURE__ */ new Map();
|
|
280
|
+
defaultMaterial = new MaterialAsset();
|
|
281
|
+
/** Bytes of instance data uploaded this frame. */
|
|
282
|
+
uploadedBytes = 0;
|
|
283
|
+
/** Slots with a batch and not hidden, and hidden ones: stats for GPU-culled views. */
|
|
284
|
+
drawableCount = 0;
|
|
285
|
+
hiddenCount = 0;
|
|
286
|
+
/** Bumps when batches or LOD sets change shape (the GPU tables re-upload). */
|
|
287
|
+
structureVersion = 0;
|
|
288
|
+
/** Visible slot lists of every CPU-culled view this frame, concatenated. */
|
|
289
|
+
visible = new Uint32Array(1024);
|
|
290
|
+
visibleCount = 0;
|
|
291
|
+
instanceBuffer;
|
|
292
|
+
visibleBuffer;
|
|
293
|
+
generation;
|
|
294
|
+
gpu;
|
|
295
|
+
bindGroup;
|
|
296
|
+
bound = "";
|
|
297
|
+
layout;
|
|
298
|
+
/** Set by the GPU culler: views it culls read their visible lists from this buffer. */
|
|
299
|
+
gpuVisible;
|
|
300
|
+
/** Bind group over `gpuVisible`, for draws of GPU-culled views. */
|
|
301
|
+
gpuBindGroup;
|
|
302
|
+
gpuBound = "";
|
|
303
|
+
/** Joint matrices, morph weights, and deform records of skinned and morphed slots. */
|
|
304
|
+
deform;
|
|
305
|
+
constructor(gpu) {
|
|
306
|
+
this.gpu = gpu;
|
|
307
|
+
this.generation = gpu.generation;
|
|
308
|
+
this.instanceBuffer = new GpuBuffer(gpu, {
|
|
309
|
+
label: "instances",
|
|
310
|
+
usage: GPUBufferUsage.STORAGE,
|
|
311
|
+
size: INSTANCE_BYTES * 256
|
|
312
|
+
});
|
|
313
|
+
this.prevBuffer = new GpuBuffer(gpu, {
|
|
314
|
+
label: "instances/previous",
|
|
315
|
+
usage: GPUBufferUsage.STORAGE,
|
|
316
|
+
size: PREV_BYTES * 256
|
|
317
|
+
});
|
|
318
|
+
this.visibleBuffer = new GpuBuffer(gpu, {
|
|
319
|
+
label: "instances/visible",
|
|
320
|
+
usage: GPUBufferUsage.STORAGE,
|
|
321
|
+
size: 4 * 1024
|
|
322
|
+
});
|
|
323
|
+
this.layout = createInstanceLayout(gpu);
|
|
324
|
+
this.deform = new DeformStore(gpu);
|
|
325
|
+
this.grow(256);
|
|
326
|
+
}
|
|
327
|
+
get live() {
|
|
328
|
+
return this.high - this.free.length;
|
|
329
|
+
}
|
|
330
|
+
grow(capacity) {
|
|
331
|
+
const f32a = new Float32Array(capacity * INSTANCE_FLOATS);
|
|
332
|
+
f32a.set(this.f32);
|
|
333
|
+
this.f32 = f32a;
|
|
334
|
+
this.u32 = new Uint32Array(f32a.buffer);
|
|
335
|
+
const batchOf = new Int32Array(capacity).fill(-1);
|
|
336
|
+
batchOf.set(this.batchOf);
|
|
337
|
+
this.batchOf = batchOf;
|
|
338
|
+
const memberIndex = new Int32Array(capacity).fill(-1);
|
|
339
|
+
memberIndex.set(this.memberIndex);
|
|
340
|
+
this.memberIndex = memberIndex;
|
|
341
|
+
const ranges = new Float32Array(capacity * 2);
|
|
342
|
+
ranges.set(this.ranges);
|
|
343
|
+
this.ranges = ranges;
|
|
344
|
+
const dirty = new Uint8Array(capacity);
|
|
345
|
+
dirty.set(this.dirty);
|
|
346
|
+
this.dirty = dirty;
|
|
347
|
+
const flags = new Uint8Array(capacity);
|
|
348
|
+
flags.set(this.flags);
|
|
349
|
+
this.flags = flags;
|
|
350
|
+
const prev = new Float32Array(capacity * 12);
|
|
351
|
+
prev.set(this.prev);
|
|
352
|
+
this.prev = prev;
|
|
353
|
+
const prevDirty = new Uint8Array(capacity);
|
|
354
|
+
prevDirty.set(this.prevDirty);
|
|
355
|
+
this.prevDirty = prevDirty;
|
|
356
|
+
const movedFrame = new Uint32Array(capacity);
|
|
357
|
+
movedFrame.set(this.movedFrame);
|
|
358
|
+
this.movedFrame = movedFrame;
|
|
359
|
+
this.capacity = capacity;
|
|
360
|
+
this.deform.ensureSlots(capacity);
|
|
361
|
+
}
|
|
362
|
+
alloc(entity) {
|
|
363
|
+
let slot = this.free.pop();
|
|
364
|
+
if (slot === void 0) {
|
|
365
|
+
if (this.high >= this.capacity) this.grow(this.capacity * 2);
|
|
366
|
+
slot = this.high++;
|
|
367
|
+
}
|
|
368
|
+
this.u32[slot * INSTANCE_FLOATS + 12] = NO_BATCH;
|
|
369
|
+
this.u32[slot * INSTANCE_FLOATS + 13] = 0;
|
|
370
|
+
this.flags[slot] = 0;
|
|
371
|
+
this.u32[slot * INSTANCE_FLOATS + 14] = 0;
|
|
372
|
+
this.u32[slot * INSTANCE_FLOATS + 15] = entity % 4294967296;
|
|
373
|
+
this.batchOf[slot] = -1;
|
|
374
|
+
this.memberIndex[slot] = -1;
|
|
375
|
+
this.lods[slot] = null;
|
|
376
|
+
this.markDirty(slot);
|
|
377
|
+
return slot;
|
|
378
|
+
}
|
|
379
|
+
release(slot) {
|
|
380
|
+
this.assign(slot, -1);
|
|
381
|
+
this.u32[slot * INSTANCE_FLOATS + 13] = 0;
|
|
382
|
+
this.flags[slot] = 0;
|
|
383
|
+
this.meshRefs[slot] = null;
|
|
384
|
+
this.materialRefs[slot] = null;
|
|
385
|
+
this.lods[slot] = null;
|
|
386
|
+
this.pending.delete(slot);
|
|
387
|
+
this.deform.clear(slot);
|
|
388
|
+
this.markDirty(slot);
|
|
389
|
+
this.free.push(slot);
|
|
390
|
+
}
|
|
391
|
+
boundsDirty = true;
|
|
392
|
+
cachedBounds = new Float32Array(6);
|
|
393
|
+
/**
|
|
394
|
+
* The world bounds of every shadow caster (spheres), cached until an instance changes. GPU
|
|
395
|
+
* culling fits cascade depth ranges to it, having no CPU cull to take a union from.
|
|
396
|
+
*/
|
|
397
|
+
casterBounds(out) {
|
|
398
|
+
if (this.boundsDirty) {
|
|
399
|
+
this.boundsDirty = false;
|
|
400
|
+
const b = this.cachedBounds;
|
|
401
|
+
b[0] = b[1] = b[2] = Number.POSITIVE_INFINITY;
|
|
402
|
+
b[3] = b[4] = b[5] = Number.NEGATIVE_INFINITY;
|
|
403
|
+
const sphere = scratchSphere;
|
|
404
|
+
const need = InstanceFlags.Visible | InstanceFlags.Caster;
|
|
405
|
+
for (let s = 0; s < this.high; s++) {
|
|
406
|
+
const a = this.batchOf[s];
|
|
407
|
+
if (a === -1 || (this.u32[s * INSTANCE_FLOATS + 13] & need) !== need) continue;
|
|
408
|
+
const batch = this.batches[a >= 0 ? a : this.lodSets[-2 - a].batches[0]];
|
|
409
|
+
if (batch.transparent) continue;
|
|
410
|
+
sphereOfSlot(sphere, this, s, this.u32[s * INSTANCE_FLOATS + 13], batch.mesh.bounds);
|
|
411
|
+
for (let k = 0; k < 3; k++) {
|
|
412
|
+
if (sphere[k] - sphere[3] < b[k]) b[k] = sphere[k] - sphere[3];
|
|
413
|
+
if (sphere[k] + sphere[3] > b[k + 3]) b[k + 3] = sphere[k] + sphere[3];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
out.set(this.cachedBounds);
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
markDirty(slot) {
|
|
421
|
+
this.boundsDirty = true;
|
|
422
|
+
if (this.dirty[slot]) return;
|
|
423
|
+
this.dirty[slot] = 1;
|
|
424
|
+
if (slot < this.dirtyLo) this.dirtyLo = slot;
|
|
425
|
+
if (slot > this.dirtyHi) this.dirtyHi = slot;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Records a transform change: `from` is where the slot's record is now (copied to its previous
|
|
429
|
+
* transform), unless the slot is new, when both get `to`.
|
|
430
|
+
*/
|
|
431
|
+
moveSlot(slot, g, offset, fresh) {
|
|
432
|
+
const f = this.f32;
|
|
433
|
+
const p = this.prev;
|
|
434
|
+
const o = slot * INSTANCE_FLOATS;
|
|
435
|
+
const q = slot * 12;
|
|
436
|
+
for (let k = 0; k < 12; k++) {
|
|
437
|
+
p[q + k] = fresh ? g[offset + k] : f[o + k];
|
|
438
|
+
f[o + k] = g[offset + k];
|
|
439
|
+
}
|
|
440
|
+
this.markDirty(slot);
|
|
441
|
+
this.markPrevDirty(slot);
|
|
442
|
+
if (this.movedFrame[slot] !== this.frame) {
|
|
443
|
+
this.movedFrame[slot] = this.frame;
|
|
444
|
+
if (this.movedCount >= this.moved.length) {
|
|
445
|
+
const grown = new Uint32Array(this.moved.length * 2);
|
|
446
|
+
grown.set(this.moved);
|
|
447
|
+
this.moved = grown;
|
|
448
|
+
}
|
|
449
|
+
this.moved[this.movedCount++] = slot;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The floating origin moved (spec 0040): adds `x, y, z` to every slot's current and previous
|
|
454
|
+
* translation, so this frame's moves compare against last frame's pose in the new frame and
|
|
455
|
+
* motion vectors stay continuous. Runs once per shift, not per frame.
|
|
456
|
+
*/
|
|
457
|
+
shiftOrigin(x, y, z) {
|
|
458
|
+
const f = this.f32;
|
|
459
|
+
const p = this.prev;
|
|
460
|
+
for (let s = 0; s < this.high; s++) {
|
|
461
|
+
const o = s * INSTANCE_FLOATS;
|
|
462
|
+
const q = s * 12;
|
|
463
|
+
f[o + 3] = f[o + 3] + x;
|
|
464
|
+
f[o + 7] = f[o + 7] + y;
|
|
465
|
+
f[o + 11] = f[o + 11] + z;
|
|
466
|
+
p[q + 3] = p[q + 3] + x;
|
|
467
|
+
p[q + 7] = p[q + 7] + y;
|
|
468
|
+
p[q + 11] = p[q + 11] + z;
|
|
469
|
+
}
|
|
470
|
+
if (this.high === 0) return;
|
|
471
|
+
this.boundsDirty = true;
|
|
472
|
+
this.dirty.fill(1, 0, this.high);
|
|
473
|
+
this.prevDirty.fill(1, 0, this.high);
|
|
474
|
+
this.dirtyLo = Math.min(this.dirtyLo, 0);
|
|
475
|
+
this.dirtyHi = Math.max(this.dirtyHi, this.high - 1);
|
|
476
|
+
this.prevLo = 0;
|
|
477
|
+
this.prevHi = Math.max(this.prevHi, this.high - 1);
|
|
478
|
+
}
|
|
479
|
+
markPrevDirty(slot) {
|
|
480
|
+
if (this.prevDirty[slot]) return;
|
|
481
|
+
this.prevDirty[slot] = 1;
|
|
482
|
+
if (slot < this.prevLo) this.prevLo = slot;
|
|
483
|
+
if (slot > this.prevHi) this.prevHi = slot;
|
|
484
|
+
}
|
|
485
|
+
/** Slots that moved last frame but not this one: their previous transform catches up. */
|
|
486
|
+
settleMoved() {
|
|
487
|
+
const f = this.f32;
|
|
488
|
+
const p = this.prev;
|
|
489
|
+
for (let i = 0; i < this.lastMovedCount; i++) {
|
|
490
|
+
const slot = this.lastMoved[i];
|
|
491
|
+
if (this.movedFrame[slot] === this.frame || slot >= this.high) continue;
|
|
492
|
+
const o = slot * INSTANCE_FLOATS;
|
|
493
|
+
const q = slot * 12;
|
|
494
|
+
for (let k = 0; k < 12; k++) p[q + k] = f[o + k];
|
|
495
|
+
this.markPrevDirty(slot);
|
|
496
|
+
}
|
|
497
|
+
const t2 = this.lastMoved;
|
|
498
|
+
this.lastMoved = this.moved;
|
|
499
|
+
this.lastMovedCount = this.movedCount;
|
|
500
|
+
this.moved = t2;
|
|
501
|
+
this.movedCount = 0;
|
|
502
|
+
this.frame++;
|
|
503
|
+
}
|
|
504
|
+
/** Assigns a slot to a batch (>= 0), a LOD set (-2 - index), or nothing (-1). */
|
|
505
|
+
assign(slot, assignment) {
|
|
506
|
+
const old = this.batchOf[slot];
|
|
507
|
+
if (old === assignment) return;
|
|
508
|
+
if (old >= 0) {
|
|
509
|
+
this.batches[old].count--;
|
|
510
|
+
this.removeMember(slot, this.batches[old]);
|
|
511
|
+
} else if (old <= -2) {
|
|
512
|
+
const set = this.lodSets[-2 - old];
|
|
513
|
+
set.count--;
|
|
514
|
+
for (const b of set.batches) this.batches[b].count--;
|
|
515
|
+
}
|
|
516
|
+
if (assignment >= 0) {
|
|
517
|
+
this.batches[assignment].count++;
|
|
518
|
+
this.addMember(slot, this.batches[assignment]);
|
|
519
|
+
} else if (assignment <= -2) {
|
|
520
|
+
const set = this.lodSets[-2 - assignment];
|
|
521
|
+
set.count++;
|
|
522
|
+
for (const b of set.batches) this.batches[b].count++;
|
|
523
|
+
}
|
|
524
|
+
this.batchOf[slot] = assignment;
|
|
525
|
+
this.u32[slot * INSTANCE_FLOATS + 12] = assignment >= 0 ? assignment : assignment <= -2 ? (LOD_BIT | -2 - assignment) >>> 0 : NO_BATCH;
|
|
526
|
+
this.markDirty(slot);
|
|
527
|
+
}
|
|
528
|
+
/** A plain batch assignment. */
|
|
529
|
+
setBatch(slot, index) {
|
|
530
|
+
this.assign(slot, index);
|
|
531
|
+
}
|
|
532
|
+
addMember(slot, batch) {
|
|
533
|
+
if (batch.memberCount >= batch.members.length) {
|
|
534
|
+
const grown = new Uint32Array(batch.members.length * 2);
|
|
535
|
+
grown.set(batch.members);
|
|
536
|
+
batch.members = grown;
|
|
537
|
+
}
|
|
538
|
+
this.memberIndex[slot] = batch.memberCount;
|
|
539
|
+
batch.members[batch.memberCount++] = slot;
|
|
540
|
+
}
|
|
541
|
+
removeMember(slot, batch) {
|
|
542
|
+
const i = this.memberIndex[slot];
|
|
543
|
+
if (i < 0) return;
|
|
544
|
+
const last = batch.members[--batch.memberCount];
|
|
545
|
+
batch.members[i] = last;
|
|
546
|
+
this.memberIndex[last] = i;
|
|
547
|
+
this.memberIndex[slot] = -1;
|
|
548
|
+
}
|
|
549
|
+
batchFor(mesh, material, meshGuid, materialGuid) {
|
|
550
|
+
let byMaterial = this.byMesh.get(mesh);
|
|
551
|
+
if (!byMaterial) {
|
|
552
|
+
byMaterial = /* @__PURE__ */ new Map();
|
|
553
|
+
this.byMesh.set(mesh, byMaterial);
|
|
554
|
+
}
|
|
555
|
+
let batch = byMaterial.get(material);
|
|
556
|
+
if (!batch) {
|
|
557
|
+
batch = {
|
|
558
|
+
index: this.batches.length,
|
|
559
|
+
mesh,
|
|
560
|
+
material,
|
|
561
|
+
meshGuid,
|
|
562
|
+
materialGuid,
|
|
563
|
+
count: 0,
|
|
564
|
+
ready: false,
|
|
565
|
+
transparent: false,
|
|
566
|
+
deferrable: true,
|
|
567
|
+
members: new Uint32Array(16),
|
|
568
|
+
memberCount: 0,
|
|
569
|
+
region: 0,
|
|
570
|
+
scratch: new Uint32Array(64),
|
|
571
|
+
scratchCount: 0,
|
|
572
|
+
forwardScratch: new Uint32Array(16),
|
|
573
|
+
forwardCount: 0
|
|
574
|
+
};
|
|
575
|
+
byMaterial.set(material, batch);
|
|
576
|
+
this.batches.push(batch);
|
|
577
|
+
if (!this.materialOrder.has(material))
|
|
578
|
+
this.materialOrder.set(material, this.materialOrder.size);
|
|
579
|
+
if (!this.meshOrder.has(mesh)) this.meshOrder.set(mesh, this.meshOrder.size);
|
|
580
|
+
let lo = 0;
|
|
581
|
+
let hi = this.sorted.length;
|
|
582
|
+
while (lo < hi) {
|
|
583
|
+
const mid = lo + hi >> 1;
|
|
584
|
+
if (this.compareBatches(this.sorted[mid], batch, this.batchKey) <= 0) lo = mid + 1;
|
|
585
|
+
else hi = mid;
|
|
586
|
+
}
|
|
587
|
+
this.sorted.splice(lo, 0, batch);
|
|
588
|
+
this.structureVersion++;
|
|
589
|
+
}
|
|
590
|
+
return batch;
|
|
591
|
+
}
|
|
592
|
+
/** The LOD set for a Lod value and material (created on first use). Undefined if a mesh is missing. */
|
|
593
|
+
lodSetFor(lod, material, materialGuid, meshes) {
|
|
594
|
+
const levels = lod.levels.slice(0, MAX_LOD_LEVELS);
|
|
595
|
+
const resolved = [];
|
|
596
|
+
for (const level of levels) {
|
|
597
|
+
const mesh = meshes.get(level.mesh);
|
|
598
|
+
if (!mesh) return void 0;
|
|
599
|
+
resolved.push(mesh);
|
|
600
|
+
}
|
|
601
|
+
const key = `${levels.map((l, i) => `${idOf(resolved[i])}@${l.screenSize}`).join(",")}|${lod.hysteresis}|${lod.bias}|${idOf(material)}`;
|
|
602
|
+
let set = this.lodByKey.get(key);
|
|
603
|
+
if (!set) {
|
|
604
|
+
set = {
|
|
605
|
+
index: this.lodSets.length,
|
|
606
|
+
key,
|
|
607
|
+
batches: resolved.map(
|
|
608
|
+
(mesh, i) => this.batchFor(mesh, material, levels[i].mesh?.guid, materialGuid).index
|
|
609
|
+
),
|
|
610
|
+
thresholds: Float32Array.from(levels.map((l) => l.screenSize)),
|
|
611
|
+
hysteresis: lod.hysteresis,
|
|
612
|
+
bias: lod.bias,
|
|
613
|
+
count: 0
|
|
614
|
+
};
|
|
615
|
+
this.lodSets.push(set);
|
|
616
|
+
this.lodByKey.set(key, set);
|
|
617
|
+
this.structureVersion++;
|
|
618
|
+
}
|
|
619
|
+
return set;
|
|
620
|
+
}
|
|
621
|
+
/** Orders batches by pipeline (set by the renderer: material type and variant). */
|
|
622
|
+
batchKey = () => 0;
|
|
623
|
+
/** Draw order: grouped by pipeline, then material, then mesh, so switches stay rare. */
|
|
624
|
+
sortBatches(key = this.batchKey) {
|
|
625
|
+
this.sorted.sort((a, b) => this.compareBatches(a, b, key));
|
|
626
|
+
}
|
|
627
|
+
/** Materials and meshes in the order their first batch appeared (draw order within a pipeline). */
|
|
628
|
+
materialOrder = /* @__PURE__ */ new Map();
|
|
629
|
+
meshOrder = /* @__PURE__ */ new Map();
|
|
630
|
+
compareBatches(a, b, key) {
|
|
631
|
+
return key(a) - key(b) || this.materialOrder.get(a.material) - this.materialOrder.get(b.material) || this.meshOrder.get(a.mesh) - this.meshOrder.get(b.mesh);
|
|
632
|
+
}
|
|
633
|
+
/** Writes a slot's visibility range (packed as two halves, as the GPU reads it). */
|
|
634
|
+
setRange(slot, start, end) {
|
|
635
|
+
halfScratch[0] = start;
|
|
636
|
+
halfScratch[1] = end;
|
|
637
|
+
const h = toHalf(halfScratch);
|
|
638
|
+
const packed = (h[0] | h[1] << 16) >>> 0;
|
|
639
|
+
const o = slot * INSTANCE_FLOATS + 14;
|
|
640
|
+
if (this.u32[o] !== packed) {
|
|
641
|
+
this.u32[o] = packed;
|
|
642
|
+
this.markDirty(slot);
|
|
643
|
+
}
|
|
644
|
+
this.ranges[slot * 2] = halfToFloat(h[0]);
|
|
645
|
+
this.ranges[slot * 2 + 1] = halfToFloat(h[1]);
|
|
646
|
+
}
|
|
647
|
+
/** Uploads dirty slots in coalesced runs. Returns bytes written. */
|
|
648
|
+
upload() {
|
|
649
|
+
if (this.generation !== this.gpu.generation) {
|
|
650
|
+
this.generation = this.gpu.generation;
|
|
651
|
+
this.layout = createInstanceLayout(this.gpu);
|
|
652
|
+
this.bindGroup = void 0;
|
|
653
|
+
this.gpuBindGroup = void 0;
|
|
654
|
+
this.dirtyLo = 0;
|
|
655
|
+
this.dirtyHi = this.high - 1;
|
|
656
|
+
this.dirty.fill(1, 0, this.high);
|
|
657
|
+
this.prevLo = 0;
|
|
658
|
+
this.prevHi = this.high - 1;
|
|
659
|
+
this.prevDirty.fill(1, 0, this.high);
|
|
660
|
+
}
|
|
661
|
+
if (this.instanceBuffer.ensureCapacity(this.capacity * INSTANCE_BYTES) && this.high > 0) {
|
|
662
|
+
this.dirtyLo = 0;
|
|
663
|
+
this.dirtyHi = this.high - 1;
|
|
664
|
+
this.dirty.fill(1, 0, this.high);
|
|
665
|
+
}
|
|
666
|
+
if (this.prevBuffer.ensureCapacity(this.capacity * PREV_BYTES) && this.high > 0) {
|
|
667
|
+
this.prevLo = 0;
|
|
668
|
+
this.prevHi = this.high - 1;
|
|
669
|
+
this.prevDirty.fill(1, 0, this.high);
|
|
670
|
+
}
|
|
671
|
+
let start = -1;
|
|
672
|
+
for (let s = this.prevLo; s <= this.prevHi + 1; s++) {
|
|
673
|
+
if (s <= this.prevHi && this.prevDirty[s] === 1) {
|
|
674
|
+
this.prevDirty[s] = 0;
|
|
675
|
+
if (start < 0) start = s;
|
|
676
|
+
} else if (start >= 0) {
|
|
677
|
+
this.prevBuffer.write(this.prev, start * PREV_BYTES, start * 12, (s - start) * 12);
|
|
678
|
+
start = -1;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this.prevLo = Number.POSITIVE_INFINITY;
|
|
682
|
+
this.prevHi = -1;
|
|
683
|
+
let bytes = 0;
|
|
684
|
+
const lo = this.dirtyLo;
|
|
685
|
+
const hi = this.dirtyHi;
|
|
686
|
+
let runStart = -1;
|
|
687
|
+
for (let s = lo; s <= hi + 1; s++) {
|
|
688
|
+
const d = s <= hi && this.dirty[s] === 1;
|
|
689
|
+
if (d) {
|
|
690
|
+
this.dirty[s] = 0;
|
|
691
|
+
if (runStart < 0) runStart = s;
|
|
692
|
+
} else if (runStart >= 0) {
|
|
693
|
+
const n = s - runStart;
|
|
694
|
+
this.instanceBuffer.write(
|
|
695
|
+
this.f32,
|
|
696
|
+
runStart * INSTANCE_BYTES,
|
|
697
|
+
runStart * INSTANCE_FLOATS,
|
|
698
|
+
n * INSTANCE_FLOATS
|
|
699
|
+
);
|
|
700
|
+
bytes += n * INSTANCE_BYTES;
|
|
701
|
+
runStart = -1;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
this.dirtyLo = Number.POSITIVE_INFINITY;
|
|
705
|
+
this.dirtyHi = -1;
|
|
706
|
+
this.uploadedBytes = bytes;
|
|
707
|
+
return bytes;
|
|
708
|
+
}
|
|
709
|
+
beginFrame() {
|
|
710
|
+
this.visibleCount = 0;
|
|
711
|
+
}
|
|
712
|
+
// Transparent instances this cull: slot, batch, and view depth, sorted back to front.
|
|
713
|
+
transparentCount = 0;
|
|
714
|
+
forwardOnlyCount = 0;
|
|
715
|
+
tSlots = new Uint32Array(64);
|
|
716
|
+
tBatches = new Int32Array(64);
|
|
717
|
+
tDepths = new Float32Array(64);
|
|
718
|
+
tOrder = [];
|
|
719
|
+
/**
|
|
720
|
+
* CPU culling of every slot into `list` (and, for camera views, `transparent` back to front and
|
|
721
|
+
* deferred `forwardOnly`). Same tests as the GPU culler: bounding sphere against the frustum,
|
|
722
|
+
* visibility range, LOD level. `bounds` gets the union of what passed (cascade fitting). With
|
|
723
|
+
* `transparentOnly`, only blended batches are culled (the GPU culls the rest).
|
|
724
|
+
*/
|
|
725
|
+
cullCpu(list, params, bounds, transparent, forward, forwardOnly, transparentOnly = false) {
|
|
726
|
+
if (bounds) {
|
|
727
|
+
bounds[0] = bounds[1] = bounds[2] = Number.POSITIVE_INFINITY;
|
|
728
|
+
bounds[3] = bounds[4] = bounds[5] = Number.NEGATIVE_INFINITY;
|
|
729
|
+
}
|
|
730
|
+
list.length = 0;
|
|
731
|
+
list.visible = 0;
|
|
732
|
+
list.culled = 0;
|
|
733
|
+
list.hidden = 0;
|
|
734
|
+
list.pending = this.pending.size;
|
|
735
|
+
list.cullView = -1;
|
|
736
|
+
this.transparentCount = 0;
|
|
737
|
+
this.forwardOnlyCount = 0;
|
|
738
|
+
const batches = this.batches;
|
|
739
|
+
for (let b = 0; b < batches.length; b++) {
|
|
740
|
+
batches[b].scratchCount = 0;
|
|
741
|
+
batches[b].forwardCount = 0;
|
|
742
|
+
}
|
|
743
|
+
const f = this.f32;
|
|
744
|
+
const u32 = this.u32;
|
|
745
|
+
const mask = InstanceFlags.Visible | params.require;
|
|
746
|
+
const eye = params.eye;
|
|
747
|
+
const sphere = scratchSphere;
|
|
748
|
+
for (let s = 0; s < this.high; s++) {
|
|
749
|
+
const assignment = this.batchOf[s];
|
|
750
|
+
if (assignment === -1) continue;
|
|
751
|
+
const flags = u32[s * INSTANCE_FLOATS + 13];
|
|
752
|
+
if ((flags & mask) !== mask) {
|
|
753
|
+
if ((flags & InstanceFlags.Visible) === 0) list.hidden++;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
let set;
|
|
757
|
+
let b = assignment;
|
|
758
|
+
if (assignment <= -2) {
|
|
759
|
+
set = this.lodSets[-2 - assignment];
|
|
760
|
+
b = set.batches[0];
|
|
761
|
+
}
|
|
762
|
+
let batch = batches[b];
|
|
763
|
+
if (transparentOnly && !batch.transparent) continue;
|
|
764
|
+
if (!batch.ready) {
|
|
765
|
+
list.pending++;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (batch.transparent && (params.require !== 0 || !transparent)) continue;
|
|
769
|
+
if (flags & InstanceFlags.Skinned) sphereOfSlot(sphere, this, s, flags, batch.mesh.bounds);
|
|
770
|
+
else slotSphere(sphere, f, s * INSTANCE_FLOATS, batch.mesh.bounds);
|
|
771
|
+
if (params.planes && !sphereInFrustum(params.planes, sphere)) {
|
|
772
|
+
list.culled++;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
let distance = 0;
|
|
776
|
+
if (eye) {
|
|
777
|
+
const dx = sphere[0] - eye[0];
|
|
778
|
+
const dy = sphere[1] - eye[1];
|
|
779
|
+
const dz = sphere[2] - eye[2];
|
|
780
|
+
distance = f32(Math.sqrt(dx * dx + dy * dy + dz * dz));
|
|
781
|
+
}
|
|
782
|
+
if (flags & InstanceFlags.Range && eye) {
|
|
783
|
+
if (distance < this.ranges[s * 2] || distance >= this.ranges[s * 2 + 1]) {
|
|
784
|
+
list.culled++;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
let level = 0;
|
|
789
|
+
if (set) {
|
|
790
|
+
const state = params.lodState;
|
|
791
|
+
const previous = state ? state[s] : LOD_UNSET;
|
|
792
|
+
if (params.updateLod || !state) {
|
|
793
|
+
const size = lodSize(sphere[3], distance, params) * 2 ** set.bias;
|
|
794
|
+
level = selectLod(size, set.thresholds, set.batches.length, set.hysteresis, previous);
|
|
795
|
+
if (state) state[s] = level;
|
|
796
|
+
} else {
|
|
797
|
+
level = previous;
|
|
798
|
+
}
|
|
799
|
+
if (level >= set.batches.length) {
|
|
800
|
+
list.culled++;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
b = set.batches[level];
|
|
804
|
+
batch = batches[b];
|
|
805
|
+
if (!batch.ready) {
|
|
806
|
+
list.pending++;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
const entry = (s | level << 28) >>> 0;
|
|
811
|
+
if (bounds) {
|
|
812
|
+
for (let k = 0; k < 3; k++) {
|
|
813
|
+
if (sphere[k] - sphere[3] < bounds[k]) bounds[k] = sphere[k] - sphere[3];
|
|
814
|
+
if (sphere[k] + sphere[3] > bounds[k + 3]) bounds[k + 3] = sphere[k] + sphere[3];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (batch.transparent) {
|
|
818
|
+
this.pushTransparent(entry, b, eye, forward);
|
|
819
|
+
list.visible++;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (forwardOnly && !batch.deferrable) {
|
|
823
|
+
if (batch.forwardCount >= batch.forwardScratch.length) {
|
|
824
|
+
const grown = new Uint32Array(batch.forwardScratch.length * 2);
|
|
825
|
+
grown.set(batch.forwardScratch);
|
|
826
|
+
batch.forwardScratch = grown;
|
|
827
|
+
}
|
|
828
|
+
batch.forwardScratch[batch.forwardCount++] = entry;
|
|
829
|
+
this.forwardOnlyCount++;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (batch.scratchCount >= batch.scratch.length) {
|
|
833
|
+
const grown = new Uint32Array(batch.scratch.length * 2);
|
|
834
|
+
grown.set(batch.scratch);
|
|
835
|
+
batch.scratch = grown;
|
|
836
|
+
}
|
|
837
|
+
batch.scratch[batch.scratchCount++] = entry;
|
|
838
|
+
list.visible++;
|
|
839
|
+
}
|
|
840
|
+
this.pack(list);
|
|
841
|
+
if (transparent) this.packTransparent(transparent);
|
|
842
|
+
if (forwardOnly) {
|
|
843
|
+
for (const batch of this.batches) {
|
|
844
|
+
const n = batch.forwardCount;
|
|
845
|
+
batch.forwardCount = 0;
|
|
846
|
+
batch.scratchCount = n;
|
|
847
|
+
if (n > 0) {
|
|
848
|
+
if (batch.scratch.length < n) batch.scratch = new Uint32Array(batch.forwardScratch.length);
|
|
849
|
+
batch.scratch.set(batch.forwardScratch.subarray(0, n));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
this.pack(forwardOnly);
|
|
853
|
+
forwardOnly.visible = this.forwardOnlyCount;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/** Transparent-only CPU cull over blended batches' members (LOD slots aren't members). */
|
|
857
|
+
cullTransparentMembers(list, params, forward) {
|
|
858
|
+
this.transparentCount = 0;
|
|
859
|
+
const u32 = this.u32;
|
|
860
|
+
const eye = params.eye;
|
|
861
|
+
const sphere = scratchSphere;
|
|
862
|
+
for (const batch of this.batches) {
|
|
863
|
+
if (!batch.transparent || !batch.ready || batch.memberCount === 0) continue;
|
|
864
|
+
for (let m = 0; m < batch.memberCount; m++) {
|
|
865
|
+
const s = batch.members[m];
|
|
866
|
+
const flags = u32[s * INSTANCE_FLOATS + 13];
|
|
867
|
+
if ((flags & InstanceFlags.Visible) === 0) continue;
|
|
868
|
+
sphereOfSlot(sphere, this, s, flags, batch.mesh.bounds);
|
|
869
|
+
if (params.planes && !sphereInFrustum(params.planes, sphere)) continue;
|
|
870
|
+
if (flags & InstanceFlags.Range && eye) {
|
|
871
|
+
const dx = sphere[0] - eye[0];
|
|
872
|
+
const dy = sphere[1] - eye[1];
|
|
873
|
+
const dz = sphere[2] - eye[2];
|
|
874
|
+
const distance = f32(Math.sqrt(dx * dx + dy * dy + dz * dz));
|
|
875
|
+
if (distance < this.ranges[s * 2] || distance >= this.ranges[s * 2 + 1]) continue;
|
|
876
|
+
}
|
|
877
|
+
this.pushTransparent(s, batch.index, eye, forward);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
this.packTransparent(list);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* CPU culling with positional arguments: planes, flag mask, bounds, transparent list, eye, view
|
|
884
|
+
* direction, and the deferred forward-only list. LOD selects fresh (no hysteresis state).
|
|
885
|
+
*/
|
|
886
|
+
cull(list, planes, require2 = 0, bounds, transparent, eye, forward, forwardOnly) {
|
|
887
|
+
legacy.planes = planes;
|
|
888
|
+
legacy.require = require2;
|
|
889
|
+
legacy.eye = eye;
|
|
890
|
+
this.cullCpu(list, legacy, bounds, transparent, forward, forwardOnly);
|
|
891
|
+
}
|
|
892
|
+
pushTransparent(entry, batch, eye, forward) {
|
|
893
|
+
const n = this.transparentCount;
|
|
894
|
+
if (n >= this.tSlots.length) {
|
|
895
|
+
const size = this.tSlots.length * 2;
|
|
896
|
+
const s = new Uint32Array(size);
|
|
897
|
+
s.set(this.tSlots);
|
|
898
|
+
this.tSlots = s;
|
|
899
|
+
const b = new Int32Array(size);
|
|
900
|
+
b.set(this.tBatches);
|
|
901
|
+
this.tBatches = b;
|
|
902
|
+
const d = new Float32Array(size);
|
|
903
|
+
d.set(this.tDepths);
|
|
904
|
+
this.tDepths = d;
|
|
905
|
+
}
|
|
906
|
+
const o = (entry & SLOT_MASK) * INSTANCE_FLOATS;
|
|
907
|
+
let depth = 0;
|
|
908
|
+
if (eye && forward) {
|
|
909
|
+
depth = (this.f32[o + 3] - eye[0]) * forward[0] + (this.f32[o + 7] - eye[1]) * forward[1] + (this.f32[o + 11] - eye[2]) * forward[2];
|
|
910
|
+
}
|
|
911
|
+
this.tSlots[n] = entry;
|
|
912
|
+
this.tBatches[n] = batch;
|
|
913
|
+
this.tDepths[n] = depth;
|
|
914
|
+
this.transparentCount = n + 1;
|
|
915
|
+
}
|
|
916
|
+
/** Sorts this cull's transparent instances back to front; runs of one batch share a draw. */
|
|
917
|
+
packTransparent(list) {
|
|
918
|
+
list.length = 0;
|
|
919
|
+
list.cullView = -1;
|
|
920
|
+
const n = this.transparentCount;
|
|
921
|
+
if (n === 0) {
|
|
922
|
+
list.visible = 0;
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const order = this.tOrder;
|
|
926
|
+
order.length = n;
|
|
927
|
+
for (let i = 0; i < n; i++) order[i] = i;
|
|
928
|
+
const depths = this.tDepths;
|
|
929
|
+
order.sort((a, b) => depths[b] - depths[a] || a - b);
|
|
930
|
+
this.reserve(n);
|
|
931
|
+
let item;
|
|
932
|
+
for (let k = 0; k < n; k++) {
|
|
933
|
+
const i = order[k];
|
|
934
|
+
const batch = this.batches[this.tBatches[i]];
|
|
935
|
+
if (!item || item.batch !== batch) {
|
|
936
|
+
item = this.item(list, batch);
|
|
937
|
+
item.first = this.visibleCount;
|
|
938
|
+
item.count = 0;
|
|
939
|
+
}
|
|
940
|
+
this.visible[this.visibleCount++] = this.tSlots[i];
|
|
941
|
+
item.count++;
|
|
942
|
+
}
|
|
943
|
+
list.visible = n;
|
|
944
|
+
}
|
|
945
|
+
reserve(n) {
|
|
946
|
+
if (this.visibleCount + n <= this.visible.length) return;
|
|
947
|
+
let size = this.visible.length * 2;
|
|
948
|
+
while (size < this.visibleCount + n) size *= 2;
|
|
949
|
+
const grown = new Uint32Array(size);
|
|
950
|
+
grown.set(this.visible.subarray(0, this.visibleCount));
|
|
951
|
+
this.visible = grown;
|
|
952
|
+
}
|
|
953
|
+
item(list, batch) {
|
|
954
|
+
let item = list.items[list.length];
|
|
955
|
+
if (!item) {
|
|
956
|
+
item = { batch, first: 0, count: 0, indirect: -1 };
|
|
957
|
+
list.items.push(item);
|
|
958
|
+
}
|
|
959
|
+
item.batch = batch;
|
|
960
|
+
item.indirect = -1;
|
|
961
|
+
list.length++;
|
|
962
|
+
return item;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* False only when every instance of a small batch is hidden (Visibility), so views skip its
|
|
966
|
+
* draw. The GPU culler otherwise issues one indirect draw per batch, empty or not: a planet's
|
|
967
|
+
* pooled chunks are mostly hidden, and empty draws still cost their binds and calls.
|
|
968
|
+
*/
|
|
969
|
+
anyVisible(batch) {
|
|
970
|
+
const n = batch.memberCount;
|
|
971
|
+
if (n === 0 || n !== batch.count || n > 8) return true;
|
|
972
|
+
for (let i = 0; i < n; i++) {
|
|
973
|
+
if (this.u32[batch.members[i] * INSTANCE_FLOATS + 13] & InstanceFlags.Visible) return true;
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
nearKeys = new Float64Array(0);
|
|
978
|
+
nearItems = [];
|
|
979
|
+
/**
|
|
980
|
+
* Reorders a camera's opaque draws nearest first within each run that shares a pipeline and
|
|
981
|
+
* material, so the depth test rejects hidden fragments before they're shaded (overdraw on heavy
|
|
982
|
+
* shaders, like terrain, costs the most). Pipeline and material switches stay as few as before.
|
|
983
|
+
* A batch's distance is its first member's origin: exact for single-instance draws (terrain
|
|
984
|
+
* chunks), a fair guess for instanced ones. No allocation after warm-up: one typed-array sort of
|
|
985
|
+
* packed keys (run, log distance, index).
|
|
986
|
+
*/
|
|
987
|
+
orderNearFirst(list, eye) {
|
|
988
|
+
const n = list.length;
|
|
989
|
+
if (n < 2 || n >= 1 << 16) return;
|
|
990
|
+
if (this.nearKeys.length < n) this.nearKeys = new Float64Array(n * 2);
|
|
991
|
+
const keys = this.nearKeys.subarray(0, n);
|
|
992
|
+
const items = list.items;
|
|
993
|
+
const f = this.f32;
|
|
994
|
+
let run = 0;
|
|
995
|
+
for (let i = 0; i < n; i++) {
|
|
996
|
+
const batch = items[i].batch;
|
|
997
|
+
if (i > 0) {
|
|
998
|
+
const prev = items[i - 1].batch;
|
|
999
|
+
const a = prev.mesh.gpu?.share;
|
|
1000
|
+
const b = batch.mesh.gpu?.share;
|
|
1001
|
+
if (prev.material !== batch.material || this.batchKey(prev) !== this.batchKey(batch) || (a !== void 0 || b !== void 0) && a !== b)
|
|
1002
|
+
run++;
|
|
1003
|
+
}
|
|
1004
|
+
let q = 0;
|
|
1005
|
+
if (batch.memberCount > 0) {
|
|
1006
|
+
const o = batch.members[0] * INSTANCE_FLOATS;
|
|
1007
|
+
const dx = f[o + 3] - eye[0];
|
|
1008
|
+
const dy = f[o + 7] - eye[1];
|
|
1009
|
+
const dz = f[o + 11] - eye[2];
|
|
1010
|
+
q = Math.min(
|
|
1011
|
+
16777215,
|
|
1012
|
+
Math.floor(Math.log2(1 + Math.sqrt(dx * dx + dy * dy + dz * dz)) * 524288)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
keys[i] = Math.min(run, 8191) * 1099511627776 + q * 65536 + i;
|
|
1016
|
+
}
|
|
1017
|
+
if (run === n - 1) return;
|
|
1018
|
+
keys.sort();
|
|
1019
|
+
const copy = this.nearItems;
|
|
1020
|
+
for (let i = 0; i < n; i++) copy[i] = items[i];
|
|
1021
|
+
for (let i = 0; i < n; i++) items[i] = copy[keys[i] % 65536];
|
|
1022
|
+
}
|
|
1023
|
+
/** Copies each batch's scratch into the shared visible array, in draw order. */
|
|
1024
|
+
pack(list) {
|
|
1025
|
+
list.length = 0;
|
|
1026
|
+
for (const batch of this.sorted) {
|
|
1027
|
+
const n = batch.scratchCount;
|
|
1028
|
+
if (n === 0) continue;
|
|
1029
|
+
this.reserve(n);
|
|
1030
|
+
this.visible.set(batch.scratch.subarray(0, n), this.visibleCount);
|
|
1031
|
+
const item = this.item(list, batch);
|
|
1032
|
+
item.first = this.visibleCount;
|
|
1033
|
+
item.count = n;
|
|
1034
|
+
this.visibleCount += n;
|
|
1035
|
+
batch.scratchCount = 0;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
createBindGroup(label, visible) {
|
|
1039
|
+
return this.gpu.device.createBindGroup({
|
|
1040
|
+
label,
|
|
1041
|
+
layout: this.layout,
|
|
1042
|
+
entries: [
|
|
1043
|
+
{ binding: 0, resource: { buffer: this.instanceBuffer.buffer } },
|
|
1044
|
+
{ binding: 1, resource: { buffer: visible } },
|
|
1045
|
+
{ binding: 2, resource: { buffer: this.prevBuffer.buffer } },
|
|
1046
|
+
{ binding: 3, resource: { buffer: this.deform.recordBuffer.buffer } },
|
|
1047
|
+
{ binding: 4, resource: { buffer: this.deform.poseBuffer.buffer } },
|
|
1048
|
+
{ binding: 5, resource: { buffer: this.deform.vertexBuffer.buffer } }
|
|
1049
|
+
]
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
/** Uploads this frame's CPU visible lists and (re)builds the bind groups the vertex stage reads. */
|
|
1053
|
+
finishFrame() {
|
|
1054
|
+
if (this.generation !== this.gpu.generation) this.upload();
|
|
1055
|
+
if (this.visibleCount > 0) this.visibleBuffer.write(this.visible, 0, 0, this.visibleCount);
|
|
1056
|
+
this.deform.upload();
|
|
1057
|
+
const d = this.deform;
|
|
1058
|
+
const deformKey = `${d.recordBuffer.version}/${d.poseBuffer.version}/${d.vertexBuffer.version}`;
|
|
1059
|
+
const key = `${this.instanceBuffer.version}/${this.visibleBuffer.version}/${this.prevBuffer.version}/${deformKey}/${this.gpu.generation}`;
|
|
1060
|
+
if (!this.bindGroup || this.bound !== key) {
|
|
1061
|
+
this.bindGroup = this.createBindGroup("instances", this.visibleBuffer.buffer);
|
|
1062
|
+
this.bound = key;
|
|
1063
|
+
}
|
|
1064
|
+
const g = this.gpuVisible;
|
|
1065
|
+
if (g) {
|
|
1066
|
+
const gkey = `${this.instanceBuffer.version}/${g.version}/${this.prevBuffer.version}/${deformKey}/${this.gpu.generation}`;
|
|
1067
|
+
if (!this.gpuBindGroup || this.gpuBound !== gkey) {
|
|
1068
|
+
this.gpuBindGroup = this.createBindGroup("instances/gpu-culled", g.buffer);
|
|
1069
|
+
this.gpuBound = gkey;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const legacy = {
|
|
1075
|
+
planes: null,
|
|
1076
|
+
require: 0,
|
|
1077
|
+
eye: void 0,
|
|
1078
|
+
lodScale: 1,
|
|
1079
|
+
orthographic: false,
|
|
1080
|
+
lodState: void 0,
|
|
1081
|
+
updateLod: true
|
|
1082
|
+
};
|
|
1083
|
+
function lodSize(radius, distance, params) {
|
|
1084
|
+
if (params.orthographic) return f32(radius * params.lodScale);
|
|
1085
|
+
return f32(radius * params.lodScale / Math.max(distance, 1e-4));
|
|
1086
|
+
}
|
|
1087
|
+
const ids = /* @__PURE__ */ new WeakMap();
|
|
1088
|
+
let nextId = 1;
|
|
1089
|
+
function idOf(o) {
|
|
1090
|
+
let id = ids.get(o);
|
|
1091
|
+
if (id === void 0) {
|
|
1092
|
+
id = nextId++;
|
|
1093
|
+
ids.set(o, id);
|
|
1094
|
+
}
|
|
1095
|
+
return id;
|
|
1096
|
+
}
|
|
1097
|
+
function createInstanceLayout(gpu) {
|
|
1098
|
+
return gpu.layouts.bindGroupLayout({
|
|
1099
|
+
label: "instances",
|
|
1100
|
+
entries: [
|
|
1101
|
+
{
|
|
1102
|
+
binding: 0,
|
|
1103
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
1104
|
+
buffer: { type: "read-only-storage" }
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
binding: 1,
|
|
1108
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
1109
|
+
buffer: { type: "read-only-storage" }
|
|
1110
|
+
},
|
|
1111
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
1112
|
+
// Deform records, poses (joint matrices, morph weights), and per-vertex deform data.
|
|
1113
|
+
{ binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
1114
|
+
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
|
|
1115
|
+
{ binding: 5, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }
|
|
1116
|
+
]
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
const Instances = defineResource("render/Instances", {
|
|
1120
|
+
description: "The persistent GPU instance buffer: one slot per Mesh3d entity."
|
|
1121
|
+
});
|
|
1122
|
+
function tableChanged(table, since) {
|
|
1123
|
+
return table.lastStructural > since || table.lastChanged(GlobalTransform) > since || table.lastChanged(Mesh3d) > since || table.lastChanged(ComputedVisibility) > since || table.lastChanged(InstanceSlot) > since || table.has(MeshMaterial) && table.lastChanged(MeshMaterial) > since || table.has(Lod) && table.lastChanged(Lod) > since || table.has(VisibilityRange) && table.lastChanged(VisibilityRange) > since || table.has(InstanceData) && table.lastChanged(InstanceData) > since;
|
|
1124
|
+
}
|
|
1125
|
+
let zeros = new Uint32Array(1024);
|
|
1126
|
+
function zeroTicks(n) {
|
|
1127
|
+
if (zeros.length < n) zeros = new Uint32Array(Math.max(n, zeros.length * 2));
|
|
1128
|
+
return zeros;
|
|
1129
|
+
}
|
|
1130
|
+
const DeformPath = defineResource("render/DeformPath", {
|
|
1131
|
+
description: "Present when skinning and morph targets (skinningPlugin) are installed."
|
|
1132
|
+
});
|
|
1133
|
+
const warnedDeforms = /* @__PURE__ */ new WeakSet();
|
|
1134
|
+
function warnNoDeforms(world) {
|
|
1135
|
+
if (warnedDeforms.has(world)) return;
|
|
1136
|
+
warnedDeforms.add(world);
|
|
1137
|
+
world.tryResource(LogResource)?.log(
|
|
1138
|
+
"warn",
|
|
1139
|
+
"An entity has SkinnedMesh or MorphWeights, but skinningPlugin isn't installed; it draws in its rest pose",
|
|
1140
|
+
{
|
|
1141
|
+
code: "render/feature-missing",
|
|
1142
|
+
hint: "Add skinningPlugin from '@aethervtt/shard-render' (forwardPlugin includes it)."
|
|
1143
|
+
}
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const prepareInstances = defineSystem({
|
|
1147
|
+
name: "render/prepare-instances",
|
|
1148
|
+
description: "Assigns instance slots, writes changed transforms and batches, and uploads dirty slots.",
|
|
1149
|
+
setup: (world) => ({
|
|
1150
|
+
q: world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility, InstanceSlot] })
|
|
1151
|
+
}),
|
|
1152
|
+
run: ({ q }, world, ctx) => {
|
|
1153
|
+
const store = world.resource(Instances);
|
|
1154
|
+
const meshes = world.resource(Meshes);
|
|
1155
|
+
const materials = world.resource(Materials);
|
|
1156
|
+
const assets = world.resource(GpuAssetsResource);
|
|
1157
|
+
assets.beginFrame();
|
|
1158
|
+
store.beginFrame();
|
|
1159
|
+
const deforms = world.hasResource(DeformPath);
|
|
1160
|
+
const since = ctx.lastRunTick;
|
|
1161
|
+
let hidden = 0;
|
|
1162
|
+
let rows = 0;
|
|
1163
|
+
const tableHidden = store.tableHidden;
|
|
1164
|
+
for (const table of q.tables) {
|
|
1165
|
+
const n = table.count;
|
|
1166
|
+
if (n === 0) continue;
|
|
1167
|
+
const cached = tableHidden[table.id];
|
|
1168
|
+
if (cached !== void 0 && cached >= 0 && !tableChanged(table, since)) {
|
|
1169
|
+
hidden += cached;
|
|
1170
|
+
rows += n;
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
const hiddenBefore = hidden;
|
|
1174
|
+
const slots = table.column(InstanceSlot, "slot");
|
|
1175
|
+
const g = table.column(GlobalTransform, "matrix");
|
|
1176
|
+
const gChanged = table.changedTicks(GlobalTransform);
|
|
1177
|
+
const meshRefs = table.column(Mesh3d, "mesh");
|
|
1178
|
+
const meshChanged = table.changedTicks(Mesh3d);
|
|
1179
|
+
const hasMaterial = table.has(MeshMaterial);
|
|
1180
|
+
const materialRefs = hasMaterial ? table.column(MeshMaterial, "material") : void 0;
|
|
1181
|
+
const materialChanged = hasMaterial ? table.changedTicks(MeshMaterial) : zeroTicks(n);
|
|
1182
|
+
const hasLod = table.has(Lod);
|
|
1183
|
+
const lodLevels = hasLod ? table.column(Lod, "levels") : void 0;
|
|
1184
|
+
const lodChanged = hasLod ? table.changedTicks(Lod) : zeroTicks(n);
|
|
1185
|
+
const hasRange = table.has(VisibilityRange);
|
|
1186
|
+
const rangeStart = hasRange ? table.column(VisibilityRange, "start") : void 0;
|
|
1187
|
+
const rangeEnd = hasRange ? table.column(VisibilityRange, "end") : void 0;
|
|
1188
|
+
const rangeChanged = hasRange ? table.changedTicks(VisibilityRange) : zeroTicks(n);
|
|
1189
|
+
const hasData = !hasRange && table.has(InstanceData);
|
|
1190
|
+
const dataX = hasData ? table.column(InstanceData, "x") : void 0;
|
|
1191
|
+
const dataY = hasData ? table.column(InstanceData, "y") : void 0;
|
|
1192
|
+
const dataChanged = hasData ? table.changedTicks(InstanceData) : zeroTicks(n);
|
|
1193
|
+
const vis = table.column(ComputedVisibility, "visible");
|
|
1194
|
+
const deformed = table.has(SkinnedMesh) || table.has(MorphWeights);
|
|
1195
|
+
if (deformed && !deforms) warnNoDeforms(world);
|
|
1196
|
+
const tableFlags = (deforms && table.has(SkinnedMesh) ? InstanceFlags.Skinned : 0) | (deforms && table.has(MorphWeights) ? InstanceFlags.Morph : 0) | (table.has(NotShadowCaster) ? 0 : InstanceFlags.Caster) | (table.has(NotShadowReceiver) ? 0 : InstanceFlags.Receiver) | (hasRange ? InstanceFlags.Range : 0) | (hasLod ? InstanceFlags.Lod : 0);
|
|
1197
|
+
let u32 = store.u32;
|
|
1198
|
+
let slotFlags = store.flags;
|
|
1199
|
+
for (let i = 0; i < n; i++) {
|
|
1200
|
+
let slot = slots[i] - 1;
|
|
1201
|
+
let fresh = false;
|
|
1202
|
+
if (slot < 0) {
|
|
1203
|
+
slot = store.alloc(table.entities[i]);
|
|
1204
|
+
slots[i] = slot + 1;
|
|
1205
|
+
fresh = true;
|
|
1206
|
+
u32 = store.u32;
|
|
1207
|
+
slotFlags = store.flags;
|
|
1208
|
+
}
|
|
1209
|
+
const o = slot * INSTANCE_FLOATS;
|
|
1210
|
+
const visible = vis[i] !== 0;
|
|
1211
|
+
if (!visible) hidden++;
|
|
1212
|
+
const flags = tableFlags | (visible ? InstanceFlags.Visible : 0);
|
|
1213
|
+
const oldFlags = slotFlags[slot];
|
|
1214
|
+
if (fresh || gChanged[i] > since) store.moveSlot(slot, g, i * 12, fresh);
|
|
1215
|
+
if (fresh || ((oldFlags ^ flags) & InstanceFlags.Lod) !== 0 || meshChanged[i] > since || materialChanged[i] > since || lodChanged[i] > since) {
|
|
1216
|
+
store.meshRefs[slot] = meshRefs[i] ?? null;
|
|
1217
|
+
store.materialRefs[slot] = materialRefs?.[i] ?? null;
|
|
1218
|
+
store.lods[slot] = hasLod ? {
|
|
1219
|
+
levels: lodLevels[i],
|
|
1220
|
+
hysteresis: table.column(Lod, "hysteresis")[i],
|
|
1221
|
+
bias: table.column(Lod, "bias")[i]
|
|
1222
|
+
} : null;
|
|
1223
|
+
resolveSlot(store, slot, meshes, materials);
|
|
1224
|
+
}
|
|
1225
|
+
if (oldFlags !== flags) {
|
|
1226
|
+
u32[o + 13] = flags;
|
|
1227
|
+
slotFlags[slot] = flags;
|
|
1228
|
+
store.markDirty(slot);
|
|
1229
|
+
}
|
|
1230
|
+
if (fresh || rangeChanged[i] > since) {
|
|
1231
|
+
if (hasRange) store.setRange(slot, rangeStart[i], rangeEnd[i]);
|
|
1232
|
+
}
|
|
1233
|
+
if (hasData && (fresh || dataChanged[i] > since))
|
|
1234
|
+
store.setRange(slot, dataX[i], dataY[i]);
|
|
1235
|
+
}
|
|
1236
|
+
tableHidden[table.id] = hidden - hiddenBefore;
|
|
1237
|
+
rows += n;
|
|
1238
|
+
}
|
|
1239
|
+
store.settleMoved();
|
|
1240
|
+
store.hiddenCount = hidden;
|
|
1241
|
+
store.drawableCount = rows - store.pending.size;
|
|
1242
|
+
if (store.pending.size > 0) {
|
|
1243
|
+
for (const slot of store.pending) resolveSlot(store, slot, meshes, materials);
|
|
1244
|
+
}
|
|
1245
|
+
for (const batch of store.batches) {
|
|
1246
|
+
if (batch.count === 0) continue;
|
|
1247
|
+
const meshGone = batch.meshGuid !== void 0 && meshes.byGuid(batch.meshGuid) !== batch.mesh;
|
|
1248
|
+
const materialGone = batch.materialGuid !== void 0 && materials.byGuid(batch.materialGuid) !== batch.material;
|
|
1249
|
+
if (meshGone || materialGone) {
|
|
1250
|
+
for (let s = 0; s < store.high; s++) {
|
|
1251
|
+
const a = store.batchOf[s];
|
|
1252
|
+
const inSet = a <= -2 && store.lodSets[-2 - a].batches.includes(batch.index);
|
|
1253
|
+
if (a === batch.index || inSet) resolveSlot(store, s, meshes, materials);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
for (const batch of store.batches) {
|
|
1258
|
+
batch.ready = batch.count > 0 && assets.materialReady(world, batch.material);
|
|
1259
|
+
const material = batch.material;
|
|
1260
|
+
batch.transparent = isTransparent(material.type.blendOf(material.value));
|
|
1261
|
+
batch.deferrable = material.type.deferrable(material.value);
|
|
1262
|
+
}
|
|
1263
|
+
store.upload();
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
function resolveSlot(store, slot, meshes, materials) {
|
|
1267
|
+
const meshRef = store.meshRefs[slot];
|
|
1268
|
+
const materialRef = store.materialRefs[slot];
|
|
1269
|
+
const material = materialRef ? materials.get(materialRef) : store.defaultMaterial;
|
|
1270
|
+
const lod = store.lods[slot];
|
|
1271
|
+
if (lod && lod.levels.length > 0) {
|
|
1272
|
+
const set = material ? store.lodSetFor(lod, material, materialRef?.guid ?? void 0, meshes) : void 0;
|
|
1273
|
+
if (!set) {
|
|
1274
|
+
store.assign(slot, -1);
|
|
1275
|
+
store.pending.add(slot);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
store.pending.delete(slot);
|
|
1279
|
+
store.assign(slot, -2 - set.index);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const mesh = meshes.get(meshRef);
|
|
1283
|
+
if (!mesh || !material) {
|
|
1284
|
+
store.assign(slot, -1);
|
|
1285
|
+
if (meshRef?.guid !== void 0 || !material) store.pending.add(slot);
|
|
1286
|
+
else store.pending.delete(slot);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
store.pending.delete(slot);
|
|
1290
|
+
store.assign(
|
|
1291
|
+
slot,
|
|
1292
|
+
store.batchFor(mesh, material, meshRef?.guid, materialRef?.guid ?? void 0).index
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
function observeInstanceRemovals(world) {
|
|
1296
|
+
world.observe(onRemove(Mesh3d), ({ entity, world: world2 }) => {
|
|
1297
|
+
if (!world2.has(entity, InstanceSlot)) return;
|
|
1298
|
+
const store = world2.tryResource(Instances);
|
|
1299
|
+
const value = world2.get(entity, InstanceSlot);
|
|
1300
|
+
if (store && value.slot > 0) store.release(value.slot - 1);
|
|
1301
|
+
const table = world2.entityTable(entity);
|
|
1302
|
+
table.column(InstanceSlot, "slot")[world2.entityRow(entity)] = 0;
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
export {
|
|
1306
|
+
DeformPath,
|
|
1307
|
+
INSTANCE_BYTES,
|
|
1308
|
+
INSTANCE_FLOATS,
|
|
1309
|
+
InstanceData,
|
|
1310
|
+
InstanceFlags,
|
|
1311
|
+
InstanceSlot,
|
|
1312
|
+
InstanceStore,
|
|
1313
|
+
Instances,
|
|
1314
|
+
LOD_BIT,
|
|
1315
|
+
LOD_UNSET,
|
|
1316
|
+
Lod,
|
|
1317
|
+
MAX_LOD_LEVELS,
|
|
1318
|
+
Mesh3d,
|
|
1319
|
+
MeshMaterial,
|
|
1320
|
+
MorphWeights,
|
|
1321
|
+
NO_BATCH,
|
|
1322
|
+
NotShadowCaster,
|
|
1323
|
+
NotShadowReceiver,
|
|
1324
|
+
SLOT_MASK,
|
|
1325
|
+
SkinnedMesh,
|
|
1326
|
+
VisibilityRange,
|
|
1327
|
+
createDrawList,
|
|
1328
|
+
createInstanceLayout,
|
|
1329
|
+
lodSize,
|
|
1330
|
+
observeInstanceRemovals,
|
|
1331
|
+
prepareInstances,
|
|
1332
|
+
selectLod,
|
|
1333
|
+
slotSphere,
|
|
1334
|
+
sphereInFrustum
|
|
1335
|
+
};
|