@jgengine/core 0.25.0 → 0.26.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/CHANGELOG.md +65 -0
- package/dist/ai/rvo.d.ts +106 -0
- package/dist/ai/rvo.js +581 -0
- package/dist/ai/utilityBrain.d.ts +173 -0
- package/dist/ai/utilityBrain.js +277 -0
- package/dist/combat/cover.d.ts +169 -0
- package/dist/combat/cover.js +264 -0
- package/dist/combat/damageRuntime.d.ts +150 -0
- package/dist/combat/damageRuntime.js +208 -0
- package/dist/combat/damageTable.d.ts +116 -0
- package/dist/combat/damageTable.js +165 -0
- package/dist/combat/hitZones.d.ts +70 -0
- package/dist/combat/hitZones.js +77 -0
- package/dist/combat/magazine.d.ts +8 -0
- package/dist/combat/magazine.js +20 -2
- package/dist/combat/squadCombat.d.ts +167 -0
- package/dist/combat/squadCombat.js +557 -0
- package/dist/combat/squadTactics.d.ts +73 -0
- package/dist/combat/squadTactics.js +49 -0
- package/dist/combat/statusRuntime.d.ts +142 -0
- package/dist/combat/statusRuntime.js +236 -0
- package/dist/combat/weaponDefinition.d.ts +106 -0
- package/dist/combat/weaponDefinition.js +108 -0
- package/dist/combat/weaponSystem.d.ts +189 -0
- package/dist/combat/weaponSystem.js +469 -0
- package/dist/combat.d.ts +9 -0
- package/dist/combat.js +9 -0
- package/dist/devtools/devtools.d.ts +4 -2
- package/dist/devtools/devtools.js +2 -1
- package/dist/devtools/discover.d.ts +2 -1
- package/dist/devtools/discover.js +38 -0
- package/dist/devtools/frame.d.ts +0 -2
- package/dist/devtools/frame.js +1 -2
- package/dist/devtools/logBus.d.ts +58 -0
- package/dist/devtools/logBus.js +79 -0
- package/dist/devtools/logs.d.ts +6 -3
- package/dist/devtools/logs.js +17 -13
- package/dist/devtools/report.d.ts +120 -0
- package/dist/devtools/report.js +49 -0
- package/dist/devtools/reportEndpoint.d.ts +17 -0
- package/dist/devtools/reportEndpoint.js +7 -0
- package/dist/devtools/types.d.ts +48 -11
- package/dist/editor/document.js +210 -0
- package/dist/editor/environment.d.ts +48 -1
- package/dist/editor/environment.js +161 -0
- package/dist/editor/index.d.ts +6 -2
- package/dist/editor/index.js +1 -1
- package/dist/editor/types.d.ts +54 -0
- package/dist/gameplay.d.ts +4 -0
- package/dist/gameplay.js +4 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +56 -1
- package/dist/nav/navCrowd.d.ts +138 -0
- package/dist/nav/navCrowd.js +412 -0
- package/dist/nav/navRuntime.d.ts +108 -0
- package/dist/nav/navRuntime.js +256 -0
- package/dist/render/colorPipeline.d.ts +53 -0
- package/dist/render/colorPipeline.js +79 -0
- package/dist/render/lightProbes.d.ts +97 -0
- package/dist/render/lightProbes.js +221 -0
- package/dist/render/lookPreset.js +4 -4
- package/dist/render/physicalSky.d.ts +39 -0
- package/dist/render/physicalSky.js +89 -0
- package/dist/render/postProcessing.d.ts +40 -8
- package/dist/render/postProcessing.js +1 -1
- package/dist/render/skyOwnedLighting.d.ts +31 -0
- package/dist/render/skyOwnedLighting.js +46 -0
- package/dist/sensor/perception.d.ts +66 -5
- package/dist/sensor/perception.js +167 -42
- package/dist/sim/nearAgents.d.ts +68 -0
- package/dist/sim/nearAgents.js +152 -0
- package/dist/sim/needs.d.ts +60 -0
- package/dist/sim/needs.js +108 -0
- package/dist/sim/offScreen.d.ts +181 -0
- package/dist/sim/offScreen.js +450 -0
- package/dist/sim/routine.d.ts +79 -0
- package/dist/sim/routine.js +137 -0
- package/dist/visibility/visibilitySystem.js +21 -0
- package/dist/world/features.d.ts +8 -0
- package/dist/world.d.ts +5 -0
- package/dist/world.js +5 -0
- package/package.json +1 -1
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-combat/SKILL.md +17 -0
- package/skills/jgengine-combat/api.md +246 -0
- package/skills/jgengine-combat/capabilities.md +112 -0
- package/skills/jgengine-combat/reference.md +32 -0
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +71 -1
- package/skills/jgengine-gameplay/capabilities.md +16 -0
- package/skills/jgengine-world/SKILL.md +3 -1
- package/skills/jgengine-world/api.md +107 -3
- package/skills/jgengine-world/capabilities.md +22 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
package/dist/devtools/types.d.ts
CHANGED
|
@@ -100,12 +100,48 @@ export interface LatencyStats {
|
|
|
100
100
|
}
|
|
101
101
|
/** Severity level for a captured devtools log entry. */
|
|
102
102
|
export type DevtoolsLogLevel = "log" | "info" | "warn" | "error";
|
|
103
|
-
/** A single captured log line with timestamp, level, and formatted message. */
|
|
103
|
+
/** A single captured log line with timestamp, level, owning system tag, and formatted message. */
|
|
104
104
|
export interface DevtoolsLogEntry {
|
|
105
105
|
at: number;
|
|
106
106
|
level: DevtoolsLogLevel;
|
|
107
|
+
/** System that wrote the line, e.g. `needs`, `nav`, `render/culling`, `console`, `runtime`. */
|
|
108
|
+
system: string;
|
|
107
109
|
message: string;
|
|
108
110
|
}
|
|
111
|
+
/** What one system reports about itself: totals it keeps anyway, named timings, and one state line. */
|
|
112
|
+
export interface SystemProbe {
|
|
113
|
+
/** Monotonic or gauge numbers a system already tracks (counts, queue depths, cache hits). */
|
|
114
|
+
counters?(): Record<string, number>;
|
|
115
|
+
/** Named timings in milliseconds the system accumulated (spatial query, path solve, tick). */
|
|
116
|
+
timers?(): Record<string, number>;
|
|
117
|
+
/** One short line a human reads in the report, e.g. `visible 120/800 · culled 680`. */
|
|
118
|
+
state?(): string;
|
|
119
|
+
}
|
|
120
|
+
/** One system's probe read out on demand; missing sections stay empty rather than absent. */
|
|
121
|
+
export interface SystemProbeReading {
|
|
122
|
+
counters: Record<string, number>;
|
|
123
|
+
timers: Record<string, number>;
|
|
124
|
+
state: string;
|
|
125
|
+
}
|
|
126
|
+
/** Bounded log ring with per-system tags, a warning/error ring, and lifetime counts. */
|
|
127
|
+
export interface DevtoolsLogs {
|
|
128
|
+
/** Tagged write for a system's own lines; allocation-free apart from the message itself. */
|
|
129
|
+
write(level: DevtoolsLogLevel, system: string, message: string): void;
|
|
130
|
+
/** Untagged convenience write; `system` defaults to `console`. */
|
|
131
|
+
push(level: DevtoolsLogLevel, message: string, system?: string): void;
|
|
132
|
+
captureConsole(): void;
|
|
133
|
+
list(): readonly DevtoolsLogEntry[];
|
|
134
|
+
severe(): readonly DevtoolsLogEntry[];
|
|
135
|
+
counts(): import("./logBus.js").LogBusCounts;
|
|
136
|
+
dropped(): number;
|
|
137
|
+
snapshot(): import("./logBus.js").LogBusSnapshot;
|
|
138
|
+
clear(): void;
|
|
139
|
+
}
|
|
140
|
+
/** Latency samples recorded by instrumented transport methods. */
|
|
141
|
+
export interface DevtoolsLatency {
|
|
142
|
+
record(ms: number): void;
|
|
143
|
+
stats(): LatencyStats | null;
|
|
144
|
+
}
|
|
109
145
|
/** A control auto-discovered by scanning an object/table, with its binding metadata and current reader. */
|
|
110
146
|
export interface DiscoveredEntry {
|
|
111
147
|
readonly id: string;
|
|
@@ -131,6 +167,8 @@ export interface DevtoolsSnapshot {
|
|
|
131
167
|
longFrames: readonly LongFrameEvent[];
|
|
132
168
|
logs: readonly DevtoolsLogEntry[];
|
|
133
169
|
probes: Record<string, unknown>;
|
|
170
|
+
/** Per-system probe readings (`counters`/`timers`/`state`), keyed by system id. */
|
|
171
|
+
systems: Record<string, SystemProbeReading>;
|
|
134
172
|
controls: readonly {
|
|
135
173
|
name: string;
|
|
136
174
|
kind: DevtoolsControlKind;
|
|
@@ -172,16 +210,8 @@ export interface Devtools {
|
|
|
172
210
|
record(sample: RenderSample): void;
|
|
173
211
|
latest(): RenderSample | null;
|
|
174
212
|
};
|
|
175
|
-
logs:
|
|
176
|
-
|
|
177
|
-
captureConsole(): void;
|
|
178
|
-
list(): readonly DevtoolsLogEntry[];
|
|
179
|
-
clear(): void;
|
|
180
|
-
};
|
|
181
|
-
latency: {
|
|
182
|
-
record(ms: number): void;
|
|
183
|
-
stats(): LatencyStats | null;
|
|
184
|
-
};
|
|
213
|
+
logs: DevtoolsLogs;
|
|
214
|
+
latency: DevtoolsLatency;
|
|
185
215
|
controls: {
|
|
186
216
|
register<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T>;
|
|
187
217
|
list(): readonly DevtoolsControl[];
|
|
@@ -206,6 +236,13 @@ export interface Devtools {
|
|
|
206
236
|
probes: {
|
|
207
237
|
register(name: string, read: () => unknown): () => void;
|
|
208
238
|
read(): Record<string, unknown>;
|
|
239
|
+
/**
|
|
240
|
+
* Register a system's own counters, timers and state line. Returns its dispose; registering the
|
|
241
|
+
* same id again replaces the earlier one (the last live instance wins).
|
|
242
|
+
*/
|
|
243
|
+
registerSystem(id: string, probe: SystemProbe): () => void;
|
|
244
|
+
/** Read every registered system probe. Allocates; call on demand, never per frame. */
|
|
245
|
+
systems(): Record<string, SystemProbeReading>;
|
|
209
246
|
};
|
|
210
247
|
signal: ChangeSignal;
|
|
211
248
|
snapshot(): DevtoolsSnapshot;
|
package/dist/editor/document.js
CHANGED
|
@@ -5,6 +5,17 @@ import { parseStaticPrefabBake } from "./staticPrefab.js";
|
|
|
5
5
|
import { cloneEditorUiDocument, decodeEditorUiDocument } from "../ui/hudDocument.js";
|
|
6
6
|
import { migrateTerrainSnapshot } from "../world/terraform.js";
|
|
7
7
|
import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
|
|
8
|
+
import { PROBE_STRIDE } from "../render/lightProbes.js";
|
|
9
|
+
function cloneLightProbes(probes) {
|
|
10
|
+
return probes.map((probe) => ({
|
|
11
|
+
...probe,
|
|
12
|
+
center: [...probe.center],
|
|
13
|
+
size: [...probe.size],
|
|
14
|
+
resolution: [...probe.resolution],
|
|
15
|
+
...(probe.samples === undefined ? {} : { samples: [...probe.samples] }),
|
|
16
|
+
...(probe.bake === undefined ? {} : { bake: { ...probe.bake, ...(probe.bake.lights === undefined ? {} : { lights: probe.bake.lights.map((light) => ({ ...light, position: [...light.position] })) }) } }),
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
8
19
|
/** Copies a baked minimap so a document's PNG + bounds are never shared by reference across rebuilds. */
|
|
9
20
|
function cloneMinimapBake(minimap) {
|
|
10
21
|
return { background: minimap.background, bounds: { ...minimap.bounds } };
|
|
@@ -26,6 +37,14 @@ function cloneEnvironment(environment) {
|
|
|
26
37
|
...(environment.sunAzimuth === undefined ? {} : { sunAzimuth: environment.sunAzimuth }),
|
|
27
38
|
...(environment.sunElevation === undefined ? {} : { sunElevation: environment.sunElevation }),
|
|
28
39
|
...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
|
|
40
|
+
...(environment.grading === undefined ? {} : { grading: cloneGrading(environment.grading) }),
|
|
41
|
+
...(environment.probes === undefined ? {} : { probes: cloneLightProbes(environment.probes) }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function cloneGrading(grading) {
|
|
45
|
+
return {
|
|
46
|
+
...grading,
|
|
47
|
+
...(grading.autoExposure === undefined ? {} : { autoExposure: { ...grading.autoExposure } }),
|
|
29
48
|
};
|
|
30
49
|
}
|
|
31
50
|
function cloneEnvironmentSource(source) {
|
|
@@ -1123,6 +1142,28 @@ function decodeEnvironment(value, path, errors) {
|
|
|
1123
1142
|
}));
|
|
1124
1143
|
}
|
|
1125
1144
|
}
|
|
1145
|
+
if (value.probes !== undefined) {
|
|
1146
|
+
if (!Array.isArray(value.probes)) {
|
|
1147
|
+
errors.push({ path: `${path}.probes`, message: "expected an array" });
|
|
1148
|
+
failed = true;
|
|
1149
|
+
}
|
|
1150
|
+
else if (value.probes.length > 16) {
|
|
1151
|
+
errors.push({ path: `${path}.probes`, message: "at most 16 irradiance volumes" });
|
|
1152
|
+
failed = true;
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
const probes = [];
|
|
1156
|
+
for (const [index, entry] of value.probes.entries()) {
|
|
1157
|
+
const probe = decodeLightProbe(entry, `${path}.probes[${index}]`, errors);
|
|
1158
|
+
if (probe === undefined)
|
|
1159
|
+
failed = true;
|
|
1160
|
+
else
|
|
1161
|
+
probes.push(probe);
|
|
1162
|
+
}
|
|
1163
|
+
if (probes.length > 0)
|
|
1164
|
+
env.probes = probes;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1126
1167
|
if (value.preset !== undefined) {
|
|
1127
1168
|
if (typeof value.preset === "string" && SKY_PRESETS.has(value.preset)) {
|
|
1128
1169
|
env.preset = value.preset;
|
|
@@ -1195,10 +1236,179 @@ function decodeEnvironment(value, path, errors) {
|
|
|
1195
1236
|
env.fog = fog;
|
|
1196
1237
|
}
|
|
1197
1238
|
}
|
|
1239
|
+
if (value.grading !== undefined) {
|
|
1240
|
+
const grading = decodeGrading(value.grading, `${path}.grading`, errors);
|
|
1241
|
+
if (grading === undefined)
|
|
1242
|
+
failed = true;
|
|
1243
|
+
else
|
|
1244
|
+
env.grading = grading;
|
|
1245
|
+
}
|
|
1198
1246
|
if (failed)
|
|
1199
1247
|
return undefined;
|
|
1200
1248
|
return env;
|
|
1201
1249
|
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Validates an authored colour grade. Swatches must be hex colours and every number finite, because
|
|
1252
|
+
* the runtime bakes the LUT straight from these values; unknown junk is rejected with its path rather
|
|
1253
|
+
* than silently grading to something the author did not pick.
|
|
1254
|
+
*/
|
|
1255
|
+
function decodeGrading(value, path, errors) {
|
|
1256
|
+
if (!isPlainObject(value)) {
|
|
1257
|
+
errors.push({ path, message: "expected an object" });
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
let failed = false;
|
|
1261
|
+
const grading = {};
|
|
1262
|
+
const swatch = (key) => {
|
|
1263
|
+
const entry = value[key];
|
|
1264
|
+
if (entry === undefined)
|
|
1265
|
+
return;
|
|
1266
|
+
if (typeof entry === "string" && /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.test(entry)) {
|
|
1267
|
+
grading[key] = entry.startsWith("#") ? entry : `#${entry}`;
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
errors.push({ path: `${path}.${key}`, message: "expected a hex colour such as #2a3a52" });
|
|
1271
|
+
failed = true;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
swatch("shadows");
|
|
1275
|
+
swatch("midtones");
|
|
1276
|
+
swatch("highlights");
|
|
1277
|
+
if (value.size !== undefined) {
|
|
1278
|
+
if (typeof value.size === "number" && Number.isInteger(value.size) && value.size >= 2 && value.size <= 16)
|
|
1279
|
+
grading.size = value.size;
|
|
1280
|
+
else {
|
|
1281
|
+
errors.push({ path: `${path}.size`, message: "expected an integer cube size from 2 to 16" });
|
|
1282
|
+
failed = true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
for (const key of ["tint", "saturation", "contrast"]) {
|
|
1286
|
+
const entry = value[key];
|
|
1287
|
+
if (entry === undefined)
|
|
1288
|
+
continue;
|
|
1289
|
+
if (typeof entry === "number" && Number.isFinite(entry))
|
|
1290
|
+
grading[key] = entry;
|
|
1291
|
+
else {
|
|
1292
|
+
errors.push({ path: `${path}.${key}`, message: "expected a finite number" });
|
|
1293
|
+
failed = true;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (value.autoExposure !== undefined) {
|
|
1297
|
+
if (!isPlainObject(value.autoExposure)) {
|
|
1298
|
+
errors.push({ path: `${path}.autoExposure`, message: "expected an object" });
|
|
1299
|
+
failed = true;
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
const exposure = {};
|
|
1303
|
+
for (const key of ["min", "max"]) {
|
|
1304
|
+
const entry = value.autoExposure[key];
|
|
1305
|
+
if (entry === undefined)
|
|
1306
|
+
continue;
|
|
1307
|
+
if (typeof entry === "number" && Number.isFinite(entry) && entry > 0)
|
|
1308
|
+
exposure[key] = entry;
|
|
1309
|
+
else {
|
|
1310
|
+
errors.push({ path: `${path}.autoExposure.${key}`, message: "expected a positive finite exposure multiplier" });
|
|
1311
|
+
failed = true;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (Object.keys(exposure).length > 0)
|
|
1315
|
+
grading.autoExposure = exposure;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (failed)
|
|
1319
|
+
return undefined;
|
|
1320
|
+
return Object.keys(grading).length === 0 ? undefined : grading;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Validates one authored irradiance volume. Probes are bounded (16 volumes, 16³ probes each) so a
|
|
1324
|
+
* scene document cannot turn into an accidental multi-megabyte bake, and every sample must be a
|
|
1325
|
+
* finite number because the renderer writes them straight into SH uniforms.
|
|
1326
|
+
*/
|
|
1327
|
+
function decodeLightProbe(value, path, errors) {
|
|
1328
|
+
if (!isPlainObject(value)) {
|
|
1329
|
+
errors.push({ path, message: "expected an object" });
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
const vec3 = (entry) => {
|
|
1333
|
+
if (!Array.isArray(entry) || entry.length !== 3)
|
|
1334
|
+
return null;
|
|
1335
|
+
if (entry.some((component) => typeof component !== "number" || !Number.isFinite(component)))
|
|
1336
|
+
return null;
|
|
1337
|
+
return [...entry];
|
|
1338
|
+
};
|
|
1339
|
+
let failed = false;
|
|
1340
|
+
const id = value.id;
|
|
1341
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1342
|
+
errors.push({ path: `${path}.id`, message: "expected a non-empty string" });
|
|
1343
|
+
failed = true;
|
|
1344
|
+
}
|
|
1345
|
+
const center = vec3(value.center);
|
|
1346
|
+
const size = vec3(value.size);
|
|
1347
|
+
const resolution = vec3(value.resolution);
|
|
1348
|
+
if (center === null) {
|
|
1349
|
+
errors.push({ path: `${path}.center`, message: "expected three finite numbers" });
|
|
1350
|
+
failed = true;
|
|
1351
|
+
}
|
|
1352
|
+
if (size === null || (size !== null && size.some((extent) => extent <= 0))) {
|
|
1353
|
+
errors.push({ path: `${path}.size`, message: "expected three positive extents" });
|
|
1354
|
+
failed = true;
|
|
1355
|
+
}
|
|
1356
|
+
if (resolution === null || (resolution !== null && resolution.some((count) => count < 1 || count > 16 || !Number.isInteger(count)))) {
|
|
1357
|
+
errors.push({ path: `${path}.resolution`, message: "expected 1-16 whole probes per axis" });
|
|
1358
|
+
failed = true;
|
|
1359
|
+
}
|
|
1360
|
+
let samples;
|
|
1361
|
+
if (value.samples !== undefined) {
|
|
1362
|
+
const expected = resolution === null ? 0 : PROBE_STRIDE * resolution[0] * resolution[1] * resolution[2];
|
|
1363
|
+
if (!Array.isArray(value.samples) || value.samples.length !== expected || value.samples.some((sample) => typeof sample !== "number" || !Number.isFinite(sample))) {
|
|
1364
|
+
errors.push({ path: `${path}.samples`, message: `expected ${expected} finite numbers for this resolution` });
|
|
1365
|
+
failed = true;
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
samples = [...value.samples];
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
if (failed || center === null || size === null || resolution === null || typeof id !== "string")
|
|
1372
|
+
return undefined;
|
|
1373
|
+
const bake = isPlainObject(value.bake) ? decodeLightProbeBake(value.bake) : undefined;
|
|
1374
|
+
return {
|
|
1375
|
+
id,
|
|
1376
|
+
center,
|
|
1377
|
+
size,
|
|
1378
|
+
resolution,
|
|
1379
|
+
...(samples === undefined ? {} : { samples }),
|
|
1380
|
+
...(typeof value.label === "string" ? { label: value.label } : {}),
|
|
1381
|
+
...(bake === undefined ? {} : { bake }),
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function decodeLightProbeBake(value) {
|
|
1385
|
+
if (typeof value.skyVisibility !== "number" || !Number.isFinite(value.skyVisibility))
|
|
1386
|
+
return undefined;
|
|
1387
|
+
const color = (entry) => (typeof entry === "string" ? entry : undefined);
|
|
1388
|
+
const lights = Array.isArray(value.lights)
|
|
1389
|
+
? value.lights.flatMap((light) => {
|
|
1390
|
+
if (!isPlainObject(light) || !Array.isArray(light.position) || light.position.length !== 3)
|
|
1391
|
+
return [];
|
|
1392
|
+
if (light.position.some((n) => typeof n !== "number" || !Number.isFinite(n)))
|
|
1393
|
+
return [];
|
|
1394
|
+
return [{
|
|
1395
|
+
position: [...light.position],
|
|
1396
|
+
...(color(light.color) === undefined ? {} : { color: color(light.color) }),
|
|
1397
|
+
...(typeof light.intensity === "number" && Number.isFinite(light.intensity) ? { intensity: light.intensity } : {}),
|
|
1398
|
+
...(typeof light.distance === "number" && Number.isFinite(light.distance) ? { distance: light.distance } : {}),
|
|
1399
|
+
}];
|
|
1400
|
+
})
|
|
1401
|
+
: undefined;
|
|
1402
|
+
return {
|
|
1403
|
+
skyVisibility: Math.max(0, Math.min(1, value.skyVisibility)),
|
|
1404
|
+
...(color(value.skyColor) === undefined ? {} : { skyColor: color(value.skyColor) }),
|
|
1405
|
+
...(color(value.groundColor) === undefined ? {} : { groundColor: color(value.groundColor) }),
|
|
1406
|
+
...(color(value.ceilingColor) === undefined ? {} : { ceilingColor: color(value.ceilingColor) }),
|
|
1407
|
+
...(color(value.floorColor) === undefined ? {} : { floorColor: color(value.floorColor) }),
|
|
1408
|
+
...(typeof value.fillIntensity === "number" && Number.isFinite(value.fillIntensity) ? { fillIntensity: value.fillIntensity } : {}),
|
|
1409
|
+
...(lights === undefined || lights.length === 0 ? {} : { lights }),
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1202
1412
|
const GRID_AXES = new Set(["xz", "xy"]);
|
|
1203
1413
|
function decodeGridPaletteEntry(item, path, errors) {
|
|
1204
1414
|
if (!isPlainObject(item)) {
|
|
@@ -2,7 +2,9 @@ import type { SkyEnvironmentConfig, WorldBounds } from "../world/features.js";
|
|
|
2
2
|
import type { AvoidZone } from "../world/geometry.js";
|
|
3
3
|
import { type ClearanceOptions } from "../world/scatterRegion.js";
|
|
4
4
|
import type { TerraformSnapshot } from "../world/terraform.js";
|
|
5
|
-
import type { EditorDocument, EditorEnvironment } from "./types.js";
|
|
5
|
+
import type { EditorAutoExposure, EditorColorGrading, EditorDocument, EditorEnvironment, EditorLightProbe } from "./types.js";
|
|
6
|
+
import { type IrradianceVolume } from "../render/lightProbes.js";
|
|
7
|
+
import type { LutCube } from "../render/postProcessing.js";
|
|
6
8
|
import type { LightingConfig } from "../game/playableGame.js";
|
|
7
9
|
/**
|
|
8
10
|
* World-authoring convergence (#1018, epic #1006 Phase 4): derive the **coordinate/placement**
|
|
@@ -47,6 +49,51 @@ export declare function skyFromDocument(doc: EditorDocument): SkyEnvironmentConf
|
|
|
47
49
|
* @internal
|
|
48
50
|
*/
|
|
49
51
|
export declare function skyConfigFromEnvironment(env: EditorEnvironment): SkyEnvironmentConfig;
|
|
52
|
+
/**
|
|
53
|
+
* The baked irradiance volumes a scene document authored, flattened for the renderer.
|
|
54
|
+
* @capability light-probes read the scene document's baked irradiance volumes at runtime
|
|
55
|
+
*/
|
|
56
|
+
export declare function lightProbesFromDocument(doc: EditorDocument): readonly IrradianceVolume[];
|
|
57
|
+
/**
|
|
58
|
+
* Bakes an authored grade into a display-space LUT cube. Contrast and saturation work around
|
|
59
|
+
* luma, then each band pulls its share of the frame toward the authored swatch, scaled to the
|
|
60
|
+
* pixel's own brightness so a swatch tints a band instead of painting over it. Pure and
|
|
61
|
+
* deterministic, so the editor's preview and the runtime produce the same bytes.
|
|
62
|
+
* @capability color-grading bake an authored grade into a display-space LUT cube
|
|
63
|
+
*/
|
|
64
|
+
export declare function lutCubeFromGrading(grading: EditorColorGrading): LutCube;
|
|
65
|
+
/** The renderer-facing result of a document's authored grade. */
|
|
66
|
+
export interface DocumentColorGrade {
|
|
67
|
+
/** Display-space LUT baked from the authored bands, saturation and contrast. */
|
|
68
|
+
lut?: LutCube;
|
|
69
|
+
/** Authored clamped auto-exposure range, when the document set one. */
|
|
70
|
+
autoExposure?: EditorAutoExposure;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The colour grade a scene document authored, baked for the renderer. `undefined` when the document
|
|
74
|
+
* authored no grade at all, so an ungraded game keeps its own post chain untouched. The shell folds
|
|
75
|
+
* this into the post config the same way {@link lightProbesFromDocument} feeds the irradiance
|
|
76
|
+
* volumes, so a game's grade is authored data rather than a pass wired in its config.
|
|
77
|
+
* @capability color-grading read the scene document's authored grade at runtime
|
|
78
|
+
*/
|
|
79
|
+
export declare function colorGradeFromDocument(doc: EditorDocument): DocumentColorGrade | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The lamps a bake should include: authored light markers plus the environment's point lights.
|
|
82
|
+
* @capability light-probes fold the document's authored lamps into a probe bake
|
|
83
|
+
*/
|
|
84
|
+
export declare function authoredLightsFromDocument(doc: EditorDocument): readonly {
|
|
85
|
+
position: readonly [number, number, number];
|
|
86
|
+
color?: string;
|
|
87
|
+
intensity?: number;
|
|
88
|
+
distance?: number;
|
|
89
|
+
}[];
|
|
90
|
+
/**
|
|
91
|
+
* Bakes one authored irradiance volume from its own settings and the document's lamps: sky where
|
|
92
|
+
* the region is open, an authored ceiling/floor fill where it is enclosed, and the authored lamps
|
|
93
|
+
* on top. Pure and deterministic, so the editor's Bake button and a script produce the same samples.
|
|
94
|
+
* @capability light-probes bake a scene document's irradiance volume from its authored lamps
|
|
95
|
+
*/
|
|
96
|
+
export declare function bakeLightProbe(probe: EditorLightProbe, doc: EditorDocument): EditorLightProbe;
|
|
50
97
|
/** Converts authored `light` markers into the runtime lighting contract. */
|
|
51
98
|
export declare function lightingFromDocument(doc: EditorDocument): LightingConfig | undefined;
|
|
52
99
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearanceZonesFrom, DEFAULT_CLEARANCE_KINDS } from "../world/scatterRegion.js";
|
|
2
2
|
import { editorDocumentBounds } from "./document.js";
|
|
3
|
+
import { bakeIrradianceVolume } from "../render/lightProbes.js";
|
|
3
4
|
/**
|
|
4
5
|
* Maps the scene document's serializable environment bag onto the runtime sky config consumed by
|
|
5
6
|
* `sky()` / `environment({ sky })`. Returns `undefined` when the document has no environment field
|
|
@@ -36,6 +37,166 @@ export function skyConfigFromEnvironment(env) {
|
|
|
36
37
|
...(env.fog === undefined ? {} : { fog: { ...env.fog } }),
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The baked irradiance volumes a scene document authored, flattened for the renderer.
|
|
42
|
+
* @capability light-probes read the scene document's baked irradiance volumes at runtime
|
|
43
|
+
*/
|
|
44
|
+
export function lightProbesFromDocument(doc) {
|
|
45
|
+
return doc.environment?.probes ?? [];
|
|
46
|
+
}
|
|
47
|
+
const GRADING_LUMA = [0.2126, 0.7152, 0.0722];
|
|
48
|
+
/** Cube sizes outside this bake either alias (2) or stop being a small authored file (16+). */
|
|
49
|
+
const MIN_LUT_SIZE = 2;
|
|
50
|
+
const MAX_LUT_SIZE = 16;
|
|
51
|
+
const DEFAULT_LUT_SIZE = 8;
|
|
52
|
+
function clamp01(value) {
|
|
53
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
54
|
+
}
|
|
55
|
+
/** Parses `#rgb`/`#rrggbb` into 0..1 display RGB; `undefined` when the swatch is not a colour. */
|
|
56
|
+
function displayRgb(hex) {
|
|
57
|
+
const body = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
58
|
+
const expanded = body.length === 3 ? body.split("").map((digit) => digit + digit).join("") : body;
|
|
59
|
+
if (expanded.length !== 6 || !/^[0-9a-f]{6}$/i.test(expanded))
|
|
60
|
+
return undefined;
|
|
61
|
+
const value = Number.parseInt(expanded, 16);
|
|
62
|
+
return [((value >> 16) & 0xff) / 255, ((value >> 8) & 0xff) / 255, (value & 0xff) / 255];
|
|
63
|
+
}
|
|
64
|
+
function luma(rgb) {
|
|
65
|
+
return rgb[0] * GRADING_LUMA[0] + rgb[1] * GRADING_LUMA[1] + rgb[2] * GRADING_LUMA[2];
|
|
66
|
+
}
|
|
67
|
+
function smoothstep(edge0, edge1, x) {
|
|
68
|
+
const t = clamp01((x - edge0) / (edge1 - edge0));
|
|
69
|
+
return t * t * (3 - 2 * t);
|
|
70
|
+
}
|
|
71
|
+
/** True when the authored grade would leave every colour unchanged, so no LUT is worth baking. */
|
|
72
|
+
function gradingIsIdentity(grading) {
|
|
73
|
+
const saturation = grading.saturation ?? 1;
|
|
74
|
+
const contrast = grading.contrast ?? 1;
|
|
75
|
+
const tint = grading.tint ?? 0.45;
|
|
76
|
+
const bands = grading.shadows !== undefined || grading.midtones !== undefined || grading.highlights !== undefined;
|
|
77
|
+
return saturation === 1 && contrast === 1 && (!bands || tint <= 0);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Bakes an authored grade into a display-space LUT cube. Contrast and saturation work around
|
|
81
|
+
* luma, then each band pulls its share of the frame toward the authored swatch, scaled to the
|
|
82
|
+
* pixel's own brightness so a swatch tints a band instead of painting over it. Pure and
|
|
83
|
+
* deterministic, so the editor's preview and the runtime produce the same bytes.
|
|
84
|
+
* @capability color-grading bake an authored grade into a display-space LUT cube
|
|
85
|
+
*/
|
|
86
|
+
export function lutCubeFromGrading(grading) {
|
|
87
|
+
const size = Math.round(Math.min(MAX_LUT_SIZE, Math.max(MIN_LUT_SIZE, grading.size ?? DEFAULT_LUT_SIZE)));
|
|
88
|
+
const saturation = grading.saturation ?? 1;
|
|
89
|
+
const contrast = grading.contrast ?? 1;
|
|
90
|
+
const tint = clamp01(grading.tint ?? 0.45);
|
|
91
|
+
const bands = [
|
|
92
|
+
{ color: grading.shadows === undefined ? undefined : displayRgb(grading.shadows), weight: (l) => 1 - smoothstep(0, 0.5, l) },
|
|
93
|
+
// Band weights partition the frame: shadows fade out by mid-grey, highlights fade in from it, and
|
|
94
|
+
// midtones are whatever is left (`s1 - s2`, which the naive `1 - wS - wH` double-counts).
|
|
95
|
+
{ color: grading.midtones === undefined ? undefined : displayRgb(grading.midtones), weight: (l) => smoothstep(0, 0.5, l) - smoothstep(0.5, 1, l) },
|
|
96
|
+
{ color: grading.highlights === undefined ? undefined : displayRgb(grading.highlights), weight: (l) => smoothstep(0.5, 1, l) },
|
|
97
|
+
].filter((band) => band.color !== undefined);
|
|
98
|
+
const data = new Uint8Array(size * size * size * 3);
|
|
99
|
+
const levels = size - 1;
|
|
100
|
+
for (let blue = 0; blue < size; blue += 1) {
|
|
101
|
+
for (let green = 0; green < size; green += 1) {
|
|
102
|
+
for (let red = 0; red < size; red += 1) {
|
|
103
|
+
const input = [red / levels, green / levels, blue / levels];
|
|
104
|
+
const bandLuma = luma(input);
|
|
105
|
+
const curved = input.map((channel) => clamp01((channel - 0.5) * contrast + 0.5));
|
|
106
|
+
const curvedLuma = luma(curved);
|
|
107
|
+
let color = curved.map((channel) => curvedLuma + (channel - curvedLuma) * saturation);
|
|
108
|
+
for (const band of bands) {
|
|
109
|
+
const weight = clamp01(band.weight(bandLuma));
|
|
110
|
+
if (weight <= 0)
|
|
111
|
+
continue;
|
|
112
|
+
// The swatch at the pixel's brightness: a #2a3a52 shadow tint darkens and cools, it does
|
|
113
|
+
// not raise every shadow to mid-blue.
|
|
114
|
+
const swatchLuma = Math.max(luma(band.color), 1e-3);
|
|
115
|
+
const target = band.color.map((channel) => clamp01((channel * bandLuma) / swatchLuma));
|
|
116
|
+
const amount = tint * weight;
|
|
117
|
+
color = color.map((channel, index) => channel + (target[index] - channel) * amount);
|
|
118
|
+
}
|
|
119
|
+
const offset = (blue * size * size + green * size + red) * 3;
|
|
120
|
+
data[offset] = Math.round(clamp01(color[0]) * 255);
|
|
121
|
+
data[offset + 1] = Math.round(clamp01(color[1]) * 255);
|
|
122
|
+
data[offset + 2] = Math.round(clamp01(color[2]) * 255);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { size, data };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The colour grade a scene document authored, baked for the renderer. `undefined` when the document
|
|
130
|
+
* authored no grade at all, so an ungraded game keeps its own post chain untouched. The shell folds
|
|
131
|
+
* this into the post config the same way {@link lightProbesFromDocument} feeds the irradiance
|
|
132
|
+
* volumes, so a game's grade is authored data rather than a pass wired in its config.
|
|
133
|
+
* @capability color-grading read the scene document's authored grade at runtime
|
|
134
|
+
*/
|
|
135
|
+
export function colorGradeFromDocument(doc) {
|
|
136
|
+
const grading = doc.environment?.grading;
|
|
137
|
+
if (grading === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
const lut = gradingIsIdentity(grading) ? undefined : lutCubeFromGrading(grading);
|
|
140
|
+
if (lut === undefined && grading.autoExposure === undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
return {
|
|
143
|
+
...(lut === undefined ? {} : { lut }),
|
|
144
|
+
...(grading.autoExposure === undefined ? {} : { autoExposure: grading.autoExposure }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The lamps a bake should include: authored light markers plus the environment's point lights.
|
|
149
|
+
* @capability light-probes fold the document's authored lamps into a probe bake
|
|
150
|
+
*/
|
|
151
|
+
export function authoredLightsFromDocument(doc) {
|
|
152
|
+
const lamps = [];
|
|
153
|
+
for (const marker of doc.markers) {
|
|
154
|
+
if (marker.kind !== "light" || marker.position === undefined)
|
|
155
|
+
continue;
|
|
156
|
+
const meta = marker.meta ?? {};
|
|
157
|
+
lamps.push({
|
|
158
|
+
position: [marker.position.x, marker.position.y, marker.position.z],
|
|
159
|
+
...(typeof meta.color === "string" ? { color: meta.color } : {}),
|
|
160
|
+
...(typeof meta.intensity === "number" ? { intensity: meta.intensity } : {}),
|
|
161
|
+
...(typeof meta.range === "number" ? { distance: meta.range } : {}),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
for (const light of doc.environment?.pointLights ?? []) {
|
|
165
|
+
lamps.push({
|
|
166
|
+
position: light.position,
|
|
167
|
+
...(light.color === undefined ? {} : { color: light.color }),
|
|
168
|
+
...(light.intensity === undefined ? {} : { intensity: light.intensity }),
|
|
169
|
+
...(light.distance === undefined ? {} : { distance: light.distance }),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return lamps;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Bakes one authored irradiance volume from its own settings and the document's lamps: sky where
|
|
176
|
+
* the region is open, an authored ceiling/floor fill where it is enclosed, and the authored lamps
|
|
177
|
+
* on top. Pure and deterministic, so the editor's Bake button and a script produce the same samples.
|
|
178
|
+
* @capability light-probes bake a scene document's irradiance volume from its authored lamps
|
|
179
|
+
*/
|
|
180
|
+
export function bakeLightProbe(probe, doc) {
|
|
181
|
+
const bake = probe.bake ?? { skyVisibility: 0 };
|
|
182
|
+
const lamps = bakedLampsFor(probe, doc);
|
|
183
|
+
return {
|
|
184
|
+
...probe,
|
|
185
|
+
samples: bakeIrradianceVolume({
|
|
186
|
+
id: probe.id,
|
|
187
|
+
center: probe.center,
|
|
188
|
+
size: probe.size,
|
|
189
|
+
resolution: probe.resolution,
|
|
190
|
+
settings: { ...bake, lights: lamps },
|
|
191
|
+
}).samples,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** Lamps folded into a probe: its own bake lights when authored, else the document's lamps. */
|
|
195
|
+
function bakedLampsFor(probe, doc) {
|
|
196
|
+
if (probe.bake?.lights !== undefined && probe.bake.lights.length > 0)
|
|
197
|
+
return probe.bake.lights;
|
|
198
|
+
return authoredLightsFromDocument(doc);
|
|
199
|
+
}
|
|
39
200
|
/** Converts authored `light` markers into the runtime lighting contract. */
|
|
40
201
|
export function lightingFromDocument(doc) {
|
|
41
202
|
const points = [];
|
package/dist/editor/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export type { EditorTerrain } from "./types.js";
|
|
|
5
5
|
/** A baked top-down minimap (PNG data URI + world bounds) persisted on the scene document. */
|
|
6
6
|
export type { EditorMinimapBake } from "./types.js";
|
|
7
7
|
/** Scene-document sky/fog/lighting bag edited by the lighting workspace. */
|
|
8
|
-
export type { EditorEnvironment, EditorFogConfig, EditorSkyPreset, PointLightingConfig } from "./types.js";
|
|
8
|
+
export type { EditorEnvironment, EditorFogConfig, EditorLightProbe, EditorSkyPreset, PointLightingConfig } from "./types.js";
|
|
9
|
+
/** Authored per-game colour grade: band tints, saturation/contrast and the clamped exposure range. */
|
|
10
|
+
export type { EditorAutoExposure, EditorColorGrading } from "./types.js";
|
|
9
11
|
/** Declarative HDRI, cube, or gradient environment source used by the shell. */
|
|
10
12
|
export type { EnvironmentSource } from "../render/environment.js";
|
|
11
13
|
/** Per-kind show/hide flags for the editor's layer panel. */
|
|
@@ -64,9 +66,11 @@ export { WORLD_BASE_SHARD_ID, WORLD_MANIFEST_KIND, decodeWorldManifest, exportWo
|
|
|
64
66
|
export type { DecodeWorldManifestResult, SplitWorldOptions, SplitWorldResult, WorldGrid, WorldManifest, WorldManifestShard, WorldQuery, WorldShardBounds, WorldShardResidency, WorldShardResolver, WorldStreamingConfig, } from "./world.js";
|
|
65
67
|
/** Result of {@link decodeEditorDocument}: a typed document, or every diagnostic collected while decoding it. */
|
|
66
68
|
export type { DecodeEditorDocumentResult } from "./document.js";
|
|
67
|
-
export { environmentContentFromDocument, lightingFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
|
|
69
|
+
export { authoredLightsFromDocument, bakeLightProbe, colorGradeFromDocument, environmentContentFromDocument, lightProbesFromDocument, lightingFromDocument, lutCubeFromGrading, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
|
|
68
70
|
/** Coordinate/placement content of an `environment()` world derived from the scene document. */
|
|
69
71
|
export type { EnvironmentContent, EnvironmentContentOptions } from "./environment.js";
|
|
72
|
+
/** A scene document's authored grade, baked for the renderer. */
|
|
73
|
+
export type { DocumentColorGrade } from "./environment.js";
|
|
70
74
|
export { bakeMinimapFromDocument, documentBakeZones } from "./minimap.js";
|
|
71
75
|
/** Editor-side minimap bake from the scene document (footprint + biome-tint zones). */
|
|
72
76
|
export type { DocumentBakeOptions } from "./minimap.js";
|
package/dist/editor/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { ENTITY_CATALOG_ID, entityDefinitionSchema, entityEntryFromCatalog, reso
|
|
|
3
3
|
export { WELL_KNOWN_MARKER_KINDS, WELL_KNOWN_PATH_KINDS, WELL_KNOWN_VOLUME_KINDS, } from "./types.js";
|
|
4
4
|
export { applyEditorDocumentOverlay, cloneEditorDocument, collectDescendants, createEmptyEditorDocument, createPrefabFragment, decodeEditorDocument, editorChildren, editorDocumentBounds, editorDocumentExtras, editorDocumentSize, editorMarkerPosition, editorMarkerXZ, editorParentOf, editorRoots, exportEditorDocumentJson, extractEditorFragment, findEditorCatalog, findEditorCatalogEntry, findEditorCollection, findEditorMarker, findEditorNote, findEditorPath, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectCollectionLocked, isEditorObjectHidden, isEditorObjectLocked, isEditorObjectSelfLocked, listEditorKinds, mergeEditorDocuments, normalizeEditorLayers, requireEditorMarker, seedEditorCatalogs, wouldCreateCycle, } from "./document.js";
|
|
5
5
|
export { WORLD_BASE_SHARD_ID, WORLD_MANIFEST_KIND, decodeWorldManifest, exportWorldManifestJson, loadWorldDocument, selectWorldShards, shardMatchesQuery, singleShardWorldManifest, splitEditorDocumentIntoShards, } from "./world.js";
|
|
6
|
-
export { environmentContentFromDocument, lightingFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
|
|
6
|
+
export { authoredLightsFromDocument, bakeLightProbe, colorGradeFromDocument, environmentContentFromDocument, lightProbesFromDocument, lightingFromDocument, lutCubeFromGrading, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
|
|
7
7
|
export { bakeMinimapFromDocument, documentBakeZones } from "./minimap.js";
|
|
8
8
|
export { createWorldStreamer, shardDistance } from "./streamer.js";
|
|
9
9
|
export { applyDirectiveOverlay, materializeDirective, materializeDirectives, materializePopulationDirective, materializeScatterDirective, resolveDirectiveFootprint, } from "./directives.js";
|