@aethervtt/shard-scatter 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/components.d.ts +44 -0
- package/dist/components.js +98 -0
- package/dist/foliage.d.ts +43 -0
- package/dist/foliage.js +175 -0
- package/dist/heightfield-surface.d.ts +35 -0
- package/dist/heightfield-surface.js +61 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +120 -0
- package/dist/material.d.ts +20 -0
- package/dist/material.js +149 -0
- package/dist/mesh-surface.d.ts +79 -0
- package/dist/mesh-surface.js +470 -0
- package/dist/methods.d.ts +10 -0
- package/dist/methods.js +341 -0
- package/dist/overlays.d.ts +4 -0
- package/dist/overlays.js +84 -0
- package/dist/planet.d.ts +74 -0
- package/dist/planet.js +675 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +265 -0
- package/dist/preview-render.d.ts +10 -0
- package/dist/preview-render.js +155 -0
- package/dist/preview.d.ts +5 -0
- package/dist/preview.js +11 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.js +170 -0
- package/dist/runtime.d.ts +138 -0
- package/dist/runtime.js +702 -0
- package/dist/set.d.ts +45 -0
- package/dist/set.js +131 -0
- package/dist/surface.d.ts +124 -0
- package/dist/surface.js +20 -0
- package/dist/testing.d.ts +236 -0
- package/dist/testing.js +231 -0
- package/dist/viewers.d.ts +9 -0
- package/dist/viewers.js +22 -0
- package/package.json +49 -4
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { AssetServerResource, assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import {
|
|
3
|
+
ChildOf,
|
|
4
|
+
defineSystem,
|
|
5
|
+
onRemove,
|
|
6
|
+
PostUpdate
|
|
7
|
+
} from "@aethervtt/shard-core";
|
|
8
|
+
import {
|
|
9
|
+
computeVisibility,
|
|
10
|
+
Mesh3d,
|
|
11
|
+
Meshes,
|
|
12
|
+
registerShaders,
|
|
13
|
+
Shaders
|
|
14
|
+
} from "@aethervtt/shard-render";
|
|
15
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
16
|
+
import {
|
|
17
|
+
Planet,
|
|
18
|
+
Terrain,
|
|
19
|
+
TerrainWorld,
|
|
20
|
+
updateHeightfields,
|
|
21
|
+
updatePlanets
|
|
22
|
+
} from "@aethervtt/shard-terrain";
|
|
23
|
+
import { GlobalTransform, TransformSystems } from "@aethervtt/shard-transform";
|
|
24
|
+
import * as componentsModule from "./components.js";
|
|
25
|
+
import {
|
|
26
|
+
addRemoved,
|
|
27
|
+
Prop,
|
|
28
|
+
propIdentity,
|
|
29
|
+
Removed,
|
|
30
|
+
removedKey,
|
|
31
|
+
ScatterBudget,
|
|
32
|
+
ScatterSurface
|
|
33
|
+
} from "./components.js";
|
|
34
|
+
import * as foliageModule from "./foliage.js";
|
|
35
|
+
import { applyWind, clearFoliage, updateFoliage, Wind } from "./foliage.js";
|
|
36
|
+
import { HeightfieldSurface } from "./heightfield-surface.js";
|
|
37
|
+
import * as materialModule from "./material.js";
|
|
38
|
+
import { SCATTER_SHADERS } from "./material.js";
|
|
39
|
+
import { MeshSurface } from "./mesh-surface.js";
|
|
40
|
+
import { scatterMethods } from "./methods.js";
|
|
41
|
+
import * as overlaysModule from "./overlays.js";
|
|
42
|
+
import { PlanetSurface } from "./planet.js";
|
|
43
|
+
import * as previewModule from "./preview.js";
|
|
44
|
+
import {
|
|
45
|
+
clearChunks,
|
|
46
|
+
planetScatters,
|
|
47
|
+
planetSources,
|
|
48
|
+
refreshSurface,
|
|
49
|
+
resolveAsset,
|
|
50
|
+
Scatter,
|
|
51
|
+
SurfaceScatter,
|
|
52
|
+
updateProps
|
|
53
|
+
} from "./runtime.js";
|
|
54
|
+
import * as setModule from "./set.js";
|
|
55
|
+
import { ScatterSet } from "./set.js";
|
|
56
|
+
import { cameraQuery } from "./viewers.js";
|
|
57
|
+
const watching = /* @__PURE__ */ new WeakSet();
|
|
58
|
+
function watchAssets(world) {
|
|
59
|
+
if (watching.has(world) || !world.tryResource(AssetServerResource)) return;
|
|
60
|
+
watching.add(world);
|
|
61
|
+
assetServer(world).onEvent((event) => {
|
|
62
|
+
if (event.kind === "loaded" || event.kind === "modified" || event.kind === "failed")
|
|
63
|
+
world.resource(Scatter).dirty = true;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const updateScatter = defineSystem({
|
|
67
|
+
name: "scatter/props",
|
|
68
|
+
description: "Places scatter props (rocks, trees, \u2026) in chunks around cameras and TerrainAnchors, spawning them nearest first within ScatterBudget and despawning those out of range.",
|
|
69
|
+
setup: (world) => ({
|
|
70
|
+
cameras: cameraQuery(world),
|
|
71
|
+
meshes: world.query({ with: [ScatterSurface, Mesh3d, GlobalTransform] })
|
|
72
|
+
}),
|
|
73
|
+
run: (s, world) => {
|
|
74
|
+
const state = world.resource(Scatter);
|
|
75
|
+
watchAssets(world);
|
|
76
|
+
state.frame++;
|
|
77
|
+
const frame = state.frame;
|
|
78
|
+
const budget = world.resource(ScatterBudget);
|
|
79
|
+
const dirty = state.dirty;
|
|
80
|
+
state.dirty = false;
|
|
81
|
+
recordRemovals(world);
|
|
82
|
+
const terrain = world.tryResource(TerrainWorld);
|
|
83
|
+
if (terrain) {
|
|
84
|
+
for (const rt of terrain.planets.values()) {
|
|
85
|
+
if (!world.isAlive(rt.entity) || !rt.ready) continue;
|
|
86
|
+
let ss = state.surfaces.get(rt.entity);
|
|
87
|
+
if (!ss) {
|
|
88
|
+
if (!planetScatters(world, rt)) continue;
|
|
89
|
+
ss = new SurfaceScatter(new PlanetSurface(rt, s.cameras));
|
|
90
|
+
state.surfaces.set(rt.entity, ss);
|
|
91
|
+
}
|
|
92
|
+
const start = performance.now();
|
|
93
|
+
const content = `${rt.version}:${rt.biomeVersion}`;
|
|
94
|
+
if (dirty || ss.content !== content || !ss.ready) {
|
|
95
|
+
ss.content = content;
|
|
96
|
+
const { sources, waiting } = planetSources(world, rt);
|
|
97
|
+
if (waiting) {
|
|
98
|
+
ss.waiting = waiting;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!refreshSurface(world, ss, sources, rt.settings.seed, content)) continue;
|
|
102
|
+
}
|
|
103
|
+
updateProps(world, ss, frame, budget, state.workers);
|
|
104
|
+
updateFoliage(world, ss, frame, state.workers);
|
|
105
|
+
ss.ms = performance.now() - start;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (terrain) {
|
|
109
|
+
const sets = world.initResource(ScatterSet.store);
|
|
110
|
+
for (const rt of terrain.heightfields.values()) {
|
|
111
|
+
if (!world.isAlive(rt.entity) || !rt.ready || !world.has(rt.entity, Terrain)) continue;
|
|
112
|
+
const own = world.get(rt.entity, Terrain).scatter;
|
|
113
|
+
let ss = state.surfaces.get(rt.entity);
|
|
114
|
+
if (!own && !ss) continue;
|
|
115
|
+
if (!ss) {
|
|
116
|
+
ss = new SurfaceScatter(new HeightfieldSurface(world, rt, s.cameras));
|
|
117
|
+
state.surfaces.set(rt.entity, ss);
|
|
118
|
+
}
|
|
119
|
+
const start = performance.now();
|
|
120
|
+
const content = `${rt.version}:${rt.asset?.hash}`;
|
|
121
|
+
if (dirty || ss.content !== content || !ss.ready) {
|
|
122
|
+
const set = resolveAsset(world, own, (g) => sets.get({ guid: g }));
|
|
123
|
+
if (!set) {
|
|
124
|
+
ss.waiting = `scatter set ${own?.path ?? own?.guid ?? "(none)"}`;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
;
|
|
128
|
+
ss.surface.setTerrain();
|
|
129
|
+
ss.content = content;
|
|
130
|
+
const sources = [{ path: own?.path ?? own?.guid ?? "", set, biome: -1 }];
|
|
131
|
+
if (!refreshSurface(world, ss, sources, rt.asset.source.seed, content)) continue;
|
|
132
|
+
}
|
|
133
|
+
updateProps(world, ss, frame, budget, state.workers);
|
|
134
|
+
updateFoliage(world, ss, frame, state.workers);
|
|
135
|
+
ss.ms = performance.now() - start;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const table of s.meshes.tables) {
|
|
139
|
+
const sets = world.initResource(ScatterSet.store);
|
|
140
|
+
const meshes = world.initResource(Meshes);
|
|
141
|
+
for (let row = 0; row < table.count; row++) {
|
|
142
|
+
const entity = table.entities[row];
|
|
143
|
+
let ss = state.surfaces.get(entity);
|
|
144
|
+
if (!ss) {
|
|
145
|
+
ss = new SurfaceScatter(new MeshSurface(world, entity, s.cameras));
|
|
146
|
+
state.surfaces.set(entity, ss);
|
|
147
|
+
}
|
|
148
|
+
const start = performance.now();
|
|
149
|
+
const surface = ss.surface;
|
|
150
|
+
const value = world.get(entity, ScatterSurface);
|
|
151
|
+
const meshRef = world.get(entity, Mesh3d).mesh;
|
|
152
|
+
const mesh = resolveAsset(world, meshRef, (g) => meshes.get({ guid: g }));
|
|
153
|
+
if (!mesh || mesh.gpu) {
|
|
154
|
+
ss.waiting = `mesh ${meshRef?.path ?? meshRef?.guid ?? "(none)"}`;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const set = resolveAsset(world, value.set, (g) => sets.get({ guid: g }));
|
|
158
|
+
if (!set) {
|
|
159
|
+
ss.waiting = `scatter set ${value.set?.path ?? value.set?.guid ?? "(none)"}`;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const changed = surface.setMesh(
|
|
163
|
+
mesh,
|
|
164
|
+
uniformScale(world.get(entity, GlobalTransform).matrix)
|
|
165
|
+
);
|
|
166
|
+
const content = `${mesh.version}:${surface.scale}`;
|
|
167
|
+
if (dirty || changed || ss.content !== content || !ss.ready) {
|
|
168
|
+
ss.content = content;
|
|
169
|
+
const sources = [{ path: value.set?.path ?? value.set?.guid ?? "", set, biome: -1 }];
|
|
170
|
+
if (!refreshSurface(world, ss, sources, value.seed, content)) continue;
|
|
171
|
+
}
|
|
172
|
+
updateProps(world, ss, frame, budget, state.workers);
|
|
173
|
+
updateFoliage(world, ss, frame, state.workers);
|
|
174
|
+
ss.ms = performance.now() - start;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
applyWind(world, windMaterials(state));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const used = /* @__PURE__ */ new Set();
|
|
181
|
+
function windMaterials(state) {
|
|
182
|
+
used.clear();
|
|
183
|
+
for (const ref of state.materials.values()) used.add(ref);
|
|
184
|
+
for (const ss of state.surfaces.values())
|
|
185
|
+
for (const items of ss.variants)
|
|
186
|
+
for (const vs of items) for (const v of vs) used.add(v.material);
|
|
187
|
+
return used;
|
|
188
|
+
}
|
|
189
|
+
function uniformScale(m) {
|
|
190
|
+
const sx = Math.sqrt(m[0] * m[0] + m[4] * m[4] + m[8] * m[8]);
|
|
191
|
+
const sy = Math.sqrt(m[1] * m[1] + m[5] * m[5] + m[9] * m[9]);
|
|
192
|
+
const sz = Math.sqrt(m[2] * m[2] + m[6] * m[6] + m[10] * m[10]);
|
|
193
|
+
return (sx + sy + sz) / 3 || 1;
|
|
194
|
+
}
|
|
195
|
+
function recordRemovals(world) {
|
|
196
|
+
const state = world.resource(Scatter);
|
|
197
|
+
if (state.pendingRemovals.length === 0) return;
|
|
198
|
+
const removed = world.initResource(Removed);
|
|
199
|
+
for (const p of state.pendingRemovals) {
|
|
200
|
+
if (!world.isAlive(p.root)) continue;
|
|
201
|
+
addRemoved(removed, removedKey(p.rule, p.chunk, p.index));
|
|
202
|
+
}
|
|
203
|
+
state.pendingRemovals.length = 0;
|
|
204
|
+
}
|
|
205
|
+
function scatterPlugin(options = {}) {
|
|
206
|
+
return definePlugin({
|
|
207
|
+
name: "scatter",
|
|
208
|
+
// Registers the scatter and foliage-chunks overlays.
|
|
209
|
+
provides: [
|
|
210
|
+
componentsModule,
|
|
211
|
+
foliageModule,
|
|
212
|
+
materialModule,
|
|
213
|
+
overlaysModule,
|
|
214
|
+
previewModule,
|
|
215
|
+
setModule,
|
|
216
|
+
Scatter
|
|
217
|
+
],
|
|
218
|
+
dependencies: ["core/transform"],
|
|
219
|
+
build(app) {
|
|
220
|
+
const world = app.world;
|
|
221
|
+
const state = world.initResource(Scatter);
|
|
222
|
+
state.workers = options.workers;
|
|
223
|
+
world.initResource(ScatterBudget);
|
|
224
|
+
world.initResource(Removed);
|
|
225
|
+
world.initResource(Wind);
|
|
226
|
+
world.observe(onRemove(Prop), ({ entity, world: world2 }) => {
|
|
227
|
+
const st = world2.resource(Scatter);
|
|
228
|
+
if (st.despawning) return;
|
|
229
|
+
const id = propIdentity(world2, entity);
|
|
230
|
+
const root = world2.tryGet(entity, ChildOf)?.parent;
|
|
231
|
+
if (!id || root === void 0 || root === null) return;
|
|
232
|
+
st.pendingRemovals.push({ ...id, root });
|
|
233
|
+
});
|
|
234
|
+
world.observe(onRemove(ScatterSurface), ({ entity, world: world2 }) => {
|
|
235
|
+
const st = world2.resource(Scatter);
|
|
236
|
+
const ss = st.surfaces.get(entity);
|
|
237
|
+
if (!ss) return;
|
|
238
|
+
clearChunks(world2, ss);
|
|
239
|
+
clearFoliage(world2, ss);
|
|
240
|
+
st.surfaces.delete(entity);
|
|
241
|
+
});
|
|
242
|
+
world.observe(onRemove(Planet), ({ entity, world: world2 }) => {
|
|
243
|
+
const st = world2.resource(Scatter);
|
|
244
|
+
const ss = st.surfaces.get(entity);
|
|
245
|
+
if (!ss) return;
|
|
246
|
+
clearChunks(world2, ss);
|
|
247
|
+
clearFoliage(world2, ss);
|
|
248
|
+
st.surfaces.delete(entity);
|
|
249
|
+
});
|
|
250
|
+
app.addSystems(
|
|
251
|
+
PostUpdate,
|
|
252
|
+
updateScatter.after(updatePlanets).after(updateHeightfields).after(TransformSystems).before(computeVisibility)
|
|
253
|
+
);
|
|
254
|
+
app.addMethod(...scatterMethods);
|
|
255
|
+
},
|
|
256
|
+
ready(app) {
|
|
257
|
+
const shaders = app.world.tryResource(Shaders);
|
|
258
|
+
if (shaders) registerShaders(shaders, SCATTER_SHADERS);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
scatterPlugin,
|
|
264
|
+
updateScatter
|
|
265
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type PreviewImage } from '@aethervtt/shard-assets';
|
|
2
|
+
import { type World } from '@aethervtt/shard-core';
|
|
3
|
+
/** Side of the previewed patch (m). */
|
|
4
|
+
export declare const PREVIEW_SIZE = 64;
|
|
5
|
+
/**
|
|
6
|
+
* A ScatterSet on a flat 64 m patch, in a private world sharing the game's GPU, assets and
|
|
7
|
+
* generator outputs: from above (left half) and at eye level (right half), or one of them with
|
|
8
|
+
* `options.view` ('top' | 'eye').
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderScatterPreview(world: World, path: string, width: number, height: number, options?: Readonly<Record<string, unknown>>): Promise<PreviewImage>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { AssetServerResource, assetServer } from "@aethervtt/shard-assets";
|
|
2
|
+
import { findResource } from "@aethervtt/shard-core";
|
|
3
|
+
import { plane } from "@aethervtt/shard-mesh";
|
|
4
|
+
import { NoiseGraphs } from "@aethervtt/shard-noise";
|
|
5
|
+
import {
|
|
6
|
+
AmbientLight,
|
|
7
|
+
Camera3d,
|
|
8
|
+
captureView,
|
|
9
|
+
DirectionalLight,
|
|
10
|
+
Exposure,
|
|
11
|
+
FoliageLayers,
|
|
12
|
+
forwardPlugin,
|
|
13
|
+
Gpu,
|
|
14
|
+
MaterialAsset,
|
|
15
|
+
Materials,
|
|
16
|
+
Mesh3d,
|
|
17
|
+
Meshes,
|
|
18
|
+
MeshMaterial,
|
|
19
|
+
OffscreenTarget,
|
|
20
|
+
RenderTargets,
|
|
21
|
+
renderPlugin,
|
|
22
|
+
Shaders,
|
|
23
|
+
Tonemapping
|
|
24
|
+
} from "@aethervtt/shard-render";
|
|
25
|
+
import { App } from "@aethervtt/shard-runtime";
|
|
26
|
+
import { ScenePlugin } from "@aethervtt/shard-scene";
|
|
27
|
+
import { Textures } from "@aethervtt/shard-texture";
|
|
28
|
+
import { lookAt, Transform, TransformPlugin } from "@aethervtt/shard-transform";
|
|
29
|
+
import { ScatterSurface } from "./components.js";
|
|
30
|
+
import { scatterPlugin } from "./plugin.js";
|
|
31
|
+
import { Scatter } from "./runtime.js";
|
|
32
|
+
import { ScatterSet } from "./set.js";
|
|
33
|
+
const PREVIEW_SIZE = 64;
|
|
34
|
+
async function renderScatterPreview(world, path, width, height, options = {}) {
|
|
35
|
+
const gpu = world.resource(Gpu);
|
|
36
|
+
const server = assetServer(world);
|
|
37
|
+
await server.load(path);
|
|
38
|
+
const entry = server.entry(path);
|
|
39
|
+
const view = options.view === "top" || options.view === "eye" ? options.view : "both";
|
|
40
|
+
const app = new App();
|
|
41
|
+
app.world.insertResource(AssetServerResource, server);
|
|
42
|
+
for (const def of [Meshes, Materials, Textures, NoiseGraphs, ScatterSet.store])
|
|
43
|
+
app.world.insertResource(def, world.initResource(def));
|
|
44
|
+
for (const name of ["procgen/Runtime", "procgen/Generators", "procgen/Data"]) {
|
|
45
|
+
const def = findResource(name);
|
|
46
|
+
if (def && world.hasResource(def)) app.world.insertResource(def, world.resource(def));
|
|
47
|
+
}
|
|
48
|
+
app.addPlugin(
|
|
49
|
+
TransformPlugin,
|
|
50
|
+
renderPlugin({ gpu, windowView: false, shaders: world.resource(Shaders) }),
|
|
51
|
+
forwardPlugin({ msaa: 1 }),
|
|
52
|
+
ScenePlugin,
|
|
53
|
+
scatterPlugin()
|
|
54
|
+
);
|
|
55
|
+
const views = view === "both" ? ["top", "eye"] : [view];
|
|
56
|
+
const cellW = view === "both" ? Math.max(1, Math.floor(width / 2)) : width;
|
|
57
|
+
const targets = [];
|
|
58
|
+
try {
|
|
59
|
+
await app.init();
|
|
60
|
+
const w = app.world;
|
|
61
|
+
const ground = w.resource(Materials).add(new MaterialAsset({ baseColor: [0.36, 0.3, 0.22, 1], roughness: 1 }));
|
|
62
|
+
w.spawn(
|
|
63
|
+
[Mesh3d, { mesh: w.resource(Meshes).add(plane({ size: 1, subdivisions: 2 })) }],
|
|
64
|
+
[MeshMaterial, { material: ground }],
|
|
65
|
+
[
|
|
66
|
+
ScatterSurface,
|
|
67
|
+
{
|
|
68
|
+
set: {
|
|
69
|
+
type: "scatter/ScatterSet",
|
|
70
|
+
guid: entry.guid,
|
|
71
|
+
path: entry.path
|
|
72
|
+
},
|
|
73
|
+
seed: 1
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
[Transform, { scale: [PREVIEW_SIZE, PREVIEW_SIZE, PREVIEW_SIZE] }]
|
|
77
|
+
);
|
|
78
|
+
Object.assign(w.resource(AmbientLight), { brightness: 4e3 });
|
|
79
|
+
w.spawn(
|
|
80
|
+
[DirectionalLight, { illuminance: 6e4, shadows: true }],
|
|
81
|
+
[Transform, { rotation: lookAt([-3, 8, 4], [0, 0, 0]) }]
|
|
82
|
+
);
|
|
83
|
+
const cameras = [];
|
|
84
|
+
for (const v of views) {
|
|
85
|
+
const target = new OffscreenTarget(gpu, {
|
|
86
|
+
label: `scatter-preview-${v}`,
|
|
87
|
+
width: cellW,
|
|
88
|
+
height
|
|
89
|
+
});
|
|
90
|
+
targets.push(target);
|
|
91
|
+
const ref = w.resource(RenderTargets).add(target, `scatter-preview-${v}`);
|
|
92
|
+
const eye = v === "top" ? [0, 40, 1e-3] : [-PREVIEW_SIZE * 0.42, 1.7, PREVIEW_SIZE * 0.42];
|
|
93
|
+
const at = v === "top" ? [0, 0, 0] : [0, 0.8, 0];
|
|
94
|
+
const camera = w.spawn(
|
|
95
|
+
[
|
|
96
|
+
Camera3d,
|
|
97
|
+
v === "top" ? {
|
|
98
|
+
target: ref,
|
|
99
|
+
projection: "orthographic",
|
|
100
|
+
orthoHeight: PREVIEW_SIZE,
|
|
101
|
+
far: 200,
|
|
102
|
+
clearColor: [0.5, 0.65, 0.85, 1]
|
|
103
|
+
} : { target: ref, fovY: 55, clearColor: [0.5, 0.65, 0.85, 1] }
|
|
104
|
+
],
|
|
105
|
+
[Exposure, { ev100: 13.5 }],
|
|
106
|
+
[Tonemapping, { dither: false }],
|
|
107
|
+
[Transform, { translation: eye, rotation: lookAt(eye, at) }]
|
|
108
|
+
);
|
|
109
|
+
cameras.push(`camera:${camera}`);
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < 600; i++) {
|
|
112
|
+
app.update(1 / 60);
|
|
113
|
+
await w.resource(Shaders).whenIdle();
|
|
114
|
+
await gpu.pipelines.whenIdle();
|
|
115
|
+
await new Promise((r) => setTimeout(r, 1));
|
|
116
|
+
const state = w.resource(Scatter);
|
|
117
|
+
let busy = state.surfaces.size === 0;
|
|
118
|
+
for (const ss of state.surfaces.values()) {
|
|
119
|
+
if (ss.problem) throw ss.problem;
|
|
120
|
+
if (!ss.ready) busy = true;
|
|
121
|
+
for (const c of ss.chunks.values()) if (c.job) busy = true;
|
|
122
|
+
for (const f of ss.foliage.values())
|
|
123
|
+
for (const c of f.chunks.values()) if (c.slot < 0) busy = true;
|
|
124
|
+
}
|
|
125
|
+
const layers = w.resource(FoliageLayers).layers;
|
|
126
|
+
for (const l of layers) if (l.pending.length > 0) busy = true;
|
|
127
|
+
if (!busy && i > 3 && gpu.pipelines.pending === 0 && gpu.pipelines.skipped === 0) break;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < 3; i++) app.update(1 / 60);
|
|
130
|
+
const shots = cameras.map((c) => captureView(w, c));
|
|
131
|
+
app.update(1 / 60);
|
|
132
|
+
const images = await Promise.all(shots);
|
|
133
|
+
if (images.length === 1) {
|
|
134
|
+
const image = images[0];
|
|
135
|
+
return { width: image.width, height: image.height, data: image.data };
|
|
136
|
+
}
|
|
137
|
+
const out = new Uint8Array(cellW * 2 * height * 4);
|
|
138
|
+
for (const [k, image] of images.entries()) {
|
|
139
|
+
for (let y = 0; y < height; y++) {
|
|
140
|
+
out.set(
|
|
141
|
+
image.data.subarray(y * cellW * 4, (y + 1) * cellW * 4),
|
|
142
|
+
(y * cellW * 2 + k * cellW) * 4
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { width: cellW * 2, height, data: out };
|
|
147
|
+
} finally {
|
|
148
|
+
await app.dispose();
|
|
149
|
+
for (const t of targets) t.destroy();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
PREVIEW_SIZE,
|
|
154
|
+
renderScatterPreview
|
|
155
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ScatterSet on a 64 m test patch (spec 0045), from above and at eye level (`options.view`:
|
|
3
|
+
* 'top', 'eye', or both side by side). `asset.preview` and `procgen.preview` use it.
|
|
4
|
+
*/
|
|
5
|
+
export declare const scatterSetPreview: import("@aethervtt/shard-assets").AssetPreview;
|
package/dist/preview.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineAssetPreview } from "@aethervtt/shard-assets";
|
|
2
|
+
const scatterSetPreview = defineAssetPreview(
|
|
3
|
+
"scatter/ScatterSet",
|
|
4
|
+
async (world, path, width, height, options) => {
|
|
5
|
+
const { renderScatterPreview } = await import("./preview-render.js");
|
|
6
|
+
return renderScatterPreview(world, path, width, height, options);
|
|
7
|
+
}
|
|
8
|
+
);
|
|
9
|
+
export {
|
|
10
|
+
scatterSetPreview
|
|
11
|
+
};
|
package/dist/rules.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { type AssetRef, type JsonValue } from '@aethervtt/shard-core';
|
|
2
|
+
import type { PropCollider, ScatterSetValue } from './set.js';
|
|
3
|
+
/** Rule kinds as numbers. */
|
|
4
|
+
export declare const PROP = 0;
|
|
5
|
+
export declare const FOLIAGE = 1;
|
|
6
|
+
/** Most items a prop chunk may hold: placement picks a chunk size to hold 64–1 024. */
|
|
7
|
+
export declare const MAX_PROPS_PER_CHUNK = 1024;
|
|
8
|
+
/** Most items a foliage chunk may hold (one GPU placement dispatch). */
|
|
9
|
+
export declare const MAX_FOLIAGE_PER_CHUNK = 16384;
|
|
10
|
+
export interface CompiledItem {
|
|
11
|
+
prefab: AssetRef | null;
|
|
12
|
+
/** Generator name, or '' for a prefab item. */
|
|
13
|
+
generator: string;
|
|
14
|
+
params: Record<string, JsonValue>;
|
|
15
|
+
variants: number;
|
|
16
|
+
material: AssetRef | null;
|
|
17
|
+
/** Footprint radius before scale, 0: from the mesh bounds. */
|
|
18
|
+
radius: number;
|
|
19
|
+
}
|
|
20
|
+
/** A rule as numbers, ready to place: its lattice, masks, and items. */
|
|
21
|
+
export interface CompiledRule {
|
|
22
|
+
/** Index in the surface's compiled list (priority order). */
|
|
23
|
+
index: number;
|
|
24
|
+
name: string;
|
|
25
|
+
/** Stable across runs: the set's path, the biome it belongs to, and the rule's name. */
|
|
26
|
+
id: string;
|
|
27
|
+
kind: number;
|
|
28
|
+
/** The biome it applies in (−1: everywhere). */
|
|
29
|
+
biome: number;
|
|
30
|
+
seed: number;
|
|
31
|
+
density: number;
|
|
32
|
+
spacing: number;
|
|
33
|
+
/** Lattice cell size (m): at most one candidate per cell. */
|
|
34
|
+
cell: number;
|
|
35
|
+
slopeLo: number;
|
|
36
|
+
slopeHi: number;
|
|
37
|
+
heightLo: number;
|
|
38
|
+
heightHi: number;
|
|
39
|
+
noise: AssetRef | null;
|
|
40
|
+
noiseAbove: number;
|
|
41
|
+
align: number;
|
|
42
|
+
scaleMin: number;
|
|
43
|
+
scaleMax: number;
|
|
44
|
+
sink: number;
|
|
45
|
+
collider: PropCollider;
|
|
46
|
+
range: number;
|
|
47
|
+
wind: number;
|
|
48
|
+
shadowRange: number;
|
|
49
|
+
/** Indices of earlier rules whose footprints this one avoids. */
|
|
50
|
+
avoid: number[];
|
|
51
|
+
items: CompiledItem[];
|
|
52
|
+
/** Running sum of item weights, normalized to end at 1. */
|
|
53
|
+
weights: Float64Array;
|
|
54
|
+
}
|
|
55
|
+
/** A set with where its rules apply: everywhere, or a biome (by index in the BiomeSet). */
|
|
56
|
+
export interface SetSource {
|
|
57
|
+
/** The set's asset path (rule ids, saves). */
|
|
58
|
+
path: string;
|
|
59
|
+
set: ScatterSetValue;
|
|
60
|
+
biome: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Turns sets into one priority-ordered rule list for a surface: a surface's own set first, then
|
|
64
|
+
* each biome's in biome order. Throws `scatter/unknown-rule` (an `avoid` naming no earlier rule),
|
|
65
|
+
* `scatter/density-too-high` (a density the spacing can't fit), and `scatter/no-items`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function compileRules(sources: readonly SetSource[], surfaceSeed: number): CompiledRule[];
|
|
68
|
+
/**
|
|
69
|
+
* The lattice cell for a density and spacing: about two cells per expected item (so acceptance is
|
|
70
|
+
* a coin flip, which hides the lattice), but never so small that jitter can't keep `spacing`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function cellSize(density: number, spacing: number): number;
|
|
73
|
+
/**
|
|
74
|
+
* Jitter as a fraction of a cell, for cells `cell` metres across: as much as `spacing` allows.
|
|
75
|
+
* Items sit in the middle `jitter` of their cell, so two in neighboring cells are at least
|
|
76
|
+
* `cell × (1 − jitter)` = `spacing` apart.
|
|
77
|
+
*/
|
|
78
|
+
export declare function jitterFor(cell: number, spacing: number): number;
|
|
79
|
+
/** Streams of a cell's hash (the w lane of hash32), so each decision is independent. */
|
|
80
|
+
export declare const STREAM_ACCEPT = 0;
|
|
81
|
+
export declare const STREAM_X = 1;
|
|
82
|
+
export declare const STREAM_Y = 2;
|
|
83
|
+
export declare const STREAM_ITEM = 3;
|
|
84
|
+
export declare const STREAM_VARIANT = 4;
|
|
85
|
+
export declare const STREAM_YAW = 5;
|
|
86
|
+
export declare const STREAM_SCALE = 6;
|
|
87
|
+
export declare const STREAM_SHADE = 7;
|
|
88
|
+
/**
|
|
89
|
+
* A cell's random number in [0, 1) for a stream: a hash of the rule's seed, the cell's global
|
|
90
|
+
* lattice coordinates, and its domain (a cube face, or 0 on a mesh). The foliage compute shader
|
|
91
|
+
* computes the same numbers.
|
|
92
|
+
*/
|
|
93
|
+
export declare function cellRandom(seed: number, gi: number, gj: number, domain: number, stream: number): number;
|
|
94
|
+
/** The item a cell picks by weight. */
|
|
95
|
+
export declare function pickItem(rule: CompiledRule, u: number): number;
|
|
96
|
+
/** Whether `x` is within [lo, hi], or the range is open (lo ≥ hi). */
|
|
97
|
+
export declare function inRange(x: number, lo: number, hi: number): boolean;
|