@combos-fun/plugin-renderer-3d 0.0.45 → 0.0.46

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,33 +1,39 @@
1
1
  # `@combos-fun/plugin-renderer-3d` — Agent notes
2
2
 
3
- 3D rendering foundation for Combos Fun. Wraps Three.js (`three` ^0.172) and exposes `Renderer3DSystem`, the `Renderer3D` base class, and a `ThreeContext` that owns the Three.js scene / camera / lights / WebGL renderer / `Clock`.
3
+ 3D rendering foundation for Combos Fun, backed by Three.js (`three` ^0.172). Read this before any `plugin-renderer-3d-*` leaf skill; shared setup, resource, pose, and stale-async rules live here.
4
4
 
5
- ## When to read
5
+ ## 3D drawable picker
6
6
 
7
- Read for any 3D rendering task: setting up the canvas, blank-3D-canvas debugging, custom 3D renderer plugins, async asset loading. All `plugin-renderer-3d-*` sub-plugins assume this file is already loaded.
7
+ - Procedural primitive, including a texture-mapped primitive `Graphics3D`
8
+ - One image on one plane → `Img3D`
9
+ - GLB / glTF / FBX / OBJ / STL / DAE / PLY asset → `Model3D`
10
+ - Repeated UV texture on a plane → `TilingSprite3D`
11
+ - Spritesheet animation on a plane → `SpriteAnimation3D`
12
+ - Replace or override materials on an existing visual → sibling `Material3D`
8
13
 
9
14
  ## Public API
10
15
 
16
+ The current package entry exports these runtime values:
17
+
11
18
  ```ts
12
19
  import {
13
- Renderer3DSystem,
14
- Renderer3D,
15
- Renderer3DManager,
16
- ThreeContext,
17
- Transform3D,
18
- Transform3DSystem,
19
- Render3D,
20
- Render3DSystem,
21
- Camera3D,
22
- Camera3DSystem,
23
- Light3D,
24
- Light3DSystem,
25
- tagObject3D,
26
- requireNamedResource,
27
- textureFromNamedImage,
20
+ Renderer3DSystem, ThreeContext, Renderer3D, Renderer3DManager,
21
+ Transform3D, Transform3DSystem,
22
+ Render3D, Render3DSystem,
23
+ Camera3D, Camera3DSystem,
24
+ Light3D, Light3DSystem,
25
+ COMBOS_GAME_OBJECT_ID, COMBOS_TRANSFORM3D,
26
+ tagObject3D, gameObjectIdFromObject3D, gameObjectIdFromIntersection,
27
+ isTransform3DRoot,
28
+ requireNamedResource, textureFromNamedImage, textureFromResourceImage,
29
+ is3DSceneParent, has3DChildGameObjects,
30
+ readPose, poseKeyOf, copyPose, isIdentityPose, seedTransform3D,
31
+ VisualPoseBridge,
28
32
  } from '@combos-fun/plugin-renderer-3d';
29
33
  ```
30
34
 
35
+ Type-only exports are `Renderer3DSystemParams`, `ThreeContextParams`, `FogParams`, `HemisphereLightParams`, `ToneMappingKind`, `Transform3DLike`, `Transform3DParams`, `Render3DParams`, `Camera3DParams`, `Light3DParams`, `Light3DKind`, and `PoseLike`.
36
+
31
37
  ### `Renderer3DSystem`
32
38
 
33
39
  `systemName = 'Renderer3DSystem'`. Init params:
