pixospritz-core 0.10.1 → 1.0.1
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/README.md +36 -286
- package/dist/bundle.js +13 -3
- package/dist/bundle.js.map +1 -1
- package/dist/style.css +1 -0
- package/package.json +43 -44
- package/src/components/WebGLView.jsx +318 -0
- package/src/css/pixos.css +372 -0
- package/src/engine/actions/animate.js +41 -0
- package/src/engine/actions/changezone.js +135 -0
- package/src/engine/actions/chat.js +109 -0
- package/src/engine/actions/dialogue.js +90 -0
- package/src/engine/actions/face.js +22 -0
- package/src/engine/actions/greeting.js +28 -0
- package/src/engine/actions/interact.js +86 -0
- package/src/engine/actions/move.js +67 -0
- package/src/engine/actions/patrol.js +109 -0
- package/src/engine/actions/prompt.js +185 -0
- package/src/engine/actions/script.js +42 -0
- package/src/engine/core/audio/AudioSystem.js +543 -0
- package/src/engine/core/cutscene/PxcPlayer.js +956 -0
- package/src/engine/core/cutscene/manager.js +243 -0
- package/src/engine/core/database/index.js +75 -0
- package/src/engine/core/debug/index.js +371 -0
- package/src/engine/core/hud/index.js +765 -0
- package/src/engine/core/index.js +540 -0
- package/src/engine/core/input/gamepad/Controller.js +71 -0
- package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
- package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
- package/src/engine/core/input/gamepad/index.js +592 -0
- package/src/engine/core/input/keyboard.js +196 -0
- package/src/engine/core/input/manager.js +485 -0
- package/src/engine/core/input/mouse.js +203 -0
- package/src/engine/core/input/touch.js +175 -0
- package/src/engine/core/mode/manager.js +199 -0
- package/src/engine/core/net/manager.js +535 -0
- package/src/engine/core/queue/action.js +83 -0
- package/src/engine/core/queue/event.js +82 -0
- package/src/engine/core/queue/index.js +44 -0
- package/src/engine/core/queue/loadable.js +33 -0
- package/src/engine/core/render/CameraEffects.js +494 -0
- package/src/engine/core/render/FrustumCuller.js +417 -0
- package/src/engine/core/render/LODManager.js +285 -0
- package/src/engine/core/render/ParticleManager.js +529 -0
- package/src/engine/core/render/TextureAtlas.js +465 -0
- package/src/engine/core/render/camera.js +338 -0
- package/src/engine/core/render/light.js +197 -0
- package/src/engine/core/render/manager.js +1079 -0
- package/src/engine/core/render/shaders.js +110 -0
- package/src/engine/core/render/skybox.js +342 -0
- package/src/engine/core/resource/manager.js +133 -0
- package/src/engine/core/resource/object.js +611 -0
- package/src/engine/core/resource/texture.js +103 -0
- package/src/engine/core/resource/tileset.js +177 -0
- package/src/engine/core/scene/avatar.js +215 -0
- package/src/engine/core/scene/speech.js +138 -0
- package/src/engine/core/scene/sprite.js +702 -0
- package/src/engine/core/scene/spritz.js +189 -0
- package/src/engine/core/scene/world.js +681 -0
- package/src/engine/core/scene/zone.js +1167 -0
- package/src/engine/core/store/index.js +110 -0
- package/src/engine/dynamic/animatedSprite.js +64 -0
- package/src/engine/dynamic/animatedTile.js +98 -0
- package/src/engine/dynamic/avatar.js +110 -0
- package/src/engine/dynamic/map.js +174 -0
- package/src/engine/dynamic/sprite.js +255 -0
- package/src/engine/dynamic/spritz.js +119 -0
- package/src/engine/events/EventSystem.js +609 -0
- package/src/engine/events/camera.js +142 -0
- package/src/engine/events/chat.js +75 -0
- package/src/engine/events/menu.js +186 -0
- package/src/engine/scripting/CallbackManager.js +514 -0
- package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
- package/src/engine/scripting/PixoScriptLibrary.js +704 -0
- package/src/engine/shaders/effects/index.js +450 -0
- package/src/engine/shaders/fs.js +222 -0
- package/src/engine/shaders/particles/fs.js +41 -0
- package/src/engine/shaders/particles/vs.js +61 -0
- package/src/engine/shaders/picker/fs.js +34 -0
- package/src/engine/shaders/picker/init.js +62 -0
- package/src/engine/shaders/picker/vs.js +42 -0
- package/src/engine/shaders/pxsl/README.md +250 -0
- package/src/engine/shaders/pxsl/index.js +25 -0
- package/src/engine/shaders/pxsl/library.js +608 -0
- package/src/engine/shaders/pxsl/manager.js +338 -0
- package/src/engine/shaders/pxsl/specification.js +363 -0
- package/src/engine/shaders/pxsl/transpiler.js +753 -0
- package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
- package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
- package/src/engine/shaders/skybox/matrix/fs.js +127 -0
- package/src/engine/shaders/skybox/matrix/vs.js +23 -0
- package/src/engine/shaders/skybox/morning/fs.js +109 -0
- package/src/engine/shaders/skybox/morning/vs.js +23 -0
- package/src/engine/shaders/skybox/neon/fs.js +119 -0
- package/src/engine/shaders/skybox/neon/vs.js +23 -0
- package/src/engine/shaders/skybox/sky/fs.js +114 -0
- package/src/engine/shaders/skybox/sky/vs.js +23 -0
- package/src/engine/shaders/skybox/sunset/fs.js +101 -0
- package/src/engine/shaders/skybox/sunset/vs.js +23 -0
- package/src/engine/shaders/transition/blur/fs.js +42 -0
- package/src/engine/shaders/transition/blur/vs.js +26 -0
- package/src/engine/shaders/transition/cross/fs.js +36 -0
- package/src/engine/shaders/transition/cross/vs.js +26 -0
- package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
- package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
- package/src/engine/shaders/transition/dissolve/fs.js +78 -0
- package/src/engine/shaders/transition/dissolve/vs.js +24 -0
- package/src/engine/shaders/transition/fade/fs.js +31 -0
- package/src/engine/shaders/transition/fade/vs.js +27 -0
- package/src/engine/shaders/transition/iris/fs.js +52 -0
- package/src/engine/shaders/transition/iris/vs.js +24 -0
- package/src/engine/shaders/transition/pixelate/fs.js +44 -0
- package/src/engine/shaders/transition/pixelate/vs.js +24 -0
- package/src/engine/shaders/transition/slide/fs.js +53 -0
- package/src/engine/shaders/transition/slide/vs.js +24 -0
- package/src/engine/shaders/transition/swirl/fs.js +39 -0
- package/src/engine/shaders/transition/swirl/vs.js +26 -0
- package/src/engine/shaders/transition/wipe/fs.js +50 -0
- package/src/engine/shaders/transition/wipe/vs.js +24 -0
- package/src/engine/shaders/vs.js +60 -0
- package/src/engine/utils/CameraController.js +506 -0
- package/src/engine/utils/ObjHelper.js +551 -0
- package/src/engine/utils/debug-logger.js +110 -0
- package/src/engine/utils/enums.js +305 -0
- package/src/engine/utils/generator.js +156 -0
- package/src/engine/utils/index.js +21 -0
- package/src/engine/utils/loaders/ActionLoader.js +77 -0
- package/src/engine/utils/loaders/AudioLoader.js +157 -0
- package/src/engine/utils/loaders/EventLoader.js +66 -0
- package/src/engine/utils/loaders/ObjectLoader.js +67 -0
- package/src/engine/utils/loaders/SpriteLoader.js +77 -0
- package/src/engine/utils/loaders/TilesetLoader.js +103 -0
- package/src/engine/utils/loaders/index.js +21 -0
- package/src/engine/utils/math/matrix4.js +367 -0
- package/src/engine/utils/math/vector.js +458 -0
- package/src/engine/utils/obj/_old_js/index.js +46 -0
- package/src/engine/utils/obj/_old_js/layout.js +308 -0
- package/src/engine/utils/obj/_old_js/material.js +711 -0
- package/src/engine/utils/obj/_old_js/mesh.js +761 -0
- package/src/engine/utils/obj/_old_js/utils.js +647 -0
- package/src/engine/utils/obj/index.js +24 -0
- package/src/engine/utils/obj/js/index.js +277 -0
- package/src/engine/utils/obj/js/loader.js +232 -0
- package/src/engine/utils/obj/layout.js +246 -0
- package/src/engine/utils/obj/material.js +665 -0
- package/src/engine/utils/obj/mesh.js +657 -0
- package/src/engine/utils/obj/ts/index.ts +72 -0
- package/src/engine/utils/obj/ts/layout.ts +265 -0
- package/src/engine/utils/obj/ts/material.ts +760 -0
- package/src/engine/utils/obj/ts/mesh.ts +785 -0
- package/src/engine/utils/obj/ts/utils.ts +501 -0
- package/src/engine/utils/obj/utils.js +428 -0
- package/src/engine/utils/resources.js +18 -0
- package/src/index.jsx +55 -0
- package/src/spritz/player.js +18 -0
- package/src/spritz/readme.md +18 -0
- package/LICENSE +0 -437
- package/dist/bundle.js.LICENSE.txt +0 -31
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/* *\
|
|
2
|
+
** ----------------------------------------------- **
|
|
3
|
+
** Calliope - Pixos Game Engine **
|
|
4
|
+
** ----------------------------------------------- **
|
|
5
|
+
** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
|
|
6
|
+
** **
|
|
7
|
+
** Any unauthorized distribution or transfer **
|
|
8
|
+
** of this work is strictly prohibited. **
|
|
9
|
+
** **
|
|
10
|
+
** All Rights Reserved. **
|
|
11
|
+
** ----------------------------------------------- **
|
|
12
|
+
\* */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* FrustumCuller - Performance optimization for 3D rendering.
|
|
16
|
+
* Determines which objects are visible within the camera's view frustum
|
|
17
|
+
* and culls (skips) objects that are outside the visible area.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Vector } from '../../utils/math/vector.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Represents a plane in 3D space (ax + by + cz + d = 0)
|
|
24
|
+
*/
|
|
25
|
+
class Plane {
|
|
26
|
+
constructor(a = 0, b = 0, c = 0, d = 0) {
|
|
27
|
+
this.a = a;
|
|
28
|
+
this.b = b;
|
|
29
|
+
this.c = c;
|
|
30
|
+
this.d = d;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize the plane equation
|
|
35
|
+
*/
|
|
36
|
+
normalize() {
|
|
37
|
+
const length = Math.sqrt(this.a * this.a + this.b * this.b + this.c * this.c);
|
|
38
|
+
if (length > 0) {
|
|
39
|
+
this.a /= length;
|
|
40
|
+
this.b /= length;
|
|
41
|
+
this.c /= length;
|
|
42
|
+
this.d /= length;
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calculate signed distance from point to plane
|
|
49
|
+
* @param {Vector} point - Point to test
|
|
50
|
+
* @returns {number} Signed distance (positive = in front, negative = behind)
|
|
51
|
+
*/
|
|
52
|
+
distanceToPoint(point) {
|
|
53
|
+
return this.a * point.x + this.b * point.y + this.c * point.z + this.d;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Axis-Aligned Bounding Box
|
|
59
|
+
*/
|
|
60
|
+
class AABB {
|
|
61
|
+
constructor(min = new Vector(0, 0, 0), max = new Vector(0, 0, 0)) {
|
|
62
|
+
this.min = min;
|
|
63
|
+
this.max = max;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the center of the bounding box
|
|
68
|
+
*/
|
|
69
|
+
getCenter() {
|
|
70
|
+
return new Vector(
|
|
71
|
+
(this.min.x + this.max.x) / 2,
|
|
72
|
+
(this.min.y + this.max.y) / 2,
|
|
73
|
+
(this.min.z + this.max.z) / 2
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the half-extents (size from center to edge)
|
|
79
|
+
*/
|
|
80
|
+
getHalfExtents() {
|
|
81
|
+
return new Vector(
|
|
82
|
+
(this.max.x - this.min.x) / 2,
|
|
83
|
+
(this.max.y - this.min.y) / 2,
|
|
84
|
+
(this.max.z - this.min.z) / 2
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create AABB from center and size
|
|
90
|
+
*/
|
|
91
|
+
static fromCenterSize(center, size) {
|
|
92
|
+
const halfSize = new Vector(size.x / 2, size.y / 2, size.z / 2);
|
|
93
|
+
return new AABB(
|
|
94
|
+
new Vector(center.x - halfSize.x, center.y - halfSize.y, center.z - halfSize.z),
|
|
95
|
+
new Vector(center.x + halfSize.x, center.y + halfSize.y, center.z + halfSize.z)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Bounding Sphere for quick culling checks
|
|
102
|
+
*/
|
|
103
|
+
class BoundingSphere {
|
|
104
|
+
constructor(center = new Vector(0, 0, 0), radius = 0) {
|
|
105
|
+
this.center = center;
|
|
106
|
+
this.radius = radius;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create bounding sphere from AABB
|
|
111
|
+
*/
|
|
112
|
+
static fromAABB(aabb) {
|
|
113
|
+
const center = aabb.getCenter();
|
|
114
|
+
const halfExtents = aabb.getHalfExtents();
|
|
115
|
+
const radius = Math.sqrt(
|
|
116
|
+
halfExtents.x * halfExtents.x +
|
|
117
|
+
halfExtents.y * halfExtents.y +
|
|
118
|
+
halfExtents.z * halfExtents.z
|
|
119
|
+
);
|
|
120
|
+
return new BoundingSphere(center, radius);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Frustum defined by 6 planes (left, right, top, bottom, near, far)
|
|
126
|
+
*/
|
|
127
|
+
class Frustum {
|
|
128
|
+
constructor() {
|
|
129
|
+
this.planes = [
|
|
130
|
+
new Plane(), // Left
|
|
131
|
+
new Plane(), // Right
|
|
132
|
+
new Plane(), // Bottom
|
|
133
|
+
new Plane(), // Top
|
|
134
|
+
new Plane(), // Near
|
|
135
|
+
new Plane(), // Far
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract frustum planes from a combined view-projection matrix
|
|
141
|
+
* @param {Float32Array} vpMatrix - 4x4 view-projection matrix (column-major)
|
|
142
|
+
*/
|
|
143
|
+
setFromMatrix(vpMatrix) {
|
|
144
|
+
const m = vpMatrix;
|
|
145
|
+
|
|
146
|
+
// Left plane: row 4 + row 1
|
|
147
|
+
this.planes[0].a = m[3] + m[0];
|
|
148
|
+
this.planes[0].b = m[7] + m[4];
|
|
149
|
+
this.planes[0].c = m[11] + m[8];
|
|
150
|
+
this.planes[0].d = m[15] + m[12];
|
|
151
|
+
this.planes[0].normalize();
|
|
152
|
+
|
|
153
|
+
// Right plane: row 4 - row 1
|
|
154
|
+
this.planes[1].a = m[3] - m[0];
|
|
155
|
+
this.planes[1].b = m[7] - m[4];
|
|
156
|
+
this.planes[1].c = m[11] - m[8];
|
|
157
|
+
this.planes[1].d = m[15] - m[12];
|
|
158
|
+
this.planes[1].normalize();
|
|
159
|
+
|
|
160
|
+
// Bottom plane: row 4 + row 2
|
|
161
|
+
this.planes[2].a = m[3] + m[1];
|
|
162
|
+
this.planes[2].b = m[7] + m[5];
|
|
163
|
+
this.planes[2].c = m[11] + m[9];
|
|
164
|
+
this.planes[2].d = m[15] + m[13];
|
|
165
|
+
this.planes[2].normalize();
|
|
166
|
+
|
|
167
|
+
// Top plane: row 4 - row 2
|
|
168
|
+
this.planes[3].a = m[3] - m[1];
|
|
169
|
+
this.planes[3].b = m[7] - m[5];
|
|
170
|
+
this.planes[3].c = m[11] - m[9];
|
|
171
|
+
this.planes[3].d = m[15] - m[13];
|
|
172
|
+
this.planes[3].normalize();
|
|
173
|
+
|
|
174
|
+
// Near plane: row 4 + row 3
|
|
175
|
+
this.planes[4].a = m[3] + m[2];
|
|
176
|
+
this.planes[4].b = m[7] + m[6];
|
|
177
|
+
this.planes[4].c = m[11] + m[10];
|
|
178
|
+
this.planes[4].d = m[15] + m[14];
|
|
179
|
+
this.planes[4].normalize();
|
|
180
|
+
|
|
181
|
+
// Far plane: row 4 - row 3
|
|
182
|
+
this.planes[5].a = m[3] - m[2];
|
|
183
|
+
this.planes[5].b = m[7] - m[6];
|
|
184
|
+
this.planes[5].c = m[11] - m[10];
|
|
185
|
+
this.planes[5].d = m[15] - m[14];
|
|
186
|
+
this.planes[5].normalize();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Test if a point is inside the frustum
|
|
191
|
+
* @param {Vector} point
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
containsPoint(point) {
|
|
195
|
+
for (const plane of this.planes) {
|
|
196
|
+
if (plane.distanceToPoint(point) < 0) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Test if a sphere intersects or is inside the frustum
|
|
205
|
+
* @param {BoundingSphere} sphere
|
|
206
|
+
* @returns {'inside'|'intersect'|'outside'}
|
|
207
|
+
*/
|
|
208
|
+
testSphere(sphere) {
|
|
209
|
+
let allInside = true;
|
|
210
|
+
|
|
211
|
+
for (const plane of this.planes) {
|
|
212
|
+
const distance = plane.distanceToPoint(sphere.center);
|
|
213
|
+
|
|
214
|
+
if (distance < -sphere.radius) {
|
|
215
|
+
return 'outside';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (distance < sphere.radius) {
|
|
219
|
+
allInside = false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return allInside ? 'inside' : 'intersect';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Test if an AABB intersects or is inside the frustum
|
|
228
|
+
* @param {AABB} aabb
|
|
229
|
+
* @returns {'inside'|'intersect'|'outside'}
|
|
230
|
+
*/
|
|
231
|
+
testAABB(aabb) {
|
|
232
|
+
let allInside = true;
|
|
233
|
+
|
|
234
|
+
for (const plane of this.planes) {
|
|
235
|
+
// Find the positive vertex (furthest in direction of plane normal)
|
|
236
|
+
const px = plane.a >= 0 ? aabb.max.x : aabb.min.x;
|
|
237
|
+
const py = plane.b >= 0 ? aabb.max.y : aabb.min.y;
|
|
238
|
+
const pz = plane.c >= 0 ? aabb.max.z : aabb.min.z;
|
|
239
|
+
|
|
240
|
+
// Find the negative vertex (furthest in opposite direction)
|
|
241
|
+
const nx = plane.a >= 0 ? aabb.min.x : aabb.max.x;
|
|
242
|
+
const ny = plane.b >= 0 ? aabb.min.y : aabb.max.y;
|
|
243
|
+
const nz = plane.c >= 0 ? aabb.min.z : aabb.max.z;
|
|
244
|
+
|
|
245
|
+
// If positive vertex is behind plane, AABB is outside
|
|
246
|
+
if (plane.a * px + plane.b * py + plane.c * pz + plane.d < 0) {
|
|
247
|
+
return 'outside';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If negative vertex is behind plane, AABB is intersecting
|
|
251
|
+
if (plane.a * nx + plane.b * ny + plane.c * nz + plane.d < 0) {
|
|
252
|
+
allInside = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return allInside ? 'inside' : 'intersect';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* FrustumCuller - Main class for frustum culling operations
|
|
262
|
+
*/
|
|
263
|
+
export default class FrustumCuller {
|
|
264
|
+
/**
|
|
265
|
+
* @param {Object} renderManager - Reference to the render manager
|
|
266
|
+
*/
|
|
267
|
+
constructor(renderManager) {
|
|
268
|
+
this.renderManager = renderManager;
|
|
269
|
+
this.frustum = new Frustum();
|
|
270
|
+
this.enabled = true;
|
|
271
|
+
this.debug = {
|
|
272
|
+
totalObjects: 0,
|
|
273
|
+
culledObjects: 0,
|
|
274
|
+
visibleObjects: 0,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Update the frustum from the current view-projection matrix
|
|
280
|
+
* @param {Float32Array} projMatrix - Projection matrix
|
|
281
|
+
* @param {Float32Array} viewMatrix - View matrix
|
|
282
|
+
*/
|
|
283
|
+
update(projMatrix, viewMatrix) {
|
|
284
|
+
if (!this.enabled) return;
|
|
285
|
+
|
|
286
|
+
// Combine view and projection matrices
|
|
287
|
+
const vpMatrix = this._multiplyMatrices(projMatrix, viewMatrix);
|
|
288
|
+
this.frustum.setFromMatrix(vpMatrix);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Cull an array of objects, returning only visible ones
|
|
293
|
+
* @param {Array} objects - Array of objects to cull
|
|
294
|
+
* @param {Function} getBounds - Function to get bounds from object (returns AABB or BoundingSphere)
|
|
295
|
+
* @returns {Array} Visible objects
|
|
296
|
+
*/
|
|
297
|
+
cull(objects, getBounds) {
|
|
298
|
+
if (!this.enabled) {
|
|
299
|
+
this.debug.totalObjects = objects.length;
|
|
300
|
+
this.debug.visibleObjects = objects.length;
|
|
301
|
+
this.debug.culledObjects = 0;
|
|
302
|
+
return objects;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const visible = [];
|
|
306
|
+
this.debug.totalObjects = objects.length;
|
|
307
|
+
|
|
308
|
+
for (const obj of objects) {
|
|
309
|
+
const bounds = getBounds(obj);
|
|
310
|
+
|
|
311
|
+
if (!bounds) {
|
|
312
|
+
// No bounds = always visible
|
|
313
|
+
visible.push(obj);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let result;
|
|
318
|
+
if (bounds instanceof BoundingSphere) {
|
|
319
|
+
result = this.frustum.testSphere(bounds);
|
|
320
|
+
} else if (bounds instanceof AABB) {
|
|
321
|
+
result = this.frustum.testAABB(bounds);
|
|
322
|
+
} else {
|
|
323
|
+
// Unknown bounds type, assume visible
|
|
324
|
+
visible.push(obj);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (result !== 'outside') {
|
|
329
|
+
visible.push(obj);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.debug.visibleObjects = visible.length;
|
|
334
|
+
this.debug.culledObjects = objects.length - visible.length;
|
|
335
|
+
|
|
336
|
+
return visible;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Quick visibility check for a single point
|
|
341
|
+
* @param {Vector} point
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
isPointVisible(point) {
|
|
345
|
+
if (!this.enabled) return true;
|
|
346
|
+
return this.frustum.containsPoint(point);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Quick visibility check for a sphere
|
|
351
|
+
* @param {Vector} center - Sphere center
|
|
352
|
+
* @param {number} radius - Sphere radius
|
|
353
|
+
* @returns {boolean}
|
|
354
|
+
*/
|
|
355
|
+
isSphereVisible(center, radius) {
|
|
356
|
+
if (!this.enabled) return true;
|
|
357
|
+
const sphere = new BoundingSphere(center, radius);
|
|
358
|
+
return this.frustum.testSphere(sphere) !== 'outside';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Quick visibility check for an AABB
|
|
363
|
+
* @param {Vector} min - Min corner
|
|
364
|
+
* @param {Vector} max - Max corner
|
|
365
|
+
* @returns {boolean}
|
|
366
|
+
*/
|
|
367
|
+
isAABBVisible(min, max) {
|
|
368
|
+
if (!this.enabled) return true;
|
|
369
|
+
const aabb = new AABB(min, max);
|
|
370
|
+
return this.frustum.testAABB(aabb) !== 'outside';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Enable/disable frustum culling
|
|
375
|
+
* @param {boolean} enabled
|
|
376
|
+
*/
|
|
377
|
+
setEnabled(enabled) {
|
|
378
|
+
this.enabled = enabled;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get culling statistics
|
|
383
|
+
* @returns {Object}
|
|
384
|
+
*/
|
|
385
|
+
getStats() {
|
|
386
|
+
const cullRate = this.debug.totalObjects > 0
|
|
387
|
+
? (this.debug.culledObjects / this.debug.totalObjects * 100).toFixed(1)
|
|
388
|
+
: 0;
|
|
389
|
+
return {
|
|
390
|
+
...this.debug,
|
|
391
|
+
cullRate: `${cullRate}%`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Multiply two 4x4 matrices (column-major)
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
_multiplyMatrices(a, b) {
|
|
400
|
+
const result = new Float32Array(16);
|
|
401
|
+
|
|
402
|
+
for (let i = 0; i < 4; i++) {
|
|
403
|
+
for (let j = 0; j < 4; j++) {
|
|
404
|
+
result[j * 4 + i] =
|
|
405
|
+
a[i] * b[j * 4] +
|
|
406
|
+
a[i + 4] * b[j * 4 + 1] +
|
|
407
|
+
a[i + 8] * b[j * 4 + 2] +
|
|
408
|
+
a[i + 12] * b[j * 4 + 3];
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Export helper classes
|
|
417
|
+
export { Plane, AABB, BoundingSphere, Frustum };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/* *\
|
|
2
|
+
** ----------------------------------------------- **
|
|
3
|
+
** Calliope - Pixos Game Engine **
|
|
4
|
+
** ----------------------------------------------- **
|
|
5
|
+
** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
|
|
6
|
+
** **
|
|
7
|
+
** Any unauthorized distribution or transfer **
|
|
8
|
+
** of this work is strictly prohibited. **
|
|
9
|
+
** **
|
|
10
|
+
** All Rights Reserved. **
|
|
11
|
+
** ----------------------------------------------- **
|
|
12
|
+
\* */
|
|
13
|
+
|
|
14
|
+
import { Vector } from '../../utils/math/vector.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} LODLevel
|
|
18
|
+
* @property {number} distance - Maximum distance for this LOD level.
|
|
19
|
+
* @property {number} detail - Detail factor (0-1, where 1 = full detail).
|
|
20
|
+
* @property {string} [asset] - Optional asset variant for this LOD.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} LODConfig
|
|
25
|
+
* @property {LODLevel[]} levels - Array of LOD levels sorted by distance.
|
|
26
|
+
* @property {number} [hysteresis=0.1] - Prevents rapid LOD switching (10% default).
|
|
27
|
+
* @property {number} [updateInterval=100] - MS between LOD updates.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* LODManager - Level of Detail system for performance optimization.
|
|
32
|
+
* Manages LOD levels for models and sprites based on camera distance.
|
|
33
|
+
* Supports smooth transitions between LOD levels with hysteresis.
|
|
34
|
+
*/
|
|
35
|
+
export default class LODManager {
|
|
36
|
+
/**
|
|
37
|
+
* Creates an instance of LODManager.
|
|
38
|
+
* @param {import('./manager.js').default} renderManager - The render manager instance.
|
|
39
|
+
*/
|
|
40
|
+
constructor(renderManager) {
|
|
41
|
+
/** @type {import('./manager.js').default} */
|
|
42
|
+
this.renderManager = renderManager;
|
|
43
|
+
|
|
44
|
+
/** @type {Map<string, LODConfig>} Entity ID -> LOD config */
|
|
45
|
+
this.lodConfigs = new Map();
|
|
46
|
+
|
|
47
|
+
/** @type {Map<string, number>} Entity ID -> Current LOD index */
|
|
48
|
+
this.currentLOD = new Map();
|
|
49
|
+
|
|
50
|
+
/** @type {number} Last update timestamp */
|
|
51
|
+
this.lastUpdateTime = 0;
|
|
52
|
+
|
|
53
|
+
/** @type {number} Default update interval in ms */
|
|
54
|
+
this.updateInterval = 100;
|
|
55
|
+
|
|
56
|
+
/** @type {LODLevel[]} Default LOD levels */
|
|
57
|
+
this.defaultLevels = [
|
|
58
|
+
{ distance: 10, detail: 1.0 }, // Full detail within 10 units
|
|
59
|
+
{ distance: 25, detail: 0.75 }, // 75% detail 10-25 units
|
|
60
|
+
{ distance: 50, detail: 0.5 }, // 50% detail 25-50 units
|
|
61
|
+
{ distance: 100, detail: 0.25 }, // 25% detail 50-100 units
|
|
62
|
+
{ distance: Infinity, detail: 0.1 } // 10% detail beyond 100 units
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** @type {boolean} Whether LOD is globally enabled */
|
|
66
|
+
this.enabled = true;
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} Debug mode shows LOD level changes */
|
|
69
|
+
this.debug = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Registers an entity with custom LOD configuration.
|
|
74
|
+
* @param {string} entityId - Unique entity identifier.
|
|
75
|
+
* @param {LODConfig} config - LOD configuration.
|
|
76
|
+
*/
|
|
77
|
+
register(entityId, config) {
|
|
78
|
+
const sortedLevels = [...config.levels].sort((a, b) => a.distance - b.distance);
|
|
79
|
+
this.lodConfigs.set(entityId, {
|
|
80
|
+
levels: sortedLevels,
|
|
81
|
+
hysteresis: config.hysteresis ?? 0.1,
|
|
82
|
+
updateInterval: config.updateInterval ?? this.updateInterval
|
|
83
|
+
});
|
|
84
|
+
this.currentLOD.set(entityId, 0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Unregisters an entity from LOD management.
|
|
89
|
+
* @param {string} entityId - Entity to remove.
|
|
90
|
+
*/
|
|
91
|
+
unregister(entityId) {
|
|
92
|
+
this.lodConfigs.delete(entityId);
|
|
93
|
+
this.currentLOD.delete(entityId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets LOD configuration for an entity.
|
|
98
|
+
* @param {string} entityId - Entity identifier.
|
|
99
|
+
* @returns {LODConfig|null} Configuration or null if not registered.
|
|
100
|
+
*/
|
|
101
|
+
getConfig(entityId) {
|
|
102
|
+
return this.lodConfigs.get(entityId) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Calculates the appropriate LOD level for a given distance.
|
|
107
|
+
* @param {LODLevel[]} levels - LOD levels to check.
|
|
108
|
+
* @param {number} distance - Distance from camera.
|
|
109
|
+
* @param {number} currentLevel - Current LOD level index.
|
|
110
|
+
* @param {number} hysteresis - Hysteresis factor.
|
|
111
|
+
* @returns {number} New LOD level index.
|
|
112
|
+
*/
|
|
113
|
+
calculateLODLevel(levels, distance, currentLevel, hysteresis) {
|
|
114
|
+
// Apply hysteresis to prevent rapid switching
|
|
115
|
+
const hysteresisDistance = distance * (1 + hysteresis);
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < levels.length; i++) {
|
|
118
|
+
const level = levels[i];
|
|
119
|
+
|
|
120
|
+
// When switching to higher detail (lower index), use normal distance
|
|
121
|
+
// When switching to lower detail (higher index), use hysteresis distance
|
|
122
|
+
const checkDistance = i < currentLevel ? distance : hysteresisDistance;
|
|
123
|
+
|
|
124
|
+
if (checkDistance <= level.distance) {
|
|
125
|
+
return i;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return levels.length - 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gets the LOD detail factor for an entity at a position.
|
|
134
|
+
* Uses default levels if entity is not registered.
|
|
135
|
+
* @param {string} entityId - Entity identifier.
|
|
136
|
+
* @param {Vector|number[]} entityPosition - Entity world position.
|
|
137
|
+
* @returns {LODLevel} Current LOD level with detail factor.
|
|
138
|
+
*/
|
|
139
|
+
getLOD(entityId, entityPosition) {
|
|
140
|
+
if (!this.enabled) {
|
|
141
|
+
return { distance: 0, detail: 1.0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const camera = this.renderManager.camera;
|
|
145
|
+
const cameraPos = camera.cameraPosition;
|
|
146
|
+
|
|
147
|
+
// Calculate distance from camera
|
|
148
|
+
const pos = entityPosition instanceof Vector ? entityPosition : new Vector(...entityPosition);
|
|
149
|
+
const dx = pos.x - cameraPos.x;
|
|
150
|
+
const dy = pos.y - cameraPos.y;
|
|
151
|
+
const dz = pos.z - cameraPos.z;
|
|
152
|
+
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
153
|
+
|
|
154
|
+
// Get config or use defaults
|
|
155
|
+
const config = this.lodConfigs.get(entityId);
|
|
156
|
+
const levels = config?.levels || this.defaultLevels;
|
|
157
|
+
const hysteresis = config?.hysteresis ?? 0.1;
|
|
158
|
+
|
|
159
|
+
// Get current level
|
|
160
|
+
const currentLevel = this.currentLOD.get(entityId) ?? 0;
|
|
161
|
+
|
|
162
|
+
// Calculate new level with hysteresis
|
|
163
|
+
const newLevel = this.calculateLODLevel(levels, distance, currentLevel, hysteresis);
|
|
164
|
+
|
|
165
|
+
// Update current level
|
|
166
|
+
this.currentLOD.set(entityId, newLevel);
|
|
167
|
+
|
|
168
|
+
if (this.debug && newLevel !== currentLevel) {
|
|
169
|
+
console.log(`[LOD] ${entityId}: Level ${currentLevel} -> ${newLevel} (distance: ${distance.toFixed(1)})`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return levels[newLevel];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Gets the detail factor for an entity (0-1 range).
|
|
177
|
+
* @param {string} entityId - Entity identifier.
|
|
178
|
+
* @param {Vector|number[]} entityPosition - Entity world position.
|
|
179
|
+
* @returns {number} Detail factor (1 = full detail, 0 = minimum detail).
|
|
180
|
+
*/
|
|
181
|
+
getDetailFactor(entityId, entityPosition) {
|
|
182
|
+
return this.getLOD(entityId, entityPosition).detail;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Checks if an entity should use its high-detail asset.
|
|
187
|
+
* @param {string} entityId - Entity identifier.
|
|
188
|
+
* @param {Vector|number[]} entityPosition - Entity world position.
|
|
189
|
+
* @param {number} [threshold=0.5] - Threshold for high-detail.
|
|
190
|
+
* @returns {boolean} True if high-detail should be used.
|
|
191
|
+
*/
|
|
192
|
+
shouldUseHighDetail(entityId, entityPosition, threshold = 0.5) {
|
|
193
|
+
return this.getDetailFactor(entityId, entityPosition) >= threshold;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Batch update LOD for multiple entities.
|
|
198
|
+
* @param {Array<{id: string, position: Vector|number[]}>} entities - Entities to update.
|
|
199
|
+
* @returns {Map<string, LODLevel>} Map of entity ID to LOD level.
|
|
200
|
+
*/
|
|
201
|
+
batchUpdate(entities) {
|
|
202
|
+
const results = new Map();
|
|
203
|
+
|
|
204
|
+
for (const entity of entities) {
|
|
205
|
+
results.set(entity.id, this.getLOD(entity.id, entity.position));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gets recommended render settings based on LOD level.
|
|
213
|
+
* @param {number} detailFactor - Detail factor (0-1).
|
|
214
|
+
* @returns {object} Render settings.
|
|
215
|
+
*/
|
|
216
|
+
getRenderSettings(detailFactor) {
|
|
217
|
+
return {
|
|
218
|
+
// Skip shadows for low detail objects
|
|
219
|
+
castShadow: detailFactor >= 0.5,
|
|
220
|
+
receiveShadow: detailFactor >= 0.25,
|
|
221
|
+
|
|
222
|
+
// Reduce animation quality for distant objects
|
|
223
|
+
animationQuality: detailFactor >= 0.75 ? 'full' :
|
|
224
|
+
detailFactor >= 0.5 ? 'reduced' : 'minimal',
|
|
225
|
+
|
|
226
|
+
// Skip secondary effects for distant objects
|
|
227
|
+
useSecondaryEffects: detailFactor >= 0.75,
|
|
228
|
+
|
|
229
|
+
// Reduce texture filtering for distant objects
|
|
230
|
+
textureFiltering: detailFactor >= 0.5 ? 'trilinear' : 'bilinear',
|
|
231
|
+
|
|
232
|
+
// Skip normal mapping for very distant objects
|
|
233
|
+
useNormalMap: detailFactor >= 0.5,
|
|
234
|
+
|
|
235
|
+
// Reduce particle count for distant emitters
|
|
236
|
+
particleCountMultiplier: Math.max(0.1, detailFactor),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Sets global LOD bias. Lower values = higher detail at distance.
|
|
242
|
+
* @param {number} bias - Bias multiplier (default 1.0).
|
|
243
|
+
*/
|
|
244
|
+
setBias(bias) {
|
|
245
|
+
// Adjust all default level distances by bias
|
|
246
|
+
this.defaultLevels = this.defaultLevels.map((level, i) => ({
|
|
247
|
+
...level,
|
|
248
|
+
distance: level.distance === Infinity ? Infinity :
|
|
249
|
+
this.defaultLevels[i].distance * bias
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Enables or disables LOD system globally.
|
|
255
|
+
* @param {boolean} enabled - Whether LOD is enabled.
|
|
256
|
+
*/
|
|
257
|
+
setEnabled(enabled) {
|
|
258
|
+
this.enabled = enabled;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Resets all LOD tracking state.
|
|
263
|
+
*/
|
|
264
|
+
reset() {
|
|
265
|
+
this.currentLOD.clear();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Gets statistics about LOD usage.
|
|
270
|
+
* @returns {object} LOD statistics.
|
|
271
|
+
*/
|
|
272
|
+
getStats() {
|
|
273
|
+
const stats = {
|
|
274
|
+
totalEntities: this.currentLOD.size,
|
|
275
|
+
byLevel: new Map(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
for (const [, level] of this.currentLOD) {
|
|
279
|
+
const count = stats.byLevel.get(level) || 0;
|
|
280
|
+
stats.byLevel.set(level, count + 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return stats;
|
|
284
|
+
}
|
|
285
|
+
}
|