@combos-fun/plugin-renderer-3d 0.0.41 → 0.0.44

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.
@@ -1,7 +1,14 @@
1
- import { OBSERVER_TYPE, System, resource } from '@combos-fun/engine';
2
- import { Scene, PerspectiveCamera, WebGLRenderer, AmbientLight, DirectionalLight, Clock, AnimationMixer, Texture, SRGBColorSpace } from 'three';
1
+ import { __decorate } from 'tslib';
2
+ import { OBSERVER_TYPE, Component, System, decorators, resource } from '@combos-fun/engine';
3
+ import { Vector3, Euler, Scene, PerspectiveCamera, WebGLRenderer, CineonToneMapping, ReinhardToneMapping, LinearToneMapping, ACESFilmicToneMapping, NoToneMapping, PCFSoftShadowMap, AmbientLight, DirectionalLight, HemisphereLight, Fog, Clock, AnimationMixer, Group, SpotLight, PointLight, Texture, SRGBColorSpace } from 'three';
4
+ import { Field } from '@combos-fun/inspector-decorator';
3
5
 
4
6
  const COMBOS_GAME_OBJECT_ID = 'combosGameObjectId';
7
+ const COMBOS_INSTANCE_GAME_OBJECT_IDS = 'combosInstanceGameObjectIds';
8
+ const COMBOS_TRANSFORM3D = 'combosTransform3D';
9
+ function isTransform3DRoot(object) {
10
+ return !!object?.userData?.[COMBOS_TRANSFORM3D];
11
+ }
5
12
  /** Stamp a Three.js object (and its children) so Event3D raycasts can resolve the GameObject. */
