@zylem/game-lib 0.6.3 → 0.6.4
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/dist/actions.d.ts +5 -5
- package/dist/actions.js +196 -32
- package/dist/actions.js.map +1 -1
- package/dist/behavior/jumper-2d.d.ts +114 -0
- package/dist/behavior/jumper-2d.js +711 -0
- package/dist/behavior/jumper-2d.js.map +1 -0
- package/dist/behavior/platformer-3d.d.ts +14 -14
- package/dist/behavior/platformer-3d.js +347 -104
- package/dist/behavior/platformer-3d.js.map +1 -1
- package/dist/behavior/ricochet-2d.d.ts +4 -3
- package/dist/behavior/ricochet-2d.js +53 -22
- package/dist/behavior/ricochet-2d.js.map +1 -1
- package/dist/behavior/ricochet-3d.d.ts +117 -0
- package/dist/behavior/ricochet-3d.js +443 -0
- package/dist/behavior/ricochet-3d.js.map +1 -0
- package/dist/behavior/screen-visibility.d.ts +79 -0
- package/dist/behavior/screen-visibility.js +358 -0
- package/dist/behavior/screen-visibility.js.map +1 -0
- package/dist/behavior/screen-wrap.d.ts +4 -3
- package/dist/behavior/screen-wrap.js +100 -49
- package/dist/behavior/screen-wrap.js.map +1 -1
- package/dist/behavior/shooter-2d.d.ts +79 -0
- package/dist/behavior/shooter-2d.js +180 -0
- package/dist/behavior/shooter-2d.js.map +1 -0
- package/dist/behavior/thruster.d.ts +5 -4
- package/dist/behavior/thruster.js +133 -75
- package/dist/behavior/thruster.js.map +1 -1
- package/dist/behavior/top-down-movement.d.ts +56 -0
- package/dist/behavior/top-down-movement.js +125 -0
- package/dist/behavior/top-down-movement.js.map +1 -0
- package/dist/behavior/world-boundary-2d.d.ts +4 -3
- package/dist/behavior/world-boundary-2d.js +90 -36
- package/dist/behavior/world-boundary-2d.js.map +1 -1
- package/dist/behavior/world-boundary-3d.d.ts +76 -0
- package/dist/behavior/world-boundary-3d.js +274 -0
- package/dist/behavior/world-boundary-3d.js.map +1 -0
- package/dist/{behavior-descriptor-BWNWmIjv.d.ts → behavior-descriptor-BXnVR8Ki.d.ts} +22 -5
- package/dist/{blueprints-BWGz8fII.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
- package/dist/camera-4XO5gbQH.d.ts +905 -0
- package/dist/camera.d.ts +1 -1
- package/dist/camera.js +876 -289
- package/dist/camera.js.map +1 -1
- package/dist/{composition-DrzFrbqI.d.ts → composition-BASvMKrW.d.ts} +1 -1
- package/dist/{core-DAkskq6Y.d.ts → core-CARRaS55.d.ts} +57 -14
- package/dist/core.d.ts +9 -8
- package/dist/core.js +4519 -1255
- package/dist/core.js.map +1 -1
- package/dist/{entities-DC9ce_vx.d.ts → entities-ChFirVL9.d.ts} +22 -28
- package/dist/entities.d.ts +4 -4
- package/dist/entities.js +1231 -314
- package/dist/entities.js.map +1 -1
- package/dist/{entity-BpbZqg19.d.ts → entity-vj-HTjzU.d.ts} +80 -11
- package/dist/{global-change-Dc8uCKi2.d.ts → global-change-2JvMaz44.d.ts} +1 -1
- package/dist/main.d.ts +718 -19
- package/dist/main.js +12129 -5959
- package/dist/main.js.map +1 -1
- package/dist/physics-pose-DCc4oE44.d.ts +25 -0
- package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
- package/dist/physics-worker.d.ts +21 -0
- package/dist/physics-worker.js +306 -0
- package/dist/physics-worker.js.map +1 -0
- package/dist/physics.d.ts +205 -0
- package/dist/physics.js +577 -0
- package/dist/physics.js.map +1 -0
- package/dist/{stage-types-BFsm3qsZ.d.ts → stage-types-C19IhuzA.d.ts} +253 -89
- package/dist/stage.d.ts +9 -8
- package/dist/stage.js +3782 -1041
- package/dist/stage.js.map +1 -1
- package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
- package/dist/{thruster-DhRaJnoL.d.ts → thruster-23lzoPZd.d.ts} +16 -8
- package/dist/world-DfgxoNMt.d.ts +105 -0
- package/package.json +25 -1
- package/dist/camera-B5e4c78l.d.ts +0 -468
- package/dist/world-Be5m1XC1.d.ts +0 -31
package/dist/camera.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// src/lib/camera/camera.ts
|
|
2
|
-
import { Vector2 as
|
|
2
|
+
import { Vector2 as Vector23, Vector3 as Vector37 } from "three";
|
|
3
3
|
|
|
4
4
|
// src/lib/camera/zylem-camera.ts
|
|
5
|
-
import { PerspectiveCamera as PerspectiveCamera2, Vector3 as
|
|
5
|
+
import { PerspectiveCamera as PerspectiveCamera2, Vector3 as Vector36, OrthographicCamera, WebGLRenderTarget as WebGLRenderTarget2, LinearFilter } from "three";
|
|
6
6
|
|
|
7
7
|
// src/lib/camera/perspective.ts
|
|
8
8
|
var Perspectives = {
|
|
@@ -10,145 +10,12 @@ var Perspectives = {
|
|
|
10
10
|
ThirdPerson: "third-person",
|
|
11
11
|
Isometric: "isometric",
|
|
12
12
|
Flat2D: "flat-2d",
|
|
13
|
-
Fixed2D: "fixed-2d"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// src/lib/camera/third-person.ts
|
|
17
|
-
import { Vector3 } from "three";
|
|
18
|
-
var ThirdPersonCamera = class {
|
|
19
|
-
distance;
|
|
20
|
-
screenResolution = null;
|
|
21
|
-
renderer = null;
|
|
22
|
-
scene = null;
|
|
23
|
-
cameraRef = null;
|
|
24
|
-
/** Padding multiplier when framing multiple targets. Higher = more zoom out. */
|
|
25
|
-
paddingFactor = 1.5;
|
|
26
|
-
/** Minimum camera distance when multi-framing (prevents extreme zoom-in). */
|
|
27
|
-
minDistance = 5;
|
|
28
|
-
/** Lerp factor for camera position smoothing. */
|
|
29
|
-
lerpFactor = 0.1;
|
|
30
|
-
constructor() {
|
|
31
|
-
this.distance = new Vector3(0, 5, 8);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Setup the third person camera controller
|
|
35
|
-
*/
|
|
36
|
-
setup(params) {
|
|
37
|
-
const { screenResolution, renderer, scene, camera } = params;
|
|
38
|
-
this.screenResolution = screenResolution;
|
|
39
|
-
this.renderer = renderer;
|
|
40
|
-
this.scene = scene;
|
|
41
|
-
this.cameraRef = camera;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Update the third person camera.
|
|
45
|
-
* Handles 0, 1, and multi-target scenarios.
|
|
46
|
-
*/
|
|
47
|
-
update(delta) {
|
|
48
|
-
if (!this.cameraRef) return;
|
|
49
|
-
const targets = this.cameraRef.targets;
|
|
50
|
-
if (targets.length === 0) {
|
|
51
|
-
this.cameraRef.camera.lookAt(new Vector3(0, 0, 0));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (targets.length === 1) {
|
|
55
|
-
this.updateSingleTarget(targets[0]);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
this.updateMultiTarget(targets);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Classic single-target follow: lerp to target position + offset, lookAt target.
|
|
62
|
-
*/
|
|
63
|
-
updateSingleTarget(target) {
|
|
64
|
-
const useTarget = target.group?.position || new Vector3(0, 0, 0);
|
|
65
|
-
const desiredCameraPosition = useTarget.clone().add(this.distance);
|
|
66
|
-
this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
|
|
67
|
-
this.cameraRef.camera.lookAt(useTarget);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Multi-target framing: compute centroid, measure spread, zoom out to fit all.
|
|
71
|
-
*/
|
|
72
|
-
updateMultiTarget(targets) {
|
|
73
|
-
const centroid = new Vector3();
|
|
74
|
-
for (const t of targets) {
|
|
75
|
-
centroid.add(t.group.position);
|
|
76
|
-
}
|
|
77
|
-
centroid.divideScalar(targets.length);
|
|
78
|
-
let maxDistFromCentroid = 0;
|
|
79
|
-
for (const t of targets) {
|
|
80
|
-
const dist = centroid.distanceTo(t.group.position);
|
|
81
|
-
if (dist > maxDistFromCentroid) {
|
|
82
|
-
maxDistFromCentroid = dist;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const dynamicDistance = Math.max(maxDistFromCentroid * this.paddingFactor, this.minDistance);
|
|
86
|
-
const offsetDirection = this.distance.clone().normalize();
|
|
87
|
-
const desiredCameraPosition = centroid.clone().add(
|
|
88
|
-
offsetDirection.multiplyScalar(dynamicDistance)
|
|
89
|
-
);
|
|
90
|
-
const baseLen = this.distance.length();
|
|
91
|
-
if (baseLen > 0) {
|
|
92
|
-
const heightRatio = this.distance.y / baseLen;
|
|
93
|
-
desiredCameraPosition.y = centroid.y + dynamicDistance * heightRatio;
|
|
94
|
-
}
|
|
95
|
-
this.cameraRef.camera.position.lerp(desiredCameraPosition, this.lerpFactor);
|
|
96
|
-
this.cameraRef.camera.lookAt(centroid);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Handle resize events
|
|
100
|
-
*/
|
|
101
|
-
resize(width, height) {
|
|
102
|
-
if (this.screenResolution) {
|
|
103
|
-
this.screenResolution.set(width, height);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Set the distance offset from the target
|
|
108
|
-
*/
|
|
109
|
-
setDistance(distance) {
|
|
110
|
-
this.distance = distance;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
// src/lib/camera/fixed-2d.ts
|
|
115
|
-
var Fixed2DCamera = class {
|
|
116
|
-
screenResolution = null;
|
|
117
|
-
renderer = null;
|
|
118
|
-
scene = null;
|
|
119
|
-
cameraRef = null;
|
|
120
|
-
constructor() {
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Setup the fixed 2D camera controller
|
|
124
|
-
*/
|
|
125
|
-
setup(params) {
|
|
126
|
-
const { screenResolution, renderer, scene, camera } = params;
|
|
127
|
-
this.screenResolution = screenResolution;
|
|
128
|
-
this.renderer = renderer;
|
|
129
|
-
this.scene = scene;
|
|
130
|
-
this.cameraRef = camera;
|
|
131
|
-
this.cameraRef.camera.position.set(0, 0, 10);
|
|
132
|
-
this.cameraRef.camera.lookAt(0, 0, 0);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Update the fixed 2D camera
|
|
136
|
-
* Fixed cameras don't need to update position/rotation automatically
|
|
137
|
-
*/
|
|
138
|
-
update(delta) {
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Handle resize events for 2D camera
|
|
142
|
-
*/
|
|
143
|
-
resize(width, height) {
|
|
144
|
-
if (this.screenResolution) {
|
|
145
|
-
this.screenResolution.set(width, height);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
13
|
+
Fixed2D: "fixed-2d",
|
|
14
|
+
TopDown: "top-down"
|
|
148
15
|
};
|
|
149
16
|
|
|
150
17
|
// src/lib/camera/camera-debug-delegate.ts
|
|
151
|
-
import { Vector3
|
|
18
|
+
import { Vector3 } from "three";
|
|
152
19
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
153
20
|
var CameraOrbitController = class {
|
|
154
21
|
camera;
|
|
@@ -157,7 +24,7 @@ var CameraOrbitController = class {
|
|
|
157
24
|
sceneRef = null;
|
|
158
25
|
orbitControls = null;
|
|
159
26
|
orbitTarget = null;
|
|
160
|
-
orbitTargetWorldPos = new
|
|
27
|
+
orbitTargetWorldPos = new Vector3();
|
|
161
28
|
debugDelegate = null;
|
|
162
29
|
debugUnsubscribe = null;
|
|
163
30
|
debugStateSnapshot = { enabled: false, selected: [] };
|
|
@@ -404,7 +271,7 @@ var CameraOrbitController = class {
|
|
|
404
271
|
return;
|
|
405
272
|
}
|
|
406
273
|
this.savedCameraLocalPosition = this.camera.position.clone();
|
|
407
|
-
const worldPos = new
|
|
274
|
+
const worldPos = new Vector3();
|
|
408
275
|
this.camera.getWorldPosition(worldPos);
|
|
409
276
|
this.cameraRig.remove(this.camera);
|
|
410
277
|
if (this.sceneRef) {
|
|
@@ -432,7 +299,7 @@ var CameraOrbitController = class {
|
|
|
432
299
|
};
|
|
433
300
|
|
|
434
301
|
// src/lib/camera/renderer-manager.ts
|
|
435
|
-
import { Vector2
|
|
302
|
+
import { Vector2, WebGLRenderer as WebGLRenderer2 } from "three";
|
|
436
303
|
import { WebGPURenderer } from "three/webgpu";
|
|
437
304
|
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|
438
305
|
|
|
@@ -564,8 +431,9 @@ var RendererManager = class {
|
|
|
564
431
|
_isWebGPU = false;
|
|
565
432
|
_initialized = false;
|
|
566
433
|
_sceneRef = null;
|
|
434
|
+
_lastAnimationTimestamp = null;
|
|
567
435
|
constructor(screenResolution, rendererType = "webgl") {
|
|
568
|
-
this.screenResolution = screenResolution || new
|
|
436
|
+
this.screenResolution = screenResolution || new Vector2(window.innerWidth, window.innerHeight);
|
|
569
437
|
this.rendererType = rendererType;
|
|
570
438
|
}
|
|
571
439
|
/**
|
|
@@ -647,14 +515,18 @@ var RendererManager = class {
|
|
|
647
515
|
* Start the render loop. Calls the provided callback each frame.
|
|
648
516
|
*/
|
|
649
517
|
startRenderLoop(onFrame) {
|
|
650
|
-
this.
|
|
651
|
-
|
|
518
|
+
this._lastAnimationTimestamp = null;
|
|
519
|
+
this.renderer.setAnimationLoop((timestamp) => {
|
|
520
|
+
const deltaSeconds = this._lastAnimationTimestamp === null ? 0 : Math.max(0, (timestamp - this._lastAnimationTimestamp) / 1e3);
|
|
521
|
+
this._lastAnimationTimestamp = timestamp;
|
|
522
|
+
onFrame(deltaSeconds);
|
|
652
523
|
});
|
|
653
524
|
}
|
|
654
525
|
/**
|
|
655
526
|
* Stop the render loop.
|
|
656
527
|
*/
|
|
657
528
|
stopRenderLoop() {
|
|
529
|
+
this._lastAnimationTimestamp = null;
|
|
658
530
|
try {
|
|
659
531
|
this.renderer.setAnimationLoop(null);
|
|
660
532
|
} catch {
|
|
@@ -683,6 +555,25 @@ var RendererManager = class {
|
|
|
683
555
|
this.composer.render(0);
|
|
684
556
|
}
|
|
685
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Render a camera to its offscreen render target (WebGL only).
|
|
560
|
+
* Bypasses the EffectComposer since post-processing is not needed
|
|
561
|
+
* for render-to-texture output.
|
|
562
|
+
*
|
|
563
|
+
* The camera must have a non-null renderTarget.
|
|
564
|
+
*/
|
|
565
|
+
renderCameraToTarget(scene, camera) {
|
|
566
|
+
if (!camera.renderTarget) return;
|
|
567
|
+
if (this.renderer instanceof WebGLRenderer2) {
|
|
568
|
+
const prevTarget = this.renderer.getRenderTarget();
|
|
569
|
+
this.renderer.setRenderTarget(camera.renderTarget);
|
|
570
|
+
this.renderer.clear();
|
|
571
|
+
this.renderer.render(scene, camera.camera);
|
|
572
|
+
this.renderer.setRenderTarget(prevTarget);
|
|
573
|
+
} else {
|
|
574
|
+
console.warn("RendererManager: Render-to-texture is not yet supported for WebGPU");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
686
577
|
/**
|
|
687
578
|
* Render a scene from multiple cameras, each with their own viewport.
|
|
688
579
|
* Cameras are rendered in order (first = bottom layer, last = top layer).
|
|
@@ -753,8 +644,540 @@ var RendererManager = class {
|
|
|
753
644
|
}
|
|
754
645
|
};
|
|
755
646
|
|
|
647
|
+
// src/lib/camera/smoothing.ts
|
|
648
|
+
import { Vector3 as Vector32, Quaternion as Quaternion2, MathUtils } from "three";
|
|
649
|
+
function defaultPose() {
|
|
650
|
+
return {
|
|
651
|
+
position: new Vector32(0, 0, 10),
|
|
652
|
+
rotation: new Quaternion2(),
|
|
653
|
+
fov: 75,
|
|
654
|
+
zoom: 1,
|
|
655
|
+
near: 0.1,
|
|
656
|
+
far: 1e3
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
function clonePose(pose) {
|
|
660
|
+
return {
|
|
661
|
+
position: pose.position.clone(),
|
|
662
|
+
rotation: pose.rotation.clone(),
|
|
663
|
+
fov: pose.fov,
|
|
664
|
+
zoom: pose.zoom,
|
|
665
|
+
near: pose.near,
|
|
666
|
+
far: pose.far,
|
|
667
|
+
lookAt: pose.lookAt?.clone()
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function applyDelta(pose, delta) {
|
|
671
|
+
const result = clonePose(pose);
|
|
672
|
+
if (delta.position) {
|
|
673
|
+
result.position.add(delta.position);
|
|
674
|
+
}
|
|
675
|
+
if (delta.rotation) {
|
|
676
|
+
result.rotation.multiply(delta.rotation);
|
|
677
|
+
}
|
|
678
|
+
if (delta.fov != null && result.fov != null) {
|
|
679
|
+
result.fov += delta.fov;
|
|
680
|
+
}
|
|
681
|
+
if (delta.zoom != null && result.zoom != null) {
|
|
682
|
+
result.zoom += delta.zoom;
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
}
|
|
686
|
+
function smoothPose(current, target, damping, dt) {
|
|
687
|
+
const t = 1 - Math.pow(1 - MathUtils.clamp(damping, 0, 1), dt * 60);
|
|
688
|
+
const result = clonePose(current);
|
|
689
|
+
result.position.lerp(target.position, t);
|
|
690
|
+
result.rotation.slerp(target.rotation, t);
|
|
691
|
+
if (target.fov != null && result.fov != null) {
|
|
692
|
+
result.fov = MathUtils.lerp(result.fov, target.fov, t);
|
|
693
|
+
}
|
|
694
|
+
if (target.zoom != null && result.zoom != null) {
|
|
695
|
+
result.zoom = MathUtils.lerp(result.zoom, target.zoom, t);
|
|
696
|
+
}
|
|
697
|
+
if (target.near != null) {
|
|
698
|
+
result.near = target.near;
|
|
699
|
+
}
|
|
700
|
+
if (target.far != null) {
|
|
701
|
+
result.far = target.far;
|
|
702
|
+
}
|
|
703
|
+
result.lookAt = target.lookAt?.clone();
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/lib/camera/camera-pipeline.ts
|
|
708
|
+
var CameraPipeline = class {
|
|
709
|
+
/** Active perspective (exactly one at a time). */
|
|
710
|
+
perspective = null;
|
|
711
|
+
/** Keyed behaviors, sorted by priority each frame. */
|
|
712
|
+
behaviors = /* @__PURE__ */ new Map();
|
|
713
|
+
/** Active transient actions. Expired actions are auto-removed. */
|
|
714
|
+
actions = [];
|
|
715
|
+
/** The desired pose after perspective + behaviors + actions. */
|
|
716
|
+
_desiredPose = defaultPose();
|
|
717
|
+
/** The smoothed final pose committed to the Three.js camera. */
|
|
718
|
+
_finalPose = defaultPose();
|
|
719
|
+
/** Whether the pipeline has run at least once (prevents lerp from origin on first frame). */
|
|
720
|
+
_initialized = false;
|
|
721
|
+
/** Smoothing factor: 0 = no movement, 1 = instant snap. */
|
|
722
|
+
damping = 0.15;
|
|
723
|
+
constructor(perspective) {
|
|
724
|
+
if (perspective) {
|
|
725
|
+
this.perspective = perspective;
|
|
726
|
+
if (perspective.defaults?.damping != null) {
|
|
727
|
+
this.damping = perspective.defaults.damping;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Run the full pipeline for one frame.
|
|
733
|
+
* Returns the final pose that should be committed to the Three.js camera.
|
|
734
|
+
*/
|
|
735
|
+
run(ctx) {
|
|
736
|
+
let pose = this.perspective ? this.perspective.getBasePose(ctx) : defaultPose();
|
|
737
|
+
const sorted = this.getSortedBehaviors();
|
|
738
|
+
for (const behavior of sorted) {
|
|
739
|
+
if (behavior.enabled === false) continue;
|
|
740
|
+
pose = behavior.update(ctx, pose);
|
|
741
|
+
}
|
|
742
|
+
this._desiredPose = clonePose(pose);
|
|
743
|
+
for (let i = this.actions.length - 1; i >= 0; i--) {
|
|
744
|
+
const action = this.actions[i];
|
|
745
|
+
const delta = action.update(ctx);
|
|
746
|
+
pose = applyDelta(pose, delta);
|
|
747
|
+
if (action.isDone(ctx)) {
|
|
748
|
+
this.actions.splice(i, 1);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!this._initialized) {
|
|
752
|
+
this._finalPose = clonePose(pose);
|
|
753
|
+
this._initialized = true;
|
|
754
|
+
} else {
|
|
755
|
+
this._finalPose = smoothPose(this._finalPose, pose, this.damping, ctx.dt);
|
|
756
|
+
}
|
|
757
|
+
return this._finalPose;
|
|
758
|
+
}
|
|
759
|
+
// ─── Behavior management ───────────────────────────────────────────────
|
|
760
|
+
/**
|
|
761
|
+
* Add or replace a behavior by key (idempotent).
|
|
762
|
+
* Calls onDetach on the old behavior and onAttach on the new one.
|
|
763
|
+
*/
|
|
764
|
+
addBehavior(key, behavior, ctx) {
|
|
765
|
+
const existing = this.behaviors.get(key);
|
|
766
|
+
if (existing?.onDetach && ctx) {
|
|
767
|
+
existing.onDetach(ctx);
|
|
768
|
+
}
|
|
769
|
+
this.behaviors.set(key, behavior);
|
|
770
|
+
if (behavior.onAttach && ctx) {
|
|
771
|
+
behavior.onAttach(ctx);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Remove a behavior by key. Calls onDetach if a context is provided.
|
|
776
|
+
*/
|
|
777
|
+
removeBehavior(key, ctx) {
|
|
778
|
+
const existing = this.behaviors.get(key);
|
|
779
|
+
if (!existing) return false;
|
|
780
|
+
if (existing.onDetach && ctx) {
|
|
781
|
+
existing.onDetach(ctx);
|
|
782
|
+
}
|
|
783
|
+
return this.behaviors.delete(key);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Check whether a behavior with the given key exists.
|
|
787
|
+
*/
|
|
788
|
+
hasBehavior(key) {
|
|
789
|
+
return this.behaviors.has(key);
|
|
790
|
+
}
|
|
791
|
+
// ─── Action management ─────────────────────────────────────────────────
|
|
792
|
+
/**
|
|
793
|
+
* Add a transient action. Actions self-expire via isDone().
|
|
794
|
+
*/
|
|
795
|
+
addAction(action) {
|
|
796
|
+
this.actions.push(action);
|
|
797
|
+
}
|
|
798
|
+
// ─── State inspection ──────────────────────────────────────────────────
|
|
799
|
+
/**
|
|
800
|
+
* Return a debug snapshot of the pipeline state.
|
|
801
|
+
*/
|
|
802
|
+
getState() {
|
|
803
|
+
return {
|
|
804
|
+
perspectiveId: this.perspective?.id ?? null,
|
|
805
|
+
desiredPose: this._desiredPose ? clonePose(this._desiredPose) : null,
|
|
806
|
+
finalPose: this._finalPose ? clonePose(this._finalPose) : null,
|
|
807
|
+
activeBehaviors: Array.from(this.behaviors.keys()),
|
|
808
|
+
activeActionCount: this.actions.length
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Set a new perspective. Resets the pipeline initialization flag so the
|
|
813
|
+
* first frame with the new perspective snaps instead of lerping from the old pose.
|
|
814
|
+
*/
|
|
815
|
+
setPerspective(perspective) {
|
|
816
|
+
this.perspective = perspective;
|
|
817
|
+
this._initialized = false;
|
|
818
|
+
if (perspective.defaults?.damping != null) {
|
|
819
|
+
this.damping = perspective.defaults.damping;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
// ─── Internal helpers ──────────────────────────────────────────────────
|
|
823
|
+
getSortedBehaviors() {
|
|
824
|
+
return Array.from(this.behaviors.values()).sort(
|
|
825
|
+
(a, b) => (a.priority ?? 0) - (b.priority ?? 0)
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/lib/camera/perspectives/third-person-perspective.ts
|
|
831
|
+
import { Vector3 as Vector33, Quaternion as Quaternion3 } from "three";
|
|
832
|
+
var DEFAULTS = {
|
|
833
|
+
distance: 8,
|
|
834
|
+
height: 5,
|
|
835
|
+
shoulderOffset: 0,
|
|
836
|
+
targetKey: "primary",
|
|
837
|
+
fov: 75,
|
|
838
|
+
paddingFactor: 1.5,
|
|
839
|
+
minDistance: 5
|
|
840
|
+
};
|
|
841
|
+
var ThirdPersonPerspective = class {
|
|
842
|
+
id = "third-person";
|
|
843
|
+
defaults = { damping: 0.15 };
|
|
844
|
+
opts;
|
|
845
|
+
initialPosition;
|
|
846
|
+
initialLookAt;
|
|
847
|
+
constructor(options) {
|
|
848
|
+
const { initialPosition, initialLookAt, ...rest } = options ?? {};
|
|
849
|
+
this.opts = { ...DEFAULTS, ...rest };
|
|
850
|
+
this.initialPosition = initialPosition;
|
|
851
|
+
this.initialLookAt = initialLookAt;
|
|
852
|
+
}
|
|
853
|
+
getBasePose(ctx) {
|
|
854
|
+
const targetKeys = Object.keys(ctx.targets);
|
|
855
|
+
const primary = ctx.targets[this.opts.targetKey];
|
|
856
|
+
if (targetKeys.length === 0 || !primary) {
|
|
857
|
+
return this.staticPose();
|
|
858
|
+
}
|
|
859
|
+
if (targetKeys.length === 1) {
|
|
860
|
+
return this.singleTargetPose(primary.position);
|
|
861
|
+
}
|
|
862
|
+
return this.multiTargetPose(ctx);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* No targets: use the user-specified initial position if available,
|
|
866
|
+
* otherwise fall back to a default pose behind the origin.
|
|
867
|
+
*/
|
|
868
|
+
staticPose() {
|
|
869
|
+
if (this.initialPosition) {
|
|
870
|
+
return {
|
|
871
|
+
position: this.initialPosition.clone(),
|
|
872
|
+
rotation: new Quaternion3(),
|
|
873
|
+
fov: this.opts.fov,
|
|
874
|
+
zoom: 1,
|
|
875
|
+
near: 0.1,
|
|
876
|
+
far: 1e3,
|
|
877
|
+
lookAt: this.initialLookAt?.clone() ?? new Vector33(0, 0, 0)
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const position = new Vector33(
|
|
881
|
+
this.opts.shoulderOffset,
|
|
882
|
+
this.opts.height,
|
|
883
|
+
this.opts.distance
|
|
884
|
+
);
|
|
885
|
+
return {
|
|
886
|
+
position,
|
|
887
|
+
rotation: new Quaternion3(),
|
|
888
|
+
fov: this.opts.fov,
|
|
889
|
+
zoom: 1,
|
|
890
|
+
near: 0.1,
|
|
891
|
+
far: 1e3,
|
|
892
|
+
lookAt: new Vector33(0, 0, 0)
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Single target: position = target + offset, lookAt = target.
|
|
897
|
+
*/
|
|
898
|
+
singleTargetPose(targetPos) {
|
|
899
|
+
const position = new Vector33(
|
|
900
|
+
targetPos.x + this.opts.shoulderOffset,
|
|
901
|
+
targetPos.y + this.opts.height,
|
|
902
|
+
targetPos.z + this.opts.distance
|
|
903
|
+
);
|
|
904
|
+
return {
|
|
905
|
+
position,
|
|
906
|
+
rotation: new Quaternion3(),
|
|
907
|
+
fov: this.opts.fov,
|
|
908
|
+
zoom: 1,
|
|
909
|
+
near: 0.1,
|
|
910
|
+
far: 1e3,
|
|
911
|
+
lookAt: targetPos.clone()
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Multi-target: compute centroid and adjust distance to frame all targets.
|
|
916
|
+
*/
|
|
917
|
+
multiTargetPose(ctx) {
|
|
918
|
+
const targets = Object.values(ctx.targets);
|
|
919
|
+
const centroid = new Vector33();
|
|
920
|
+
for (const t of targets) {
|
|
921
|
+
centroid.add(t.position);
|
|
922
|
+
}
|
|
923
|
+
centroid.divideScalar(targets.length);
|
|
924
|
+
let maxDist = 0;
|
|
925
|
+
for (const t of targets) {
|
|
926
|
+
const d = centroid.distanceTo(t.position);
|
|
927
|
+
if (d > maxDist) maxDist = d;
|
|
928
|
+
}
|
|
929
|
+
const dynamicDistance = Math.max(
|
|
930
|
+
maxDist * this.opts.paddingFactor,
|
|
931
|
+
this.opts.minDistance
|
|
932
|
+
);
|
|
933
|
+
const baseOffset = new Vector33(
|
|
934
|
+
this.opts.shoulderOffset,
|
|
935
|
+
this.opts.height,
|
|
936
|
+
this.opts.distance
|
|
937
|
+
);
|
|
938
|
+
const baseLen = baseOffset.length();
|
|
939
|
+
const dir = baseLen > 0 ? baseOffset.clone().normalize() : new Vector33(0, 0.5, 1).normalize();
|
|
940
|
+
const position = centroid.clone().add(dir.multiplyScalar(dynamicDistance));
|
|
941
|
+
if (baseLen > 0) {
|
|
942
|
+
const heightRatio = this.opts.height / baseLen;
|
|
943
|
+
position.y = centroid.y + dynamicDistance * heightRatio;
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
position,
|
|
947
|
+
rotation: new Quaternion3(),
|
|
948
|
+
fov: this.opts.fov,
|
|
949
|
+
zoom: 1,
|
|
950
|
+
near: 0.1,
|
|
951
|
+
far: 1e3,
|
|
952
|
+
lookAt: centroid.clone()
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// src/lib/camera/perspectives/fixed-2d-perspective.ts
|
|
958
|
+
import { Vector3 as Vector34, Quaternion as Quaternion4 } from "three";
|
|
959
|
+
var DEFAULTS2 = {
|
|
960
|
+
position: { x: 0, y: 0, z: 10 },
|
|
961
|
+
zoom: 10
|
|
962
|
+
};
|
|
963
|
+
var Fixed2DPerspective = class {
|
|
964
|
+
id = "fixed-2d";
|
|
965
|
+
defaults = { damping: 1 };
|
|
966
|
+
opts;
|
|
967
|
+
constructor(options) {
|
|
968
|
+
this.opts = { ...DEFAULTS2, ...options };
|
|
969
|
+
}
|
|
970
|
+
getBasePose(_ctx) {
|
|
971
|
+
return {
|
|
972
|
+
position: new Vector34(
|
|
973
|
+
this.opts.position.x,
|
|
974
|
+
this.opts.position.y,
|
|
975
|
+
this.opts.position.z
|
|
976
|
+
),
|
|
977
|
+
rotation: new Quaternion4(),
|
|
978
|
+
zoom: this.opts.zoom,
|
|
979
|
+
near: 1,
|
|
980
|
+
far: 1e3,
|
|
981
|
+
lookAt: new Vector34(this.opts.position.x, this.opts.position.y, 0)
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// src/lib/camera/perspectives/first-person-perspective.ts
|
|
987
|
+
import { Vector3 as Vector35, Quaternion as Quaternion5, Euler, MathUtils as MathUtils2 } from "three";
|
|
988
|
+
var DEFAULTS3 = {
|
|
989
|
+
eyeHeight: 1.7,
|
|
990
|
+
defaultFov: 75,
|
|
991
|
+
pitchLimit: Math.PI / 2 - 0.01,
|
|
992
|
+
lookAtLerpSpeed: 5,
|
|
993
|
+
fovLerpSpeed: 8,
|
|
994
|
+
targetKey: "primary"
|
|
995
|
+
};
|
|
996
|
+
var FirstPersonPerspective = class {
|
|
997
|
+
id = "first-person";
|
|
998
|
+
defaults = { damping: 1 };
|
|
999
|
+
opts;
|
|
1000
|
+
/** Fallback position when no target entity is attached. Mutate directly for manual movement. */
|
|
1001
|
+
initialPosition;
|
|
1002
|
+
initialLookAt;
|
|
1003
|
+
// --- Look state ---
|
|
1004
|
+
_yaw = 0;
|
|
1005
|
+
_pitch = 0;
|
|
1006
|
+
// --- FOV zoom state ---
|
|
1007
|
+
_currentFov;
|
|
1008
|
+
_targetFov;
|
|
1009
|
+
// --- Look-at target state ---
|
|
1010
|
+
_lookAtTarget = null;
|
|
1011
|
+
_lookAtLerpSpeed;
|
|
1012
|
+
_currentRotation = new Quaternion5();
|
|
1013
|
+
_rotationInitialized = false;
|
|
1014
|
+
constructor(options) {
|
|
1015
|
+
const { initialPosition, initialLookAt, ...rest } = options ?? {};
|
|
1016
|
+
this.opts = { ...DEFAULTS3, ...rest };
|
|
1017
|
+
this.initialPosition = initialPosition;
|
|
1018
|
+
this.initialLookAt = initialLookAt;
|
|
1019
|
+
this._currentFov = this.opts.defaultFov;
|
|
1020
|
+
this._targetFov = this.opts.defaultFov;
|
|
1021
|
+
this._lookAtLerpSpeed = this.opts.lookAtLerpSpeed;
|
|
1022
|
+
if (initialPosition && initialLookAt) {
|
|
1023
|
+
this.deriveYawPitchFromLookAt(initialPosition, initialLookAt);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// --- Public API (called by game code / FPS behavior) ---
|
|
1027
|
+
/** Accumulate yaw and pitch deltas. Pitch is clamped to the configured limit. */
|
|
1028
|
+
look(deltaYaw, deltaPitch) {
|
|
1029
|
+
this._yaw += deltaYaw;
|
|
1030
|
+
this._pitch = MathUtils2.clamp(
|
|
1031
|
+
this._pitch + deltaPitch,
|
|
1032
|
+
-this.opts.pitchLimit,
|
|
1033
|
+
this.opts.pitchLimit
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
/** Set absolute yaw and pitch. Pitch is clamped to the configured limit. */
|
|
1037
|
+
setLook(yaw, pitch) {
|
|
1038
|
+
this._yaw = yaw;
|
|
1039
|
+
this._pitch = MathUtils2.clamp(pitch, -this.opts.pitchLimit, this.opts.pitchLimit);
|
|
1040
|
+
}
|
|
1041
|
+
/** Current yaw in radians. */
|
|
1042
|
+
get yaw() {
|
|
1043
|
+
return this._yaw;
|
|
1044
|
+
}
|
|
1045
|
+
/** Current pitch in radians. */
|
|
1046
|
+
get pitch() {
|
|
1047
|
+
return this._pitch;
|
|
1048
|
+
}
|
|
1049
|
+
/** Set the target FOV for a smooth zoom transition (e.g. sniper scope). */
|
|
1050
|
+
zoom(fov) {
|
|
1051
|
+
this._targetFov = fov;
|
|
1052
|
+
}
|
|
1053
|
+
/** Return to the default FOV. */
|
|
1054
|
+
resetZoom() {
|
|
1055
|
+
this._targetFov = this.opts.defaultFov;
|
|
1056
|
+
}
|
|
1057
|
+
/** Current field of view. */
|
|
1058
|
+
get currentFov() {
|
|
1059
|
+
return this._currentFov;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Enable smooth look-at toward a world position.
|
|
1063
|
+
* The camera will slerp from the current rotation toward the look-at direction.
|
|
1064
|
+
*/
|
|
1065
|
+
lookAt(target, lerpSpeed) {
|
|
1066
|
+
this._lookAtTarget = target;
|
|
1067
|
+
if (lerpSpeed != null) {
|
|
1068
|
+
this._lookAtLerpSpeed = lerpSpeed;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/** Disable look-at and return to manual yaw/pitch control. */
|
|
1072
|
+
clearLookAt() {
|
|
1073
|
+
if (this._lookAtTarget) {
|
|
1074
|
+
this.deriveYawPitchFromQuaternion(this._currentRotation);
|
|
1075
|
+
}
|
|
1076
|
+
this._lookAtTarget = null;
|
|
1077
|
+
}
|
|
1078
|
+
// --- CameraPerspective interface ---
|
|
1079
|
+
getBasePose(ctx) {
|
|
1080
|
+
const position = this.computePosition(ctx);
|
|
1081
|
+
const rotation = this.computeRotation(position, ctx.dt);
|
|
1082
|
+
this._currentFov = this.lerpFov(ctx.dt);
|
|
1083
|
+
return {
|
|
1084
|
+
position,
|
|
1085
|
+
rotation,
|
|
1086
|
+
fov: this._currentFov,
|
|
1087
|
+
near: 0.1,
|
|
1088
|
+
far: 1e3
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
// --- Private helpers ---
|
|
1092
|
+
computePosition(ctx) {
|
|
1093
|
+
const target = ctx.targets[this.opts.targetKey];
|
|
1094
|
+
if (target) {
|
|
1095
|
+
return new Vector35(
|
|
1096
|
+
target.position.x,
|
|
1097
|
+
target.position.y + this.opts.eyeHeight,
|
|
1098
|
+
target.position.z
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
if (this.initialPosition) {
|
|
1102
|
+
return this.initialPosition.clone();
|
|
1103
|
+
}
|
|
1104
|
+
return new Vector35(0, this.opts.eyeHeight, 0);
|
|
1105
|
+
}
|
|
1106
|
+
computeRotation(eyePosition, dt) {
|
|
1107
|
+
const yawPitchQuat = new Quaternion5().setFromEuler(
|
|
1108
|
+
new Euler(this._pitch, this._yaw, 0, "YXZ")
|
|
1109
|
+
);
|
|
1110
|
+
if (!this._rotationInitialized) {
|
|
1111
|
+
this._currentRotation.copy(yawPitchQuat);
|
|
1112
|
+
this._rotationInitialized = true;
|
|
1113
|
+
}
|
|
1114
|
+
if (this._lookAtTarget) {
|
|
1115
|
+
const dir = _vec3.copy(this._lookAtTarget).sub(eyePosition);
|
|
1116
|
+
if (dir.lengthSq() > 1e-4) {
|
|
1117
|
+
dir.normalize();
|
|
1118
|
+
const desiredYaw = Math.atan2(-dir.x, -dir.z);
|
|
1119
|
+
const desiredPitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
|
|
1120
|
+
const desiredQuat = _quat.setFromEuler(
|
|
1121
|
+
_euler.set(desiredPitch, desiredYaw, 0, "YXZ")
|
|
1122
|
+
);
|
|
1123
|
+
const t = 1 - Math.pow(1 - Math.min(this._lookAtLerpSpeed * dt, 1), 1);
|
|
1124
|
+
this._currentRotation.slerp(desiredQuat, MathUtils2.clamp(t, 0, 1));
|
|
1125
|
+
return this._currentRotation.clone();
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
this._currentRotation.copy(yawPitchQuat);
|
|
1129
|
+
return yawPitchQuat;
|
|
1130
|
+
}
|
|
1131
|
+
lerpFov(dt) {
|
|
1132
|
+
if (Math.abs(this._currentFov - this._targetFov) < 0.01) {
|
|
1133
|
+
return this._targetFov;
|
|
1134
|
+
}
|
|
1135
|
+
const t = 1 - Math.pow(1 - Math.min(this.opts.fovLerpSpeed * dt, 1), 1);
|
|
1136
|
+
return MathUtils2.lerp(this._currentFov, this._targetFov, MathUtils2.clamp(t, 0, 1));
|
|
1137
|
+
}
|
|
1138
|
+
deriveYawPitchFromLookAt(from, to) {
|
|
1139
|
+
const dir = _vec3.copy(to).sub(from).normalize();
|
|
1140
|
+
this._yaw = Math.atan2(-dir.x, -dir.z);
|
|
1141
|
+
this._pitch = Math.asin(MathUtils2.clamp(dir.y, -1, 1));
|
|
1142
|
+
}
|
|
1143
|
+
deriveYawPitchFromQuaternion(q) {
|
|
1144
|
+
const euler = _euler.setFromQuaternion(q, "YXZ");
|
|
1145
|
+
this._yaw = euler.y;
|
|
1146
|
+
this._pitch = MathUtils2.clamp(euler.x, -this.opts.pitchLimit, this.opts.pitchLimit);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
var _vec3 = new Vector35();
|
|
1150
|
+
var _quat = new Quaternion5();
|
|
1151
|
+
var _euler = new Euler();
|
|
1152
|
+
|
|
1153
|
+
// src/lib/camera/perspectives/index.ts
|
|
1154
|
+
function createPerspective(type, options) {
|
|
1155
|
+
switch (type) {
|
|
1156
|
+
case "third-person":
|
|
1157
|
+
return new ThirdPersonPerspective(options);
|
|
1158
|
+
case "first-person":
|
|
1159
|
+
return new FirstPersonPerspective(options);
|
|
1160
|
+
case "isometric":
|
|
1161
|
+
return new ThirdPersonPerspective({
|
|
1162
|
+
distance: 10,
|
|
1163
|
+
height: 10,
|
|
1164
|
+
...options
|
|
1165
|
+
});
|
|
1166
|
+
case "flat-2d":
|
|
1167
|
+
return new Fixed2DPerspective(options);
|
|
1168
|
+
case "fixed-2d":
|
|
1169
|
+
return new Fixed2DPerspective(options);
|
|
1170
|
+
default:
|
|
1171
|
+
return new ThirdPersonPerspective(options);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
756
1175
|
// src/lib/camera/zylem-camera.ts
|
|
757
1176
|
var ZylemCamera = class {
|
|
1177
|
+
/**
|
|
1178
|
+
* @deprecated No longer used. Kept as null for backward compatibility
|
|
1179
|
+
* with code that checks `camera.cameraRig` (e.g. scene graph insertion).
|
|
1180
|
+
*/
|
|
758
1181
|
cameraRig = null;
|
|
759
1182
|
camera;
|
|
760
1183
|
screenResolution;
|
|
@@ -762,7 +1185,7 @@ var ZylemCamera = class {
|
|
|
762
1185
|
frustumSize = 10;
|
|
763
1186
|
rendererType;
|
|
764
1187
|
sceneRef = null;
|
|
765
|
-
/** Name for camera manager lookup */
|
|
1188
|
+
/** Name for camera manager lookup. */
|
|
766
1189
|
name = "";
|
|
767
1190
|
/**
|
|
768
1191
|
* Viewport in normalized coordinates (0-1).
|
|
@@ -771,11 +1194,21 @@ var ZylemCamera = class {
|
|
|
771
1194
|
viewport = { ...DEFAULT_VIEWPORT };
|
|
772
1195
|
/**
|
|
773
1196
|
* Multiple targets for the camera to follow/frame.
|
|
774
|
-
* Replaces the old single `target` property.
|
|
775
1197
|
*/
|
|
776
1198
|
targets = [];
|
|
777
1199
|
/**
|
|
778
|
-
*
|
|
1200
|
+
* The camera pose pipeline.
|
|
1201
|
+
* Exposed so CameraWrapper can delegate addBehavior/addAction/getState.
|
|
1202
|
+
*/
|
|
1203
|
+
pipeline;
|
|
1204
|
+
/**
|
|
1205
|
+
* Offscreen render target for render-to-texture (RTT) cameras.
|
|
1206
|
+
* When set, the camera renders to this target instead of the screen viewport.
|
|
1207
|
+
* Created via createRenderTarget() or automatically by setCameraFeed().
|
|
1208
|
+
*/
|
|
1209
|
+
renderTarget = null;
|
|
1210
|
+
/**
|
|
1211
|
+
* @deprecated Use `targets` array instead. Kept for backward compatibility.
|
|
779
1212
|
*/
|
|
780
1213
|
get target() {
|
|
781
1214
|
return this.targets.length > 0 ? this.targets[0] : null;
|
|
@@ -791,13 +1224,15 @@ var ZylemCamera = class {
|
|
|
791
1224
|
this.targets = [];
|
|
792
1225
|
}
|
|
793
1226
|
}
|
|
794
|
-
// Perspective controller delegation
|
|
795
|
-
perspectiveController = null;
|
|
796
1227
|
// Orbit controls
|
|
797
1228
|
orbitController = null;
|
|
798
1229
|
_useOrbitalControls = false;
|
|
799
|
-
/**
|
|
1230
|
+
/** When true, debug-mode orbital controls are not attached to this camera. */
|
|
1231
|
+
_skipDebugOrbit = false;
|
|
1232
|
+
/** Reference to the shared renderer manager (set during setup). */
|
|
800
1233
|
_rendererManager = null;
|
|
1234
|
+
/** Elapsed time tracker for CameraContext. */
|
|
1235
|
+
_elapsedTime = 0;
|
|
801
1236
|
constructor(perspective, screenResolution, frustumSize = 10, rendererType = "webgl") {
|
|
802
1237
|
this._perspective = perspective;
|
|
803
1238
|
this.screenResolution = screenResolution;
|
|
@@ -805,20 +1240,13 @@ var ZylemCamera = class {
|
|
|
805
1240
|
this.rendererType = rendererType;
|
|
806
1241
|
const aspectRatio = screenResolution.x / screenResolution.y;
|
|
807
1242
|
this.camera = this.createCameraForPerspective(aspectRatio);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
this.camera.lookAt(new Vector33(0, 2, 0));
|
|
813
|
-
} else {
|
|
814
|
-
this.camera.position.set(0, 0, 10);
|
|
815
|
-
this.camera.lookAt(new Vector33(0, 0, 0));
|
|
816
|
-
}
|
|
817
|
-
this.initializePerspectiveController();
|
|
1243
|
+
this.camera.position.set(0, 0, 10);
|
|
1244
|
+
this.camera.lookAt(new Vector36(0, 0, 0));
|
|
1245
|
+
const perspectiveImpl = createPerspective(perspective);
|
|
1246
|
+
this.pipeline = new CameraPipeline(perspectiveImpl);
|
|
818
1247
|
}
|
|
819
1248
|
/**
|
|
820
1249
|
* Setup the camera with a scene and renderer manager.
|
|
821
|
-
* The renderer manager provides shared rendering infrastructure.
|
|
822
1250
|
*/
|
|
823
1251
|
async setup(scene, rendererManager) {
|
|
824
1252
|
this.sceneRef = scene;
|
|
@@ -828,19 +1256,12 @@ var ZylemCamera = class {
|
|
|
828
1256
|
if (this._rendererManager && !this._rendererManager.initialized) {
|
|
829
1257
|
await this._rendererManager.initRenderer();
|
|
830
1258
|
}
|
|
831
|
-
if (this.perspectiveController && this._rendererManager) {
|
|
832
|
-
this.perspectiveController.setup({
|
|
833
|
-
screenResolution: this.screenResolution,
|
|
834
|
-
renderer: this._rendererManager.renderer,
|
|
835
|
-
scene,
|
|
836
|
-
camera: this
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
1259
|
if (this._rendererManager) {
|
|
840
1260
|
this.orbitController = new CameraOrbitController(
|
|
841
1261
|
this.camera,
|
|
842
1262
|
this._rendererManager.renderer.domElement,
|
|
843
|
-
|
|
1263
|
+
null
|
|
1264
|
+
// no camera rig
|
|
844
1265
|
);
|
|
845
1266
|
this.orbitController.setScene(scene);
|
|
846
1267
|
if (this._useOrbitalControls) {
|
|
@@ -868,17 +1289,24 @@ var ZylemCamera = class {
|
|
|
868
1289
|
await this.setup(scene, this._rendererManager);
|
|
869
1290
|
}
|
|
870
1291
|
/**
|
|
871
|
-
* Update camera
|
|
872
|
-
*
|
|
1292
|
+
* Update the camera each frame.
|
|
1293
|
+
*
|
|
1294
|
+
* When orbit/debug controls are active, the pipeline is skipped and
|
|
1295
|
+
* orbit controls manage the camera directly. Otherwise, the pipeline
|
|
1296
|
+
* runs: Perspective -> Behaviors -> Actions -> Smoothing -> Commit.
|
|
873
1297
|
*/
|
|
874
1298
|
update(delta) {
|
|
875
1299
|
this.orbitController?.update();
|
|
876
|
-
if (this.
|
|
877
|
-
|
|
1300
|
+
if (this.isDebugModeActive() || this._useOrbitalControls) {
|
|
1301
|
+
return;
|
|
878
1302
|
}
|
|
1303
|
+
this._elapsedTime += delta;
|
|
1304
|
+
const ctx = this.buildContext(delta);
|
|
1305
|
+
const finalPose = this.pipeline.run(ctx);
|
|
1306
|
+
this.commitPose(finalPose);
|
|
879
1307
|
}
|
|
880
1308
|
/**
|
|
881
|
-
* Check if debug mode is active (orbit controls taking over camera)
|
|
1309
|
+
* Check if debug mode is active (orbit controls taking over camera).
|
|
882
1310
|
*/
|
|
883
1311
|
isDebugModeActive() {
|
|
884
1312
|
return this.orbitController?.isActive ?? false;
|
|
@@ -903,6 +1331,7 @@ var ZylemCamera = class {
|
|
|
903
1331
|
get useOrbitalControls() {
|
|
904
1332
|
return this._useOrbitalControls;
|
|
905
1333
|
}
|
|
1334
|
+
// ─── Target management ──────────────────────────────────────────────────
|
|
906
1335
|
/**
|
|
907
1336
|
* Add a target entity for the camera to follow/frame.
|
|
908
1337
|
*/
|
|
@@ -926,23 +1355,12 @@ var ZylemCamera = class {
|
|
|
926
1355
|
clearTargets() {
|
|
927
1356
|
this.targets = [];
|
|
928
1357
|
}
|
|
1358
|
+
// ─── Viewport & resize ──────────────────────────────────────────────────
|
|
929
1359
|
/**
|
|
930
|
-
*
|
|
931
|
-
*/
|
|
932
|
-
destroy() {
|
|
933
|
-
try {
|
|
934
|
-
this.orbitController?.dispose();
|
|
935
|
-
} catch {
|
|
936
|
-
}
|
|
937
|
-
this.sceneRef = null;
|
|
938
|
-
this.targets = [];
|
|
939
|
-
this._rendererManager = null;
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Attach a delegate to react to debug state changes.
|
|
1360
|
+
* Set the viewport for this camera (normalized 0-1 coordinates).
|
|
943
1361
|
*/
|
|
944
|
-
|
|
945
|
-
this.
|
|
1362
|
+
setViewport(x, y, width, height) {
|
|
1363
|
+
this.viewport = { x, y, width, height };
|
|
946
1364
|
}
|
|
947
1365
|
/**
|
|
948
1366
|
* Resize camera projection.
|
|
@@ -953,110 +1371,87 @@ var ZylemCamera = class {
|
|
|
953
1371
|
this.camera.aspect = width / height;
|
|
954
1372
|
this.camera.updateProjectionMatrix();
|
|
955
1373
|
}
|
|
956
|
-
if (this.
|
|
957
|
-
|
|
1374
|
+
if (this.camera instanceof OrthographicCamera) {
|
|
1375
|
+
const aspect = width / height;
|
|
1376
|
+
this.camera.left = this.frustumSize * aspect / -2;
|
|
1377
|
+
this.camera.right = this.frustumSize * aspect / 2;
|
|
1378
|
+
this.camera.top = this.frustumSize / 2;
|
|
1379
|
+
this.camera.bottom = this.frustumSize / -2;
|
|
1380
|
+
this.camera.updateProjectionMatrix();
|
|
958
1381
|
}
|
|
959
1382
|
}
|
|
1383
|
+
// ─── Render-to-texture ─────────────────────────────────────────────────
|
|
960
1384
|
/**
|
|
961
|
-
*
|
|
1385
|
+
* Create an offscreen render target for this camera.
|
|
1386
|
+
* When a render target is present, CameraManager renders this camera
|
|
1387
|
+
* to the target instead of the screen viewport.
|
|
1388
|
+
*
|
|
1389
|
+
* @param width Texture width in pixels (default 512)
|
|
1390
|
+
* @param height Texture height in pixels (default 512)
|
|
962
1391
|
*/
|
|
963
|
-
|
|
964
|
-
this.
|
|
1392
|
+
createRenderTarget(width = 512, height = 512) {
|
|
1393
|
+
if (this.renderTarget) {
|
|
1394
|
+
this.renderTarget.dispose();
|
|
1395
|
+
}
|
|
1396
|
+
this.renderTarget = new WebGLRenderTarget2(width, height, {
|
|
1397
|
+
minFilter: LinearFilter,
|
|
1398
|
+
magFilter: LinearFilter
|
|
1399
|
+
});
|
|
1400
|
+
return this.renderTarget;
|
|
965
1401
|
}
|
|
966
1402
|
/**
|
|
967
|
-
*
|
|
1403
|
+
* Get the texture from the render target (for applying to a mesh material).
|
|
1404
|
+
* Returns null if no render target has been created.
|
|
968
1405
|
*/
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
case Perspectives.ThirdPerson:
|
|
972
|
-
return this.createThirdPersonCamera(aspectRatio);
|
|
973
|
-
case Perspectives.FirstPerson:
|
|
974
|
-
return this.createFirstPersonCamera(aspectRatio);
|
|
975
|
-
case Perspectives.Isometric:
|
|
976
|
-
return this.createIsometricCamera(aspectRatio);
|
|
977
|
-
case Perspectives.Flat2D:
|
|
978
|
-
return this.createFlat2DCamera(aspectRatio);
|
|
979
|
-
case Perspectives.Fixed2D:
|
|
980
|
-
return this.createFixed2DCamera(aspectRatio);
|
|
981
|
-
default:
|
|
982
|
-
return this.createThirdPersonCamera(aspectRatio);
|
|
983
|
-
}
|
|
1406
|
+
getRenderTexture() {
|
|
1407
|
+
return this.renderTarget?.texture ?? null;
|
|
984
1408
|
}
|
|
1409
|
+
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
985
1410
|
/**
|
|
986
|
-
*
|
|
1411
|
+
* Dispose camera resources.
|
|
987
1412
|
*/
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
break;
|
|
993
|
-
case Perspectives.Fixed2D:
|
|
994
|
-
this.perspectiveController = new Fixed2DCamera();
|
|
995
|
-
break;
|
|
996
|
-
default:
|
|
997
|
-
this.perspectiveController = new ThirdPersonCamera();
|
|
1413
|
+
destroy() {
|
|
1414
|
+
try {
|
|
1415
|
+
this.orbitController?.dispose();
|
|
1416
|
+
} catch {
|
|
998
1417
|
}
|
|
1418
|
+
try {
|
|
1419
|
+
this.renderTarget?.dispose();
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
this.renderTarget = null;
|
|
1423
|
+
this.sceneRef = null;
|
|
1424
|
+
this.targets = [];
|
|
1425
|
+
this._rendererManager = null;
|
|
999
1426
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
this.frustumSize * aspectRatio / -2,
|
|
1009
|
-
this.frustumSize * aspectRatio / 2,
|
|
1010
|
-
this.frustumSize / 2,
|
|
1011
|
-
this.frustumSize / -2,
|
|
1012
|
-
1,
|
|
1013
|
-
1e3
|
|
1014
|
-
);
|
|
1015
|
-
}
|
|
1016
|
-
createFlat2DCamera(aspectRatio) {
|
|
1017
|
-
return new OrthographicCamera(
|
|
1018
|
-
this.frustumSize * aspectRatio / -2,
|
|
1019
|
-
this.frustumSize * aspectRatio / 2,
|
|
1020
|
-
this.frustumSize / 2,
|
|
1021
|
-
this.frustumSize / -2,
|
|
1022
|
-
1,
|
|
1023
|
-
1e3
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
1026
|
-
createFixed2DCamera(aspectRatio) {
|
|
1027
|
-
return this.createFlat2DCamera(aspectRatio);
|
|
1427
|
+
// ─── Debug delegate ─────────────────────────────────────────────────────
|
|
1428
|
+
/**
|
|
1429
|
+
* Attach a delegate to react to debug state changes.
|
|
1430
|
+
* Skipped when _skipDebugOrbit is true so the pipeline always runs.
|
|
1431
|
+
*/
|
|
1432
|
+
setDebugDelegate(delegate) {
|
|
1433
|
+
if (this._skipDebugOrbit) return;
|
|
1434
|
+
this.orbitController?.setDebugDelegate(delegate);
|
|
1028
1435
|
}
|
|
1029
|
-
// Movement
|
|
1030
|
-
|
|
1436
|
+
// ─── Movement helpers (backward compat) ─────────────────────────────────
|
|
1437
|
+
/**
|
|
1438
|
+
* Directly set the camera position.
|
|
1439
|
+
*/
|
|
1440
|
+
move(position) {
|
|
1031
1441
|
if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
|
|
1032
1442
|
this.frustumSize = position.z;
|
|
1033
1443
|
}
|
|
1034
|
-
|
|
1035
|
-
this.cameraRig.position.set(position.x, position.y, position.z);
|
|
1036
|
-
} else {
|
|
1037
|
-
this.camera.position.set(position.x, position.y, position.z);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
move(position) {
|
|
1041
|
-
this.moveCamera(position);
|
|
1042
|
-
}
|
|
1043
|
-
rotate(pitch, yaw, roll) {
|
|
1044
|
-
if (this.cameraRig) {
|
|
1045
|
-
this.cameraRig.rotateX(pitch);
|
|
1046
|
-
this.cameraRig.rotateY(yaw);
|
|
1047
|
-
this.cameraRig.rotateZ(roll);
|
|
1048
|
-
} else {
|
|
1049
|
-
this.camera.rotateX(pitch);
|
|
1050
|
-
this.camera.rotateY(yaw);
|
|
1051
|
-
this.camera.rotateZ(roll);
|
|
1052
|
-
}
|
|
1444
|
+
this.camera.position.set(position.x, position.y, position.z);
|
|
1053
1445
|
}
|
|
1054
1446
|
/**
|
|
1055
|
-
*
|
|
1447
|
+
* Apply incremental rotation to the camera.
|
|
1056
1448
|
*/
|
|
1057
|
-
|
|
1058
|
-
|
|
1449
|
+
rotate(pitch, yaw, roll) {
|
|
1450
|
+
this.camera.rotateX(pitch);
|
|
1451
|
+
this.camera.rotateY(yaw);
|
|
1452
|
+
this.camera.rotateZ(roll);
|
|
1059
1453
|
}
|
|
1454
|
+
// ─── Renderer manager access ────────────────────────────────────────────
|
|
1060
1455
|
/**
|
|
1061
1456
|
* Get the DOM element for the renderer.
|
|
1062
1457
|
* @deprecated Access via RendererManager instead.
|
|
@@ -1080,24 +1475,108 @@ var ZylemCamera = class {
|
|
|
1080
1475
|
this._rendererManager = manager;
|
|
1081
1476
|
}
|
|
1082
1477
|
// ─── Legacy compatibility methods ────────────────────────────────────────
|
|
1083
|
-
/**
|
|
1084
|
-
* @deprecated Renderer is now owned by RendererManager
|
|
1085
|
-
*/
|
|
1478
|
+
/** @deprecated Renderer is now owned by RendererManager */
|
|
1086
1479
|
get renderer() {
|
|
1087
1480
|
return this._rendererManager?.renderer;
|
|
1088
1481
|
}
|
|
1089
|
-
/**
|
|
1090
|
-
* @deprecated Composer is now owned by RendererManager
|
|
1091
|
-
*/
|
|
1482
|
+
/** @deprecated Composer is now owned by RendererManager */
|
|
1092
1483
|
get composer() {
|
|
1093
1484
|
return this._rendererManager?.composer;
|
|
1094
1485
|
}
|
|
1095
|
-
/**
|
|
1096
|
-
* @deprecated Use RendererManager.setPixelRatio() instead
|
|
1097
|
-
*/
|
|
1486
|
+
/** @deprecated Use RendererManager.setPixelRatio() instead */
|
|
1098
1487
|
setPixelRatio(dpr) {
|
|
1099
1488
|
this._rendererManager?.setPixelRatio(dpr);
|
|
1100
1489
|
}
|
|
1490
|
+
// ─── Private helpers ────────────────────────────────────────────────────
|
|
1491
|
+
/**
|
|
1492
|
+
* Build a CameraContext from current ZylemCamera state.
|
|
1493
|
+
* Converts StageEntity[] targets into Record<string, TransformLike>.
|
|
1494
|
+
*/
|
|
1495
|
+
buildContext(delta) {
|
|
1496
|
+
const targets = {};
|
|
1497
|
+
for (let i = 0; i < this.targets.length; i++) {
|
|
1498
|
+
const entity = this.targets[i];
|
|
1499
|
+
const key = i === 0 ? "primary" : `target_${i}`;
|
|
1500
|
+
if (entity.group) {
|
|
1501
|
+
targets[key] = {
|
|
1502
|
+
position: entity.group.position,
|
|
1503
|
+
rotation: entity.group.quaternion
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
dt: delta,
|
|
1509
|
+
time: this._elapsedTime,
|
|
1510
|
+
viewport: {
|
|
1511
|
+
width: this.screenResolution.x,
|
|
1512
|
+
height: this.screenResolution.y,
|
|
1513
|
+
aspect: this.screenResolution.x / this.screenResolution.y
|
|
1514
|
+
},
|
|
1515
|
+
targets
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Apply the final pipeline pose to the Three.js camera.
|
|
1520
|
+
*/
|
|
1521
|
+
commitPose(pose) {
|
|
1522
|
+
this.camera.position.copy(pose.position);
|
|
1523
|
+
if (pose.lookAt) {
|
|
1524
|
+
this.camera.lookAt(pose.lookAt);
|
|
1525
|
+
} else {
|
|
1526
|
+
this.camera.quaternion.copy(pose.rotation);
|
|
1527
|
+
}
|
|
1528
|
+
if (this.camera instanceof PerspectiveCamera2) {
|
|
1529
|
+
if (pose.fov != null) this.camera.fov = pose.fov;
|
|
1530
|
+
if (pose.near != null) this.camera.near = pose.near;
|
|
1531
|
+
if (pose.far != null) this.camera.far = pose.far;
|
|
1532
|
+
this.camera.updateProjectionMatrix();
|
|
1533
|
+
}
|
|
1534
|
+
if (this.camera instanceof OrthographicCamera) {
|
|
1535
|
+
if (pose.zoom != null) {
|
|
1536
|
+
const aspect = this.screenResolution.x / this.screenResolution.y;
|
|
1537
|
+
const size = pose.zoom;
|
|
1538
|
+
this.camera.left = -size * aspect / 2;
|
|
1539
|
+
this.camera.right = size * aspect / 2;
|
|
1540
|
+
this.camera.top = size / 2;
|
|
1541
|
+
this.camera.bottom = -size / 2;
|
|
1542
|
+
}
|
|
1543
|
+
if (pose.near != null) this.camera.near = pose.near;
|
|
1544
|
+
if (pose.far != null) this.camera.far = pose.far;
|
|
1545
|
+
this.camera.updateProjectionMatrix();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Create a Three.js camera based on perspective type.
|
|
1550
|
+
*/
|
|
1551
|
+
createCameraForPerspective(aspectRatio) {
|
|
1552
|
+
switch (this._perspective) {
|
|
1553
|
+
case Perspectives.ThirdPerson:
|
|
1554
|
+
return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
|
|
1555
|
+
case Perspectives.FirstPerson:
|
|
1556
|
+
return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
|
|
1557
|
+
case Perspectives.Isometric:
|
|
1558
|
+
return new OrthographicCamera(
|
|
1559
|
+
this.frustumSize * aspectRatio / -2,
|
|
1560
|
+
this.frustumSize * aspectRatio / 2,
|
|
1561
|
+
this.frustumSize / 2,
|
|
1562
|
+
this.frustumSize / -2,
|
|
1563
|
+
1,
|
|
1564
|
+
1e3
|
|
1565
|
+
);
|
|
1566
|
+
case Perspectives.Flat2D:
|
|
1567
|
+
case Perspectives.Fixed2D:
|
|
1568
|
+
return new OrthographicCamera(
|
|
1569
|
+
this.frustumSize * aspectRatio / -2,
|
|
1570
|
+
this.frustumSize * aspectRatio / 2,
|
|
1571
|
+
this.frustumSize / 2,
|
|
1572
|
+
this.frustumSize / -2,
|
|
1573
|
+
1,
|
|
1574
|
+
1e3
|
|
1575
|
+
);
|
|
1576
|
+
default:
|
|
1577
|
+
return new PerspectiveCamera2(75, aspectRatio, 0.1, 1e3);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1101
1580
|
};
|
|
1102
1581
|
|
|
1103
1582
|
// src/lib/camera/camera.ts
|
|
@@ -1151,9 +1630,73 @@ var CameraWrapper = class {
|
|
|
1151
1630
|
setViewport(x, y, width, height) {
|
|
1152
1631
|
this.cameraRef.setViewport(x, y, width, height);
|
|
1153
1632
|
}
|
|
1633
|
+
// ─── Pipeline: Behaviors ────────────────────────────────────────────────
|
|
1634
|
+
/**
|
|
1635
|
+
* Add or replace a behavior by key (idempotent).
|
|
1636
|
+
* Behaviors modify the desired camera pose each frame.
|
|
1637
|
+
*
|
|
1638
|
+
* @param key Unique key for this behavior (used for replacement/removal).
|
|
1639
|
+
* @param behavior The CameraBehavior implementation.
|
|
1640
|
+
*/
|
|
1641
|
+
addBehavior(key, behavior) {
|
|
1642
|
+
this.cameraRef.pipeline.addBehavior(key, behavior);
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Remove a behavior by key.
|
|
1646
|
+
*/
|
|
1647
|
+
removeBehavior(key) {
|
|
1648
|
+
return this.cameraRef.pipeline.removeBehavior(key);
|
|
1649
|
+
}
|
|
1650
|
+
// ─── Pipeline: Actions ──────────────────────────────────────────────────
|
|
1651
|
+
/**
|
|
1652
|
+
* Add a transient action (screenshake, recoil, etc.).
|
|
1653
|
+
* Actions apply additive deltas and self-expire when isDone() returns true.
|
|
1654
|
+
*/
|
|
1655
|
+
addAction(action) {
|
|
1656
|
+
this.cameraRef.pipeline.addAction(action);
|
|
1657
|
+
}
|
|
1658
|
+
// ─── Pipeline: Perspective ──────────────────────────────────────────────
|
|
1659
|
+
/**
|
|
1660
|
+
* Switch the camera's active perspective at runtime.
|
|
1661
|
+
* The first frame after switching snaps to the new pose (no lerp).
|
|
1662
|
+
*
|
|
1663
|
+
* @param type Perspective type string (e.g. Perspectives.ThirdPerson).
|
|
1664
|
+
* @param options Perspective-specific options (distance, height, zoom, etc.).
|
|
1665
|
+
*/
|
|
1666
|
+
setPerspective(type, options) {
|
|
1667
|
+
this.cameraRef.pipeline.setPerspective(createPerspective(type, options));
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Retrieve the active perspective instance, cast to the desired type.
|
|
1671
|
+
* Useful for calling perspective-specific methods (e.g. FirstPersonPerspective.look()).
|
|
1672
|
+
*
|
|
1673
|
+
* @example
|
|
1674
|
+
* const fps = camera.getPerspective<FirstPersonPerspective>();
|
|
1675
|
+
* fps.look(dx, dy);
|
|
1676
|
+
*/
|
|
1677
|
+
getPerspective() {
|
|
1678
|
+
return this.cameraRef.pipeline.perspective;
|
|
1679
|
+
}
|
|
1680
|
+
// ─── Pipeline: Debug state ──────────────────────────────────────────────
|
|
1681
|
+
/**
|
|
1682
|
+
* Return a debug snapshot of the camera pipeline state.
|
|
1683
|
+
* Includes: active perspective, desired/final pose, behavior keys, action count.
|
|
1684
|
+
*/
|
|
1685
|
+
getState() {
|
|
1686
|
+
return this.cameraRef.pipeline.getState();
|
|
1687
|
+
}
|
|
1688
|
+
// ─── Render-to-texture ─────────────────────────────────────────────────
|
|
1689
|
+
/**
|
|
1690
|
+
* Get the offscreen render texture for this camera.
|
|
1691
|
+
* Returns null if the camera was not created with renderToTexture.
|
|
1692
|
+
* Use with setCameraFeed() or apply directly to a mesh material.
|
|
1693
|
+
*/
|
|
1694
|
+
getRenderTexture() {
|
|
1695
|
+
return this.cameraRef.getRenderTexture();
|
|
1696
|
+
}
|
|
1154
1697
|
};
|
|
1155
1698
|
function createCamera(options) {
|
|
1156
|
-
const screenResolution = options.screenResolution || new
|
|
1699
|
+
const screenResolution = options.screenResolution || new Vector23(window.innerWidth, window.innerHeight);
|
|
1157
1700
|
let frustumSize = 10;
|
|
1158
1701
|
if (options.perspective === "fixed-2d") {
|
|
1159
1702
|
frustumSize = options.zoom || 10;
|
|
@@ -1167,21 +1710,49 @@ function createCamera(options) {
|
|
|
1167
1710
|
if (options.name) {
|
|
1168
1711
|
zylemCamera.name = options.name;
|
|
1169
1712
|
}
|
|
1170
|
-
const position = options.position ? options.position instanceof
|
|
1171
|
-
const target = options.target ? options.target instanceof
|
|
1713
|
+
const position = options.position ? options.position instanceof Vector37 ? options.position : new Vector37(options.position.x, options.position.y, options.position.z) : new Vector37(0, 0, 0);
|
|
1714
|
+
const target = options.target ? options.target instanceof Vector37 ? options.target : new Vector37(options.target.x, options.target.y, options.target.z) : new Vector37(0, 0, 0);
|
|
1172
1715
|
zylemCamera.move(position);
|
|
1173
1716
|
zylemCamera.camera.lookAt(target);
|
|
1717
|
+
const perspType = options.perspective || "third-person";
|
|
1718
|
+
if (perspType === "fixed-2d" || perspType === "flat-2d") {
|
|
1719
|
+
zylemCamera.pipeline.setPerspective(
|
|
1720
|
+
createPerspective(perspType, { zoom: frustumSize })
|
|
1721
|
+
);
|
|
1722
|
+
} else {
|
|
1723
|
+
zylemCamera.pipeline.setPerspective(
|
|
1724
|
+
createPerspective(perspType, {
|
|
1725
|
+
initialPosition: position.clone(),
|
|
1726
|
+
initialLookAt: target.clone()
|
|
1727
|
+
})
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1174
1730
|
if (options.viewport) {
|
|
1175
1731
|
zylemCamera.viewport = { ...options.viewport };
|
|
1176
1732
|
}
|
|
1177
1733
|
if (options.useOrbitalControls) {
|
|
1178
1734
|
zylemCamera._useOrbitalControls = true;
|
|
1179
1735
|
}
|
|
1736
|
+
if (options.skipDebugOrbit) {
|
|
1737
|
+
zylemCamera._skipDebugOrbit = true;
|
|
1738
|
+
}
|
|
1739
|
+
if (options.damping != null) {
|
|
1740
|
+
zylemCamera.pipeline.damping = options.damping;
|
|
1741
|
+
}
|
|
1742
|
+
if (options.behaviors) {
|
|
1743
|
+
for (const [key, behavior] of Object.entries(options.behaviors)) {
|
|
1744
|
+
zylemCamera.pipeline.addBehavior(key, behavior);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
if (options.renderToTexture) {
|
|
1748
|
+
const { width = 512, height = 512 } = options.renderToTexture;
|
|
1749
|
+
zylemCamera.createRenderTarget(width, height);
|
|
1750
|
+
}
|
|
1180
1751
|
return new CameraWrapper(zylemCamera);
|
|
1181
1752
|
}
|
|
1182
1753
|
|
|
1183
1754
|
// src/lib/camera/camera-manager.ts
|
|
1184
|
-
import { Vector2 as
|
|
1755
|
+
import { Vector2 as Vector24 } from "three";
|
|
1185
1756
|
var CameraManager = class {
|
|
1186
1757
|
/** Named camera registry */
|
|
1187
1758
|
cameras = /* @__PURE__ */ new Map();
|
|
@@ -1344,17 +1915,33 @@ var CameraManager = class {
|
|
|
1344
1915
|
}
|
|
1345
1916
|
/**
|
|
1346
1917
|
* Render all active cameras through the renderer manager.
|
|
1918
|
+
* RTT cameras (those with a renderTarget) are rendered first to their
|
|
1919
|
+
* offscreen textures, then viewport cameras are rendered to the screen.
|
|
1347
1920
|
*/
|
|
1348
1921
|
render(scene) {
|
|
1349
1922
|
if (!this._rendererManager || this._activeCameras.length === 0) return;
|
|
1350
|
-
|
|
1923
|
+
const rttCameras = [];
|
|
1924
|
+
const viewportCameras = [];
|
|
1925
|
+
for (const cam of this._activeCameras) {
|
|
1926
|
+
if (cam.renderTarget) {
|
|
1927
|
+
rttCameras.push(cam);
|
|
1928
|
+
} else {
|
|
1929
|
+
viewportCameras.push(cam);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
for (const cam of rttCameras) {
|
|
1933
|
+
this._rendererManager.renderCameraToTarget(scene, cam);
|
|
1934
|
+
}
|
|
1935
|
+
if (viewportCameras.length > 0) {
|
|
1936
|
+
this._rendererManager.renderCameras(scene, viewportCameras);
|
|
1937
|
+
}
|
|
1351
1938
|
}
|
|
1352
1939
|
/**
|
|
1353
1940
|
* Create a default third-person camera if no cameras have been added.
|
|
1354
1941
|
*/
|
|
1355
1942
|
ensureDefaultCamera() {
|
|
1356
1943
|
if (this.cameras.size === 0 || this._activeCameras.length === 0) {
|
|
1357
|
-
const screenRes = this._rendererManager?.screenResolution || new
|
|
1944
|
+
const screenRes = this._rendererManager?.screenResolution || new Vector24(window.innerWidth, window.innerHeight);
|
|
1358
1945
|
const defaultCam = new ZylemCamera(Perspectives.ThirdPerson, screenRes);
|
|
1359
1946
|
this.addCamera(defaultCam, "default");
|
|
1360
1947
|
return defaultCam;
|