@@ -41,95 +47,21 @@ import {
41
47
  | `antialias` | `boolean` | `true` |
42
48
  | `backgroundColor` | `number` | `0x000000` |
43
49
  | `backgroundAlpha` | `number` | `1` |
44
- | `fog` | `false` or `{ color?, near?, far? }` | off |
45
- | `hemisphereLight` | `false` or `{ skyColor?, groundColor?, intensity? }` | on (`0xffffff` / `0x444444` / `0.35`) |
50
+ | `fog` | `boolean \| { color?, near?, far? }` | `false`; object defaults: background color / `8` / `40` |
51
+ | `hemisphereLight` | `boolean \| { skyColor?, groundColor?, intensity? }` | on; `0xffffff` / `0x444444` / `0.35` |
46
52
  | `shadows` | `boolean` | `false` |
47
53
  | `toneMapping` | `'none' \| 'aces' \| 'linear' \| 'reinhard' \| 'cineon'` | `'aces'` |
48
54
  | `toneMappingExposure` | `number` | `1` |
49
55
  | `ambientIntensity` | `number` | `0.6` |
50
56
  | `directionalIntensity` | `number` | `0.8` |
51
57
 
52
- Either `canvas` or `container` must be provided.
53
-
54
- ### Built-in scene defaults
55
-
56
- `ThreeContext` automatically creates:
57
-
58
- - `PerspectiveCamera` (FOV 75, z=5)
59
- - `AmbientLight` (`0xffffff`, intensity `0.6`)
60
- - `DirectionalLight` (`0xffffff`, intensity `0.8`, position `(5, 10, 7.5)`)
61
- - `HemisphereLight` (disable with `hemisphereLight: false`)
62
- - `ACESFilmicToneMapping` (set `toneMapping: 'none'` to turn off)
63
- - Optional `Fog` / shadow maps when those params are set
64
- - `WebGLRenderer`, `Clock`
65
-
66
- `Graphics3D` / `Model3D` / `Img3D` meshes receive `castShadow` / `receiveShadow` only when `shadows: true`. `Model3D` traverses the loaded group and stamps every mesh.
67
-
68
- ### Transform3D (pose + empty parents)
58
+ Pass `canvas` to reuse an element. With no `canvas`, `container` appends the newly created renderer canvas; with neither, the canvas is created but not mounted. Defaults also include a perspective camera (FOV `75`, near `0.1`, far `1000`, z=`5`), ambient light, directional light at `(5, 10, 7.5)`, hemisphere light, `WebGLRenderer`, and `Clock`.
69
59
 
70
- `Transform3D` is the 3D pose source of truth (`position*` / `rotation*` / `scale*`, plus read-only `worldPosition*` / `worldRotation*`). `Renderer3DSystem` auto-adds `Transform3D` on every `Transform` ADD except the Scene itself, and creates a Three.js `Group` root. Visuals (`Graphics3D`, `Model3D`, …) hang under that Group via `attachVisual` — they no longer replace the root.
60
+ ## Shared contracts for every leaf plugin
71
61
 
72
- Empty parents work: `hero → hand → sword` does not need a mesh on `hand`.
62
+ ### System order
73
63
 
74
- Child `Transform3D` / visual `position*` are **local**. A `Graphics3D` that is instanced is converted to a `Group` when it gains a parent, children, or a `Transform3D` root.
75
-
76
- `Physics3D.update()` writes pose back onto sibling `Transform3D` (and other `position*` components).
77
-
78
- ### Render3D / Camera3D / Light3D
79
-
80
- | Component | System | Role |
81
- |-----------|--------|------|
82
- | `Render3D` | `Render3DSystem` | `visible`, `opacity` (2D `alpha`), `renderOrder` (2D `zIndex`), `sortableChildren` |
83
- | `Camera3D` | `Camera3DSystem` | `fov` / `near` / `far` / `active` / optional `lookAt`. Pose is `Transform3D`. First camera without a pose is placed at `z=5`. No `Camera3D` → keep the default `PerspectiveCamera`. |
84
- | `Light3D` | `Light3DSystem` | `ambient` / `directional` / `hemisphere` / `point` / `spot`. **The first `Light3D` removes the built-in default lights.** |
85
-
86
- Register those systems after `Renderer3DSystem`. `Transform3DSystem` is optional (the renderer already creates roots) but recommended so pose stays in the Transform3D system.
87
-
88
- ### `Renderer3D` base class
89
-
90
- Extend this for any custom 3D rendering plugin:
91
-
92
- ```ts
93
- class MyRenderer3D extends Renderer3D {
94
- init() {
95
- this.rendererSystem = this.game.getSystem(Renderer3DSystem);
96
- this.rendererSystem.rendererManager.register(this);
97
- }
98
- componentChanged(changed) {
99
- const scene = this.threeContext.scene;
100
- /* create / update / dispose THREE.Object3D under scene */
101
- tagObject3D(object3d, changed.gameObject.id);
102
- }
103
- rendererUpdate(gameObject) {
104
- /* per-frame sync */
105
- }
106
- }
107
- ```
108
-
109
- Always call `tagObject3D(object, gameObject.id)` so `plugin-renderer-3d-event` can raycast the mesh. Attach visuals with `this.threeContext.attachVisual(id, object, gameObject)` so they parent under the `Transform3D` Group.
110
-
111
- ### Named resources (same as 2D)
112
-
113
- 3D `Img3D` / `Model3D` / `TilingSprite3D` / `SpriteAnimation3D` take an engine **resource name**. Register with `resource.addResource` (`RESOURCE_TYPE.IMAGE` / `MODEL` / `SPRITE_ANIMATION`). Put CDN URLs in `src`, not on the component. A missing name throws; there is no `http` / `./` / `/` fallback.
114
-
115
- Use `requireNamedResource(name)` and `textureFromNamedImage(name)` from this package. 3D image systems read `data.image` (and `data.json` for spritesheets) so they do not overwrite 2D Pixi `registerInstance(IMAGE)`.
116
-
117
- ### Async loading pattern
118
-
119
- For 3D plugins that load assets (`Img3D`, `Model3D`, etc.) use the inherited helpers to drop stale work when a component is removed mid-load:
120
-
121
- ```ts
122
- const asyncId = this.increaseAsyncId(gameObject.id);
123
- const data = await loadSomething();
124
- if (!this.validateAsyncId(gameObject.id, asyncId)) return; // stale
125
- /* attach data */
126
- ```
127
-
128
- This prevents memory leaks and double-add bugs when the same `GameObject` is re-used quickly.
129
-
130
- ## Required setup
131
-
132
- `Renderer3DSystem` **must** be the first system added before any 3D sub-system that calls `getSystem(Renderer3DSystem)` in `init`. For taps, also register `Event3DSystem` from `@combos-fun/plugin-renderer-3d-event`.
64
+ `Renderer3DSystem` must precede every 3D leaf system because each leaf registers with its `rendererManager` during `init`. Add `Transform3DSystem`, `Render3DSystem`, camera/light systems, drawable systems, then overlays such as `Material3DSystem`; add `Event3DSystem` for picking. `Transform3DSystem` is optional for root creation because `Renderer3DSystem` also ensures roots, but it remains the normal pose system.
133
65
 
134
66
  ```ts
135
67
  new Game({
@@ -141,68 +73,66 @@ new Game({
141
73
  new Light3DSystem(),
142
74
  new Graphics3DSystem(),
143
75
  new Img3DSystem(),
76
+ new Material3DSystem(),
144
77
  new Event3DSystem(),
145
- // Physics3DSystem + Physics3DConstraintSystem from @combos-fun/plugin-cannon
146
78
  ],
147
79
  });
148
80
  ```
149
81
 
150
- ## Runtime behaviour
82
+ ### Named resources
151
83
 
152
- - `ThreeContext` owns the render loop. `Renderer3D` subclasses register with `rendererManager` and are driven each frame.
153
- - Three.js `Object3D`s should be attached with `attachVisual` / `attachObject3D`, not by calling `scene.add` in a plugin.
84
+ `resource` fields contain an engine resource **name**, not a URL or path. Register the appropriate `RESOURCE_TYPE` with `resource.addResource`; URLs belong in `src`. There is no URL fallback. `requireNamedResource` rejects empty or unknown names. Asset components currently skip ADD when their own `resource` is empty; image helpers build a Three `Texture` from `data.image`, leaving the 2D Pixi resource instance alone.
154
85
 
155
- ## Common pitfalls
86
+ ### Pose and hierarchy
156
87
 
157
- | Symptom | Fix |
158
- |---------|-----|
159
- | Blank canvas | Add `Renderer3DSystem`; ensure `autoStart: true` or call `game.start()` |
160
- | Nothing draws | Add the matching 3D sub-system (`Graphics3DSystem`, `Model3DSystem`, etc.) before adding components |
161
- | Object loaded but not visible | Object likely loaded at origin — set `Transform3D.position*` (or sibling visual `position*`, which seeds `Transform3D`), or check camera distance (default `z=5`) |
162
- | Empty parent / hand slot has no child | Add `Transform3DSystem` or rely on `Renderer3DSystem` auto-`Transform3D`; `addChild` the slot then the item |
163
- | Custom lights + still have default sun | First `Light3D` removes defaults — add `Light3DSystem` |
164
- | Want 2D-like hide / fade / draw order | `Render3D` + `Render3DSystem` (`visible` / `opacity` / `renderOrder`) |
165
- | `getSystem` undefined | Use class reference `game.getSystem(Renderer3DSystem)` |
166
- | Memory leak after fast remove | Use `increaseAsyncId` / `validateAsyncId` to drop stale async work |
167
- | CORS errors loading 3D assets | Host on same origin or CORS-enabled CDN; URLs live in `addResource` `src` |
168
- | `tap` never fires | Use `Event3D` + `Event3DSystem`, not 2D `Event` |
169
- | Instanced primitives not pickable | `gameObjectIdFromIntersection` reads `instanceId`; official `Graphics3DSystem` fills `userData.combosInstanceGameObjectIds` |
170
-
171
- ## Minimal example
88
+ `Transform3D` is the pose source of truth (`position*`, `rotation*`, `scale*`, plus computed world position/rotation). A Three `Group` root is created per non-Scene GameObject, so empty parents and local child transforms work.
172
89
 
173
- ```ts
174
- import { Game } from '@combos-fun/engine';
175
- import { Renderer3DSystem } from '@combos-fun/plugin-renderer-3d';
90
+ Leaf visual pose fields are compatibility inputs. On first bridge sync, a non-identity visual pose seeds an identity sibling `Transform3D`; the visual object is then reset to identity under that root. Prefer writing `Transform3D` after setup. `attachVisual` tags the visual and parents it under the root; use it instead of `scene.add`.
176
91
 
177
- new Game({
178
- systems: [
179
- new Renderer3DSystem({
180
- canvas: document.querySelector('#canvas')!,
181
- width: 750,
182
- height: 1334,
183
- }),
184
- ],
185
- });
186
- ```
187
-
188
- This alone shows an empty 3D scene with default lighting. Add `Graphics3DSystem` etc. and matching components to render anything.
92
+ ### Async lifecycle
189
93
 
190
- Empty parent / equipment slot:
94
+ Asset-loading systems use a per-GameObject async id to reject stale work after
95
+ removal, replacement, or resource changes:
191
96
 
192
97
  ```ts
193
- const hero = new GameObject('hero');
194
- hero.addComponent(new Transform3D({ positionY: 1 }));
195
- hero.addComponent(new Graphics3D({ shape: 'box', width: 0.6, height: 1.2, depth: 0.4 }));
196
- const hand = new GameObject('hand');
197
- hand.addComponent(new Transform3D({ positionX: 0.4, positionY: 0.3 }));
198
- const sword = new GameObject('sword');
199
- sword.addComponent(new Graphics3D({ shape: 'box', width: 0.1, height: 0.8, depth: 0.1, color: 0xcccccc }));
200
- hero.addChild(hand);
201
- hand.addChild(sword);
98
+ const asyncId = this.increaseAsyncId(gameObject.id);
99
+ const asset = await loadSomething();
100
+ if (!this.validateAsyncId(gameObject.id, asyncId)) return;
202
101
  ```
203
102
 
103
+ Dispose assets created by rejected work. `Model3DSystem` is an exception: it
104
+ validates after attachment, so its skill documents the stale-clone risk.
105
+
106
+ ## Core components
107
+
108
+ | Component | Defaults and behavior |
109
+ |-----------|-----------------------|
110
+ | `Render3D` | `visible: true`, `opacity: 1`, `renderOrder: 0`, `sortableChildren: false`. Every renderer update, `opacity` is written to **every mesh material under this GameObject's registered root Object3D subtree** (including nested child visuals), so it overrides visual/`Material3D` opacity for that frame. |
111
+ | `Camera3D` | Controls `fov`, `near`, `far`, `active`; `lookAt` is boolean with `lookAtX/Y/Z`. Without one, the fallback camera remains active. |
112
+ | `Light3D` | `type` defaults to `'directional'`; supports color/intensity/shadows and type-specific fields. The first `Light3D` removes all built-in default lights. |
113
+
114
+ When `shadows: true`, official drawable systems mark their meshes `castShadow` and `receiveShadow`; `Model3D` traverses the loaded clone.
115
+
116
+ ## Custom renderer plugins
117
+
118
+ Extend `Renderer3D`, register it with `Renderer3DSystem.rendererManager`, handle observer changes in `componentChanged`, and do frame sync in `rendererUpdate`. `attachVisual(id, object, gameObject)` both tags and parents a visual. Use `tagObject3D` yourself only when bypassing that helper.
119
+
120
+ The engine owns the loop: `Renderer3DSystem.update()` applies `Transform3D` and invokes leaf `rendererUpdate`; `lateUpdate()` advances animation mixers and renders the frame.
121
+
122
+ ## Common pitfalls
123
+
124
+ | Symptom | Fix |
125
+ |---------|-----|
126
+ | Blank canvas | Add `Renderer3DSystem`; mount its canvas; ensure the game starts |
127
+ | Nothing draws | Add the matching leaf system before adding its component |
128
+ | Loaded object is invisible | Check `Transform3D`, camera distance, front/back side, lights, and resource errors |
129
+ | `getSystem` is undefined | Use `game.getSystem(Renderer3DSystem)` or exact string `'Renderer3DSystem'` |
130
+ | Fast remove/re-add leaks or double-adds | Follow the async-id invalidation pattern |
131
+ | CORS failure | Serve assets from the same origin or a CORS-enabled URL registered in `src` |
132
+
133
+ Key 2D/3D differences: use `Render3D.opacity` rather than `Render.alpha`, `Event3D`/`Event3DSystem` rather than `Event`/`EventSystem`, and Cannon (`plugin-cannon`) rather than Matter for 3D physics.
134
+
204
135
  ## Verification
205
136
 
206
137
  - `pnpm --filter @combos-fun/plugin-renderer-3d run build`
207
- - Run a 3D example app and check the browser console for Three.js / WebGL
208
- errors and missing assets.
138
+ - Run a 3D example and inspect WebGL/resource errors in the browser console.
@@ -6,7 +6,6 @@ var three = require('three');
6
6
  var inspectorDecorator = require('@combos-fun/inspector-decorator');
7
7
 
8
8
  const COMBOS_GAME_OBJECT_ID = 'combosGameObjectId';
9
- const COMBOS_INSTANCE_GAME_OBJECT_IDS = 'combosInstanceGameObjectIds';
10
9
  const COMBOS_TRANSFORM3D = 'combosTransform3D';
11
10
  function isTransform3DRoot(object) {
12
11
  return !!object?.userData?.[COMBOS_TRANSFORM3D];
@@ -28,21 +27,9 @@ function gameObjectIdFromObject3D(object) {
28
27
  }
29
28
  return undefined;
30
29
  }
31
- /** Resolve a raycast hit, including InstancedMesh `instanceId` rows. */
30
+ /** Resolve a raycast hit to a GameObject id. */
32
31
  function gameObjectIdFromIntersection(intersection) {
33
- if (!intersection?.object)
34
- return undefined;
35
- if (typeof intersection.instanceId === 'number') {
36
- let current = intersection.object;
37
- while (current) {
38
- const ids = current.userData?.[COMBOS_INSTANCE_GAME_OBJECT_IDS];
39
- if (Array.isArray(ids) && typeof ids[intersection.instanceId] === 'number') {
40
- return ids[intersection.instanceId];
41
- }
42
- current = current.parent;
43
- }
44
- }
45
- return gameObjectIdFromObject3D(intersection.object);
32
+ return gameObjectIdFromObject3D(intersection?.object);
46
33
  }
47
34
 
48
35
  /** True when this GameObject is a scene root (no 3D parent). */
@@ -684,7 +671,7 @@ var Renderer3DSystem = Renderer3DSystem$1;
684
671
  /** Auto-generated by scripts/build-package.mjs — do not edit. */
685
672
  Object.assign(Renderer3DSystem, {
686
673
  packageName: "@combos-fun/plugin-renderer-3d",
687
- packageVersion: "0.0.45",
674
+ packageVersion: "0.0.46",
688
675
  });
689
676
 
690
677
  class Renderer3D extends engine.System {
@@ -1225,7 +1212,6 @@ async function textureFromNamedImage(name) {
1225
1212
  }
1226
1213
 
1227
1214
  exports.COMBOS_GAME_OBJECT_ID = COMBOS_GAME_OBJECT_ID;
1228
- exports.COMBOS_INSTANCE_GAME_OBJECT_IDS = COMBOS_INSTANCE_GAME_OBJECT_IDS;
1229
1215
  exports.COMBOS_TRANSFORM3D = COMBOS_TRANSFORM3D;
1230
1216
  exports.Camera3D = Camera3D;
1231
1217
  exports.Camera3DSystem = Camera3DSystem_default;