6
13
  function tagObject3D(object, gameObjectId) {
7
14
  object.traverse((child) => {
@@ -19,15 +26,130 @@ function gameObjectIdFromObject3D(object) {
19
26
  }
20
27
  return undefined;
21
28
  }
29
+ /** Resolve a raycast hit, including InstancedMesh `instanceId` rows. */
30
+ function gameObjectIdFromIntersection(intersection) {
31
+ if (!intersection?.object)
32
+ return undefined;
33
+ if (typeof intersection.instanceId === 'number') {
34
+ let current = intersection.object;
35
+ while (current) {
36
+ const ids = current.userData?.[COMBOS_INSTANCE_GAME_OBJECT_IDS];
37
+ if (Array.isArray(ids) && typeof ids[intersection.instanceId] === 'number') {
38
+ return ids[intersection.instanceId];
39
+ }
40
+ current = current.parent;
41
+ }
42
+ }
43
+ return gameObjectIdFromObject3D(intersection.object);
44
+ }
45
+
46
+ /** True when this GameObject is a scene root (no 3D parent). */
47
+ function is3DSceneParent(gameObject) {
48
+ if (!gameObject)
49
+ return true;
50
+ const parent = gameObject.parent;
51
+ return !parent || parent === gameObject.scene;
52
+ }
53
+ function has3DChildGameObjects(gameObject) {
54
+ return (gameObject?.transform?.children?.length ?? 0) > 0;
55
+ }
56
+
57
+ function readPose(pose) {
58
+ return {
59
+ positionX: pose.positionX ?? 0,
60
+ positionY: pose.positionY ?? 0,
61
+ positionZ: pose.positionZ ?? 0,
62
+ rotationX: pose.rotationX ?? 0,
63
+ rotationY: pose.rotationY ?? 0,
64
+ rotationZ: pose.rotationZ ?? 0,
65
+ scaleX: pose.scaleX ?? 1,
66
+ scaleY: pose.scaleY ?? 1,
67
+ scaleZ: pose.scaleZ ?? 1,
68
+ };
69
+ }
70
+ function poseKeyOf(pose) {
71
+ const r = readPose(pose);
72
+ return [
73
+ r.positionX, r.positionY, r.positionZ,
74
+ r.rotationX, r.rotationY, r.rotationZ,
75
+ r.scaleX, r.scaleY, r.scaleZ,
76
+ ].join(',');
77
+ }
78
+ function copyPose(from, to) {
79
+ const r = readPose(from);
80
+ to.positionX = r.positionX;
81
+ to.positionY = r.positionY;
82
+ to.positionZ = r.positionZ;
83
+ to.rotationX = r.rotationX;
84
+ to.rotationY = r.rotationY;
85
+ to.rotationZ = r.rotationZ;
86
+ to.scaleX = r.scaleX;
87
+ to.scaleY = r.scaleY;
88
+ to.scaleZ = r.scaleZ;
89
+ }
90
+ function isIdentityPose(pose) {
91
+ const r = readPose(pose);
92
+ return r.positionX === 0 && r.positionY === 0 && r.positionZ === 0
93
+ && r.rotationX === 0 && r.rotationY === 0 && r.rotationZ === 0
94
+ && r.scaleX === 1 && r.scaleY === 1 && r.scaleZ === 1;
95
+ }
96
+ function seedTransform3D(visual, transform) {
97
+ if (isIdentityPose(transform) && !isIdentityPose(visual)) {
98
+ copyPose(visual, transform);
99
+ }
100
+ }
101
+ /**
102
+ * Keep a visual component's pose fields and a sibling Transform3D in sync.
103
+ * Returns true when the visual Object3D should still apply pose itself
104
+ * (no Transform3D root yet).
105
+ */
106
+ class VisualPoseBridge {
107
+ constructor() {
108
+ this.lastVisual = '';
109
+ this.lastRoot = '';
110
+ }
111
+ sync(visual, root) {
112
+ if (!root)
113
+ return true;
114
+ if (this.lastVisual === '' && this.lastRoot === '') {
115
+ seedTransform3D(visual, root);
116
+ this.lastVisual = poseKeyOf(visual);
117
+ this.lastRoot = poseKeyOf(root);
118
+ return false;
119
+ }
120
+ const visualKey = poseKeyOf(visual);
121
+ const rootKey = poseKeyOf(root);
122
+ if (visualKey !== this.lastVisual && rootKey === this.lastRoot) {
123
+ copyPose(visual, root);
124
+ }
125
+ this.lastVisual = poseKeyOf(visual);
126
+ this.lastRoot = poseKeyOf(root);
127
+ return false;
128
+ }
129
+ }
22
130
 
131
+ const TONE_MAPPING = {
132
+ none: NoToneMapping,
133
+ aces: ACESFilmicToneMapping,
134
+ linear: LinearToneMapping,
135
+ reinhard: ReinhardToneMapping,
136
+ cineon: CineonToneMapping,
137
+ };
138
+ const _world = new Vector3();
139
+ const _euler = new Euler();
23
140
  class ThreeContext {
24
141
  constructor(params) {
25
142
  this.mixers = new Map();
26
143
  this.models = new Map();
144
+ /** Root Object3D for each GameObject that has a 3D node. */
145
+ this.nodes = new Map();
146
+ this.defaultLights = [];
27
147
  const { canvas, container, width, height, antialias = true, backgroundColor = 0x000000, backgroundAlpha = 1, } = params;
28
148
  this.scene = new Scene();
149
+ this.shadows = !!params.shadows;
29
150
  this.camera = new PerspectiveCamera(75, width / height, 0.1, 1000);
30
151
  this.camera.position.z = 5;
152
+ this.fallbackCamera = this.camera;
31
153
  const rendererOpts = { antialias };
32
154
  if (canvas) {
33
155
  rendererOpts.canvas = canvas;
@@ -39,15 +161,36 @@ class ThreeContext {
39
161
  this.renderer.setSize(width, height);
40
162
  this.renderer.setPixelRatio(window.devicePixelRatio);
41
163
  this.renderer.setClearColor(backgroundColor, backgroundAlpha);
42
- // If no canvas was provided but a container was, append the generated canvas
164
+ this.renderer.toneMapping = TONE_MAPPING[params.toneMapping ?? 'aces'] ?? ACESFilmicToneMapping;
165
+ this.renderer.toneMappingExposure = params.toneMappingExposure ?? 1;
166
+ if (this.shadows) {
167
+ this.renderer.shadowMap.enabled = true;
168
+ this.renderer.shadowMap.type = PCFSoftShadowMap;
169
+ }
43
170
  if (!canvas && container) {
44
171
  container.appendChild(this.renderer.domElement);
45
172
  }
46
- const ambientLight = new AmbientLight(0xffffff, 0.6);
173
+ const ambientLight = new AmbientLight(0xffffff, params.ambientIntensity ?? 0.6);
47
174
  this.scene.add(ambientLight);
48
- const directionalLight = new DirectionalLight(0xffffff, 0.8);
175
+ this.defaultLights.push(ambientLight);
176
+ const directionalLight = new DirectionalLight(0xffffff, params.directionalIntensity ?? 0.8);
49
177
  directionalLight.position.set(5, 10, 7.5);
178
+ if (this.shadows) {
179
+ directionalLight.castShadow = true;
180
+ directionalLight.shadow.mapSize.set(1024, 1024);
181
+ }
50
182
  this.scene.add(directionalLight);
183
+ this.defaultLights.push(directionalLight);
184
+ if (params.hemisphereLight !== false) {
185
+ const hemi = typeof params.hemisphereLight === 'object' ? params.hemisphereLight : {};
186
+ const hemisphereLight = new HemisphereLight(hemi.skyColor ?? 0xffffff, hemi.groundColor ?? 0x444444, hemi.intensity ?? 0.35);
187
+ this.scene.add(hemisphereLight);
188
+ this.defaultLights.push(hemisphereLight);
189
+ }
190
+ if (params.fog) {
191
+ const fog = typeof params.fog === 'object' ? params.fog : {};
192
+ this.scene.fog = new Fog(fog.color ?? backgroundColor, fog.near ?? 8, fog.far ?? 40);
193
+ }
51
194
  this.clock = new Clock();
52
195
  }
53
196
  update() {
@@ -57,10 +200,25 @@ class ThreeContext {
57
200
  }
58
201
  this.renderer.render(this.scene, this.camera);
59
202
  }
60
- attachParsedModel(id, model, animations) {
203
+ hasTransform3DRoot(id) {
204
+ return isTransform3DRoot(this.nodes.get(id));
205
+ }
206
+ applyModelShadows(object) {
207
+ if (!this.shadows)
208
+ return;
209
+ object.traverse((child) => {
210
+ const mesh = child;
211
+ if (mesh.isMesh) {
212
+ mesh.castShadow = true;
213
+ mesh.receiveShadow = true;
214
+ }
215
+ });
216
+ }
217
+ attachParsedModel(id, model, animations, gameObject) {
61
218
  tagObject3D(model, id);
62
- this.scene.add(model);
219
+ this.applyModelShadows(model);
63
220
  this.models.set(id, model);
221
+ this.attachVisual(id, model, gameObject);
64
222
  if (animations && animations.length > 0) {
65
223
  const mixer = new AnimationMixer(model);
66
224
  this.mixers.set(id, mixer);
@@ -68,6 +226,185 @@ class ThreeContext {
68
226
  }
69
227
  return model;
70
228
  }
229
+ object3DFor(id) {
230
+ return this.nodes.get(id);
231
+ }
232
+ /** Place a GameObject's root Object3D under the nearest ancestor visual (or the scene). */
233
+ attachObject3D(id, object, gameObject) {
234
+ const prev = this.nodes.get(id);
235
+ if (prev && prev !== object) {
236
+ this.rehomeRegisteredChildren(prev);
237
+ prev.parent?.remove(prev);
238
+ }
239
+ this.nodes.set(id, object);
240
+ const parentNode = this.findParentNode(gameObject);
241
+ if (object.parent !== parentNode) {
242
+ parentNode.add(object);
243
+ }
244
+ if (gameObject) {
245
+ this.adoptChildGameObjects(gameObject, object);
246
+ }
247
+ }
248
+ /**
249
+ * Attach a visual / camera / light under the Transform3D root when one exists.
250
+ * Otherwise the object itself becomes the GameObject root (legacy path).
251
+ */
252
+ attachVisual(id, object, gameObject) {
253
+ tagObject3D(object, id);
254
+ const root = this.nodes.get(id);
255
+ if (isTransform3DRoot(root) && root !== object) {
256
+ if (object.parent !== root) {
257
+ root.add(object);
258
+ }
259
+ return;
260
+ }
261
+ this.attachObject3D(id, object, gameObject);
262
+ }
263
+ detachVisual(id, object) {
264
+ const root = this.nodes.get(id);
265
+ if (isTransform3DRoot(root) && root !== object) {
266
+ object.parent?.remove(object);
267
+ return object;
268
+ }
269
+ if (root === object) {
270
+ return this.detachObject3D(id);
271
+ }
272
+ object.parent?.remove(object);
273
+ return object;
274
+ }
275
+ /** Remove a registered node without disposing it; child GameObject nodes stay in the scene. */
276
+ detachObject3D(id) {
277
+ const object = this.nodes.get(id);
278
+ if (!object)
279
+ return undefined;
280
+ this.rehomeRegisteredChildren(object);
281
+ object.parent?.remove(object);
282
+ this.nodes.delete(id);
283
+ return object;
284
+ }
285
+ reparentGameObject(gameObject) {
286
+ const object = this.nodes.get(gameObject.id);
287
+ if (!object)
288
+ return;
289
+ const parentNode = this.findParentNode(gameObject);
290
+ if (object.parent === parentNode)
291
+ return;
292
+ parentNode.add(object);
293
+ }
294
+ findParentNode(gameObject) {
295
+ if (!gameObject || is3DSceneParent(gameObject)) {
296
+ return this.scene;
297
+ }
298
+ let current = gameObject.parent;
299
+ while (current && current !== gameObject.scene) {
300
+ const node = this.nodes.get(current.id);
301
+ if (node)
302
+ return node;
303
+ current = current.parent;
304
+ }
305
+ return this.scene;
306
+ }
307
+ adoptChildGameObjects(gameObject, object) {
308
+ const children = gameObject.transform?.children;
309
+ if (!children)
310
+ return;
311
+ for (const childTransform of children) {
312
+ const childGo = childTransform.gameObject;
313
+ if (!childGo)
314
+ continue;
315
+ const childNode = this.nodes.get(childGo.id);
316
+ if (childNode && childNode.parent !== object) {
317
+ object.add(childNode);
318
+ }
319
+ }
320
+ }
321
+ ensureTransformRoot(gameObject, transform) {
322
+ const existing = this.nodes.get(gameObject.id);
323
+ if (isTransform3DRoot(existing)) {
324
+ this.applyTransformPose(existing, transform);
325
+ return existing;
326
+ }
327
+ const group = new Group();
328
+ group.name = gameObject.name;
329
+ group.userData[COMBOS_TRANSFORM3D] = true;
330
+ tagObject3D(group, gameObject.id);
331
+ if (existing && existing !== group) {
332
+ if (isIdentityPoseObject(transform)) {
333
+ transform.positionX = existing.position.x;
334
+ transform.positionY = existing.position.y;
335
+ transform.positionZ = existing.position.z;
336
+ transform.rotationX = existing.rotation.x;
337
+ transform.rotationY = existing.rotation.y;
338
+ transform.rotationZ = existing.rotation.z;
339
+ transform.scaleX = existing.scale.x;
340
+ transform.scaleY = existing.scale.y;
341
+ transform.scaleZ = existing.scale.z;
342
+ }
343
+ const parent = existing.parent || this.scene;
344
+ parent.add(group);
345
+ group.add(existing);
346
+ existing.position.set(0, 0, 0);
347
+ existing.rotation.set(0, 0, 0);
348
+ existing.scale.set(1, 1, 1);
349
+ this.nodes.set(gameObject.id, group);
350
+ this.adoptChildGameObjects(gameObject, group);
351
+ }
352
+ else {
353
+ this.attachObject3D(gameObject.id, group, gameObject);
354
+ }
355
+ this.applyTransformPose(group, transform);
356
+ return group;
357
+ }
358
+ applyTransform3D(gameObject, transform) {
359
+ const root = this.ensureTransformRoot(gameObject, transform);
360
+ this.applyTransformPose(root, transform);
361
+ root.updateWorldMatrix(true, false);
362
+ root.getWorldPosition(_world);
363
+ transform.worldPositionX = _world.x;
364
+ transform.worldPositionY = _world.y;
365
+ transform.worldPositionZ = _world.z;
366
+ _euler.setFromRotationMatrix(root.matrixWorld);
367
+ transform.worldRotationX = _euler.x;
368
+ transform.worldRotationY = _euler.y;
369
+ transform.worldRotationZ = _euler.z;
370
+ return root;
371
+ }
372
+ applyTransformPose(root, transform) {
373
+ const pose = readPose(transform);
374
+ root.position.set(pose.positionX, pose.positionY, pose.positionZ);
375
+ root.rotation.set(pose.rotationX, pose.rotationY, pose.rotationZ);
376
+ root.scale.set(pose.scaleX, pose.scaleY, pose.scaleZ);
377
+ }
378
+ removeDefaultLights() {
379
+ if (!this.defaultLights.length)
380
+ return;
381
+ for (const light of this.defaultLights) {
382
+ light.parent?.remove(light);
383
+ light.dispose?.();
384
+ }
385
+ this.defaultLights = [];
386
+ }
387
+ setActiveCamera(camera) {
388
+ const prev = this.camera;
389
+ this.camera = camera;
390
+ if (prev) {
391
+ camera.aspect = prev.aspect;
392
+ }
393
+ camera.updateProjectionMatrix();
394
+ }
395
+ restoreFallbackCamera() {
396
+ if (this.camera !== this.fallbackCamera) {
397
+ this.setActiveCamera(this.fallbackCamera);
398
+ }
399
+ }
400
+ rehomeRegisteredChildren(object) {
401
+ for (const child of [...object.children]) {
402
+ const childId = child.userData?.[COMBOS_GAME_OBJECT_ID];
403
+ if (typeof childId === 'number' && this.nodes.get(childId) === child) {
404
+ this.scene.add(child);
405
+ }
406
+ }
407
+ }
71
408
  playAnimation(id, animationIndex, speed) {
72
409
  const mixer = this.mixers.get(id);
73
410
  const model = this.models.get(id);
@@ -89,7 +426,7 @@ class ThreeContext {
89
426
  removeModel(id) {
90
427
  const model = this.models.get(id);
91
428
  if (model) {
92
- this.scene.remove(model);
429
+ this.detachVisual(id, model);
93
430
  model.traverse((child) => {
94
431
  const mesh = child;
95
432
  if (mesh.geometry) {
@@ -113,20 +450,33 @@ class ThreeContext {
113
450
  resize(width, height) {
114
451
  this.camera.aspect = width / height;
115
452
  this.camera.updateProjectionMatrix();
453
+ if (this.fallbackCamera !== this.camera) {
454
+ this.fallbackCamera.aspect = width / height;
455
+ this.fallbackCamera.updateProjectionMatrix();
456
+ }
116
457
  this.renderer.setSize(width, height);
117
458
  }
118
459
  destroy() {
119
460
  for (const id of this.models.keys()) {
120
461
  this.removeModel(id);
121
462
  }
463
+ this.removeDefaultLights();
122
464
  this.renderer.dispose();
123
465
  this.renderer.domElement.remove();
466
+ this.nodes.clear();
124
467
  this.scene = null;
125
468
  this.camera = null;
469
+ this.fallbackCamera = null;
126
470
  this.renderer = null;
127
471
  this.clock = null;
128
472
  }
129
473
  }
474
+ function isIdentityPoseObject(transform) {
475
+ const pose = readPose(transform);
476
+ return pose.positionX === 0 && pose.positionY === 0 && pose.positionZ === 0
477
+ && pose.rotationX === 0 && pose.rotationY === 0 && pose.rotationZ === 0
478
+ && pose.scaleX === 1 && pose.scaleY === 1 && pose.scaleZ === 1;
479
+ }
130
480
 
131
481
  class Renderer3DManager {
132
482
  constructor({ game, rendererSystem }) {
@@ -193,7 +543,60 @@ class Renderer3DManager {
193
543
  }
194
544
  }
195
545
 
196
- class Renderer3DSystem extends System {
546
+ class Transform3D extends Component {
547
+ constructor() {
548
+ super(...arguments);
549
+ this.positionX = 0;
550
+ this.positionY = 0;
551
+ this.positionZ = 0;
552
+ this.rotationX = 0;
553
+ this.rotationY = 0;
554
+ this.rotationZ = 0;
555
+ this.scaleX = 1;
556
+ this.scaleY = 1;
557
+ this.scaleZ = 1;
558
+ this.worldPositionX = 0;
559
+ this.worldPositionY = 0;
560
+ this.worldPositionZ = 0;
561
+ this.worldRotationX = 0;
562
+ this.worldRotationY = 0;
563
+ this.worldRotationZ = 0;
564
+ }
565
+ static { this.componentName = 'Transform3D'; }
566
+ init(obj) {
567
+ if (obj)
568
+ Object.assign(this, obj);
569
+ }
570
+ }
571
+ __decorate([
572
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'positionX', editor: 'number-stepper' })
573
+ ], Transform3D.prototype, "positionX", void 0);
574
+ __decorate([
575
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'positionY', editor: 'number-stepper' })
576
+ ], Transform3D.prototype, "positionY", void 0);
577
+ __decorate([
578
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'positionZ', editor: 'number-stepper' })
579
+ ], Transform3D.prototype, "positionZ", void 0);
580
+ __decorate([
581
+ Field({ type: 'number', step: 0.01, group: 'Transform3D', label: 'rotationX', editor: 'number-stepper' })
582
+ ], Transform3D.prototype, "rotationX", void 0);
583
+ __decorate([
584
+ Field({ type: 'number', step: 0.01, group: 'Transform3D', label: 'rotationY', editor: 'number-stepper' })
585
+ ], Transform3D.prototype, "rotationY", void 0);
586
+ __decorate([
587
+ Field({ type: 'number', step: 0.01, group: 'Transform3D', label: 'rotationZ', editor: 'number-stepper' })
588
+ ], Transform3D.prototype, "rotationZ", void 0);
589
+ __decorate([
590
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'scaleX', editor: 'number-stepper' })
591
+ ], Transform3D.prototype, "scaleX", void 0);
592
+ __decorate([
593
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'scaleY', editor: 'number-stepper' })
594
+ ], Transform3D.prototype, "scaleY", void 0);
595
+ __decorate([
596
+ Field({ type: 'number', step: 0.1, group: 'Transform3D', label: 'scaleZ', editor: 'number-stepper' })
597
+ ], Transform3D.prototype, "scaleZ", void 0);
598
+
599
+ let Renderer3DSystem = class Renderer3DSystem extends System {
197
600
  constructor() {
198
601
  super(...arguments);
199
602
  this.name = 'Renderer3DSystem';
@@ -211,6 +614,13 @@ class Renderer3DSystem extends System {
211
614
  antialias: opts.antialias,
212
615
  backgroundColor: opts.backgroundColor,
213
616
  backgroundAlpha: opts.backgroundAlpha,
617
+ fog: opts.fog,
618
+ hemisphereLight: opts.hemisphereLight,
619
+ shadows: opts.shadows,
620
+ toneMapping: opts.toneMapping,
621
+ toneMappingExposure: opts.toneMappingExposure,
622
+ ambientIntensity: opts.ambientIntensity,
623
+ directionalIntensity: opts.directionalIntensity,
214
624
  });
215
625
  this.game.canvas = this.threeContext.renderer.domElement;
216
626
  this.rendererManager = new Renderer3DManager({
@@ -219,10 +629,38 @@ class Renderer3DSystem extends System {
219
629
  });
220
630
  }
221
631
  update(e) {
632
+ const changes = this.componentObserver.clear();
633
+ for (const changed of changes) {
634
+ this.handleHierarchyChange(changed);
635
+ }
222
636
  for (const gameObject of this.game.gameObjects) {
637
+ const transform = gameObject.getComponent(Transform3D);
638
+ if (transform) {
639
+ this.threeContext.applyTransform3D(gameObject, transform);
640
+ }
223
641
  this.rendererManager.update(gameObject);
224
642
  }
225
643
  }
644
+ handleHierarchyChange(changed) {
645
+ if (changed.componentName !== 'Transform')
646
+ return;
647
+ if (changed.type !== OBSERVER_TYPE.CHANGE && changed.type !== OBSERVER_TYPE.ADD)
648
+ return;
649
+ this.ensureTransform3D(changed.gameObject);
650
+ if (changed.gameObject.parent)
651
+ this.ensureTransform3D(changed.gameObject.parent);
652
+ this.threeContext?.reparentGameObject(changed.gameObject);
653
+ }
654
+ ensureTransform3D(gameObject) {
655
+ if (!gameObject || gameObject === gameObject.scene)
656
+ return;
657
+ let transform = gameObject.getComponent(Transform3D);
658
+ if (!transform) {
659
+ transform = new Transform3D();
660
+ gameObject.addComponent(transform);
661
+ }
662
+ this.threeContext?.ensureTransformRoot(gameObject, transform);
663
+ }
226
664
  lateUpdate() {
227
665
  this.threeContext?.update();
228
666
  }
@@ -233,12 +671,18 @@ class Renderer3DSystem extends System {
233
671
  this.threeContext?.destroy();
234
672
  this.threeContext = null;
235
673
  }
236
- }
674
+ };
675
+ Renderer3DSystem = __decorate([
676
+ decorators.componentObserver({
677
+ Transform: ['_parent'],
678
+ })
679
+ ], Renderer3DSystem);
680
+ var Renderer3DSystem$1 = Renderer3DSystem;
237
681
 
238
682
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
239
- Object.assign(Renderer3DSystem, {
683
+ Object.assign(Renderer3DSystem$1, {
240
684
  packageName: "@combos-fun/plugin-renderer-3d",
241
- packageVersion: "0.0.41",
685
+ packageVersion: "0.0.44",
242
686
  });
243
687
 
244
688
  class Renderer3D extends System {
@@ -265,6 +709,492 @@ class Renderer3D extends System {
265
709
  }
266
710
  }
267
711
 
712
+ const POSE_PROPS = new Set([
713
+ 'positionX', 'positionY', 'positionZ',
714
+ 'rotationX', 'rotationY', 'rotationZ',
715
+ 'scaleX', 'scaleY', 'scaleZ',
716
+ ]);
717
+ let Transform3DSystem = class Transform3DSystem extends Renderer3D {
718
+ constructor() {
719
+ super(...arguments);
720
+ this.name = 'Transform3DSystem';
721
+ }
722
+ static { this.systemName = 'Transform3DSystem'; }
723
+ init() {
724
+ const renderer3DSystem = this.game.getSystem('Renderer3DSystem');
725
+ renderer3DSystem.rendererManager.register(this);
726
+ }
727
+ componentChanged(changed) {
728
+ if (changed.componentName === 'Transform') {
729
+ this.ensureOn(changed.gameObject);
730
+ if (changed.gameObject.parent)
731
+ this.ensureOn(changed.gameObject.parent);
732
+ this.threeContext.reparentGameObject(changed.gameObject);
733
+ return;
734
+ }
735
+ if (changed.componentName !== 'Transform3D')
736
+ return;
737
+ if (changed.type === OBSERVER_TYPE.ADD) {
738
+ this.threeContext.ensureTransformRoot(changed.gameObject, changed.component);
739
+ }
740
+ else if (changed.type === OBSERVER_TYPE.REMOVE) {
741
+ this.threeContext.detachObject3D(changed.gameObject.id);
742
+ }
743
+ else if (POSE_PROPS.has(changed.prop?.prop?.[0])) {
744
+ this.threeContext.applyTransform3D(changed.gameObject, changed.component);
745
+ }
746
+ }
747
+ rendererUpdate(gameObject) {
748
+ const transform = gameObject.getComponent(Transform3D);
749
+ if (!transform)
750
+ return;
751
+ this.threeContext.applyTransform3D(gameObject, transform);
752
+ }
753
+ ensureOn(gameObject) {
754
+ if (!gameObject || gameObject === gameObject.scene)
755
+ return;
756
+ let transform = gameObject.getComponent(Transform3D);
757
+ if (!transform) {
758
+ transform = new Transform3D();
759
+ gameObject.addComponent(transform);
760
+ }
761
+ this.threeContext.ensureTransformRoot(gameObject, transform);
762
+ }
763
+ };
764
+ Transform3DSystem = __decorate([
765
+ decorators.componentObserver({
766
+ Transform3D: [
767
+ 'positionX', 'positionY', 'positionZ',
768
+ 'rotationX', 'rotationY', 'rotationZ',
769
+ 'scaleX', 'scaleY', 'scaleZ',
770
+ ],
771
+ Transform: ['_parent'],
772
+ })
773
+ ], Transform3DSystem);
774
+ var Transform3DSystem_default = Transform3DSystem;
775
+
776
+ class Render3D extends Component {
777
+ constructor() {
778
+ super(...arguments);
779
+ this.visible = true;
780
+ this.opacity = 1;
781
+ this.renderOrder = 0;
782
+ this.sortableChildren = false;
783
+ }
784
+ static { this.componentName = 'Render3D'; }
785
+ init(obj) {
786
+ if (obj)
787
+ Object.assign(this, obj);
788
+ }
789
+ }
790
+ __decorate([
791
+ Field({ type: 'boolean', group: 'Render3D', label: 'visible', editor: 'toggle' })
792
+ ], Render3D.prototype, "visible", void 0);
793
+ __decorate([
794
+ Field({ type: 'number', step: 0.01, min: 0, max: 1, group: 'Render3D', label: 'opacity', editor: 'number-slider' })
795
+ ], Render3D.prototype, "opacity", void 0);
796
+ __decorate([
797
+ Field({ type: 'number', step: 1, group: 'Render3D', label: 'renderOrder', editor: 'number-stepper' })
798
+ ], Render3D.prototype, "renderOrder", void 0);
799
+ __decorate([
800
+ Field({ type: 'boolean', group: 'Render3D', label: 'sortableChildren', editor: 'toggle' })
801
+ ], Render3D.prototype, "sortableChildren", void 0);
802
+
803
+ let Render3DSystem = class Render3DSystem extends Renderer3D {
804
+ constructor() {
805
+ super(...arguments);
806
+ this.name = 'Render3DSystem';
807
+ }
808
+ static { this.systemName = 'Render3DSystem'; }
809
+ init() {
810
+ const renderer3DSystem = this.game.getSystem('Renderer3DSystem');
811
+ renderer3DSystem.rendererManager.register(this);
812
+ }
813
+ componentChanged(changed) {
814
+ if (changed.componentName !== 'Render3D')
815
+ return;
816
+ if (changed.type === OBSERVER_TYPE.REMOVE) {
817
+ const root = this.threeContext.object3DFor(changed.gameObject.id);
818
+ if (root)
819
+ this.apply(root, { visible: true, opacity: 1, renderOrder: 0, sortableChildren: false });
820
+ return;
821
+ }
822
+ this.applyTo(changed.gameObject, changed.component);
823
+ }
824
+ rendererUpdate(gameObject) {
825
+ const component = gameObject.getComponent(Render3D);
826
+ if (!component)
827
+ return;
828
+ this.applyTo(gameObject, component);
829
+ }
830
+ applyTo(gameObject, component) {
831
+ const root = this.threeContext.object3DFor(gameObject.id);
832
+ if (!root)
833
+ return;
834
+ this.apply(root, component);
835
+ if (component.sortableChildren) {
836
+ this.sortChildren(gameObject, root);
837
+ }
838
+ }
839
+ apply(root, component) {
840
+ root.visible = component.visible;
841
+ root.traverse((child) => {
842
+ child.renderOrder = component.renderOrder;
843
+ const mesh = child;
844
+ if (!mesh.isMesh || !mesh.material)
845
+ return;
846
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
847
+ for (const material of materials) {
848
+ const mat = material;
849
+ if (typeof mat.opacity === 'number') {
850
+ mat.opacity = component.opacity;
851
+ mat.transparent = component.opacity < 1;
852
+ }
853
+ }
854
+ });
855
+ }
856
+ sortChildren(gameObject, root) {
857
+ const ranked = gameObject.transform?.children
858
+ ?.map((childTransform) => {
859
+ const childGo = childTransform.gameObject;
860
+ if (!childGo)
861
+ return null;
862
+ const node = this.threeContext.object3DFor(childGo.id);
863
+ if (!node || node.parent !== root)
864
+ return null;
865
+ const render = childGo.getComponent(Render3D);
866
+ return { node, order: render?.renderOrder ?? 0 };
867
+ })
868
+ .filter((item) => !!item)
869
+ .sort((a, b) => a.order - b.order);
870
+ if (!ranked?.length)
871
+ return;
872
+ for (const item of ranked) {
873
+ root.add(item.node);
874
+ }
875
+ }
876
+ };
877
+ Render3DSystem = __decorate([
878
+ decorators.componentObserver({
879
+ Render3D: ['visible', 'opacity', 'renderOrder', 'sortableChildren'],
880
+ })
881
+ ], Render3DSystem);
882
+ var Render3DSystem_default = Render3DSystem;
883
+
884
+ class Camera3D extends Component {
885
+ constructor() {
886
+ super(...arguments);
887
+ this.fov = 75;
888
+ this.near = 0.1;
889
+ this.far = 1000;
890
+ this.active = true;
891
+ this.lookAt = false;
892
+ this.lookAtX = 0;
893
+ this.lookAtY = 0;
894
+ this.lookAtZ = 0;
895
+ }
896
+ static { this.componentName = 'Camera3D'; }
897
+ init(obj) {
898
+ if (obj)
899
+ Object.assign(this, obj);
900
+ }
901
+ }
902
+ __decorate([
903
+ Field({ type: 'number', step: 1, group: 'Camera3D', label: 'fov', editor: 'number-stepper' })
904
+ ], Camera3D.prototype, "fov", void 0);
905
+ __decorate([
906
+ Field({ type: 'number', step: 0.01, group: 'Camera3D', label: 'near', editor: 'number-stepper' })
907
+ ], Camera3D.prototype, "near", void 0);
908
+ __decorate([
909
+ Field({ type: 'number', step: 1, group: 'Camera3D', label: 'far', editor: 'number-stepper' })
910
+ ], Camera3D.prototype, "far", void 0);
911
+ __decorate([
912
+ Field({ type: 'boolean', group: 'Camera3D', label: 'active', editor: 'toggle' })
913
+ ], Camera3D.prototype, "active", void 0);
914
+ __decorate([
915
+ Field({ type: 'boolean', group: 'Camera3D', label: 'lookAt', editor: 'toggle' })
916
+ ], Camera3D.prototype, "lookAt", void 0);
917
+ __decorate([
918
+ Field({ type: 'number', step: 0.1, group: 'Camera3D', label: 'lookAtX', editor: 'number-stepper' })
919
+ ], Camera3D.prototype, "lookAtX", void 0);
920
+ __decorate([
921
+ Field({ type: 'number', step: 0.1, group: 'Camera3D', label: 'lookAtY', editor: 'number-stepper' })
922
+ ], Camera3D.prototype, "lookAtY", void 0);
923
+ __decorate([
924
+ Field({ type: 'number', step: 0.1, group: 'Camera3D', label: 'lookAtZ', editor: 'number-stepper' })
925
+ ], Camera3D.prototype, "lookAtZ", void 0);
926
+
927
+ let Camera3DSystem = class Camera3DSystem extends Renderer3D {
928
+ constructor() {
929
+ super(...arguments);
930
+ this.name = 'Camera3DSystem';
931
+ this.cameras = new Map();
932
+ }
933
+ static { this.systemName = 'Camera3DSystem'; }
934
+ init() {
935
+ const renderer3DSystem = this.game.getSystem('Renderer3DSystem');
936
+ renderer3DSystem.rendererManager.register(this);
937
+ }
938
+ componentChanged(changed) {
939
+ if (changed.componentName !== 'Camera3D')
940
+ return;
941
+ const component = changed.component;
942
+ if (changed.type === OBSERVER_TYPE.ADD) {
943
+ this.mount(changed.gameObject, component);
944
+ }
945
+ else if (changed.type === OBSERVER_TYPE.REMOVE) {
946
+ this.unmount(changed.gameObject.id);
947
+ }
948
+ else {
949
+ this.sync(changed.gameObject, component);
950
+ }
951
+ }
952
+ rendererUpdate(gameObject) {
953
+ const component = gameObject.getComponent(Camera3D);
954
+ const camera = this.cameras.get(gameObject.id);
955
+ if (!component || !camera)
956
+ return;
957
+ if (component.lookAt) {
958
+ camera.lookAt(component.lookAtX, component.lookAtY, component.lookAtZ);
959
+ }
960
+ }
961
+ mount(gameObject, component) {
962
+ this.ensureTransform(gameObject);
963
+ const camera = new PerspectiveCamera(component.fov, this.threeContext.camera.aspect, component.near, component.far);
964
+ this.threeContext.attachVisual(gameObject.id, camera, gameObject);
965
+ this.cameras.set(gameObject.id, camera);
966
+ this.sync(gameObject, component);
967
+ }
968
+ sync(gameObject, component) {
969
+ const camera = this.cameras.get(gameObject.id);
970
+ if (!camera)
971
+ return;
972
+ camera.fov = component.fov;
973
+ camera.near = component.near;
974
+ camera.far = component.far;
975
+ camera.updateProjectionMatrix();
976
+ if (component.active) {
977
+ this.threeContext.setActiveCamera(camera);
978
+ }
979
+ else if (this.threeContext.camera === camera) {
980
+ this.activateAnother(gameObject.id);
981
+ }
982
+ }
983
+ unmount(id) {
984
+ const camera = this.cameras.get(id);
985
+ if (!camera)
986
+ return;
987
+ this.threeContext.detachVisual(id, camera);
988
+ this.cameras.delete(id);
989
+ if (this.threeContext.camera === camera) {
990
+ this.activateAnother(id);
991
+ }
992
+ }
993
+ activateAnother(exceptId) {
994
+ for (const [id, camera] of this.cameras) {
995
+ if (id === exceptId)
996
+ continue;
997
+ const go = this.game.gameObjects.find((item) => item.id === id);
998
+ const component = go?.getComponent(Camera3D);
999
+ if (component?.active) {
1000
+ this.threeContext.setActiveCamera(camera);
1001
+ return;
1002
+ }
1003
+ }
1004
+ this.threeContext.restoreFallbackCamera();
1005
+ }
1006
+ ensureTransform(gameObject) {
1007
+ let transform = gameObject.getComponent(Transform3D);
1008
+ if (!transform) {
1009
+ transform = new Transform3D();
1010
+ transform.positionZ = 5;
1011
+ gameObject.addComponent(transform);
1012
+ return;
1013
+ }
1014
+ if (isIdentityPose(transform)) {
1015
+ transform.positionZ = 5;
1016
+ }
1017
+ }
1018
+ onDestroy() {
1019
+ for (const [id] of this.cameras) {
1020
+ this.unmount(id);
1021
+ }
1022
+ this.cameras.clear();
1023
+ }
1024
+ };
1025
+ Camera3DSystem = __decorate([
1026
+ decorators.componentObserver({
1027
+ Camera3D: ['fov', 'near', 'far', 'active', 'lookAt', 'lookAtX', 'lookAtY', 'lookAtZ'],
1028
+ })
1029
+ ], Camera3DSystem);
1030
+ var Camera3DSystem_default = Camera3DSystem;
1031
+
1032
+ class Light3D extends Component {
1033
+ constructor() {
1034
+ super(...arguments);
1035
+ this.type = 'directional';
1036
+ this.color = 0xffffff;
1037
+ this.intensity = 1;
1038
+ this.castShadow = false;
1039
+ this.skyColor = 0xffffff;
1040
+ this.groundColor = 0x444444;
1041
+ this.distance = 0;
1042
+ this.decay = 2;
1043
+ this.angle = Math.PI / 3;
1044
+ this.penumbra = 0;
1045
+ }
1046
+ static { this.componentName = 'Light3D'; }
1047
+ init(obj) {
1048
+ if (obj)
1049
+ Object.assign(this, obj);
1050
+ }
1051
+ }
1052
+ __decorate([
1053
+ Field({
1054
+ type: 'string',
1055
+ group: 'Light3D',
1056
+ label: 'type',
1057
+ editor: 'enum',
1058
+ enumOptions: ['ambient', 'directional', 'hemisphere', 'point', 'spot'],
1059
+ })
1060
+ ], Light3D.prototype, "type", void 0);
1061
+ __decorate([
1062
+ Field({ type: 'number', group: 'Light3D', label: 'color', editor: 'number-stepper' })
1063
+ ], Light3D.prototype, "color", void 0);
1064
+ __decorate([
1065
+ Field({ type: 'number', step: 0.05, group: 'Light3D', label: 'intensity', editor: 'number-stepper' })
1066
+ ], Light3D.prototype, "intensity", void 0);
1067
+ __decorate([
1068
+ Field({ type: 'boolean', group: 'Light3D', label: 'castShadow', editor: 'toggle' })
1069
+ ], Light3D.prototype, "castShadow", void 0);
1070
+ __decorate([
1071
+ Field({ type: 'number', group: 'Light3D', label: 'skyColor', editor: 'number-stepper' })
1072
+ ], Light3D.prototype, "skyColor", void 0);
1073
+ __decorate([
1074
+ Field({ type: 'number', group: 'Light3D', label: 'groundColor', editor: 'number-stepper' })
1075
+ ], Light3D.prototype, "groundColor", void 0);
1076
+ __decorate([
1077
+ Field({ type: 'number', step: 0.1, group: 'Light3D', label: 'distance', editor: 'number-stepper' })
1078
+ ], Light3D.prototype, "distance", void 0);
1079
+ __decorate([
1080
+ Field({ type: 'number', step: 0.1, group: 'Light3D', label: 'decay', editor: 'number-stepper' })
1081
+ ], Light3D.prototype, "decay", void 0);
1082
+ __decorate([
1083
+ Field({ type: 'number', step: 0.01, group: 'Light3D', label: 'angle', editor: 'number-stepper' })
1084
+ ], Light3D.prototype, "angle", void 0);
1085
+ __decorate([
1086
+ Field({ type: 'number', step: 0.01, min: 0, max: 1, group: 'Light3D', label: 'penumbra', editor: 'number-slider' })
1087
+ ], Light3D.prototype, "penumbra", void 0);
1088
+
1089
+ let Light3DSystem = class Light3DSystem extends Renderer3D {
1090
+ constructor() {
1091
+ super(...arguments);
1092
+ this.name = 'Light3DSystem';
1093
+ this.lights = new Map();
1094
+ }
1095
+ static { this.systemName = 'Light3DSystem'; }
1096
+ init() {
1097
+ const renderer3DSystem = this.game.getSystem('Renderer3DSystem');
1098
+ renderer3DSystem.rendererManager.register(this);
1099
+ }
1100
+ componentChanged(changed) {
1101
+ if (changed.componentName !== 'Light3D')
1102
+ return;
1103
+ const component = changed.component;
1104
+ if (changed.type === OBSERVER_TYPE.ADD) {
1105
+ this.mount(changed.gameObject, component);
1106
+ }
1107
+ else if (changed.type === OBSERVER_TYPE.REMOVE) {
1108
+ this.unmount(changed.gameObject.id);
1109
+ }
1110
+ else if (changed.prop?.prop?.[0] === 'type') {
1111
+ this.unmount(changed.gameObject.id);
1112
+ this.mount(changed.gameObject, component);
1113
+ }
1114
+ else {
1115
+ this.sync(changed.gameObject.id, component);
1116
+ }
1117
+ }
1118
+ rendererUpdate(_gameObject) { }
1119
+ mount(gameObject, component) {
1120
+ this.ensureTransform(gameObject);
1121
+ if (this.lights.size === 0) {
1122
+ this.threeContext.removeDefaultLights();
1123
+ }
1124
+ const light = this.createLight(component);
1125
+ this.threeContext.attachVisual(gameObject.id, light, gameObject);
1126
+ this.lights.set(gameObject.id, light);
1127
+ this.sync(gameObject.id, component);
1128
+ }
1129
+ createLight(component) {
1130
+ switch (component.type) {
1131
+ case 'ambient':
1132
+ return new AmbientLight(component.color, component.intensity);
1133
+ case 'hemisphere':
1134
+ return new HemisphereLight(component.skyColor, component.groundColor, component.intensity);
1135
+ case 'point':
1136
+ return new PointLight(component.color, component.intensity, component.distance, component.decay);
1137
+ case 'spot':
1138
+ return new SpotLight(component.color, component.intensity, component.distance, component.angle, component.penumbra, component.decay);
1139
+ case 'directional':
1140
+ default:
1141
+ return new DirectionalLight(component.color, component.intensity);
1142
+ }
1143
+ }
1144
+ sync(id, component) {
1145
+ const light = this.lights.get(id);
1146
+ if (!light)
1147
+ return;
1148
+ if ('color' in light && light.color) {
1149
+ light.color.set(component.color);
1150
+ }
1151
+ light.intensity = component.intensity;
1152
+ if (light instanceof HemisphereLight) {
1153
+ light.color.set(component.skyColor);
1154
+ light.groundColor.set(component.groundColor);
1155
+ }
1156
+ if (light instanceof PointLight || light instanceof SpotLight) {
1157
+ light.distance = component.distance;
1158
+ light.decay = component.decay;
1159
+ }
1160
+ if (light instanceof SpotLight) {
1161
+ light.angle = component.angle;
1162
+ light.penumbra = component.penumbra;
1163
+ }
1164
+ if ('castShadow' in light) {
1165
+ light.castShadow = !!(component.castShadow && this.threeContext.shadows);
1166
+ }
1167
+ }
1168
+ unmount(id) {
1169
+ const light = this.lights.get(id);
1170
+ if (!light)
1171
+ return;
1172
+ this.threeContext.detachVisual(id, light);
1173
+ light.dispose?.();
1174
+ this.lights.delete(id);
1175
+ }
1176
+ ensureTransform(gameObject) {
1177
+ if (gameObject.getComponent(Transform3D))
1178
+ return;
1179
+ gameObject.addComponent(new Transform3D());
1180
+ }
1181
+ onDestroy() {
1182
+ for (const [id] of this.lights) {
1183
+ this.unmount(id);
1184
+ }
1185
+ this.lights.clear();
1186
+ }
1187
+ };
1188
+ Light3DSystem = __decorate([
1189
+ decorators.componentObserver({
1190
+ Light3D: [
1191
+ 'type', 'color', 'intensity', 'castShadow',
1192
+ 'skyColor', 'groundColor', 'distance', 'decay', 'angle', 'penumbra',
1193
+ ],
1194
+ })
1195
+ ], Light3DSystem);
1196
+ var Light3DSystem_default = Light3DSystem;
1197
+
268
1198
  async function requireNamedResource(name) {
269
1199
  const trimmed = String(name ?? '').trim();
270
1200
  if (!trimmed) {
@@ -292,5 +1222,5 @@ async function textureFromNamedImage(name) {
292
1222
  return textureFromResourceImage(await requireNamedResource(name));
293
1223
  }
294
1224
 
295
- export { COMBOS_GAME_OBJECT_ID, Renderer3D, Renderer3DManager, Renderer3DSystem, ThreeContext, gameObjectIdFromObject3D, requireNamedResource, tagObject3D, textureFromNamedImage, textureFromResourceImage };
1225
+ export { COMBOS_GAME_OBJECT_ID, COMBOS_INSTANCE_GAME_OBJECT_IDS, COMBOS_TRANSFORM3D, Camera3D, Camera3DSystem_default as Camera3DSystem, Light3D, Light3DSystem_default as Light3DSystem, Render3D, Render3DSystem_default as Render3DSystem, Renderer3D, Renderer3DManager, Renderer3DSystem$1 as Renderer3DSystem, ThreeContext, Transform3D, Transform3DSystem_default as Transform3DSystem, VisualPoseBridge, copyPose, gameObjectIdFromIntersection, gameObjectIdFromObject3D, has3DChildGameObjects, is3DSceneParent, isIdentityPose, isTransform3DRoot, poseKeyOf, readPose, requireNamedResource, seedTransform3D, tagObject3D, textureFromNamedImage, textureFromResourceImage };
296
1226
  //# sourceMappingURL=plugin-renderer-3d.esm.js.map