@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
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type LightView2d, type ViewGpu } from '../lights2d.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bins, shadows and reduces a view's lights into its fragment-stage data textures. Returns the
|
|
4
|
+
* bytes written. `segmentsChanged`: the occluder segments differ from last frame.
|
|
5
|
+
*/
|
|
6
|
+
export declare function lightOnCpu(view: LightView2d, g: ViewGpu, segmentsChanged: boolean): number;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
binLightsInto,
|
|
3
|
+
COARSE_RES,
|
|
4
|
+
LIGHT2D_FLOATS,
|
|
5
|
+
SHADOW_RES,
|
|
6
|
+
TILE_STRIDE,
|
|
7
|
+
writeShadowRow
|
|
8
|
+
} from "../lights2d.js";
|
|
9
|
+
const SPAN = SHADOW_RES / COARSE_RES;
|
|
10
|
+
const views = /* @__PURE__ */ new WeakMap();
|
|
11
|
+
function stateOf(view) {
|
|
12
|
+
let s = views.get(view);
|
|
13
|
+
if (!s) {
|
|
14
|
+
s = {
|
|
15
|
+
tiles: new Uint32Array(0),
|
|
16
|
+
scratch: new Uint32Array(0),
|
|
17
|
+
rows: new Float32Array(0),
|
|
18
|
+
coarse: new Float32Array(0),
|
|
19
|
+
keys: new Float64Array(0)
|
|
20
|
+
};
|
|
21
|
+
views.set(view, s);
|
|
22
|
+
}
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
function lightOnCpu(view, g, segmentsChanged) {
|
|
26
|
+
const s = stateOf(view);
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
const words = view.tilesX * view.tilesY * TILE_STRIDE;
|
|
29
|
+
if (s.scratch.length < words) {
|
|
30
|
+
s.scratch = new Uint32Array(words);
|
|
31
|
+
const tiles = new Uint32Array(words);
|
|
32
|
+
tiles.fill(4294967295);
|
|
33
|
+
s.tiles = tiles;
|
|
34
|
+
}
|
|
35
|
+
binLightsInto(view.circles, view.count, view.tilesX, view.tilesY, s.scratch);
|
|
36
|
+
let first = -1;
|
|
37
|
+
let last = -1;
|
|
38
|
+
for (let i = 0; i < words; i++) {
|
|
39
|
+
if (s.scratch[i] !== s.tiles[i]) {
|
|
40
|
+
if (first < 0) first = i;
|
|
41
|
+
last = i;
|
|
42
|
+
s.tiles[i] = s.scratch[i];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (first >= 0) {
|
|
46
|
+
g.tiles.write(s.tiles, first * 4, first, last - first + 1);
|
|
47
|
+
bytes += (last - first + 1) * 4;
|
|
48
|
+
}
|
|
49
|
+
const rows = view.shadowedCount;
|
|
50
|
+
if (s.keys.length < rows * 4) {
|
|
51
|
+
const keys = new Float64Array(Math.max(rows, 1) * 4 * 2).fill(Number.NaN);
|
|
52
|
+
keys.set(s.keys);
|
|
53
|
+
s.keys = keys;
|
|
54
|
+
const grown = new Float32Array(keys.length / 4 * SHADOW_RES);
|
|
55
|
+
grown.set(s.rows);
|
|
56
|
+
s.rows = grown;
|
|
57
|
+
const coarse = new Float32Array(keys.length / 4 * COARSE_RES * 2);
|
|
58
|
+
coarse.set(s.coarse);
|
|
59
|
+
s.coarse = coarse;
|
|
60
|
+
}
|
|
61
|
+
let lo = -1;
|
|
62
|
+
let hi = -1;
|
|
63
|
+
for (let r = 0; r < rows; r++) {
|
|
64
|
+
const o = view.shadowed[r] * LIGHT2D_FLOATS;
|
|
65
|
+
const x = view.lights[o];
|
|
66
|
+
const y = view.lights[o + 1];
|
|
67
|
+
const radius = view.lights[o + 3];
|
|
68
|
+
const layers = view.lightBits[o + 14];
|
|
69
|
+
const k = r * 4;
|
|
70
|
+
if (!segmentsChanged && s.keys[k] === x && s.keys[k + 1] === y && s.keys[k + 2] === radius && s.keys[k + 3] === layers) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
s.keys[k] = x;
|
|
74
|
+
s.keys[k + 1] = y;
|
|
75
|
+
s.keys[k + 2] = radius;
|
|
76
|
+
s.keys[k + 3] = layers;
|
|
77
|
+
const at = r * SHADOW_RES;
|
|
78
|
+
writeShadowRow(
|
|
79
|
+
x,
|
|
80
|
+
y,
|
|
81
|
+
radius,
|
|
82
|
+
layers,
|
|
83
|
+
view.segments,
|
|
84
|
+
view.segmentBits,
|
|
85
|
+
view.segmentCount,
|
|
86
|
+
s.rows,
|
|
87
|
+
at
|
|
88
|
+
);
|
|
89
|
+
for (let c = 0; c < COARSE_RES; c++) {
|
|
90
|
+
let min = Number.POSITIVE_INFINITY;
|
|
91
|
+
let max = 0;
|
|
92
|
+
for (let i = 0; i < SPAN; i++) {
|
|
93
|
+
const v = s.rows[at + c * SPAN + i];
|
|
94
|
+
if (v < min) min = v;
|
|
95
|
+
if (v > max) max = v;
|
|
96
|
+
}
|
|
97
|
+
s.coarse[(r * COARSE_RES + c) * 2] = min;
|
|
98
|
+
s.coarse[(r * COARSE_RES + c) * 2 + 1] = max;
|
|
99
|
+
}
|
|
100
|
+
if (lo < 0) lo = r;
|
|
101
|
+
hi = r;
|
|
102
|
+
}
|
|
103
|
+
for (let r = rows; r < s.keys.length / 4; r++) s.keys[r * 4] = Number.NaN;
|
|
104
|
+
if (lo >= 0) {
|
|
105
|
+
const n = hi - lo + 1;
|
|
106
|
+
g.shadowMap.write(s.rows, lo * SHADOW_RES * 4, lo * SHADOW_RES, n * SHADOW_RES);
|
|
107
|
+
g.shadowCoarse.write(s.coarse, lo * COARSE_RES * 8, lo * COARSE_RES * 2, n * COARSE_RES * 2);
|
|
108
|
+
bytes += n * (SHADOW_RES + COARSE_RES * 2) * 4;
|
|
109
|
+
}
|
|
110
|
+
return bytes;
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
lightOnCpu
|
|
114
|
+
};
|
package/dist/ground.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import { Mesh } from '@aethervtt/shard-mesh';
|
|
3
|
+
import { type PickDetail, type PickHit } from '@aethervtt/shard-render';
|
|
4
|
+
import { type TextureAtlas } from './atlas.js';
|
|
5
|
+
import { type TilemapData } from './tilemap.js';
|
|
6
|
+
/** Marks a chunk mesh a ground tilemap spawned: which layer and chunk of which tilemap. */
|
|
7
|
+
export declare const TileChunk: import("@aethervtt/shard-core").ComponentDef<{
|
|
8
|
+
readonly tilemap: import("@aethervtt/shard-core").FieldType<number | null, "f64">;
|
|
9
|
+
readonly layer: import("@aethervtt/shard-core").FieldType<number, "u16">;
|
|
10
|
+
readonly chunk: import("@aethervtt/shard-core").FieldType<number, "u32">;
|
|
11
|
+
}>;
|
|
12
|
+
/** The rotation that lays a tilemap's XY plane on the ground, rows going +Z, facing up. */
|
|
13
|
+
export declare function tilemapOnGround(elevation?: number): {
|
|
14
|
+
translation: [number, number, number];
|
|
15
|
+
rotation: [number, number, number, number];
|
|
16
|
+
};
|
|
17
|
+
/** Tiles lit like a floor: flat albedo from the atlas, an up normal, roughness 1. */
|
|
18
|
+
export declare const TileLit: import("@aethervtt/shard-render").MaterialType;
|
|
19
|
+
/** Tiles drawn without lighting: the atlas as it is, display-referred like any overlay. */
|
|
20
|
+
export declare const TileUnlit: import("@aethervtt/shard-render").MaterialType;
|
|
21
|
+
export declare const GROUND_TILE_SHADERS: Record<string, string>;
|
|
22
|
+
interface ChunkState {
|
|
23
|
+
entity: Entity;
|
|
24
|
+
mesh: Mesh;
|
|
25
|
+
meshRef: AssetRef<'Mesh'>;
|
|
26
|
+
}
|
|
27
|
+
interface LayerState {
|
|
28
|
+
/** Edit log position and base (see TileLayer.edits): what's been rebuilt. */
|
|
29
|
+
cursor: number;
|
|
30
|
+
base: number;
|
|
31
|
+
chunks: Map<number, ChunkState>;
|
|
32
|
+
/** Chunks holding an animated tile: rebuilt when a frame turns. */
|
|
33
|
+
animated: number[];
|
|
34
|
+
}
|
|
35
|
+
interface GroundMap {
|
|
36
|
+
data: TilemapData;
|
|
37
|
+
dataVersion: number;
|
|
38
|
+
atlas: TextureAtlas;
|
|
39
|
+
atlasVersion: number;
|
|
40
|
+
chunkSize: number;
|
|
41
|
+
tileSize: [number, number];
|
|
42
|
+
material: AssetRef<'Material'>;
|
|
43
|
+
lit: '2d' | '3d' | 'none';
|
|
44
|
+
layers: LayerState[];
|
|
45
|
+
/** Tile id → region + 1 with animations applied, and what it was built from. */
|
|
46
|
+
remap: Uint32Array;
|
|
47
|
+
base: Uint32Array;
|
|
48
|
+
/** The palette's length when `base` was resolved: an edit naming a new tile grows it. */
|
|
49
|
+
paletteLength: number;
|
|
50
|
+
/** Tile ids that animate: chunks holding one rebuild when its frame turns. */
|
|
51
|
+
animatedIds: Set<number>;
|
|
52
|
+
band: number;
|
|
53
|
+
order: number;
|
|
54
|
+
level: number;
|
|
55
|
+
mask: number;
|
|
56
|
+
seen: number;
|
|
57
|
+
}
|
|
58
|
+
export interface GroundTilesState {
|
|
59
|
+
maps: Map<Entity, GroundMap>;
|
|
60
|
+
/** Chunk meshes rebuilt and uploaded last frame. */
|
|
61
|
+
chunkUploads: number;
|
|
62
|
+
materials: Map<string, AssetRef<'Material'>>;
|
|
63
|
+
frame: number;
|
|
64
|
+
}
|
|
65
|
+
export declare const GroundTiles: import("@aethervtt/shard-core").ResourceDef<GroundTilesState>;
|
|
66
|
+
/**
|
|
67
|
+
* Keeps each ground tilemap's chunk meshes current: all of them when the map, its atlas or its
|
|
68
|
+
* settings change; the chunks its edits touched otherwise; animated chunks as their frames turn.
|
|
69
|
+
*/
|
|
70
|
+
export declare const syncGroundTiles: import("@aethervtt/shard-core").SystemDef<{
|
|
71
|
+
q: import("@aethervtt/shard-core").Query;
|
|
72
|
+
}>;
|
|
73
|
+
/** Frees a ground tilemap's chunks when it loses its Tilemap or GroundLayer. */
|
|
74
|
+
export declare function observeGroundTiles(world: World): void;
|
|
75
|
+
/**
|
|
76
|
+
* The tile under a pick (0059): the cell of the tilemap the hit landed on, and the tile there
|
|
77
|
+
* named as the palette names it. A ground chunk knows its layer; a tilemap in the sprite pass
|
|
78
|
+
* reports its topmost filled layer at the cell. Undefined for hits on anything else.
|
|
79
|
+
*/
|
|
80
|
+
export declare function tileDetail(world: World, hit: PickHit): PickDetail | undefined;
|
|
81
|
+
export {};
|
package/dist/ground.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import {
|
|
2
|
+
affine,
|
|
3
|
+
ChildOf,
|
|
4
|
+
Derived,
|
|
5
|
+
defineComponent,
|
|
6
|
+
defineResource,
|
|
7
|
+
defineSystem,
|
|
8
|
+
onRemove,
|
|
9
|
+
t
|
|
10
|
+
} from "@aethervtt/shard-core";
|
|
11
|
+
import { Mesh } from "@aethervtt/shard-mesh";
|
|
12
|
+
import {
|
|
13
|
+
defineMaterial,
|
|
14
|
+
GroundLayer,
|
|
15
|
+
MaterialAsset,
|
|
16
|
+
Materials,
|
|
17
|
+
Mesh3d,
|
|
18
|
+
Meshes,
|
|
19
|
+
MeshMaterial,
|
|
20
|
+
NotShadowCaster,
|
|
21
|
+
RenderLayers
|
|
22
|
+
} from "@aethervtt/shard-render";
|
|
23
|
+
import { Time } from "@aethervtt/shard-runtime";
|
|
24
|
+
import { Textures } from "@aethervtt/shard-texture";
|
|
25
|
+
import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
|
|
26
|
+
import { TextureAtlases } from "./atlas.js";
|
|
27
|
+
import { resolveTiles } from "./render.js";
|
|
28
|
+
import { Tilemap, TilemapDatas } from "./tilemap.js";
|
|
29
|
+
const TileChunk = defineComponent(
|
|
30
|
+
"sprite/TileChunk",
|
|
31
|
+
{
|
|
32
|
+
tilemap: t.entity({ readonly: true, description: "The Tilemap this chunk belongs to." }),
|
|
33
|
+
layer: t.u16({ readonly: true, description: "Layer index in the tilemap data." }),
|
|
34
|
+
chunk: t.u32({ readonly: true, description: "Chunk index: cy \xB7 chunks across + cx." })
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
description: "A ground tilemap's chunk mesh: spawned and kept by the sprite plugin.",
|
|
38
|
+
serialize: false,
|
|
39
|
+
save: false
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
function tilemapOnGround(elevation = 0) {
|
|
43
|
+
return { translation: [0, elevation, 0], rotation: [-Math.SQRT1_2, 0, 0, Math.SQRT1_2] };
|
|
44
|
+
}
|
|
45
|
+
const TileLit = defineMaterial("sprite/TileLit", {
|
|
46
|
+
extends: "standard",
|
|
47
|
+
blend: "mask",
|
|
48
|
+
standardTextures: false,
|
|
49
|
+
fields: { atlas: t.handle("Texture", { description: "The tile atlas." }) },
|
|
50
|
+
colors: ["atlas"],
|
|
51
|
+
shader: "sprite::tile_lit",
|
|
52
|
+
description: "Lit ground tiles (a Tilemap on a GroundLayer, lit: 3d or 2d)."
|
|
53
|
+
});
|
|
54
|
+
const TileUnlit = defineMaterial("sprite/TileUnlit", {
|
|
55
|
+
extends: "none",
|
|
56
|
+
blend: "mask",
|
|
57
|
+
fields: { atlas: t.handle("Texture", { description: "The tile atlas." }) },
|
|
58
|
+
colors: ["atlas"],
|
|
59
|
+
shader: "sprite::tile_unlit",
|
|
60
|
+
description: "Unlit ground tiles (a Tilemap on a GroundLayer, lit: none)."
|
|
61
|
+
});
|
|
62
|
+
const GROUND_TILE_SHADERS = {
|
|
63
|
+
"sprite::tile_sample": `
|
|
64
|
+
/** Trilinear at the larger axis's footprint, never anisotropic: a region's edge stays its edge. */
|
|
65
|
+
fn tile_sample(atlas: texture_2d<f32>, s: sampler, uv: vec2f) -> vec4f {
|
|
66
|
+
let size = vec2f(textureDimensions(atlas));
|
|
67
|
+
let dx = dpdx(uv * size);
|
|
68
|
+
let dy = dpdy(uv * size);
|
|
69
|
+
let lod = 0.5 * log2(max(max(dot(dx, dx), dot(dy, dy)), 1e-8));
|
|
70
|
+
return textureSampleLevel(atlas, s, uv, lod);
|
|
71
|
+
}`,
|
|
72
|
+
"sprite::tile_lit": `
|
|
73
|
+
import shard::pbr::types::{ VertexOutput, PbrInput };
|
|
74
|
+
import sprite::tile_sample::tile_sample;
|
|
75
|
+
import material::tile_lit::{ TileLit_atlas, TileLit_atlas_sampler };
|
|
76
|
+
|
|
77
|
+
override fn pbr_input(in: VertexOutput) -> PbrInput {
|
|
78
|
+
let c = tile_sample(TileLit_atlas, TileLit_atlas_sampler, in.uv);
|
|
79
|
+
var p: PbrInput;
|
|
80
|
+
p.base_color = c.rgb;
|
|
81
|
+
p.alpha = c.a;
|
|
82
|
+
p.normal = normalize(in.world_normal);
|
|
83
|
+
p.metallic = 0.0;
|
|
84
|
+
p.roughness = 1.0;
|
|
85
|
+
p.emissive = vec3f(0.0);
|
|
86
|
+
p.occlusion = 1.0;
|
|
87
|
+
return p;
|
|
88
|
+
}`,
|
|
89
|
+
"sprite::tile_unlit": `
|
|
90
|
+
import shard::pbr::types::VertexOutput;
|
|
91
|
+
import shard::view::view;
|
|
92
|
+
import sprite::tile_sample::tile_sample;
|
|
93
|
+
import material::tile_unlit::{ TileUnlit_atlas, TileUnlit_atlas_sampler };
|
|
94
|
+
|
|
95
|
+
override fn shade(in: VertexOutput) -> vec4f {
|
|
96
|
+
let c = tile_sample(TileUnlit_atlas, TileUnlit_atlas_sampler, in.uv);
|
|
97
|
+
return vec4f(c.rgb / view.exposure, c.a);
|
|
98
|
+
}`
|
|
99
|
+
};
|
|
100
|
+
const GroundTiles = defineResource("sprite/GroundTiles", {
|
|
101
|
+
description: "Ground tilemaps (0059): their chunk meshes, and how many were rebuilt last frame.",
|
|
102
|
+
init: () => ({ maps: /* @__PURE__ */ new Map(), chunkUploads: 0, materials: /* @__PURE__ */ new Map(), frame: 0 })
|
|
103
|
+
});
|
|
104
|
+
const LITS = ["2d", "3d", "none"];
|
|
105
|
+
function materialFor(world, state, texture, lit) {
|
|
106
|
+
const key = `${texture.guid}/${lit === "none" ? "none" : "3d"}`;
|
|
107
|
+
let ref = state.materials.get(key);
|
|
108
|
+
if (!ref) {
|
|
109
|
+
const materials = world.resource(Materials);
|
|
110
|
+
const material = lit === "none" ? new MaterialAsset({ atlas: texture }, TileUnlit) : new MaterialAsset({ atlas: texture, alphaMode: "mask", alphaCutoff: 0.5 }, TileLit);
|
|
111
|
+
ref = materials.add(material, `tiles:${key}`);
|
|
112
|
+
state.materials.set(key, ref);
|
|
113
|
+
}
|
|
114
|
+
return ref;
|
|
115
|
+
}
|
|
116
|
+
function buildChunk(map, layerIndex, chunk, texW, texH) {
|
|
117
|
+
const layer = map.data.layers[layerIndex];
|
|
118
|
+
const cs = map.chunkSize;
|
|
119
|
+
const chunksX = Math.ceil(layer.width / cs);
|
|
120
|
+
const cx = chunk % chunksX;
|
|
121
|
+
const cy = Math.floor(chunk / chunksX);
|
|
122
|
+
const [tw, th] = map.tileSize;
|
|
123
|
+
const positions = [];
|
|
124
|
+
const normals = [];
|
|
125
|
+
const uvs = [];
|
|
126
|
+
const indices = [];
|
|
127
|
+
let animated = false;
|
|
128
|
+
const rects = map.atlas.rects;
|
|
129
|
+
for (let y = cy * cs; y < Math.min((cy + 1) * cs, layer.height); y++) {
|
|
130
|
+
for (let x = cx * cs; x < Math.min((cx + 1) * cs, layer.width); x++) {
|
|
131
|
+
const i = y * layer.width + x;
|
|
132
|
+
const id = layer.tiles[i];
|
|
133
|
+
if (id === 0) continue;
|
|
134
|
+
const region = (map.remap[Math.min(id, map.remap.length - 1)] ?? 0) - 1;
|
|
135
|
+
if (region < 0) continue;
|
|
136
|
+
if (map.animatedIds.has(id)) animated = true;
|
|
137
|
+
const u0 = (rects[region * 4] + 0.5) / texW;
|
|
138
|
+
const v0 = (rects[region * 4 + 1] + 0.5) / texH;
|
|
139
|
+
const u1 = (rects[region * 4] + rects[region * 4 + 2] - 0.5) / texW;
|
|
140
|
+
const v1 = (rects[region * 4 + 1] + rects[region * 4 + 3] - 0.5) / texH;
|
|
141
|
+
const flags = layer.flags[i];
|
|
142
|
+
const base = positions.length / 3;
|
|
143
|
+
for (const [ax, ay] of CORNERS) {
|
|
144
|
+
positions.push((x + ax) * tw, -(y + ay) * th, 0);
|
|
145
|
+
normals.push(0, 0, 1);
|
|
146
|
+
let sx = ax;
|
|
147
|
+
let sy = ay;
|
|
148
|
+
if (flags & 4) {
|
|
149
|
+
const r = sx;
|
|
150
|
+
sx = sy;
|
|
151
|
+
sy = 1 - r;
|
|
152
|
+
}
|
|
153
|
+
if (flags & 1) sx = 1 - sx;
|
|
154
|
+
if (flags & 2) sy = 1 - sy;
|
|
155
|
+
uvs.push(u0 + (u1 - u0) * sx, v0 + (v1 - v0) * sy);
|
|
156
|
+
}
|
|
157
|
+
indices.push(base, base + 2, base + 1, base, base + 3, base + 2);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (indices.length === 0) return { mesh: void 0, animated };
|
|
161
|
+
const vertexCount = positions.length / 3;
|
|
162
|
+
return {
|
|
163
|
+
mesh: Mesh.create({
|
|
164
|
+
positions: new Float32Array(positions),
|
|
165
|
+
normals: new Float32Array(normals),
|
|
166
|
+
uvs: new Float32Array(uvs),
|
|
167
|
+
indices: vertexCount > 65535 ? new Uint32Array(indices) : new Uint16Array(indices)
|
|
168
|
+
}),
|
|
169
|
+
animated
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const CORNERS = [
|
|
173
|
+
[0, 0],
|
|
174
|
+
[1, 0],
|
|
175
|
+
[1, 1],
|
|
176
|
+
[0, 1]
|
|
177
|
+
];
|
|
178
|
+
function despawnMap(world, map) {
|
|
179
|
+
for (const layer of map.layers) {
|
|
180
|
+
for (const c of layer.chunks.values()) {
|
|
181
|
+
if (world.isAlive(c.entity)) world.despawn(c.entity);
|
|
182
|
+
world.resource(Meshes).delete(c.meshRef.guid);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const syncGroundTiles = defineSystem({
|
|
187
|
+
name: "sprite/ground-tiles",
|
|
188
|
+
description: "Builds and rebuilds the chunk meshes of tilemaps on the ground (0059).",
|
|
189
|
+
setup: (world) => ({ q: world.query({ with: [Tilemap, GroundLayer] }) }),
|
|
190
|
+
// Per frame with nothing edited and no frame turning, this reads columns and allocates nothing.
|
|
191
|
+
run: ({ q }, world) => {
|
|
192
|
+
const state = world.resource(GroundTiles);
|
|
193
|
+
state.frame++;
|
|
194
|
+
state.chunkUploads = 0;
|
|
195
|
+
const datas = world.tryResource(TilemapDatas);
|
|
196
|
+
const atlases = world.tryResource(TextureAtlases);
|
|
197
|
+
const textures = world.tryResource(Textures);
|
|
198
|
+
const time = world.tryResource(Time)?.elapsed ?? 0;
|
|
199
|
+
let seen = 0;
|
|
200
|
+
for (const table of q.tables) {
|
|
201
|
+
const dataRefs = table.column(Tilemap, "data");
|
|
202
|
+
const atlasRefs = table.column(Tilemap, "atlas");
|
|
203
|
+
const tileSizes = table.column(Tilemap, "tileSize");
|
|
204
|
+
const chunkSizes = table.column(Tilemap, "chunkSize");
|
|
205
|
+
const lits = table.column(Tilemap, "lit");
|
|
206
|
+
const bands = table.column(GroundLayer, "band");
|
|
207
|
+
const orders = table.column(GroundLayer, "order");
|
|
208
|
+
const levels = table.column(GroundLayer, "level");
|
|
209
|
+
const masks = table.has(RenderLayers) ? table.column(RenderLayers, "mask") : void 0;
|
|
210
|
+
for (let row = 0; row < table.count; row++) {
|
|
211
|
+
const entity = table.entities[row];
|
|
212
|
+
const data = datas?.get(dataRefs[row]);
|
|
213
|
+
const atlas = atlases?.get(atlasRefs[row]);
|
|
214
|
+
const texture = atlas?.texture ? textures?.get(atlas.texture) : void 0;
|
|
215
|
+
if (!data || !atlas || !texture || !atlas.texture) continue;
|
|
216
|
+
const lit = LITS[lits[row]] ?? "2d";
|
|
217
|
+
const mask = masks ? masks[row] : 1;
|
|
218
|
+
const tw = tileSizes[row * 2];
|
|
219
|
+
const th = tileSizes[row * 2 + 1];
|
|
220
|
+
const chunkSize = chunkSizes[row];
|
|
221
|
+
const band = bands[row];
|
|
222
|
+
const order = orders[row];
|
|
223
|
+
const level = levels[row];
|
|
224
|
+
let map = state.maps.get(entity);
|
|
225
|
+
const stale = !map || map.data !== data || map.dataVersion !== data.version || map.atlas !== atlas || map.atlasVersion !== atlas.version || map.chunkSize !== chunkSize || map.tileSize[0] !== tw || map.tileSize[1] !== th || map.lit !== lit || map.band !== band || map.order !== order || map.level !== level || map.mask !== mask;
|
|
226
|
+
if (stale) {
|
|
227
|
+
if (map) despawnMap(world, map);
|
|
228
|
+
const base = resolveTiles(world, data, atlas);
|
|
229
|
+
map = {
|
|
230
|
+
data,
|
|
231
|
+
dataVersion: data.version,
|
|
232
|
+
atlas,
|
|
233
|
+
atlasVersion: atlas.version,
|
|
234
|
+
chunkSize,
|
|
235
|
+
tileSize: [tw, th],
|
|
236
|
+
material: materialFor(world, state, atlas.texture, lit),
|
|
237
|
+
lit,
|
|
238
|
+
layers: data.layers.map((l) => ({
|
|
239
|
+
cursor: l.edits.length,
|
|
240
|
+
base: -1,
|
|
241
|
+
chunks: /* @__PURE__ */ new Map(),
|
|
242
|
+
animated: []
|
|
243
|
+
})),
|
|
244
|
+
remap: base.slice(),
|
|
245
|
+
base,
|
|
246
|
+
paletteLength: data.palette?.length ?? -1,
|
|
247
|
+
animatedIds: new Set(data.animations.map((a) => a.tile)),
|
|
248
|
+
band,
|
|
249
|
+
order,
|
|
250
|
+
level,
|
|
251
|
+
mask,
|
|
252
|
+
seen: 0
|
|
253
|
+
};
|
|
254
|
+
state.maps.set(entity, map);
|
|
255
|
+
}
|
|
256
|
+
const m = map;
|
|
257
|
+
m.seen = state.frame;
|
|
258
|
+
seen++;
|
|
259
|
+
if ((data.palette?.length ?? -1) !== m.paletteLength) {
|
|
260
|
+
m.base = resolveTiles(world, data, atlas);
|
|
261
|
+
m.remap = m.base.slice();
|
|
262
|
+
m.paletteLength = data.palette?.length ?? -1;
|
|
263
|
+
}
|
|
264
|
+
let frameChanged = false;
|
|
265
|
+
for (let k = 0; k < data.animations.length; k++) {
|
|
266
|
+
const a = data.animations[k];
|
|
267
|
+
if (a.frames.length === 0 || a.tile >= m.remap.length) continue;
|
|
268
|
+
const id = a.frames[Math.floor(time / a.frameTime) % a.frames.length];
|
|
269
|
+
const region = m.base[Math.min(id, m.base.length - 1)];
|
|
270
|
+
if (m.remap[a.tile] !== region) {
|
|
271
|
+
m.remap[a.tile] = region;
|
|
272
|
+
frameChanged = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (let li = 0; li < data.layers.length; li++) {
|
|
276
|
+
const l = data.layers[li];
|
|
277
|
+
const ls = m.layers[li];
|
|
278
|
+
if (ls.base === l.editBase && ls.cursor === l.edits.length && !frameChanged) continue;
|
|
279
|
+
const cs = m.chunkSize;
|
|
280
|
+
const chunksX = Math.ceil(l.width / cs);
|
|
281
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
282
|
+
if (ls.base !== l.editBase) {
|
|
283
|
+
for (let c = 0; c < chunksX * Math.ceil(l.height / cs); c++) dirty.add(c);
|
|
284
|
+
ls.base = l.editBase;
|
|
285
|
+
} else {
|
|
286
|
+
for (let k = ls.cursor; k < l.edits.length; k++) {
|
|
287
|
+
const i = l.edits[k];
|
|
288
|
+
dirty.add(
|
|
289
|
+
Math.floor(Math.floor(i / l.width) / cs) * chunksX + Math.floor(i % l.width / cs)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
ls.cursor = l.edits.length;
|
|
294
|
+
if (frameChanged) for (let k = 0; k < ls.animated.length; k++) dirty.add(ls.animated[k]);
|
|
295
|
+
for (const c of dirty)
|
|
296
|
+
rebuild(world, state, entity, m, li, c, texture.width, texture.height);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (seen === state.maps.size) return;
|
|
301
|
+
for (const [entity, map] of state.maps) {
|
|
302
|
+
if (map.seen === state.frame) continue;
|
|
303
|
+
despawnMap(world, map);
|
|
304
|
+
state.maps.delete(entity);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
function rebuild(world, state, tilemap, map, li, c, texW, texH) {
|
|
309
|
+
const ls = map.layers[li];
|
|
310
|
+
const existing = ls.chunks.get(c);
|
|
311
|
+
const built = buildChunk(map, li, c, texW, texH);
|
|
312
|
+
state.chunkUploads++;
|
|
313
|
+
const at = ls.animated.indexOf(c);
|
|
314
|
+
if (built.mesh && built.animated) {
|
|
315
|
+
if (at === -1) ls.animated.push(c);
|
|
316
|
+
} else if (at !== -1) ls.animated.splice(at, 1);
|
|
317
|
+
if (!built.mesh) {
|
|
318
|
+
if (existing) {
|
|
319
|
+
if (world.isAlive(existing.entity)) world.despawn(existing.entity);
|
|
320
|
+
world.resource(Meshes).delete(existing.meshRef.guid);
|
|
321
|
+
ls.chunks.delete(c);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (existing && world.isAlive(existing.entity)) {
|
|
326
|
+
existing.mesh.update(built.mesh.data());
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const meshRef = world.resource(Meshes).add(built.mesh, `tiles:${tilemap}/${li}/${c}`);
|
|
330
|
+
const entity = world.spawn(
|
|
331
|
+
[Mesh3d, { mesh: meshRef }],
|
|
332
|
+
[MeshMaterial, { material: map.material }],
|
|
333
|
+
// Layers stack in order within the tilemap's band.
|
|
334
|
+
[GroundLayer, { band: map.band, order: map.order * 64 + li, level: map.level }],
|
|
335
|
+
[RenderLayers, { mask: map.mask }],
|
|
336
|
+
[TileChunk, { tilemap, layer: li, chunk: c }],
|
|
337
|
+
NotShadowCaster,
|
|
338
|
+
Derived,
|
|
339
|
+
Transform,
|
|
340
|
+
[ChildOf, { parent: tilemap }]
|
|
341
|
+
);
|
|
342
|
+
ls.chunks.set(c, { entity, mesh: built.mesh, meshRef });
|
|
343
|
+
}
|
|
344
|
+
function observeGroundTiles(world) {
|
|
345
|
+
const drop = ({ entity }) => {
|
|
346
|
+
const state = world.tryResource(GroundTiles);
|
|
347
|
+
const map = state?.maps.get(entity);
|
|
348
|
+
if (!map || !state) return;
|
|
349
|
+
despawnMap(world, map);
|
|
350
|
+
state.maps.delete(entity);
|
|
351
|
+
};
|
|
352
|
+
world.observe(onRemove(Tilemap), drop);
|
|
353
|
+
world.observe(onRemove(GroundLayer), drop);
|
|
354
|
+
}
|
|
355
|
+
const inverse = affine.create();
|
|
356
|
+
const local = new Float32Array(3);
|
|
357
|
+
function tileDetail(world, hit) {
|
|
358
|
+
const chunk = world.tryGet(hit.entity, TileChunk);
|
|
359
|
+
const tilemap = chunk ? chunk.tilemap : hit.entity;
|
|
360
|
+
const value = world.tryGet(tilemap, Tilemap);
|
|
361
|
+
const g = world.tryGet(tilemap, GlobalTransform)?.matrix;
|
|
362
|
+
if (!value || !g) return void 0;
|
|
363
|
+
const data = world.tryResource(TilemapDatas)?.get(value.data);
|
|
364
|
+
if (!data || data.layers.length === 0 || !affine.invert(inverse, g)) return void 0;
|
|
365
|
+
affine.transformPoint(local, inverse, hit.position);
|
|
366
|
+
const [tw, th] = value.tileSize;
|
|
367
|
+
const x = Math.floor(local[0] / tw);
|
|
368
|
+
const y = Math.floor(-local[1] / th);
|
|
369
|
+
let layer = chunk ? chunk.layer : -1;
|
|
370
|
+
if (!chunk) {
|
|
371
|
+
for (let li = data.layers.length - 1; li >= 0; li--) {
|
|
372
|
+
if (data.layers[li].get(x, y) !== 0) {
|
|
373
|
+
layer = li;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const l = data.layers[layer];
|
|
379
|
+
if (!l || x < 0 || y < 0 || x >= l.width || y >= l.height) return void 0;
|
|
380
|
+
const atlas = world.tryResource(TextureAtlases)?.get(value.atlas);
|
|
381
|
+
const id = l.get(x, y);
|
|
382
|
+
return { kind: "tile", tilemap, layer: l.name, x, y, id, tile: data.tileName(id, atlas) ?? null };
|
|
383
|
+
}
|
|
384
|
+
export {
|
|
385
|
+
GROUND_TILE_SHADERS,
|
|
386
|
+
GroundTiles,
|
|
387
|
+
TileChunk,
|
|
388
|
+
TileLit,
|
|
389
|
+
TileUnlit,
|
|
390
|
+
observeGroundTiles,
|
|
391
|
+
syncGroundTiles,
|
|
392
|
+
tileDetail,
|
|
393
|
+
tilemapOnGround
|
|
394
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
export { AtlasPackImporter, AtlasPackSchema, packImages, packNormals, regionOutline, TextureAtlas, TextureAtlasAssetType, TextureAtlases, TextureAtlasImporter, TextureAtlasSchema, TextureAtlasStore, } from './atlas.js';
|
|
2
2
|
export { animateSprites, CLIP_LOOPS, SpriteAnimation, SpriteAnimationEvent, type SpriteAnimationEventValue, SpriteClip, SpriteClipAssetType, SpriteClipImporter, SpriteClipSchema, SpriteClipStore, SpriteClips, } from './clip.js';
|
|
3
|
+
export { GROUND_TILE_SHADERS, GroundTiles, type GroundTilesState, syncGroundTiles, TileChunk, TileLit, TileUnlit, tileDetail, tilemapOnGround, } from './ground.js';
|
|
3
4
|
export { Lighting2d, LightOccluder2d, layerBit, OCCLUDER_SHAPES, type OccluderShape, PointLight2d, SpotLight2d, SpriteLighting, } from './lighting.js';
|
|
4
5
|
export { binLightsCpu, describeLights2d, LIGHT2D_FLOATS, Lights2d, Lights2dStore, LightView2d, lightCircle, MAX_SHADOWED, OccluderStore, prepareLights2d, SEGMENT_FLOATS, SHADOW_RES, shadowFactorCpu, shadowRowCpu, TILE_MAX, TILE_PIXELS, TILE_STRIDE, } from './lights2d.js';
|
|
6
|
+
export { type EditTilesOptions, editTiles, findTilemapData, type ReadTilesOptions, readTiles, type TileRect, tilemapMethods, } from './methods.js';
|
|
5
7
|
export { boxPolygon, capsulePolygon, circlePolygon, convexHull, occluderSegments, polygonProblem, polygonSegments, spriteOutline, tileChunkEdges, transformSegments, } from './occluders.js';
|
|
6
8
|
export { alphaOutline, distanceToLoop, loopArea, simplifyLoop, traceAlpha } from './outline.js';
|
|
7
9
|
export { spritePlugin } from './plugin.js';
|
|
8
|
-
export { describeSprites, prepareSprites, SPRITE_FLOATS, type SpriteBatch, SpriteStore, Sprites, TilemapStore, Tilemaps, } from './render.js';
|
|
10
|
+
export { describeSprites, prepareSprites, resolveTiles, SPRITE_FLOATS, type SpriteBatch, SpriteStore, Sprites, TilemapStore, Tilemaps, } from './render.js';
|
|
9
11
|
export { SPRITE_SHADERS } from './shaders.js';
|
|
10
12
|
export { SPRITE_BLENDS, SPRITE_SORTS, SPRITE_SPACES, Sprite, Sprite2dSettings, type Sprite2dSettingsValue, SpriteSlot, } from './sprite.js';
|
|
11
|
-
export { setTile, TileFlags, TileLayer, Tilemap, TilemapData, TilemapDataAssetType, TilemapDataImporter, TilemapDataSchema, TilemapDataStore, TilemapDatas, tileAt, } from './tilemap.js';
|
|
13
|
+
export { setTile, TileFlags, TileLayer, Tilemap, TilemapData, TilemapDataAssetType, TilemapDataImporter, TilemapDataSchema, TilemapDataStore, TilemapDatas, tileAt, tilemapToText, tileNames, unknownTiles, } from './tilemap.js';
|
|
14
|
+
export { type CellGrid, chunkRows, readChunkRows, rectRows, rowsCanName, TILE_FLAG_NAMES, type TileEncoding, } from './tilemap-format.js';
|
|
15
|
+
export { validateTilemaps } from './validate.js';
|