@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,355 @@
|
|
|
1
|
+
// Terrain helpers for the shared world module.
|
|
2
|
+
//
|
|
3
|
+
// Terrain in a `WorldData` is a uniform grid heightmap: `width * depth` float
|
|
4
|
+
// heights, row-major with `z*width + x`. This file provides the operations
|
|
5
|
+
// that both the editor (to sculpt / display) and games (to render / sample)
|
|
6
|
+
// need:
|
|
7
|
+
//
|
|
8
|
+
// buildHeightmapMesh — grid floats -> flat vertex/index arrays suitable
|
|
9
|
+
// for `updateSceneNodeGeometry`. Vertex layout matches
|
|
10
|
+
// the scene graph's expected 12-float stride:
|
|
11
|
+
// [x, y, z, nx, ny, nz, r, g, b, a, u, v].
|
|
12
|
+
// sampleHeight — bilinear interpolation at arbitrary world-space xz.
|
|
13
|
+
// Used by character controllers and the place tool
|
|
14
|
+
// to drop entities onto the terrain.
|
|
15
|
+
// raycastTerrain — mouse ray -> terrain hit point + cell indices. Used
|
|
16
|
+
// by the brush tool to find where the user is painting.
|
|
17
|
+
// defaultTerrain — a flat 128x128 terrain centered at origin.
|
|
18
|
+
|
|
19
|
+
import { TerrainData, TerrainLayer, Vec3Lit } from './types';
|
|
20
|
+
|
|
21
|
+
// Vertex stride in floats (matches scene graph expectation).
|
|
22
|
+
// See `bloom/engine/src/scene/index.ts` updateSceneNodeGeometry docs.
|
|
23
|
+
const STRIDE = 12;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// ---- splat mask preview -----------------------------------------------------
|
|
27
|
+
|
|
28
|
+
// Mask colours for the first eight splat layers, used to tint the heightmap
|
|
29
|
+
// mesh's vertex colours so a painted layer is VISIBLE while you paint it.
|
|
30
|
+
//
|
|
31
|
+
// This is a MASK preview, not a material preview: it shows you *where* layer 2
|
|
32
|
+
// is, not what layer 2's texture looks like. A game renders the real textures
|
|
33
|
+
// from `TerrainLayer.textureRef` (the shooter samples them triplanar, blended by
|
|
34
|
+
// exactly these weights); nothing but the editor viewport ever sees these
|
|
35
|
+
// colours. They are ordered to read naturally for the common
|
|
36
|
+
// grass/dry/dirt/rock set, so the preview is not actively misleading.
|
|
37
|
+
const MASK_PALETTE: number[] = [
|
|
38
|
+
0.36, 0.60, 0.24, // 0 — green (lush grass)
|
|
39
|
+
0.72, 0.68, 0.30, // 1 — olive (dry grass)
|
|
40
|
+
0.55, 0.40, 0.26, // 2 — brown (dirt)
|
|
41
|
+
0.58, 0.58, 0.60, // 3 — grey (rock)
|
|
42
|
+
0.80, 0.74, 0.55, // 4 — sand
|
|
43
|
+
0.90, 0.92, 0.95, // 5 — snow
|
|
44
|
+
0.30, 0.34, 0.42, // 6 — slate
|
|
45
|
+
0.62, 0.30, 0.28, // 7 — clay
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Unpainted ground. Also the colour of a terrain with no layers at all, which
|
|
49
|
+
// is every terrain until someone opens the paint tool.
|
|
50
|
+
const BARE_R = 0.55;
|
|
51
|
+
const BARE_G = 0.60;
|
|
52
|
+
const BARE_B = 0.50;
|
|
53
|
+
|
|
54
|
+
/// The mask colour the editor viewport uses for splat layer `i`. Wraps past 8.
|
|
55
|
+
export function terrainLayerMaskColor(i: number): Vec3Lit {
|
|
56
|
+
const k = (i % 8) * 3;
|
|
57
|
+
return [MASK_PALETTE[k], MASK_PALETTE[k + 1], MASK_PALETTE[k + 2]];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Default grid size for new worlds. Large enough for meaningful terrain,
|
|
61
|
+
// small enough to rebuild in <1ms on any modern CPU.
|
|
62
|
+
const DEFAULT_WIDTH = 128;
|
|
63
|
+
const DEFAULT_DEPTH = 128;
|
|
64
|
+
|
|
65
|
+
// Build a flat-shaded triangle mesh from a heightmap. Returns two number
|
|
66
|
+
// arrays that can be passed directly to `updateSceneNodeGeometry`. Computes
|
|
67
|
+
// per-vertex normals from finite differences of the 4 neighbouring heights.
|
|
68
|
+
//
|
|
69
|
+
// Vertex count: width * depth
|
|
70
|
+
// Triangle count: (width - 1) * (depth - 1) * 2
|
|
71
|
+
//
|
|
72
|
+
// For a 128x128 grid that's 16,384 vertices and 32,258 triangles — well
|
|
73
|
+
// within what one scene node can hold.
|
|
74
|
+
export function buildHeightmapMesh(t: TerrainData): { vertices: number[]; indices: number[] } {
|
|
75
|
+
const width = t.width;
|
|
76
|
+
const depth = t.depth;
|
|
77
|
+
const cellSize = t.cellSize;
|
|
78
|
+
const originX = t.origin[0];
|
|
79
|
+
const originY = t.origin[1];
|
|
80
|
+
const originZ = t.origin[2];
|
|
81
|
+
const heights = t.heights;
|
|
82
|
+
|
|
83
|
+
const vertexCount = width * depth;
|
|
84
|
+
const vertices: number[] = new Array<number>(vertexCount * STRIDE);
|
|
85
|
+
const indices: number[] = new Array<number>((width - 1) * (depth - 1) * 6);
|
|
86
|
+
|
|
87
|
+
// Splat preview. Vertex colour = the bare-ground grey, with each painted
|
|
88
|
+
// layer's mask colour mixed in by its weight at this cell. A cell nobody has
|
|
89
|
+
// painted keeps the grey exactly, so an unpainted terrain looks precisely as
|
|
90
|
+
// it did before painting existed.
|
|
91
|
+
//
|
|
92
|
+
// Hoisted out of the vertex loop: `layers` is up to eight parallel arrays and
|
|
93
|
+
// resolving `t.layers[l].weights` 16,384 times is 16,384 property chains.
|
|
94
|
+
const layerCount = t.layers.length;
|
|
95
|
+
const layerWeights = new Array<number[]>(layerCount);
|
|
96
|
+
const layerColor = new Array<Vec3Lit>(layerCount);
|
|
97
|
+
for (let l = 0; l < layerCount; l++) {
|
|
98
|
+
layerWeights[l] = t.layers[l].weights;
|
|
99
|
+
layerColor[l] = terrainLayerMaskColor(l);
|
|
100
|
+
}
|
|
101
|
+
const ca = 1.0;
|
|
102
|
+
|
|
103
|
+
// Vertex pass.
|
|
104
|
+
for (let z = 0; z < depth; z++) {
|
|
105
|
+
for (let x = 0; x < width; x++) {
|
|
106
|
+
const idx = z * width + x;
|
|
107
|
+
const h = heights[idx];
|
|
108
|
+
|
|
109
|
+
let cr = BARE_R;
|
|
110
|
+
let cg = BARE_G;
|
|
111
|
+
let cb = BARE_B;
|
|
112
|
+
for (let l = 0; l < layerCount; l++) {
|
|
113
|
+
const ws = layerWeights[l];
|
|
114
|
+
// A layer whose weights array is short (hand-edited file, or a layer
|
|
115
|
+
// added before the grid was resized) contributes nothing rather than
|
|
116
|
+
// reading undefined and poisoning the whole vertex with NaN.
|
|
117
|
+
if (idx >= ws.length) continue;
|
|
118
|
+
const wgt = ws[idx];
|
|
119
|
+
if (wgt <= 0.0) continue;
|
|
120
|
+
const c = layerColor[l];
|
|
121
|
+
cr = cr + (c[0] - cr) * wgt;
|
|
122
|
+
cg = cg + (c[1] - cg) * wgt;
|
|
123
|
+
cb = cb + (c[2] - cb) * wgt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// World-space position.
|
|
127
|
+
const wx = originX + x * cellSize;
|
|
128
|
+
const wy = originY + h;
|
|
129
|
+
const wz = originZ + z * cellSize;
|
|
130
|
+
|
|
131
|
+
// Finite-difference normal (central differences, clamped at edges).
|
|
132
|
+
const xm = x > 0 ? heights[z * width + (x - 1)] : h;
|
|
133
|
+
const xp = x < width - 1 ? heights[z * width + (x + 1)] : h;
|
|
134
|
+
const zm = z > 0 ? heights[(z - 1) * width + x] : h;
|
|
135
|
+
const zp = z < depth - 1 ? heights[(z + 1) * width + x] : h;
|
|
136
|
+
|
|
137
|
+
const dx = (xp - xm) / (2 * cellSize);
|
|
138
|
+
const dz = (zp - zm) / (2 * cellSize);
|
|
139
|
+
// Normal of y = f(x, z) is (-df/dx, 1, -df/dz), normalized.
|
|
140
|
+
const nx = -dx;
|
|
141
|
+
const ny = 1.0;
|
|
142
|
+
const nz = -dz;
|
|
143
|
+
const invLen = 1.0 / Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
144
|
+
|
|
145
|
+
const base = idx * STRIDE;
|
|
146
|
+
vertices[base + 0] = wx;
|
|
147
|
+
vertices[base + 1] = wy;
|
|
148
|
+
vertices[base + 2] = wz;
|
|
149
|
+
vertices[base + 3] = nx * invLen;
|
|
150
|
+
vertices[base + 4] = ny * invLen;
|
|
151
|
+
vertices[base + 5] = nz * invLen;
|
|
152
|
+
vertices[base + 6] = cr;
|
|
153
|
+
vertices[base + 7] = cg;
|
|
154
|
+
vertices[base + 8] = cb;
|
|
155
|
+
vertices[base + 9] = ca;
|
|
156
|
+
vertices[base + 10] = x / (width - 1);
|
|
157
|
+
vertices[base + 11] = z / (depth - 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Index pass — two triangles per cell, wound counter-clockwise when looking
|
|
162
|
+
// down the -Y axis.
|
|
163
|
+
let idxWrite = 0;
|
|
164
|
+
for (let z = 0; z < depth - 1; z++) {
|
|
165
|
+
for (let x = 0; x < width - 1; x++) {
|
|
166
|
+
const i00 = z * width + x;
|
|
167
|
+
const i10 = i00 + 1;
|
|
168
|
+
const i01 = i00 + width;
|
|
169
|
+
const i11 = i01 + 1;
|
|
170
|
+
|
|
171
|
+
indices[idxWrite++] = i00;
|
|
172
|
+
indices[idxWrite++] = i01;
|
|
173
|
+
indices[idxWrite++] = i11;
|
|
174
|
+
|
|
175
|
+
indices[idxWrite++] = i00;
|
|
176
|
+
indices[idxWrite++] = i11;
|
|
177
|
+
indices[idxWrite++] = i10;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { vertices: vertices, indices: indices };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
// Bilinear sample of the terrain at world-space (wx, wz). Returns the world
|
|
186
|
+
// Y of the surface at that point, including the terrain's origin offset.
|
|
187
|
+
// Points outside the grid clamp to the nearest edge cell.
|
|
188
|
+
//
|
|
189
|
+
// Used by:
|
|
190
|
+
// - character controllers ("what height is the player standing on?")
|
|
191
|
+
// - the place tool (drop an entity onto the terrain surface)
|
|
192
|
+
// - the brush tool (show a cursor ring at the correct height)
|
|
193
|
+
export function sampleHeight(t: TerrainData, wx: number, wz: number): number {
|
|
194
|
+
const fx = (wx - t.origin[0]) / t.cellSize;
|
|
195
|
+
const fz = (wz - t.origin[2]) / t.cellSize;
|
|
196
|
+
|
|
197
|
+
const x0 = clamp(Math.floor(fx), 0, t.width - 1);
|
|
198
|
+
const z0 = clamp(Math.floor(fz), 0, t.depth - 1);
|
|
199
|
+
const x1 = clamp(x0 + 1, 0, t.width - 1);
|
|
200
|
+
const z1 = clamp(z0 + 1, 0, t.depth - 1);
|
|
201
|
+
|
|
202
|
+
const tx = clamp(fx - x0, 0, 1);
|
|
203
|
+
const tz = clamp(fz - z0, 0, 1);
|
|
204
|
+
|
|
205
|
+
const h00 = t.heights[z0 * t.width + x0];
|
|
206
|
+
const h10 = t.heights[z0 * t.width + x1];
|
|
207
|
+
const h01 = t.heights[z1 * t.width + x0];
|
|
208
|
+
const h11 = t.heights[z1 * t.width + x1];
|
|
209
|
+
|
|
210
|
+
const h0 = h00 * (1 - tx) + h10 * tx;
|
|
211
|
+
const h1 = h01 * (1 - tx) + h11 * tx;
|
|
212
|
+
return t.origin[1] + h0 * (1 - tz) + h1 * tz;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TerrainRaycastHit {
|
|
216
|
+
hit: boolean;
|
|
217
|
+
point: Vec3Lit;
|
|
218
|
+
// Grid cell the hit falls into. -1 when there is no hit.
|
|
219
|
+
cellX: number;
|
|
220
|
+
cellZ: number;
|
|
221
|
+
distance: number;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// March a ray through the terrain looking for the first intersection with the
|
|
225
|
+
// surface. This is not a closed-form solution — it's a simple iterative
|
|
226
|
+
// marcher that steps along the ray in small increments and samples the height
|
|
227
|
+
// at each step. Good enough for the brush tool, where the player is always
|
|
228
|
+
// looking at the ground from above.
|
|
229
|
+
//
|
|
230
|
+
// @param origin Ray origin in world space.
|
|
231
|
+
// @param dir Ray direction (should be normalized for accurate distances).
|
|
232
|
+
// @param maxDist Maximum distance to march before giving up.
|
|
233
|
+
// @param step Step size. Smaller = more accurate, larger = faster.
|
|
234
|
+
export function raycastTerrain(
|
|
235
|
+
t: TerrainData,
|
|
236
|
+
origin: Vec3Lit,
|
|
237
|
+
dir: Vec3Lit,
|
|
238
|
+
maxDist: number,
|
|
239
|
+
step: number,
|
|
240
|
+
): TerrainRaycastHit {
|
|
241
|
+
// Early-out: ray must be pointing roughly downward (negative Y component)
|
|
242
|
+
// for the heightmap to be visible from above.
|
|
243
|
+
if (dir[1] >= 0) {
|
|
244
|
+
// Still allow it in case origin is below terrain, but clamp iterations.
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let d = 0;
|
|
248
|
+
let prevAbove = origin[1] > sampleHeight(t, origin[0], origin[2]);
|
|
249
|
+
|
|
250
|
+
while (d < maxDist) {
|
|
251
|
+
const px = origin[0] + dir[0] * d;
|
|
252
|
+
const py = origin[1] + dir[1] * d;
|
|
253
|
+
const pz = origin[2] + dir[2] * d;
|
|
254
|
+
|
|
255
|
+
const h = sampleHeight(t, px, pz);
|
|
256
|
+
const above = py > h;
|
|
257
|
+
|
|
258
|
+
if (above !== prevAbove) {
|
|
259
|
+
// Crossed the surface. Refine with one binary-search step for accuracy.
|
|
260
|
+
let lo = d - step;
|
|
261
|
+
let hi = d;
|
|
262
|
+
for (let i = 0; i < 6; i++) {
|
|
263
|
+
const mid = (lo + hi) * 0.5;
|
|
264
|
+
const mx = origin[0] + dir[0] * mid;
|
|
265
|
+
const my = origin[1] + dir[1] * mid;
|
|
266
|
+
const mz = origin[2] + dir[2] * mid;
|
|
267
|
+
const mh = sampleHeight(t, mx, mz);
|
|
268
|
+
if ((my > mh) === prevAbove) {
|
|
269
|
+
lo = mid;
|
|
270
|
+
} else {
|
|
271
|
+
hi = mid;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const finalD = (lo + hi) * 0.5;
|
|
275
|
+
const hitX = origin[0] + dir[0] * finalD;
|
|
276
|
+
const hitY = origin[1] + dir[1] * finalD;
|
|
277
|
+
const hitZ = origin[2] + dir[2] * finalD;
|
|
278
|
+
const cellX = clamp(Math.floor((hitX - t.origin[0]) / t.cellSize), 0, t.width - 1);
|
|
279
|
+
const cellZ = clamp(Math.floor((hitZ - t.origin[2]) / t.cellSize), 0, t.depth - 1);
|
|
280
|
+
return {
|
|
281
|
+
hit: true,
|
|
282
|
+
point: [hitX, hitY, hitZ],
|
|
283
|
+
cellX: cellX,
|
|
284
|
+
cellZ: cellZ,
|
|
285
|
+
distance: finalD,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
d += step;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { hit: false, point: [0, 0, 0], cellX: -1, cellZ: -1, distance: 0 };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Build a flat 128x128 terrain centered at the origin. Used by the editor's
|
|
296
|
+
// "New World" command when the user chooses a terrain-backed template.
|
|
297
|
+
export function defaultTerrain(): TerrainData {
|
|
298
|
+
const width = DEFAULT_WIDTH;
|
|
299
|
+
const depth = DEFAULT_DEPTH;
|
|
300
|
+
const cellSize = 1.0;
|
|
301
|
+
const heights: number[] = new Array<number>(width * depth);
|
|
302
|
+
for (let i = 0; i < heights.length; i++) heights[i] = 0;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
width: width,
|
|
306
|
+
depth: depth,
|
|
307
|
+
cellSize: cellSize,
|
|
308
|
+
origin: [-(width * cellSize) / 2, 0, -(depth * cellSize) / 2],
|
|
309
|
+
heights: heights,
|
|
310
|
+
layers: [],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// A new, fully-unpainted splat layer sized to `t`'s grid.
|
|
315
|
+
///
|
|
316
|
+
/// Weights start at zero everywhere, which means "this layer is nowhere" — the
|
|
317
|
+
/// terrain keeps whatever it looked like before the layer was added. Adding a
|
|
318
|
+
/// layer is therefore always a no-op until you paint with it, which is the only
|
|
319
|
+
/// behaviour that makes "add layer" a safe button to press.
|
|
320
|
+
export function createTerrainLayer(t: TerrainData, id: string, textureRef: string, tileScale: number): TerrainLayer {
|
|
321
|
+
const n = t.width * t.depth;
|
|
322
|
+
const w = new Array<number>(n);
|
|
323
|
+
for (let i = 0; i < n; i++) w[i] = 0;
|
|
324
|
+
return { id: id, textureRef: textureRef, weights: w, tileScale: tileScale };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Quantize a splat weight for storage.
|
|
328
|
+
///
|
|
329
|
+
/// Weights are consumed as an 8-bit texture, so anything past ~3 decimals is
|
|
330
|
+
/// precision that cannot survive the trip to the GPU — but it CAN survive the
|
|
331
|
+
/// trip to the JSON file, where `0.5019607843137255` costs 18 bytes and there
|
|
332
|
+
/// are `width * depth * layers` of them. A 128² four-layer terrain is 65,536
|
|
333
|
+
/// weights; at full precision that is a megabyte of noise in the diff.
|
|
334
|
+
export function quantizeWeight(w: number): number {
|
|
335
|
+
const c = clamp(w, 0, 1);
|
|
336
|
+
return Math.round(c * 1000) / 1000;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Written with `if` statements, NOT the obvious `v < lo ? lo : (v > hi ? hi : v)`.
|
|
340
|
+
//
|
|
341
|
+
// Perry miscompiles a module-private helper whose body is a single nested-ternary
|
|
342
|
+
// return: called from another function in the same module it evaluates to the
|
|
343
|
+
// FIRST branch regardless of the condition — `clamp(0.5, 0, 1)` returns 0. The
|
|
344
|
+
// same helper written with `if` statements is correct, and so is the same ternary
|
|
345
|
+
// inlined at the call site. See the shooter's docs/perry-quirks.md #8.
|
|
346
|
+
//
|
|
347
|
+
// Pinned by the editor self-test `testSplatPaintPartition` ("weights quantize to
|
|
348
|
+
// 3dp"), which fails outright if this is "simplified" back to a ternary — at
|
|
349
|
+
// which point every splat weight silently becomes 0 and every painted terrain
|
|
350
|
+
// loads unpainted.
|
|
351
|
+
function clamp(v: number, lo: number, hi: number): number {
|
|
352
|
+
if (v < lo) return lo;
|
|
353
|
+
if (v > hi) return hi;
|
|
354
|
+
return v;
|
|
355
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Shared world schema — consumed by the Bloom world editor and by every game
|
|
2
|
+
// that loads `*.world.json` files. All fields are chosen to round-trip cleanly
|
|
3
|
+
// through `JSON.stringify` / `JSON.parse`, so positions, rotations, colors, and
|
|
4
|
+
// matrices are stored as plain number arrays rather than `{x,y,z}` objects.
|
|
5
|
+
// The engine's runtime `Vec3` / `Mat4` / `BoundingBox` types (in `core/types.ts`)
|
|
6
|
+
// use a different shape; the loader in `./loader.ts` does the conversion.
|
|
7
|
+
|
|
8
|
+
// v2 (2026-07-11) — lights became a first-class top-level array. Before that,
|
|
9
|
+
// games carried them as entities tagged `userData.kind === "point_light"`, so
|
|
10
|
+
// only the game knew they were lights: the editor could not light its preview,
|
|
11
|
+
// and every game re-invented the same convention. Sun/ambient/fog were already
|
|
12
|
+
// first-class in `environment`; point lights now sit beside them.
|
|
13
|
+
// `migrateWorldData` converts v1 point_light entities automatically.
|
|
14
|
+
export const WORLD_SCHEMA_VERSION = 2;
|
|
15
|
+
|
|
16
|
+
// Literal types for JSON-friendly serialization.
|
|
17
|
+
// Use `Vec3Lit` in serialized data; convert to engine `Vec3` at load time.
|
|
18
|
+
export type Vec3Lit = [number, number, number];
|
|
19
|
+
export type Vec4Lit = [number, number, number, number];
|
|
20
|
+
|
|
21
|
+
// Column-major 4x4 matrix, same convention as `engine/src/core/types.ts` Mat4.
|
|
22
|
+
// Stored as a flat length-16 number array so it serializes as JSON.
|
|
23
|
+
export type Mat4Lit = number[];
|
|
24
|
+
|
|
25
|
+
// Top-level world document. One `*.world.json` file holds exactly one of these.
|
|
26
|
+
export interface WorldData {
|
|
27
|
+
schemaVersion: number; // Must equal WORLD_SCHEMA_VERSION on save.
|
|
28
|
+
name: string; // Human-readable display name.
|
|
29
|
+
id: string; // Stable slug, e.g. "garden_main".
|
|
30
|
+
bounds: Bounds; // Axis-aligned bounding box in world space.
|
|
31
|
+
environment: EnvironmentData;
|
|
32
|
+
terrain: TerrainData | null; // null for games that don't use terrain.
|
|
33
|
+
entities: EntityData[];
|
|
34
|
+
lights: LightData[]; // Point lights. Sun/ambient live in `environment`.
|
|
35
|
+
water: WaterVolume[];
|
|
36
|
+
rivers: RiverSpline[];
|
|
37
|
+
metadata: Record<string, string>; // Game-specific extensibility (e.g. "gameId").
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A placed light. Only point lights for now — the sun and ambient are
|
|
41
|
+
// environment-wide and live in `EnvironmentData`, and spot lights are not yet
|
|
42
|
+
// in the renderer.
|
|
43
|
+
//
|
|
44
|
+
// Lights are engine-universal (every renderer knows what a point light is),
|
|
45
|
+
// which is why they are schema rather than `userData`: a spawner or a wave plan
|
|
46
|
+
// means nothing without the game, but a light means the same thing everywhere.
|
|
47
|
+
export interface LightData {
|
|
48
|
+
id: string; // Stable within the world file, e.g. "light_0001".
|
|
49
|
+
name: string; // Display name.
|
|
50
|
+
kind: "point";
|
|
51
|
+
position: Vec3Lit;
|
|
52
|
+
color: Vec3Lit; // 0..1 RGB (the runtime API takes 0-255; loader converts).
|
|
53
|
+
intensity: number;
|
|
54
|
+
range: number; // World units; beyond this the light contributes nothing.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Bounds {
|
|
58
|
+
min: Vec3Lit;
|
|
59
|
+
max: Vec3Lit;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sky, lighting, and atmospheric settings applied when the world is loaded.
|
|
63
|
+
export interface EnvironmentData {
|
|
64
|
+
skyColor: Vec3Lit; // 0..1 RGB, used as clear color.
|
|
65
|
+
ambientColor: Vec3Lit; // 0..1 RGB.
|
|
66
|
+
ambientIntensity: number; // 0..1.
|
|
67
|
+
sunDirection: Vec3Lit; // Unit vector pointing from the sun.
|
|
68
|
+
sunColor: Vec3Lit; // 0..1 RGB.
|
|
69
|
+
sunIntensity: number;
|
|
70
|
+
fogStart: number; // World-space distance where fog begins.
|
|
71
|
+
fogEnd: number; // World-space distance of full fog.
|
|
72
|
+
fogColor: Vec3Lit;
|
|
73
|
+
shadowsEnabled: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Heightmap terrain. Row-major grid of float heights, indexed as z*width + x.
|
|
77
|
+
// Runtime consumers build a mesh via `buildHeightmapMesh` in `./terrain.ts`
|
|
78
|
+
// and sample via `sampleHeight` (bilinear).
|
|
79
|
+
export interface TerrainData {
|
|
80
|
+
width: number; // Grid cells along X (e.g. 128).
|
|
81
|
+
depth: number; // Grid cells along Z.
|
|
82
|
+
cellSize: number; // World units per cell, e.g. 1.0.
|
|
83
|
+
origin: Vec3Lit; // World-space position of the (0,0) corner.
|
|
84
|
+
heights: number[]; // Length == width*depth, row-major, z*width + x.
|
|
85
|
+
layers: TerrainLayer[]; // Splat texture layers; empty array if unused.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TerrainLayer {
|
|
89
|
+
id: string; // "grass", "dirt", "rock".
|
|
90
|
+
textureRef: string; // Relative asset path.
|
|
91
|
+
weights: number[]; // Length == width*depth, 0..1 per cell.
|
|
92
|
+
tileScale: number; // UV tiling factor.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A placed instance in the world. Exactly one of `modelRef` / `prefabRef` is
|
|
96
|
+
// non-null; the other is null. The editor enforces this invariant.
|
|
97
|
+
export interface EntityData {
|
|
98
|
+
id: string; // Stable within the world file, e.g. "ent_0001".
|
|
99
|
+
name: string; // Display name; defaults to model basename.
|
|
100
|
+
modelRef: string | null; // Relative path, e.g. "models/tree_oak.glb".
|
|
101
|
+
prefabRef: string | null; // Prefab id, e.g. "small_house".
|
|
102
|
+
transform: TransformData;
|
|
103
|
+
tint: Vec4Lit | null; // Optional per-instance RGBA color override.
|
|
104
|
+
tags: string[]; // Game-defined, e.g. "climbable", "zone_marker".
|
|
105
|
+
userData: Record<string, string>; // Arbitrary game-specific key/value data.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Transform expressed as TRS with Euler rotation for diffable JSON.
|
|
109
|
+
// The loader converts Euler -> quaternion / matrix as needed.
|
|
110
|
+
export interface TransformData {
|
|
111
|
+
position: Vec3Lit;
|
|
112
|
+
rotation: Vec3Lit; // Euler radians, XYZ order.
|
|
113
|
+
scale: Vec3Lit; // Uniform scale is [s, s, s].
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Axis-aligned water volume with a wave-animated surface.
|
|
117
|
+
// M1 supports only `kind: "box"`; future: "mesh" for arbitrary shapes.
|
|
118
|
+
export interface WaterVolume {
|
|
119
|
+
id: string;
|
|
120
|
+
kind: "box";
|
|
121
|
+
center: Vec3Lit;
|
|
122
|
+
size: Vec3Lit; // Full extents (not half-extents).
|
|
123
|
+
surfaceHeight: number; // World Y of the water surface.
|
|
124
|
+
color: Vec4Lit; // RGBA tint.
|
|
125
|
+
waveAmplitude: number;
|
|
126
|
+
waveSpeed: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Catmull-Rom spline river with per-point width.
|
|
130
|
+
export interface RiverSpline {
|
|
131
|
+
id: string;
|
|
132
|
+
controlPoints: Vec3Lit[]; // At least 2 points.
|
|
133
|
+
widths: number[]; // Same length as controlPoints.
|
|
134
|
+
depth: number; // Below the surface.
|
|
135
|
+
flowSpeed: number;
|
|
136
|
+
color: Vec4Lit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- Prefabs ----------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
// A prefab is a reusable composite object saved as its own `*.prefab.json`.
|
|
142
|
+
// Each child references either a raw .glb model or another prefab (nested).
|
|
143
|
+
// Cycles are detected at load time and rejected.
|
|
144
|
+
export interface PrefabData {
|
|
145
|
+
schemaVersion: number; // Must equal WORLD_SCHEMA_VERSION.
|
|
146
|
+
id: string; // Stable slug, e.g. "small_house".
|
|
147
|
+
name: string; // Display name.
|
|
148
|
+
children: PrefabChild[];
|
|
149
|
+
bounds: Bounds; // Cached AABB of the expanded prefab for previews.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One child of a prefab. Exactly one of `modelRef` / `prefabRef` is non-null.
|
|
153
|
+
export interface PrefabChild {
|
|
154
|
+
id: string; // Local id within the prefab, e.g. "wall_0".
|
|
155
|
+
modelRef: string | null;
|
|
156
|
+
prefabRef: string | null; // Reference to another prefab (nested).
|
|
157
|
+
transform: TransformData;
|
|
158
|
+
tint: Vec4Lit | null;
|
|
159
|
+
tags: string[];
|
|
160
|
+
}
|