@fonsecabarreto/genesis-gl-core 0.1.0 → 0.1.2

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 (40) hide show
  1. package/README.md +19 -2
  2. package/dist/{Camera-DY_8gx3C.d.ts → Camera-CJVYy9fH.d.ts} +13 -2
  3. package/dist/Core/classes/Material.d.ts +1 -1
  4. package/dist/Core/classes/Material.js +1 -1
  5. package/dist/Core/classes/Model.d.ts +3 -3
  6. package/dist/Core/classes/Model.js +1 -1
  7. package/dist/Core/classes/Renderer.d.ts +11 -5
  8. package/dist/Core/classes/Renderer.js +4 -4
  9. package/dist/Core/classes/Scene.d.ts +2 -2
  10. package/dist/Core/classes/Viewport.d.ts +1 -1
  11. package/dist/Core/classes/Viewport.js +1 -1
  12. package/dist/Core/index.d.ts +4 -4
  13. package/dist/Core/index.js +4 -4
  14. package/dist/Core/utils/load-glb.d.ts +3 -3
  15. package/dist/Core/utils/load-glb.js +4 -4
  16. package/dist/Core/utils/parse-obj.d.ts +2 -2
  17. package/dist/Core/utils/parse-obj.js +4 -4
  18. package/dist/Editor/index.d.ts +126 -15
  19. package/dist/Editor/index.js +471 -74
  20. package/dist/Editor/index.js.map +1 -1
  21. package/dist/Game/controls/KeyboardInput.d.ts +4 -4
  22. package/dist/Game/index.d.ts +308 -7
  23. package/dist/Game/index.js +470 -24
  24. package/dist/Game/index.js.map +1 -1
  25. package/dist/{KeyboardInput-DTsfj3tE.d.ts → KeyboardInput-1xOAabI0.d.ts} +95 -14
  26. package/dist/{Material-BGLkldxv.d.ts → Material-DhwSRbP2.d.ts} +8 -0
  27. package/dist/{Model-CQvDXd-b.d.ts → Model-BBZHnUp1.d.ts} +24 -8
  28. package/dist/{chunk-6LS6AO5H.js → chunk-L66K4AZU.js} +36 -30
  29. package/dist/chunk-L66K4AZU.js.map +1 -0
  30. package/dist/{chunk-JK2HEZAT.js → chunk-QOAQVTAB.js} +26 -22
  31. package/dist/chunk-QOAQVTAB.js.map +1 -0
  32. package/dist/{chunk-5TAAXI6S.js → chunk-XMW2MS66.js} +39 -16
  33. package/dist/chunk-XMW2MS66.js.map +1 -0
  34. package/dist/{chunk-QCQVJCSR.js → chunk-ZCJ3MJZD.js} +103 -67
  35. package/dist/chunk-ZCJ3MJZD.js.map +1 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-5TAAXI6S.js.map +0 -1
  38. package/dist/chunk-6LS6AO5H.js.map +0 -1
  39. package/dist/chunk-JK2HEZAT.js.map +0 -1
  40. package/dist/chunk-QCQVJCSR.js.map +0 -1
