@bornengine/engine 0.4.16
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 +231 -0
- package/native/android/Cargo.lock +1848 -0
- package/native/android/Cargo.toml +24 -0
- package/native/android/src/lib.rs +702 -0
- package/native/ios/Cargo.lock +1690 -0
- package/native/ios/Cargo.toml +32 -0
- package/native/ios/src/lib.rs +1267 -0
- package/native/linux/Cargo.lock +3279 -0
- package/native/linux/Cargo.toml +29 -0
- package/native/linux/src/lib.rs +1331 -0
- package/native/macos/Cargo.lock +3310 -0
- package/native/macos/Cargo.toml +46 -0
- package/native/macos/src/lib.rs +1302 -0
- package/native/shared/Cargo.lock +1899 -0
- package/native/shared/Cargo.toml +62 -0
- package/native/shared/assets/default_font.ttf +0 -0
- package/native/shared/build.rs +270 -0
- package/native/shared/shaders/common/clouds.wgsl +122 -0
- package/native/shared/shaders/common/fog.wgsl +16 -0
- package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
- package/native/shared/shaders/common/imposter.wgsl +112 -0
- package/native/shared/shaders/common/pbr.wgsl +186 -0
- package/native/shared/shaders/common/shadows.wgsl +186 -0
- package/native/shared/shaders/common/sky.wgsl +8 -0
- package/native/shared/shaders/common/tonemap.wgsl +25 -0
- package/native/shared/shaders/impulse_field.wgsl +57 -0
- package/native/shared/shaders/material_abi.wgsl +383 -0
- package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
- package/native/shared/src/anim_mixer.rs +61 -0
- package/native/shared/src/attach.rs +263 -0
- package/native/shared/src/audio/decode.rs +123 -0
- package/native/shared/src/audio/mod.rs +863 -0
- package/native/shared/src/audio/render.rs +892 -0
- package/native/shared/src/audio/spsc.rs +156 -0
- package/native/shared/src/audio/stream.rs +226 -0
- package/native/shared/src/custom_shaders.rs +104 -0
- package/native/shared/src/decals.rs +245 -0
- package/native/shared/src/drs.rs +211 -0
- package/native/shared/src/engine.rs +261 -0
- package/native/shared/src/ffi.rs +116 -0
- package/native/shared/src/ffi_core/assets.rs +388 -0
- package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
- package/native/shared/src/ffi_core/draw.rs +334 -0
- package/native/shared/src/ffi_core/game_loop.rs +577 -0
- package/native/shared/src/ffi_core/input.rs +234 -0
- package/native/shared/src/ffi_core/mod.rs +127 -0
- package/native/shared/src/ffi_core/models.rs +1154 -0
- package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
- package/native/shared/src/ffi_core/scene.rs +626 -0
- package/native/shared/src/ffi_core/vfx.rs +212 -0
- package/native/shared/src/ffi_core/visual.rs +691 -0
- package/native/shared/src/frame_callbacks.rs +122 -0
- package/native/shared/src/geometry.rs +236 -0
- package/native/shared/src/handles.rs +182 -0
- package/native/shared/src/input.rs +448 -0
- package/native/shared/src/jolt_sys.rs +822 -0
- package/native/shared/src/lib.rs +55 -0
- package/native/shared/src/models.rs +1093 -0
- package/native/shared/src/models_gltf.rs +1280 -0
- package/native/shared/src/particles.rs +391 -0
- package/native/shared/src/physics_jolt.rs +1908 -0
- package/native/shared/src/picking.rs +298 -0
- package/native/shared/src/postfx.rs +345 -0
- package/native/shared/src/profiler.rs +492 -0
- package/native/shared/src/ragdoll.rs +474 -0
- package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
- package/native/shared/src/renderer/brdf_lut.rs +154 -0
- package/native/shared/src/renderer/draw2d.rs +143 -0
- package/native/shared/src/renderer/formats.rs +822 -0
- package/native/shared/src/renderer/froxel.rs +421 -0
- package/native/shared/src/renderer/gi_bake.rs +653 -0
- package/native/shared/src/renderer/graph.rs +462 -0
- package/native/shared/src/renderer/hiz.rs +269 -0
- package/native/shared/src/renderer/hot_reload.rs +390 -0
- package/native/shared/src/renderer/impulse_field.rs +456 -0
- package/native/shared/src/renderer/lighting.rs +154 -0
- package/native/shared/src/renderer/material_instancing.rs +171 -0
- package/native/shared/src/renderer/material_pipeline.rs +700 -0
- package/native/shared/src/renderer/material_system.rs +1996 -0
- package/native/shared/src/renderer/material_system_tests.rs +601 -0
- package/native/shared/src/renderer/material_system_wasm.rs +41 -0
- package/native/shared/src/renderer/mod.rs +12556 -0
- package/native/shared/src/renderer/model_draw.rs +641 -0
- package/native/shared/src/renderer/occlusion.rs +429 -0
- package/native/shared/src/renderer/planar_pass.rs +593 -0
- package/native/shared/src/renderer/planar_reflection.rs +499 -0
- package/native/shared/src/renderer/post_pass.rs +249 -0
- package/native/shared/src/renderer/postfx_chain.rs +728 -0
- package/native/shared/src/renderer/pt_pass.rs +577 -0
- package/native/shared/src/renderer/scene_pass.rs +607 -0
- package/native/shared/src/renderer/shader_include.rs +205 -0
- package/native/shared/src/renderer/shader_library.rs +135 -0
- package/native/shared/src/renderer/shaders/ao.rs +570 -0
- package/native/shared/src/renderer/shaders/core.rs +1243 -0
- package/native/shared/src/renderer/shaders/env.rs +907 -0
- package/native/shared/src/renderer/shaders/gi.rs +810 -0
- package/native/shared/src/renderer/shaders/mod.rs +19 -0
- package/native/shared/src/renderer/shaders/post.rs +1558 -0
- package/native/shared/src/renderer/shaders/pt.rs +1859 -0
- package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
- package/native/shared/src/renderer/shadow_pass.rs +731 -0
- package/native/shared/src/renderer/ssgi_pass.rs +392 -0
- package/native/shared/src/renderer/ssr_pass.rs +188 -0
- package/native/shared/src/renderer/texture_store.rs +473 -0
- package/native/shared/src/renderer/transient.rs +591 -0
- package/native/shared/src/renderer/types.rs +941 -0
- package/native/shared/src/renderer/util.rs +152 -0
- package/native/shared/src/scene.rs +1362 -0
- package/native/shared/src/sdf_cache.rs +274 -0
- package/native/shared/src/shadows.rs +1036 -0
- package/native/shared/src/staging.rs +102 -0
- package/native/shared/src/string_header.rs +266 -0
- package/native/shared/src/text_renderer.rs +502 -0
- package/native/shared/src/textures.rs +197 -0
- package/native/tvos/Cargo.lock +1693 -0
- package/native/tvos/Cargo.toml +36 -0
- package/native/tvos/metal-patched/Cargo.toml +178 -0
- package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
- package/native/tvos/metal-patched/LICENSE-MIT +25 -0
- package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
- package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
- package/native/tvos/metal-patched/src/argument.rs +366 -0
- package/native/tvos/metal-patched/src/blitpass.rs +102 -0
- package/native/tvos/metal-patched/src/buffer.rs +71 -0
- package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
- package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
- package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
- package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
- package/native/tvos/metal-patched/src/computepass.rs +107 -0
- package/native/tvos/metal-patched/src/constants.rs +152 -0
- package/native/tvos/metal-patched/src/counters.rs +119 -0
- package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
- package/native/tvos/metal-patched/src/device.rs +2134 -0
- package/native/tvos/metal-patched/src/drawable.rs +39 -0
- package/native/tvos/metal-patched/src/encoder.rs +2041 -0
- package/native/tvos/metal-patched/src/heap.rs +281 -0
- package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
- package/native/tvos/metal-patched/src/lib.rs +657 -0
- package/native/tvos/metal-patched/src/library.rs +902 -0
- package/native/tvos/metal-patched/src/mps.rs +575 -0
- package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
- package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
- package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
- package/native/tvos/metal-patched/src/renderpass.rs +443 -0
- package/native/tvos/metal-patched/src/resource.rs +182 -0
- package/native/tvos/metal-patched/src/sampler.rs +165 -0
- package/native/tvos/metal-patched/src/sync.rs +178 -0
- package/native/tvos/metal-patched/src/texture.rs +352 -0
- package/native/tvos/metal-patched/src/types.rs +90 -0
- package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
- package/native/tvos/src/audio_backend.rs +197 -0
- package/native/tvos/src/lib.rs +1891 -0
- package/native/visionos/Cargo.lock +1693 -0
- package/native/visionos/Cargo.toml +40 -0
- package/native/visionos/src/audio_backend.rs +197 -0
- package/native/visionos/src/lib.rs +1887 -0
- package/native/watchos/Cargo.lock +16 -0
- package/native/watchos/Cargo.toml +19 -0
- package/native/watchos/shaders/bloom_postfx.metal +99 -0
- package/native/watchos/src/BloomWatchApp.swift +1267 -0
- package/native/watchos/src/BloomWatchAudio.swift +179 -0
- package/native/watchos/src/audio.rs +55 -0
- package/native/watchos/src/draw_list.rs +229 -0
- package/native/watchos/src/ffi_stubs.rs +915 -0
- package/native/watchos/src/ffi_stubs_manual.rs +35 -0
- package/native/watchos/src/lib.rs +1124 -0
- package/native/watchos/src/models.rs +746 -0
- package/native/watchos/src/postfx.rs +95 -0
- package/native/watchos/src/scene.rs +534 -0
- package/native/watchos/src/textures.rs +184 -0
- package/native/web/Cargo.lock +1657 -0
- package/native/web/Cargo.toml +43 -0
- package/native/web/bloom_glue.js +695 -0
- package/native/web/build.sh +131 -0
- package/native/web/index.html +35 -0
- package/native/web/jolt_bridge.js +1519 -0
- package/native/web/src/input_ffi.rs +286 -0
- package/native/web/src/lib.rs +1796 -0
- package/native/web/src/material_ffi.rs +710 -0
- package/native/web/src/parity_ffi.rs +343 -0
- package/native/web/src/physics_ffi.rs +643 -0
- package/native/web/src/ragdoll_ffi.rs +250 -0
- package/native/web/src/render_settings.rs +98 -0
- package/native/windows/Cargo.lock +1815 -0
- package/native/windows/Cargo.toml +68 -0
- package/native/windows/src/lib.rs +1486 -0
- package/package.json +4279 -0
- package/src/audio/index.ts +315 -0
- package/src/core/colors.ts +63 -0
- package/src/core/index.ts +1206 -0
- package/src/core/keys.ts +63 -0
- package/src/core/types.ts +104 -0
- package/src/index.ts +171 -0
- package/src/math/index.ts +516 -0
- package/src/mobile/index.ts +294 -0
- package/src/models/index.ts +1258 -0
- package/src/physics/index.ts +1134 -0
- package/src/scene/index.ts +698 -0
- package/src/shapes/index.ts +120 -0
- package/src/text/index.ts +48 -0
- package/src/textures/index.ts +187 -0
- package/src/vfx/index.ts +191 -0
- package/src/world/index.ts +24 -0
- package/src/world/loader.ts +423 -0
- package/src/world/prefab.ts +217 -0
- package/src/world/render.ts +172 -0
- package/src/world/saver.ts +108 -0
- package/src/world/serialize.ts +301 -0
- package/src/world/terrain.ts +355 -0
- package/src/world/types.ts +160 -0
- package/src/world/validate.ts +319 -0
- package/src/world/version.ts +114 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// World loader — reads a `*.world.json` file from disk, validates it, and
|
|
2
|
+
// spawns scene nodes. Consumed by both the editor (to display the world) and
|
|
3
|
+
// by every Bloom game (to load its level at runtime).
|
|
4
|
+
//
|
|
5
|
+
// The loader has two entry points:
|
|
6
|
+
//
|
|
7
|
+
// loadWorld(path) — pure: reads + parses + migrates + validates.
|
|
8
|
+
// Returns a WorldData object. No scene side-effects.
|
|
9
|
+
//
|
|
10
|
+
// instantiateWorld(w, ctx) — creates scene nodes, applies lighting, and
|
|
11
|
+
// returns a map of entity id -> scene node handle.
|
|
12
|
+
// The caller provides a context that resolves
|
|
13
|
+
// model references to loaded model handles.
|
|
14
|
+
//
|
|
15
|
+
// Splitting parse from instantiate lets the editor hold a `WorldData` in memory
|
|
16
|
+
// and re-sync scene nodes on edits, without re-reading the file every frame.
|
|
17
|
+
|
|
18
|
+
import { readFile } from '../core/index';
|
|
19
|
+
import {
|
|
20
|
+
createSceneNode,
|
|
21
|
+
attachModelToNode,
|
|
22
|
+
setSceneNodeTransform,
|
|
23
|
+
setSceneNodeColor,
|
|
24
|
+
setSceneNodeVisible,
|
|
25
|
+
setSceneNodeParent,
|
|
26
|
+
updateSceneNodeGeometry,
|
|
27
|
+
SceneNodeHandle,
|
|
28
|
+
enableShadows,
|
|
29
|
+
disableShadows,
|
|
30
|
+
addDirectionalLight,
|
|
31
|
+
} from '../scene/index';
|
|
32
|
+
import {
|
|
33
|
+
mat4Identity,
|
|
34
|
+
mat4Translate,
|
|
35
|
+
mat4RotateX,
|
|
36
|
+
mat4RotateY,
|
|
37
|
+
mat4RotateZ,
|
|
38
|
+
mat4Scale,
|
|
39
|
+
} from '../math/index';
|
|
40
|
+
import { Mat4, Vec3 } from '../core/types';
|
|
41
|
+
import { spawnWaterVolume, spawnRiver } from './render';
|
|
42
|
+
import {
|
|
43
|
+
WORLD_SCHEMA_VERSION,
|
|
44
|
+
WorldData,
|
|
45
|
+
EntityData,
|
|
46
|
+
TransformData,
|
|
47
|
+
Vec3Lit,
|
|
48
|
+
Vec4Lit,
|
|
49
|
+
} from './types';
|
|
50
|
+
import { migrateWorldData } from './version';
|
|
51
|
+
import { validateWorld, formatValidationErrors, listUnknownWorldFields } from './validate';
|
|
52
|
+
import {
|
|
53
|
+
PrefabRegistry,
|
|
54
|
+
PrefabLeaf,
|
|
55
|
+
expandPrefab,
|
|
56
|
+
} from './prefab';
|
|
57
|
+
import { buildHeightmapMesh } from './terrain';
|
|
58
|
+
|
|
59
|
+
// Context passed to `instantiateWorld`. The caller is responsible for loading
|
|
60
|
+
// GLB models and supplying handles via `getModelHandle`. This inverts the
|
|
61
|
+
// dependency so games and the editor can use their own asset-caching policy.
|
|
62
|
+
export interface InstantiateContext {
|
|
63
|
+
// Resolve a `modelRef` (e.g. "models/tree_oak.glb") to a loaded model handle.
|
|
64
|
+
// Return 0 if the model is unavailable — the entity is then skipped and a
|
|
65
|
+
// warning is appended to `result.warnings`.
|
|
66
|
+
getModelHandle: (modelRef: string) => number;
|
|
67
|
+
|
|
68
|
+
// Prefab registry for expanding prefab entities. Passing null causes prefab
|
|
69
|
+
// entities to be skipped (with a warning).
|
|
70
|
+
prefabRegistry: PrefabRegistry | null;
|
|
71
|
+
|
|
72
|
+
// Called once per successfully spawned entity. The editor uses this hook to
|
|
73
|
+
// populate its `HandleMap` (EntityId <-> SceneNodeHandle bi-map).
|
|
74
|
+
onEntitySpawned?: (entityId: string, handle: SceneNodeHandle) => void;
|
|
75
|
+
|
|
76
|
+
// Called once for the terrain scene node, if the world has a terrain.
|
|
77
|
+
// The editor uses this to track the terrain handle for brush updates.
|
|
78
|
+
onTerrainSpawned?: (handle: SceneNodeHandle) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface InstantiateResult {
|
|
82
|
+
// One handle per spawned world entity. Prefab entities yield a root group
|
|
83
|
+
// node whose children are the expanded prefab leaves.
|
|
84
|
+
entityHandles: Map<string, SceneNodeHandle>;
|
|
85
|
+
|
|
86
|
+
// Scene node handle for the terrain mesh, or 0 if the world has no terrain.
|
|
87
|
+
terrainHandle: SceneNodeHandle;
|
|
88
|
+
|
|
89
|
+
// Handles for water volumes and rivers, index-aligned with `world.water` and
|
|
90
|
+
// `world.rivers`. Arrays rather than Maps on purpose: Perry 0.5.x miscompiles
|
|
91
|
+
// interfaces that declare more than one Map field (see the editor's
|
|
92
|
+
// docs/perry-map-size-av.md), and this interface already carries one.
|
|
93
|
+
waterHandles: SceneNodeHandle[];
|
|
94
|
+
riverHandles: SceneNodeHandle[];
|
|
95
|
+
|
|
96
|
+
// Non-fatal problems encountered during instantiation: missing models,
|
|
97
|
+
// unresolved prefab references, cycles. The world still instantiates,
|
|
98
|
+
// with the offending entities skipped.
|
|
99
|
+
warnings: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Read, parse, migrate, and validate a world file. Throws on parse error or
|
|
103
|
+
// validation failure — the caller can catch and present the error message.
|
|
104
|
+
export function loadWorld(path: string): WorldData {
|
|
105
|
+
const text = readFile(path);
|
|
106
|
+
if (!text || text.length === 0) {
|
|
107
|
+
throw new Error('loadWorld: file is empty or missing: ' + path);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let raw: WorldData;
|
|
111
|
+
try {
|
|
112
|
+
raw = JSON.parse(text) as WorldData;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
throw new Error('loadWorld: invalid JSON in ' + path + ': ' + (e as Error).message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const migrated = migrateWorldData(raw);
|
|
118
|
+
const check = validateWorld(migrated);
|
|
119
|
+
if (!check.ok) {
|
|
120
|
+
throw new Error('loadWorld: ' + path + '\n' + formatValidationErrors(check.errors));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Unknown fields survive load (JSON.parse keeps them) but NOT save — the
|
|
124
|
+
// schema-explicit saver drops them. Warn loudly rather than lose data
|
|
125
|
+
// silently; game-specific data belongs in metadata/userData/tags.
|
|
126
|
+
// console.error, not console.log: stdout is block-buffered under Perry and
|
|
127
|
+
// lost if the process later crashes.
|
|
128
|
+
const unknown = listUnknownWorldFields(migrated);
|
|
129
|
+
for (let i = 0; i < unknown.length; i++) {
|
|
130
|
+
console.error(
|
|
131
|
+
'loadWorld: WARNING: ' + path + ' contains unknown field "' + unknown[i] +
|
|
132
|
+
'" — not part of schema v' + WORLD_SCHEMA_VERSION +
|
|
133
|
+
'; it will be DROPPED if this file is saved. Extensions belong in metadata/userData/tags.',
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return migrated;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Spawn scene nodes for every entity in the world and apply environment
|
|
141
|
+
// settings (lighting, shadows). Terrain, water volumes, and rivers are spawned
|
|
142
|
+
// when present — water and rivers via the shared helpers in ./render.ts, which
|
|
143
|
+
// the editor uses too so the two never diverge.
|
|
144
|
+
export function instantiateWorld(world: WorldData, ctx: InstantiateContext): InstantiateResult {
|
|
145
|
+
const result: InstantiateResult = {
|
|
146
|
+
entityHandles: new Map<string, SceneNodeHandle>(),
|
|
147
|
+
terrainHandle: 0,
|
|
148
|
+
waterHandles: [],
|
|
149
|
+
riverHandles: [],
|
|
150
|
+
warnings: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Apply environment first so the first frame renders with correct lighting.
|
|
154
|
+
applyEnvironment(world);
|
|
155
|
+
|
|
156
|
+
// Terrain before entities so entities can drop onto it visually.
|
|
157
|
+
if (world.terrain) {
|
|
158
|
+
result.terrainHandle = spawnTerrain(world);
|
|
159
|
+
if (result.terrainHandle !== 0 && ctx.onTerrainSpawned) {
|
|
160
|
+
ctx.onTerrainSpawned(result.terrainHandle);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Spawn each entity. Prefab entities are expanded into a root group node
|
|
165
|
+
// with one child per leaf glb.
|
|
166
|
+
for (let i = 0; i < world.entities.length; i++) {
|
|
167
|
+
const entity = world.entities[i];
|
|
168
|
+
const handle = spawnEntity(entity, ctx, result.warnings);
|
|
169
|
+
if (handle !== 0) {
|
|
170
|
+
result.entityHandles.set(entity.id, handle);
|
|
171
|
+
if (ctx.onEntitySpawned) {
|
|
172
|
+
ctx.onEntitySpawned(entity.id, handle);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Water and rivers render through the shared helpers in ./render.ts, so a
|
|
178
|
+
// game and the editor produce identical geometry and materials.
|
|
179
|
+
for (let i = 0; i < world.water.length; i++) {
|
|
180
|
+
const handle = spawnWaterVolume(world.water[i]);
|
|
181
|
+
result.waterHandles.push(handle);
|
|
182
|
+
if (handle === 0) {
|
|
183
|
+
result.warnings.push('water volume "' + world.water[i].id + '" failed to spawn');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < world.rivers.length; i++) {
|
|
188
|
+
const river = world.rivers[i];
|
|
189
|
+
const handle = spawnRiver(river);
|
|
190
|
+
result.riverHandles.push(handle);
|
|
191
|
+
if (handle === 0) {
|
|
192
|
+
result.warnings.push(
|
|
193
|
+
'river "' + river.id + '" failed to spawn (needs at least 2 control points)',
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build the terrain mesh from the heightmap grid and upload it to a dedicated
|
|
202
|
+
// scene node. Called once per world load; the editor's brush tool re-uploads
|
|
203
|
+
// the mesh on each stroke via `updateSceneNodeGeometry` directly.
|
|
204
|
+
function spawnTerrain(world: WorldData): SceneNodeHandle {
|
|
205
|
+
if (!world.terrain) return 0;
|
|
206
|
+
const mesh = buildHeightmapMesh(world.terrain);
|
|
207
|
+
const node = createSceneNode();
|
|
208
|
+
updateSceneNodeGeometry(node, mesh.vertices, mesh.indices);
|
|
209
|
+
setSceneNodeTransform(node, mat4Identity());
|
|
210
|
+
setSceneNodeVisible(node, true);
|
|
211
|
+
return node;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- entity spawning -------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
function spawnEntity(
|
|
217
|
+
entity: EntityData,
|
|
218
|
+
ctx: InstantiateContext,
|
|
219
|
+
warnings: string[],
|
|
220
|
+
): SceneNodeHandle {
|
|
221
|
+
if (entity.modelRef !== null && entity.modelRef.length > 0) {
|
|
222
|
+
return spawnModelEntity(entity, ctx, warnings);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (entity.prefabRef !== null && entity.prefabRef.length > 0) {
|
|
226
|
+
return spawnPrefabEntity(entity, ctx, warnings);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
warnings.push('entity ' + entity.id + ' has neither modelRef nor prefabRef — skipped');
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function spawnModelEntity(
|
|
234
|
+
entity: EntityData,
|
|
235
|
+
ctx: InstantiateContext,
|
|
236
|
+
warnings: string[],
|
|
237
|
+
): SceneNodeHandle {
|
|
238
|
+
const modelRef = entity.modelRef as string;
|
|
239
|
+
const modelHandle = ctx.getModelHandle(modelRef);
|
|
240
|
+
if (modelHandle === 0) {
|
|
241
|
+
warnings.push('entity ' + entity.id + ' references unknown model "' + modelRef + '" — skipped');
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const node = createSceneNode();
|
|
246
|
+
attachModelToNode(node, modelHandle, 0);
|
|
247
|
+
setSceneNodeTransform(node, trsToMat4(entity.transform));
|
|
248
|
+
|
|
249
|
+
if (entity.tint !== null) {
|
|
250
|
+
applyTint(node, entity.tint);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
setSceneNodeVisible(node, true);
|
|
254
|
+
return node;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function spawnPrefabEntity(
|
|
258
|
+
entity: EntityData,
|
|
259
|
+
ctx: InstantiateContext,
|
|
260
|
+
warnings: string[],
|
|
261
|
+
): SceneNodeHandle {
|
|
262
|
+
if (!ctx.prefabRegistry) {
|
|
263
|
+
warnings.push(
|
|
264
|
+
'entity ' + entity.id + ' references prefab "' + entity.prefabRef + '" but no PrefabRegistry was provided — skipped',
|
|
265
|
+
);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create a root group node at the entity's transform. Expanded leaves are
|
|
270
|
+
// parented to this root so the editor gizmo can move the whole prefab as
|
|
271
|
+
// one unit.
|
|
272
|
+
const root = createSceneNode();
|
|
273
|
+
const rootMatrix = trsToMat4(entity.transform);
|
|
274
|
+
setSceneNodeTransform(root, rootMatrix);
|
|
275
|
+
setSceneNodeVisible(root, true);
|
|
276
|
+
|
|
277
|
+
// Expand the prefab into its leaf glbs and spawn one scene node per leaf.
|
|
278
|
+
// `expandPrefab` accumulates leaves into `leaves` and any errors into a
|
|
279
|
+
// local list which we fold into the top-level warnings.
|
|
280
|
+
const leaves: PrefabLeaf[] = [];
|
|
281
|
+
const expandErrors: string[] = [];
|
|
282
|
+
const visited = new Set<string>();
|
|
283
|
+
expandPrefab(
|
|
284
|
+
ctx.prefabRegistry,
|
|
285
|
+
entity.prefabRef as string,
|
|
286
|
+
mat4Identity(),
|
|
287
|
+
entity.tint,
|
|
288
|
+
entity.tags,
|
|
289
|
+
leaves,
|
|
290
|
+
expandErrors,
|
|
291
|
+
visited,
|
|
292
|
+
'world.entities[' + entity.id + ']',
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
for (let i = 0; i < expandErrors.length; i++) warnings.push(expandErrors[i]);
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
298
|
+
const leaf = leaves[i];
|
|
299
|
+
const modelHandle = ctx.getModelHandle(leaf.modelRef);
|
|
300
|
+
if (modelHandle === 0) {
|
|
301
|
+
warnings.push(
|
|
302
|
+
'prefab leaf ' + leaf.sourcePath + ' references unknown model "' + leaf.modelRef + '" — skipped',
|
|
303
|
+
);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const leafNode = createSceneNode();
|
|
307
|
+
attachModelToNode(leafNode, modelHandle, 0);
|
|
308
|
+
setSceneNodeTransform(leafNode, leaf.worldMatrix);
|
|
309
|
+
if (leaf.tint !== null) applyTint(leafNode, leaf.tint);
|
|
310
|
+
setSceneNodeParent(leafNode, root);
|
|
311
|
+
setSceneNodeVisible(leafNode, true);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return root;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---- environment -----------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
function applyEnvironment(world: WorldData): void {
|
|
320
|
+
const env = world.environment;
|
|
321
|
+
if (!env) return;
|
|
322
|
+
|
|
323
|
+
// Primary directional light (sun). Additional lights can be added by the
|
|
324
|
+
// game/editor through `addDirectionalLight` / `addPointLight` directly.
|
|
325
|
+
addDirectionalLight(
|
|
326
|
+
env.sunDirection[0], env.sunDirection[1], env.sunDirection[2],
|
|
327
|
+
env.sunColor[0] * env.sunIntensity,
|
|
328
|
+
env.sunColor[1] * env.sunIntensity,
|
|
329
|
+
env.sunColor[2] * env.sunIntensity,
|
|
330
|
+
env.sunIntensity,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (env.shadowsEnabled) {
|
|
334
|
+
enableShadows();
|
|
335
|
+
} else {
|
|
336
|
+
disableShadows();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---- TRS -> column-major 4x4 -----------------------------------------------
|
|
341
|
+
|
|
342
|
+
// Build a column-major 4x4 matrix from TRS: M = T * Rz * Ry * Rx * S.
|
|
343
|
+
// The rotation order matches glTF / Blender / most game engines (XYZ Euler,
|
|
344
|
+
// applied right-to-left so X spin happens first in object space).
|
|
345
|
+
export function trsToMat4(t: TransformData): Mat4 {
|
|
346
|
+
const pos: Vec3 = { x: t.position[0], y: t.position[1], z: t.position[2] };
|
|
347
|
+
const scl: Vec3 = { x: t.scale[0], y: t.scale[1], z: t.scale[2] };
|
|
348
|
+
|
|
349
|
+
let m = mat4Identity();
|
|
350
|
+
m = mat4Translate(m, pos);
|
|
351
|
+
m = mat4RotateZ(m, t.rotation[2]);
|
|
352
|
+
m = mat4RotateY(m, t.rotation[1]);
|
|
353
|
+
m = mat4RotateX(m, t.rotation[0]);
|
|
354
|
+
m = mat4Scale(m, scl);
|
|
355
|
+
return m;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Apply an RGBA tint to a scene node. Pulled into a helper so it can be reused
|
|
359
|
+
// by prefab expansion (where children inherit the parent entity's tint unless
|
|
360
|
+
// they override it).
|
|
361
|
+
function applyTint(node: SceneNodeHandle, tint: Vec4Lit): void {
|
|
362
|
+
// World-format tints stay 0-1 floats (serialized data, unchanged);
|
|
363
|
+
// the runtime API takes 0-255.
|
|
364
|
+
setSceneNodeColor(node, tint[0] * 255, tint[1] * 255, tint[2] * 255, tint[3] * 255);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---- factory helpers used by the editor to create new data ----------------
|
|
368
|
+
|
|
369
|
+
// Build an empty world with sensible defaults. The editor calls this from
|
|
370
|
+
// File -> New. Games should prefer `loadWorld` from a file on disk.
|
|
371
|
+
export function createEmptyWorld(id: string, name: string): WorldData {
|
|
372
|
+
return {
|
|
373
|
+
schemaVersion: WORLD_SCHEMA_VERSION,
|
|
374
|
+
name: name,
|
|
375
|
+
id: id,
|
|
376
|
+
bounds: { min: [-50, -10, -50], max: [50, 50, 50] },
|
|
377
|
+
environment: {
|
|
378
|
+
skyColor: [0.53, 0.81, 0.92],
|
|
379
|
+
ambientColor: [1.0, 1.0, 1.0],
|
|
380
|
+
ambientIntensity: 0.35,
|
|
381
|
+
sunDirection: [-0.5, -1.0, -0.3],
|
|
382
|
+
sunColor: [1.0, 0.95, 0.85],
|
|
383
|
+
sunIntensity: 1.0,
|
|
384
|
+
fogStart: 40,
|
|
385
|
+
fogEnd: 120,
|
|
386
|
+
fogColor: [0.7, 0.8, 0.9],
|
|
387
|
+
shadowsEnabled: true,
|
|
388
|
+
},
|
|
389
|
+
terrain: null,
|
|
390
|
+
entities: [],
|
|
391
|
+
lights: [],
|
|
392
|
+
water: [],
|
|
393
|
+
rivers: [],
|
|
394
|
+
metadata: {},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Build an empty entity with default transform. The editor calls this when
|
|
399
|
+
// the place tool drops a model at a hit point.
|
|
400
|
+
export function createEntity(id: string, modelRef: string, position: Vec3Lit): EntityData {
|
|
401
|
+
return {
|
|
402
|
+
id: id,
|
|
403
|
+
name: basename(modelRef),
|
|
404
|
+
modelRef: modelRef,
|
|
405
|
+
prefabRef: null,
|
|
406
|
+
transform: {
|
|
407
|
+
position: position,
|
|
408
|
+
rotation: [0, 0, 0],
|
|
409
|
+
scale: [1, 1, 1],
|
|
410
|
+
},
|
|
411
|
+
tint: null,
|
|
412
|
+
tags: [],
|
|
413
|
+
userData: {},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function basename(path: string): string {
|
|
418
|
+
const slash = path.lastIndexOf('/');
|
|
419
|
+
const dot = path.lastIndexOf('.');
|
|
420
|
+
const start = slash < 0 ? 0 : slash + 1;
|
|
421
|
+
const end = dot < start ? path.length : dot;
|
|
422
|
+
return path.substring(start, end);
|
|
423
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Prefab expansion and registry.
|
|
2
|
+
//
|
|
3
|
+
// A prefab is a reusable composite object — e.g. a house made of 4 walls + a
|
|
4
|
+
// roof + a door. Prefabs are authored in the editor and stored as individual
|
|
5
|
+
// `*.prefab.json` files on disk. When a world places a prefab (via an entity
|
|
6
|
+
// with `prefabRef` set), the runtime expands the prefab tree into a flat list
|
|
7
|
+
// of leaf glb placements with world-space transforms, and the loader spawns
|
|
8
|
+
// one scene node per leaf.
|
|
9
|
+
//
|
|
10
|
+
// Prefabs can nest: one prefab's child can reference another prefab. Cycles
|
|
11
|
+
// are detected at expansion time (A -> B -> A) and reported as errors; the
|
|
12
|
+
// offending child is treated as empty so the rest of the world still loads.
|
|
13
|
+
|
|
14
|
+
import { readFile } from '../core/index';
|
|
15
|
+
import {
|
|
16
|
+
mat4Identity,
|
|
17
|
+
mat4Multiply,
|
|
18
|
+
mat4Translate,
|
|
19
|
+
mat4RotateX,
|
|
20
|
+
mat4RotateY,
|
|
21
|
+
mat4RotateZ,
|
|
22
|
+
mat4Scale,
|
|
23
|
+
} from '../math/index';
|
|
24
|
+
import { Mat4, Vec3 } from '../core/types';
|
|
25
|
+
import {
|
|
26
|
+
WORLD_SCHEMA_VERSION,
|
|
27
|
+
PrefabData,
|
|
28
|
+
PrefabChild,
|
|
29
|
+
TransformData,
|
|
30
|
+
Vec4Lit,
|
|
31
|
+
} from './types';
|
|
32
|
+
import { migratePrefabData } from './version';
|
|
33
|
+
import { validatePrefab, formatValidationErrors, listUnknownPrefabFields } from './validate';
|
|
34
|
+
|
|
35
|
+
// One leaf of an expanded prefab — a single glb to spawn at a world-space
|
|
36
|
+
// matrix. The loader turns each of these into one scene node.
|
|
37
|
+
export interface PrefabLeaf {
|
|
38
|
+
modelRef: string; // Always a glb path; prefabs are fully flattened.
|
|
39
|
+
worldMatrix: Mat4; // Column-major 4x4 in world space.
|
|
40
|
+
tint: Vec4Lit | null; // Inherited from ancestor unless a child overrides.
|
|
41
|
+
tags: string[]; // Union of the leaf's own tags and ancestor tags.
|
|
42
|
+
sourcePath: string; // Dotted path for debugging: "small_house.wall_0".
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Registry of all prefabs known to a project. The editor populates it at
|
|
46
|
+
// startup by scanning `project.prefabsDir`; games populate it from their
|
|
47
|
+
// asset bundle. Keyed by `PrefabData.id`.
|
|
48
|
+
export interface PrefabRegistry {
|
|
49
|
+
byId: Map<string, PrefabData>;
|
|
50
|
+
getPrefab: (id: string) => PrefabData | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createPrefabRegistry(): PrefabRegistry {
|
|
54
|
+
const byId = new Map<string, PrefabData>();
|
|
55
|
+
return {
|
|
56
|
+
byId: byId,
|
|
57
|
+
getPrefab: function(id: string): PrefabData | null {
|
|
58
|
+
const found = byId.get(id);
|
|
59
|
+
return found ? found : null;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Register a prefab in the registry. Overwrites any existing entry with the
|
|
65
|
+
// same id (hot-reload friendly).
|
|
66
|
+
export function registerPrefab(registry: PrefabRegistry, prefab: PrefabData): void {
|
|
67
|
+
registry.byId.set(prefab.id, prefab);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Load a single prefab file from disk, validate, migrate, and return it.
|
|
71
|
+
// Throws on parse or validation error.
|
|
72
|
+
export function loadPrefab(path: string): PrefabData {
|
|
73
|
+
const text = readFile(path);
|
|
74
|
+
if (!text || text.length === 0) {
|
|
75
|
+
throw new Error('loadPrefab: file is empty or missing: ' + path);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let raw: PrefabData;
|
|
79
|
+
try {
|
|
80
|
+
raw = JSON.parse(text) as PrefabData;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
throw new Error('loadPrefab: invalid JSON in ' + path + ': ' + (e as Error).message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const migrated = migratePrefabData(raw);
|
|
86
|
+
const check = validatePrefab(migrated);
|
|
87
|
+
if (!check.ok) {
|
|
88
|
+
throw new Error('loadPrefab: ' + path + '\n' + formatValidationErrors(check.errors));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Same policy as loadWorld: unknown fields don't survive a save, so say so
|
|
92
|
+
// at load time instead of losing them silently.
|
|
93
|
+
const unknown = listUnknownPrefabFields(migrated);
|
|
94
|
+
for (let i = 0; i < unknown.length; i++) {
|
|
95
|
+
console.error(
|
|
96
|
+
'loadPrefab: WARNING: ' + path + ' contains unknown field "' + unknown[i] +
|
|
97
|
+
'" — it will be DROPPED if this prefab is saved.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return migrated;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Recursively flatten a prefab into its leaf glb placements.
|
|
105
|
+
//
|
|
106
|
+
// @param registry Registry to look up child prefab references.
|
|
107
|
+
// @param prefabId Id of the prefab to expand.
|
|
108
|
+
// @param parentMatrix World-space matrix to apply to all children.
|
|
109
|
+
// @param parentTint Tint inherited from the placing entity (null if none).
|
|
110
|
+
// @param parentTags Tags to propagate to each leaf.
|
|
111
|
+
// @param out Output array of flattened leaves. Caller supplies.
|
|
112
|
+
// @param errors Output error list (cycles, missing references).
|
|
113
|
+
// @param visited Set of ancestor prefab ids, used for cycle detection.
|
|
114
|
+
// @param pathPrefix Debug path for error messages, e.g. "world_root".
|
|
115
|
+
export function expandPrefab(
|
|
116
|
+
registry: PrefabRegistry,
|
|
117
|
+
prefabId: string,
|
|
118
|
+
parentMatrix: Mat4,
|
|
119
|
+
parentTint: Vec4Lit | null,
|
|
120
|
+
parentTags: ReadonlyArray<string>,
|
|
121
|
+
out: PrefabLeaf[],
|
|
122
|
+
errors: string[],
|
|
123
|
+
visited: Set<string>,
|
|
124
|
+
pathPrefix: string,
|
|
125
|
+
): void {
|
|
126
|
+
if (visited.has(prefabId)) {
|
|
127
|
+
errors.push(
|
|
128
|
+
'prefab cycle detected: ' + pathPrefix + ' -> ' + prefabId +
|
|
129
|
+
' (already in chain)',
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const prefab = registry.getPrefab(prefabId);
|
|
135
|
+
if (!prefab) {
|
|
136
|
+
errors.push('prefab not found: "' + prefabId + '" (referenced by ' + pathPrefix + ')');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
visited.add(prefabId);
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < prefab.children.length; i++) {
|
|
143
|
+
const child = prefab.children[i];
|
|
144
|
+
const childMatrix = mat4Multiply(parentMatrix, trsToMat4(child.transform));
|
|
145
|
+
const childTint = child.tint !== null ? child.tint : parentTint;
|
|
146
|
+
const childTags = mergeTags(parentTags, child.tags);
|
|
147
|
+
const childPath = pathPrefix + '.' + child.id;
|
|
148
|
+
|
|
149
|
+
if (child.modelRef !== null && child.modelRef.length > 0) {
|
|
150
|
+
out.push({
|
|
151
|
+
modelRef: child.modelRef,
|
|
152
|
+
worldMatrix: childMatrix,
|
|
153
|
+
tint: childTint,
|
|
154
|
+
tags: childTags,
|
|
155
|
+
sourcePath: childPath,
|
|
156
|
+
});
|
|
157
|
+
} else if (child.prefabRef !== null && child.prefabRef.length > 0) {
|
|
158
|
+
expandPrefab(
|
|
159
|
+
registry,
|
|
160
|
+
child.prefabRef,
|
|
161
|
+
childMatrix,
|
|
162
|
+
childTint,
|
|
163
|
+
childTags,
|
|
164
|
+
out,
|
|
165
|
+
errors,
|
|
166
|
+
visited,
|
|
167
|
+
childPath,
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
errors.push('prefab child ' + childPath + ' has neither modelRef nor prefabRef');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
visited.delete(prefabId);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Build a column-major 4x4 from TRS components. Duplicated here (rather than
|
|
178
|
+
// imported from `./loader.ts`) to keep the dependency graph flat — prefab is
|
|
179
|
+
// used *by* the loader, not the other way around.
|
|
180
|
+
function trsToMat4(t: TransformData): Mat4 {
|
|
181
|
+
const pos: Vec3 = { x: t.position[0], y: t.position[1], z: t.position[2] };
|
|
182
|
+
const scl: Vec3 = { x: t.scale[0], y: t.scale[1], z: t.scale[2] };
|
|
183
|
+
|
|
184
|
+
let m = mat4Identity();
|
|
185
|
+
m = mat4Translate(m, pos);
|
|
186
|
+
m = mat4RotateZ(m, t.rotation[2]);
|
|
187
|
+
m = mat4RotateY(m, t.rotation[1]);
|
|
188
|
+
m = mat4RotateX(m, t.rotation[0]);
|
|
189
|
+
m = mat4Scale(m, scl);
|
|
190
|
+
return m;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function mergeTags(parent: ReadonlyArray<string>, child: ReadonlyArray<string>): string[] {
|
|
194
|
+
if (parent.length === 0) return [...child];
|
|
195
|
+
if (child.length === 0) return [...parent];
|
|
196
|
+
const seen = new Set<string>();
|
|
197
|
+
const out: string[] = [];
|
|
198
|
+
for (let i = 0; i < parent.length; i++) {
|
|
199
|
+
if (!seen.has(parent[i])) { seen.add(parent[i]); out.push(parent[i]); }
|
|
200
|
+
}
|
|
201
|
+
for (let i = 0; i < child.length; i++) {
|
|
202
|
+
if (!seen.has(child[i])) { seen.add(child[i]); out.push(child[i]); }
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Build an empty prefab with sensible defaults. Editor calls this from
|
|
208
|
+
// "New Prefab".
|
|
209
|
+
export function createEmptyPrefab(id: string, name: string): PrefabData {
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: WORLD_SCHEMA_VERSION,
|
|
212
|
+
id: id,
|
|
213
|
+
name: name,
|
|
214
|
+
children: [],
|
|
215
|
+
bounds: { min: [0, 0, 0], max: [0, 0, 0] },
|
|
216
|
+
};
|
|
217
|
+
}
|