@combos-fun/plugin-renderer-3d-graphics 0.0.42 → 0.0.45

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,8 +1,8 @@
1
1
  import { __decorate } from 'tslib';
2
2
  import { Component, OBSERVER_TYPE, decorators } from '@combos-fun/engine';
3
3
  import { Field } from '@combos-fun/inspector-decorator';
4
- import { Renderer3D, Renderer3DSystem, tagObject3D } from '@combos-fun/plugin-renderer-3d';
5
- import { MeshStandardMaterial, Mesh, BoxGeometry, TorusGeometry, ConeGeometry, PlaneGeometry, CylinderGeometry, SphereGeometry } from 'three';
4
+ import { COMBOS_INSTANCE_GAME_OBJECT_IDS, textureFromNamedImage, Renderer3D, Renderer3DSystem, Transform3D, is3DSceneParent, has3DChildGameObjects, VisualPoseBridge, tagObject3D } from '@combos-fun/plugin-renderer-3d';
5
+ import { Vector3, Quaternion, Euler, Matrix4, InstancedMesh, BoxGeometry, CapsuleGeometry, TorusGeometry, ConeGeometry, PlaneGeometry, CylinderGeometry, SphereGeometry, DoubleSide, FrontSide, MeshStandardMaterial, MeshToonMaterial, MeshLambertMaterial, MeshBasicMaterial, Group, Mesh } from 'three';
6
6
 