@@ -54,7 +54,6 @@ var Model = class _Model {
54
54
  translation;
55
55
  rotation;
56
56
  scale;
57
- boundingBox;
58
57
  // ── Skeletal animation ──────────────────────────────────────
59
58
  /** The skeleton driving skinned meshes (set after GLB load if present). */
60
59
  skeleton = null;
@@ -62,9 +61,24 @@ var Model = class _Model {
62
61
  animations = /* @__PURE__ */ new Map();
63
62
  /** The currently playing animation clip (if any). */
64
63
  activeAnimation = null;
64
+ _boundingBox = { min: [0, 0, 0], max: [0, 0, 0] };
65
+ _bboxDirty = false;
65
66
  _boundsDefinedManually = false;
66
67
  _modelMatrix = mat42.create();
67
68
  _matrixDirty = true;
69
+ _poseMap = /* @__PURE__ */ new Map();
70
+ /** World-space AABB, lazily recomputed after any transform change. */
71
+ get boundingBox() {
72
+ if (this._bboxDirty && !this._boundsDefinedManually) {
73
+ this._boundingBox = this.computeBoundingBox();
74
+ this._bboxDirty = false;
75
+ }
76
+ return this._boundingBox;
77
+ }
78
+ set boundingBox(value) {
79
+ this._boundingBox = value;
80
+ this._bboxDirty = false;
81
+ }
68
82
  /** When `false` the model is excluded from collision detection. */
69
83
  collidable = true;
70
84
  constructor(meshes, translation = [0, 0, 0], scale = [1, 1, 1], _name = "model", skeleton = null) {
@@ -75,31 +89,31 @@ var Model = class _Model {
75
89
  this.collider = new Collider("box");
76
90
  this.name = _name;
77
91
  this.skeleton = skeleton;
78
- this.boundingBox = this.computeBoundingBox();
79
- this.translation[1] -= this.boundingBox.min[1];
92
+ this._boundingBox = this.computeBoundingBox();
93
+ this.translation[1] -= this._boundingBox.min[1];
80
94
  this._matrixDirty = true;
81
- this.updateBoundingBox();
95
+ this._bboxDirty = true;
82
96
  }
83
97
  // ─────────────────────────────────────────────
84
98
  // ░░░ Transform API ░░░
85
99
  // ─────────────────────────────────────────────
86
- /** Set absolute position. Marks matrix dirty and updates bounding box. */
100
+ /** Set absolute position. Marks matrix and bounding box dirty. */
87
101
  setTranslation(x, y, z) {
88
102
  this.translation = [x, y, z];
89
103
  this._matrixDirty = true;
90
- this.updateBoundingBox();
104
+ this._bboxDirty = true;
91
105
  }
92
106
  /** Set absolute rotation (radians per axis). */
93
107
  setRotation(x, y, z) {
94
108
  this.rotation = [x, y, z];
95
109
  this._matrixDirty = true;
96
- this.updateBoundingBox();
110
+ this._bboxDirty = true;
97
111
  }
98
112
  /** Set absolute scale. */
99
113
  setScale(x, y, z) {
100
114
  this.scale = [x, y, z];
101
115
  this._matrixDirty = true;
102
- this.updateBoundingBox();
116
+ this._bboxDirty = true;
103
117
  }
104
118
  /** Translate the model by a delta. */
105
119
  move(dx, dy, dz = 0) {
@@ -107,7 +121,7 @@ var Model = class _Model {
107
121
  this.translation[1] += dy;
108
122
  this.translation[2] += dz;
109
123
  this._matrixDirty = true;
110
- this.updateBoundingBox();
124
+ this._bboxDirty = true;
111
125
  }
112
126
  // ─────────────────────────────────────────────
113
127
  // ░░░ Matrix Handling ░░░
@@ -296,22 +310,12 @@ var Model = class _Model {
296
310
  update(deltaTime) {
297
311
  if (!this.activeAnimation || !this.skeleton) return;
298
312
  this.activeAnimation.update(deltaTime);
299
- const poses = this.activeAnimation.sample();
300
- const merged = /* @__PURE__ */ new Map();
301
- for (let i = 0; i < this.skeleton.joints.length; i++) {
302
- const joint = this.skeleton.joints[i];
303
- const pose = poses.get(i);
304
- merged.set(i, {
305
- t: pose?.t ?? joint.localTranslation,
306
- r: pose?.r ?? joint.localRotation,
307
- s: pose?.s ?? joint.localScale
308
- });
309
- }
310
- this.skeleton.computeJointMatrices(merged);
313
+ this.activeAnimation.sample(this._poseMap);
314
+ this.skeleton.computeJointMatrices(this._poseMap);
311
315
  }
312
316
  };
313
317
 
314
318
  export {
315
319
  Model
316
320
  };
317
- //# sourceMappingURL=chunk-JK2HEZAT.js.map
321
+ //# sourceMappingURL=chunk-QOAQVTAB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Core/classes/Model.ts","../src/Core/domain/value-objects/Collider.ts","../src/Core/utils/gltf-utils.ts"],"sourcesContent":["// classes/Model.ts\nimport { mat4 } from 'gl-matrix';\n\nimport { AABB, Collider } from '../domain/value-objects/Collider';\nimport { transformAABB } from '../utils/gltf-utils';\nimport { Vector3 } from '../domain/interfaces/Vectors';\nimport { Mesh } from './Mesh';\nimport { Skeleton, JointPose } from './Skeleton';\nimport { AnimationClip } from './AnimationClip';\nimport WebGLCore from './WebGLCore';\n\n/**\n * A high-level renderable object composed of one or more {@link Mesh}es.\n * Provides transform, bounding-box, AABB collision, and optional skeletal\n * animation utilities.\n */\nexport class Model {\n public name: string;\n public meshes: Mesh[];\n public collider: Collider;\n public translation: Vector3;\n public rotation: Vector3;\n public scale: Vector3;\n\n // ── Skeletal animation ──────────────────────────────────────\n /** The skeleton driving skinned meshes (set after GLB load if present). */\n public skeleton: Skeleton | null = null;\n\n /** Named animation clips available on this model. */\n public animations: Map<string, AnimationClip> = new Map();\n\n /** The currently playing animation clip (if any). */\n public activeAnimation: AnimationClip | null = null;\n\n private _boundingBox: AABB = { min: [0, 0, 0], max: [0, 0, 0] };\n private _bboxDirty = false;\n private _boundsDefinedManually = false;\n private _modelMatrix: mat4 = mat4.create();\n private _matrixDirty = true;\n private readonly _poseMap = new Map<number, JointPose>();\n\n /** World-space AABB, lazily recomputed after any transform change. */\n get boundingBox(): AABB {\n if (this._bboxDirty && !this._boundsDefinedManually) {\n this._boundingBox = this.computeBoundingBox();\n this._bboxDirty = false;\n }\n return this._boundingBox;\n }\n\n set boundingBox(value: AABB) {\n this._boundingBox = value;\n this._bboxDirty = false;\n }\n\n /** When `false` the model is excluded from collision detection. */\n public collidable = true;\n\n constructor(\n meshes: Mesh[],\n translation: Vector3 = [0, 0, 0],\n scale: Vector3 = [1, 1, 1],\n _name = 'model',\n skeleton: Skeleton | null = null,\n ) {\n this.meshes = meshes;\n this.translation = translation;\n this.scale = scale;\n this.rotation = [0, 0, 0];\n this.collider = new Collider('box');\n this.name = _name;\n this.skeleton = skeleton;\n this._boundingBox = this.computeBoundingBox();\n\n // Align bottom to y=0 — must dirty the matrix since we mutate translation directly\n this.translation[1] -= this._boundingBox.min[1];\n this._matrixDirty = true;\n this._bboxDirty = true;\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Transform API ░░░\n // ─────────────────────────────────────────────\n /** Set absolute position. Marks matrix and bounding box dirty. */\n public setTranslation(x: number, y: number, z: number) {\n this.translation = [x, y, z];\n this._matrixDirty = true;\n this._bboxDirty = true;\n }\n\n /** Set absolute rotation (radians per axis). */\n public setRotation(x: number, y: number, z: number) {\n this.rotation = [x, y, z];\n this._matrixDirty = true;\n this._bboxDirty = true;\n }\n\n /** Set absolute scale. */\n public setScale(x: number, y: number, z: number) {\n this.scale = [x, y, z];\n this._matrixDirty = true;\n this._bboxDirty = true;\n }\n\n /** Translate the model by a delta. */\n public move(dx: number, dy: number, dz: number = 0) {\n this.translation[0] += dx;\n this.translation[1] += dy;\n this.translation[2] += dz;\n this._matrixDirty = true;\n this._bboxDirty = true;\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Matrix Handling ░░░\n // ─────────────────────────────────────────────\n public getModelMatrix(): mat4 {\n if (this._matrixDirty) {\n mat4.identity(this._modelMatrix);\n mat4.translate(this._modelMatrix, this._modelMatrix, this.translation);\n mat4.rotateX(this._modelMatrix, this._modelMatrix, this.rotation[0]);\n mat4.rotateY(this._modelMatrix, this._modelMatrix, this.rotation[1]);\n mat4.rotateZ(this._modelMatrix, this._modelMatrix, this.rotation[2]);\n mat4.scale(this._modelMatrix, this._modelMatrix, this.scale);\n this._matrixDirty = false;\n }\n return this._modelMatrix;\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Bounding Box & Collider ░░░\n // ─────────────────────────────────────────────\n /**\n * Manually define the collision bounding box relative to the model's\n * current translation.\n */\n public setBounds(\n xOffset: number,\n yOffset: number,\n zOffset: number,\n width: number,\n height: number,\n depth: number,\n ) {\n this.boundingBox.min = [\n this.translation[0] + xOffset,\n this.translation[1] + yOffset,\n this.translation[2] + zOffset,\n ];\n this.boundingBox.max = [\n this.boundingBox.min[0] + width,\n this.boundingBox.min[1] + height,\n this.boundingBox.min[2] + depth,\n ];\n this._boundsDefinedManually = true;\n }\n\n private computeBoundingBox(): AABB {\n if (this.meshes.length === 0) {\n return { min: [...this.translation], max: [...this.translation] };\n }\n\n const modelMat = this.getModelMatrix();\n\n // For skinned models the shader applies: uModel * skinMat * vertex.\n // In bind pose skinMat ≈ skeleton.rootTransform (the non-joint ancestor\n // transform that many exporters don't bake into the IBM). We must\n // include it so the bbox matches what the GPU actually renders.\n let effectiveMat = modelMat;\n if (this.skeleton) {\n effectiveMat = mat4.create();\n mat4.multiply(effectiveMat, modelMat, this.skeleton.rootTransform);\n }\n\n const min: Vector3 = [Infinity, Infinity, Infinity];\n const max: Vector3 = [-Infinity, -Infinity, -Infinity];\n\n for (const mesh of this.meshes) {\n if (!mesh.isCollidable) continue;\n\n mesh.computeBounds();\n\n // Skip degenerate meshes (helper objects, IK controls, etc. that have\n // all vertices at the local origin — they would pull the bbox to include\n // the world-space origin even when the model is far away).\n const mb = mesh.boundingBox;\n const isDegenerate =\n mb.min[0] === mb.max[0] &&\n mb.min[1] === mb.max[1] &&\n mb.min[2] === mb.max[2];\n if (isDegenerate) continue;\n\n const transformed = transformAABB(\n mesh.boundingBox.min,\n mesh.boundingBox.max,\n effectiveMat,\n );\n\n for (let i = 0; i < 3; i++) {\n min[i] = Math.min(min[i], transformed.min[i]);\n max[i] = Math.max(max[i], transformed.max[i]);\n }\n }\n\n // No collidable mesh contributed — return a unit box at the model's position\n // to avoid propagating Infinity/NaN into translation.\n if (!isFinite(min[0])) {\n const t = effectiveMat;\n const tx = t[12];\n const ty = t[13];\n const tz = t[14];\n return {\n min: [tx - 0.5, ty, tz - 0.5],\n max: [tx + 0.5, ty + 1, tz + 0.5],\n };\n }\n\n return { min, max };\n }\n\n /** Recompute the world-space AABB from mesh data (unless manually set). */\n public updateBoundingBox() {\n if (this._boundsDefinedManually) return;\n this.boundingBox = this.computeBoundingBox();\n }\n\n /** World-space centre of the bounding box. */\n public getCenter(): Vector3 {\n const { min, max } = this.boundingBox;\n return [\n (min[0] + max[0]) / 2,\n (min[1] + max[1]) / 2,\n (min[2] + max[2]) / 2,\n ];\n }\n\n /** Extents of the bounding box in each axis. */\n public getSize(): Vector3 {\n const { min, max } = this.boundingBox;\n return [max[0] - min[0], max[1] - min[1], max[2] - min[2]];\n }\n\n /** Test for intersection with another model using their colliders. */\n public intersects(other: Model): boolean {\n const a = this.collider;\n const b = other.collider;\n\n if (a.type !== 'box' || b.type !== 'box') {\n console.warn(\n `[Model] Collider types '${a.type}' / '${b.type}' not yet implemented — falling back to AABB.`,\n );\n }\n\n return this.intersectAABB(other);\n }\n\n private intersectAABB(other: Model): boolean {\n const a = this.boundingBox;\n const b = other.boundingBox;\n return !(\n a.max[0] < b.min[0] ||\n a.min[0] > b.max[0] ||\n a.max[1] < b.min[1] ||\n a.min[1] > b.max[1] ||\n a.max[2] < b.min[2] ||\n a.min[2] > b.max[2]\n );\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Resource Management ░░░\n // ─────────────────────────────────────────────\n /** Release all GPU resources held by this model's meshes. */\n public dispose(glCore: WebGLCore) {\n for (const mesh of this.meshes) {\n mesh.dispose?.(glCore);\n }\n this.meshes = [];\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Cloning ░░░\n // ─────────────────────────────────────────────\n /** Deep-clone the model including all meshes. Materials are shared. */\n public clone(): Model {\n const clonedMeshes = this.meshes.map((m) => (m.clone ? m.clone() : m));\n const c = new Model(\n clonedMeshes,\n [...this.translation],\n [...this.scale],\n this.name,\n this.skeleton?.clone() ?? null,\n );\n c.rotation = [...this.rotation];\n c.collider = new Collider(this.collider.type);\n c.boundingBox = {\n min: [...this.boundingBox.min],\n max: [...this.boundingBox.max],\n };\n for (const [name, clip] of this.animations) {\n c.animations.set(name, clip.clone());\n }\n return c;\n }\n\n // ─────────────────────────────────────────────\n // ░░░ Animation ░░░\n // ─────────────────────────────────────────────\n\n /**\n * Start playing a named animation clip.\n * @param name - Must match a key in {@link animations}.\n * @param loop - Whether the clip should loop (default `true`).\n */\n public playAnimation(name: string, loop = true): void {\n const clip = this.animations.get(name);\n if (!clip) {\n console.warn(`[Model:${this.name}] Animation \"${name}\" not found`);\n return;\n }\n this.activeAnimation = clip;\n clip.loop = loop;\n clip.reset();\n clip.play();\n }\n\n /** Pause the active animation without rewinding. */\n public pauseAnimation(): void {\n this.activeAnimation?.pause();\n }\n\n /** Stop the active animation and rewind. */\n public stopAnimation(): void {\n if (this.activeAnimation) {\n this.activeAnimation.stop();\n this.activeAnimation = null;\n }\n }\n\n /** List the names of all available animations. */\n public getAnimationNames(): string[] {\n return [...this.animations.keys()];\n }\n\n /**\n * Advance animation and recompute joint matrices.\n * Call this every frame from your game loop.\n *\n * @param deltaTime - Elapsed time in seconds since the last frame.\n */\n public update(deltaTime: number) {\n if (!this.activeAnimation || !this.skeleton) return;\n\n this.activeAnimation.update(deltaTime);\n // Reuse _poseMap to avoid a new Map allocation every frame.\n // Skeleton.computeJointMatrices already falls back to bind-pose for any\n // joint not present in the map, so no manual merging is needed.\n this.activeAnimation.sample(this._poseMap);\n this.skeleton.computeJointMatrices(this._poseMap);\n }\n}\n","import { Vector3 } from '../interfaces/Vectors';\n\nexport interface AABB {\n min: Vector3;\n max: Vector3;\n}\n\nexport type ColliderType = 'box' | 'sphere' | 'capsule';\nexport class Collider {\n type: ColliderType;\n offset: Vector3;\n size: Vector3;\n\n constructor(\n type: ColliderType,\n offset: Vector3 = [0, 0, 0],\n size: Vector3 = [1, 1, 1],\n ) {\n this.type = type;\n this.offset = offset;\n this.size = size;\n }\n}\n","// utils/gltf-utils.ts\nimport { mat4, vec3 } from 'gl-matrix';\n\n/** Transforma um ponto (vec3) por uma mat4 */\nexport function transformPoint(\n out: [number, number, number],\n m: mat4,\n p: [number, number, number],\n) {\n const v = vec3.fromValues(p[0], p[1], p[2]);\n vec3.transformMat4(v, v, m);\n out[0] = v[0];\n out[1] = v[1];\n out[2] = v[2];\n}\n\n/** Recebe uma AABB local (min,max) e uma mat4 e retorna AABB transformada */\nexport function transformAABB(\n min: [number, number, number],\n max: [number, number, number],\n m: mat4,\n) {\n // calcula os 8 cantos, transforma, e retorna new min/max\n const corners: [number, number, number][] = [\n [min[0], min[1], min[2]],\n [min[0], min[1], max[2]],\n [min[0], max[1], min[2]],\n [min[0], max[1], max[2]],\n [max[0], min[1], min[2]],\n [max[0], min[1], max[2]],\n [max[0], max[1], min[2]],\n [max[0], max[1], max[2]],\n ];\n const outMin: [number, number, number] = [Infinity, Infinity, Infinity];\n const outMax: [number, number, number] = [-Infinity, -Infinity, -Infinity];\n const t: [number, number, number] = [0, 0, 0];\n for (const c of corners) {\n transformPoint(t, m, c);\n for (let i = 0; i < 3; i++) {\n outMin[i] = Math.min(outMin[i], t[i]);\n outMax[i] = Math.max(outMax[i], t[i]);\n }\n }\n return { min: outMin, max: outMax };\n}\n"],"mappings":"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","names":["mat4","mat4"]}
@@ -27,15 +27,32 @@ var Camera = class {
27
27
  /* Render Utils */
28
28
  lastViewMatrix;
29
29
  lastProjectionMatrix;
30
+ // ── Cached matrices (rebuilt only when camera state changes) ──
31
+ _view = mat4.create();
32
+ _proj = mat4.create();
33
+ _mvp = mat4.create();
34
+ _viewDirty = true;
35
+ _projDirty = true;
30
36
  /** Translate the camera by a delta. */
31
37
  move(dx, dy, dz = 0) {
32
38
  this.position[0] += dx;
33
39
  this.position[1] += dy;
34
40
  this.position[2] += dz;
41
+ this._viewDirty = true;
35
42
  }
36
43
  /** Clamp-set the zoom level. */
37
44
  setZoom(zoom) {
38
45
  this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));
46
+ this._viewDirty = true;
47
+ this._projDirty = true;
48
+ }
49
+ /**
50
+ * Mark the view matrix as dirty so it is rebuilt on the next
51
+ * {@link getViewMatrix} call. Call this after directly mutating
52
+ * {@link target} or {@link position} (e.g. from a follow-camera update).
53
+ */
54
+ invalidate() {
55
+ this._viewDirty = true;
39
56
  }
40
57
  /** Zoom by a signed delta (positive = zoom in). */
41
58
  zoomBy(delta) {
@@ -56,6 +73,7 @@ var Camera = class {
56
73
  setViewport(width, height) {
57
74
  this.viewportWidth = width;
58
75
  this.viewportHeight = height;
76
+ this._projDirty = true;
59
77
  }
60
78
  /** Compute the eye position from yaw/pitch/distance to target. */
61
79
  getComputedPosition() {
@@ -65,27 +83,31 @@ var Camera = class {
65
83
  const camZ = this.target[2] + radius * Math.cos(this.pitch) * Math.cos(this.yaw);
66
84
  return vec3.fromValues(camX, camY, camZ);
67
85
  }
68
- /** Build the view matrix (lookAt). */
86
+ /** Build the view matrix (lookAt). Returns a cached matrix rebuilt only when the camera moves. */
69
87
  getViewMatrix() {
70
- const view = mat4.create();
71
- const eye = this.getComputedPosition();
72
- const up = vec3.fromValues(0, 1, 0);
73
- mat4.lookAt(view, eye, this.target, up);
74
- return view;
88
+ if (this._viewDirty) {
89
+ const eye = this.getComputedPosition();
90
+ mat4.lookAt(this._view, eye, this.target, vec3.fromValues(0, 1, 0));
91
+ this._viewDirty = false;
92
+ this.lastViewMatrix = this._view;
93
+ }
94
+ return this._view;
75
95
  }
76
- /** Build the perspective projection matrix. */
96
+ /** Build the perspective projection matrix. Returns a cached matrix rebuilt only when viewport/zoom changes. */
77
97
  getProjectionMatrix() {
78
- const projection = mat4.create();
79
- const aspect = this.viewportWidth / this.viewportHeight;
80
- const fov = Math.PI / 4 / this.zoom;
81
- mat4.perspective(projection, fov, aspect, this.near, this.far);
82
- return projection;
98
+ if (this._projDirty) {
99
+ const aspect = this.viewportWidth / this.viewportHeight;
100
+ const fov = Math.PI / 4 / this.zoom;
101
+ mat4.perspective(this._proj, fov, aspect, this.near, this.far);
102
+ this._projDirty = false;
103
+ this.lastProjectionMatrix = this._proj;
104
+ }
105
+ return this._proj;
83
106
  }
84
107
  worldToNDC3D(x, y, z = 0) {
85
108
  const world = vec4.fromValues(x, y, z, 1);
86
- const mvp = mat4.create();
87
- mat4.multiply(mvp, this.getProjectionMatrix(), this.getViewMatrix());
88
- vec4.transformMat4(world, world, mvp);
109
+ mat4.multiply(this._mvp, this.getProjectionMatrix(), this.getViewMatrix());
110
+ vec4.transformMat4(world, world, this._mvp);
89
111
  return [world[0] / world[3], world[1] / world[3], world[2] / world[3]];
90
112
  }
91
113
  worldScale3D(scale) {
@@ -101,6 +123,7 @@ var Camera = class {
101
123
  this.yaw += deltaYaw;
102
124
  this.pitch -= deltaPitch;
103
125
  this.pitch = Math.max(this.minPitch, Math.min(this.maxPitch, this.pitch));
126
+ this._viewDirty = true;
104
127
  }
105
128
  };
106
129
 
@@ -327,4 +350,4 @@ export {
327
350
  Camera,
328
351
  Viewport
329
352
  };
330
- //# sourceMappingURL=chunk-5TAAXI6S.js.map
353
+ //# sourceMappingURL=chunk-XMW2MS66.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Core/classes/Camera.ts","../src/Core/controls/Mouse/MouseWheelControl.ts","../src/Core/controls/Mouse/MouseDragControl.ts","../src/Core/controls/Mouse/MousePointerLockControl.ts","../src/Core/classes/Viewport.ts"],"sourcesContent":["import { mat4, vec3, vec4 } from 'gl-matrix';\nimport { Vector3 } from '../domain/interfaces/Vectors';\n\n/**\n * Perspective camera with orbit (yaw/pitch), zoom, and viewport controls.\n */\nexport class Camera {\n public target: Vector3 = [0, 0, 0]; // camera looks at this\n\n /* Translation */\n public position: Vector3 = [0, 0, 0]; // 3D position\n\n /* Zoom */\n public zoom: number = 0.89;\n public minZoom: number = 0.5;\n public maxZoom: number = 1;\n public zoomSpeed: number = 0.06;\n\n /* Rotation */\n public yaw: number = 0; // horizontal rotation\n public pitch: number = 0.5; // vertical rotation\n\n public near = 0.01;\n public far = 1000.0;\n\n /* Render Utils */\n public lastViewMatrix?: Float32Array;\n public lastProjectionMatrix?: Float32Array;\n\n // ── Cached matrices (rebuilt only when camera state changes) ──\n private readonly _view: mat4 = mat4.create();\n private readonly _proj: mat4 = mat4.create();\n private readonly _mvp: mat4 = mat4.create();\n private _viewDirty = true;\n private _projDirty = true;\n\n constructor(\n public viewportWidth: number,\n public viewportHeight: number,\n ) {}\n\n /** Translate the camera by a delta. */\n move(dx: number, dy: number, dz: number = 0) {\n this.position[0] += dx;\n this.position[1] += dy;\n this.position[2] += dz;\n this._viewDirty = true;\n }\n\n /** Clamp-set the zoom level. */\n setZoom(zoom: number) {\n this.zoom = Math.max(this.minZoom, Math.min(this.maxZoom, zoom));\n this._viewDirty = true;\n this._projDirty = true;\n }\n\n /**\n * Mark the view matrix as dirty so it is rebuilt on the next\n * {@link getViewMatrix} call. Call this after directly mutating\n * {@link target} or {@link position} (e.g. from a follow-camera update).\n */\n invalidate(): void {\n this._viewDirty = true;\n }\n\n /** Zoom by a signed delta (positive = zoom in). */\n zoomBy(delta: number) {\n const factor = Math.exp(delta * this.zoomSpeed);\n this.setZoom(this.zoom * factor);\n }\n\n worldToNDC(x: number, y: number): [number, number] {\n const ndcX = ((x - this.position[0]) / this.viewportWidth) * 2 * this.zoom;\n const ndcY = ((y - this.position[1]) / this.viewportHeight) * 2 * this.zoom;\n return [ndcX, ndcY];\n }\n\n worldScale(scale: [number, number]): [number, number] {\n return [\n (scale[0] / this.viewportWidth) * 2 * this.zoom,\n (scale[1] / this.viewportHeight) * 2 * this.zoom,\n ];\n }\n\n setViewport(width: number, height: number) {\n this.viewportWidth = width;\n this.viewportHeight = height;\n this._projDirty = true;\n }\n\n /** Compute the eye position from yaw/pitch/distance to target. */\n getComputedPosition(): vec3 {\n const radius = vec3.distance(this.position, this.target);\n\n const camX =\n this.target[0] + radius * Math.cos(this.pitch) * Math.sin(this.yaw);\n const camY = this.target[1] + radius * Math.sin(this.pitch);\n const camZ =\n this.target[2] + radius * Math.cos(this.pitch) * Math.cos(this.yaw);\n\n return vec3.fromValues(camX, camY, camZ);\n }\n\n /** Build the view matrix (lookAt). Returns a cached matrix rebuilt only when the camera moves. */\n getViewMatrix(): mat4 {\n if (this._viewDirty) {\n const eye = this.getComputedPosition();\n mat4.lookAt(this._view, eye, this.target, vec3.fromValues(0, 1, 0));\n this._viewDirty = false;\n this.lastViewMatrix = this._view as unknown as Float32Array;\n }\n return this._view;\n }\n\n /** Build the perspective projection matrix. Returns a cached matrix rebuilt only when viewport/zoom changes. */\n getProjectionMatrix(): mat4 {\n if (this._projDirty) {\n const aspect = this.viewportWidth / this.viewportHeight;\n const fov = Math.PI / 4 / this.zoom;\n mat4.perspective(this._proj, fov, aspect, this.near, this.far);\n this._projDirty = false;\n this.lastProjectionMatrix = this._proj as unknown as Float32Array;\n }\n return this._proj;\n }\n\n worldToNDC3D(x: number, y: number, z: number = 0): [number, number, number] {\n const world = vec4.fromValues(x, y, z, 1.0);\n mat4.multiply(this._mvp, this.getProjectionMatrix(), this.getViewMatrix());\n vec4.transformMat4(world, world, this._mvp);\n return [world[0] / world[3], world[1] / world[3], world[2] / world[3]];\n }\n\n worldScale3D(scale: [number, number, number]): [number, number, number] {\n return [\n (scale[0] / this.viewportWidth) * 2 * this.zoom,\n (scale[1] / this.viewportHeight) * 2 * this.zoom,\n (scale[2] / ((this.viewportWidth + this.viewportHeight) / 2)) *\n 2 *\n this.zoom,\n ];\n }\n\n public minPitch: number = -Math.PI / 2 + 1.49;\n public maxPitch: number = Math.PI / 2 - 0.01;\n\n rotate(deltaYaw: number, deltaPitch: number) {\n this.yaw += deltaYaw;\n\n this.pitch -= deltaPitch; // ← add deltaPitch here\n this.pitch = Math.max(this.minPitch, Math.min(this.maxPitch, this.pitch));\n this._viewDirty = true;\n }\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type WheelDirection = 'up' | 'down';\nexport type WheelListener = (direction: WheelDirection, delta: number) => void;\n\nexport class MouseWheelControl implements IMouseControl {\n private listeners: WheelListener[] = [];\n private wheelHandler?: (e: WheelEvent) => void;\n\n public enable() {\n if (this.wheelHandler) return;\n\n this.wheelHandler = (e: WheelEvent) => {\n const direction: WheelDirection = e.deltaY < 0 ? 'up' : 'down';\n this.notify(direction, e.deltaY);\n };\n\n window.addEventListener('wheel', this.wheelHandler, { passive: true });\n }\n\n public disable() {\n if (this.wheelHandler) {\n window.removeEventListener('wheel', this.wheelHandler);\n this.wheelHandler = undefined;\n }\n }\n\n public onChange(listener: WheelListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n public removeListener(listener: WheelListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n private notify(direction: WheelDirection, delta: number) {\n for (const listener of this.listeners) listener(direction, delta);\n }\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type DragListener = (dx: number, dy: number, button: number) => void;\n\nexport class MouseDragControl implements IMouseControl {\n private listeners: DragListener[] = [];\n public isDragging = false;\n\n private dragButton: number = 0; // 0 = left, 2 = right\n private lastX = 0;\n private lastY = 0;\n\n constructor(private element: HTMLElement) {}\n\n enable() {\n this.element.addEventListener('mousedown', this.onMouseDown);\n window.addEventListener('mouseup', this.onMouseUp);\n window.addEventListener('mousemove', this.onMouseMove);\n }\n\n disable() {\n this.element.removeEventListener('mousedown', this.onMouseDown);\n window.removeEventListener('mouseup', this.onMouseUp);\n window.removeEventListener('mousemove', this.onMouseMove);\n }\n\n onChange(listener: DragListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n removeListener(listener: DragListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n private onMouseDown = (e: MouseEvent) => {\n this.isDragging = true;\n this.dragButton = e.button;\n\n this.lastX = e.clientX;\n this.lastY = e.clientY;\n };\n\n private onMouseUp = () => {\n this.isDragging = false;\n };\n\n private onMouseMove = (e: MouseEvent) => {\n if (!this.isDragging) return;\n\n const dx = e.clientX - this.lastX;\n const dy = e.clientY - this.lastY;\n\n this.lastX = e.clientX;\n this.lastY = e.clientY;\n\n for (const listener of this.listeners) listener(dx, dy, this.dragButton);\n };\n}\n","import { IMouseControl } from './IMouseControl';\n\nexport type MoveListener = (dx: number, dy: number) => void;\n\n/**\n * Captures mouse movement using the Pointer Lock API.\n * Ideal for camera-like controls where the cursor should not hit screen edges.\n */\nexport class MousePointerLockControl implements IMouseControl {\n private listeners: MoveListener[] = [];\n private isEnabled = false;\n private isPointerLocked = false;\n\n constructor(\n private element: HTMLElement,\n private sensitivity = 1,\n ) {}\n\n enable() {\n if (this.isEnabled) return;\n this.isEnabled = true;\n\n // Must be initiated by user click\n this.element.addEventListener('click', this.requestPointerLock);\n document.addEventListener('pointerlockchange', this.onPointerLockChange);\n document.addEventListener('mousemove', this.onMouseMove);\n }\n\n disable() {\n if (!this.isEnabled) return;\n this.isEnabled = false;\n\n this.element.removeEventListener('click', this.requestPointerLock);\n document.removeEventListener('pointerlockchange', this.onPointerLockChange);\n document.removeEventListener('mousemove', this.onMouseMove);\n\n if (this.isPointerLocked) document.exitPointerLock();\n }\n\n onChange(listener: MoveListener) {\n this.listeners.push(listener);\n }\n\n /** Remove a previously registered listener. */\n removeListener(listener: MoveListener) {\n this.listeners = this.listeners.filter((l) => l !== listener);\n }\n\n setSensitivity(value: number) {\n this.sensitivity = value;\n }\n\n private requestPointerLock = () => {\n this.element.requestPointerLock();\n };\n\n private onPointerLockChange = () => {\n this.isPointerLocked = document.pointerLockElement === this.element;\n };\n\n private onMouseMove = (e: MouseEvent) => {\n if (!this.isPointerLocked) return;\n\n const dx = e.movementX * this.sensitivity;\n const dy = e.movementY * this.sensitivity;\n\n for (const listener of this.listeners) listener(dx, dy);\n };\n}\n","import { Camera } from './Camera';\nimport {\n MouseWheelControl,\n WheelDirection,\n} from '../controls/Mouse/MouseWheelControl';\nimport { MouseDragControl } from '../controls/Mouse/MouseDragControl';\nimport { MousePointerLockControl } from '../controls/Mouse/MousePointerLockControl';\nimport WebGLCore from './WebGLCore';\n\nexport class Viewport {\n private static zoomInterval = 25;\n\n private wheelControl = new MouseWheelControl();\n private dragControl: MouseDragControl;\n private moveControl: MousePointerLockControl;\n\n private lastZoomTime: number = 0;\n private canvas: HTMLCanvasElement;\n private scale: number = 1;\n private webglCore: WebGLCore | null = null;\n private resizeHandler = () => this.resizeCanvas();\n public camera: Camera;\n\n /**\n * @param canvasOrId - An HTMLCanvasElement or the `id` attribute of one.\n * @param width - Logical viewport width.\n * @param height - Logical viewport height.\n * @param webglCore - Optional WebGLCore instance. When provided, the existing\n * GL context is reused for viewport resize instead of\n * requesting a new one.\n */\n constructor(\n canvasOrId: string | HTMLCanvasElement,\n public width: number,\n public height: number,\n webglCore?: WebGLCore,\n ) {\n this.canvas =\n typeof canvasOrId === 'string'\n ? (document.getElementById(canvasOrId) as HTMLCanvasElement)\n : canvasOrId;\n if (!this.canvas) throw new Error('Canvas not found');\n\n this.webglCore = webglCore ?? null;\n this.camera = new Camera(width, height);\n this.dragControl = new MouseDragControl(this.canvas);\n this.moveControl = new MousePointerLockControl(this.canvas, 0.25);\n\n this.resizeCanvas();\n this.setupControls();\n\n window.addEventListener('resize', this.resizeHandler);\n }\n\n private setupControls() {\n const zoomInterval = Viewport.zoomInterval;\n\n // Camera Zoom\n this.wheelControl.onChange((direction: WheelDirection) => {\n const now = performance.now();\n if (now - this.lastZoomTime < zoomInterval) return;\n\n const zoomDelta = direction === 'up' ? 1 : -1;\n this.camera.zoomBy(zoomDelta);\n\n this.lastZoomTime = now;\n });\n this.wheelControl.enable();\n\n // Drag / pan / rotate\n /* this.dragControl.onChange((dx, dy, button) => {\n const factor = 0.01 * this.camera.zoom;\n\n if (button === 2) {\n // Right click = orbit\n this.camera.rotate(-dx * factor, -dy * factor);\n } else if (button === 0) {\n this.camera.target[0] -= (dx * factor) / this.scale;\n this.camera.target[1] += (dy * factor) / this.scale;\n }\n\n });\n this.dragControl.enable(); */\n\n this.moveControl.onChange((dx, dy) => {\n const factor = 0.01 * this.camera.zoom;\n this.camera.rotate(-dx * factor, -dy * factor);\n });\n this.moveControl.enable();\n\n this.canvas.addEventListener('contextmenu', (e) => e.preventDefault());\n }\n\n public isDraggingCamera(): boolean {\n return this.dragControl.isDragging;\n }\n\n private resizeCanvas() {\n const screenWidth = window.innerWidth;\n const screenHeight = window.innerHeight;\n\n const aspectViewport = this.width / this.height;\n const aspectScreen = screenWidth / screenHeight;\n\n let canvasWidth: number, canvasHeight: number;\n\n if (aspectScreen > aspectViewport) {\n canvasHeight = screenHeight;\n canvasWidth = canvasHeight * aspectViewport;\n } else {\n canvasWidth = screenWidth;\n canvasHeight = canvasWidth / aspectViewport;\n }\n\n this.canvas.width = canvasWidth;\n this.canvas.height = canvasHeight;\n\n this.scale = canvasWidth / this.width;\n\n // Reuse the existing GL context when available, otherwise fall back.\n const gl = this.webglCore\n ? this.webglCore.getRenderingContext()\n : (this.canvas.getContext('webgl2') ?? this.canvas.getContext('webgl'));\n\n if (!gl) throw new Error('WebGL not supported');\n gl.viewport(0, 0, canvasWidth, canvasHeight);\n\n this.camera.setViewport(canvasWidth, canvasHeight);\n }\n\n getCanvas(): HTMLCanvasElement {\n return this.canvas;\n }\n\n getScale(): number {\n return this.scale;\n }\n\n /** Release event listeners. Call when the viewport is no longer needed. */\n dispose() {\n window.removeEventListener('resize', this.resizeHandler);\n this.wheelControl.disable();\n this.dragControl.disable();\n this.moveControl.disable();\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Material
3
- } from "./chunk-6LS6AO5H.js";
3
+ } from "./chunk-L66K4AZU.js";
4
4
 
5
5
  // src/Core/classes/Renderer.ts
6
6
  import { mat4 as mat42 } from "gl-matrix";
@@ -444,6 +444,11 @@ var Skeleton = class _Skeleton {
444
444
  * `global * inverseBindMatrix` evaluates to identity in the bind pose.
445
445
  */
446
446
  rootTransform;
447
+ // ── Pre-allocated scratch buffers ──
448
+ /** Per-joint global transform matrices, reused each frame (allocated once in constructor). */
449
+ _globals;
450
+ _local = mat4.create();
451
+ _final = mat4.create();
447
452
  constructor(joints, rootTransform) {
448
453
  if (joints.length > MAX_JOINTS) {
449
454
  console.warn(
@@ -453,6 +458,7 @@ var Skeleton = class _Skeleton {
453
458
  this.joints = joints;
454
459
  this.jointMatrices = new Float32Array(joints.length * 16);
455
460
  this.rootTransform = rootTransform ?? mat4.create();
461
+ this._globals = Array.from({ length: joints.length }, () => mat4.create());
456
462
  }
457
463
  /** Number of joints in the skeleton. */
458
464
  get jointCount() {
@@ -468,8 +474,9 @@ var Skeleton = class _Skeleton {
468
474
  * node indices).
469
475
  */
470
476
  computeJointMatrices(animatedPoses) {
471
- const globals = new Array(this.joints.length);
472
- const local = mat4.create();
477
+ const local = this._local;
478
+ const final = this._final;
479
+ const globals = this._globals;
473
480
  for (let i = 0; i < this.joints.length; i++) {
474
481
  const joint = this.joints[i];
475
482
  const pose = animatedPoses?.get(i);
@@ -478,13 +485,10 @@ var Skeleton = class _Skeleton {
478
485
  const s = pose?.s ?? joint.localScale;
479
486
  mat4.fromRotationTranslationScale(local, r, t, s);
480
487
  if (joint.parentIndex >= 0) {
481
- globals[i] = mat4.create();
482
488
  mat4.multiply(globals[i], globals[joint.parentIndex], local);
483
489
  } else {
484
- globals[i] = mat4.create();
485
490
  mat4.multiply(globals[i], this.rootTransform, local);
486
491
  }
487
- const final = mat4.create();
488
492
  mat4.multiply(final, globals[i], joint.inverseBindMatrix);
489
493
  this.jointMatrices.set(final, i * 16);
490
494
  }
@@ -508,6 +512,12 @@ var Skeleton = class _Skeleton {
508
512
 
509
513
  // src/Core/classes/AnimationClip.ts
510
514
  import { quat as quat2, vec3 as vec32 } from "gl-matrix";
515
+ var _sv0Vec3 = vec32.create();
516
+ var _sv1Vec3 = vec32.create();
517
+ var _sOutVec3 = vec32.create();
518
+ var _sv0Quat = quat2.create();
519
+ var _sv1Quat = quat2.create();
520
+ var _sOutQuat = quat2.create();
511
521
  var AnimationClip = class _AnimationClip {
512
522
  /** Human-readable name (e.g. "Walk", "Idle"). */
513
523
  name;
@@ -523,6 +533,11 @@ var AnimationClip = class _AnimationClip {
523
533
  speed = 1;
524
534
  /** Whether the clip is currently advancing. */
525
535
  playing = false;
536
+ /**
537
+ * Per-joint pose objects reused across frames to avoid per-frame allocations.
538
+ * The vec3/quat inside each entry are also pre-allocated on first use.
539
+ */
540
+ _posePool = /* @__PURE__ */ new Map();
526
541
  constructor(name, channels, duration) {
527
542
  this.name = name;
528
543
  this.channels = channels;
@@ -565,26 +580,34 @@ var AnimationClip = class _AnimationClip {
565
580
  }
566
581
  }
567
582
  /**
568
- * Sample every channel at the current playhead and return a map of
569
- * joint-index local pose.
583
+ * Sample every channel at the current playhead and write per-joint local
584
+ * transforms into `out` (or a newly created map if none is provided).
585
+ *
586
+ * Pass a persistent `Map` to avoid allocating a new one every frame.
570
587
  */
571
- sample() {
572
- const result = /* @__PURE__ */ new Map();
588
+ sample(out) {
589
+ const result = out ?? /* @__PURE__ */ new Map();
590
+ if (out) out.clear();
573
591
  for (const channel of this.channels) {
574
- if (!result.has(channel.jointIndex)) {
575
- result.set(channel.jointIndex, {});
592
+ let entry = this._posePool.get(channel.jointIndex);
593
+ if (!entry) {
594
+ entry = {};
595
+ this._posePool.set(channel.jointIndex, entry);
576
596
  }
577
- const entry = result.get(channel.jointIndex);
597
+ result.set(channel.jointIndex, entry);
578
598
  const value = sampleChannel(channel, this.currentTime);
579
599
  switch (channel.path) {
580
600
  case "translation":
581
- entry.t = value;
601
+ if (!entry.t) entry.t = vec32.create();
602
+ vec32.copy(entry.t, value);
582
603
  break;
583
604
  case "rotation":
584
- entry.r = value;
605
+ if (!entry.r) entry.r = quat2.create();
606
+ quat2.copy(entry.r, value);
585
607
  break;
586
608
  case "scale":
587
- entry.s = value;
609
+ if (!entry.s) entry.s = vec32.create();
610
+ vec32.copy(entry.s, value);
588
611
  break;
589
612
  }
590
613
  }
@@ -601,30 +624,30 @@ var AnimationClip = class _AnimationClip {
601
624
  return copy;
602
625
  }
603
626
  };
604
- function readValue(values, index, numComponents) {
627
+ function readValueInto(values, index, numComponents, out) {
605
628
  const off = index * numComponents;
606
- if (numComponents === 4) {
607
- return quat2.fromValues(
608
- values[off],
609
- values[off + 1],
610
- values[off + 2],
611
- values[off + 3]
612
- );
613
- }
614
- return vec32.fromValues(values[off], values[off + 1], values[off + 2]);
629
+ out[0] = values[off];
630
+ out[1] = values[off + 1];
631
+ out[2] = values[off + 2];
632
+ if (numComponents === 4) out[3] = values[off + 3];
615
633
  }
616
634
  function sampleChannel(channel, time) {
617
635
  const { sampler, path } = channel;
618
636
  const { times, values, interpolation } = sampler;
619
637
  const numComponents = path === "rotation" ? 4 : 3;
638
+ const outScratch = numComponents === 4 ? _sOutQuat : _sOutVec3;
620
639
  if (times.length === 0) {
621
- return numComponents === 4 ? quat2.create() : vec32.create();
640
+ if (numComponents === 4) quat2.identity(outScratch);
641
+ else vec32.set(outScratch, 0, 0, 0);
642
+ return outScratch;
622
643
  }
623
644
  if (time <= times[0]) {
624
- return readValue(values, 0, numComponents);
645
+ readValueInto(values, 0, numComponents, outScratch);
646
+ return outScratch;
625
647
  }
626
648
  if (time >= times[times.length - 1]) {
627
- return readValue(values, times.length - 1, numComponents);
649
+ readValueInto(values, times.length - 1, numComponents, outScratch);
650
+ return outScratch;
628
651
  }
629
652
  let lo = 0;
630
653
  let hi = times.length - 1;
@@ -639,19 +662,20 @@ function sampleChannel(channel, time) {
639
662
  const t0 = times[lo];
640
663
  const t1 = times[hi];
641
664
  const factor = (time - t0) / (t1 - t0);
642
- const v0 = readValue(values, lo, numComponents);
643
- const v1 = readValue(values, hi, numComponents);
665
+ const v0Scratch = numComponents === 4 ? _sv0Quat : _sv0Vec3;
666
+ const v1Scratch = numComponents === 4 ? _sv1Quat : _sv1Vec3;
667
+ readValueInto(values, lo, numComponents, v0Scratch);
668
+ readValueInto(values, hi, numComponents, v1Scratch);
644
669
  if (interpolation === "STEP") {
645
- return v0;
670
+ readValueInto(values, lo, numComponents, outScratch);
671
+ return outScratch;
646
672
  }
647
673
  if (path === "rotation") {
648
- const out2 = quat2.create();
649
- quat2.slerp(out2, v0, v1, factor);
650
- return out2;
674
+ quat2.slerp(outScratch, v0Scratch, v1Scratch, factor);
675
+ return outScratch;
651
676
  }
652
- const out = vec32.create();
653
- vec32.lerp(out, v0, v1, factor);
654
- return out;
677
+ vec32.lerp(outScratch, v0Scratch, v1Scratch, factor);
678
+ return outScratch;
655
679
  }
656
680
 
657
681
  // src/Core/utils/create-wire-box.ts
@@ -736,19 +760,13 @@ function createWireBox(min, max, material) {
736
760
  }
737
761
 
738
762
  // src/Core/classes/Renderer.ts
739
- var computeModelMatrix = (model) => {
740
- const m = mat42.create();
741
- mat42.translate(m, m, model.translation);
742
- mat42.rotateX(m, m, model.rotation[0]);
743
- mat42.rotateY(m, m, model.rotation[1]);
744
- mat42.rotateZ(m, m, model.rotation[2]);
745
- mat42.scale(m, m, model.scale);
746
- return m;
747
- };
748
763
  var Renderer = class {
749
764
  constructor(webglCore, viewport) {
750
765
  this.webglCore = webglCore;
751
766
  this.viewport = viewport;
767
+ const gl = webglCore.getRenderingContext();
768
+ this._isWebGL2 = gl instanceof WebGL2RenderingContext;
769
+ if (gl) gl.enable(gl.DEPTH_TEST);
752
770
  }
753
771
  webglCore;
754
772
  viewport;
@@ -757,6 +775,13 @@ var Renderer = class {
757
775
  // Cached debug materials to avoid per-frame GPU allocations
758
776
  _modelHitboxMaterial = null;
759
777
  _meshHitboxMaterial = null;
778
+ // Cache wire-box debug meshes: recreated only when the bounding box changes.
779
+ _modelWireBoxCache = /* @__PURE__ */ new WeakMap();
780
+ _meshWireBoxCache = /* @__PURE__ */ new WeakMap();
781
+ /** Pre-allocated identity matrix for world-space hitbox draws. */
782
+ _identity = mat42.create();
783
+ /** Cached `gl instanceof WebGL2RenderingContext` — computed once at construction. */
784
+ _isWebGL2;
760
785
  /** Lazily-created material for model-level hitboxes (red). */
761
786
  get modelHitboxMaterial() {
762
787
  if (!this._modelHitboxMaterial) {
@@ -780,7 +805,6 @@ var Renderer = class {
780
805
  if (!gl) return;
781
806
  gl.clearColor(...color);
782
807
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
783
- gl.enable(gl.DEPTH_TEST);
784
808
  }
785
809
  render(scene) {
786
810
  const gl = this.webglCore.getRenderingContext();
@@ -792,9 +816,9 @@ var Renderer = class {
792
816
  this.clearFrame();
793
817
  const viewMatrix = camera.getViewMatrix();
794
818
  const projMatrix = camera.getProjectionMatrix();
795
- const activeProgram = null;
819
+ let activeProgram = null;
796
820
  for (const model of models) {
797
- this.drawModel(
821
+ activeProgram = this.drawModel(
798
822
  gl,
799
823
  model,
800
824
  lights,
@@ -806,7 +830,7 @@ var Renderer = class {
806
830
  }
807
831
  }
808
832
  drawModel(gl, model, lights, viewPosition, viewMatrix, projMatrix, activeProgram) {
809
- const modelMatrix = computeModelMatrix(model);
833
+ const modelMatrix = model.getModelMatrix();
810
834
  const hasSkeleton = model.skeleton !== null;
811
835
  for (const mesh of model.meshes) {
812
836
  activeProgram = this.drawMesh(
@@ -830,8 +854,9 @@ var Renderer = class {
830
854
  }
831
855
  }
832
856
  if (this.debug) {
833
- this.drawHitBox(model, modelMatrix, activeProgram);
857
+ this.drawHitBox(model, activeProgram);
834
858
  }
859
+ return activeProgram;
835
860
  }
836
861
  drawMesh(mesh, lights, viewPosition, modelMatrix, viewMatrix, projMatrix, activeProgram, skinnedModel = null) {
837
862
  const gl = this.webglCore.getRenderingContext();
@@ -847,7 +872,7 @@ var Renderer = class {
847
872
  if (uniformLocations["uProjection"])
848
873
  gl.uniformMatrix4fv(uniformLocations["uProjection"], false, projMatrix);
849
874
  }
850
- const isWebGL2 = gl instanceof WebGL2RenderingContext;
875
+ const isWebGL2 = this._isWebGL2;
851
876
  if (isWebGL2 && mesh.buffers?.vao) {
852
877
  gl.bindVertexArray(
853
878
  mesh.buffers.vao
@@ -913,33 +938,44 @@ var Renderer = class {
913
938
  }
914
939
  return activeProgram;
915
940
  }
916
- drawHitBox(model, modelMatrix, activeProgram) {
917
- const hitboxMesh = createWireBox(
918
- model.boundingBox.min,
919
- model.boundingBox.max,
920
- this.modelHitboxMaterial
921
- );
922
- hitboxMesh.isCollidable = false;
941
+ drawHitBox(model, activeProgram) {
942
+ const bbox = model.boundingBox;
943
+ const key = `${bbox.min[0]},${bbox.min[1]},${bbox.min[2]},${bbox.max[0]},${bbox.max[1]},${bbox.max[2]}`;
944
+ const cached = this._modelWireBoxCache.get(model);
945
+ let hitboxMesh;
946
+ if (!cached || cached.key !== key) {
947
+ hitboxMesh = createWireBox(bbox.min, bbox.max, this.modelHitboxMaterial);
948
+ hitboxMesh.isCollidable = false;
949
+ this._modelWireBoxCache.set(model, { mesh: hitboxMesh, key });
950
+ } else {
951
+ hitboxMesh = cached.mesh;
952
+ }
923
953
  const camera = this.viewport.camera;
924
954
  const viewMatrix = camera.getViewMatrix();
925
955
  const projMatrix = camera.getProjectionMatrix();
926
- const identity = mat42.create();
927
956
  this.drawMesh(
928
957
  hitboxMesh,
929
958
  [],
930
959
  camera.position,
931
- identity,
960
+ this._identity,
932
961
  viewMatrix,
933
962
  projMatrix,
934
963
  activeProgram
935
964
  );
936
965
  }
937
966
  drawHitBoxForMesh(mesh, modelMatrix, viewMatrix, projMatrix, activeProgram) {
938
- const gl = this.webglCore.getRenderingContext();
939
- if (!gl || !mesh.boundingBox) return;
967
+ if (!mesh.boundingBox) return;
940
968
  const { min, max } = mesh.boundingBox;
941
- const wireBox = createWireBox(min, max, this.meshHitboxMaterial);
942
- wireBox.isCollidable = false;
969
+ const key = `${min[0]},${min[1]},${min[2]},${max[0]},${max[1]},${max[2]}`;
970
+ const cached = this._meshWireBoxCache.get(mesh);
971
+ let wireBox;
972
+ if (!cached || cached.key !== key) {
973
+ wireBox = createWireBox(min, max, this.meshHitboxMaterial);
974
+ wireBox.isCollidable = false;
975
+ this._meshWireBoxCache.set(mesh, { mesh: wireBox, key });
976
+ } else {
977
+ wireBox = cached.mesh;
978
+ }
943
979
  const camera = this.viewport.camera;
944
980
  this.drawMesh(
945
981
  wireBox,
@@ -965,4 +1001,4 @@ export {
965
1001
  Skeleton,
966
1002
  AnimationClip
967
1003
  };
968
- //# sourceMappingURL=chunk-QCQVJCSR.js.map
1004
+ //# sourceMappingURL=chunk-ZCJ3MJZD.js.map