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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent-skill.md CHANGED
@@ -1,22 +1,25 @@
1
1
  # `@combos-fun/plugin-renderer-3d-graphics` — Agent notes
2
2
 
3
- Procedural 3D primitives for the 3D pipeline. One `Graphics3D` becomes either a shared `InstancedMesh` (identical singles) or a `Group` of meshes (`parts`).
3
+ Procedural primitives and compound primitive groups. Use this for plain or texture-mapped boxes, spheres, cylinders, planes, cones, tori, and capsules.
4
4
 
5
- ## When to read
6
-
7
- Read for any procedural primitive: box, sphere, cylinder, plane, cone, torus, capsule, debug placeholders, simple terrain, compound furniture.
8
-
9
- Read **before** guessing that 3D has no rotation or no per-frame pose — do not wait for a compile error. Pose fields and a chase-camera pattern are below.
5
+ Prerequisite: follow the parent `@combos-fun/plugin-renderer-3d` skill for system order, named resources, pose seeding, hierarchy, and stale-async handling.
10
6
 
11
7
  ## Public API
12
8
 
13
9
  ```ts
14
- import { Graphics3D, Graphics3DSystem, type Graphics3DParams, type Graphics3DPartParams } from '@combos-fun/plugin-renderer-3d-graphics';
10
+ import {
11
+ Graphics3D,
12
+ Graphics3DSystem,
13
+ type Graphics3DParams,
14
+ type Graphics3DPartParams,
15
+ type Graphics3DMaterialKind,
16
+ type Graphics3DShape,
17
+ } from '@combos-fun/plugin-renderer-3d-graphics';
15
18
  ```
16
19
 
17
- `componentName = 'Graphics3D'`, `systemName = 'Graphics3DSystem'`.
20
+ `componentName = 'Graphics3D'`; `systemName = 'Graphics3DSystem'`.
18
21
 
19
- ### `Graphics3DParams`
22
+ ## Parameters
20
23
 
21
24
  | Field | Type | Default |
22
25
  |-------|------|---------|
