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.
Files changed (157) hide show
  1. package/README.md +36 -286
  2. package/dist/bundle.js +13 -3
  3. package/dist/bundle.js.map +1 -1
  4. package/dist/style.css +1 -0
  5. package/package.json +43 -44
  6. package/src/components/WebGLView.jsx +318 -0
  7. package/src/css/pixos.css +372 -0
  8. package/src/engine/actions/animate.js +41 -0
  9. package/src/engine/actions/changezone.js +135 -0
  10. package/src/engine/actions/chat.js +109 -0
  11. package/src/engine/actions/dialogue.js +90 -0
  12. package/src/engine/actions/face.js +22 -0
  13. package/src/engine/actions/greeting.js +28 -0
  14. package/src/engine/actions/interact.js +86 -0
  15. package/src/engine/actions/move.js +67 -0
  16. package/src/engine/actions/patrol.js +109 -0
  17. package/src/engine/actions/prompt.js +185 -0
  18. package/src/engine/actions/script.js +42 -0
  19. package/src/engine/core/audio/AudioSystem.js +543 -0
  20. package/src/engine/core/cutscene/PxcPlayer.js +956 -0
  21. package/src/engine/core/cutscene/manager.js +243 -0
  22. package/src/engine/core/database/index.js +75 -0
  23. package/src/engine/core/debug/index.js +371 -0
  24. package/src/engine/core/hud/index.js +765 -0
  25. package/src/engine/core/index.js +540 -0
  26. package/src/engine/core/input/gamepad/Controller.js +71 -0
  27. package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
  28. package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
  29. package/src/engine/core/input/gamepad/index.js +592 -0
  30. package/src/engine/core/input/keyboard.js +196 -0
  31. package/src/engine/core/input/manager.js +485 -0
  32. package/src/engine/core/input/mouse.js +203 -0
  33. package/src/engine/core/input/touch.js +175 -0
  34. package/src/engine/core/mode/manager.js +199 -0
  35. package/src/engine/core/net/manager.js +535 -0
  36. package/src/engine/core/queue/action.js +83 -0
  37. package/src/engine/core/queue/event.js +82 -0
  38. package/src/engine/core/queue/index.js +44 -0
  39. package/src/engine/core/queue/loadable.js +33 -0
  40. package/src/engine/core/render/CameraEffects.js +494 -0
  41. package/src/engine/core/render/FrustumCuller.js +417 -0
  42. package/src/engine/core/render/LODManager.js +285 -0
  43. package/src/engine/core/render/ParticleManager.js +529 -0
  44. package/src/engine/core/render/TextureAtlas.js +465 -0
  45. package/src/engine/core/render/camera.js +338 -0
  46. package/src/engine/core/render/light.js +197 -0
  47. package/src/engine/core/render/manager.js +1079 -0
  48. package/src/engine/core/render/shaders.js +110 -0
  49. package/src/engine/core/render/skybox.js +342 -0
  50. package/src/engine/core/resource/manager.js +133 -0
  51. package/src/engine/core/resource/object.js +611 -0
  52. package/src/engine/core/resource/texture.js +103 -0
  53. package/src/engine/core/resource/tileset.js +177 -0
  54. package/src/engine/core/scene/avatar.js +215 -0
  55. package/src/engine/core/scene/speech.js +138 -0
  56. package/src/engine/core/scene/sprite.js +702 -0
  57. package/src/engine/core/scene/spritz.js +189 -0
  58. package/src/engine/core/scene/world.js +681 -0
  59. package/src/engine/core/scene/zone.js +1167 -0
  60. package/src/engine/core/store/index.js +110 -0
  61. package/src/engine/dynamic/animatedSprite.js +64 -0
  62. package/src/engine/dynamic/animatedTile.js +98 -0
  63. package/src/engine/dynamic/avatar.js +110 -0
  64. package/src/engine/dynamic/map.js +174 -0
  65. package/src/engine/dynamic/sprite.js +255 -0
  66. package/src/engine/dynamic/spritz.js +119 -0
  67. package/src/engine/events/EventSystem.js +609 -0
  68. package/src/engine/events/camera.js +142 -0
  69. package/src/engine/events/chat.js +75 -0
  70. package/src/engine/events/menu.js +186 -0
  71. package/src/engine/scripting/CallbackManager.js +514 -0
  72. package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
  73. package/src/engine/scripting/PixoScriptLibrary.js +704 -0
  74. package/src/engine/shaders/effects/index.js +450 -0
  75. package/src/engine/shaders/fs.js +222 -0
  76. package/src/engine/shaders/particles/fs.js +41 -0
  77. package/src/engine/shaders/particles/vs.js +61 -0
  78. package/src/engine/shaders/picker/fs.js +34 -0
  79. package/src/engine/shaders/picker/init.js +62 -0
  80. package/src/engine/shaders/picker/vs.js +42 -0
  81. package/src/engine/shaders/pxsl/README.md +250 -0
  82. package/src/engine/shaders/pxsl/index.js +25 -0
  83. package/src/engine/shaders/pxsl/library.js +608 -0
  84. package/src/engine/shaders/pxsl/manager.js +338 -0
  85. package/src/engine/shaders/pxsl/specification.js +363 -0
  86. package/src/engine/shaders/pxsl/transpiler.js +753 -0
  87. package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
  88. package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
  89. package/src/engine/shaders/skybox/matrix/fs.js +127 -0
  90. package/src/engine/shaders/skybox/matrix/vs.js +23 -0
  91. package/src/engine/shaders/skybox/morning/fs.js +109 -0
  92. package/src/engine/shaders/skybox/morning/vs.js +23 -0
  93. package/src/engine/shaders/skybox/neon/fs.js +119 -0
  94. package/src/engine/shaders/skybox/neon/vs.js +23 -0
  95. package/src/engine/shaders/skybox/sky/fs.js +114 -0
  96. package/src/engine/shaders/skybox/sky/vs.js +23 -0
  97. package/src/engine/shaders/skybox/sunset/fs.js +101 -0
  98. package/src/engine/shaders/skybox/sunset/vs.js +23 -0
  99. package/src/engine/shaders/transition/blur/fs.js +42 -0
  100. package/src/engine/shaders/transition/blur/vs.js +26 -0
  101. package/src/engine/shaders/transition/cross/fs.js +36 -0
  102. package/src/engine/shaders/transition/cross/vs.js +26 -0
  103. package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
  104. package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
  105. package/src/engine/shaders/transition/dissolve/fs.js +78 -0
  106. package/src/engine/shaders/transition/dissolve/vs.js +24 -0
  107. package/src/engine/shaders/transition/fade/fs.js +31 -0
  108. package/src/engine/shaders/transition/fade/vs.js +27 -0
  109. package/src/engine/shaders/transition/iris/fs.js +52 -0
  110. package/src/engine/shaders/transition/iris/vs.js +24 -0
  111. package/src/engine/shaders/transition/pixelate/fs.js +44 -0
  112. package/src/engine/shaders/transition/pixelate/vs.js +24 -0
  113. package/src/engine/shaders/transition/slide/fs.js +53 -0
  114. package/src/engine/shaders/transition/slide/vs.js +24 -0
  115. package/src/engine/shaders/transition/swirl/fs.js +39 -0
  116. package/src/engine/shaders/transition/swirl/vs.js +26 -0
  117. package/src/engine/shaders/transition/wipe/fs.js +50 -0
  118. package/src/engine/shaders/transition/wipe/vs.js +24 -0
  119. package/src/engine/shaders/vs.js +60 -0
  120. package/src/engine/utils/CameraController.js +506 -0
  121. package/src/engine/utils/ObjHelper.js +551 -0
  122. package/src/engine/utils/debug-logger.js +110 -0
  123. package/src/engine/utils/enums.js +305 -0
  124. package/src/engine/utils/generator.js +156 -0
  125. package/src/engine/utils/index.js +21 -0
  126. package/src/engine/utils/loaders/ActionLoader.js +77 -0
  127. package/src/engine/utils/loaders/AudioLoader.js +157 -0
  128. package/src/engine/utils/loaders/EventLoader.js +66 -0
  129. package/src/engine/utils/loaders/ObjectLoader.js +67 -0
  130. package/src/engine/utils/loaders/SpriteLoader.js +77 -0
  131. package/src/engine/utils/loaders/TilesetLoader.js +103 -0
  132. package/src/engine/utils/loaders/index.js +21 -0
  133. package/src/engine/utils/math/matrix4.js +367 -0
  134. package/src/engine/utils/math/vector.js +458 -0
  135. package/src/engine/utils/obj/_old_js/index.js +46 -0
  136. package/src/engine/utils/obj/_old_js/layout.js +308 -0
  137. package/src/engine/utils/obj/_old_js/material.js +711 -0
  138. package/src/engine/utils/obj/_old_js/mesh.js +761 -0
  139. package/src/engine/utils/obj/_old_js/utils.js +647 -0
  140. package/src/engine/utils/obj/index.js +24 -0
  141. package/src/engine/utils/obj/js/index.js +277 -0
  142. package/src/engine/utils/obj/js/loader.js +232 -0
  143. package/src/engine/utils/obj/layout.js +246 -0
  144. package/src/engine/utils/obj/material.js +665 -0
  145. package/src/engine/utils/obj/mesh.js +657 -0
  146. package/src/engine/utils/obj/ts/index.ts +72 -0
  147. package/src/engine/utils/obj/ts/layout.ts +265 -0
  148. package/src/engine/utils/obj/ts/material.ts +760 -0
  149. package/src/engine/utils/obj/ts/mesh.ts +785 -0
  150. package/src/engine/utils/obj/ts/utils.ts +501 -0
  151. package/src/engine/utils/obj/utils.js +428 -0
  152. package/src/engine/utils/resources.js +18 -0
  153. package/src/index.jsx +55 -0
  154. package/src/spritz/player.js +18 -0
  155. package/src/spritz/readme.md +18 -0
  156. package/LICENSE +0 -437
  157. 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
+ }