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