7
7
  class Graphics3D extends Component {
8
8
  constructor() {
@@ -14,7 +14,8 @@ class Graphics3D extends Component {
14
14
  this.height = 1;
15
15
  this.depth = 1;
16
16
  this.radius = 0.5;
17
- this.segments = 32;
17
+ this.segments = 12;
18
+ this.tube = 0;
18
19
  this.positionX = 0;
19
20
  this.positionY = 0;
20
21
  this.positionZ = 0;
@@ -25,6 +26,13 @@ class Graphics3D extends Component {
25
26
  this.scaleY = 1;
26
27
  this.scaleZ = 1;
27
28
  this.opacity = 1;
29
+ this.roughness = 1;
30
+ this.metalness = 0;
31
+ this.emissive = 0;
32
+ this.map = '';
33
+ this.doubleSide = false;
34
+ this.material = 'standard';
35
+ this.instanced = true;
28
36
  }
29
37
  static { this.componentName = 'Graphics3D'; }
30
38
  init(obj) {
@@ -38,7 +46,8 @@ __decorate([
38
46
  group: 'Graphics3D',
39
47
  label: 'shape',
40
48
  description: 'Primitive shape kind.',
41
- editor: 'text',
49
+ editor: 'enum',
50
+ enumOptions: ['box', 'sphere', 'cylinder', 'plane', 'cone', 'torus', 'capsule'],
42
51
  })
43
52
  ], Graphics3D.prototype, "shape", void 0);
44
53
  __decorate([
@@ -109,6 +118,16 @@ __decorate([
109
118
  editor: 'number-stepper',
110
119
  })
111
120
  ], Graphics3D.prototype, "segments", void 0);
121
+ __decorate([
122
+ Field({
123
+ type: 'number',
124
+ step: 0.05,
125
+ group: 'Graphics3D',
126
+ label: 'tube',
127
+ description: 'Torus tube radius. 0 means 0.4 × radius.',
128
+ editor: 'number-stepper',
129
+ })
130
+ ], Graphics3D.prototype, "tube", void 0);
112
131
  __decorate([
113
132
  Field({
114
133
  type: 'number',
@@ -211,31 +230,498 @@ __decorate([
211
230
  editor: 'number-slider',
212
231
  })
213
232
  ], Graphics3D.prototype, "opacity", void 0);
233
+ __decorate([
234
+ Field({
235
+ type: 'number',
236
+ step: 0.05,
237
+ min: 0,
238
+ max: 1,
239
+ group: 'Graphics3D',
240
+ label: 'roughness',
241
+ description: 'PBR roughness (standard material).',
242
+ editor: 'number-slider',
243
+ })
244
+ ], Graphics3D.prototype, "roughness", void 0);
245
+ __decorate([
246
+ Field({
247
+ type: 'number',
248
+ step: 0.05,
249
+ min: 0,
250
+ max: 1,
251
+ group: 'Graphics3D',
252
+ label: 'metalness',
253
+ description: 'PBR metalness (standard material).',
254
+ editor: 'number-slider',
255
+ })
256
+ ], Graphics3D.prototype, "metalness", void 0);
257
+ __decorate([
258
+ Field({
259
+ type: 'number',
260
+ group: 'Graphics3D',
261
+ label: 'emissive',
262
+ description: 'Emissive color (hex).',
263
+ editor: 'number-stepper',
264
+ })
265
+ ], Graphics3D.prototype, "emissive", void 0);
266
+ __decorate([
267
+ Field({
268
+ type: 'string',
269
+ group: 'Graphics3D',
270
+ label: 'map',
271
+ description: 'Named IMAGE resource used as a color map.',
272
+ editor: 'text',
273
+ })
274
+ ], Graphics3D.prototype, "map", void 0);
275
+ __decorate([
276
+ Field({
277
+ type: 'boolean',
278
+ group: 'Graphics3D',
279
+ label: 'doubleSide',
280
+ description: 'Render both sides of the mesh.',
281
+ editor: 'toggle',
282
+ })
283
+ ], Graphics3D.prototype, "doubleSide", void 0);
284
+ __decorate([
285
+ Field({
286
+ type: 'string',
287
+ group: 'Graphics3D',
288
+ label: 'material',
289
+ description: 'Surface shader.',
290
+ editor: 'enum',
291
+ enumOptions: ['standard', 'basic', 'lambert', 'toon'],
292
+ })
293
+ ], Graphics3D.prototype, "material", void 0);
294
+ __decorate([
295
+ Field({
296
+ type: 'boolean',
297
+ group: 'Graphics3D',
298
+ label: 'instanced',
299
+ description: 'Share an InstancedMesh with identical primitives.',
300
+ editor: 'toggle',
301
+ })
302
+ ], Graphics3D.prototype, "instanced", void 0);
303
+
304
+ const _pos = new Vector3();
305
+ const _quat = new Quaternion();
306
+ const _scale = new Vector3();
307
+ const _euler = new Euler();
308
+ const _mat = new Matrix4();
309
+ const _swap = new Matrix4();
310
+ function composePose(px, py, pz, rx, ry, rz, sx, sy, sz, target = _mat) {
311
+ _pos.set(px, py, pz);
312
+ _euler.set(rx, ry, rz);
313
+ _quat.setFromEuler(_euler);
314
+ _scale.set(sx, sy, sz);
315
+ return target.compose(_pos, _quat, _scale);
316
+ }
317
+ class InstanceBuckets {
318
+ constructor(scene, shadows) {
319
+ this.scene = scene;
320
+ this.shadows = shadows;
321
+ this.buckets = new Map();
322
+ }
323
+ add(key, id, geometry, material, matrix) {
324
+ let bucket = this.buckets.get(key);
325
+ if (!bucket) {
326
+ bucket = this.create(key, geometry, material);
327
+ }
328
+ if (bucket.live >= bucket.capacity) {
329
+ this.grow(bucket);
330
+ }
331
+ const index = bucket.live;
332
+ bucket.ids[index] = id;
333
+ bucket.indexOf.set(id, index);
334
+ bucket.mesh.setMatrixAt(index, matrix);
335
+ bucket.live += 1;
336
+ bucket.mesh.count = bucket.live;
337
+ bucket.mesh.instanceMatrix.needsUpdate = true;
338
+ }
339
+ setMatrix(key, id, matrix) {
340
+ const bucket = this.buckets.get(key);
341
+ if (!bucket)
342
+ return;
343
+ const index = bucket.indexOf.get(id);
344
+ if (index == null)
345
+ return;
346
+ bucket.mesh.setMatrixAt(index, matrix);
347
+ bucket.mesh.instanceMatrix.needsUpdate = true;
348
+ }
349
+ remove(key, id) {
350
+ const bucket = this.buckets.get(key);
351
+ if (!bucket)
352
+ return;
353
+ const index = bucket.indexOf.get(id);
354
+ if (index == null)
355
+ return;
356
+ const last = bucket.live - 1;
357
+ if (index !== last) {
358
+ const lastId = bucket.ids[last];
359
+ bucket.mesh.getMatrixAt(last, _swap);
360
+ bucket.mesh.setMatrixAt(index, _swap);
361
+ bucket.ids[index] = lastId;
362
+ if (typeof lastId === 'number') {
363
+ bucket.indexOf.set(lastId, index);
364
+ }
365
+ }
366
+ bucket.ids[last] = undefined;
367
+ bucket.indexOf.delete(id);
368
+ bucket.live -= 1;
369
+ bucket.mesh.count = bucket.live;
370
+ bucket.mesh.instanceMatrix.needsUpdate = true;
371
+ if (bucket.live <= 0) {
372
+ this.scene.remove(bucket.mesh);
373
+ bucket.mesh.dispose();
374
+ this.buckets.delete(key);
375
+ }
376
+ }
377
+ clear() {
378
+ for (const bucket of this.buckets.values()) {
379
+ this.scene.remove(bucket.mesh);
380
+ bucket.mesh.dispose();
381
+ }
382
+ this.buckets.clear();
383
+ }
384
+ create(key, geometry, material) {
385
+ const capacity = 8;
386
+ const mesh = new InstancedMesh(geometry, material, capacity);
387
+ mesh.count = 0;
388
+ mesh.frustumCulled = false;
389
+ mesh.castShadow = this.shadows;
390
+ mesh.receiveShadow = this.shadows;
391
+ const ids = new Array(capacity);
392
+ mesh.userData[COMBOS_INSTANCE_GAME_OBJECT_IDS] = ids;
393
+ this.scene.add(mesh);
394
+ const bucket = {
395
+ mesh,
396
+ ids,
397
+ indexOf: new Map(),
398
+ live: 0,
399
+ capacity,
400
+ geometry,
401
+ material,
402
+ };
403
+ this.buckets.set(key, bucket);
404
+ return bucket;
405
+ }
406
+ grow(bucket) {
407
+ const capacity = bucket.capacity * 2;
408
+ const mesh = new InstancedMesh(bucket.geometry, bucket.material, capacity);
409
+ mesh.count = bucket.live;
410
+ mesh.frustumCulled = false;
411
+ mesh.castShadow = this.shadows;
412
+ mesh.receiveShadow = this.shadows;
413
+ const ids = new Array(capacity);
414
+ for (let i = 0; i < bucket.live; i++) {
415
+ bucket.mesh.getMatrixAt(i, _swap);
416
+ mesh.setMatrixAt(i, _swap);
417
+ ids[i] = bucket.ids[i];
418
+ }
419
+ mesh.userData[COMBOS_INSTANCE_GAME_OBJECT_IDS] = ids;
420
+ this.scene.remove(bucket.mesh);
421
+ bucket.mesh.dispose();
422
+ this.scene.add(mesh);
423
+ bucket.mesh = mesh;
424
+ bucket.ids = ids;
425
+ bucket.capacity = capacity;
426
+ }
427
+ }
214
428
 
215
- const GEOMETRY_PROPS = ['shape', 'width', 'height', 'depth', 'radius', 'segments'];
216
- const MATERIAL_PROPS = ['color', 'wireframe', 'opacity'];
429
+ function torusTube(radius, tube) {
430
+ return tube && tube > 0 ? tube : radius * 0.4;
431
+ }
432
+ function resolvePrimitive(part, fallback) {
433
+ const src = { ...fallback, ...part };
434
+ const radius = src.radius ?? 0.5;
435
+ return {
436
+ shape: src.shape || 'box',
437
+ color: src.color ?? 0x00ff00,
438
+ wireframe: !!src.wireframe,
439
+ width: src.width ?? 1,
440
+ height: src.height ?? 1,
441
+ depth: src.depth ?? 1,
442
+ radius,
443
+ segments: Math.max(3, Math.round(src.segments ?? 12)),
444
+ tube: torusTube(radius, src.tube),
445
+ positionX: src.positionX ?? 0,
446
+ positionY: src.positionY ?? 0,
447
+ positionZ: src.positionZ ?? 0,
448
+ rotationX: src.rotationX ?? 0,
449
+ rotationY: src.rotationY ?? 0,
450
+ rotationZ: src.rotationZ ?? 0,
451
+ scaleX: src.scaleX ?? 1,
452
+ scaleY: src.scaleY ?? 1,
453
+ scaleZ: src.scaleZ ?? 1,
454
+ opacity: src.opacity ?? 1,
455
+ roughness: src.roughness ?? 1,
456
+ metalness: src.metalness ?? 0,
457
+ emissive: src.emissive ?? 0,
458
+ map: src.map || '',
459
+ doubleSide: !!src.doubleSide,
460
+ material: src.material || 'standard',
461
+ };
462
+ }
463
+ function geometryKey(p) {
464
+ switch (p.shape) {
465
+ case 'sphere':
466
+ return `sphere:${p.radius}:${p.segments}`;
467
+ case 'cylinder':
468
+ return `cylinder:${p.radius}:${p.height}:${p.segments}`;
469
+ case 'cone':
470
+ return `cone:${p.radius}:${p.height}:${p.segments}`;
471
+ case 'plane':
472
+ return `plane:${p.width}:${p.height}:${p.segments}`;
473
+ case 'torus':
474
+ return `torus:${p.radius}:${p.tube}:${p.segments}`;
475
+ case 'capsule':
476
+ return `capsule:${p.radius}:${p.height}:${p.segments}`;
477
+ case 'box':
478
+ default:
479
+ return `box:${p.width}:${p.height}:${p.depth}`;
480
+ }
481
+ }
482
+ function createGeometry(p) {
483
+ const segs = p.segments;
484
+ switch (p.shape) {
485
+ case 'sphere':
486
+ return new SphereGeometry(p.radius, segs, segs);
487
+ case 'cylinder':
488
+ return new CylinderGeometry(p.radius, p.radius, p.height, segs);
489
+ case 'plane':
490
+ return new PlaneGeometry(p.width, p.height, segs, segs);
491
+ case 'cone':
492
+ return new ConeGeometry(p.radius, p.height, segs);
493
+ case 'torus':
494
+ return new TorusGeometry(p.radius, p.tube, 8, segs);
495
+ case 'capsule': {
496
+ const length = Math.max(0, p.height - 2 * p.radius);
497
+ return new CapsuleGeometry(p.radius, length, 4, segs);
498
+ }
499
+ case 'box':
500
+ default:
501
+ return new BoxGeometry(p.width, p.height, p.depth);
502
+ }
503
+ }
504
+ function materialKey(p) {
505
+ return [
506
+ p.material,
507
+ p.color,
508
+ p.wireframe ? 1 : 0,
509
+ p.opacity,
510
+ p.roughness,
511
+ p.metalness,
512
+ p.emissive,
513
+ p.map,
514
+ p.doubleSide ? 1 : 0,
515
+ ].join(':');
516
+ }
517
+ function poseKey(px, py, pz, rx, ry, rz, sx, sy, sz) {
518
+ return `${px},${py},${pz},${rx},${ry},${rz},${sx},${sy},${sz}`;
519
+ }
520
+
521
+ class RefCache {
522
+ constructor() {
523
+ this.items = new Map();
524
+ }
525
+ get(key) {
526
+ return this.items.get(key)?.value;
527
+ }
528
+ acquire(key, create) {
529
+ const hit = this.items.get(key);
530
+ if (hit) {
531
+ hit.refs += 1;
532
+ return hit.value;
533
+ }
534
+ const value = create();
535
+ this.items.set(key, { value, refs: 1 });
536
+ return value;
537
+ }
538
+ release(key, dispose) {
539
+ const hit = this.items.get(key);
540
+ if (!hit)
541
+ return;
542
+ hit.refs -= 1;
543
+ if (hit.refs > 0)
544
+ return;
545
+ dispose(hit.value);
546
+ this.items.delete(key);
547
+ }
548
+ clear(dispose) {
549
+ for (const hit of this.items.values()) {
550
+ dispose(hit.value);
551
+ }
552
+ this.items.clear();
553
+ }
554
+ }
555
+ class GeometryCache {
556
+ constructor() {
557
+ this.cache = new RefCache();
558
+ }
559
+ acquire(primitive) {
560
+ const key = geometryKey(primitive);
561
+ return { key, geometry: this.cache.acquire(key, () => createGeometry(primitive)) };
562
+ }
563
+ release(key) {
564
+ this.cache.release(key, (geometry) => geometry.dispose());
565
+ }
566
+ clear() {
567
+ this.cache.clear((geometry) => geometry.dispose());
568
+ }
569
+ }
570
+ class TextureCache {
571
+ constructor() {
572
+ this.items = new Map();
573
+ this.inflight = new Map();
574
+ }
575
+ async acquire(name) {
576
+ const hit = this.items.get(name);
577
+ if (hit) {
578
+ hit.refs += 1;
579
+ return hit.texture;
580
+ }
581
+ let pending = this.inflight.get(name);
582
+ if (!pending) {
583
+ pending = textureFromNamedImage(name);
584
+ this.inflight.set(name, pending);
585
+ }
586
+ const texture = await pending;
587
+ this.inflight.delete(name);
588
+ const existing = this.items.get(name);
589
+ if (existing) {
590
+ existing.refs += 1;
591
+ return existing.texture;
592
+ }
593
+ this.items.set(name, { texture, refs: 1 });
594
+ return texture;
595
+ }
596
+ release(name) {
597
+ if (!name)
598
+ return;
599
+ const hit = this.items.get(name);
600
+ if (!hit)
601
+ return;
602
+ hit.refs -= 1;
603
+ if (hit.refs > 0)
604
+ return;
605
+ hit.texture.dispose();
606
+ this.items.delete(name);
607
+ }
608
+ clear() {
609
+ for (const hit of this.items.values()) {
610
+ hit.texture.dispose();
611
+ }
612
+ this.items.clear();
613
+ this.inflight.clear();
614
+ }
615
+ }
616
+ function createMaterial(p, map) {
617
+ const common = {
618
+ color: p.color,
619
+ wireframe: p.wireframe,
620
+ transparent: p.opacity < 1 || !!map,
621
+ opacity: p.opacity,
622
+ side: p.doubleSide ? DoubleSide : FrontSide,
623
+ map,
624
+ };
625
+ switch (p.material) {
626
+ case 'basic':
627
+ return new MeshBasicMaterial(common);
628
+ case 'lambert':
629
+ return new MeshLambertMaterial(common);
630
+ case 'toon':
631
+ return new MeshToonMaterial(common);
632
+ case 'standard':
633
+ default:
634
+ return new MeshStandardMaterial({
635
+ ...common,
636
+ roughness: p.roughness,
637
+ metalness: p.metalness,
638
+ emissive: p.emissive,
639
+ });
640
+ }
641
+ }
642
+ class MaterialCache {
643
+ constructor() {
644
+ this.cache = new RefCache();
645
+ this.maps = new Map();
646
+ this.textures = new TextureCache();
647
+ }
648
+ async acquire(primitive) {
649
+ const key = materialKey(primitive);
650
+ const hit = this.cache.get(key);
651
+ if (hit) {
652
+ this.cache.acquire(key, () => hit);
653
+ return { key, material: hit };
654
+ }
655
+ const texture = primitive.map ? await this.textures.acquire(primitive.map) : null;
656
+ const again = this.cache.get(key);
657
+ if (again) {
658
+ if (primitive.map)
659
+ this.textures.release(primitive.map);
660
+ this.cache.acquire(key, () => again);
661
+ return { key, material: again };
662
+ }
663
+ const material = createMaterial(primitive, texture);
664
+ this.cache.acquire(key, () => material);
665
+ if (primitive.map)
666
+ this.maps.set(key, primitive.map);
667
+ return { key, material };
668
+ }
669
+ release(key) {
670
+ const mapName = this.maps.get(key);
671
+ this.cache.release(key, (material) => {
672
+ material.dispose();
673
+ if (mapName) {
674
+ this.textures.release(mapName);
675
+ this.maps.delete(key);
676
+ }
677
+ });
678
+ }
679
+ clear() {
680
+ this.cache.clear((material) => material.dispose());
681
+ this.maps.clear();
682
+ this.textures.clear();
683
+ }
684
+ }
685
+
686
+ const POSE_PROPS = new Set([
687
+ 'positionX', 'positionY', 'positionZ',
688
+ 'rotationX', 'rotationY', 'rotationZ',
689
+ 'scaleX', 'scaleY', 'scaleZ',
690
+ ]);
217
691
  let Graphics3DSystem = class Graphics3DSystem extends Renderer3D {
218
692
  constructor() {
219
693
  super(...arguments);
220
694
  this.name = 'Graphics3DSystem';
221
695
  this.entries = new Map();
696
+ this.geos = new GeometryCache();
697
+ this.mats = new MaterialCache();
698
+ this.buckets = null;
222
699
  }
223
700
  static { this.systemName = 'Graphics3DSystem'; }
224
701
  init() {
225
702
  const renderer3DSystem = this.game.getSystem(Renderer3DSystem);
226
703
  renderer3DSystem.rendererManager.register(this);
704
+ this.buckets = new InstanceBuckets(this.threeContext.scene, !!this.threeContext.shadows);
227
705
  }
228
706
  componentChanged(changed) {
707
+ if (changed.componentName === 'Transform') {
708
+ this.handleHierarchyChange(changed.gameObject);
709
+ return;
710
+ }
229
711
  if (changed.componentName !== 'Graphics3D')
230
712
  return;
231
713
  if (changed.type === OBSERVER_TYPE.ADD) {
232
- this.handleAdd(changed.gameObject, changed.component);
714
+ this.mount(changed.gameObject, changed.component);
233
715
  }
234
716
  else if (changed.type === OBSERVER_TYPE.REMOVE) {
235
717
  this.handleRemove(changed.gameObject.id);
236
718
  }
237
719
  else {
238
- this.handleChange(changed);
720
+ const changedProp = changed.prop?.prop?.[0];
721
+ if (POSE_PROPS.has(changedProp))
722
+ return;
723
+ this.handleRemove(changed.gameObject.id);
724
+ this.mount(changed.gameObject, changed.component);
239
725
  }
240
726
  }
241
727
  rendererUpdate(gameObject) {
@@ -245,87 +731,195 @@ let Graphics3DSystem = class Graphics3DSystem extends Renderer3D {
245
731
  const entry = this.entries.get(gameObject.id);
246
732
  if (!entry)
247
733
  return;
248
- entry.mesh.position.set(component.positionX, component.positionY, component.positionZ);
249
- entry.mesh.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
250
- entry.mesh.scale.set(component.scaleX, component.scaleY, component.scaleZ);
251
- }
252
- handleAdd(gameObject, component) {
253
- const geometry = this.createGeometry(component);
254
- const material = new MeshStandardMaterial({
255
- color: component.color,
256
- wireframe: component.wireframe,
257
- transparent: component.opacity < 1,
258
- opacity: component.opacity,
259
- });
260
- const mesh = new Mesh(geometry, material);
261
- tagObject3D(mesh, gameObject.id);
262
- mesh.position.set(component.positionX, component.positionY, component.positionZ);
263
- mesh.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
264
- mesh.scale.set(component.scaleX, component.scaleY, component.scaleZ);
265
- this.threeContext.scene.add(mesh);
266
- this.entries.set(gameObject.id, { mesh });
267
- }
268
- handleChange(changed) {
269
- const component = changed.component;
270
- const changedProp = changed.prop?.prop?.[0];
271
- const entry = this.entries.get(changed.gameObject.id);
272
- if (!entry)
734
+ const transform = gameObject.getComponent(Transform3D);
735
+ const applyVisualPose = entry.poseBridge.sync(component, transform);
736
+ const next = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
737
+ if (!applyVisualPose) {
738
+ entry.lastPose = next;
739
+ if (entry.group) {
740
+ entry.group.position.set(0, 0, 0);
741
+ entry.group.rotation.set(0, 0, 0);
742
+ entry.group.scale.set(1, 1, 1);
743
+ }
744
+ return;
745
+ }
746
+ if (entry.lastPose === next)
747
+ return;
748
+ entry.lastPose = next;
749
+ if (entry.mode === 'instance' && entry.bucketKey) {
750
+ this.buckets?.setMatrix(entry.bucketKey, gameObject.id, composePose(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ));
273
751
  return;
274
- if (GEOMETRY_PROPS.includes(changedProp)) {
275
- entry.mesh.geometry.dispose();
276
- entry.mesh.geometry = this.createGeometry(component);
277
752
  }
278
- if (MATERIAL_PROPS.includes(changedProp)) {
279
- const material = entry.mesh.material;
280
- material.color.setHex(component.color);
281
- material.wireframe = component.wireframe;
282
- material.opacity = component.opacity;
283
- material.transparent = component.opacity < 1;
753
+ if (entry.group) {
754
+ entry.group.position.set(component.positionX, component.positionY, component.positionZ);
755
+ entry.group.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
756
+ entry.group.scale.set(component.scaleX, component.scaleY, component.scaleZ);
757
+ }
758
+ }
759
+ async mount(gameObject, component) {
760
+ const asyncId = this.increaseAsyncId(gameObject.id);
761
+ let prepared;
762
+ try {
763
+ prepared = await this.prepare(component);
764
+ }
765
+ catch (err) {
766
+ console.error(`Graphics3D prepare failed for ${gameObject.name}`, err);
767
+ return;
768
+ }
769
+ if (!this.validateAsyncId(gameObject.id, asyncId)) {
770
+ this.releasePrepared(prepared);
771
+ return;
772
+ }
773
+ this.attach(gameObject, component, prepared);
774
+ }
775
+ async prepare(component) {
776
+ const specs = this.resolveParts(component);
777
+ const prepared = [];
778
+ try {
779
+ for (const primitive of specs) {
780
+ const { key: geoKey, geometry } = this.geos.acquire(primitive);
781
+ try {
782
+ const { key: matKey, material } = await this.mats.acquire(primitive);
783
+ prepared.push({ primitive, geoKey, matKey, geometry, material });
784
+ }
785
+ catch (err) {
786
+ this.geos.release(geoKey);
787
+ throw err;
788
+ }
789
+ }
790
+ }
791
+ catch (err) {
792
+ this.releasePrepared(prepared);
793
+ throw err;
794
+ }
795
+ return prepared;
796
+ }
797
+ resolveParts(component) {
798
+ if (component.parts && component.parts.length > 0) {
799
+ return component.parts.map((part) => resolvePrimitive(part, component));
800
+ }
801
+ return [resolvePrimitive(component)];
802
+ }
803
+ handleHierarchyChange(gameObject) {
804
+ const parent = gameObject.parent;
805
+ if (parent && parent !== gameObject.scene) {
806
+ this.ensureGroupMode(parent);
807
+ }
808
+ this.ensureGroupMode(gameObject);
809
+ this.threeContext.reparentGameObject(gameObject);
810
+ }
811
+ ensureGroupMode(gameObject) {
812
+ const entry = this.entries.get(gameObject.id);
813
+ if (!entry || entry.mode !== 'instance')
814
+ return;
815
+ const component = gameObject.getComponent(Graphics3D);
816
+ if (!component)
817
+ return;
818
+ if (entry.bucketKey) {
819
+ this.buckets?.remove(entry.bucketKey, gameObject.id);
820
+ }
821
+ this.attachGroup(gameObject, component, entry.prepared, entry.lastPose);
822
+ }
823
+ attach(gameObject, component, prepared) {
824
+ const useInstance = component.instanced !== false
825
+ && prepared.length === 1
826
+ && is3DSceneParent(gameObject)
827
+ && !has3DChildGameObjects(gameObject)
828
+ && !gameObject.getComponent(Transform3D)
829
+ && !this.threeContext.hasTransform3DRoot(gameObject.id);
830
+ const lastPose = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
831
+ if (useInstance) {
832
+ const part = prepared[0];
833
+ const bucketKey = `${part.geoKey}|${part.matKey}`;
834
+ this.buckets?.add(bucketKey, gameObject.id, part.geometry, part.material, composePose(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ));
835
+ this.entries.set(gameObject.id, {
836
+ mode: 'instance',
837
+ bucketKey,
838
+ prepared,
839
+ lastPose,
840
+ poseBridge: new VisualPoseBridge(),
841
+ });
842
+ return;
843
+ }
844
+ this.attachGroup(gameObject, component, prepared, lastPose);
845
+ }
846
+ attachGroup(gameObject, component, prepared, lastPose) {
847
+ const group = new Group();
848
+ const shadows = !!this.threeContext.shadows;
849
+ for (const part of prepared) {
850
+ const mesh = new Mesh(part.geometry, part.material);
851
+ mesh.position.set(part.primitive.positionX, part.primitive.positionY, part.primitive.positionZ);
852
+ mesh.rotation.set(part.primitive.rotationX, part.primitive.rotationY, part.primitive.rotationZ);
853
+ mesh.scale.set(part.primitive.scaleX, part.primitive.scaleY, part.primitive.scaleZ);
854
+ mesh.castShadow = shadows;
855
+ mesh.receiveShadow = shadows;
856
+ group.add(mesh);
857
+ }
858
+ tagObject3D(group, gameObject.id);
859
+ const transform = gameObject.getComponent(Transform3D);
860
+ const poseBridge = new VisualPoseBridge();
861
+ const applyVisualPose = poseBridge.sync(component, transform);
862
+ if (applyVisualPose) {
863
+ group.position.set(component.positionX, component.positionY, component.positionZ);
864
+ group.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
865
+ group.scale.set(component.scaleX, component.scaleY, component.scaleZ);
866
+ }
867
+ else {
868
+ group.position.set(0, 0, 0);
869
+ group.rotation.set(0, 0, 0);
870
+ group.scale.set(1, 1, 1);
871
+ }
872
+ this.threeContext.attachVisual(gameObject.id, group, gameObject);
873
+ this.entries.set(gameObject.id, {
874
+ mode: 'group',
875
+ group,
876
+ prepared,
877
+ lastPose,
878
+ poseBridge,
879
+ });
880
+ }
881
+ releasePrepared(prepared) {
882
+ for (const part of prepared) {
883
+ this.geos.release(part.geoKey);
884
+ this.mats.release(part.matKey);
284
885
  }
285
886
  }
286
887
  handleRemove(id) {
888
+ this.increaseAsyncId(id);
287
889
  const entry = this.entries.get(id);
288
890
  if (!entry)
289
891
  return;
290
- this.threeContext.scene.remove(entry.mesh);
291
- entry.mesh.geometry.dispose();
292
- entry.mesh.material.dispose();
293
- this.entries.delete(id);
294
- }
295
- createGeometry(component) {
296
- switch (component.shape) {
297
- case 'sphere':
298
- return new SphereGeometry(component.radius, component.segments, component.segments);
299
- case 'cylinder':
300
- return new CylinderGeometry(component.radius, component.radius, component.height, component.segments);
301
- case 'plane':
302
- return new PlaneGeometry(component.width, component.height);
303
- case 'cone':
304
- return new ConeGeometry(component.radius, component.height, component.segments);
305
- case 'torus':
306
- return new TorusGeometry(component.radius, component.radius * 0.4, 16, component.segments);
307
- case 'box':
308
- default:
309
- return new BoxGeometry(component.width, component.height, component.depth);
892
+ if (entry.mode === 'instance' && entry.bucketKey) {
893
+ this.buckets?.remove(entry.bucketKey, id);
310
894
  }
895
+ else if (entry.group) {
896
+ this.threeContext.detachVisual(id, entry.group);
897
+ }
898
+ this.releasePrepared(entry.prepared);
899
+ this.entries.delete(id);
311
900
  }
312
901
  onDestroy() {
313
902
  for (const [id] of this.entries) {
314
903
  this.handleRemove(id);
315
904
  }
316
905
  this.entries.clear();
906
+ this.buckets?.clear();
907
+ this.geos.clear();
908
+ this.mats.clear();
317
909
  }
318
910
  };
319
911
  Graphics3DSystem = __decorate([
320
912
  decorators.componentObserver({
321
913
  Graphics3D: [
322
914
  'shape', 'color', 'wireframe',
323
- 'width', 'height', 'depth', 'radius', 'segments',
915
+ 'width', 'height', 'depth', 'radius', 'segments', 'tube',
324
916
  'positionX', 'positionY', 'positionZ',
325
917
  'rotationX', 'rotationY', 'rotationZ',
326
918
  'scaleX', 'scaleY', 'scaleZ',
327
- 'opacity',
919
+ 'opacity', 'roughness', 'metalness', 'emissive',
920
+ 'map', 'doubleSide', 'material', 'instanced', 'parts',
328
921
  ],
922
+ Transform: ['_parent'],
329
923
  })
330
924
  ], Graphics3DSystem);
331
925
  var Graphics3DSystem_default = Graphics3DSystem;