pixi-effects 0.48.2 → 0.49.0

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +51 -7
  3. package/ai/chat-template.html +8 -4
  4. package/ai/tools/check.mjs +6 -6
  5. package/dist/3d.cjs +963 -0
  6. package/dist/3d.cjs.map +1 -0
  7. package/dist/3d.d.cts +303 -0
  8. package/dist/3d.d.ts +303 -0
  9. package/dist/3d.js +956 -0
  10. package/dist/3d.js.map +1 -0
  11. package/dist/Canvas-BfZkAFJm.d.cts +12 -0
  12. package/dist/Canvas-BfZkAFJm.d.ts +12 -0
  13. package/dist/Composition-464AWDV6.cjs +16 -0
  14. package/dist/{Composition-TJ5D7Z63.cjs.map → Composition-464AWDV6.cjs.map} +1 -1
  15. package/dist/Composition-5U63LMPX.js +7 -0
  16. package/dist/{Composition-5FKV2IBP.js.map → Composition-5U63LMPX.js.map} +1 -1
  17. package/dist/Controller.d.cts +2 -2
  18. package/dist/Controller.d.ts +2 -2
  19. package/dist/{Movie--qSYKgAQ.d.ts → Movie-6EWOz6Yw.d.ts} +21 -5
  20. package/dist/{Movie-svZQF-9Z.d.cts → Movie-BxB7bauG.d.cts} +21 -5
  21. package/dist/{Spans-5ENZ3XGS.cjs → Spans-DUR2W5EH.cjs} +11 -11
  22. package/dist/{Spans-5ENZ3XGS.cjs.map → Spans-DUR2W5EH.cjs.map} +1 -1
  23. package/dist/{Spans-6A3VLHRH.js → Spans-W5VJRMGC.js} +6 -6
  24. package/dist/{Spans-6A3VLHRH.js.map → Spans-W5VJRMGC.js.map} +1 -1
  25. package/dist/{blendModes-V5XHC7U4.cjs → blendModes-NC76DNVX.cjs} +5 -5
  26. package/dist/{blendModes-V5XHC7U4.cjs.map → blendModes-NC76DNVX.cjs.map} +1 -1
  27. package/dist/{blendModes-535EMYGK.js → blendModes-ZOBLPSR3.js} +4 -4
  28. package/dist/{blendModes-535EMYGK.js.map → blendModes-ZOBLPSR3.js.map} +1 -1
  29. package/dist/{chunk-Z3DM4ETD.cjs → chunk-33USHRV5.cjs} +43 -32
  30. package/dist/chunk-33USHRV5.cjs.map +1 -0
  31. package/dist/{chunk-WYR2AU4W.cjs → chunk-5YNME6AH.cjs} +5 -5
  32. package/dist/{chunk-WYR2AU4W.cjs.map → chunk-5YNME6AH.cjs.map} +1 -1
  33. package/dist/{chunk-NGJXVJB7.cjs → chunk-ABWRISRK.cjs} +3 -2
  34. package/dist/chunk-ABWRISRK.cjs.map +1 -0
  35. package/dist/{chunk-CCPJD76D.cjs → chunk-BSFWVTSR.cjs} +93 -93
  36. package/dist/chunk-BSFWVTSR.cjs.map +1 -0
  37. package/dist/{chunk-IVKI6CPH.js → chunk-DOGY3EAW.js} +19 -9
  38. package/dist/chunk-DOGY3EAW.js.map +1 -0
  39. package/dist/{chunk-45ALIMAF.cjs → chunk-GUDLIAIG.cjs} +58 -55
  40. package/dist/chunk-GUDLIAIG.cjs.map +1 -0
  41. package/dist/{chunk-BFRWSV3R.js → chunk-H363KMJR.js} +3 -3
  42. package/dist/chunk-H363KMJR.js.map +1 -0
  43. package/dist/{chunk-JBDBICS2.js → chunk-MXACRKAN.js} +4 -4
  44. package/dist/{chunk-JBDBICS2.js.map → chunk-MXACRKAN.js.map} +1 -1
  45. package/dist/{chunk-RCKLUCB7.js → chunk-PBMDZT2T.js} +15 -12
  46. package/dist/chunk-PBMDZT2T.js.map +1 -0
  47. package/dist/{chunk-6D4W2KZQ.js → chunk-USUUAYTE.js} +6 -6
  48. package/dist/chunk-USUUAYTE.js.map +1 -0
  49. package/dist/index.cjs +35 -35
  50. package/dist/index.d.cts +3 -3
  51. package/dist/index.d.ts +3 -3
  52. package/dist/index.js +5 -5
  53. package/dist/loader.css +1 -1
  54. package/dist/{music-P4JRT73P.js → music-ATX22VAT.js} +3 -3
  55. package/dist/{music-P4JRT73P.js.map → music-ATX22VAT.js.map} +1 -1
  56. package/dist/{music-XQEPBIDT.cjs → music-OFVM3PYH.cjs} +3 -3
  57. package/dist/{music-XQEPBIDT.cjs.map → music-OFVM3PYH.cjs.map} +1 -1
  58. package/dist/present.cjs +5 -5
  59. package/dist/present.d.cts +2 -2
  60. package/dist/present.d.ts +2 -2
  61. package/dist/present.js +5 -5
  62. package/dist/three.cjs +7 -8
  63. package/dist/three.cjs.map +1 -1
  64. package/dist/three.d.cts +2 -12
  65. package/dist/three.d.ts +2 -12
  66. package/dist/three.js +2 -3
  67. package/dist/three.js.map +1 -1
  68. package/dist/{types-CTSrPvY3.d.cts → types-kt7DQKfO.d.cts} +1 -1
  69. package/dist/{types-CTSrPvY3.d.ts → types-kt7DQKfO.d.ts} +1 -1
  70. package/docs/api.md +41 -1
  71. package/docs/dsl.md +81 -1
  72. package/llms-full.txt +218 -5
  73. package/package.json +19 -3
  74. package/skills/pixi-effects/SKILL.md +3 -2
  75. package/skills/pixi-effects/reference/cheatsheet.md +2 -0
  76. package/skills/pixi-effects/reference/pitfalls.md +14 -0
  77. package/skills/pixi-effects/reference/recipes.md +77 -1
  78. package/skills/pixi-effects/template.html +13 -8
  79. package/dist/Composition-5FKV2IBP.js +0 -7
  80. package/dist/Composition-TJ5D7Z63.cjs +0 -16
  81. package/dist/chunk-45ALIMAF.cjs.map +0 -1
  82. package/dist/chunk-6D4W2KZQ.js.map +0 -1
  83. package/dist/chunk-BFRWSV3R.js.map +0 -1
  84. package/dist/chunk-CCPJD76D.cjs.map +0 -1
  85. package/dist/chunk-IVKI6CPH.js.map +0 -1
  86. package/dist/chunk-NGJXVJB7.cjs.map +0 -1
  87. package/dist/chunk-RCKLUCB7.js.map +0 -1
  88. package/dist/chunk-Z3DM4ETD.cjs.map +0 -1
@@ -963,4 +963,4 @@ interface SpaceNow {
963
963
  fog: FogState | null;
964
964
  }
965
965
 
