@solidrt/2d 0.0.70 → 0.0.72
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 +222 -40
- package/README.md +7 -3
- package/examples/README.md +1 -1
- package/examples/camera-probe.tsx +4 -4
- package/examples/markers.tsx +3 -3
- package/package.json +5 -4
- package/src/animation.ts +26 -5
- package/src/atlas.ts +8 -4
- package/src/camera.ts +2 -2
- package/src/components/camera2d.tsx +2 -2
- package/src/components/group.tsx +15 -3
- package/src/components/shots.tsx +2 -2
- package/src/components/sprite-layer.tsx +40 -12
- package/src/components/sprite.tsx +31 -3
- package/src/components/text2d.tsx +2 -2
- package/src/components/tile-layer.tsx +21 -7
- package/src/components/view2d.tsx +2 -2
- package/src/dispatch.ts +10 -6
- package/src/frames.ts +1 -1
- package/src/glsl.ts +399 -0
- package/src/index.ts +14 -6
- package/src/layer.ts +260 -113
- package/src/material.ts +368 -0
- package/src/records.ts +198 -104
- package/src/style.ts +98 -0
- package/src/text.ts +64 -11
- package/src/tiles.ts +119 -24
- package/src/views.ts +17 -9
- package/src/shaders.ts +0 -323
package/src/text.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
//
|
|
10
10
|
// The node layer only: a run is a group. Three verbs like the sprites':
|
|
11
11
|
// addText, setText (re-laid out, the sprite pool reused), destroyText.
|
|
12
|
+
// A setText that changes only the paint (tint, outline, renderOrder)
|
|
13
|
+
// re-writes the glyph sprites without shaping: a fade is sprite writes,
|
|
14
|
+
// not an engine call per frame.
|
|
12
15
|
import { addGroup, addSprite, destroyGroup, destroySprite, setGroup, setSprite } from "./layer.ts"
|
|
13
16
|
import type { Sprite, SpriteGroup, SpriteLayer } from "./layer.ts"
|
|
14
17
|
import type { SpriteFont } from "./font.ts"
|
|
@@ -119,7 +122,9 @@ export function addText(layer: SpriteLayer, opts: AddTextOptions): TextRun {
|
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/** Change a run: the style fields re-lay it out (the sprite pool is
|
|
122
|
-
* reused), the pose fields move its group. Absent keys keep values
|
|
125
|
+
* reused), the pose fields move its group. Absent keys keep values, and so
|
|
126
|
+
* does a key passed with its current value; a change to only tint,
|
|
127
|
+
* outline or renderOrder re-writes the glyph sprites without shaping. */
|
|
123
128
|
export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void {
|
|
124
129
|
let state = run as RunState
|
|
125
130
|
if (state.group.layer === null) return
|
|
@@ -127,12 +132,42 @@ export function setText(run: TextRun, opts: Partial<TextStyle> & TextPose): void
|
|
|
127
132
|
if (x !== undefined || y !== undefined || rotation !== undefined || visible !== undefined) {
|
|
128
133
|
setGroup(state.group, { x, y, rotation, visible })
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
// Compare by value, not by presence: a component effect passes every
|
|
136
|
+
// style key on any change, and only a key that differs costs anything.
|
|
137
|
+
let current = state._style
|
|
138
|
+
let next: TextStyle = { ...current }
|
|
139
|
+
let reshape = false
|
|
140
|
+
let repaint = false
|
|
141
|
+
for (let k of Object.keys(style) as (keyof TextStyle)[]) {
|
|
142
|
+
let value = style[k]
|
|
143
|
+
if (value === undefined || sameStyleValue(k, value, current[k])) continue
|
|
144
|
+
;(next as Record<string, unknown>)[k] = value
|
|
145
|
+
if (PAINT_KEYS.has(k)) repaint = true
|
|
146
|
+
else reshape = true
|
|
135
147
|
}
|
|
148
|
+
if (reshape) relayout(state, next)
|
|
149
|
+
else if (repaint) {
|
|
150
|
+
checkStyle(next)
|
|
151
|
+
state._style = next
|
|
152
|
+
paint(state)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The style keys that change only how a glyph sprite is drawn, never which
|
|
157
|
+
// glyphs or where: every other key re-shapes.
|
|
158
|
+
const PAINT_KEYS: ReadonlySet<keyof TextStyle> = new Set<keyof TextStyle>(["tint", "outline", "renderOrder"])
|
|
159
|
+
|
|
160
|
+
// Equal as a style value: tint by channel, outline by colour and width,
|
|
161
|
+
// the rest by identity.
|
|
162
|
+
function sameStyleValue(key: keyof TextStyle, a: unknown, b: unknown): boolean {
|
|
163
|
+
if (a === b) return true
|
|
164
|
+
if (key === "tint" && Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((c, i) => c === b[i])
|
|
165
|
+
if (key === "outline" && a != null && b != null) {
|
|
166
|
+
let { color: ca, width: wa } = a as NonNullable<TextStyle["outline"]>
|
|
167
|
+
let { color: cb, width: wb } = b as NonNullable<TextStyle["outline"]>
|
|
168
|
+
return wa === wb && ca[0] === cb[0] && ca[1] === cb[1] && ca[2] === cb[2]
|
|
169
|
+
}
|
|
170
|
+
return false
|
|
136
171
|
}
|
|
137
172
|
|
|
138
173
|
/** Remove the run: its group and every glyph sprite. */
|
|
@@ -164,6 +199,27 @@ function relayout(run: RunState, style: TextStyle): void {
|
|
|
164
199
|
place(run)
|
|
165
200
|
}
|
|
166
201
|
|
|
202
|
+
// The paint fields of the run's style, as every glyph sprite takes them.
|
|
203
|
+
function paintOf(style: TextStyle) {
|
|
204
|
+
return { tint: style.tint ?? [1, 1, 1, 1], outline: style.outline ?? null, renderOrder: style.renderOrder ?? 0 }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Write a sprite's outline from the style (none clears it).
|
|
208
|
+
function writeOutline(layer: SpriteLayer, sprite: Sprite, outline: TextStyle["outline"]): void {
|
|
209
|
+
if (outline != null) layer._outline(sprite, outline.color[0], outline.color[1], outline.color[2], outline.width)
|
|
210
|
+
else layer._outline(sprite, 0, 0, 0, 0)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Re-paint the glyph sprites in place: the layout, the pool and the
|
|
214
|
+
// pending cells all stand, only tint, outline and order are written.
|
|
215
|
+
function paint(run: RunState): void {
|
|
216
|
+
let { tint, outline, renderOrder } = paintOf(run._style)
|
|
217
|
+
for (let sprite of run.sprites) {
|
|
218
|
+
setSprite(sprite, { tint, renderOrder })
|
|
219
|
+
writeOutline(run.layer, sprite, outline)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
167
223
|
// Write every glyph sprite from the layout and the font's cells: a cell
|
|
168
224
|
// the atlas lacks leaves its sprite hidden and asks the font for it.
|
|
169
225
|
function place(run: RunState): void {
|
|
@@ -172,9 +228,7 @@ function place(run: RunState): void {
|
|
|
172
228
|
let fontSize = style.fontSize ?? font.fontSize
|
|
173
229
|
// Cells are made at `font.size` texels per em; the run draws at fontSize.
|
|
174
230
|
let scale = fontSize / font.size
|
|
175
|
-
let
|
|
176
|
-
let outline = style.outline ?? null
|
|
177
|
-
let renderOrder = style.renderOrder ?? 0
|
|
231
|
+
let { tint, outline, renderOrder } = paintOf(style)
|
|
178
232
|
let placements = run._layout.glyphs
|
|
179
233
|
run._pending.clear()
|
|
180
234
|
let missing: number[] = []
|
|
@@ -210,8 +264,7 @@ function place(run: RunState): void {
|
|
|
210
264
|
} else {
|
|
211
265
|
setSprite(sprite, fields)
|
|
212
266
|
}
|
|
213
|
-
|
|
214
|
-
else layer._outline(sprite, 0, 0, 0, 0)
|
|
267
|
+
writeOutline(layer, sprite, outline)
|
|
215
268
|
used++
|
|
216
269
|
}
|
|
217
270
|
for (let i = used; i < sprites.length; i++) destroySprite(sprites[i]!)
|
package/src/tiles.ts
CHANGED
|
@@ -5,15 +5,18 @@
|
|
|
5
5
|
// chunk textures, and scrolling is a transform on the composited world
|
|
6
6
|
// (see <TileLayer> in components/tile-layer.tsx), never a repaint.
|
|
7
7
|
//
|
|
8
|
-
// Each chunk is a manual target over the layer's ONE
|
|
9
|
-
//
|
|
8
|
+
// Each chunk is a manual target over the layer's ONE pipeline (its
|
|
9
|
+
// material's, resolved once per layer for its atlases) with fixed record
|
|
10
10
|
// slots - record localRow * chunkTiles + localCol IS that tile, an empty
|
|
11
11
|
// tile is a zero-size quad, instance count is constant per chunk. Records
|
|
12
12
|
// hold WORLD pixel coordinates; the chunk target's uCamera is its pixel
|
|
13
13
|
// origin, so the shared vertex stage does the chunk-local mapping (the
|
|
14
14
|
// same mechanism a camera pass uses, pointed at a chunk rect). A cell's
|
|
15
15
|
// frame names its atlas like a sprite's (the record's atlas index), so a
|
|
16
|
-
// tile world draws from several tilesets in one bake.
|
|
16
|
+
// tile world draws from several tilesets in one bake. The material's
|
|
17
|
+
// style streams are per chunk too (a cell's `style`), plain arrays
|
|
18
|
+
// uploaded whole with the chunk's records; its params reach every
|
|
19
|
+
// resident chunk, each write a re-bake like the layer tint.
|
|
17
20
|
//
|
|
18
21
|
// Chunks allocate lazily on the first setTile that gives them content: an
|
|
19
22
|
// empty chunk costs nothing - no records, no buffer, no texture - so a
|
|
@@ -32,20 +35,27 @@ import {
|
|
|
32
35
|
destroyBuffer,
|
|
33
36
|
destroyTexture,
|
|
34
37
|
endBufferWrite,
|
|
38
|
+
isFloatLayout,
|
|
39
|
+
layoutFields,
|
|
40
|
+
layoutStride,
|
|
35
41
|
limits,
|
|
36
42
|
renderTarget,
|
|
37
43
|
setTargetParams,
|
|
38
44
|
setTargetSize,
|
|
45
|
+
writeBuffer,
|
|
46
|
+
writeRecord,
|
|
39
47
|
} from "@solidrt/core/gpu"
|
|
40
|
-
import type {
|
|
48
|
+
import type { AttributeAccess, BufferId, FilterMode, ShaderParams, TextureId, VertexAttribute } from "@solidrt/core/gpu"
|
|
41
49
|
import { checkAtlases, frameIndex } from "./atlas.ts"
|
|
42
50
|
import type { Atlas } from "./atlas.ts"
|
|
43
51
|
import { isFrame } from "./frames.ts"
|
|
44
52
|
import type { Frame } from "./frames.ts"
|
|
45
|
-
import {
|
|
53
|
+
import { atlasBindings, RECORD_ATTRIBUTES } from "./glsl.ts"
|
|
54
|
+
import { checkParams, checkTint } from "./layer.ts"
|
|
55
|
+
import { materialBlank, materialSamplers, unlit } from "./material.ts"
|
|
56
|
+
import type { Material } from "./material.ts"
|
|
46
57
|
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
47
58
|
import { INSTANCE_FLOATS } from "./records.ts"
|
|
48
|
-
import { atlasBindings, createSpritePipeline, INSTANCE_ATTRIBUTES, VERTEX } from "./shaders.ts"
|
|
49
59
|
import { checkCell, checkRect, chunkOf, eachChunkSlice, slotOf } from "./tiles-math.ts"
|
|
50
60
|
|
|
51
61
|
const RESOLVED = Promise.resolve()
|
|
@@ -77,12 +87,19 @@ export type TileLayerOptions = {
|
|
|
77
87
|
*/
|
|
78
88
|
filter?: FilterMode
|
|
79
89
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
90
|
+
* What the cells look like: the layer's material (see
|
|
91
|
+
* SpriteLayerOptions.material), fixed at creation; default `unlit()`.
|
|
92
|
+
* Its blend is how the cells bake into their chunks ("add" for a glow
|
|
93
|
+
* or light map baked over the transparent chunk clear), its params and
|
|
94
|
+
* textures reach every chunk target, its instance buffers are a style
|
|
95
|
+
* record per cell (`setTile`'s `style`). A tile layer BAKES, so a
|
|
96
|
+
* param change re-renders every resident chunk, like the tint:
|
|
97
|
+
* animated uniforms belong on a sprite layer.
|
|
84
98
|
*/
|
|
85
|
-
|
|
99
|
+
material?: Material
|
|
100
|
+
/** Uniform values seeded on every chunk over the material's own; see
|
|
101
|
+
* TileLayer.setParams. */
|
|
102
|
+
params?: ShaderParams
|
|
86
103
|
/**
|
|
87
104
|
* Target texels per world pixel in the baked chunks (positive integer,
|
|
88
105
|
* default 1); see TileLayer.setOversample. `<TileLayer>` picks it from
|
|
@@ -120,10 +137,26 @@ export type TileChunk = {
|
|
|
120
137
|
height: number
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/** A cell write's options: the cell's tint and, under a material with
|
|
141
|
+
* instance buffers, its style record. */
|
|
142
|
+
export type TileCellOptions = {
|
|
143
|
+
/** The cell's tint, multiplied over its texels like a sprite's; absent
|
|
144
|
+
* keeps the cell's current tint. */
|
|
145
|
+
tint?: Tint
|
|
146
|
+
/** The cell's style record (the material's FIRST instance buffer, one
|
|
147
|
+
* value per component in attribute order), absent keeps it; a cell set
|
|
148
|
+
* from empty starts at the material's instanceStyle. Throws on a
|
|
149
|
+
* material without an instance buffer. */
|
|
150
|
+
style?: ArrayLike<number>
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
export type TileLayer = {
|
|
124
154
|
/** The atlases the layer bakes from, as declared at creation: every
|
|
125
155
|
* frame set names one of their textures (see LayerBase.atlases). */
|
|
126
156
|
readonly atlases: readonly Atlas[]
|
|
157
|
+
/** The material the layer bakes with, as declared at creation (the
|
|
158
|
+
* stock `unlit()` when none was). */
|
|
159
|
+
readonly material: Material
|
|
127
160
|
/** Grid shape, fixed at creation. */
|
|
128
161
|
cols: number
|
|
129
162
|
rows: number
|
|
@@ -164,7 +197,7 @@ export type TileLayer = {
|
|
|
164
197
|
* a frame) publishes and re-bakes ONLY the chunks that changed, however
|
|
165
198
|
* many tiles did.
|
|
166
199
|
*/
|
|
167
|
-
setTile(col: number, row: number, frame: Frame | null, opts?:
|
|
200
|
+
setTile(col: number, row: number, frame: Frame | null, opts?: TileCellOptions): void
|
|
168
201
|
/**
|
|
169
202
|
* Set a rect of cells at once: `cols` x `rows` cells from `col`, `row`,
|
|
170
203
|
* `cells` in row-major order (`cols * rows` long) - frames (null clears)
|
|
@@ -174,10 +207,10 @@ export type TileLayer = {
|
|
|
174
207
|
* touches, the per-cell loop inside the layer over its own records, the
|
|
175
208
|
* same batching and flush. A chunk the rect only clears never allocates,
|
|
176
209
|
* so a whole-world write of a sparse world stays sparse. `tint` applies
|
|
177
|
-
* to every cell set, as setTile's
|
|
178
|
-
* bad entry throws before any cell changes.
|
|
210
|
+
* to every cell set, as setTile's, and so does `style`; absent, each
|
|
211
|
+
* cell keeps its own. A bad entry throws before any cell changes.
|
|
179
212
|
*/
|
|
180
|
-
setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?:
|
|
213
|
+
setTiles(col: number, row: number, cols: number, rows: number, cells: ArrayLike<Frame | null> | ArrayLike<number>, opts?: TileCellOptions): void
|
|
181
214
|
/** The frame at a cell, or null when empty. */
|
|
182
215
|
getTile(col: number, row: number): Frame | null
|
|
183
216
|
/** The frames table given at creation (what setTiles' indices name), or
|
|
@@ -191,18 +224,35 @@ export type TileLayer = {
|
|
|
191
224
|
* animate it coarsely, not per frame.
|
|
192
225
|
*/
|
|
193
226
|
setTint(tint: [number, number, number, number]): void
|
|
227
|
+
/**
|
|
228
|
+
* Write uniform values to every resident chunk (the material's prelude
|
|
229
|
+
* uniforms, over its own `params`): LayerBase.setParams for a baked
|
|
230
|
+
* layer, so each write re-renders every resident chunk, like setTint -
|
|
231
|
+
* drive it from slow state, not per frame. Absent names keep their
|
|
232
|
+
* values; a name the program does not declare is skipped.
|
|
233
|
+
*/
|
|
234
|
+
setParams(params: ShaderParams): void
|
|
194
235
|
dispose(): void
|
|
195
236
|
}
|
|
196
237
|
|
|
197
238
|
type Chunk = TileChunk & {
|
|
198
239
|
records: Float32Array
|
|
199
240
|
buffer: BufferId
|
|
241
|
+
/** The material's style records of the chunk, one array and buffer
|
|
242
|
+
* per instance buffer, uploaded whole with the records. */
|
|
243
|
+
styles: ChunkStyle[]
|
|
200
244
|
dirty: boolean
|
|
201
245
|
/** Records changed since the last flush (a tint-only or resize re-bake
|
|
202
246
|
* renders without re-uploading them). */
|
|
203
247
|
wrote: boolean
|
|
204
248
|
}
|
|
205
249
|
|
|
250
|
+
type ChunkStyle = { bytes: Uint8Array; buffer: BufferId; fields: AttributeAccess[]; floats: Float32Array | null }
|
|
251
|
+
|
|
252
|
+
// One style stream of the material as the tile layer keeps it: the
|
|
253
|
+
// layout, the blank record, and the stride.
|
|
254
|
+
type StyleShape = { layout: VertexAttribute[]; blank: Uint8Array; stride: number }
|
|
255
|
+
|
|
206
256
|
/**
|
|
207
257
|
* Create a baked tile layer: `cols` x `rows` cells of `tileW` x `tileH`
|
|
208
258
|
* pixels, every tile drawing one atlas frame, baked into lazily-allocated
|
|
@@ -243,9 +293,12 @@ export function createTileLayer(
|
|
|
243
293
|
)
|
|
244
294
|
}
|
|
245
295
|
let label = opts?.label ?? "tiles"
|
|
246
|
-
let blend: BlendMode = opts?.blend ?? "alpha"
|
|
247
296
|
let tint: Tint = opts?.tint ?? [1, 1, 1, 1]
|
|
248
297
|
checkTint("createTileLayer", tint)
|
|
298
|
+
let material = opts?.material ?? unlit()
|
|
299
|
+
checkAtlases("createTileLayer", atlases, materialSamplers(material))
|
|
300
|
+
let params: ShaderParams = { ...material.params, ...opts?.params }
|
|
301
|
+
let shapes: StyleShape[] = (material.instanceBuffers ?? []).map((b, i) => ({ layout: b.attributes, blank: materialBlank(material, i), stride: layoutStride(b.attributes) }))
|
|
249
302
|
// The frames table, copied (the caller's array may move on) and checked
|
|
250
303
|
// once, plus its UVs and atlas index as five floats per entry: an index
|
|
251
304
|
// write copies by offset instead of reading a frame object per cell.
|
|
@@ -273,9 +326,10 @@ export function createTileLayer(
|
|
|
273
326
|
let chunkCols = Math.ceil(cols / chunkTiles)
|
|
274
327
|
let perChunk = chunkTiles * chunkTiles
|
|
275
328
|
// The one pipeline (and unit quad) every chunk target draws with:
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
let
|
|
329
|
+
// the material's, resolved once per layer, however many chunks
|
|
330
|
+
// allocate.
|
|
331
|
+
let gpu = material.pipeline(atlases, [RECORD_ATTRIBUTES], label)
|
|
332
|
+
let textures = { ...atlasBindings(atlases.map(a => a.texture)), ...material.textures }
|
|
279
333
|
|
|
280
334
|
let disposed = false
|
|
281
335
|
let scheduled = false
|
|
@@ -309,6 +363,7 @@ export function createTileLayer(
|
|
|
309
363
|
let out = beginBufferWrite(chunk.buffer)
|
|
310
364
|
out.set(chunk.records)
|
|
311
365
|
endBufferWrite(chunk.buffer, chunk.records.byteLength)
|
|
366
|
+
for (let style of chunk.styles) writeBuffer(style.buffer, style.bytes)
|
|
312
367
|
}
|
|
313
368
|
renderTarget(chunk.texture)
|
|
314
369
|
}
|
|
@@ -328,16 +383,29 @@ export function createTileLayer(
|
|
|
328
383
|
let y = Math.floor(index / chunkCols) * chunkH
|
|
329
384
|
let records = new Float32Array(perChunk * INSTANCE_FLOATS)
|
|
330
385
|
let buffer = createBuffer(records.byteLength, { label: `${label}-chunk-records`, autoFree: false })
|
|
386
|
+
// Every cell's style slot starts blank (the material's
|
|
387
|
+
// instanceStyle), so an unwritten cell shows the material's default.
|
|
388
|
+
let styles: ChunkStyle[] = shapes.map((shape, i) => {
|
|
389
|
+
let bytes = new Uint8Array(perChunk * shape.stride)
|
|
390
|
+
for (let slot = 0; slot < perChunk; slot++) bytes.set(shape.blank, slot * shape.stride)
|
|
391
|
+
let view = new DataView(bytes.buffer)
|
|
392
|
+
return {
|
|
393
|
+
bytes,
|
|
394
|
+
buffer: createBuffer(bytes.byteLength, { label: `${label}-chunk-style${i}`, autoFree: false }),
|
|
395
|
+
fields: layoutFields(shape.layout, () => view),
|
|
396
|
+
floats: isFloatLayout(shape.layout) ? new Float32Array(bytes.buffer) : null,
|
|
397
|
+
}
|
|
398
|
+
})
|
|
331
399
|
let texture = createShaderTarget(
|
|
332
400
|
gpu.pipeline,
|
|
333
401
|
chunkW * oversample,
|
|
334
402
|
chunkH * oversample,
|
|
335
403
|
// Bake targets pin the camera rotation to identity: the tile camera
|
|
336
404
|
// rotates the COMPOSITED world (<TileLayer>'s view), never the bake.
|
|
337
|
-
{ uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint },
|
|
405
|
+
{ uViewport: [chunkW, chunkH], uCamera: [x, y, 1, 1], uCameraRot: [1, 0, 0, 0], uTint: tint, ...params },
|
|
338
406
|
{
|
|
339
407
|
label: `${label}-chunk`,
|
|
340
|
-
buffers: [gpu.quad, buffer],
|
|
408
|
+
buffers: [gpu.quad, buffer, ...styles.map(s => s.buffer)],
|
|
341
409
|
vertexCount: 4,
|
|
342
410
|
instanceCount: perChunk,
|
|
343
411
|
textures,
|
|
@@ -347,7 +415,7 @@ export function createTileLayer(
|
|
|
347
415
|
autoFree: false,
|
|
348
416
|
},
|
|
349
417
|
)
|
|
350
|
-
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, dirty: false, wrote: false }
|
|
418
|
+
let chunk: Chunk = { texture, x, y, width: chunkW, height: chunkH, records, buffer, styles, dirty: false, wrote: false }
|
|
351
419
|
resident.set(index, chunk)
|
|
352
420
|
layer.chunks.push(chunk)
|
|
353
421
|
layer.onChunk?.(chunk)
|
|
@@ -386,14 +454,27 @@ export function createTileLayer(
|
|
|
386
454
|
}
|
|
387
455
|
}
|
|
388
456
|
// Clear one cell: a zero-size quad draws nothing (the tint stays, and
|
|
389
|
-
// a later set from empty resets it).
|
|
457
|
+
// a later set from empty resets it; the style stays the same way).
|
|
390
458
|
let clearCell = (r: Float32Array, at: number): void => {
|
|
391
459
|
r[at + 2] = 0
|
|
392
460
|
r[at + 3] = 0
|
|
393
461
|
}
|
|
462
|
+
// A cell write's style, checked before any cell changes: the material
|
|
463
|
+
// must declare an instance buffer, and the record must be its length
|
|
464
|
+
// (the encode throws on a wrong count).
|
|
465
|
+
let checkStyle = (verb: string, style: ArrayLike<number> | undefined): void => {
|
|
466
|
+
if (style === undefined) return
|
|
467
|
+
if (shapes.length === 0) throw new Error(`${verb}: the layer's material declares no instance buffer (no style record)`)
|
|
468
|
+
}
|
|
469
|
+
// Write a cell's style record into its chunk's first style stream.
|
|
470
|
+
let writeStyle = (chunk: Chunk, slot: number, style: ArrayLike<number>, verb: string): void => {
|
|
471
|
+
let s = chunk.styles[0]!
|
|
472
|
+
writeRecord(shapes[0]!.layout, s.fields, s.floats, slot, style, verb)
|
|
473
|
+
}
|
|
394
474
|
|
|
395
475
|
let layer: TileLayer = {
|
|
396
476
|
atlases,
|
|
477
|
+
material,
|
|
397
478
|
cols,
|
|
398
479
|
rows,
|
|
399
480
|
tileW,
|
|
@@ -432,9 +513,11 @@ export function createTileLayer(
|
|
|
432
513
|
} else {
|
|
433
514
|
if (!isFrame(frame)) throw new Error(`setTile: not a frame, got ${JSON.stringify(frame)}`)
|
|
434
515
|
if (opts?.tint !== undefined) checkTint("setTile", opts.tint)
|
|
516
|
+
checkStyle("setTile", opts?.style)
|
|
435
517
|
let atlas = frameIndex("setTile", atlasIndex, frame)
|
|
436
518
|
chunk ??= allocate(index)
|
|
437
519
|
writeCell(chunk.records, at, col, row, frame.u0, frame.v0, frame.u1, frame.v1, atlas, opts?.tint)
|
|
520
|
+
if (opts?.style !== undefined) writeStyle(chunk, at / INSTANCE_FLOATS, opts.style, "setTile")
|
|
438
521
|
}
|
|
439
522
|
touch(chunk)
|
|
440
523
|
},
|
|
@@ -443,7 +526,9 @@ export function createTileLayer(
|
|
|
443
526
|
checkRect("setTiles", col, row, w, h, cols, rows)
|
|
444
527
|
if (cells.length !== w * h) throw new Error(`setTiles: a ${w} x ${h} rect takes ${w * h} cells, got ${cells.length}`)
|
|
445
528
|
if (opts?.tint !== undefined) checkTint("setTiles", opts.tint)
|
|
529
|
+
checkStyle("setTiles", opts?.style)
|
|
446
530
|
let cellTint = opts?.tint
|
|
531
|
+
let cellStyle = opts?.style
|
|
447
532
|
// The two forms: indices into the table, or frames. Every entry is
|
|
448
533
|
// checked (and a frame resolved to its atlas) before the first write,
|
|
449
534
|
// so a bad one changes nothing.
|
|
@@ -512,6 +597,7 @@ export function createTileLayer(
|
|
|
512
597
|
r = chunk.records
|
|
513
598
|
}
|
|
514
599
|
writeCell(r, at, x, y, u0, v0, u1, v1, atlas, cellTint)
|
|
600
|
+
if (cellStyle !== undefined) writeStyle(chunk!, at / INSTANCE_FLOATS, cellStyle, "setTiles")
|
|
515
601
|
}
|
|
516
602
|
}
|
|
517
603
|
if (chunk) touch(chunk)
|
|
@@ -536,6 +622,15 @@ export function createTileLayer(
|
|
|
536
622
|
touch(chunk, false)
|
|
537
623
|
}
|
|
538
624
|
},
|
|
625
|
+
setParams(next) {
|
|
626
|
+
if (disposed) return
|
|
627
|
+
checkParams("setParams", next)
|
|
628
|
+
Object.assign(params, next)
|
|
629
|
+
for (let chunk of resident.values()) {
|
|
630
|
+
setTargetParams(chunk.texture, next)
|
|
631
|
+
touch(chunk, false)
|
|
632
|
+
}
|
|
633
|
+
},
|
|
539
634
|
dispose() {
|
|
540
635
|
if (disposed) return
|
|
541
636
|
disposed = true
|
|
@@ -543,10 +638,10 @@ export function createTileLayer(
|
|
|
543
638
|
for (let chunk of resident.values()) {
|
|
544
639
|
destroyTexture(chunk.texture)
|
|
545
640
|
destroyBuffer(chunk.buffer)
|
|
641
|
+
for (let style of chunk.styles) destroyBuffer(style.buffer)
|
|
546
642
|
}
|
|
547
643
|
resident.clear()
|
|
548
644
|
layer.chunks.length = 0
|
|
549
|
-
gpu.dispose()
|
|
550
645
|
},
|
|
551
646
|
}
|
|
552
647
|
|
package/src/views.ts
CHANGED
|
@@ -18,14 +18,13 @@
|
|
|
18
18
|
// a sprite under a minimap gets its ordinary handlers, and the view's
|
|
19
19
|
// listeners are the last stop.
|
|
20
20
|
import { addDraw, createDrawTarget, destroyTexture, removeDraw, setDrawBuffers, setDrawRange, setTargetParams, setTargetSize } from "@solidrt/core/gpu"
|
|
21
|
-
import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, TextureId } from "@solidrt/core/gpu"
|
|
21
|
+
import type { BufferId, DrawId, InstanceOrder, RenderPipelineId, ShaderParams, TextureBindings, TextureId } from "@solidrt/core/gpu"
|
|
22
22
|
import { applyCamera, cameraParams, checkCamera, defaultCamera, projectCamera, unprojectCamera } from "./camera.ts"
|
|
23
23
|
import type { CameraState, CameraUpdate } from "./camera.ts"
|
|
24
24
|
import { spriteDispatch } from "./dispatch.ts"
|
|
25
25
|
import type { PointerFeed } from "@solidrt/core"
|
|
26
26
|
import type { LayerPointerListener, Sprite, LayerHandlers } from "./layer.ts"
|
|
27
27
|
import { checkOversample, thrashSentinel } from "./oversample.ts"
|
|
28
|
-
import { atlasBindings } from "./shaders.ts"
|
|
29
28
|
|
|
30
29
|
export type ViewOptions = {
|
|
31
30
|
/** View pixels: the viewport its camera maps the world onto. */
|
|
@@ -111,16 +110,20 @@ export type ViewDeps = {
|
|
|
111
110
|
label: string
|
|
112
111
|
pipeline: RenderPipelineId
|
|
113
112
|
quad: BufferId
|
|
114
|
-
/** The
|
|
115
|
-
* compiled for exactly
|
|
116
|
-
|
|
113
|
+
/** The sampler bindings every view binds: the layer's atlases by
|
|
114
|
+
* sampler index (the pipeline was compiled for exactly those) and the
|
|
115
|
+
* material's own textures. */
|
|
116
|
+
textures: TextureBindings
|
|
117
117
|
/** The layer's current instance buffers, in the pipeline's layout order
|
|
118
|
-
* after the quad (the pose and
|
|
119
|
-
* record buffer on records). */
|
|
118
|
+
* after the quad (the pose and sprite pair on the node layer, the one
|
|
119
|
+
* record buffer on records, the material's style buffers after them). */
|
|
120
120
|
buffers: () => BufferId[]
|
|
121
121
|
/** The instance count as last published. */
|
|
122
122
|
count: () => number
|
|
123
123
|
tint: () => [number, number, number, number]
|
|
124
|
+
/** The params every view seeds beyond the viewport, the camera and
|
|
125
|
+
* the tint: the material's, with the layer's own over them. */
|
|
126
|
+
params: () => ShaderParams
|
|
124
127
|
/** The layer's pick, `zoom` the camera zoom its floors are measured at. */
|
|
125
128
|
pick: (x: number, y: number, zoom: number) => Sprite[]
|
|
126
129
|
/** The layer's key order (`orderBy`), declared by ONE live entry. */
|
|
@@ -135,6 +138,8 @@ export type Views = {
|
|
|
135
138
|
setBuffers(instanceBuffers: BufferId[]): void
|
|
136
139
|
setCount(count: number): void
|
|
137
140
|
setTint(tint: [number, number, number, number]): void
|
|
141
|
+
/** The layer's params changed: every view's target takes the write. */
|
|
142
|
+
setParams(params: ShaderParams): void
|
|
138
143
|
dispose(): void
|
|
139
144
|
}
|
|
140
145
|
|
|
@@ -180,8 +185,8 @@ export function createViews(deps: ViewDeps): Views {
|
|
|
180
185
|
let texture = createDrawTarget(
|
|
181
186
|
width * oversample,
|
|
182
187
|
height * oversample,
|
|
183
|
-
{ uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint() },
|
|
184
|
-
{ textures:
|
|
188
|
+
{ uViewport: [width, height], ...cameraParams(cam), uTint: deps.tint(), ...deps.params() },
|
|
189
|
+
{ textures: deps.textures, clearColor: opts.clearColor ?? [0, 0, 0, 0], label, autoFree: false },
|
|
185
190
|
)
|
|
186
191
|
let order = ordered === null ? deps.order : undefined
|
|
187
192
|
let entry = entryFor(texture, order)
|
|
@@ -277,6 +282,9 @@ export function createViews(deps: ViewDeps): Views {
|
|
|
277
282
|
setTint(tint) {
|
|
278
283
|
for (let v of views) setTargetParams(v.texture, { uTint: tint })
|
|
279
284
|
},
|
|
285
|
+
setParams(params) {
|
|
286
|
+
for (let v of views) setTargetParams(v.texture, params)
|
|
287
|
+
},
|
|
280
288
|
dispose() {
|
|
281
289
|
for (let v of [...views]) v.dispose()
|
|
282
290
|
},
|