@@ -26,54 +29,34 @@ import { Graphics3D, Graphics3DSystem, type Graphics3DParams, type Graphics3DPar
26
29
  | `width` / `height` / `depth` | `number` | `1` |
27
30
  | `radius` | `number` | `0.5` |
28
31
  | `segments` | `number` | `12` |
29
- | `tube` | `number` | `0` `0.4 × radius` (torus only) |
30
- | `positionX` / `Y` / `Z` | `number` | `0` |
31
- | `rotationX` / `Y` / `Z` | `number` | `0` |
32
- | `scaleX` / `Y` / `Z` | `number` | `1` |
32
+ | `tube` | `number` | `0`; torus resolves it to `0.4 × radius` |
33
+ | `positionX/Y/Z` | `number` | `0` |
34
+ | `rotationX/Y/Z` | `number` | `0` radians |
35
+ | `scaleX/Y/Z` | `number` | `1` |
33
36
  | `opacity` | `number` | `1` |
34
- | `roughness` / `metalness` | `number` | `1` / `0` (`standard` only) |
35
- | `emissive` | `number` | `0` (hex) |
36
- | `map` | `string` | `''` (engine `IMAGE` name) |
37
+ | `roughness` / `metalness` | `number` | `1` / `0` |
38
+ | `emissive` | `number` | `0` |
39
+ | `map` | `string` | `''` (`IMAGE` resource name) |
37
40
  | `doubleSide` | `boolean` | `false` |
38
41
  | `material` | `'standard' \| 'basic' \| 'lambert' \| 'toon'` | `'standard'` |
39
- | `instanced` | `boolean` | `true` |
40
- | `parts` | `Graphics3DPartParams[]` | — |
41
-
42
- `parts` replaces the single primitive. Each part can override shape / color / map / local pose. The component pose still moves the whole group.
43
-
44
- Shape → Three.js geometry: `box → BoxGeometry`, `sphere → SphereGeometry`, `cylinder → CylinderGeometry`, `plane → PlaneGeometry` (uses `segments` as width/height segs), `cone → ConeGeometry`, `torus → TorusGeometry`, `capsule → CapsuleGeometry` (`height` is total height).
42
+ | `parts` | `Graphics3DPartParams[]` | `undefined` |
45
43
 
46
- Identical singles (same geometry + material, `instanced !== false`, no `parts`) share one `InstancedMesh`. Geometry and materials are cached.
44
+ When non-empty, `parts` replaces the single primitive. Each part inherits component values and may override geometry, appearance, and its local pose; the component pose moves the resulting `Group`.
47
45
 
48
- ## Required setup
46
+ Geometry mapping is direct to Three.js. Plane segments apply to both axes; capsule `height` is total height. Geometry and materials are reference-counted caches. Creating or rebuilding a mapped primitive is async; stale preparation releases acquired cache entries instead of attaching them.
49
47
 
50
- `Renderer3DSystem` then `Graphics3DSystem`. For `map`, register `RESOURCE_TYPE.IMAGE` first. Pose can live on sibling `Transform3D` (preferred once a hierarchy exists); `Graphics3D.position*` still seeds / updates it.
48
+ Non-pose changes rebuild the group. Pose changes are synchronized per frame without rebuilding. A sibling `Material3D` can replace the generated materials.
51
49
 
52
- ## Common pitfalls
50
+ ## Package-specific pitfalls
53
51
 
54
- | Symptom | Fix |
55
- |---------|-----|
56
- | Plane invisible from one side | Set `doubleSide: true`, or rotate so the camera sees the front |
57
- | Sphere very dark | Raise lights / `Renderer3DSystem` hemisphere, or use `material: 'basic'` |
58
- | Wireframe shimmer | `wireframe: true` that's by design |
59
- | Sync drift with `Physics3D` | Same `GameObject`; `Physics3D.update()` writes positions back |
60
- | `map` throws `Unknown resource` | Use the engine resource name, not a URL |
61
- | Compound house as 3 GameObjects | Prefer one `Graphics3D` with `parts`, or `parent.addChild(child)` so the child mesh hangs off the parent |
62
- | Child does not follow parent | `car.addChild(wheel)` — child `position*` are local. Do not also write world-space pose onto the wheel each frame unless it has its own `Physics3D` |
52
+ - A plane is single-sided unless `doubleSide: true`.
53
+ - A visual `PlaneGeometry` is finite; Cannon `Physics3DType.PLANE` is infinite unless `finite: true`.
54
+ - Lit materials may look dark without enough light; `basic` is unlit.
55
+ - Prefer `parts` for one compound visual. Use child GameObjects when parts need independent components or physics.
56
+ - Physics and graphics should share a GameObject when physics owns its pose.
63
57
 
64
58
  ## Minimal example
65
59
 
66
- ```ts
67
- const box = new GameObject('box');
68
- box.addComponent(new Graphics3D({
69
- shape: 'box',
70
- width: 1, height: 1, depth: 1,
71
- color: 0xff0000,
72
- roughness: 0.8,
73
- positionX: 0, positionY: 1, positionZ: 0,
74
- }));
75
- ```
76
-
77
60
  ```ts
78
61
  const house = new GameObject('house');
79
62
  house.addComponent(new Graphics3D({
@@ -82,47 +65,8 @@ house.addComponent(new Graphics3D({
82
65
  { shape: 'cone', radius: 1.4, height: 1, color: 0x8b2942, positionY: 1.1 },
83
66
  ],
84
67
  }));
85
-
86
- const car = new GameObject('car');
87
- car.addComponent(new Graphics3D({ shape: 'box', width: 2, height: 0.6, depth: 1, color: 0x3366ff }));
88
- const wheel = new GameObject('wheel');
89
- wheel.addComponent(new Graphics3D({ shape: 'cylinder', radius: 0.3, height: 0.2, color: 0x222222, rotationZ: Math.PI / 2, positionX: 0.7, positionY: -0.2 }));
90
- car.addChild(wheel);
91
- ```
92
-
93
- ## Per-frame pose + chase camera
94
-
95
- `rotationX/Y/Z` and `positionX/Y/Z` are writable every tick. Use `Component.update` (`deltaTime` is milliseconds). Camera: `this.game.getSystem(Renderer3DSystem).threeContext.camera`. Do not hide `#canvas` or draw a second Canvas 2D game.
96
-
97
- | Wrong | Correct |
98
- |-------|---------|
99
- | Static placemarker cube → “no rotation / don’t know how to change pose each frame” | Assign `graphics.rotationY` / `positionX` / `positionZ` in `update` |
100
- | Hide WebGL `#canvas` and cover the viewport with Canvas 2D / DOM fake perspective | Keep `#canvas` visible and interactive; keep drawing with `Graphics3D` / `Model3D` |
101
- | `setInterval` / extra `requestAnimationFrame` to move the mesh | `Component.update` so pause/resume follows `Game.ticker` |
102
- | 2D `TransformParams` on a 3D GameObject | Pose lives on `Transform3D` (or `Graphics3D` / `Model3D` / `Img3D` fields, which seed `Transform3D`) |
103
-
104
- ```ts
105
- class ChaseCraft extends Component {
106
- static componentName = 'ChaseCraft';
107
- heading = 0;
108
- update(frame: UpdateParams) {
109
- const mesh = this.gameObject.getComponent(Graphics3D);
110
- const camera = this.game.getSystem(Renderer3DSystem).threeContext.camera;
111
- const dt = frame.deltaTime / 1000;
112
- this.heading += dt * 0.4;
113
- mesh.rotationY = this.heading;
114
- mesh.positionX += Math.sin(this.heading) * 6 * dt;
115
- mesh.positionZ += Math.cos(this.heading) * 6 * dt;
116
- camera.position.set(
117
- mesh.positionX - Math.sin(this.heading) * 8,
118
- mesh.positionY + 3,
119
- mesh.positionZ - Math.cos(this.heading) * 8,
120
- );
121
- camera.lookAt(mesh.positionX, mesh.positionY, mesh.positionZ);
122
- }
123
- }
124
68
  ```
125
69
 
126
70
  ## Verification
127
71
 
128
- `pnpm --filter @combos-fun/plugin-renderer-3d-graphics run build`.
72
+ `pnpm --filter @combos-fun/plugin-renderer-3d-graphics run build`
@@ -34,7 +34,7 @@ class Graphics3D extends engine.Component {
34
34
  this.map = '';
35
35
  this.doubleSide = false;
36
36
  this.material = 'standard';
37
- this.instanced = true;
37
+ this.parts = undefined;
38
38
  }
39
39
  static { this.componentName = 'Graphics3D'; }
40
40
  init(obj) {
@@ -293,140 +293,6 @@ tslib.__decorate([
293
293
  enumOptions: ['standard', 'basic', 'lambert', 'toon'],
294
294
  })
295
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
- }
430
296
 
431
297
  function torusTube(radius, tube) {
432
298
  return tube && tube > 0 ? tube : radius * 0.4;
@@ -697,17 +563,15 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
697
563
  this.entries = new Map();
698
564
  this.geos = new GeometryCache();
699
565
  this.mats = new MaterialCache();
700
- this.buckets = null;
701
566
  }
702
567
  static { this.systemName = 'Graphics3DSystem'; }
703
568
  init() {
704
569
  const renderer3DSystem = this.game.getSystem(pluginRenderer3d.Renderer3DSystem);
705
570
  renderer3DSystem.rendererManager.register(this);
706
- this.buckets = new InstanceBuckets(this.threeContext.scene, !!this.threeContext.shadows);
707
571
  }
708
572
  componentChanged(changed) {
709
573
  if (changed.componentName === 'Transform') {
710
- this.handleHierarchyChange(changed.gameObject);
574
+ this.threeContext.reparentGameObject(changed.gameObject);
711
575
  return;
712
576
  }
713
577
  if (changed.componentName !== 'Graphics3D')
@@ -738,25 +602,17 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
738
602
  const next = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
739
603
  if (!applyVisualPose) {
740
604
  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
- }
605
+ entry.group.position.set(0, 0, 0);
606
+ entry.group.rotation.set(0, 0, 0);
607
+ entry.group.scale.set(1, 1, 1);
746
608
  return;
747
609
  }
748
610
  if (entry.lastPose === next)
749
611
  return;
750
612
  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));