966
- export { type SfxPreset as $, type AssetSpec as A, type BlendModeName as B, type CompositionSpec as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type MaskRefSpec as H, type ImageSequenceSpec as I, type MatteRefSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicDrum as N, type MusicDrums as O, type Props as P, type MusicInstrument as Q, type MusicLevel as R, type SequenceSpec as S, type TransitionSpec as T, type MusicTrack as U, type NullSequenceSpec as V, type PathShapeSpec as W, type PolygonShapeSpec as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type Stop as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type StopSpec as a4, type TextSpan as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, Sequence as b, type SpaceNow as c, type SequenceCommon as d, type PropValue as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
966
+ export { type SfxPreset as $, type AssetSpec as A, type BlendModeName as B, type CompositionSpec as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type MaskRefSpec as H, type ImageSequenceSpec as I, type MatteRefSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicDrum as N, type MusicDrums as O, type Props as P, type MusicInstrument as Q, type MusicLevel as R, type SequenceSpec as S, type TransitionSpec as T, type MusicTrack as U, type NullSequenceSpec as V, type PathShapeSpec as W, type PolygonShapeSpec as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type Stop as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type StopSpec as a4, type TextSpan as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, type SpaceNow as b, type SequenceCommon as c, type PropValue as d, Sequence as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
@@ -963,4 +963,4 @@ interface SpaceNow {
963
963
  fog: FogState | null;
964
964
  }
965
965
 
966
- export { type SfxPreset as $, type AssetSpec as A, type BlendModeName as B, type CompositionSpec as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type MaskRefSpec as H, type ImageSequenceSpec as I, type MatteRefSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicDrum as N, type MusicDrums as O, type Props as P, type MusicInstrument as Q, type MusicLevel as R, type SequenceSpec as S, type TransitionSpec as T, type MusicTrack as U, type NullSequenceSpec as V, type PathShapeSpec as W, type PolygonShapeSpec as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type Stop as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type StopSpec as a4, type TextSpan as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, Sequence as b, type SpaceNow as c, type SequenceCommon as d, type PropValue as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
966
+ export { type SfxPreset as $, type AssetSpec as A, type BlendModeName as B, type CompositionSpec as C, type DipTransition as D, type EllipseShapeSpec as E, type FilterSpec as F, type GradientSpec as G, type MaskRefSpec as H, type ImageSequenceSpec as I, type MatteRefSpec as J, type Keyframe as K, type LightSequenceSpec as L, type MusicOptions as M, type MusicDrum as N, type MusicDrums as O, type Props as P, type MusicInstrument as Q, type MusicLevel as R, type SequenceSpec as S, type TransitionSpec as T, type MusicTrack as U, type NullSequenceSpec as V, type PathShapeSpec as W, type PolygonShapeSpec as X, type RectShapeSpec as Y, type SfxKnobs as Z, type SfxOptions as _, type Stop as a, type SfxVoice as a0, type ShaderSequenceSpec as a1, type ShapeSequenceSpec as a2, type SlideTransition as a3, type StopSpec as a4, type TextSpan as a5, type TransitionCommon as a6, type VideoSequenceSpec as a7, type WipeTransition as a8, type ZoomTransition as a9, type SpaceNow as b, type SequenceCommon as c, type PropValue as d, Sequence as e, type PathRouters as f, type CompositionShape as g, type CameraSequenceSpec as h, type TextSequenceSpec as i, type ArcShapeSpec as j, type AudioAssetSpec as k, type AudioMusicSpec as l, type AudioSequenceSpec as m, type AudioSfxSpec as n, type CanvasInfo as o, type CanvasSequenceSpec as p, type ChromaKeyFilterSpec as q, type CircleShapeSpec as r, type CompositionSequenceSpec as s, type CrossfadeTransition as t, type CustomFilterSpec as u, type DissolveTransition as v, type Expr as w, type IrisTransition as x, type LineShapeSpec as y, type LumaTransition as z };
package/docs/api.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  - [`Movie`](#movie) — composition runtime: init, playback, render
4
4
  - [`Controller`](#controller) — drop-in player UI overlay
5
- - [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration
5
+ - [`pixi-effects/3d`](#pixi-effects3d) — 3D scenes drawn by PixiJS 3D (the main 3D engine)
6
+ - [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration (the sub 3D engine)
6
7
 
7
8
  For DSL types (composition spec, sequences, filters, keyframes, expressions), see [DSL reference](./dsl.md).
8
9
 
@@ -644,6 +645,45 @@ Active when `keyboard: true` and the key target is not `<input>`/`<textarea>`/`<
644
645
 
645
646
  The bar uses fixed colors (`#007AFF` for the active track / fill / confirm button, white for icons, `rgba(0,0,0,0.75)` gradient background). Override by adding stricter CSS rules under `.movie-controller`. A theming API (CSS custom properties) is on the roadmap.
646
647
 
648
+ ## `pixi-effects/3d`
649
+
650
+ ```ts
651
+ import { register3D, scene3d, layerTexture, loadModel, fitCamera, Scene3DSequence } from 'pixi-effects/3d';
652
+ import { Mesh3D, Model3D, DirectionalLight } from '@pixi/3d';
653
+ ```
654
+
655
+ **Install:** `npm i @pixi/3d` (it needs `pixi.js` 8.23.0 or later). `@pixi/3d` is a peer dependency marked optional — a page that never imports `pixi-effects/3d` does not load it. On a page with an import map: `"pixi-effects/3d"`, `"@pixi/3d": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d.min.mjs"` and `"@pixi/3d/extras": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d-extras.min.mjs"` (the template has them). **Import it before `movie.init`.** The layer is described in [DSL: scene3d](./dsl.md#scene3d).
656
+
657
+ ### `register3D(): void`
658
+
659
+ Registers the `scene3d` layer type (explicit, so a bundler cannot drop it; calling it again is fine) and turns off the self-updating of `Model3D` and `AnimationPlayer` (`defaultOptions.autoUpdate = false`): a video draws from the time. Warns once when PixiJS is older than 8.23.
660
+
661
+ ### `scene3d(spec: Scene3DSequenceSpec): SequenceSpec`
662
+
663
+ Typing helper: takes a typed scene3d spec and returns it as a core `SequenceSpec`.
664
+
665
+ ### `layerTexture(ctx, name, options?): Promise<LayerTexture>`
666
+
667
+ In a scene3d layer's `setup`: the 2D layer `name` (a composition listed before the scene3d layer) shown on a mesh. Returns `{ material, texture, width, height, dispose }`; options `resolution`, `transparent`. See [DSL: scene3d](./dsl.md#a-2d-layer-on-a-mesh-layertexture).
668
+
669
+ ### `loadModel(url, options?): Promise<LoadedModel>`
670
+
671
+ Loads a glTF / GLB as a PixiJS 3D `Model3D`: `{ object, model, clips, size(), fit(n), pose(t) }`; options `animation` (a name, an index, `false`), `speed`, `offset`, `loop`.
672
+
673
+ ### `fitCamera(ctx, object, options?): void`
674
+
675
+ Puts the layer's camera where the whole of `object` fits; options `direction` (default `[0.45, 0.3, 1]`), `margin` (default 1.1).
676
+
677
+ ### `Scene3DSequence`
678
+
679
+ The layer class; `view` is its `View3D`. `checkDeterminism(times)` is what `check` runs; `pickAt(u, v)` and `dragPlane(point, plane)` are what `movie.pick` and `hit.scene3d.grab` call.
680
+
681
+ ### Exported types
682
+
683
+ ```ts
684
+ import type { Scene3DContext, Scene3DSetupResult, Scene3DSequenceSpec, LayerTexture, LayerTextureOptions, LoadedModel, LoadModelOptions, FitCameraOptions } from 'pixi-effects/3d';
685
+ ```
686
+
647
687
  ## `pixi-effects/three`
648
688
 
649
689
  Optional three.js integration, imported from its own entry so the core `pixi-effects` entry never touches three.js:
package/docs/dsl.md CHANGED
@@ -735,9 +735,89 @@ The camera takes `fogNear`, `fogFar` (distances along the view direction, px), `
735
735
 
736
736
  ---
737
737
 
738
+ ## scene3d
739
+
740
+ A real 3D scene (meshes, glTF models, PBR materials, lights, shadows) as a layer, drawn by [PixiJS 3D](https://pixijs.com/3d) (`@pixi/3d`), the main 3D engine of pixi-effects. The scene is drawn **inside PixiJS's own renderer**, in the layer's place in the stack: no second renderer, no canvas per layer, no copy. Everything a layer has works on it: `at` / `duration`, transform keyframes, `alpha` (and a parent's alpha), `blendMode`, [filters](#filters), [masks](#masks), nesting in compositions and in `threeD` cards.
741
+
742
+ `type: 'scene3d'` lives in the optional `pixi-effects/3d` entry (see [API reference](./api.md#pixi-effects3d)). **Import it before `movie.init`**: PixiJS loads the 3D drawing code while its renderer starts, so a renderer that started before `@pixi/3d` was imported cannot draw a scene3d layer (the build says so).
743
+
744
+ ```ts
745
+ import { register3D, scene3d } from 'pixi-effects/3d';
746
+ import { Mesh3D, CubeGeometry, Material3D, DirectionalLight } from '@pixi/3d';
747
+
748
+ register3D(); // once, before Movie.init
749
+
750
+ scene3d({
751
+ type: 'scene3d',
752
+ name: 'hero',
753
+ view: { toneMapping: 'aces', environment: { skyColor: 0xbcd4f0, groundColor: 0x6b6250, intensity: 0.7 } },
754
+ setup: ({ root, camera }) => {
755
+ const cube = new Mesh3D({ geometry: new CubeGeometry(), material: new Material3D({ baseColor: 0xdd4444, roughness: 0.5 }) });
756
+ const sun = new DirectionalLight({ intensity: 3 });
757
+ sun.rotation.set(-0.8, 0.5, 0);
758
+ root.addChild(cube, sun);
759
+ camera.position.set(0, 1.5, 5);
760
+ camera.lookAt({ x: 0, y: 0, z: 0 });
761
+ return { objects: { cube } }; // exposes `cube` to keyframes
762
+ },
763
+ update: (t, { objects }) => { objects.cube.rotation.y = t * 1.2; }, // set everything from t
764
+ keyframes: [{ at: 1, to: { 'scene3d.cube.position.x': 1.5 }, duration: 1 }],
765
+ })
766
+ ```
767
+
768
+ | Field | Type | Notes |
769
+ |---|---|---|
770
+ | `width?`, `height?` | `PropValue` | layer size in composition px; expressions allowed. Default: the parent composition's size. Place it like any layer (`initial: { x, y, anchorX, anchorY }`). |
771
+ | `view?` | `View3DOptions` | options of the PixiJS 3D view: `environment`, `toneMapping` (`'none'` default; `'aces'` for strong lights), `exposure`, `shadowQuality`, `passes` (bloom, SSAO … from `@pixi/3d/extras`), `cull`. The layer owns `width` / `height` / `autoResize`, and draws with `cull: 'cpu'` unless you name one. |
772
+ | `resolution?` | `number` | the view's resolution (a value other than 1 draws the scene at that scale and composites it). |
773
+ | `followCamera?`, `followLights?`, `followFog?` | `boolean` | live in the composition's space (see below). |
774
+ | `reuse?` | `boolean` | a rebuild (`setVars`, `resize`, `rebuild`) keeps the scene of a layer that did not change (same `name`, `setup`, size and options): `setup` runs once. Default `true`; a layer with no `name` is always built again; `false` runs `setup` at every rebuild. |
775
+ | `setup` | `(ctx) => { objects? } \| Promise<…>` | builds the scene once; async, so `Movie.init` waits for a model to load. A setup that throws stops `init` with the layer's name and the reason. |
776
+ | `update?` | `(t, ctx) => void` | per frame, `t` = the layer's own time in seconds. Set everything from `t`. |
777
+ | `dispose?` | `(ctx) => void` | cleanup for what you made (the view and everything under its root are destroyed by the layer). |
778
+
779
+ `ctx` is `{ view, root, camera, renderer, width, height, objects, space? }`: the `View3D`, its `root` (add meshes, models and lights here), its `camera` (`fov` in degrees, `mode: 'perspective' | 'orthographic'`; place it with `position.set` and `lookAt`), PixiJS's renderer, the layer size, and the `objects` setup returned (plus `camera` and `view`). Keyframes reach any of them as **`scene3d.<object>.<path>`** (`'scene3d.cube.rotation.y'`, `'scene3d.view.exposure'`); a path to an object setup did not name warns once with the known names.
780
+
781
+ **Time.** A frame is a function of the time, in playback, a seek and an export:
782
+ - `register3D()` turns off the self-updating of models and animation players (`autoUpdate`); anything else under the root that still updates itself on PixiJS's ticker (a particle system, an `autoUpdate: true` you passed) is turned off after `setup`, with one warning.
783
+ - **A model's clip:** pose it in `update`: `model.player.getState('Run').gotoAndStop(t % clip.duration)`. A clip left playing (a `Model3D` made with `autoPlay`) and posed by nobody is played on the layer's time by the layer (its speed and loop kept).
784
+ - PixiJS 3D's shader time `uTime` reads the layer's time (it is the wall clock in PixiJS 3D itself).
785
+ - `update` that calls `Math.random`, `Date.now`, `performance.now` or `new Date` warns at build; `check` draws the layer at four moments in two orders and reports a layer whose same frame is not the same picture (state kept from call to call, a particle system or a `MotionBlurEffect`, which reads the frame drawn before).
786
+ - Particles (`ParticleSystem3D`): give a `seed`; their births add up from frame to frame, so a seek does not reproduce them: drive a `ParticleContainer3D` from `t` instead, or keep particles in 2D (`particles()`).
787
+
788
+ **Look.** PixiJS 3D ignores `alpha` and `blendMode` on its view; the layer applies them (and a parent composition's alpha) through one filter of its own, added only while the layer is not at full strength or blends (`add`, `screen`, `multiply`; the other modes work as on any layer). `tint` does nothing on a scene3d layer (it warns): colour the materials, or use a filter. The two backends draw nearly the same scene (measured 2026-11-13 on one Apple machine: a mean difference of 0.03–0.04 of 255 between WebGL and WebGPU for a lit cube), but they are separate pipelines in PixiJS 3D and are not required to match.
789
+
790
+ ### Living in the composition's space: `followCamera`, `followLights`, `followFog`
791
+
792
+ With `followCamera: true` the view's camera is put where the composition's `camera` layer is, every frame (position, look-at, vertical fov), so a `cameraPath()` flies through flat `threeD` layers and the 3D scene together. The scene is in **composition pixels with y flipped**: a point the composition calls (x, y, z) is (x, −y, z) in the scene; +z is toward the viewer in both. Give the layer the composition's size (the default; another shape warns). Measured 2026-11-13: planes of the scene land on the composition's own cards within 0.02 of 255 at 1280 × 720.
793
+
794
+ `followLights: true` makes the composition's `light` layers PixiJS 3D lights in the scene (ambient, point, spot, parallel; position, look-at, colour, intensity, cone, falloff), moved every frame; a light that ends is switched off. `followFog: true` makes the camera's fog the view's linear fog. `ctx.space` holds the raw numbers (camera, lights, fog) before `update` runs.
795
+
796
+ - **Brightness differs from the cards.** PixiJS 3D lights in linear light; the composition's cards shade the sRGB values. Positions, directions, cones and falloff match; a 3D surface is brighter in the shade (ambient 0.3: a card is 0.3 × its colour, a 3D surface 0.58 ×). Measured mean against the cards: ambient 7.2–7.7, parallel 2.6–2.8, spot 1.7–2.0, point 1.1–1.2 of 255. Pick intensities by looking.
797
+ - **Falloff.** `none` is constant over the scene; `inverseSquare` is exact beyond the radius (0.71 at the radius where the composition holds 1); `smooth` is fitted (largest difference 0.10–0.12 of full light for ordinary falls, 0.20–0.28 for a fall that is steep against its radius).
798
+ - **Shadows.** A `parallel` or `spot` light with `castsShadows` casts in the scene; meshes cast and receive by default in PixiJS 3D (`castShadow` / `receiveShadow` on a mesh to change it). A point light casts no shadow in PixiJS 3D (it warns). `shadowDarkness` / `shadowDiffusion` are not mirrored (warns once); the shadow box of a parallel light is the whole composition, and a spot light's shadow reaches far beyond it (PixiJS 3D's defaults are for a scene of a few units: in pixels they lost every shadow).
799
+ - **Fog** fades by smoothstep (the composition: linearly) and mixes in linear light: measured mean 1.3–3.5 of 255 against the cards; `fogAmount` below 1 warns (no amount in PixiJS 3D).
800
+ - With no light layer alive, the scene's own lights stay as they are.
801
+
802
+ ### Picking: `movie.pick` into the scene
803
+
804
+ `movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`.
805
+
806
+ ### A 2D layer on a mesh: `layerTexture`
807
+
808
+ `const screen = await layerTexture(ctx, 'screen')` in `setup`, then `screen.material` on a mesh: the 2D layer named `screen` (a `composition` listed **before** the scene3d layer) is shown on it, upright on a `PlaneGeometry` and on a `CubeGeometry`'s faces. The 2D layer stays a layer of the movie (its timeline, keyframes, filters); it is drawn into a PixiJS texture at every frame (measured: identical to the 2D layer drawn flat, mean 0.000 of 255) and is no longer drawn on the stage. Options: `resolution` (2 for a screen seen large), `transparent` (its empty parts let the scene through). A pick on the mesh names the 2D layer under the point (`via` says which mesh). An unlit material: a screen shines by itself.
809
+
810
+ ### glTF models: `loadModel`, `fitCamera`
811
+
812
+ `const model = await loadModel('model.glb', { animation, speed, offset, loop })` in `setup`: `model.object` (a group centred on the model's box: add it to `root`, place, turn and scale it about its centre), `model.clips` (the clip names), `model.size()`, `model.fit(n)` (the longest side n units: n px under `followCamera`), `model.pose(t)` (the clip at `t × speed + offset`, looped or held: call it in `update`). `animation` is a clip name, an index or `false` (the bind pose); default the first clip. `fitCamera(ctx, model.object, { direction, margin })` puts the camera where the whole model fits (a three-quarter view by default). Recipes `scene3d-basics` and `scene3d-in-space`.
813
+
814
+ ### Keeping the scene across rebuilds: `reuse`
815
+
816
+ A rebuild builds the new composition while the old one plays. A named scene3d layer whose `setup`, size and options did not change takes over the old layer's scene at the swap: `setup` (and a model's loading) runs once, and the objects it made are the same objects. The view itself is a new one (a PixiJS display object has one parent, and the old tree draws until the swap); what `setup` did to it (background, environment, exposure, tone mapping, post effects, shadow settings, the camera it switched to) moves with the scene. Use `ctx.view` (or `objects.view`), not a `view` kept from `setup`. A rebuild that fails leaves the old layer drawing. Known limit: the new composition's `initial` values on `scene3d.<object>.<path>` are written to the shared objects while it is built, before the swap, and stay if the rebuild then fails (it matters only when such an initial value comes from a variable). `reuse: false` builds the layer again every time.
817
+
738
818
  ## three
739
819
 
740
- Composites a [three.js](https://threejs.org/) scene as a normal layer. Each frame, the scene is rendered into an offscreen WebGL canvas and uploaded into the sequence's PIXI texture — so once built, the layer is a plain sprite: 2D `initial` / keyframe props, [masks](#masks), and [filters](#filters) all apply to it exactly like any other sequence type.
820
+ The sub 3D engine. Choose it over [`scene3d`](#scene3d) for three.js's own ecosystem (its loaders, materials, shaders and post-processing) or a scene you already have in three.js. Composites a [three.js](https://threejs.org/) scene as a normal layer. Each frame, the scene is rendered into an offscreen WebGL canvas and uploaded into the sequence's PIXI texture — so once built, the layer is a plain sprite: 2D `initial` / keyframe props, [masks](#masks), and [filters](#filters) all apply to it exactly like any other sequence type.
741
821
 
742
822
  `type: 'three'` lives in the optional `pixi-effects/three` entry, not core `pixi-effects` — see [API reference](./api.md#pixi-effectsthree) for install/import details.
743
823
 
package/llms-full.txt CHANGED
@@ -6,7 +6,7 @@ Concatenation of the skill, cheatsheet, pitfalls, recipes and the DSL / API / me
6
6
 
7
7
  <!-- ===== skills/pixi-effects/SKILL.md ===== -->
8
8
 
9
- > Written for pixi-effects 0.48.2. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
9
+ > Written for pixi-effects 0.49.0. Update this skill with `npx skills update`; the newest docs are at https://yjmtmtk.github.io/pixi-effects/
10
10
 
11
11
  # pixi-effects — write a video as data
12
12
 
@@ -64,6 +64,7 @@ The same command checks a file you MADE before it goes somewhere with rules (a s
64
64
  - **Built in:** `animateText()` (per-letter / per-word / per-line entrances and exits in one call: `...animateText('Title', style, { x, y, align: 'center', at: 0.5, duration: 5, in: 'rise', out: 'fade', stagger: { each: 0.05 } })`), `splitText()` / `measureText()` (per-letter / per-word layers, exact positions, by hand), `shape: 'arc'` (progress rings, pie / donut slices: animate `endAngle`), `fillGradient` (linear/radial, alpha stops → vignettes), `{value}` counters, `repeat`/`yoyo`, `orbit()`, seeded randomness (`rand(seed)` / `noise(x, seed)` in expressions, `random(seed)` in JS, never `Math.random()`) `wiggle()` (a seeded shake as keyframes; on a camera put it on `offsetX` / `offsetY` so it rides on the move) `cameraPath()` (a camera flight through `[x, y, z]` points as keyframes; layers it passes: `hideBehindCamera: true`) depth of field (camera `focus` + `aperture`: `{ type: 'camera', initial: { focus: 'title' } }` blurs the other threeD layers by depth; a rack focus is a keyframe on `focus`) `music` (a tune with no file: `{ type: 'audio', music: { bpm, tracks: [{ inst: 'keys', notes: 'Cmaj7:4 Am7:4' }, { inst: 'bass', notes: 'c2:2 g1:2' }], drums: { from: 8, kick: 'x...x...' } } }`: notes and chords as text, counted in beats; instruments `keys pluck pad bass sub lead bell musicbox`; `musicEnvelope(music)` feeds `react()`), `audioEnvelope()` / `bpmEnvelope()` + `react()` (visuals that follow music: `const env = await audioEnvelope('music.mp3', { frameRate })` before `movie.init`, then `keyframes: react(env, { duration, props: { scale: { base: 1, amount: 0.4, band: 'bass', release: 0.2 } } })`; `beats: true` pulses on the beats; `bpmEnvelope(120, …)` when there is no file), `deck()` + `Presenter` from `pixi-effects/present` (a talk as one movie: pages, steps, stops that pause playback, arrow keys / click / swipe to move on: `...deck({ transition: { kind: 'slide', duration: 0.6 }, pages: [{ name, duration, stops, sequences }] })`, `new Presenter(movie, { canvas })` from `pixi-effects/presenter`; G = page overview, P = presenter view with notes and a timer; `pixi-effects render talk.html -o talk.pdf` = a PDF of the pages), `particles()` (bursts, snow, confetti, sparks as seeded layers: `...particles({ count: 90, at: 2, life: [1, 1.8], area: { x: 640, y: 330 }, speed: [120, 420], gravity: 240, fade: { out: 0.6 } })`), `followPath()` (a layer along an SVG path: `keyframes: followPath({ d, duration, orient: true })`), path morphing (`{ shape: 'path', d, morphTo, initial/keyframes: morph 0 → 1 }`) and `stagger()` (delays for a wave of layers: `stagger(letters, { each: 0.05, from: 'center' })`, or `stagger(n, …)` for numbers; GSAP-style `each` / `amount` / `from` / `grid` / `ease`). **Not in the DSL (hand-roll with a JS loop):** other per-frame curves (waves, spirals: sample them into short linear keyframes). (Draw-on strokes `trimEnd`, text that changes over time `set: { text }` / `visibleChars`, shake `wiggle()` and particles are built in.)
65
65
  - **Draw it yourself (`canvas` layer):** when nothing in the library draws what you need (an afterimage, rings, a brush stroke, a pixel sprite, a chart), `{ type: 'canvas', width, height, draw: (ctx, t, info) => { … } }`: your function of the time draws into the layer's own cleared 2D canvas and it is a normal layer. Pure function of `t` and `info.params` (`info.random()` is seeded; no `Math.random` / `Date.now`); a simulation is computed in `setup` and read by `info.frame`. `info.time` is the movie's clock (several layers share it; never add `at` by hand), `info.canvas(w, h)` makes sprites and masks in `setup`, data shared by several canvas layers lives in module scope, and `import { ease, noise, noise2, random, samplePath } from 'pixi-effects'` gives the library's easing, seeded noise and a path walked by length. `check` prints a `canvas` line (deterministic, slowest draw in ms) and does NOT see text drawn in a canvas. `help canvas`, recipes `canvas-trail`, `canvas-life`, `canvas-text-mask`, `canvas-text-on-path`, `canvas-bars`.
66
66
  - **Shader layer and warp:** `{ type: 'shader', fragment: 'void mainImage(out vec4 fragColor, in vec2 fragCoord) { … }', uniforms: { speed: 1, tint: '#ff8040' } }` draws a Shadertoy-style formula (GLSL ES 3.00; `iTime` `iResolution` `iFrame` are declared; keyframes move `'uniforms.speed'`; opaque by default; it cannot read the layers behind it; a compile error comes back in your own line numbers). `filters: [{ type: 'warp', kind: 'wave' | 'haze', strength: 6 }]` bends a layer's picture like water or heat.
67
+ - **A real 3D scene** (meshes, a glTF model, PBR, lights, shadows) is a `scene3d` layer (`pixi-effects/3d`, PixiJS 3D; `register3D()` before `init`; recipe `scene3d-basics`): `setup` builds it with `@pixi/3d`, `update(t)` sets it from the time, keyframes reach `scene3d.<object>.<path>`; masks, filters, `alpha` and `blendMode` work on it; `followCamera` / `followLights` / `followFog` put it in the composition's own camera and lights (recipe `scene3d-in-space`), `loadModel` / `fitCamera` load and frame a glTF, `layerTexture(ctx, 'screen')` shows a 2D layer on a mesh, `movie.pick` names what is clicked. A `three` layer (`pixi-effects/three`) is for three.js's own ecosystem.
67
68
  - **Light, shadow, fog (2.5D):** a `{ type: 'light', kind: 'ambient', initial: { intensity: 0.2 } }` plus a `spot` / `point` / `parallel` light (`castsShadows: true` on the light AND on the `threeD` layers that cast) shades every `threeD` layer by angle and distance; `lit: false` opts a layer out; camera `fogNear` / `fogFar` / `fogColor` fade what is far. What no light reaches is black (add the ambient light); layers are drawn in distance order (no depth buffer). `check --at` prints how lit each layer is.
68
69
  - **Blend modes:** `blendMode` takes 18 CSS names (`add` `screen` `multiply` `overlay` `soft-light` `hard-light` `color-dodge` `color-burn` `darken` `lighten` `difference` `exclusion` `hue` `saturation` `color` `luminosity` `linear-burn`, hyphenated) on any layer: glows and light leaks (`add`, `color-dodge`), colour casts and contrast (`soft-light`, `overlay`), vignettes (`multiply`). It blends with everything below the layer; on a composition each child blends (as one picture: `threeD: true` or a filter); 10+ of the new modes on screen at once warn; it works with every mask.
69
70
  - **One `Movie` per page.** A sound effect needs no `duration` (it lasts as long as its clip). Seeking is safe: counters, colours and growing shapes are right after any jump.
@@ -76,7 +77,7 @@ Centred text must be positioned by `anchorX/anchorY: 0.5`; estimate text width a
76
77
 
77
78
  - For an AI in a chat with no shell, the short version is https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/CHAT.md (it copies https://raw.githubusercontent.com/yjmtmtk/pixi-effects/main/ai/chat-template.html: one self-contained page that loads from a single CDN and shows warnings in a red box).
78
79
  - `reference/cheatsheet.md` — every layer type, prop, default and rule on one page.
79
- - `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, three.js metal.
80
+ - `reference/recipes.md` — tested building blocks: slam type, lower-third, marquee, count-up, bar chart, 2.5D title, camera orbit, slideshow with transitions + music, looping, particles, gradients/vignette, a 3D scene (scene3d), three.js metal.
80
81
  - `reference/pitfalls.md` — the full list of real mistakes, with status.
81
82
  - `reference/direction.md` — how to direct a film (any renderer): brief, style guide, shot list, assets rule, sound on the beat grid, review protocol, formats, gates.
82
83
  - `npx pixi-effects check` — the one-command review (see Workflow step 5).
@@ -210,6 +211,8 @@ Style (`initial` / keyframes): `fillColor fillAlpha strokeColor strokeAlpha stro
210
211
 
211
212
  **camera** (2.5D) — props in `initial`/keyframes: `x y z lookAtX lookAtY lookAtZ fov focus aperture` (defaults: centred, looking at the z = 0 plane, `fov` 40, `z` auto = `(H/2)/tan(fov/2)` ≈ 989 at 720p). **Depth of field:** `focus` = the sharp plane (a `threeD` layer's name = its first z, or a z number), `aperture` = the lens diameter in px (30 default, 0 off); writing either blurs the other `threeD` layers by their distance from the focus (a rack focus is a keyframe on `focus`; recipe `rack-focus`; `inspect` reports `depthBlur`, and `check --at 2.4` prints each layer's blur). A dolly with no typed distance: leave `z` automatic and animate `offsetZ`. Never on the camera object itself. An orbit is `orbit()`; by hand: `x = cx + R·sin θ`, `z = R·cos θ`, `R = (H/2)/tan(fov/2)`, `lookAt` = the centre at `z = 0`.
212
213
 
214
+ **scene3d** (optional entry `pixi-effects/3d`, PixiJS 3D: the main 3D engine) — `{ type: 'scene3d', name, width?, height?, view?: { toneMapping: 'aces', environment, shadowQuality, passes }, setup({ view, root, camera }) { root.addChild(mesh, light); camera.position.set(0, 1.5, 5); camera.lookAt({ x: 0, y: 0, z: 0 }); return { objects: { cube } } }, update?(t, { objects }), dispose? }`; drive with `'scene3d.cube.rotation.y'` keyframes. `import { register3D } from 'pixi-effects/3d'` and call `register3D()` **before** `init`; meshes, models and lights come from `@pixi/3d` (`Mesh3D`, `Model3D` + `Assets.load('model.glb')`, `CubeGeometry`, `PlaneGeometry`, `Material3D`, `DirectionalLight` …). Drawn inside PixiJS: masks, filters, `alpha`, `blendMode` work as on any layer (`tint` does not). A clip: `model.player.getState(name).gotoAndStop(t)` in `update`. **In the composition's space:** `followCamera: true` (the composition's `camera` is the view's: the scene is in composition px with y flipped, (x, y, z) → (x, −y, z)), `followLights: true` (the `light` layers light the scene; parallel/spot `castsShadows` cast, a point light cannot; PixiJS 3D lights in linear light, so 3D is brighter in the shade than a card), `followFog: true`. **glTF:** `import { loadModel, fitCamera } from 'pixi-effects/3d'`: `const m = await loadModel(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`root.addChild(m.object)`; `m.fit(px)`; `m.clips`), `update: (t) => m.pose(t)`, `fitCamera(ctx, m.object)`. **A 2D layer on a mesh:** `const screen = await layerTexture(ctx, 'screen')`, `screen.material` on a mesh (`screen` is a composition listed BEFORE the scene3d layer). `movie.pick` names the object (`hit.scene3d.object`, `hit.scene3d.grab('floor').move(x, y)`). A named layer keeps its scene across `setVars` (`reuse`). Recipes `scene3d-basics`, `scene3d-in-space`.
215
+
213
216
  **three** (optional entry `pixi-effects/three`) — `three({ type:'three', width, height, setup(ctx) { …; return { objects: { knot } } }, update?, dispose? })`; drive with `'three.knot.rotation.y'` keyframes. Call `registerThree()` before `init`. **In the composition's space:** `followCamera: true` (the layer's PerspectiveCamera is the composition's `camera`; the scene is in composition px with y flipped: the composition's `(x, y, z)` is `(x, -y, z)`; give the layer the composition's size), `followLights: true` (the `light` layers become three lights) and `followFog: true` (the camera's fog becomes the scene's `Fog`: close to, not equal to, the card fog); a light's `castsShadows` casts in three (mark your meshes `castShadow` / `receiveShadow`); `ctx.space` has the raw numbers. **glTF:** `import { threeGltf, threeFrameCamera } from 'pixi-effects/three'`: `const m = await threeGltf(url, { animation: 'Run' | 0 | false, speed, offset, loop })` in `setup` (`ctx.scene.add(m.object)`; `m.fit(px)` scales the longest side to px; `m.clips` names the clips), `update: (t) => m.pose(t)` (a function of t: never `mixer.update(dt)`), `threeFrameCamera(ctx.camera, m.object)` frames the whole model from a three-quarter view. Needs `"three/addons/"` in the import map. Recipe `gltf-in-space`. **A 2D layer on a mesh** (a phone screen, a television): `const screen = await threeLayerTexture(ctx, 'screen')` in `setup`, `screen.material` on a mesh; `screen` is a `composition` listed BEFORE the three layer (recipe `three-screen`).
214
217
 
215
218
  ## Camera flight and a camera shake
@@ -466,12 +469,26 @@ A good habit that catches most of these: after `init`, read `window.__logs` (see
466
469
  118. **`check` does not see text drawn in a canvas.** The layout test (cut-off, off-canvas, overlap) reads the text layers; a label or a caption you draw with `fillText` can run off the edge or collide with another without a word, and "no cut-off text" only speaks for the text layers. Look at the sheet and at `--at … --crop`, and keep a margin.
467
470
  119. **`bitrate` is a ceiling, not a target: a quiet or mostly black picture is written far below it, and no setting changes that.** Asking for `'150M'` gave 105 to 151 Mbps on grainy dark pictures and 13 to 18 Mbps on clean ones (a platform that wants at least 35 Mbps for 4K then rejects the file). The encoder spends bits on what changes: a constant bitrate mode reaches the encoder (it was measured, with H.264, HEVC and VP9) and still writes 0.1 Mbps for a black frame with a circle, because no encoder pads. So read the real rate of the file (`render` prints it and says when it is far under what you asked; `npx pixi-effects probe file.mp4` too) and, when a minimum is needed, raise it by what the picture really has: fine film grain (a seeded noise of about ±2/255 in the darks, which also hides banding in dark gradients) costs bits and looks right. Do not stretch it with an invisible pattern.
468
471
 
472
+ ## 3D scenes (`scene3d`, PixiJS 3D)
473
+
474
+ 120. **Import `pixi-effects/3d` before `movie.init` and call `register3D()`.** PixiJS loads the 3D drawing code while its renderer starts; a scene3d layer built on a renderer that started before `@pixi/3d` was imported stops the build with `PixiJS 3D was loaded after the renderer started`. The import map needs `pixi-effects/3d`, `@pixi/3d` and `@pixi/3d/extras`, and `pixi.js` 8.23.0 or later. *(warns)*
475
+ 121. **`update(t)` sets everything from `t`.** `angle += 0.02` per call, a velocity carried from the last call, `Math.random()` or a clock draw a different picture in playback, a seek and an export; `check` draws the layer in two orders and names it. A model's clip: `model.player.getState(name).gotoAndStop(t % clip.animation.duration)` (a clip left playing with `autoPlay` is played on the layer's time by the layer). *(check catches it)*
476
+ 122. **Particle systems (`ParticleSystem3D`) and a `MotionBlurEffect` are not a function of the time.** Particle births add up from frame to frame (a seek does not reproduce them) and motion blur reads the camera of the frame drawn before. Keep particles in 2D (`particles()`) or drive a `ParticleContainer3D` from `t`; leave motion blur out of a scene3d layer. *(check catches it)*
477
+ 123. **An inline shape mask needs a `fillColor`.** A circle or rect with no fill masks out everything: the layer disappears without a word. Give the mask shape `initial: { fillColor: '#fff' }`. (Any layer, not only scene3d.)
478
+ 124. **`tint` does nothing on a scene3d layer** (PixiJS 3D draws its materials' colours): colour the materials, or put a filter on the layer. `alpha` and `blendMode` do work (the layer applies them). *(warns)*
479
+ 125. **PixiJS 3D's units are not pixels (unless the layer has `followCamera`).** The scene has its own units (a glTF is in metres: a fox is about 1.3 units long) and its own camera (`fov` in degrees, default 60, the camera looks down its `-z`); `width` / `height` only size the layer's rectangle. Place the camera with `camera.position.set(…)` and `camera.lookAt({ x, y, z })`; read a model's size with `model.getLocalBounds()` before scaling it.
480
+ 126. **A light is aimed by its rotation, a plane lies in XY.** `DirectionalLight` / `SpotLight` look down their local `-z` (`sun.rotation.set(-0.9, 0.6, 0)`, or `lookAt`); `PlaneGeometry` faces `+z`, so a floor is `rotation: { x: -Math.PI / 2, y: 0, z: 0 }`. A view's `toneMapping` is `'none'` by default: strong lights clip, use `view: { toneMapping: 'aces' }`.
481
+ 127. **Under `followCamera` the scene is in composition PIXELS, y flipped.** A composition point (x, y, z) is `(x, -y, z)` in the scene: put a model at `position.set(640, -400, 0)` for (640, 400), and size it in pixels (`model.fit(480)`), not in metres. Give the layer the composition's size (another shape warns).
482
+ 128. **`followLights` matches where light falls, not how bright the shade is.** PixiJS 3D lights in linear light, the composition's cards shade the sRGB values: a 3D surface under ambient 0.3 is 0.58 × its colour, a card 0.3 ×. A point light casts no shadow in PixiJS 3D (use a spot or a parallel light); `shadowDarkness` / `shadowDiffusion` are not mirrored. *(warns)*
483
+ 129. **A 2D layer for `layerTexture` is a `composition` listed BEFORE the scene3d layer**, in the same composition. It is no longer drawn on the stage (it shows only on the mesh). *(warns with the names it found)*
484
+ 130. **After a rebuild (`setVars`) the scene is the same but the view is a new one.** Its settings (background, environment, exposure, tone mapping, effects, the active camera) move with the scene; read the view from `ctx.view` (or `objects.view`), not from a variable kept in `setup`.
485
+
469
486
 
470
487
  <!-- ===== skills/pixi-effects/reference/recipes.md ===== -->
471
488
 
472
489
  # pixi-effects recipes
473
490
 
474
- Copy, adapt, run. Every block marked `@recipe` is a function body that **returns the composition's `sequences`** (or `{ sequences, transitions, duration }`); the repo's tests build each one and fail on any warning, so they stay correct. Blocks marked `@docs-only` need a browser (canvas / three.js) and are not executed by the tests.
491
+ Copy, adapt, run. Every block marked `@recipe` is a function body that **returns the composition's `sequences`** (or `{ sequences, transitions, duration }`); the repo's tests build each one and fail on any warning, so they stay correct. Blocks marked `@docs-only` need a browser (canvas / 3D / three.js) and are not executed by the tests.
475
492
 
476
493
  Assumed canvas: 1280×720 @ 30 fps. `kenBurns`, `withFade` and `orbit` come from `pixi-effects`.
477
494
 
@@ -1763,6 +1780,82 @@ function placeholderPhoto(name, hueA, hueB, w = 1920, h = 1080) {
1763
1780
 
1764
1781
  ---
1765
1782
 
1783
+ ## A 3D scene with a model, a light and a shadow, under a title (`scene3d`)
1784
+
1785
+ A real 3D scene as one layer, drawn by PixiJS 3D inside PixiJS itself: a floor that receives a shadow, a glTF model whose clip is posed from the time, a cube moved by a keyframe, a sun that casts. The layer takes a mask like any layer: a circle wipe reveals it, and a 2D title sits over it. Everything in `update` is set from `t` (a function of the time: playback, a seek and an export agree). `loadModel` names the clips (`model.clips`), measures the model (`model.size()`), fits it (`model.fit(n)`: the longest side n units) and poses it from the time (`model.pose(t)`); `fitCamera(ctx, model.object)` puts the camera where the whole model fits.
1786
+
1787
+ ```js
1788
+ // @docs-only scene3d-basics
1789
+ // import { register3D, loadModel } from 'pixi-effects/3d'; register3D(); (before movie.init)
1790
+ // import { Mesh3D, PlaneGeometry, CubeGeometry, Material3D, DirectionalLight } from '@pixi/3d';
1791
+ // composition 1280 x 720, 5 s. 'model.glb' sits next to the page.
1792
+ let model;
1793
+ return [
1794
+ { type: 'shape', shape: 'rect', width: 'GW', height: 'GH', initial: { x: 'GW/2', y: 'GH/2', fillColor: '#0d1220' } },
1795
+ { type: 'scene3d', name: 'stage',
1796
+ view: { toneMapping: 'aces', shadowQuality: 'medium', environment: { skyColor: 0xbcd4f0, groundColor: 0x3a3226, intensity: 0.6 } },
1797
+ setup: async ({ root, camera }) => {
1798
+ const floor = new Mesh3D({ geometry: new PlaneGeometry({ width: 40, height: 40 }), material: new Material3D({ baseColor: 0x2a3142, roughness: 0.9 }),
1799
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 }, receiveShadow: true }); // a plane lies in XY: turned flat, it is a floor
1800
+ model = await loadModel('model.glb'); // its first clip, posed from t below
1801
+ model.fit(1.6); // the longest side 1.6 units, whatever the file's own size
1802
+ model.object.position.set(0, 0.8, 0); // its centre: standing on the floor
1803
+ const cube = new Mesh3D({ geometry: new CubeGeometry({ width: 0.8, height: 0.8, depth: 0.8 }), material: new Material3D({ baseColor: 0xff7a1a, roughness: 0.4 }),
1804
+ position: { x: -2.2, y: 0.4, z: 0 }, castShadow: true });
1805
+ const sun = new DirectionalLight({ intensity: 3, castShadow: true });
1806
+ sun.rotation.set(-0.9, 0.6, 0); // lights look down their -z: aim by rotation
1807
+ root.addChild(floor, model.object, cube, sun);
1808
+ camera.position.set(0, 2.2, 6);
1809
+ camera.lookAt({ x: 0, y: 0.6, z: 0 });
1810
+ return { objects: { cube } }; // keyframes reach it as scene3d.cube.…
1811
+ },
1812
+ update: (t, { objects }) => {
1813
+ model.pose(t); // the clip at the layer's time
1814
+ objects.cube.rotation.y = t * 1.5;
1815
+ },
1816
+ keyframes: [{ at: 1, to: { 'scene3d.cube.position.x': 2.2 }, duration: 2.5, ease: 'sine.inOut' }],
1817
+ mask: { type: 'shape', shape: 'circle', radius: 1, initial: { x: 'GW/2', y: 'GH/2', fillColor: '#ffffff' }, // a mask shape needs a fill
1818
+ keyframes: [{ at: 0, to: { radius: 760 }, duration: 1.2, ease: 'power2.inOut' }] } }, // a circle wipe reveals the scene
1819
+ { type: 'text', text: 'PIXIJS 3D', style: { fontSize: 72, fontWeight: '900', fill: '#ffffff', letterSpacing: 8 },
1820
+ initial: { x: 'GW/2', y: 110, anchorX: 0.5, anchorY: 0.5 },
1821
+ keyframes: [{ at: 0.8, from: { alpha: 0, y: '+=20' }, duration: 0.6, ease: 'power2.out' }] },
1822
+ ];
1823
+ ```
1824
+
1825
+ Opacity, blend modes and filters work on the layer as on any layer (`alpha` fades the whole scene). It can live in the composition's own camera and lights (`followCamera`, `followLights`: recipe `scene3d-in-space`), be clicked into (`movie.pick` names the object), and show a 2D layer on a mesh (`layerTexture`).
1826
+
1827
+ ---
1828
+
1829
+ ## A model that runs through the composition's space, past a title (`scene3d`, `followCamera`, `followLights`)
1830
+
1831
+ The scene3d layer lives in the composition's space: its camera IS the composition's `camera` layer and the composition's `light` layers light it, so one camera move carries a flat text card and a 3D model together. The scene is in composition pixels with y flipped: a composition point (x, y, z) is (x, −y, z) in the scene; `fit(px)` sizes a model in metres to pixels. PixiJS 3D lights in linear light, so a 3D surface is brighter in the shade than a flat card under the same light: pick the intensities by looking.
1832
+
1833
+ ```js
1834
+ // @docs-only scene3d-in-space
1835
+ // import { register3D, loadModel } from 'pixi-effects/3d'; register3D(); (before movie.init)
1836
+ // composition 1280 x 720, 4 s. 'Fox.glb' sits next to the page; the model faces +z in the file: rotation.y = PI / 2 runs it toward +x.
1837
+ let fox;
1838
+ return [
1839
+ { type: 'camera', keyframes: [{ at: 0, from: { x: 420, lookAtX: 'GW/2' }, to: { x: 860 }, duration: 4, ease: 'sine.inOut' }] }, // moves the text card AND the model
1840
+ { type: 'light', kind: 'ambient', initial: { intensity: 0.4 } },
1841
+ { type: 'light', kind: 'parallel', initial: { x: 140, y: -260, z: 900, lookAtX: 'GW/2', lookAtY: 'GH/2', lookAtZ: 0, intensity: 0.8 } },
1842
+ { type: 'text', text: 'FOX', threeD: true, style: { fontSize: 420, fontWeight: '900', fill: '#ff7a1a' }, initial: { x: 'GW/2', y: 'GH/2', z: -500, anchorX: 0.5, anchorY: 0.5 } },
1843
+ { type: 'scene3d', name: 'fox', followCamera: true, followLights: true, // covers the composition and sits in the stack like any layer
1844
+ setup: async ({ root }) => {
1845
+ fox = await loadModel('Fox.glb', { animation: 'Run' }); // a clip name, an index, or false (the bind pose); speed, offset, loop
1846
+ fox.fit(480); // the longest side in px: the model is in metres, the scene in pixels
1847
+ fox.object.rotation.y = Math.PI / 2; // faces +x
1848
+ fox.object.position.set(640, -400, 0); // the composition point (640, 400), y flipped
1849
+ root.addChild(fox.object);
1850
+ },
1851
+ update: (t) => fox.pose(t) }, // the clip at the layer's time: a function of t
1852
+ ];
1853
+ ```
1854
+
1855
+ Shadows: give a `parallel` or `spot` light `castsShadows: true` (meshes cast and receive by default in PixiJS 3D; a point light casts none). Fog: `followFog: true`.
1856
+
1857
+ ---
1858
+
1766
1859
  ## Metallic three.js object
1767
1860
 
1768
1861
  `ctx.renderer` is a normal `WebGLRenderer`, so an environment map works; lights alone make `metalness: 1` look black. `three/addons` is not in the importmap: build the environment yourself. Size the layer to the area you want (it clips at its own rectangle) and pull the camera back (`z ≈ 5.4` for a radius-1 torus knot at fov 50).
@@ -3045,9 +3138,89 @@ The camera takes `fogNear`, `fogFar` (distances along the view direction, px), `
3045
3138
 
3046
3139
  ---
3047
3140
 
3141
+ ## scene3d
3142
+
3143
+ A real 3D scene (meshes, glTF models, PBR materials, lights, shadows) as a layer, drawn by [PixiJS 3D](https://pixijs.com/3d) (`@pixi/3d`), the main 3D engine of pixi-effects. The scene is drawn **inside PixiJS's own renderer**, in the layer's place in the stack: no second renderer, no canvas per layer, no copy. Everything a layer has works on it: `at` / `duration`, transform keyframes, `alpha` (and a parent's alpha), `blendMode`, [filters](#filters), [masks](#masks), nesting in compositions and in `threeD` cards.
3144
+
3145
+ `type: 'scene3d'` lives in the optional `pixi-effects/3d` entry (see [API reference](./api.md#pixi-effects3d)). **Import it before `movie.init`**: PixiJS loads the 3D drawing code while its renderer starts, so a renderer that started before `@pixi/3d` was imported cannot draw a scene3d layer (the build says so).
3146
+
3147
+ ```ts
3148
+ import { register3D, scene3d } from 'pixi-effects/3d';
3149
+ import { Mesh3D, CubeGeometry, Material3D, DirectionalLight } from '@pixi/3d';
3150
+
3151
+ register3D(); // once, before Movie.init
3152
+
3153
+ scene3d({
3154
+ type: 'scene3d',
3155
+ name: 'hero',
3156
+ view: { toneMapping: 'aces', environment: { skyColor: 0xbcd4f0, groundColor: 0x6b6250, intensity: 0.7 } },
3157
+ setup: ({ root, camera }) => {
3158
+ const cube = new Mesh3D({ geometry: new CubeGeometry(), material: new Material3D({ baseColor: 0xdd4444, roughness: 0.5 }) });
3159
+ const sun = new DirectionalLight({ intensity: 3 });
3160
+ sun.rotation.set(-0.8, 0.5, 0);
3161
+ root.addChild(cube, sun);
3162
+ camera.position.set(0, 1.5, 5);
3163
+ camera.lookAt({ x: 0, y: 0, z: 0 });
3164
+ return { objects: { cube } }; // exposes `cube` to keyframes
3165
+ },
3166
+ update: (t, { objects }) => { objects.cube.rotation.y = t * 1.2; }, // set everything from t
3167
+ keyframes: [{ at: 1, to: { 'scene3d.cube.position.x': 1.5 }, duration: 1 }],
3168
+ })
3169
+ ```
3170
+
3171
+ | Field | Type | Notes |
3172
+ |---|---|---|
3173
+ | `width?`, `height?` | `PropValue` | layer size in composition px; expressions allowed. Default: the parent composition's size. Place it like any layer (`initial: { x, y, anchorX, anchorY }`). |
3174
+ | `view?` | `View3DOptions` | options of the PixiJS 3D view: `environment`, `toneMapping` (`'none'` default; `'aces'` for strong lights), `exposure`, `shadowQuality`, `passes` (bloom, SSAO … from `@pixi/3d/extras`), `cull`. The layer owns `width` / `height` / `autoResize`, and draws with `cull: 'cpu'` unless you name one. |
3175
+ | `resolution?` | `number` | the view's resolution (a value other than 1 draws the scene at that scale and composites it). |
3176
+ | `followCamera?`, `followLights?`, `followFog?` | `boolean` | live in the composition's space (see below). |
3177
+ | `reuse?` | `boolean` | a rebuild (`setVars`, `resize`, `rebuild`) keeps the scene of a layer that did not change (same `name`, `setup`, size and options): `setup` runs once. Default `true`; a layer with no `name` is always built again; `false` runs `setup` at every rebuild. |
3178
+ | `setup` | `(ctx) => { objects? } \| Promise<…>` | builds the scene once; async, so `Movie.init` waits for a model to load. A setup that throws stops `init` with the layer's name and the reason. |
3179
+ | `update?` | `(t, ctx) => void` | per frame, `t` = the layer's own time in seconds. Set everything from `t`. |
3180
+ | `dispose?` | `(ctx) => void` | cleanup for what you made (the view and everything under its root are destroyed by the layer). |
3181
+
3182
+ `ctx` is `{ view, root, camera, renderer, width, height, objects, space? }`: the `View3D`, its `root` (add meshes, models and lights here), its `camera` (`fov` in degrees, `mode: 'perspective' | 'orthographic'`; place it with `position.set` and `lookAt`), PixiJS's renderer, the layer size, and the `objects` setup returned (plus `camera` and `view`). Keyframes reach any of them as **`scene3d.<object>.<path>`** (`'scene3d.cube.rotation.y'`, `'scene3d.view.exposure'`); a path to an object setup did not name warns once with the known names.
3183
+
3184
+ **Time.** A frame is a function of the time, in playback, a seek and an export:
3185
+ - `register3D()` turns off the self-updating of models and animation players (`autoUpdate`); anything else under the root that still updates itself on PixiJS's ticker (a particle system, an `autoUpdate: true` you passed) is turned off after `setup`, with one warning.
3186
+ - **A model's clip:** pose it in `update`: `model.player.getState('Run').gotoAndStop(t % clip.duration)`. A clip left playing (a `Model3D` made with `autoPlay`) and posed by nobody is played on the layer's time by the layer (its speed and loop kept).
3187
+ - PixiJS 3D's shader time `uTime` reads the layer's time (it is the wall clock in PixiJS 3D itself).
3188
+ - `update` that calls `Math.random`, `Date.now`, `performance.now` or `new Date` warns at build; `check` draws the layer at four moments in two orders and reports a layer whose same frame is not the same picture (state kept from call to call, a particle system or a `MotionBlurEffect`, which reads the frame drawn before).
3189
+ - Particles (`ParticleSystem3D`): give a `seed`; their births add up from frame to frame, so a seek does not reproduce them: drive a `ParticleContainer3D` from `t` instead, or keep particles in 2D (`particles()`).
3190
+
3191
+ **Look.** PixiJS 3D ignores `alpha` and `blendMode` on its view; the layer applies them (and a parent composition's alpha) through one filter of its own, added only while the layer is not at full strength or blends (`add`, `screen`, `multiply`; the other modes work as on any layer). `tint` does nothing on a scene3d layer (it warns): colour the materials, or use a filter. The two backends draw nearly the same scene (measured 2026-11-13 on one Apple machine: a mean difference of 0.03–0.04 of 255 between WebGL and WebGPU for a lit cube), but they are separate pipelines in PixiJS 3D and are not required to match.
3192
+
3193
+ ### Living in the composition's space: `followCamera`, `followLights`, `followFog`
3194
+
3195
+ With `followCamera: true` the view's camera is put where the composition's `camera` layer is, every frame (position, look-at, vertical fov), so a `cameraPath()` flies through flat `threeD` layers and the 3D scene together. The scene is in **composition pixels with y flipped**: a point the composition calls (x, y, z) is (x, −y, z) in the scene; +z is toward the viewer in both. Give the layer the composition's size (the default; another shape warns). Measured 2026-11-13: planes of the scene land on the composition's own cards within 0.02 of 255 at 1280 × 720.
3196
+
3197
+ `followLights: true` makes the composition's `light` layers PixiJS 3D lights in the scene (ambient, point, spot, parallel; position, look-at, colour, intensity, cone, falloff), moved every frame; a light that ends is switched off. `followFog: true` makes the camera's fog the view's linear fog. `ctx.space` holds the raw numbers (camera, lights, fog) before `update` runs.
3198
+
3199
+ - **Brightness differs from the cards.** PixiJS 3D lights in linear light; the composition's cards shade the sRGB values. Positions, directions, cones and falloff match; a 3D surface is brighter in the shade (ambient 0.3: a card is 0.3 × its colour, a 3D surface 0.58 ×). Measured mean against the cards: ambient 7.2–7.7, parallel 2.6–2.8, spot 1.7–2.0, point 1.1–1.2 of 255. Pick intensities by looking.
3200
+ - **Falloff.** `none` is constant over the scene; `inverseSquare` is exact beyond the radius (0.71 at the radius where the composition holds 1); `smooth` is fitted (largest difference 0.10–0.12 of full light for ordinary falls, 0.20–0.28 for a fall that is steep against its radius).
3201
+ - **Shadows.** A `parallel` or `spot` light with `castsShadows` casts in the scene; meshes cast and receive by default in PixiJS 3D (`castShadow` / `receiveShadow` on a mesh to change it). A point light casts no shadow in PixiJS 3D (it warns). `shadowDarkness` / `shadowDiffusion` are not mirrored (warns once); the shadow box of a parallel light is the whole composition, and a spot light's shadow reaches far beyond it (PixiJS 3D's defaults are for a scene of a few units: in pixels they lost every shadow).
3202
+ - **Fog** fades by smoothstep (the composition: linearly) and mixes in linear light: measured mean 1.3–3.5 of 255 against the cards; `fogAmount` below 1 warns (no amount in PixiJS 3D).
3203
+ - With no light layer alive, the scene's own lights stay as they are.
3204
+
3205
+ ### Picking: `movie.pick` into the scene
3206
+
3207
+ `movie.pick(x, y)` on a scene3d layer returns `kind: 'scene3d'` and `scene3d: { object, point, distance, node, grab }`: what the ray through that canvas point meets first, as the frame on screen shows it. `object` is the node's `label` (or its nearest labelled ancestor's: label a group to name a model); `point` is in the scene's units. `hit.scene3d.grab('floor' | 'wall' | 'screen' | [nx, ny, nz]).move(x, y)` gives where the pointer's ray meets a plane through the hit point and the `delta` to move the object by (recipes `pick-click`, `drag-drop`, `drag-3d` work the same way). The layer makes its meshes pickable per triangle (`eventMode: 'static'`, `hitVolume: 'geometry'`) unless you set your own. Skinned and morphed meshes are picked in their rest pose. `check --at T --pick X,Y` prints `world (scene3d: red)`.
3208
+
3209
+ ### A 2D layer on a mesh: `layerTexture`
3210
+
3211
+ `const screen = await layerTexture(ctx, 'screen')` in `setup`, then `screen.material` on a mesh: the 2D layer named `screen` (a `composition` listed **before** the scene3d layer) is shown on it, upright on a `PlaneGeometry` and on a `CubeGeometry`'s faces. The 2D layer stays a layer of the movie (its timeline, keyframes, filters); it is drawn into a PixiJS texture at every frame (measured: identical to the 2D layer drawn flat, mean 0.000 of 255) and is no longer drawn on the stage. Options: `resolution` (2 for a screen seen large), `transparent` (its empty parts let the scene through). A pick on the mesh names the 2D layer under the point (`via` says which mesh). An unlit material: a screen shines by itself.
3212
+
3213
+ ### glTF models: `loadModel`, `fitCamera`
3214
+
3215
+ `const model = await loadModel('model.glb', { animation, speed, offset, loop })` in `setup`: `model.object` (a group centred on the model's box: add it to `root`, place, turn and scale it about its centre), `model.clips` (the clip names), `model.size()`, `model.fit(n)` (the longest side n units: n px under `followCamera`), `model.pose(t)` (the clip at `t × speed + offset`, looped or held: call it in `update`). `animation` is a clip name, an index or `false` (the bind pose); default the first clip. `fitCamera(ctx, model.object, { direction, margin })` puts the camera where the whole model fits (a three-quarter view by default). Recipes `scene3d-basics` and `scene3d-in-space`.
3216
+
3217
+ ### Keeping the scene across rebuilds: `reuse`
3218
+
3219
+ A rebuild builds the new composition while the old one plays. A named scene3d layer whose `setup`, size and options did not change takes over the old layer's scene at the swap: `setup` (and a model's loading) runs once, and the objects it made are the same objects. The view itself is a new one (a PixiJS display object has one parent, and the old tree draws until the swap); what `setup` did to it (background, environment, exposure, tone mapping, post effects, shadow settings, the camera it switched to) moves with the scene. Use `ctx.view` (or `objects.view`), not a `view` kept from `setup`. A rebuild that fails leaves the old layer drawing. Known limit: the new composition's `initial` values on `scene3d.<object>.<path>` are written to the shared objects while it is built, before the swap, and stay if the rebuild then fails (it matters only when such an initial value comes from a variable). `reuse: false` builds the layer again every time.
3220
+
3048
3221
  ## three
3049
3222
 
3050
- Composites a [three.js](https://threejs.org/) scene as a normal layer. Each frame, the scene is rendered into an offscreen WebGL canvas and uploaded into the sequence's PIXI texture — so once built, the layer is a plain sprite: 2D `initial` / keyframe props, [masks](#masks), and [filters](#filters) all apply to it exactly like any other sequence type.
3223
+ The sub 3D engine. Choose it over [`scene3d`](#scene3d) for three.js's own ecosystem (its loaders, materials, shaders and post-processing) or a scene you already have in three.js. Composites a [three.js](https://threejs.org/) scene as a normal layer. Each frame, the scene is rendered into an offscreen WebGL canvas and uploaded into the sequence's PIXI texture — so once built, the layer is a plain sprite: 2D `initial` / keyframe props, [masks](#masks), and [filters](#filters) all apply to it exactly like any other sequence type.
3051
3224
 
3052
3225
  `type: 'three'` lives in the optional `pixi-effects/three` entry, not core `pixi-effects` — see [API reference](./api.md#pixi-effectsthree) for install/import details.
3053
3226
 
@@ -4176,7 +4349,8 @@ The added keyframes are layered on top of any keyframes the spec already has. `w
4176
4349
 
4177
4350
  - [`Movie`](#movie) — composition runtime: init, playback, render
4178
4351
  - [`Controller`](#controller) — drop-in player UI overlay
4179
- - [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration
4352
+ - [`pixi-effects/3d`](#pixi-effects3d) — 3D scenes drawn by PixiJS 3D (the main 3D engine)
4353
+ - [`pixi-effects/three`](#pixi-effectsthree) — optional three.js integration (the sub 3D engine)
4180
4354
 
4181
4355
  For DSL types (composition spec, sequences, filters, keyframes, expressions), see [DSL reference](./dsl.md).
4182
4356
 
@@ -4818,6 +4992,45 @@ Active when `keyboard: true` and the key target is not `<input>`/`<textarea>`/`<
4818
4992
 
4819
4993
  The bar uses fixed colors (`#007AFF` for the active track / fill / confirm button, white for icons, `rgba(0,0,0,0.75)` gradient background). Override by adding stricter CSS rules under `.movie-controller`. A theming API (CSS custom properties) is on the roadmap.
4820
4994
 
4995
+ ## `pixi-effects/3d`
4996
+
4997
+ ```ts
4998
+ import { register3D, scene3d, layerTexture, loadModel, fitCamera, Scene3DSequence } from 'pixi-effects/3d';
4999
+ import { Mesh3D, Model3D, DirectionalLight } from '@pixi/3d';
5000
+ ```
5001
+
5002
+ **Install:** `npm i @pixi/3d` (it needs `pixi.js` 8.23.0 or later). `@pixi/3d` is a peer dependency marked optional — a page that never imports `pixi-effects/3d` does not load it. On a page with an import map: `"pixi-effects/3d"`, `"@pixi/3d": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d.min.mjs"` and `"@pixi/3d/extras": "https://cdn.jsdelivr.net/npm/@pixi/3d@0.1.0/dist/pixijs-3d-extras.min.mjs"` (the template has them). **Import it before `movie.init`.** The layer is described in [DSL: scene3d](./dsl.md#scene3d).
5003
+
5004
+ ### `register3D(): void`
5005
+
5006
+ Registers the `scene3d` layer type (explicit, so a bundler cannot drop it; calling it again is fine) and turns off the self-updating of `Model3D` and `AnimationPlayer` (`defaultOptions.autoUpdate = false`): a video draws from the time. Warns once when PixiJS is older than 8.23.
5007
+
5008
+ ### `scene3d(spec: Scene3DSequenceSpec): SequenceSpec`
5009
+
5010
+ Typing helper: takes a typed scene3d spec and returns it as a core `SequenceSpec`.
5011
+
5012
+ ### `layerTexture(ctx, name, options?): Promise<LayerTexture>`
5013
+
5014
+ In a scene3d layer's `setup`: the 2D layer `name` (a composition listed before the scene3d layer) shown on a mesh. Returns `{ material, texture, width, height, dispose }`; options `resolution`, `transparent`. See [DSL: scene3d](./dsl.md#a-2d-layer-on-a-mesh-layertexture).
5015
+
5016
+ ### `loadModel(url, options?): Promise<LoadedModel>`
5017
+
5018
+ Loads a glTF / GLB as a PixiJS 3D `Model3D`: `{ object, model, clips, size(), fit(n), pose(t) }`; options `animation` (a name, an index, `false`), `speed`, `offset`, `loop`.
5019
+
5020
+ ### `fitCamera(ctx, object, options?): void`
5021
+
5022
+ Puts the layer's camera where the whole of `object` fits; options `direction` (default `[0.45, 0.3, 1]`), `margin` (default 1.1).
5023
+
5024
+ ### `Scene3DSequence`
5025
+
5026
+ The layer class; `view` is its `View3D`. `checkDeterminism(times)` is what `check` runs; `pickAt(u, v)` and `dragPlane(point, plane)` are what `movie.pick` and `hit.scene3d.grab` call.
5027
+
5028
+ ### Exported types
5029
+
5030
+ ```ts
5031
+ import type { Scene3DContext, Scene3DSetupResult, Scene3DSequenceSpec, LayerTexture, LayerTextureOptions, LoadedModel, LoadModelOptions, FitCameraOptions } from 'pixi-effects/3d';
5032
+ ```
5033
+
4821
5034
  ## `pixi-effects/three`
4822
5035
 
4823
5036
  Optional three.js integration, imported from its own entry so the core `pixi-effects` entry never touches three.js:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixi-effects",
3
- "version": "0.48.2",
3
+ "version": "0.49.0",
4
4
  "description": "Declarative composition and video rendering for the web. Built on PixiJS v8.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,6 +35,16 @@
35
35
  "default": "./dist/present.cjs"
36
36
  }
37
37
  },
38
+ "./3d": {
39
+ "import": {
40
+ "types": "./dist/3d.d.ts",
41
+ "default": "./dist/3d.js"
42
+ },
43
+ "require": {
44
+ "types": "./dist/3d.d.cts",
45
+ "default": "./dist/3d.cjs"
46
+ }
47
+ },
38
48
  "./three": {
39
49
  "import": {
40
50
  "types": "./dist/three.d.ts",
@@ -78,6 +88,7 @@
78
88
  "webgl",
79
89
  "keyframe",
80
90
  "typescript",
91
+ "pixijs-3d",
81
92
  "three",
82
93
  "threejs"
83
94
  ],
@@ -121,24 +132,29 @@
121
132
  "mediabunny": "^1.61.3"
122
133
  },
123
134
  "peerDependencies": {
135
+ "@pixi/3d": "^0.1.0",
124
136
  "pixi-filters": "^6.1.0",
125
137
  "pixi.js": "^8.0.0",
126
138
  "three": ">=0.150.0"
127
139
  },
128
140
  "peerDependenciesMeta": {
129
- "three": {
141
+ "@pixi/3d": {
130
142
  "optional": true
131
143
  },
132
144
  "pixi-filters": {
133
145
  "optional": true
146
+ },
147
+ "three": {
148
+ "optional": true
134
149
  }
135
150
  },
136
151
  "devDependencies": {
152
+ "@pixi/3d": "^0.1.0",
137
153
  "@types/three": "^0.185.0",
138
154
  "gsap": "3.15.0",
139
155
  "happy-dom": "^15.0.0",
140
156
  "pixi-filters": "^6.1.5",
141
- "pixi.js": "^8.22.0",
157
+ "pixi.js": "^8.23.0",
142
158
  "three": "^0.185.1",
143
159
  "tsup": "^8.0.0",
144
160
  "typescript": "^5.5.0",