@solidrt/2d 0.0.55 → 0.0.56
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/AGENTS.md +241 -57
- package/examples/README.md +22 -5
- package/examples/anim.tsx +86 -0
- package/examples/camera-probe.tsx +200 -0
- package/examples/camera.tsx +190 -0
- package/examples/parity.tsx +4 -4
- package/examples/pick.tsx +50 -33
- package/examples/tiles.tsx +19 -10
- package/package.json +3 -3
- package/src/animation.ts +178 -0
- package/src/camera-motion.ts +532 -0
- package/src/camera.ts +86 -0
- package/src/camera2d.ts +140 -0
- package/src/components.tsx +330 -76
- package/src/dispatch.ts +231 -0
- package/src/index.ts +30 -5
- package/src/layer.ts +454 -186
- package/src/oversample-math.ts +73 -0
- package/src/oversample.ts +39 -11
- package/src/records.ts +132 -27
- package/src/shaders.ts +29 -12
- package/src/tiles.ts +79 -24
package/src/tiles.ts
CHANGED
|
@@ -30,11 +30,13 @@ import {
|
|
|
30
30
|
endBufferWrite,
|
|
31
31
|
limits,
|
|
32
32
|
renderTarget,
|
|
33
|
+
setTargetParams,
|
|
33
34
|
setTargetSize,
|
|
34
35
|
} from "@solidrt/core/gpu"
|
|
35
36
|
import type { BufferId, FilterMode, TextureId } from "@solidrt/core/gpu"
|
|
36
37
|
import type { Frame } from "./frames.ts"
|
|
37
|
-
import {
|
|
38
|
+
import { checkTint } from "./layer.ts"
|
|
39
|
+
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
38
40
|
import { FLOATS_PER_SPRITE } from "./records.ts"
|
|
39
41
|
import { FRAGMENT, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
40
42
|
|
|
@@ -44,8 +46,9 @@ const RESOLVED = Promise.resolve()
|
|
|
44
46
|
const CHUNK_TARGET_PX = 512
|
|
45
47
|
|
|
46
48
|
export type TileLayerOptions = {
|
|
47
|
-
/** Per-chunk clear color
|
|
48
|
-
|
|
49
|
+
/** Per-chunk clear color (the name says the scope: chunks that never
|
|
50
|
+
* allocate render nothing, so this is not a layer background). */
|
|
51
|
+
chunkClearColor?: [number, number, number, number]
|
|
49
52
|
/**
|
|
50
53
|
* Sampler filter for the baked chunk textures at composite time; default
|
|
51
54
|
* "linear", which with `oversample` is the proper resample at any scale.
|
|
@@ -66,6 +69,8 @@ export type TileLayerOptions = {
|
|
|
66
69
|
* chunk count (larger = fewer textures and leaves).
|
|
67
70
|
*/
|
|
68
71
|
chunkTiles?: number
|
|
72
|
+
/** Layer tint, [r, g, b, a] in 0..1; see TileLayer.setTint. */
|
|
73
|
+
tint?: [number, number, number, number]
|
|
69
74
|
label?: string
|
|
70
75
|
/** Skip the owner-scoped auto-dispose (see createTileLayer). */
|
|
71
76
|
autoFree?: boolean
|
|
@@ -114,13 +119,24 @@ export type TileLayer = {
|
|
|
114
119
|
/** Called after a chunk allocates - the composition hook. Assignable. */
|
|
115
120
|
onChunk?: (chunk: TileChunk) => void
|
|
116
121
|
/**
|
|
117
|
-
* Set one cell: a frame draws it, null clears it
|
|
118
|
-
*
|
|
119
|
-
*
|
|
122
|
+
* Set one cell: a frame draws it, null clears it (a clear ignores `opts` -
|
|
123
|
+
* an absent cell has no tint, it draws nothing). `tint` multiplies the
|
|
124
|
+
* cell's sampled texels like a sprite's tint; absent it keeps the cell's
|
|
125
|
+
* current tint (a cell being set from empty starts at [1, 1, 1, 1]).
|
|
126
|
+
* Batched; the microtask flush publishes and re-bakes ONLY the chunks
|
|
127
|
+
* that changed, however many tiles did.
|
|
120
128
|
*/
|
|
121
|
-
setTile(col: number, row: number, frame: Frame | null): void
|
|
129
|
+
setTile(col: number, row: number, frame: Frame | null, opts?: { tint?: [number, number, number, number] }): void
|
|
122
130
|
/** The frame at a cell, or null when empty. */
|
|
123
131
|
getTile(col: number, row: number): Frame | null
|
|
132
|
+
/**
|
|
133
|
+
* Tint the whole layer, [r, g, b, a] in 0..1: a uniform multiplied over
|
|
134
|
+
* every cell's own tint (day/night, a dimmed parallax plane). Not a
|
|
135
|
+
* record write - resident chunks re-render GPU-side with nothing
|
|
136
|
+
* re-uploaded - but each write does re-render every resident chunk, so
|
|
137
|
+
* animate it coarsely, not per frame.
|
|
138
|
+
*/
|
|
139
|
+
setTint(tint: [number, number, number, number]): void
|
|
124
140
|
dispose(): void
|
|
125
141
|
}
|
|
126
142
|
|
|
@@ -128,15 +144,21 @@ type Chunk = TileChunk & {
|
|
|
128
144
|
records: Float32Array
|
|
129
145
|
buffer: BufferId
|
|
130
146
|
dirty: boolean
|
|
147
|
+
/** Records changed since the last flush (a tint-only or resize re-bake
|
|
148
|
+
* renders without re-uploading them). */
|
|
149
|
+
wrote: boolean
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
/**
|
|
134
153
|
* Create a baked tile layer: `cols` x `rows` cells of `tileW` x `tileH`
|
|
135
154
|
* pixels, every tile drawing one atlas frame, baked into lazily-allocated
|
|
136
|
-
* chunk textures and composited as a few quads.
|
|
137
|
-
* creation -
|
|
138
|
-
*
|
|
139
|
-
*
|
|
155
|
+
* chunk textures and composited as a few quads. A tile world is FINITE and
|
|
156
|
+
* sized at creation - that is the contract, not a provisional limit:
|
|
157
|
+
* recreate the layer to resize, and for a huge sparse world just pick big
|
|
158
|
+
* numbers, since a chunk nobody writes costs nothing (no records, no
|
|
159
|
+
* texture) and the grid bound is bookkeeping, not allocation. Disposed
|
|
160
|
+
* automatically with the owning reactive scope (opt out with `{ autoFree:
|
|
161
|
+
* false }`); the atlas is NOT owned - dispose it yourself.
|
|
140
162
|
*/
|
|
141
163
|
export function createTileLayer(
|
|
142
164
|
cols: number,
|
|
@@ -164,8 +186,11 @@ export function createTileLayer(
|
|
|
164
186
|
)
|
|
165
187
|
}
|
|
166
188
|
let label = opts?.label ?? "tiles"
|
|
189
|
+
let tint: [number, number, number, number] = opts?.tint ?? [1, 1, 1, 1]
|
|
190
|
+
checkTint("createTileLayer", tint)
|
|
167
191
|
let oversample = opts?.oversample ?? 1
|
|
168
192
|
checkOversample("createTileLayer", oversample, chunkW, chunkH)
|
|
193
|
+
let thrash = thrashSentinel(`tile layer "${label}"`)
|
|
169
194
|
let chunkCols = Math.ceil(cols / chunkTiles)
|
|
170
195
|
let perChunk = chunkTiles * chunkTiles
|
|
171
196
|
// One unit quad (triangle strip), shared by every chunk's pipeline.
|
|
@@ -188,13 +213,17 @@ export function createTileLayer(
|
|
|
188
213
|
for (let chunk of baked) {
|
|
189
214
|
if (!chunk.dirty) continue
|
|
190
215
|
chunk.dirty = false
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
if (chunk.wrote) {
|
|
217
|
+
chunk.wrote = false
|
|
218
|
+
let out = beginBufferWrite(chunk.buffer)
|
|
219
|
+
out.set(chunk.records)
|
|
220
|
+
endBufferWrite(chunk.buffer, chunk.records.byteLength)
|
|
221
|
+
}
|
|
194
222
|
renderTarget(chunk.texture)
|
|
195
223
|
}
|
|
196
224
|
}
|
|
197
|
-
let touch = (chunk: Chunk) => {
|
|
225
|
+
let touch = (chunk: Chunk, wroteRecords = true) => {
|
|
226
|
+
if (wroteRecords) chunk.wrote = true
|
|
198
227
|
if (chunk.dirty) return
|
|
199
228
|
chunk.dirty = true
|
|
200
229
|
dirtyChunks.push(chunk)
|
|
@@ -213,7 +242,9 @@ export function createTileLayer(
|
|
|
213
242
|
FRAGMENT,
|
|
214
243
|
chunkW * oversample,
|
|
215
244
|
chunkH * oversample,
|
|
216
|
-
|
|
245
|
+
// Bake targets pin the camera rotation to identity: the tile camera
|
|
246
|
+
// rotates the COMPOSITED world (<TileLayer>'s view), never the bake.
|
|
247
|
+
{ uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
217
248
|
{
|
|
218
249
|
label: `${label}-chunk`,
|
|
219
250
|
topology: "triangle-strip",
|
|
@@ -225,13 +256,13 @@ export function createTileLayer(
|
|
|
225
256
|
instanceCount: perChunk,
|
|
226
257
|
blend: "alpha",
|
|
227
258
|
textures: { uAtlas: atlas },
|
|
228
|
-
clearColor: opts?.
|
|
259
|
+
clearColor: opts?.chunkClearColor ?? [0, 0, 0, 0],
|
|
229
260
|
filter: opts?.filter,
|
|
230
261
|
render: "manual",
|
|
231
262
|
autoFree: false,
|
|
232
263
|
},
|
|
233
264
|
)
|
|
234
|
-
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false }
|
|
265
|
+
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false, wrote: false }
|
|
235
266
|
resident.set(index, chunk)
|
|
236
267
|
layer.chunks.push(chunk)
|
|
237
268
|
layer.onChunk?.(chunk)
|
|
@@ -263,15 +294,17 @@ export function createTileLayer(
|
|
|
263
294
|
setOversample(n) {
|
|
264
295
|
if (disposed || n === oversample) return
|
|
265
296
|
checkOversample("setOversample", n, chunkW, chunkH)
|
|
297
|
+
thrash()
|
|
266
298
|
oversample = n
|
|
267
299
|
// Resizing a target re-renders it, but a manual target's content is
|
|
268
300
|
// its last bake: mark every chunk so the flush bakes it at the new size.
|
|
269
301
|
for (let chunk of resident.values()) {
|
|
270
302
|
setTargetSize(chunk.texture, chunkW * n, chunkH * n)
|
|
271
|
-
|
|
303
|
+
// The records are unchanged; the resized target just needs a bake.
|
|
304
|
+
touch(chunk, false)
|
|
272
305
|
}
|
|
273
306
|
},
|
|
274
|
-
setTile(col, row, frame) {
|
|
307
|
+
setTile(col, row, frame, opts) {
|
|
275
308
|
if (disposed) return
|
|
276
309
|
let [index, at] = locate(col, row, "setTile")
|
|
277
310
|
let chunk = resident.get(index)
|
|
@@ -283,6 +316,10 @@ export function createTileLayer(
|
|
|
283
316
|
} else {
|
|
284
317
|
chunk ??= allocate(index)
|
|
285
318
|
let r = chunk.records
|
|
319
|
+
// A cell coming up from empty starts at the default tint; a
|
|
320
|
+
// re-set keeps its tint unless opts carries one (absent keys keep
|
|
321
|
+
// their values, like every options object here).
|
|
322
|
+
let fresh = r[at + 2] === 0
|
|
286
323
|
r[at] = (col + 0.5) * tileW
|
|
287
324
|
r[at + 1] = (row + 0.5) * tileH
|
|
288
325
|
r[at + 2] = tileW
|
|
@@ -291,10 +328,18 @@ export function createTileLayer(
|
|
|
291
328
|
r[at + 5] = frame.v0
|
|
292
329
|
r[at + 6] = frame.u1
|
|
293
330
|
r[at + 7] = frame.v1
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
331
|
+
if (opts?.tint !== undefined) {
|
|
332
|
+
checkTint("setTile", opts.tint)
|
|
333
|
+
r[at + 9] = opts.tint[0]
|
|
334
|
+
r[at + 10] = opts.tint[1]
|
|
335
|
+
r[at + 11] = opts.tint[2]
|
|
336
|
+
r[at + 12] = opts.tint[3]
|
|
337
|
+
} else if (fresh) {
|
|
338
|
+
r[at + 9] = 1
|
|
339
|
+
r[at + 10] = 1
|
|
340
|
+
r[at + 11] = 1
|
|
341
|
+
r[at + 12] = 1
|
|
342
|
+
}
|
|
298
343
|
}
|
|
299
344
|
touch(chunk)
|
|
300
345
|
},
|
|
@@ -305,6 +350,16 @@ export function createTileLayer(
|
|
|
305
350
|
let r = chunk.records
|
|
306
351
|
return { u0: r[at + 4]!, v0: r[at + 5]!, u1: r[at + 6]!, v1: r[at + 7]! }
|
|
307
352
|
},
|
|
353
|
+
setTint(next) {
|
|
354
|
+
if (disposed) return
|
|
355
|
+
checkTint("setTint", next)
|
|
356
|
+
if (next[0] === tint[0] && next[1] === tint[1] && next[2] === tint[2] && next[3] === tint[3]) return
|
|
357
|
+
tint = next
|
|
358
|
+
for (let chunk of resident.values()) {
|
|
359
|
+
setTargetParams(chunk.texture, { uTint: tint })
|
|
360
|
+
touch(chunk, false)
|
|
361
|
+
}
|
|
362
|
+
},
|
|
308
363
|
dispose() {
|
|
309
364
|
if (disposed) return
|
|
310
365
|
disposed = true
|