@aethervtt/shard-sprite 0.0.1 → 0.1.0
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/dist/baseline/lights2d.d.ts +6 -0
- package/dist/baseline/lights2d.js +114 -0
- package/dist/ground.d.ts +81 -0
- package/dist/ground.js +394 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +52 -2
- package/dist/lighting-shaders.js +4 -4
- package/dist/lights2d.d.ts +23 -7
- package/dist/lights2d.js +78 -40
- package/dist/methods.d.ts +82 -0
- package/dist/methods.js +246 -0
- package/dist/plugin.d.ts +2 -1
- package/dist/plugin.js +43 -6
- package/dist/render.d.ts +19 -7
- package/dist/render.js +60 -51
- package/dist/shaders.js +11 -6
- package/dist/tilemap-format.d.ts +39 -0
- package/dist/tilemap-format.js +177 -0
- package/dist/tilemap.d.ts +83 -12
- package/dist/tilemap.js +311 -58
- package/dist/validate.d.ts +11 -0
- package/dist/validate.js +49 -0
- package/package.json +9 -9
package/dist/methods.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import { defineSchema, ShardError, t } from "@aethervtt/shard-core";
|
|
3
|
+
import { TextureAtlases } from "./atlas.js";
|
|
4
|
+
import { Tilemap, TilemapData, TilemapDatas, tileNames } from "./tilemap.js";
|
|
5
|
+
import { layerChunks, readChunkRows, rectRows, TILE_FLAG_NAMES } from "./tilemap-format.js";
|
|
6
|
+
function layerNamed(data, name) {
|
|
7
|
+
const layer = name ? data.layers.find((l) => l.name === name) : data.layers[0];
|
|
8
|
+
if (!layer) {
|
|
9
|
+
throw new ShardError("sprite/no-layer", `The tilemap has no layer ${JSON.stringify(name)}`, {
|
|
10
|
+
hint: `Its layers: ${data.layers.map((l) => l.name).join(", ") || "none"}.`
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return layer;
|
|
14
|
+
}
|
|
15
|
+
function checkRect(layer, r, path) {
|
|
16
|
+
if (r.x + r.w > layer.width || r.y + r.h > layer.height) {
|
|
17
|
+
throw new ShardError(
|
|
18
|
+
"sprite/tile-out-of-range",
|
|
19
|
+
`Rect ${r.x},${r.y} ${r.w}\xD7${r.h} is outside the ${layer.width}\xD7${layer.height} layer "${layer.name}"`,
|
|
20
|
+
{ path }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function chunkRect(data, layer, chunk, path) {
|
|
25
|
+
if (chunk.length !== 2) throw new ShardError("sprite/bad-chunk", "A chunk is [cx, cy]", { path });
|
|
26
|
+
const cs = data.chunkSize;
|
|
27
|
+
const x = chunk[0] * cs;
|
|
28
|
+
const y = chunk[1] * cs;
|
|
29
|
+
checkRect(layer, { x, y, w: 1, h: 1 }, path);
|
|
30
|
+
return { x, y, w: Math.min(cs, layer.width - x), h: Math.min(cs, layer.height - y) };
|
|
31
|
+
}
|
|
32
|
+
function flagsOf(value, path) {
|
|
33
|
+
if (typeof value === "number") return value;
|
|
34
|
+
if (!value) return 0;
|
|
35
|
+
let flags = 0;
|
|
36
|
+
for (const f of String(value).split("+")) {
|
|
37
|
+
const bit = TILE_FLAG_NAMES[f];
|
|
38
|
+
if (!bit) {
|
|
39
|
+
throw new ShardError("sprite/bad-flag", `Unknown tile flag "${f}"`, {
|
|
40
|
+
path,
|
|
41
|
+
hint: "Flags are fx, fy and r90, joined by +."
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
flags |= bit;
|
|
45
|
+
}
|
|
46
|
+
return flags;
|
|
47
|
+
}
|
|
48
|
+
function idOf(data, name) {
|
|
49
|
+
return name === "." || name === "" ? 0 : data.tileId(name);
|
|
50
|
+
}
|
|
51
|
+
function readTiles(data, options = {}, atlas) {
|
|
52
|
+
const layer = layerNamed(data, options.layer);
|
|
53
|
+
const names = tileNames(data, atlas);
|
|
54
|
+
const head = {
|
|
55
|
+
layer: layer.name,
|
|
56
|
+
width: layer.width,
|
|
57
|
+
height: layer.height,
|
|
58
|
+
chunkSize: data.chunkSize,
|
|
59
|
+
encoding: data.encoding
|
|
60
|
+
};
|
|
61
|
+
const rect = options.rect;
|
|
62
|
+
if (rect && rect.w > 0 && rect.h > 0) {
|
|
63
|
+
checkRect(layer, rect, "/rect");
|
|
64
|
+
return { ...head, rect, rows: rectRows(layer, names, rect.x, rect.y, rect.w, rect.h).rows };
|
|
65
|
+
}
|
|
66
|
+
if (options.chunk && options.chunk.length > 0) {
|
|
67
|
+
const r = chunkRect(data, layer, options.chunk, "/chunk");
|
|
68
|
+
return {
|
|
69
|
+
...head,
|
|
70
|
+
chunk: [...options.chunk],
|
|
71
|
+
rows: rectRows(layer, names, r.x, r.y, r.w, r.h).rows
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { ...head, chunks: layerChunks(layer, names, data.chunkSize) };
|
|
75
|
+
}
|
|
76
|
+
function editTiles(data, options, atlas) {
|
|
77
|
+
const name = layerNamed(data, options.layer).name;
|
|
78
|
+
if (!data.palette) {
|
|
79
|
+
data.copyFrom(TilemapData.fromJson(data.toJson({ atlas: { names: tileNames(data, atlas) } })));
|
|
80
|
+
}
|
|
81
|
+
const layer = layerNamed(data, name);
|
|
82
|
+
let changed = 0;
|
|
83
|
+
const set = (x, y, id, flags, path) => {
|
|
84
|
+
if (x >= layer.width || y >= layer.height) {
|
|
85
|
+
throw new ShardError(
|
|
86
|
+
"sprite/tile-out-of-range",
|
|
87
|
+
`Cell (${x}, ${y}) is outside the ${layer.width}\xD7${layer.height} layer "${layer.name}"`,
|
|
88
|
+
{ path }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const i = y * layer.width + x;
|
|
92
|
+
if (layer.tiles[i] === id && layer.flags[i] === flags) return;
|
|
93
|
+
layer.set(x, y, id, flags);
|
|
94
|
+
changed++;
|
|
95
|
+
};
|
|
96
|
+
const cells = options.cells ?? [];
|
|
97
|
+
for (let k = 0; k < cells.length; k++) {
|
|
98
|
+
const c = cells[k];
|
|
99
|
+
set(c.x, c.y, idOf(data, c.tile), flagsOf(c.flags, `/cells/${k}/flags`), `/cells/${k}`);
|
|
100
|
+
}
|
|
101
|
+
const fill = options.fill;
|
|
102
|
+
if (fill && fill.rect.w > 0 && fill.rect.h > 0) {
|
|
103
|
+
checkRect(layer, fill.rect, "/fill/rect");
|
|
104
|
+
const id = idOf(data, fill.tile);
|
|
105
|
+
for (let y = fill.rect.y; y < fill.rect.y + fill.rect.h; y++)
|
|
106
|
+
for (let x = fill.rect.x; x < fill.rect.x + fill.rect.w; x++) set(x, y, id, 0, "/fill");
|
|
107
|
+
}
|
|
108
|
+
const rows = options.rows;
|
|
109
|
+
if (rows && rows.chunk.length > 0) {
|
|
110
|
+
const r = chunkRect(data, layer, rows.chunk, "/rows/chunk");
|
|
111
|
+
const grid = {
|
|
112
|
+
width: layer.width,
|
|
113
|
+
height: layer.height,
|
|
114
|
+
tiles: layer.tiles.slice(),
|
|
115
|
+
flags: layer.flags.slice()
|
|
116
|
+
};
|
|
117
|
+
for (let y = r.y; y < r.y + r.h; y++) {
|
|
118
|
+
grid.tiles.fill(0, y * layer.width + r.x, y * layer.width + r.x + r.w);
|
|
119
|
+
grid.flags.fill(0, y * layer.width + r.x, y * layer.width + r.x + r.w);
|
|
120
|
+
}
|
|
121
|
+
const palette = data.palette;
|
|
122
|
+
const index = new Map(palette.map((n, i) => [n, i]));
|
|
123
|
+
readChunkRows(
|
|
124
|
+
grid,
|
|
125
|
+
palette,
|
|
126
|
+
index,
|
|
127
|
+
rows.chunk[0],
|
|
128
|
+
rows.chunk[1],
|
|
129
|
+
data.chunkSize,
|
|
130
|
+
rows.rows,
|
|
131
|
+
"/rows/rows"
|
|
132
|
+
);
|
|
133
|
+
for (let y = r.y; y < r.y + r.h; y++)
|
|
134
|
+
for (let x = r.x; x < r.x + r.w; x++) {
|
|
135
|
+
const i = y * layer.width + x;
|
|
136
|
+
set(x, y, grid.tiles[i], grid.flags[i], "/rows");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return changed;
|
|
140
|
+
}
|
|
141
|
+
function findTilemapData(world, asset) {
|
|
142
|
+
const server = assetServer(world);
|
|
143
|
+
const entry = server.entry(asset);
|
|
144
|
+
const data = server.item(asset);
|
|
145
|
+
if (!(data instanceof TilemapData)) {
|
|
146
|
+
throw new ShardError(
|
|
147
|
+
"sprite/unknown-tilemap",
|
|
148
|
+
entry ? `${asset} is a ${entry.type}, not loaded TilemapData` : `No tilemap data "${asset}" is loaded`,
|
|
149
|
+
{ hint: "Name a *.tilemap.json asset by path or guid that a Tilemap in the world uses." }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const store = world.resource(TilemapDatas);
|
|
153
|
+
let atlas;
|
|
154
|
+
for (const table of world.query({ with: [Tilemap] }).tables) {
|
|
155
|
+
const dataRefs = table.column(Tilemap, "data");
|
|
156
|
+
const atlasRefs = table.column(Tilemap, "atlas");
|
|
157
|
+
for (let row = 0; row < table.count && !atlas; row++) {
|
|
158
|
+
if (store.get(dataRefs[row]) !== data) continue;
|
|
159
|
+
atlas = world.tryResource(TextureAtlases)?.get(atlasRefs[row]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { data, guid: entry?.guid, atlas };
|
|
163
|
+
}
|
|
164
|
+
const Rect = t.struct({
|
|
165
|
+
x: t.u32({ description: "Left column." }),
|
|
166
|
+
y: t.u32({ description: "Top row." }),
|
|
167
|
+
w: t.u32({ description: "Width in cells (0: no rect)." }),
|
|
168
|
+
h: t.u32({ description: "Height in cells." })
|
|
169
|
+
});
|
|
170
|
+
const tilemapMethods = [
|
|
171
|
+
{
|
|
172
|
+
name: "tilemap.read",
|
|
173
|
+
description: "A tilemap layer's cells as rows (0059): runs of name[:flags][*count], '.' empty, flags fx/fy/r90 joined by +. The whole layer by chunk, one chunk, or a rect.",
|
|
174
|
+
params: defineSchema("sprite/TilemapReadParams", {
|
|
175
|
+
asset: t.string({ required: true, description: "The TilemapData asset: path or guid." }),
|
|
176
|
+
layer: t.string({ description: "Layer name (default: the first)." }),
|
|
177
|
+
chunk: t.list(t.u32(), { description: "[cx, cy]: just this chunk." }),
|
|
178
|
+
rect: Rect
|
|
179
|
+
}),
|
|
180
|
+
handler: ({ world }, p) => {
|
|
181
|
+
const { data, atlas } = findTilemapData(world, p.asset);
|
|
182
|
+
return {
|
|
183
|
+
asset: p.asset,
|
|
184
|
+
...readTiles(
|
|
185
|
+
data,
|
|
186
|
+
{
|
|
187
|
+
layer: p.layer || void 0,
|
|
188
|
+
chunk: p.chunk,
|
|
189
|
+
rect: p.rect
|
|
190
|
+
},
|
|
191
|
+
atlas
|
|
192
|
+
)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "tilemap.edit",
|
|
198
|
+
description: "Edits a tilemap layer's cells by tile name (0059): cells, a filled rect, or a chunk's rows. Applies to the live world (only the touched chunks re-upload); save writes the asset file in its own encoding. Returns the changed cell count.",
|
|
199
|
+
params: defineSchema("sprite/TilemapEditParams", {
|
|
200
|
+
asset: t.string({ required: true, description: "The TilemapData asset: path or guid." }),
|
|
201
|
+
layer: t.string({ description: "Layer name (default: the first)." }),
|
|
202
|
+
cells: t.list(
|
|
203
|
+
t.struct({
|
|
204
|
+
x: t.u32(),
|
|
205
|
+
y: t.u32(),
|
|
206
|
+
tile: t.string({ description: "Palette name; '.' clears the cell." }),
|
|
207
|
+
flags: t.string({ description: "fx, fy and r90, joined by +." })
|
|
208
|
+
}),
|
|
209
|
+
{ description: "Cells to set." }
|
|
210
|
+
),
|
|
211
|
+
fill: t.struct(
|
|
212
|
+
{ rect: Rect, tile: t.string({ description: "Palette name; '.' clears." }) },
|
|
213
|
+
{ description: "A rect to fill with one tile." }
|
|
214
|
+
),
|
|
215
|
+
rows: t.struct(
|
|
216
|
+
{
|
|
217
|
+
chunk: t.list(t.u32(), { description: "[cx, cy]" }),
|
|
218
|
+
rows: t.list(t.string(), { description: "The chunk rows, from the top." })
|
|
219
|
+
},
|
|
220
|
+
{ description: "Replaces one chunk's cells with rows, as tilemap.read returns them." }
|
|
221
|
+
),
|
|
222
|
+
save: t.bool({ description: "Also write the asset file, keeping its encoding." })
|
|
223
|
+
}),
|
|
224
|
+
handler: async ({ world }, p) => {
|
|
225
|
+
const { data, guid, atlas } = findTilemapData(world, p.asset);
|
|
226
|
+
const changed = editTiles(
|
|
227
|
+
data,
|
|
228
|
+
{
|
|
229
|
+
layer: p.layer || void 0,
|
|
230
|
+
cells: p.cells,
|
|
231
|
+
fill: p.fill,
|
|
232
|
+
rows: p.rows
|
|
233
|
+
},
|
|
234
|
+
atlas
|
|
235
|
+
);
|
|
236
|
+
const saved = p.save ? await assetServer(world).writeSource(guid ?? p.asset, data.toJson()) : null;
|
|
237
|
+
return { changed, saved };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
export {
|
|
242
|
+
editTiles,
|
|
243
|
+
findTilemapData,
|
|
244
|
+
readTiles,
|
|
245
|
+
tilemapMethods
|
|
246
|
+
};
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sprites, atlases, frame animation, and tilemaps, drawn in the render graph: world sprites after
|
|
3
3
|
* opaque 3D (so 2D gets HDR and post-processing), screen sprites over the finished image. Cameras
|
|
4
|
-
* with Lighting2d light them with 2D lights and shadows.
|
|
4
|
+
* with Lighting2d light them with 2D lights and shadows. A tilemap with a GroundLayer draws on the
|
|
5
|
+
* ground instead, as chunk meshes among the tabletop's bands (0059).
|
|
5
6
|
*/
|
|
6
7
|
export declare const spritePlugin: import("@aethervtt/shard-runtime").Plugin;
|
package/dist/plugin.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
import { Last, Update } from "@aethervtt/shard-core";
|
|
2
|
-
import {
|
|
1
|
+
import { Last, PostUpdate, Update } from "@aethervtt/shard-core";
|
|
2
|
+
import {
|
|
3
|
+
addRenderFeatures,
|
|
4
|
+
Gpu,
|
|
5
|
+
Graph,
|
|
6
|
+
Picking,
|
|
7
|
+
RenderDescribers,
|
|
8
|
+
RenderSet,
|
|
9
|
+
Shaders
|
|
10
|
+
} from "@aethervtt/shard-render";
|
|
3
11
|
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
12
|
+
import { TransformSystems } from "@aethervtt/shard-transform";
|
|
4
13
|
import * as atlasModule from "./atlas.js";
|
|
5
14
|
import { TextureAtlases } from "./atlas.js";
|
|
6
15
|
import * as clipModule from "./clip.js";
|
|
7
16
|
import { animateSprites, SpriteClips } from "./clip.js";
|
|
17
|
+
import * as groundModule from "./ground.js";
|
|
18
|
+
import {
|
|
19
|
+
GROUND_TILE_SHADERS,
|
|
20
|
+
GroundTiles,
|
|
21
|
+
observeGroundTiles,
|
|
22
|
+
syncGroundTiles,
|
|
23
|
+
tileDetail
|
|
24
|
+
} from "./ground.js";
|
|
8
25
|
import * as lightingModule from "./lighting.js";
|
|
9
26
|
import * as lights2dModule from "./lights2d.js";
|
|
10
|
-
import { lights2dNode, prepareLights2d } from "./lights2d.js";
|
|
27
|
+
import { lights2dNode, loadBaselineLighting, prepareLights2d } from "./lights2d.js";
|
|
28
|
+
import { tilemapMethods } from "./methods.js";
|
|
11
29
|
import * as renderModule from "./render.js";
|
|
12
30
|
import {
|
|
13
31
|
describeSprites,
|
|
@@ -30,6 +48,7 @@ const spritePlugin = definePlugin({
|
|
|
30
48
|
provides: [
|
|
31
49
|
atlasModule,
|
|
32
50
|
clipModule,
|
|
51
|
+
groundModule,
|
|
33
52
|
lightingModule,
|
|
34
53
|
lights2dModule,
|
|
35
54
|
renderModule,
|
|
@@ -44,22 +63,40 @@ const spritePlugin = definePlugin({
|
|
|
44
63
|
w.initResource(TilemapDatas);
|
|
45
64
|
w.initResource(Sprite2dSettings);
|
|
46
65
|
w.initResource(Tilemaps);
|
|
66
|
+
w.initResource(GroundTiles);
|
|
47
67
|
observeSpriteRemovals(w);
|
|
48
|
-
|
|
68
|
+
observeGroundTiles(w);
|
|
69
|
+
app.addSystems(Update, animateSprites).addSystems(PostUpdate, syncGroundTiles.before(TransformSystems));
|
|
70
|
+
app.addMethod(...tilemapMethods).addSystems(Last, prepareSprites.inSet(RenderSet.Prepare)).addSystems(Last, prepareLights2d.inSet(RenderSet.Prepare));
|
|
49
71
|
},
|
|
50
72
|
ready(app) {
|
|
51
73
|
const world = app.world;
|
|
52
74
|
app.insertResource(Sprites, new SpriteStore(world.resource(Gpu)));
|
|
53
75
|
const shaders = world.resource(Shaders);
|
|
54
|
-
for (const [path, source] of Object.entries(SPRITE_SHADERS)) {
|
|
76
|
+
for (const [path, source] of Object.entries({ ...SPRITE_SHADERS, ...GROUND_TILE_SHADERS })) {
|
|
55
77
|
shaders.register(path, source, `engine:${path}`);
|
|
56
78
|
}
|
|
57
79
|
const graph = world.resource(Graph);
|
|
80
|
+
addRenderFeatures(world, {
|
|
81
|
+
name: "sprite/lights2d",
|
|
82
|
+
description: "2D lighting: light tiles, occluder shadows, soft-shadow coarse maps.",
|
|
83
|
+
nodes: ["sprites/lights2d"],
|
|
84
|
+
baseline: { strategy: "Light binning and shadow rows on the CPU, uploaded as data textures" }
|
|
85
|
+
});
|
|
86
|
+
addRenderFeatures(world, {
|
|
87
|
+
name: "sprite",
|
|
88
|
+
description: "Sprites and tilemaps, in the scene and in the overlay.",
|
|
89
|
+
nodes: ["sprites", "sprites/overlay"],
|
|
90
|
+
baseline: { strategy: "Sprite, tile and chunk records in data textures" }
|
|
91
|
+
});
|
|
58
92
|
graph.addNode("sprites/lights2d", lights2dNode(world));
|
|
59
93
|
graph.addNode("sprites", spriteNode(world));
|
|
60
94
|
graph.addNode("sprites/overlay", overlayNode(world));
|
|
61
95
|
world.initResource(RenderDescribers).set("sprites", (w) => describeSprites(w));
|
|
62
|
-
world.initResource(Picking)
|
|
96
|
+
const picking = world.initResource(Picking);
|
|
97
|
+
picking.drawers.set("sprites", drawSpritePicks);
|
|
98
|
+
picking.detailers.set("tiles", tileDetail);
|
|
99
|
+
if (world.resource(Gpu).tier === "baseline") return loadBaselineLighting();
|
|
63
100
|
}
|
|
64
101
|
});
|
|
65
102
|
export {
|
package/dist/render.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
|
|
2
2
|
import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
|
|
3
|
-
import { type CameraData, type NodeContext, type NodeDescriptor } from '@aethervtt/shard-render';
|
|
3
|
+
import { type CameraData, DataStore, type NodeContext, type NodeDescriptor } from '@aethervtt/shard-render';
|
|
4
4
|
import { type Texture } from '@aethervtt/shard-texture';
|
|
5
|
+
import { type TilemapData } from './tilemap.js';
|
|
5
6
|
/** Floats per sprite record: affine rows (12), uv rect (4), size (2), anchor (2), color, flags, pad. */
|
|
6
7
|
export declare const SPRITE_FLOATS = 24;
|
|
7
8
|
/** Sprites sharing a texture, blend, and space: one draw per contiguous run in sorted order. */
|
|
@@ -55,8 +56,8 @@ export declare class SpriteStore {
|
|
|
55
56
|
/** Bytes of sprite records uploaded this frame. */
|
|
56
57
|
uploadedBytes: number;
|
|
57
58
|
sorts: number;
|
|
58
|
-
readonly records:
|
|
59
|
-
readonly orderBuffer:
|
|
59
|
+
readonly records: DataStore;
|
|
60
|
+
readonly orderBuffer: DataStore;
|
|
60
61
|
private orderUploaded;
|
|
61
62
|
/** Hidden sprites per ECS table id, from the last visit. */
|
|
62
63
|
readonly tableSeen: number[];
|
|
@@ -93,7 +94,7 @@ export declare const prepareSprites: import("@aethervtt/shard-core").SystemDef<{
|
|
|
93
94
|
/** Frees a sprite's slot when the Sprite goes away (including on despawn). */
|
|
94
95
|
export declare function observeSpriteRemovals(world: World): void;
|
|
95
96
|
interface LayerGpu {
|
|
96
|
-
buffer:
|
|
97
|
+
buffer: DataStore;
|
|
97
98
|
chunksX: number;
|
|
98
99
|
chunksY: number;
|
|
99
100
|
cursor: number;
|
|
@@ -106,10 +107,12 @@ interface TilemapGpu {
|
|
|
106
107
|
version: number;
|
|
107
108
|
chunkSize: number;
|
|
108
109
|
layers: LayerGpu[];
|
|
109
|
-
regions:
|
|
110
|
+
regions: DataStore;
|
|
110
111
|
regionKey: string;
|
|
111
|
-
remap:
|
|
112
|
+
remap: DataStore;
|
|
112
113
|
remapData: Uint32Array;
|
|
114
|
+
/** Tile id → atlas region + 1, before animation. */
|
|
115
|
+
baseRemap: Uint32Array;
|
|
113
116
|
layer: number;
|
|
114
117
|
tileSize: [number, number];
|
|
115
118
|
affine: Float32Array;
|
|
@@ -133,6 +136,15 @@ export declare class TilemapStore {
|
|
|
133
136
|
}>;
|
|
134
137
|
}
|
|
135
138
|
export declare const Tilemaps: import("@aethervtt/shard-core").ResourceDef<TilemapStore>;
|
|
139
|
+
/**
|
|
140
|
+
* Tile id → atlas region + 1 for a map (0059): each palette name looked up in the atlas once, so a
|
|
141
|
+
* re-packed atlas draws the same map. Version 1 ids are regions + 1 already (clamped to the atlas).
|
|
142
|
+
* A name the atlas lacks draws nothing and is reported once (`sprite/unknown-tile`, see unknownTiles).
|
|
143
|
+
*/
|
|
144
|
+
export declare function resolveTiles(world: World, data: TilemapData, atlas: {
|
|
145
|
+
count: number;
|
|
146
|
+
region(name: string): number;
|
|
147
|
+
}): Uint32Array;
|
|
136
148
|
interface DrawCaches {
|
|
137
149
|
generation: number;
|
|
138
150
|
layouts?: {
|
|
@@ -152,7 +164,7 @@ interface DrawCaches {
|
|
|
152
164
|
}>;
|
|
153
165
|
views: Map<string, {
|
|
154
166
|
uniform: GpuBuffer;
|
|
155
|
-
chunks:
|
|
167
|
+
chunks: DataStore;
|
|
156
168
|
}>;
|
|
157
169
|
batchParams: Map<SpriteBatch, GpuBuffer>;
|
|
158
170
|
samplers?: {
|
package/dist/render.js
CHANGED
|
@@ -7,14 +7,17 @@ import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
|
7
7
|
import {
|
|
8
8
|
ComputedVisibility,
|
|
9
9
|
cameraOf,
|
|
10
|
+
DataStore,
|
|
11
|
+
dataEntry,
|
|
10
12
|
Gpu,
|
|
11
13
|
GpuAssetsResource,
|
|
14
|
+
GroundLayer,
|
|
12
15
|
PICK_TARGETS,
|
|
13
16
|
RenderPhase,
|
|
14
17
|
Shaders,
|
|
15
18
|
sceneColor
|
|
16
19
|
} from "@aethervtt/shard-render";
|
|
17
|
-
import { Time } from "@aethervtt/shard-runtime";
|
|
20
|
+
import { LogResource, Time } from "@aethervtt/shard-runtime";
|
|
18
21
|
import { Textures } from "@aethervtt/shard-texture";
|
|
19
22
|
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
20
23
|
import { TextureAtlases } from "./atlas.js";
|
|
@@ -26,7 +29,7 @@ import {
|
|
|
26
29
|
litView
|
|
27
30
|
} from "./lights2d.js";
|
|
28
31
|
import { Sprite, Sprite2dSettings, SpriteSlot } from "./sprite.js";
|
|
29
|
-
import { Tilemap, TilemapDatas } from "./tilemap.js";
|
|
32
|
+
import { Tilemap, TilemapDatas, unknownTiles } from "./tilemap.js";
|
|
30
33
|
const SPRITE_FLOATS = 24;
|
|
31
34
|
const SPRITE_BYTES = SPRITE_FLOATS * 4;
|
|
32
35
|
const BLEND_ALPHA = 0;
|
|
@@ -80,13 +83,8 @@ class SpriteStore {
|
|
|
80
83
|
constructor(gpu) {
|
|
81
84
|
this.gpu = gpu;
|
|
82
85
|
this.generation = gpu.generation;
|
|
83
|
-
|
|
84
|
-
this.
|
|
85
|
-
label: "sprites",
|
|
86
|
-
usage: storage,
|
|
87
|
-
size: SPRITE_BYTES * 256
|
|
88
|
-
});
|
|
89
|
-
this.orderBuffer = new GpuBuffer(gpu, { label: "sprites/order", usage: storage, size: 1024 });
|
|
86
|
+
this.records = new DataStore(gpu, { label: "sprites", size: SPRITE_BYTES * 256 });
|
|
87
|
+
this.orderBuffer = new DataStore(gpu, { label: "sprites/order", size: 1024 });
|
|
90
88
|
this.grow(256);
|
|
91
89
|
}
|
|
92
90
|
get live() {
|
|
@@ -553,9 +551,8 @@ function syncTilemaps(world) {
|
|
|
553
551
|
const chunksX = Math.ceil(l.width / cs);
|
|
554
552
|
const chunksY = Math.ceil(l.height / cs);
|
|
555
553
|
return {
|
|
556
|
-
buffer: new
|
|
554
|
+
buffer: new DataStore(gpu, {
|
|
557
555
|
label: `tilemap/${entity}/${i}`,
|
|
558
|
-
usage: GPUBufferUsage.STORAGE,
|
|
559
556
|
size: Math.max(16, chunksX * chunksY * cs * cs * 4)
|
|
560
557
|
}),
|
|
561
558
|
chunksX,
|
|
@@ -569,18 +566,11 @@ function syncTilemaps(world) {
|
|
|
569
566
|
})
|
|
570
567
|
};
|
|
571
568
|
}),
|
|
572
|
-
regions: map?.regions ?? new
|
|
573
|
-
label: `tilemap/${entity}/regions`,
|
|
574
|
-
usage: GPUBufferUsage.STORAGE,
|
|
575
|
-
size: 256
|
|
576
|
-
}),
|
|
569
|
+
regions: map?.regions ?? new DataStore(gpu, { label: `tilemap/${entity}/regions`, size: 256 }),
|
|
577
570
|
regionKey: "",
|
|
578
|
-
remap: map?.remap ?? new
|
|
579
|
-
label: `tilemap/${entity}/remap`,
|
|
580
|
-
usage: GPUBufferUsage.STORAGE,
|
|
581
|
-
size: 256
|
|
582
|
-
}),
|
|
571
|
+
remap: map?.remap ?? new DataStore(gpu, { label: `tilemap/${entity}/remap`, size: 256 }),
|
|
583
572
|
remapData: new Uint32Array(0),
|
|
573
|
+
baseRemap: new Uint32Array(0),
|
|
584
574
|
layer: 0,
|
|
585
575
|
tileSize: [1, 1],
|
|
586
576
|
affine: new Float32Array(12),
|
|
@@ -647,7 +637,7 @@ function syncTilemaps(world) {
|
|
|
647
637
|
chunkBounds(g, x0, y0, x1, y1, b, chunk * 6);
|
|
648
638
|
}
|
|
649
639
|
});
|
|
650
|
-
const key = `${atlas.version}/${texture.width}x${texture.height}/${atlas.count}`;
|
|
640
|
+
const key = `${atlas.version}/${texture.width}x${texture.height}/${atlas.count}/${data.version}/${data.palette?.length ?? -1}`;
|
|
651
641
|
if (map.regionKey !== key) {
|
|
652
642
|
const uv = new Float32Array(Math.max(4, atlas.count * 4));
|
|
653
643
|
for (let r = 0; r < atlas.count; r++) {
|
|
@@ -658,18 +648,16 @@ function syncTilemaps(world) {
|
|
|
658
648
|
}
|
|
659
649
|
map.regions.write(uv);
|
|
660
650
|
map.regionKey = key;
|
|
661
|
-
map.
|
|
662
|
-
|
|
651
|
+
map.baseRemap = resolveTiles(world, data, atlas);
|
|
652
|
+
map.remapData = map.baseRemap.slice();
|
|
663
653
|
map.remap.write(map.remapData);
|
|
664
654
|
}
|
|
665
655
|
if (data.animations.length > 0) {
|
|
666
656
|
let changed = false;
|
|
667
657
|
for (const a of data.animations) {
|
|
668
658
|
if (a.frames.length === 0 || a.tile >= map.remapData.length) continue;
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
atlas.count
|
|
672
|
-
);
|
|
659
|
+
const id = a.frames[Math.floor(time / a.frameTime) % a.frames.length];
|
|
660
|
+
const frame = map.baseRemap[Math.min(id, map.baseRemap.length - 1)];
|
|
673
661
|
if (map.remapData[a.tile] !== frame) {
|
|
674
662
|
map.remapData[a.tile] = frame;
|
|
675
663
|
changed = true;
|
|
@@ -680,6 +668,27 @@ function syncTilemaps(world) {
|
|
|
680
668
|
}
|
|
681
669
|
for (const entity of tilemaps.maps.keys()) if (!seen.has(entity)) tilemaps.maps.delete(entity);
|
|
682
670
|
}
|
|
671
|
+
const unknownChecked = /* @__PURE__ */ new WeakMap();
|
|
672
|
+
function resolveTiles(world, data, atlas) {
|
|
673
|
+
if (!data.palette) {
|
|
674
|
+
const out2 = new Uint32Array(atlas.count + 1);
|
|
675
|
+
for (let t = 0; t < out2.length; t++) out2[t] = t;
|
|
676
|
+
return out2;
|
|
677
|
+
}
|
|
678
|
+
const out = new Uint32Array(data.palette.length + 1);
|
|
679
|
+
let unknown = false;
|
|
680
|
+
for (let i = 0; i < data.palette.length; i++) {
|
|
681
|
+
const region = atlas.region(data.palette[i]);
|
|
682
|
+
out[i + 1] = region + 1;
|
|
683
|
+
if (region === -1) unknown = true;
|
|
684
|
+
}
|
|
685
|
+
if (unknown && unknownChecked.get(data) !== data.palette.length) {
|
|
686
|
+
unknownChecked.set(data, data.palette.length);
|
|
687
|
+
const log = world.tryResource(LogResource);
|
|
688
|
+
for (const error of unknownTiles(data, atlas)) log?.error(error);
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
683
692
|
function chunkBounds(g, x0, y0, x1, y1, out, o) {
|
|
684
693
|
out[o] = out[o + 1] = out[o + 2] = Number.POSITIVE_INFINITY;
|
|
685
694
|
out[o + 3] = out[o + 4] = out[o + 5] = Number.NEGATIVE_INFINITY;
|
|
@@ -706,7 +715,10 @@ function boxInFrustum(planes, b, o) {
|
|
|
706
715
|
return true;
|
|
707
716
|
}
|
|
708
717
|
function* queryTilemaps(world) {
|
|
709
|
-
for (const table of world.query({
|
|
718
|
+
for (const table of world.query({
|
|
719
|
+
with: [Tilemap, GlobalTransform, ComputedVisibility],
|
|
720
|
+
without: [GroundLayer]
|
|
721
|
+
}).tables) {
|
|
710
722
|
const g = table.column(GlobalTransform, "matrix");
|
|
711
723
|
const vis = table.column(ComputedVisibility, "visible");
|
|
712
724
|
const atlas = table.column(Tilemap, "atlas");
|
|
@@ -725,7 +737,8 @@ function* queryTilemaps(world) {
|
|
|
725
737
|
tileSize: [tileSize[i * 2], tileSize[i * 2 + 1]],
|
|
726
738
|
chunkSize: chunkSize[i],
|
|
727
739
|
layer: layer[i],
|
|
728
|
-
|
|
740
|
+
// Enum index: 0 is '2d', the only mode the sprite pass lights.
|
|
741
|
+
lit: lit[i] === 0
|
|
729
742
|
},
|
|
730
743
|
g.subarray(i * 12, i * 12 + 12)
|
|
731
744
|
];
|
|
@@ -752,6 +765,9 @@ function idOf(o) {
|
|
|
752
765
|
}
|
|
753
766
|
return id;
|
|
754
767
|
}
|
|
768
|
+
function dataKey(d) {
|
|
769
|
+
return `${idOf(d)}:${d.version}`;
|
|
770
|
+
}
|
|
755
771
|
function check(ctx) {
|
|
756
772
|
const gpu = ctx.gpu;
|
|
757
773
|
const caches = ctx.world.initResource(SpriteDrawCaches);
|
|
@@ -768,11 +784,7 @@ function check(ctx) {
|
|
|
768
784
|
if (!caches.layouts) {
|
|
769
785
|
const V = GPUShaderStage.VERTEX;
|
|
770
786
|
const F = GPUShaderStage.FRAGMENT;
|
|
771
|
-
const storage = (binding) => (
|
|
772
|
-
binding,
|
|
773
|
-
visibility: V,
|
|
774
|
-
buffer: { type: "read-only-storage" }
|
|
775
|
-
});
|
|
787
|
+
const storage = (binding) => dataEntry(gpu, binding, V);
|
|
776
788
|
caches.layouts = {
|
|
777
789
|
view: gpu.layouts.bindGroupLayout({
|
|
778
790
|
label: "sprites/view",
|
|
@@ -900,11 +912,7 @@ function viewGroup(ctx, cam) {
|
|
|
900
912
|
usage: GPUBufferUsage.UNIFORM,
|
|
901
913
|
size: 96
|
|
902
914
|
}),
|
|
903
|
-
chunks: new
|
|
904
|
-
label: `${ctx.view.name}/tilemap-chunks`,
|
|
905
|
-
usage: GPUBufferUsage.STORAGE,
|
|
906
|
-
size: 1024
|
|
907
|
-
})
|
|
915
|
+
chunks: new DataStore(gpu, { label: `${ctx.view.name}/tilemap-chunks`, size: 1024 })
|
|
908
916
|
};
|
|
909
917
|
c.views.set(ctx.view.name, v);
|
|
910
918
|
}
|
|
@@ -1047,13 +1055,13 @@ function drawSpace(ctx, space) {
|
|
|
1047
1055
|
group(
|
|
1048
1056
|
ctx,
|
|
1049
1057
|
`${ctx.view.name}/tilemap/${d.map.entity}/${d.layer}`,
|
|
1050
|
-
`${
|
|
1058
|
+
`${dataKey(layer.buffer)}/${dataKey(v.chunks)}/${dataKey(d.map.regions)}/${dataKey(d.map.remap)}/${idOf(layer.params.buffer)}`,
|
|
1051
1059
|
c.layouts.tilemap,
|
|
1052
1060
|
() => [
|
|
1053
|
-
{ binding: 0, resource:
|
|
1054
|
-
{ binding: 1, resource:
|
|
1055
|
-
{ binding: 2, resource:
|
|
1056
|
-
{ binding: 3, resource:
|
|
1061
|
+
{ binding: 0, resource: layer.buffer.resource() },
|
|
1062
|
+
{ binding: 1, resource: v.chunks.resource() },
|
|
1063
|
+
{ binding: 2, resource: d.map.regions.resource() },
|
|
1064
|
+
{ binding: 3, resource: d.map.remap.resource() },
|
|
1057
1065
|
{ binding: 4, resource: { buffer: layer.params.buffer } }
|
|
1058
1066
|
]
|
|
1059
1067
|
)
|
|
@@ -1098,11 +1106,11 @@ function drawSpace(ctx, space) {
|
|
|
1098
1106
|
group(
|
|
1099
1107
|
ctx,
|
|
1100
1108
|
"sprites/records",
|
|
1101
|
-
`${
|
|
1109
|
+
`${dataKey(store.records)}/${dataKey(store.orderBuffer)}`,
|
|
1102
1110
|
c.layouts.sprites,
|
|
1103
1111
|
() => [
|
|
1104
|
-
{ binding: 0, resource:
|
|
1105
|
-
{ binding: 1, resource:
|
|
1112
|
+
{ binding: 0, resource: store.records.resource() },
|
|
1113
|
+
{ binding: 1, resource: store.orderBuffer.resource() }
|
|
1106
1114
|
]
|
|
1107
1115
|
)
|
|
1108
1116
|
);
|
|
@@ -1138,11 +1146,11 @@ function drawSpritePicks(ctx, cam) {
|
|
|
1138
1146
|
group(
|
|
1139
1147
|
ctx,
|
|
1140
1148
|
"sprites/records",
|
|
1141
|
-
`${
|
|
1149
|
+
`${dataKey(store.records)}/${dataKey(store.orderBuffer)}`,
|
|
1142
1150
|
c.layouts.sprites,
|
|
1143
1151
|
() => [
|
|
1144
|
-
{ binding: 0, resource:
|
|
1145
|
-
{ binding: 1, resource:
|
|
1152
|
+
{ binding: 0, resource: store.records.resource() },
|
|
1153
|
+
{ binding: 1, resource: store.orderBuffer.resource() }
|
|
1146
1154
|
]
|
|
1147
1155
|
)
|
|
1148
1156
|
);
|
|
@@ -1212,5 +1220,6 @@ export {
|
|
|
1212
1220
|
observeSpriteRemovals,
|
|
1213
1221
|
overlayNode,
|
|
1214
1222
|
prepareSprites,
|
|
1223
|
+
resolveTiles,
|
|
1215
1224
|
spriteNode
|
|
1216
1225
|
};
|