@aethervtt/shard-sprite 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.
@@ -0,0 +1,1093 @@
1
+ import {
2
+ defineResource,
3
+ defineSystem,
4
+ findComponent,
5
+ ProfilerResource,
6
+ ShardError
7
+ } from "@aethervtt/shard-core";
8
+ import { GpuBuffer } from "@aethervtt/shard-gpu";
9
+ import {
10
+ Cameras,
11
+ ComputedVisibility,
12
+ defineOverlay,
13
+ Gpu,
14
+ RenderPhase,
15
+ Shaders
16
+ } from "@aethervtt/shard-render";
17
+ import { LogResource } from "@aethervtt/shard-runtime";
18
+ import { Textures } from "@aethervtt/shard-texture";
19
+ import { GlobalTransform } from "@aethervtt/shard-transform";
20
+ import { TextureAtlases } from "./atlas.js";
21
+ import { Lighting2d, LightOccluder2d, PointLight2d, SpotLight2d } from "./lighting.js";
22
+ import {
23
+ occluderSegments,
24
+ spriteOutline,
25
+ tileChunkEdges,
26
+ transformSegments
27
+ } from "./occluders.js";
28
+ import { Sprite, Sprite2dSettings } from "./sprite.js";
29
+ import { Tilemap, TilemapDatas } from "./tilemap.js";
30
+ const LIGHT2D_FLOATS = 16;
31
+ const SEGMENT_FLOATS = 8;
32
+ const TILE_PIXELS = 16;
33
+ const TILE_MAX = 64;
34
+ const TILE_STRIDE = TILE_MAX + 1;
35
+ const SHADOW_RES = 1024;
36
+ const COARSE_RES = 32;
37
+ const MAX_SHADOWED = 64;
38
+ const FAR = 34028234663852886e22;
39
+ function lightCircle(viewProj, width, height, x, y, z, r, out, o) {
40
+ const m = viewProj;
41
+ const w0 = m[3] * x + m[7] * y + m[11] * z + m[15];
42
+ if (w0 <= 1e-6) return false;
43
+ const cx = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w0;
44
+ const cy = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w0;
45
+ const ax = m[0] * r / w0;
46
+ const ay = m[1] * r / w0;
47
+ const bx = m[4] * r / w0;
48
+ const by = m[5] * r / w0;
49
+ const hw = width / 2;
50
+ const hh = height / 2;
51
+ const rx = Math.sqrt(ax * ax * hw * hw + ay * ay * hh * hh);
52
+ const ry = Math.sqrt(bx * bx * hw * hw + by * by * hh * hh);
53
+ const px = (cx + 1) * hw;
54
+ const py = (1 - cy) * hh;
55
+ const rp = rx > ry ? rx : ry;
56
+ out[o] = px;
57
+ out[o + 1] = py;
58
+ out[o + 2] = rp;
59
+ out[o + 3] = 0;
60
+ return px + rp > 0 && px - rp < width && py + rp > 0 && py - rp < height;
61
+ }
62
+ function binLightsCpu(circles, count, tilesX, tilesY) {
63
+ const counts = new Uint32Array(tilesX * tilesY);
64
+ const indices = new Uint32Array(tilesX * tilesY * TILE_MAX);
65
+ for (let t = 0; t < tilesX * tilesY; t++) {
66
+ const lx = t % tilesX * TILE_PIXELS;
67
+ const ly = Math.floor(t / tilesX) * TILE_PIXELS;
68
+ let n = 0;
69
+ for (let i = 0; i < count; i++) {
70
+ const cx = circles[i * 4];
71
+ const cy = circles[i * 4 + 1];
72
+ const r = circles[i * 4 + 2];
73
+ const qx = Math.min(Math.max(cx, lx), lx + TILE_PIXELS) - cx;
74
+ const qy = Math.min(Math.max(cy, ly), ly + TILE_PIXELS) - cy;
75
+ if (qx * qx + qy * qy > r * r) continue;
76
+ if (n < TILE_MAX) indices[t * TILE_MAX + n] = i;
77
+ n++;
78
+ }
79
+ counts[t] = n;
80
+ }
81
+ return { counts, indices };
82
+ }
83
+ function shadowRowCpu(lx, ly, radius, layers, segments, count, out) {
84
+ out.fill(FAR, 0, SHADOW_RES);
85
+ const bits = new Uint32Array(segments.buffer, segments.byteOffset, segments.length);
86
+ const step = Math.PI * 2 / SHADOW_RES;
87
+ for (let s = 0; s < count; s++) {
88
+ const o = s * SEGMENT_FLOATS;
89
+ if ((bits[o + 5] & layers) >>> 0 === 0) continue;
90
+ const ax = segments[o] - lx;
91
+ const ay = segments[o + 1] - ly;
92
+ const ex = segments[o + 2] - lx - ax;
93
+ const ey = segments[o + 3] - ly - ay;
94
+ const len2 = ex * ex + ey * ey;
95
+ const t0 = len2 > 0 ? Math.min(1, Math.max(0, -(ax * ex + ay * ey) / len2)) : 0;
96
+ const nx = ax + ex * t0;
97
+ const ny = ay + ey * t0;
98
+ if (nx * nx + ny * ny >= radius * radius) continue;
99
+ const bx = ax + ex;
100
+ const by = ay + ey;
101
+ if (Math.abs(ax * by - ay * bx) < 1e-9) continue;
102
+ let start = Math.atan2(ay, ax);
103
+ let span = Math.atan2(by, bx) - start;
104
+ if (span > Math.PI) span -= Math.PI * 2;
105
+ if (span < -Math.PI) span += Math.PI * 2;
106
+ if (span < 0) {
107
+ start += span;
108
+ span = -span;
109
+ }
110
+ const first = Math.floor((start + Math.PI) / step);
111
+ const last = Math.min(Math.floor((start + span + Math.PI) / step), first + SHADOW_RES - 1);
112
+ const pen = segments[o + 4];
113
+ for (let k = first; k <= last; k++) {
114
+ const theta = Math.min(Math.max(-Math.PI + (k + 0.5) * step, start), start + span);
115
+ const dx = Math.cos(theta);
116
+ const dy = Math.sin(theta);
117
+ const den = dx * ey - dy * ex;
118
+ if (Math.abs(den) < 1e-12) continue;
119
+ const t = (ax * ey - ay * ex) / den;
120
+ if (t <= 0) continue;
121
+ const w = (k % SHADOW_RES + SHADOW_RES) % SHADOW_RES;
122
+ const v = Math.fround(t + pen);
123
+ if (v < out[w]) out[w] = v;
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ function shadowFactorCpu(row, softness, d, toX, toY) {
129
+ const step = Math.PI * 2 / SHADOW_RES;
130
+ const at = (k) => row[(k % SHADOW_RES + SHADOW_RES) % SHADOW_RES];
131
+ const fb = (Math.atan2(toY, toX) + Math.PI) / step;
132
+ if (softness <= 0) return d <= at(Math.floor(fb)) ? 1 : 0;
133
+ const search = Math.min(softness / Math.max(d, 1e-3) / step, 32);
134
+ let blockers = 0;
135
+ let sum = 0;
136
+ for (let i = 0; i < 8; i++) {
137
+ const o = at(Math.floor(fb + (i / 7 * 2 - 1) * search));
138
+ if (o < d) {
139
+ sum += o;
140
+ blockers++;
141
+ }
142
+ }
143
+ if (blockers === 0) return 1;
144
+ if (blockers === 8) return 0;
145
+ const blocker = sum / blockers;
146
+ const width = Math.min(softness * (d - blocker) / Math.max(blocker, 1e-3) / d / step, 32);
147
+ let lit = 0;
148
+ for (let i = 0; i < 24; i++) {
149
+ const x = fb + (i / 23 * 2 - 1) * width;
150
+ const k = Math.floor(x - 0.5);
151
+ const a = d <= at(k) ? 1 : 0;
152
+ const b = d <= at(k + 1) ? 1 : 0;
153
+ lit += a + (b - a) * (x - 0.5 - k);
154
+ }
155
+ return lit / 24;
156
+ }
157
+ class OccluderStore {
158
+ records = [];
159
+ index = /* @__PURE__ */ new Map();
160
+ tiles = /* @__PURE__ */ new Map();
161
+ /** Bumps whenever any occluder's world segments change. */
162
+ version = 0;
163
+ /** Tile chunks whose edges were rebuilt in the last update. */
164
+ chunkRebuilds = 0;
165
+ frame = 0;
166
+ /** Errors already logged, per entity and code. */
167
+ logged = /* @__PURE__ */ new Set();
168
+ /** Segments across all occluders (entities and tile chunks). */
169
+ get segmentCount() {
170
+ let n = 0;
171
+ for (const r of this.records) n += r.world.length / 4;
172
+ for (const t of this.tiles.values()) for (const w of t.world) n += w.length / 4;
173
+ return n;
174
+ }
175
+ }
176
+ let colliderDef;
177
+ function colliderOf(world, entity) {
178
+ if (!colliderDef) colliderDef = findComponent("physics/Collider") ?? null;
179
+ if (!colliderDef) return void 0;
180
+ return world.tryGet(entity, colliderDef);
181
+ }
182
+ function spriteShape(world, entity) {
183
+ const s = world.tryGet(entity, Sprite);
184
+ if (!s) return void 0;
185
+ const textures = world.resource(Textures);
186
+ const ppu = world.resource(Sprite2dSettings).pixelsPerUnit;
187
+ let outline;
188
+ let rw = 0;
189
+ let rh = 0;
190
+ let ax = s.anchor[0];
191
+ let ay = s.anchor[1];
192
+ const atlasRef = s.atlas;
193
+ if (atlasRef && (atlasRef.guid || atlasRef.path) && s.region) {
194
+ const atlas = world.resource(TextureAtlases).get(atlasRef);
195
+ if (!atlas) return void 0;
196
+ const r = atlas.region(s.region);
197
+ if (r < 0) return void 0;
198
+ rw = atlas.rects[r * 4 + 2];
199
+ rh = atlas.rects[r * 4 + 3];
200
+ outline = atlas.outlines[r];
201
+ const px = atlas.pivots[r * 2];
202
+ const py = atlas.pivots[r * 2 + 1];
203
+ if (px !== 0.5 || py !== 0.5) {
204
+ ax = px;
205
+ ay = py;
206
+ }
207
+ } else {
208
+ const ref = s.texture;
209
+ const texture = ref ? textures.get(ref) : void 0;
210
+ if (!texture) return void 0;
211
+ rw = texture.width;
212
+ rh = texture.height;
213
+ }
214
+ let w = s.size[0];
215
+ let h = s.size[1];
216
+ if (w === 0 && h === 0) {
217
+ w = rw / ppu;
218
+ h = rh / ppu;
219
+ }
220
+ return spriteOutline(outline, w, h, ax, ay, s.flipX, s.flipY);
221
+ }
222
+ function reportOnce(world, store, entity, err) {
223
+ const key = `${entity}|${err.code}`;
224
+ if (store.logged.has(key)) return;
225
+ store.logged.add(key);
226
+ world.tryResource(LogResource)?.error(err);
227
+ }
228
+ function updateOccluders(world, store, since, q) {
229
+ const frame = ++store.frame;
230
+ store.chunkRebuilds = 0;
231
+ if (!colliderDef) colliderDef = findComponent("physics/Collider") ?? null;
232
+ for (const table of q.occluders.tables) {
233
+ const n = table.count;
234
+ if (n === 0) continue;
235
+ const g = table.column(GlobalTransform, "matrix");
236
+ const oChanged = table.changedTicks(LightOccluder2d);
237
+ const gChanged = table.changedTicks(GlobalTransform);
238
+ const sChanged = table.has(Sprite) ? table.changedTicks(Sprite) : void 0;
239
+ const colliderChanged = colliderDef && table.has(colliderDef) ? table.changedTicks(colliderDef) : void 0;
240
+ for (let i = 0; i < n; i++) {
241
+ const entity = table.entities[i];
242
+ let at = store.index.get(entity);
243
+ let rec = at === void 0 ? void 0 : store.records[at];
244
+ const shapeChanged = !rec || oChanged[i] > since || sChanged !== void 0 && sChanged[i] > since || colliderChanged !== void 0 && colliderChanged[i] > since || rec.local.length === 0;
245
+ if (rec) rec.seen = frame;
246
+ if (!shapeChanged && gChanged[i] <= since) continue;
247
+ if (shapeChanged) {
248
+ const value = world.get(entity, LightOccluder2d);
249
+ let local = [];
250
+ try {
251
+ if (value.shape === "sprite") {
252
+ const outline = spriteShape(world, entity);
253
+ local = outline ? occluderSegments(value, { outline }) : [];
254
+ } else {
255
+ local = occluderSegments(value, { collider: colliderOf(world, entity) });
256
+ }
257
+ } catch (err) {
258
+ if (!(err instanceof ShardError)) throw err;
259
+ reportOnce(world, store, entity, err);
260
+ }
261
+ if (!rec) {
262
+ rec = {
263
+ entity,
264
+ local,
265
+ world: new Float32Array(local.length),
266
+ bounds: new Float32Array(4),
267
+ penetration: value.lightPenetration,
268
+ layers: value.layers >>> 0,
269
+ seen: frame
270
+ };
271
+ at = store.records.length;
272
+ store.records.push(rec);
273
+ store.index.set(entity, at);
274
+ }
275
+ rec.local = local;
276
+ rec.penetration = value.lightPenetration;
277
+ rec.layers = value.layers >>> 0;
278
+ if (rec.world.length !== local.length) rec.world = new Float32Array(local.length);
279
+ }
280
+ if (!rec) continue;
281
+ transformSegments(rec.local, g, i * 12, 1, 1, rec.world, 0, rec.bounds);
282
+ store.version++;
283
+ }
284
+ }
285
+ for (let k = store.records.length - 1; k >= 0; k--) {
286
+ const rec = store.records[k];
287
+ if (rec.seen === frame) continue;
288
+ const last = store.records.pop();
289
+ store.index.delete(rec.entity);
290
+ if (last !== rec) {
291
+ store.records[k] = last;
292
+ store.index.set(last.entity, k);
293
+ }
294
+ store.version++;
295
+ }
296
+ updateTileOccluders(world, store, frame, q.tilemaps);
297
+ }
298
+ function updateTileOccluders(world, store, frame, maps) {
299
+ const datas = world.resource(TilemapDatas);
300
+ for (const table of maps.tables) {
301
+ const g = table.column(GlobalTransform, "matrix");
302
+ const dataCol = table.column(Tilemap, "data");
303
+ const tileSize = table.column(Tilemap, "tileSize");
304
+ const chunkSize = table.column(Tilemap, "chunkSize");
305
+ for (let i = 0; i < table.count; i++) {
306
+ const data = datas.get(dataCol[i]);
307
+ if (!data) continue;
308
+ const entity = table.entities[i];
309
+ const cs = chunkSize[i];
310
+ for (let li = 0; li < data.layers.length; li++) {
311
+ const layer = data.layers[li];
312
+ if (!layer.occludes) continue;
313
+ const key = `${entity}/${li}`;
314
+ let t = store.tiles.get(key);
315
+ if (!t || t.layer !== layer || t.chunkSize !== cs) {
316
+ const chunksX = Math.ceil(layer.width / cs);
317
+ const chunksY = Math.ceil(layer.height / cs);
318
+ t = {
319
+ entity,
320
+ layerIndex: li,
321
+ layer,
322
+ chunkSize: cs,
323
+ chunksX,
324
+ chunksY,
325
+ editBase: layer.editBase - 1,
326
+ cursor: 0,
327
+ local: new Array(chunksX * chunksY),
328
+ world: new Array(chunksX * chunksY),
329
+ bounds: new Float32Array(chunksX * chunksY * 4),
330
+ affine: new Float32Array(12),
331
+ tileSize: [0, 0],
332
+ penetration: 0,
333
+ seen: frame
334
+ };
335
+ store.tiles.set(key, t);
336
+ }
337
+ t.seen = frame;
338
+ const chunks = t.chunksX * t.chunksY;
339
+ let moved = t.tileSize[0] !== tileSize[i * 2] || t.tileSize[1] !== tileSize[i * 2 + 1];
340
+ for (let k = 0; k < 12; k++) {
341
+ if (t.affine[k] !== g[i * 12 + k]) moved = true;
342
+ }
343
+ const rebuild = new Uint8Array(chunks);
344
+ let any = false;
345
+ if (t.editBase !== layer.editBase) {
346
+ rebuild.fill(1);
347
+ any = true;
348
+ t.editBase = layer.editBase;
349
+ t.cursor = layer.edits.length;
350
+ } else if (t.cursor < layer.edits.length) {
351
+ for (let k = t.cursor; k < layer.edits.length; k++) {
352
+ const e = layer.edits[k];
353
+ const x = e % layer.width;
354
+ const y = Math.floor(e / layer.width);
355
+ rebuild[Math.floor(y / cs) * t.chunksX + Math.floor(x / cs)] = 1;
356
+ any = true;
357
+ }
358
+ t.cursor = layer.edits.length;
359
+ }
360
+ if (!any && !moved) continue;
361
+ if (moved) {
362
+ t.affine.set(g.subarray(i * 12, i * 12 + 12));
363
+ t.tileSize = [tileSize[i * 2], tileSize[i * 2 + 1]];
364
+ t.penetration = 0.75 * Math.min(Math.abs(t.tileSize[0]), Math.abs(t.tileSize[1]));
365
+ }
366
+ for (let c = 0; c < chunks; c++) {
367
+ if (rebuild[c]) {
368
+ const edges = [];
369
+ tileChunkEdges(layer, cs, c, edges);
370
+ t.local[c] = edges;
371
+ store.chunkRebuilds++;
372
+ }
373
+ if (!rebuild[c] && !moved) continue;
374
+ const local = t.local[c] ?? [];
375
+ let w = t.world[c];
376
+ if (!w || w.length !== local.length) w = t.world[c] = new Float32Array(local.length);
377
+ transformSegments(local, t.affine, 0, t.tileSize[0], t.tileSize[1], w, 0, boundsScratch);
378
+ t.bounds.set(boundsScratch, c * 4);
379
+ }
380
+ store.version++;
381
+ }
382
+ }
383
+ }
384
+ for (const [key, t] of store.tiles) {
385
+ if (t.seen !== frame) {
386
+ store.tiles.delete(key);
387
+ store.version++;
388
+ }
389
+ }
390
+ }
391
+ const boundsScratch = new Float32Array(4);
392
+ class LightView2d {
393
+ name;
394
+ camera = 0;
395
+ lights = new Float32Array(LIGHT2D_FLOATS * 64);
396
+ lightBits = new Uint32Array(this.lights.buffer);
397
+ circles = new Float32Array(4 * 64);
398
+ entities = new Float64Array(64);
399
+ count = 0;
400
+ /** Light index per shadow row. */
401
+ shadowed = new Uint32Array(MAX_SHADOWED);
402
+ shadowedCount = 0;
403
+ segments = new Float32Array(SEGMENT_FLOATS * 256);
404
+ segmentBits = new Uint32Array(this.segments.buffer);
405
+ segmentCount = 0;
406
+ tilesX = 1;
407
+ tilesY = 1;
408
+ uniformData = new Float32Array(12);
409
+ uniformBits = new Uint32Array(this.uniformData.buffer);
410
+ /** Stats for describe. */
411
+ visible = 0;
412
+ culled = 0;
413
+ dropped = 0;
414
+ demoted = 0;
415
+ uploadedBytes = 0;
416
+ frame = 0;
417
+ segmentKey = -1;
418
+ gpu;
419
+ constructor(name) {
420
+ this.name = name;
421
+ }
422
+ ensureLights(n) {
423
+ if (this.entities.length >= n) return;
424
+ const lights = new Float32Array(n * LIGHT2D_FLOATS);
425
+ lights.set(this.lights);
426
+ this.lights = lights;
427
+ this.lightBits = new Uint32Array(lights.buffer);
428
+ const circles = new Float32Array(n * 4);
429
+ circles.set(this.circles);
430
+ this.circles = circles;
431
+ const entities = new Float64Array(n);
432
+ entities.set(this.entities);
433
+ this.entities = entities;
434
+ }
435
+ ensureSegments(n) {
436
+ if (this.segments.length >= n * SEGMENT_FLOATS) return;
437
+ let size = this.segments.length;
438
+ while (size < n * SEGMENT_FLOATS) size *= 2;
439
+ const s = new Float32Array(size);
440
+ s.set(this.segments);
441
+ this.segments = s;
442
+ this.segmentBits = new Uint32Array(s.buffer);
443
+ }
444
+ /** Uploads what changed. Segments go up only when the selection or an occluder changed. */
445
+ upload(gpu, segmentKey) {
446
+ let g = this.gpu;
447
+ if (!g || g.generation !== gpu.generation) g = this.gpu = createViewGpu(gpu, this.name);
448
+ let bytes = 0;
449
+ this.uniformBits[4] = this.count;
450
+ this.uniformBits[5] = this.tilesX;
451
+ this.uniformBits[6] = this.tilesY;
452
+ this.uniformBits[7] = this.shadowedCount;
453
+ this.uniformBits[8] = this.segmentCount;
454
+ g.uniform.write(this.uniformData);
455
+ bytes += 48;
456
+ if (this.count > 0) {
457
+ g.lights.write(this.lights, 0, 0, this.count * LIGHT2D_FLOATS);
458
+ g.circles.write(this.circles, 0, 0, this.count * 4);
459
+ bytes += this.count * (LIGHT2D_FLOATS + 4) * 4;
460
+ }
461
+ if (this.shadowedCount > 0) {
462
+ g.shadowed.write(this.shadowed, 0, 0, this.shadowedCount);
463
+ bytes += this.shadowedCount * 4;
464
+ }
465
+ g.tiles.ensureCapacity(this.tilesX * this.tilesY * TILE_STRIDE * 4);
466
+ g.shadowMap.ensureCapacity(Math.max(1, this.shadowedCount) * SHADOW_RES * 4);
467
+ g.shadowCoarse.ensureCapacity(Math.max(1, this.shadowedCount) * COARSE_RES * 8);
468
+ if (segmentKey !== this.segmentKey && this.segmentCount > 0) {
469
+ g.segments.write(this.segments, 0, 0, this.segmentCount * SEGMENT_FLOATS);
470
+ bytes += this.segmentCount * SEGMENT_FLOATS * 4;
471
+ }
472
+ this.segmentKey = segmentKey;
473
+ this.uploadedBytes = bytes;
474
+ }
475
+ }
476
+ function createViewGpu(gpu, name) {
477
+ const storage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC;
478
+ const buffer = (label, size, usage = storage) => new GpuBuffer(gpu, { label: `${name}/lights2d/${label}`, usage, size });
479
+ return {
480
+ generation: gpu.generation,
481
+ uniform: buffer("view", 48, GPUBufferUsage.UNIFORM),
482
+ lights: buffer("lights", LIGHT2D_FLOATS * 4 * 64),
483
+ circles: buffer("circles", 16 * 64),
484
+ tiles: buffer("tiles", TILE_STRIDE * 4 * 1024),
485
+ shadowMap: buffer("shadow-map", SHADOW_RES * 4 * 4),
486
+ shadowCoarse: buffer("shadow-coarse", COARSE_RES * 8 * 4),
487
+ segments: buffer("segments", SEGMENT_FLOATS * 4 * 256),
488
+ shadowed: buffer("shadowed", MAX_SHADOWED * 4)
489
+ };
490
+ }
491
+ class Lights2dStore {
492
+ views = /* @__PURE__ */ new Map();
493
+ occluders = new OccluderStore();
494
+ /** Budget warnings already logged, per view and code. */
495
+ warned = /* @__PURE__ */ new Set();
496
+ }
497
+ const Lights2d = defineResource("sprite/Lights2d", {
498
+ description: "The 2D lighting state: lights, shadow rows, and occluder segments per lit view, and the occluders in world space.",
499
+ init: () => new Lights2dStore()
500
+ });
501
+ let sortKeys = new Float64Array(256);
502
+ let sortIdx = new Uint32Array(256);
503
+ let stage = new Float32Array(256 * LIGHT2D_FLOATS);
504
+ let stageCircles = new Float32Array(256 * 4);
505
+ let stageShadows = new Uint8Array(256);
506
+ let stageEntities = new Float64Array(256);
507
+ function growStage(n) {
508
+ if (sortKeys.length >= n) return;
509
+ const size = Math.max(n, sortKeys.length * 2);
510
+ const grow = (a, make, per) => {
511
+ const b = make(size * per);
512
+ b.set(a);
513
+ return b;
514
+ };
515
+ sortKeys = grow(sortKeys, (k) => new Float64Array(k), 1);
516
+ sortIdx = grow(sortIdx, (k) => new Uint32Array(k), 1);
517
+ stage = grow(stage, (k) => new Float32Array(k), LIGHT2D_FLOATS);
518
+ stageCircles = grow(stageCircles, (k) => new Float32Array(k), 4);
519
+ stageShadows = grow(stageShadows, (k) => new Uint8Array(k), 1);
520
+ stageEntities = grow(stageEntities, (k) => new Float64Array(k), 1);
521
+ stageU32 = new Uint32Array(stage.buffer);
522
+ }
523
+ function insertionSort(idx, n) {
524
+ for (let i = 1; i < n; i++) {
525
+ const v = idx[i];
526
+ const key = sortKeys[v];
527
+ let j = i - 1;
528
+ while (j >= 0 && sortKeys[idx[j]] > key) {
529
+ idx[j + 1] = idx[j];
530
+ j--;
531
+ }
532
+ idx[j + 1] = v;
533
+ }
534
+ }
535
+ let stageU32 = new Uint32Array(stage.buffer);
536
+ const DEG = Math.PI / 180;
537
+ function stageLights(table, spot, cam, n, view) {
538
+ const def = spot ? SpotLight2d : PointLight2d;
539
+ const g = table.column(GlobalTransform, "matrix");
540
+ const vis = table.column(ComputedVisibility, "visible");
541
+ const color = table.column(def, "color");
542
+ const intensity = table.column(def, "intensity");
543
+ const radius = table.column(def, "radius");
544
+ const falloff = table.column(def, "falloff");
545
+ const height = table.column(def, "height");
546
+ const shadows = table.column(def, "shadows");
547
+ const softness = table.column(def, "softness");
548
+ const layers = table.column(def, "layers");
549
+ const inner = spot ? table.column(SpotLight2d, "innerAngle") : void 0;
550
+ const outer = spot ? table.column(SpotLight2d, "outerAngle") : void 0;
551
+ const cx = cam.position[0];
552
+ const cy = cam.position[1];
553
+ for (let i = 0; i < table.count; i++) {
554
+ if (!vis[i] || layers[i] === 0) continue;
555
+ const r = radius[i];
556
+ const s = intensity[i];
557
+ if (r <= 0 || s <= 0) continue;
558
+ const x = g[i * 12 + 3];
559
+ const y = g[i * 12 + 7];
560
+ if (n >= sortKeys.length) growStage(n + 1);
561
+ if (!lightCircle(
562
+ cam.viewProj,
563
+ cam.width,
564
+ cam.height,
565
+ x,
566
+ y,
567
+ g[i * 12 + 11],
568
+ r,
569
+ stageCircles,
570
+ n * 4
571
+ )) {
572
+ view.culled++;
573
+ continue;
574
+ }
575
+ const o = n * LIGHT2D_FLOATS;
576
+ stage[o] = x;
577
+ stage[o + 1] = y;
578
+ stage[o + 2] = height[i];
579
+ stage[o + 3] = r;
580
+ stage[o + 4] = color[i * 4] * s;
581
+ stage[o + 5] = color[i * 4 + 1] * s;
582
+ stage[o + 6] = color[i * 4 + 2] * s;
583
+ stage[o + 7] = falloff[i];
584
+ if (spot) {
585
+ const dx = g[i * 12];
586
+ const dy = g[i * 12 + 4];
587
+ const l = Math.sqrt(dx * dx + dy * dy) || 1;
588
+ stage[o + 8] = dx / l;
589
+ stage[o + 9] = dy / l;
590
+ const a0 = Math.min(inner[i], outer[i]);
591
+ stage[o + 10] = Math.cos(a0 * DEG);
592
+ stage[o + 11] = Math.cos(outer[i] * DEG);
593
+ } else {
594
+ stage[o + 8] = 1;
595
+ stage[o + 9] = 0;
596
+ stage[o + 10] = -1;
597
+ stage[o + 11] = -2;
598
+ }
599
+ stageU32[o + 12] = 4294967295;
600
+ stage[o + 13] = softness[i];
601
+ stageU32[o + 14] = layers[i] >>> 0;
602
+ stage[o + 15] = 1 / (r * r);
603
+ stageShadows[n] = shadows[i] ? 1 : 0;
604
+ stageEntities[n] = table.entities[i];
605
+ const ddx = x - cx;
606
+ const ddy = y - cy;
607
+ sortKeys[n] = ddx * ddx + ddy * ddy;
608
+ n++;
609
+ }
610
+ return n;
611
+ }
612
+ function warnOnce(world, store, view, code, message, hint) {
613
+ const key = `${view}|${code}`;
614
+ if (store.warned.has(key)) return;
615
+ store.warned.add(key);
616
+ world.tryResource(LogResource)?.log("warn", message, { code, hint });
617
+ }
618
+ function extractView(world, q, store, view, cam, ambient, ambientAt, ambientIntensity, maxLights, maxShadowed) {
619
+ view.visible = view.culled = view.dropped = view.demoted = 0;
620
+ let n = 0;
621
+ for (const table of q.points.tables)
622
+ if (table.count > 0) n = stageLights(table, false, cam, n, view);
623
+ for (const table of q.spots.tables)
624
+ if (table.count > 0) n = stageLights(table, true, cam, n, view);
625
+ view.visible = n;
626
+ growStage(n * 2 + 1);
627
+ for (let i = 0; i < n; i++) sortIdx[i] = i;
628
+ if (n > maxLights) {
629
+ insertionSort(sortIdx, n);
630
+ view.dropped = n - maxLights;
631
+ warnOnce(
632
+ world,
633
+ store,
634
+ view.name,
635
+ "sprite/too-many-lights",
636
+ `${n} 2D lights reach ${view.name}; only the nearest ${maxLights} (Lighting2d.maxLights) light it`,
637
+ "Raise Lighting2d.maxLights, or shrink or remove distant lights."
638
+ );
639
+ }
640
+ const count = Math.min(n, maxLights);
641
+ view.ensureLights(count);
642
+ view.count = count;
643
+ const lights = view.lights;
644
+ const circles = view.circles;
645
+ for (let k = 0; k < count; k++) {
646
+ const s = sortIdx[k];
647
+ for (let f = 0; f < LIGHT2D_FLOATS; f++)
648
+ lights[k * LIGHT2D_FLOATS + f] = stage[s * LIGHT2D_FLOATS + f];
649
+ for (let f = 0; f < 4; f++) circles[k * 4 + f] = stageCircles[s * 4 + f];
650
+ view.entities[k] = stageEntities[s];
651
+ }
652
+ let shadowedN = 0;
653
+ for (let k = 0; k < count; k++) if (stageShadows[sortIdx[k]]) sortIdx[n + shadowedN++] = k;
654
+ for (let a = 1; a < shadowedN; a++) {
655
+ const v = sortIdx[n + a];
656
+ const key = sortKeys[sortIdx[v]];
657
+ let b = a - 1;
658
+ while (b >= 0 && sortKeys[sortIdx[sortIdx[n + b]]] > key) {
659
+ sortIdx[n + b + 1] = sortIdx[n + b];
660
+ b--;
661
+ }
662
+ sortIdx[n + b + 1] = v;
663
+ }
664
+ const shadowCount = Math.min(shadowedN, maxShadowed, MAX_SHADOWED);
665
+ view.demoted = shadowedN - shadowCount;
666
+ if (view.demoted > 0) {
667
+ warnOnce(
668
+ world,
669
+ store,
670
+ view.name,
671
+ "sprite/too-many-shadowed-lights",
672
+ `${shadowedN} shadowed 2D lights reach ${view.name}; the farthest ${view.demoted} render without shadows (Lighting2d.maxShadowed ${maxShadowed})`,
673
+ "Raise Lighting2d.maxShadowed (at most 64), or turn shadows off on minor lights."
674
+ );
675
+ }
676
+ const bits = view.lightBits;
677
+ for (let k = 0; k < count; k++) bits[k * LIGHT2D_FLOATS + 12] = 4294967295;
678
+ for (let r = 0; r < shadowCount; r++) {
679
+ const k = sortIdx[n + r];
680
+ view.shadowed[r] = k;
681
+ bits[k * LIGHT2D_FLOATS + 12] = r;
682
+ }
683
+ view.shadowedCount = shadowCount;
684
+ view.tilesX = Math.max(1, Math.ceil(cam.width / TILE_PIXELS));
685
+ view.tilesY = Math.max(1, Math.ceil(cam.height / TILE_PIXELS));
686
+ view.uniformData[0] = ambient[ambientAt] * ambientIntensity;
687
+ view.uniformData[1] = ambient[ambientAt + 1] * ambientIntensity;
688
+ view.uniformData[2] = ambient[ambientAt + 2] * ambientIntensity;
689
+ view.uniformData[3] = 0;
690
+ return selectSegments(store.occluders, view);
691
+ }
692
+ function reachesShadowed(view, b, o, layers) {
693
+ for (let r = 0; r < view.shadowedCount; r++) {
694
+ const lo = view.shadowed[r] * LIGHT2D_FLOATS;
695
+ if ((view.lightBits[lo + 14] & layers) >>> 0 === 0) continue;
696
+ const x = view.lights[lo];
697
+ const y = view.lights[lo + 1];
698
+ const rad = view.lights[lo + 3];
699
+ const qx = Math.min(Math.max(x, b[o]), b[o + 2]) - x;
700
+ const qy = Math.min(Math.max(y, b[o + 1]), b[o + 3]) - y;
701
+ if (qx * qx + qy * qy < rad * rad) return true;
702
+ }
703
+ return false;
704
+ }
705
+ function pushSegments(view, src, pen, layers) {
706
+ const n = src.length / 4;
707
+ view.ensureSegments(view.segmentCount + n);
708
+ const f = view.segments;
709
+ const u = view.segmentBits;
710
+ for (let s = 0; s < n; s++) {
711
+ const o = (view.segmentCount + s) * SEGMENT_FLOATS;
712
+ f[o] = src[s * 4];
713
+ f[o + 1] = src[s * 4 + 1];
714
+ f[o + 2] = src[s * 4 + 2];
715
+ f[o + 3] = src[s * 4 + 3];
716
+ f[o + 4] = pen;
717
+ u[o + 5] = layers;
718
+ u[o + 6] = 0;
719
+ u[o + 7] = 0;
720
+ }
721
+ view.segmentCount += n;
722
+ }
723
+ function selectSegments(occ, view) {
724
+ view.segmentCount = 0;
725
+ if (view.shadowedCount === 0) return 0;
726
+ let key = occ.version * 31 + 7;
727
+ for (let k = 0; k < occ.records.length; k++) {
728
+ const rec = occ.records[k];
729
+ if (rec.world.length === 0 || !reachesShadowed(view, rec.bounds, 0, rec.layers)) continue;
730
+ pushSegments(view, rec.world, rec.penetration, rec.layers);
731
+ key = (key * 33 + k + 1) % 2147483647;
732
+ }
733
+ for (const t of occ.tiles.values()) {
734
+ for (let c = 0; c < t.world.length; c++) {
735
+ const w = t.world[c];
736
+ if (!w || w.length === 0 || !reachesShadowed(view, t.bounds, c * 4, 4294967295)) continue;
737
+ pushSegments(view, w, t.penetration, 4294967295);
738
+ key = (key * 33 + c + 1) % 2147483647;
739
+ }
740
+ }
741
+ return (key * 33 + view.segmentCount) % 2147483647;
742
+ }
743
+ const prepareLights2d = defineSystem({
744
+ name: "sprite/lights2d",
745
+ description: "Extracts 2D lights and occluders for each Lighting2d camera, and uploads them.",
746
+ setup: (world) => ({
747
+ cameras: world.query({ with: [Lighting2d] }),
748
+ points: world.query({ with: [PointLight2d, GlobalTransform, ComputedVisibility] }),
749
+ spots: world.query({ with: [SpotLight2d, GlobalTransform, ComputedVisibility] }),
750
+ occluders: world.query({ with: [LightOccluder2d, GlobalTransform] }),
751
+ tilemaps: world.query({ with: [Tilemap, GlobalTransform] })
752
+ }),
753
+ run: (q, world, ctx) => {
754
+ let any = false;
755
+ for (const table of q.cameras.tables) if (table.count > 0) any = true;
756
+ const store = world.tryResource(Lights2d);
757
+ if (!any) {
758
+ if (store && store.views.size > 0) store.views.clear();
759
+ return;
760
+ }
761
+ const lights = store ?? world.initResource(Lights2d);
762
+ const gpu = world.tryResource(Gpu);
763
+ updateOccluders(world, lights.occluders, ctx.lastRunTick, q);
764
+ const cameras = world.resource(Cameras);
765
+ const frame = lights.occluders.frame;
766
+ for (const table of q.cameras.tables) {
767
+ const ambient = table.column(Lighting2d, "ambient");
768
+ const ambientIntensity = table.column(Lighting2d, "ambientIntensity");
769
+ const maxLights = table.column(Lighting2d, "maxLights");
770
+ const maxShadowed = table.column(Lighting2d, "maxShadowed");
771
+ for (let i = 0; i < table.count; i++) {
772
+ const entity = table.entities[i];
773
+ const cam = cameras.get(entity);
774
+ if (!cam) continue;
775
+ const name = `camera:${entity}`;
776
+ let view = lights.views.get(name);
777
+ if (!view) {
778
+ view = new LightView2d(name);
779
+ lights.views.set(name, view);
780
+ }
781
+ view.camera = entity;
782
+ view.frame = frame;
783
+ const key = extractView(
784
+ world,
785
+ q,
786
+ lights,
787
+ view,
788
+ cam,
789
+ ambient,
790
+ i * 4,
791
+ ambientIntensity[i],
792
+ maxLights[i],
793
+ maxShadowed[i]
794
+ );
795
+ if (gpu) view.upload(gpu, key);
796
+ }
797
+ }
798
+ for (const [name, view] of lights.views) if (view.frame !== frame) lights.views.delete(name);
799
+ }
800
+ });
801
+ const gpuState = /* @__PURE__ */ new WeakMap();
802
+ function layoutsOf(gpu) {
803
+ let s = gpuState.get(gpu);
804
+ if (s && s.generation === gpu.generation) return s;
805
+ const C = GPUShaderStage.COMPUTE;
806
+ const F = GPUShaderStage.FRAGMENT;
807
+ const read = { type: "read-only-storage" };
808
+ const write = { type: "storage" };
809
+ s = {
810
+ generation: gpu.generation,
811
+ compute: gpu.layouts.bindGroupLayout({
812
+ label: "lights2d/compute",
813
+ entries: [
814
+ { binding: 0, visibility: C, buffer: { type: "uniform" } },
815
+ { binding: 1, visibility: C, buffer: read },
816
+ { binding: 2, visibility: C, buffer: read },
817
+ { binding: 3, visibility: C, buffer: write },
818
+ { binding: 4, visibility: C, buffer: write },
819
+ { binding: 5, visibility: C, buffer: read },
820
+ { binding: 6, visibility: C, buffer: read },
821
+ { binding: 7, visibility: C, buffer: write }
822
+ ]
823
+ }),
824
+ render: gpu.layouts.bindGroupLayout({
825
+ label: "lights2d/shade",
826
+ entries: [
827
+ { binding: 0, visibility: F, buffer: { type: "uniform" } },
828
+ { binding: 1, visibility: F, buffer: read },
829
+ { binding: 2, visibility: F, buffer: read },
830
+ { binding: 3, visibility: F, buffer: read },
831
+ { binding: 4, visibility: F, buffer: read }
832
+ ]
833
+ }),
834
+ pipelines: /* @__PURE__ */ new Map()
835
+ };
836
+ gpuState.set(gpu, s);
837
+ return s;
838
+ }
839
+ function lights2dLayout(gpu) {
840
+ return layoutsOf(gpu).render;
841
+ }
842
+ const ids = /* @__PURE__ */ new WeakMap();
843
+ let nextId = 1;
844
+ function idOf(o) {
845
+ let id = ids.get(o);
846
+ if (id === void 0) {
847
+ id = nextId++;
848
+ ids.set(o, id);
849
+ }
850
+ return id;
851
+ }
852
+ function litView(world, view) {
853
+ const v = world.tryResource(Lights2d)?.views.get(view);
854
+ return v?.gpu ? v : void 0;
855
+ }
856
+ function lights2dGroup(gpu, view) {
857
+ const g = view.gpu;
858
+ const key = `${idOf(g.uniform.buffer)}/${idOf(g.lights.buffer)}/${idOf(g.tiles.buffer)}/${idOf(g.shadowMap.buffer)}/${idOf(g.shadowCoarse.buffer)}`;
859
+ if (!g.renderGroup || g.renderGroup.key !== key) {
860
+ g.renderGroup = {
861
+ key,
862
+ group: gpu.device.createBindGroup({
863
+ label: `${view.name}/lights2d/shade`,
864
+ layout: layoutsOf(gpu).render,
865
+ entries: [
866
+ { binding: 0, resource: { buffer: g.uniform.buffer } },
867
+ { binding: 1, resource: { buffer: g.lights.buffer } },
868
+ { binding: 2, resource: { buffer: g.tiles.buffer } },
869
+ { binding: 3, resource: { buffer: g.shadowMap.buffer } },
870
+ { binding: 4, resource: { buffer: g.shadowCoarse.buffer } }
871
+ ]
872
+ })
873
+ };
874
+ }
875
+ return g.renderGroup.group;
876
+ }
877
+ function computePipeline(ctx, entry) {
878
+ const gpu = ctx.gpu;
879
+ const s = layoutsOf(gpu);
880
+ const cached = s.pipelines.get(entry);
881
+ if (cached) return cached;
882
+ const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::sprite::light2d::compute" });
883
+ if (!module) {
884
+ gpu.pipelines.skipped++;
885
+ return void 0;
886
+ }
887
+ const p = gpu.pipelines.compute({
888
+ label: `lights2d/${entry}`,
889
+ layout: gpu.layouts.pipelineLayout({ label: "lights2d", bindGroupLayouts: [s.compute] }),
890
+ compute: { module, entryPoint: entry }
891
+ });
892
+ if (p) s.pipelines.set(entry, p);
893
+ return p;
894
+ }
895
+ function lights2dNode(world) {
896
+ return {
897
+ kind: "compute",
898
+ phase: RenderPhase.Sprites - 10,
899
+ enabled: (view) => litView(world, view.name) !== void 0,
900
+ writes: ["lights2d"],
901
+ run: (ctx) => {
902
+ const view = litView(ctx.world, ctx.view.name);
903
+ if (!view) return;
904
+ const bin = computePipeline(ctx, "bin");
905
+ const clear = computePipeline(ctx, "clear");
906
+ const shadows = computePipeline(ctx, "shadows");
907
+ const coarse = computePipeline(ctx, "coarse");
908
+ if (!bin || !clear || !shadows || !coarse) return;
909
+ const g = view.gpu;
910
+ const key = `${idOf(g.uniform.buffer)}/${idOf(g.lights.buffer)}/${idOf(g.circles.buffer)}/${idOf(g.tiles.buffer)}/${idOf(g.shadowMap.buffer)}/${idOf(g.segments.buffer)}/${idOf(g.shadowed.buffer)}/${idOf(g.shadowCoarse.buffer)}`;
911
+ if (!g.computeGroup || g.computeGroup.key !== key) {
912
+ g.computeGroup = {
913
+ key,
914
+ group: ctx.gpu.device.createBindGroup({
915
+ label: `${view.name}/lights2d`,
916
+ layout: layoutsOf(ctx.gpu).compute,
917
+ entries: [
918
+ { binding: 0, resource: { buffer: g.uniform.buffer } },
919
+ { binding: 1, resource: { buffer: g.lights.buffer } },
920
+ { binding: 2, resource: { buffer: g.circles.buffer } },
921
+ { binding: 3, resource: { buffer: g.tiles.buffer } },
922
+ { binding: 4, resource: { buffer: g.shadowMap.buffer } },
923
+ { binding: 5, resource: { buffer: g.segments.buffer } },
924
+ { binding: 6, resource: { buffer: g.shadowed.buffer } },
925
+ { binding: 7, resource: { buffer: g.shadowCoarse.buffer } }
926
+ ]
927
+ })
928
+ };
929
+ }
930
+ const pass = ctx.computePass;
931
+ pass.setBindGroup(0, g.computeGroup.group);
932
+ pass.setPipeline(bin);
933
+ pass.dispatchWorkgroups(Math.ceil(view.tilesX * view.tilesY / 64));
934
+ if (view.shadowedCount > 0) {
935
+ pass.setPipeline(clear);
936
+ pass.dispatchWorkgroups(Math.ceil(view.shadowedCount * SHADOW_RES / 64));
937
+ if (view.segmentCount > 0) {
938
+ pass.setPipeline(shadows);
939
+ pass.dispatchWorkgroups(Math.ceil(view.segmentCount / 64), view.shadowedCount);
940
+ }
941
+ pass.setPipeline(coarse);
942
+ pass.dispatchWorkgroups(Math.ceil(view.shadowedCount * COARSE_RES / 64));
943
+ }
944
+ }
945
+ };
946
+ }
947
+ function describeLights2d(world) {
948
+ const store = world.tryResource(Lights2d);
949
+ if (!store || store.views.size === 0) return void 0;
950
+ const profiler = world.tryResource(ProfilerResource);
951
+ const views = {};
952
+ for (const [name, v] of store.views) {
953
+ const bins = binLightsCpu(v.circles, v.count, v.tilesX, v.tilesY);
954
+ let atCap = 0;
955
+ let maxPerTile = 0;
956
+ for (let t = 0; t < bins.counts.length; t++) {
957
+ if (bins.counts[t] >= TILE_MAX) atCap++;
958
+ if (bins.counts[t] > maxPerTile) maxPerTile = bins.counts[t];
959
+ }
960
+ views[name] = {
961
+ lights: v.count,
962
+ visible: v.visible,
963
+ culled: v.culled,
964
+ dropped: v.dropped,
965
+ shadowed: v.shadowedCount,
966
+ demoted: v.demoted,
967
+ segments: v.segmentCount,
968
+ tiles: v.tilesX * v.tilesY,
969
+ tilesAtCap: atCap,
970
+ maxLightsPerTile: maxPerTile,
971
+ uploadedBytes: v.uploadedBytes,
972
+ gpuMs: {
973
+ binAndShadows: profiler?.timing("gpu:sprites/lights2d")?.avg,
974
+ sprites: profiler?.timing("gpu:sprites")?.avg
975
+ },
976
+ entities: Array.from(v.entities.subarray(0, v.count))
977
+ };
978
+ }
979
+ return {
980
+ occluders: store.occluders.records.length,
981
+ tileLayers: store.occluders.tiles.size,
982
+ segments: store.occluders.segmentCount,
983
+ chunkRebuilds: store.occluders.chunkRebuilds,
984
+ views
985
+ };
986
+ }
987
+ const ringScratch = new Float32Array(SHADOW_RES);
988
+ const pa = new Float32Array(3);
989
+ const pb = new Float32Array(3);
990
+ const lightColor = new Float32Array(4);
991
+ const OCCLUDER_COLOR = [1, 0.35, 0.2, 1];
992
+ const RING_COLOR = [1, 1, 1, 0.8];
993
+ const lights2dOverlay = defineOverlay({
994
+ name: "lights2d",
995
+ description: "2D lights: each light's radius (and cone), its shadow row as a ring, and occluder segments, from the first lit view.",
996
+ draw(world, g, passes) {
997
+ const store = world.tryResource(Lights2d);
998
+ if (!store) return;
999
+ const opts = { depthTest: false };
1000
+ for (const rec of store.occluders.records) {
1001
+ if (!passes(rec.entity)) continue;
1002
+ for (let s = 0; s < rec.world.length; s += 4) {
1003
+ pa[0] = rec.world[s];
1004
+ pa[1] = rec.world[s + 1];
1005
+ pb[0] = rec.world[s + 2];
1006
+ pb[1] = rec.world[s + 3];
1007
+ g.line(pa, pb, OCCLUDER_COLOR, opts);
1008
+ }
1009
+ }
1010
+ for (const t of store.occluders.tiles.values()) {
1011
+ if (!passes(t.entity)) continue;
1012
+ for (const w of t.world) {
1013
+ if (!w) continue;
1014
+ for (let s = 0; s < w.length; s += 4) {
1015
+ pa[0] = w[s];
1016
+ pa[1] = w[s + 1];
1017
+ pb[0] = w[s + 2];
1018
+ pb[1] = w[s + 3];
1019
+ g.line(pa, pb, OCCLUDER_COLOR, opts);
1020
+ }
1021
+ }
1022
+ }
1023
+ const view = store.views.values().next().value;
1024
+ if (!view) return;
1025
+ for (let k = 0; k < view.count; k++) {
1026
+ const entity = view.entities[k];
1027
+ if (!passes(entity)) continue;
1028
+ const o = k * LIGHT2D_FLOATS;
1029
+ const x = view.lights[o];
1030
+ const y = view.lights[o + 1];
1031
+ const r = view.lights[o + 3];
1032
+ const m = Math.max(view.lights[o + 4], view.lights[o + 5], view.lights[o + 6], 1e-6);
1033
+ lightColor[0] = view.lights[o + 4] / m;
1034
+ lightColor[1] = view.lights[o + 5] / m;
1035
+ lightColor[2] = view.lights[o + 6] / m;
1036
+ lightColor[3] = 1;
1037
+ pa[0] = x;
1038
+ pa[1] = y;
1039
+ pa[2] = 0;
1040
+ g.circle(pa, r, 2, lightColor, opts);
1041
+ if (view.lights[o + 11] > -1.5) {
1042
+ const base = Math.atan2(view.lights[o + 9], view.lights[o + 8]);
1043
+ const half = Math.acos(view.lights[o + 11]);
1044
+ for (const sgn of [-1, 1]) {
1045
+ pb[0] = x + Math.cos(base + sgn * half) * r;
1046
+ pb[1] = y + Math.sin(base + sgn * half) * r;
1047
+ g.line(pa, pb, lightColor, opts);
1048
+ }
1049
+ }
1050
+ const row = view.lightBits[o + 12] | 0;
1051
+ if (row < 0) continue;
1052
+ shadowRowCpu(x, y, r, view.lightBits[o + 14], view.segments, view.segmentCount, ringScratch);
1053
+ const steps = 256;
1054
+ const per = SHADOW_RES / steps;
1055
+ for (let a = 0; a < steps; a++) {
1056
+ const d0 = Math.min(r, ringScratch[a * per]);
1057
+ const d1 = Math.min(r, ringScratch[(a + 1) % steps * per]);
1058
+ const t0 = -Math.PI + (a * per + 0.5) / SHADOW_RES * Math.PI * 2;
1059
+ const t1 = -Math.PI + ((a + 1) % steps * per + 0.5) / SHADOW_RES * Math.PI * 2;
1060
+ pa[0] = x + Math.cos(t0) * d0;
1061
+ pa[1] = y + Math.sin(t0) * d0;
1062
+ pb[0] = x + Math.cos(t1) * d1;
1063
+ pb[1] = y + Math.sin(t1) * d1;
1064
+ g.line(pa, pb, RING_COLOR, opts);
1065
+ }
1066
+ }
1067
+ }
1068
+ });
1069
+ export {
1070
+ COARSE_RES,
1071
+ LIGHT2D_FLOATS,
1072
+ LightView2d,
1073
+ Lights2d,
1074
+ Lights2dStore,
1075
+ MAX_SHADOWED,
1076
+ OccluderStore,
1077
+ SEGMENT_FLOATS,
1078
+ SHADOW_RES,
1079
+ TILE_MAX,
1080
+ TILE_PIXELS,
1081
+ TILE_STRIDE,
1082
+ binLightsCpu,
1083
+ describeLights2d,
1084
+ lightCircle,
1085
+ lights2dGroup,
1086
+ lights2dLayout,
1087
+ lights2dNode,
1088
+ lights2dOverlay,
1089
+ litView,
1090
+ prepareLights2d,
1091
+ shadowFactorCpu,
1092
+ shadowRowCpu
1093
+ };