753
- return;
754
- }
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
- }
613
+ entry.group.position.set(component.positionX, component.positionY, component.positionZ);
614
+ entry.group.rotation.set(component.rotationX, component.rotationY, component.rotationZ);
615
+ entry.group.scale.set(component.scaleX, component.scaleY, component.scaleZ);
760
616
  }
761
617
  async mount(gameObject, component) {
762
618
  const asyncId = this.increaseAsyncId(gameObject.id);
@@ -772,7 +628,7 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
772
628
  this.releasePrepared(prepared);
773
629
  return;
774
630
  }
775
- this.attach(gameObject, component, prepared);
631
+ this.attachGroup(gameObject, component, prepared);
776
632
  }
777
633
  async prepare(component) {
778
634
  const specs = this.resolveParts(component);
@@ -802,50 +658,7 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
802
658
  }
803
659
  return [resolvePrimitive(component)];
804
660
  }
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) {
661
+ attachGroup(gameObject, component, prepared) {
849
662
  const group = new three.Group();
850
663
  const shadows = !!this.threeContext.shadows;
851
664
  for (const part of prepared) {
@@ -871,9 +684,9 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
871
684
  group.rotation.set(0, 0, 0);
872
685
  group.scale.set(1, 1, 1);
873
686
  }
687
+ const lastPose = poseKey(component.positionX, component.positionY, component.positionZ, component.rotationX, component.rotationY, component.rotationZ, component.scaleX, component.scaleY, component.scaleZ);
874
688
  this.threeContext.attachVisual(gameObject.id, group, gameObject);
875
689
  this.entries.set(gameObject.id, {
876
- mode: 'group',
877
690
  group,
878
691
  prepared,
879
692
  lastPose,
@@ -891,12 +704,7 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
891
704
  const entry = this.entries.get(id);
892
705
  if (!entry)
893
706
  return;
894
- if (entry.mode === 'instance' && entry.bucketKey) {
895
- this.buckets?.remove(entry.bucketKey, id);
896
- }
897
- else if (entry.group) {
898
- this.threeContext.detachVisual(id, entry.group);
899
- }
707
+ this.threeContext.detachVisual(id, entry.group);
900
708
  this.releasePrepared(entry.prepared);
901
709
  this.entries.delete(id);
902
710
  }
@@ -905,7 +713,6 @@ let Graphics3DSystem = class Graphics3DSystem extends pluginRenderer3d.Renderer3
905
713
  this.handleRemove(id);
906
714
  }
907
715
  this.entries.clear();
908
- this.buckets?.clear();
909
716
  this.geos.clear();
910
717
  this.mats.clear();
911
718
  }
@@ -919,7 +726,7 @@ Graphics3DSystem = tslib.__decorate([
919
726
  'rotationX', 'rotationY', 'rotationZ',
920
727
  'scaleX', 'scaleY', 'scaleZ',
921
728
  'opacity', 'roughness', 'metalness', 'emissive',
922
- 'map', 'doubleSide', 'material', 'instanced', 'parts',
729
+ 'map', 'doubleSide', 'material', 'parts',
923
730
  ],
924
731
  Transform: ['_parent'],
925
732
  })