@adatechnology/game-interactables-three 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +172 -2
- package/dist/index.d.ts +395 -0
- package/dist/index.global.js +1713 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1663 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Fernandes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,173 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @adatechnology/game-interactables-three
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
three.js adapter for [`@adatechnology/game-interactables`](../game-interactables) and for content packs that draw through a
|
|
4
|
+
`HouseholdApi` (the `PropsApi` of [`@adatechnology/game-props3d`](../game-props3d) plus `part(id, pivot, ry, draw)`).
|
|
5
|
+
|
|
6
|
+
It turns the primitives of a content pack into a few merged meshes, moves the parts the world animates, and draws the effects, screens,
|
|
7
|
+
mirrors and cords of the world. `three` is a peer dependency imported **as a type only**: the host passes its own `THREE` (and `Reflector`)
|
|
8
|
+
in, so there is never a second copy of three in the bundle. No assets, no other runtime dependencies.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add @adatechnology/game-interactables-three three
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Browser without a bundler (the global is `GameInteractablesThree`):
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<script src="https://unpkg.com/@adatechnology/game-interactables-three/dist/index.global.js"></script>
|
|
20
|
+
<script>
|
|
21
|
+
const adapter = GameInteractablesThree.createThreeAdapter(THREE, { document })
|
|
22
|
+
</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import * as THREE from 'three'
|
|
29
|
+
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
|
|
30
|
+
import { createThreeAdapter } from '@adatechnology/game-interactables-three'
|
|
31
|
+
|
|
32
|
+
const gfx = createThreeAdapter(THREE, { document, Reflector, envMap })
|
|
33
|
+
|
|
34
|
+
// 1. build an object: every primitive is merged into one geometry per material, every part into its own group
|
|
35
|
+
const { api, finish } = gfx.createMeshApi({ quality: 'game' })
|
|
36
|
+
pack.build('wardrobe', {}, api) // your content pack draws through api.box / cylinder / leaf / plane / light / part
|
|
37
|
+
const { group, parts } = finish() // parts: { door: Group, drawer_1: Group, ... }
|
|
38
|
+
scene.add(group)
|
|
39
|
+
|
|
40
|
+
// 2. each frame
|
|
41
|
+
world.update(dt)
|
|
42
|
+
gfx.syncObject(world, 'wardrobe-1', parts, { time: clock.elapsedTime })
|
|
43
|
+
effects.update(dt, world.emitters())
|
|
44
|
+
mirrors.update(camera, world.mirrors([x, y, z], 6))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Every function also exists unbound and takes `THREE` first: `createMeshApi(THREE, options)`, `applyPose(THREE, group, motion, t)`, ...
|
|
48
|
+
|
|
49
|
+
### Mesh api
|
|
50
|
+
|
|
51
|
+
`createMeshApi(THREE, { materials, quality, shadows })` returns `{ api, finish, materials }`.
|
|
52
|
+
|
|
53
|
+
- Implements `box`, `cylinder` (shapes `cyl`, `cyl6`, `cone`, `sphere`), `leaf`, `plane` (with atlas `uv`), `light` and `part`.
|
|
54
|
+
- Static primitives are baked (vertex colors) into **one `BufferGeometry` per material**: the draw calls of the static body are the number
|
|
55
|
+
of materials in use (`plain`, `metal`, `wood`, `glass`, `fabric`, `plant`, `water`, `emissive`, `atlas_picture`, `atlas_floor`).
|
|
56
|
+
- `part(id, pivot, ry, draw)` routes everything drawn inside `draw` into a group of its own, merged per material as well. The group origin is
|
|
57
|
+
the pivot and it is turned by `ry`, so a hinge or slide axis is expressed in the part frame. Names are `part:<id>` and `userData.part = id`
|
|
58
|
+
on the group **and** its meshes, so `raycaster.intersectObject(group, true)[0].object.userData.part` gives the part that was hit.
|
|
59
|
+
- `M.x`, `M.z`, `M.ry` of each primitive's item are applied; pivots are object-local, coordinates drawn inside a part are relative to its pivot.
|
|
60
|
+
- `materials` maps keys to your materials; missing keys get defaults with `vertexColors` (`MeshStandardMaterial` for `quality: 'high'`,
|
|
61
|
+
`MeshLambertMaterial` for `'game'`, which also uses fewer segments). Atlas materials are yours to provide (a `CanvasTexture` of the atlas).
|
|
62
|
+
- `api.light` creates a `PointLight` inside the right group; `finish()` also returns them as `lights`.
|
|
63
|
+
- **Draw calls in `game` quality.** `plain`, `metal`, `wood` and `fabric` are one shared Lambert material, so they merge into one mesh per
|
|
64
|
+
target. `finish(spec)` (pass the spec the pack returned; option `mergeParts`, default on in `game` quality) also merges the parts of one
|
|
65
|
+
`PartSpec.group` that share a motion (blind slats: a per-vertex pivot in the vertex shader, one set of uniforms, shadows follow) and the
|
|
66
|
+
`cloth` soft volumes (clothes on a line: one mesh, a per-vertex cloth index and anchor, each cloth keeps its own swing). `parts[id]` then
|
|
67
|
+
points at the merged group (`userData.merged`) and `syncObject` drives it. Parts with a mirror, a turned part (`ry`) or a lone part stay as they are.
|
|
68
|
+
Measured on the household pack: blinds 38 meshes to 3, clothesline 31 to 2.
|
|
69
|
+
- **Material hook.** `materialFor(key, partId)` (option of `createMeshApi`, or `finish(spec, { materialFor })` which wins) returns the material
|
|
70
|
+
of each mesh (`partId` is `null` for the static body), e.g. one carrying a per-room light mask. Merged groups clone that material for their
|
|
71
|
+
batch shader but **keep** its `onBeforeCompile`, `customProgramCacheKey`, `defines` and `userData` (the batch patch runs after yours), so a
|
|
72
|
+
merged blind or clothesline is masked and lit like everything else; meshes with different materials are never mixed into one merged mesh.
|
|
73
|
+
- `createScreen` asks for a `willReadFrequently` 2D context (the canvas is re-uploaded every redraw; this avoids the browser's readback warning).
|
|
74
|
+
|
|
75
|
+
### Poses and sync
|
|
76
|
+
|
|
77
|
+
`applyPose(THREE, group, motion, t)` poses a part from its `Motion` (hinge / rocker / knob rotate, slide translates, scale scales along
|
|
78
|
+
the axis). The rest pose is remembered in `group.userData.rest`, so calling it every frame never drifts. `syncObject(THREE, world, objectId,
|
|
79
|
+
parts, { time })` applies every part's t from the world and the swing of every soft volume (the group under `soft.part ?? soft.id`).
|
|
80
|
+
|
|
81
|
+
### Cloth sway that casts the right shadow
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const handle = gfx.attachSway(clothMesh, { amplitude: 0.04 })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`swayMaterial(THREE, base, { amplitude, anchorY })` clones the material and patches the vertex shader (uniforms `time`, `ax`, `az`,
|
|
88
|
+
`anchorY`) so vertices hang from the anchor line and swing. The **same patch** goes into `customDepthMaterial` (directional and spot
|
|
89
|
+
shadows) and `customDistanceMaterial` (point-light shadows), so shadows follow the animation instead of flickering. `attachSway` sets all
|
|
90
|
+
three on a mesh; `syncObject` drives its uniforms from `world.sway(...)`.
|
|
91
|
+
|
|
92
|
+
### Effects
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// `scene` (where the particle objects go) and `camera` (its fov sizes the point sprites) are required options.
|
|
96
|
+
// `camera` may also be a function returning the current camera, or `null` for a 90 degree lens.
|
|
97
|
+
const effects = gfx.createEffects({ scene, camera, maxParticles: 800, document })
|
|
98
|
+
effects.update(dt, world.emitters()) // reads the camera it was created with; `effects.setCamera(other)` swaps it
|
|
99
|
+
effects.setViewHeight(renderer.domElement.height) // on resize
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Smoke and steam (thick, soft, rising and growing), fire (additive, yellow to red), sparks and water. One `Points` per kind, fixed buffers,
|
|
103
|
+
swap-remove, no allocations per frame; the total alive never exceeds `maxParticles` (split by kind). The sprite is painted on a canvas from
|
|
104
|
+
`document`, or generated as a data texture without one. Seeded, so the same seed gives the same plumes. Sprite sizes use the camera's
|
|
105
|
+
vertical `fov` (and `zoom`), read on every `update` (or `setCamera(camera)`); without a camera a 90 degree lens is assumed.
|
|
106
|
+
|
|
107
|
+
- **Water** (intensity 0.25 and up) is a tapered, translucent stream mesh with scrolling streaks along the gravity arc from the outlet down
|
|
108
|
+
to the floor (`floorY` option) or to `emitter.reach` meters below it (a tap ends in the basin), plus spray droplets around it and a splash
|
|
109
|
+
with a little mist where it lands. A thin emitter (tap) narrows as it falls, a wide one (shower) opens up. One draw call per stream
|
|
110
|
+
(`maxStreams`, default 6). Low intensities drip.
|
|
111
|
+
- **Gas `flame`** is a ring of small blue cones per burner (all rings share one mesh, one draw call, `maxFlames`) plus a soft glow.
|
|
112
|
+
- The meshes live in `effects.meshes` (`group`, `streams`, `flames`).
|
|
113
|
+
|
|
114
|
+
### Screens
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const tv = gfx.createScreen({ w: 128, h: 72 })
|
|
118
|
+
tvMesh.material = tv.material
|
|
119
|
+
tv.setOn(true); tv.setChannel(2); tv.update(dt)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Canvas texture: black and not emissive when off; when on, channel 0 is color bars, 1 static noise, 2 an animated landscape (higher
|
|
123
|
+
numbers repeat them with shifted hues).
|
|
124
|
+
|
|
125
|
+
### Mirrors
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const mirrors = gfx.createMirrors({ max: 1, resolution: 512 })
|
|
129
|
+
mirrors.register('wardrobe-1:mirror', mirrorMesh) // world id objectId:mirrorId
|
|
130
|
+
mirrors.update(camera, world.mirrors(eye, 6)) // returns the ids that got a real reflector
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Registered meshes show an env-map material. The nearest `max` facing the camera get a pooled `Reflector`, placed 1 mm in front of the plane
|
|
134
|
+
with the mesh's rotation and size. Without a `Reflector` everything stays on the env map.
|
|
135
|
+
|
|
136
|
+
**`renderer.info` and the Reflector.** three's `Reflector` renders the scene again inside the main `render()`; that nested render resets
|
|
137
|
+
`renderer.info` (when `info.autoReset` is on), so `info.render.calls` would only count the reflector pass. `createMirrors({ keepInfo })`
|
|
138
|
+
(default `true`) wraps the reflector so the counters of the main pass are added back and `info.render.calls` / `triangles` cover the whole
|
|
139
|
+
frame. A host that sets `renderer.info.autoReset = false` and resets it itself is left alone.
|
|
140
|
+
|
|
141
|
+
### Cords
|
|
142
|
+
|
|
143
|
+
`createCord(THREE, points, radius, material)` builds a tube; `catenary(a, b, sag, n)` returns `n` points of a hanging cord whose lowest
|
|
144
|
+
point is `sag` meters below the chord.
|
|
145
|
+
|
|
146
|
+
## Notes
|
|
147
|
+
|
|
148
|
+
- `poseOf` converts the pose math of the interactables package (same defaults) to a quaternion, so the rendered part always matches the engine.
|
|
149
|
+
- Everything is deterministic: effects and screen noise use seeded generators.
|
|
150
|
+
|
|
151
|
+
## Destruction
|
|
152
|
+
|
|
153
|
+
`finish(spec)` copies the spec's destruction tags to Three objects so a destruction host can find what each mesh is made of: `userData.destructionMaterial` (SDK material id) on the static meshes (object default) and on each part group and its meshes, and, on part groups and their meshes, `userData.destructionPart` (the part id, as `@adatechnology/game-destruction-three` expects), `userData.destructionPiece` and `userData.destructionJoint`. Parts merged into a shared group (blind slats) carry the tags of the first part. The damage itself is done by `@adatechnology/game-destruction`; this package only tags. Without a spec nothing is tagged.
|
|
154
|
+
|
|
155
|
+
## Português
|
|
156
|
+
|
|
157
|
+
Adaptador three.js para o `@adatechnology/game-interactables`. Recebe as primitivas dos packs de conteúdo (`box`, `cylinder`, `leaf`,
|
|
158
|
+
`plane`, `light` e `part`) e as junta em **uma geometria por material**, com cada parte móvel (porta, gaveta, tampa) em um grupo próprio com
|
|
159
|
+
a origem no pivô. O `THREE` é sempre passado pelo jogo (o pacote só importa tipos).
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const gfx = createThreeAdapter(THREE, { document, Reflector, envMap })
|
|
163
|
+
const { api, finish } = gfx.createMeshApi({ quality: 'game' })
|
|
164
|
+
pack.build('guarda_roupa', {}, api)
|
|
165
|
+
const { group, parts } = finish()
|
|
166
|
+
scene.add(group)
|
|
167
|
+
// a cada quadro
|
|
168
|
+
gfx.syncObject(world, 'guarda-roupa-1', parts, { time: clock.elapsedTime })
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Também traz `swayMaterial` / `attachSway` (balanço de roupa no vertex shader, com material de profundidade e de distância iguais, para a
|
|
172
|
+
sombra acompanhar), efeitos em pool (fumaça, fogo, vapor, água, faíscas), telas com canais animados, espelhos com `Reflector` só para o mais
|
|
173
|
+
próximo, e cabos (`createCord`, `catenary`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { ActiveEmitter, ObjectSpec, Motion, World } from '@adatechnology/game-interactables';
|
|
2
|
+
import * as ThreeNS from 'three';
|
|
3
|
+
import { PropsApi } from '@adatechnology/game-props3d';
|
|
4
|
+
|
|
5
|
+
/** The host's `THREE` namespace (or any object with the same classes). Always passed in, never imported at runtime. */
|
|
6
|
+
type ThreeLib = typeof ThreeNS;
|
|
7
|
+
|
|
8
|
+
type Vec3 = [number, number, number];
|
|
9
|
+
/** Material keys the mesh api understands (same as `PropsApi` materials). */
|
|
10
|
+
type MaterialKey = 'plain' | 'metal' | 'wood' | 'glass' | 'fabric' | 'plant' | 'water' | 'emissive' | 'atlas_picture' | 'atlas_floor';
|
|
11
|
+
declare const MATERIAL_KEYS: readonly MaterialKey[];
|
|
12
|
+
type Quality = 'high' | 'game';
|
|
13
|
+
/**
|
|
14
|
+
* What content packs draw through: the props primitives plus `part`.
|
|
15
|
+
* `part(id, pivot, ry, draw)` runs `draw()` and routes every primitive drawn inside it into the movable part `id`.
|
|
16
|
+
* `pivot` is object-local; the primitives drawn inside `draw` are in the part frame (relative to the pivot, turned by `ry`): the part's
|
|
17
|
+
* group origin sits at `pivot`, turned by `ry` around +y.
|
|
18
|
+
*/
|
|
19
|
+
interface HouseholdApi extends PropsApi {
|
|
20
|
+
part(id: string, pivot: Vec3, ry: number, draw: () => void): void;
|
|
21
|
+
}
|
|
22
|
+
/** Anything that can create a canvas (the global `document`, or a fake in tests). */
|
|
23
|
+
interface CanvasFactory {
|
|
24
|
+
createElement(tag: 'canvas'): any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface EffectsOptions {
|
|
28
|
+
/** Where the particle objects are added (the `Points` of every kind, the water streams and the flame ring). Required. */
|
|
29
|
+
scene: ThreeNS.Object3D;
|
|
30
|
+
/**
|
|
31
|
+
* Camera whose vertical field of view sizes the sprites, read again every `update` so zoom and fov changes follow. Required: pass the
|
|
32
|
+
* camera the scene is rendered with (a `PerspectiveCamera` fits), or a function returning the current one (`null` = 90 degree lens).
|
|
33
|
+
*/
|
|
34
|
+
camera: PerspectiveLike | (() => PerspectiveLike | null) | null;
|
|
35
|
+
/** Total particles alive at once, shared by all kinds (split by weight). Default 800. */
|
|
36
|
+
maxParticles?: number;
|
|
37
|
+
/** Used to paint the soft sprite on a canvas. Without it (or without a 2D context) a data texture is generated. */
|
|
38
|
+
document?: CanvasFactory;
|
|
39
|
+
/** Seed of the random spread. Default 1. */
|
|
40
|
+
seed?: number;
|
|
41
|
+
/** Wind vector [x, z] in m/s that bends smoke and steam. Default [0, 0]. */
|
|
42
|
+
wind?: [number, number];
|
|
43
|
+
/** Viewport height in pixels, used to size the point sprites. Update with `setViewHeight` on resize. Default 720. */
|
|
44
|
+
viewHeight?: number;
|
|
45
|
+
/** Height (m) water streams fall to when the emitter has no `reach`. Default 0. */
|
|
46
|
+
floorY?: number;
|
|
47
|
+
/** Most water streams drawn at once (one draw call each). Default 6. */
|
|
48
|
+
maxStreams?: number;
|
|
49
|
+
/** Most gas flame rings drawn at once (all rings share one draw call). Default 16. */
|
|
50
|
+
maxFlames?: number;
|
|
51
|
+
}
|
|
52
|
+
/** What the effects read from a camera. */
|
|
53
|
+
interface PerspectiveLike {
|
|
54
|
+
/** Vertical field of view in degrees. */
|
|
55
|
+
fov: number;
|
|
56
|
+
zoom?: number;
|
|
57
|
+
}
|
|
58
|
+
interface EffectsStats {
|
|
59
|
+
alive: number;
|
|
60
|
+
capacity: number;
|
|
61
|
+
byKind: Record<PoolKind, number>;
|
|
62
|
+
}
|
|
63
|
+
type PoolKind = 'smoke' | 'steam' | 'fire' | 'water' | 'spark';
|
|
64
|
+
interface Effects {
|
|
65
|
+
/**
|
|
66
|
+
* Spawns for the active emitters and advances every particle by `dt` seconds, sizing the sprites for the camera given to
|
|
67
|
+
* `createEffects` (or `setCamera`). No allocations per frame once warmed up. `camera` overrides it for this call (deprecated).
|
|
68
|
+
*/
|
|
69
|
+
update(dt: number, emitters: readonly ActiveEmitter[], camera?: PerspectiveLike): void;
|
|
70
|
+
/** Replace the camera used for the sprite size (or `null` to go back to a 90 degree lens). */
|
|
71
|
+
setCamera(camera: PerspectiveLike | (() => PerspectiveLike | null) | null): void;
|
|
72
|
+
setWind(x: number, z: number): void;
|
|
73
|
+
setViewHeight(px: number): void;
|
|
74
|
+
stats(): EffectsStats;
|
|
75
|
+
/** The Points objects (one per kind). */
|
|
76
|
+
objects: Record<PoolKind, ThreeNS.Points>;
|
|
77
|
+
/** Water stream meshes (tapered, translucent, following the gravity arc) and the one mesh holding every gas flame ring. */
|
|
78
|
+
meshes: {
|
|
79
|
+
group: ThreeNS.Group;
|
|
80
|
+
streams: ThreeNS.Mesh[];
|
|
81
|
+
flames: ThreeNS.Mesh;
|
|
82
|
+
};
|
|
83
|
+
clear(): void;
|
|
84
|
+
dispose(): void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pooled particle effects: smoke, steam, fire (and the blue gas `flame` ring), water streams with drips, and sparks.
|
|
88
|
+
* One `Points` per kind, fixed-size buffers, swap-remove; the total alive never exceeds `maxParticles`.
|
|
89
|
+
*/
|
|
90
|
+
declare function createEffects(THREE: ThreeLib, options: EffectsOptions): Effects;
|
|
91
|
+
/** @deprecated Pass `{ scene, camera, ... }` as the one options argument. */
|
|
92
|
+
declare function createEffects(THREE: ThreeLib, scene: ThreeNS.Object3D, options?: Partial<EffectsOptions>): Effects;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Points of a hanging cord between `a` and `b` (n points including both ends, n >= 2). The deepest point sits `sag` meters below
|
|
96
|
+
* the straight line between the ends, on a real catenary (y = a cosh(x/a)); when the ends differ in height the curve is
|
|
97
|
+
* measured against the straight line between them.
|
|
98
|
+
*/
|
|
99
|
+
declare function catenary(a: Vec3, b: Vec3, sag: number, n?: number): Vec3[];
|
|
100
|
+
/** A thin tube through `points` (Catmull-Rom smoothed) for wires, cords and conduits. */
|
|
101
|
+
declare function createCord(THREE: ThreeLib, points: readonly Vec3[], radius: number, material: ThreeNS.Material, radialSegments?: number): ThreeNS.Mesh;
|
|
102
|
+
|
|
103
|
+
type MaterialMap = Partial<Record<MaterialKey, ThreeNS.Material>>;
|
|
104
|
+
interface MeshApiOptions {
|
|
105
|
+
/** Materials per key. Missing keys get a default (vertex-colored) material. */
|
|
106
|
+
materials?: MaterialMap;
|
|
107
|
+
/** `high`: smoother cylinders and spheres, standard materials. `game`: fewer segments, Lambert materials. Default `high`. */
|
|
108
|
+
quality?: Quality;
|
|
109
|
+
/** Cast and receive shadows on the merged meshes. Default true. */
|
|
110
|
+
shadows?: boolean;
|
|
111
|
+
/** Make `plane` primitives double sided (applies to the default materials only). Default false. */
|
|
112
|
+
doubleSidedPlanes?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Cut draw calls: `finish(spec)` merges the parts of one `PartSpec.group` that share a motion (blind slats) and the `cloth` soft
|
|
115
|
+
* volumes (clothes on a line) into one mesh per material (see `mergeParts`). Needs the spec in `finish`. Default: on in `game` quality.
|
|
116
|
+
*/
|
|
117
|
+
mergeParts?: boolean;
|
|
118
|
+
/** Idle flutter of merged cloth. Default 0.04. */
|
|
119
|
+
swayAmplitude?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Material of one draw: called per mesh with its material key and the part id (`null` for the static body). Return the material the
|
|
122
|
+
* host wants there (a per-room light mask, a flickering emissive...); the default is `materials[key]`. Merged groups (blind slats,
|
|
123
|
+
* clothes) keep what you return, including its `onBeforeCompile`, `customProgramCacheKey`, `defines` and `userData`.
|
|
124
|
+
*/
|
|
125
|
+
materialFor?: (key: MaterialKey, partId: string | null) => ThreeNS.Material | undefined;
|
|
126
|
+
}
|
|
127
|
+
interface MeshResult {
|
|
128
|
+
/** Root group: static meshes (one per material) plus one child group per part (merged parts share one group, see `mergeParts`). */
|
|
129
|
+
group: ThreeNS.Group;
|
|
130
|
+
/** Part groups by id. Origin at the pivot, rotated by the part's `ry`; `userData.rest` holds the rest pose. A merged part maps to the shared group (`userData.merged`). */
|
|
131
|
+
parts: Record<string, ThreeNS.Group>;
|
|
132
|
+
/** Real lights requested by the builders (already children of their group). */
|
|
133
|
+
lights: ThreeNS.Light[];
|
|
134
|
+
}
|
|
135
|
+
interface FinishOptions {
|
|
136
|
+
/** Same as `MeshApiOptions.materialFor`; wins over it for this call. */
|
|
137
|
+
materialFor?: MeshApiOptions['materialFor'];
|
|
138
|
+
}
|
|
139
|
+
interface MeshApi {
|
|
140
|
+
api: HouseholdApi;
|
|
141
|
+
/** Builds the groups from everything drawn so far. Call once; the api is spent afterwards. Pass the spec the pack returned to allow `mergeParts`. */
|
|
142
|
+
finish(spec?: ObjectSpec, finishOptions?: FinishOptions): MeshResult;
|
|
143
|
+
/** Materials in use (defaults merged with the given ones). */
|
|
144
|
+
materials: Record<MaterialKey, ThreeNS.Material>;
|
|
145
|
+
}
|
|
146
|
+
/** Default materials: vertex colors everywhere (in `game` quality plain, metal, wood and fabric are one shared Lambert material), glass and water transparent, emissive unlit. */
|
|
147
|
+
declare function createDefaultMaterials(THREE: ThreeLib, quality?: Quality): Record<MaterialKey, ThreeNS.Material>;
|
|
148
|
+
/**
|
|
149
|
+
* Mesh api for content packs. Every primitive is baked (vertex colors) into one `BufferGeometry` per material and per target
|
|
150
|
+
* (the root, or a part), so the draw calls are the number of materials in use plus the same again for each part.
|
|
151
|
+
*/
|
|
152
|
+
declare function createMeshApi(THREE: ThreeLib, options?: MeshApiOptions): MeshApi;
|
|
153
|
+
|
|
154
|
+
/** `Reflector` from `three/examples/jsm/objects/Reflector.js`, passed by the host. */
|
|
155
|
+
type ReflectorConstructor = new (geometry: ThreeNS.BufferGeometry, options?: {
|
|
156
|
+
textureWidth?: number;
|
|
157
|
+
textureHeight?: number;
|
|
158
|
+
clipBias?: number;
|
|
159
|
+
color?: ThreeNS.ColorRepresentation;
|
|
160
|
+
}) => ThreeNS.Mesh;
|
|
161
|
+
interface MirrorOptions {
|
|
162
|
+
/** Without it every mirror shows the env map only. */
|
|
163
|
+
Reflector?: ReflectorConstructor;
|
|
164
|
+
/** Cube / equirect env map for the cheap look. */
|
|
165
|
+
envMap?: ThreeNS.Texture | null;
|
|
166
|
+
/** Real reflectors alive at once. Default 1. */
|
|
167
|
+
max?: number;
|
|
168
|
+
/** Reflector texture size in pixels. Default 512. */
|
|
169
|
+
resolution?: number;
|
|
170
|
+
/** Where reflectors are added. Default: the root ancestor of the registered mesh. */
|
|
171
|
+
scene?: ThreeNS.Object3D;
|
|
172
|
+
/** Distance in front of the plane, meters. Default 0.001. */
|
|
173
|
+
offset?: number;
|
|
174
|
+
/**
|
|
175
|
+
* three's `Reflector` renders the scene again inside the main `render()`, and that nested render resets `renderer.info` when
|
|
176
|
+
* `info.autoReset` is on, so the counters would only show the reflector pass. With `keepInfo` (default true) the reflector adds
|
|
177
|
+
* what was counted before it back, so `renderer.info.render.calls` covers the whole frame. Skipped when `info.autoReset` is off.
|
|
178
|
+
*/
|
|
179
|
+
keepInfo?: boolean;
|
|
180
|
+
}
|
|
181
|
+
/** The mirror entries of `world.mirrors(...)` (only these fields are read). */
|
|
182
|
+
interface MirrorCandidate {
|
|
183
|
+
id: string;
|
|
184
|
+
center: Vec3;
|
|
185
|
+
normal: Vec3;
|
|
186
|
+
}
|
|
187
|
+
interface Mirrors {
|
|
188
|
+
/** Registers the display mesh of a mirror (world id `objectId:mirrorId`). It gets the env-map material. `normal` is the world normal; default: the mesh's +z. */
|
|
189
|
+
register(id: string, mesh: ThreeNS.Mesh, normal?: Vec3): void;
|
|
190
|
+
unregister(id: string): void;
|
|
191
|
+
/** Activates real reflectors for the nearest facing registered mirrors among `candidates`. Returns the active ids. */
|
|
192
|
+
update(camera: ThreeNS.Camera, candidates: readonly MirrorCandidate[]): string[];
|
|
193
|
+
/** Ids that currently have a real reflector. */
|
|
194
|
+
active(): string[];
|
|
195
|
+
/** The shared env-map material registered meshes use. */
|
|
196
|
+
envMaterial: ThreeNS.Material;
|
|
197
|
+
dispose(): void;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Mirrors: every registered mirror shows an env-map material; the nearest `max` that face the camera get a real `Reflector`
|
|
201
|
+
* (pooled and reused), placed `offset` in front of the plane with the mesh's rotation and size.
|
|
202
|
+
*/
|
|
203
|
+
declare function createMirrors(THREE: ThreeLib, options?: MirrorOptions): Mirrors;
|
|
204
|
+
|
|
205
|
+
/** Pure pose of a part at `t`, in the part's own frame (the frame at rest: pivot at the origin, already turned by the part's `ry`). */
|
|
206
|
+
interface Pose {
|
|
207
|
+
/** Translation, part frame. */
|
|
208
|
+
position: Vec3;
|
|
209
|
+
/** Rotation as a quaternion [x, y, z, w], part frame. */
|
|
210
|
+
quaternion: [number, number, number, number];
|
|
211
|
+
/** Scale per local axis. */
|
|
212
|
+
scale: Vec3;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The engine's pose math, as a quaternion. This is the engine's own `poseOf` (same default angles and distances), so the rendered
|
|
216
|
+
* part and the engine's `slotPosition`, colliders, mirrors and `fitSweep` always agree. `t` is clamped to [0, 1].
|
|
217
|
+
*/
|
|
218
|
+
declare function poseOf(motion: Motion, t: number): Pose;
|
|
219
|
+
interface Rest {
|
|
220
|
+
position: Vec3;
|
|
221
|
+
quaternion: [number, number, number, number];
|
|
222
|
+
scale: Vec3;
|
|
223
|
+
}
|
|
224
|
+
/** Remembers the rest transform of a part group in `userData.rest` (once). */
|
|
225
|
+
declare function rememberRest(group: {
|
|
226
|
+
position: any;
|
|
227
|
+
quaternion: any;
|
|
228
|
+
scale: any;
|
|
229
|
+
userData: Record<string, any>;
|
|
230
|
+
}): Rest;
|
|
231
|
+
/**
|
|
232
|
+
* Poses `group` for a motion at `t`. The rest pose (position at the pivot, rotation `ry`) is saved in `group.userData.rest` the first
|
|
233
|
+
* time, so calling this every frame never drifts. The motion axis lives in the part frame (after `ry`).
|
|
234
|
+
*/
|
|
235
|
+
declare function applyPose<G extends ThreeNS.Object3D>(THREE: ThreeLib, group: G, motion: Motion, t: number): G;
|
|
236
|
+
interface SyncOptions {
|
|
237
|
+
/** Seconds, drives the idle flutter of sway shaders. Default 0. */
|
|
238
|
+
time?: number;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Per-frame sync of one world object: every part group takes the motion of its `PartSpec` at the world's current t,
|
|
242
|
+
* and every soft volume swings. A soft volume drives the group in `parts` under its `part` (or, failing that, its own id):
|
|
243
|
+
* meshes patched with `attachSway` get the swing through their shader uniforms (so shadows follow), any other group is rotated
|
|
244
|
+
* around the anchor line.
|
|
245
|
+
*/
|
|
246
|
+
declare function syncObject(THREE: ThreeLib, world: World, objectId: string, parts: Record<string, ThreeNS.Object3D>, options?: SyncOptions): void;
|
|
247
|
+
|
|
248
|
+
interface ScreenOptions {
|
|
249
|
+
/** Where the canvas is created (the global `document`, or a fake). */
|
|
250
|
+
document: CanvasFactory;
|
|
251
|
+
/** Canvas size in pixels. Default 128 x 72. */
|
|
252
|
+
w?: number;
|
|
253
|
+
h?: number;
|
|
254
|
+
/** Seed of the static noise. Default 1. */
|
|
255
|
+
seed?: number;
|
|
256
|
+
/** Redraws per second while on. Default 15. */
|
|
257
|
+
fps?: number;
|
|
258
|
+
/** Emissive strength while on. Default 1. */
|
|
259
|
+
intensity?: number;
|
|
260
|
+
}
|
|
261
|
+
interface Screen {
|
|
262
|
+
material: ThreeNS.MeshStandardMaterial;
|
|
263
|
+
texture: ThreeNS.CanvasTexture;
|
|
264
|
+
canvas: any;
|
|
265
|
+
readonly on: boolean;
|
|
266
|
+
readonly channel: number;
|
|
267
|
+
setOn(on: boolean): void;
|
|
268
|
+
setChannel(channel: number): void;
|
|
269
|
+
/** Advance the animation; redraws the canvas at `fps` while on. */
|
|
270
|
+
update(dt: number): void;
|
|
271
|
+
dispose(): void;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* A screen material backed by a canvas texture. Off: black canvas, no emission. On: the channel's content (0 color bars,
|
|
275
|
+
* 1 static noise, 2 animated landscape, higher numbers repeat them with shifted hues) lights the surface.
|
|
276
|
+
*/
|
|
277
|
+
declare function createScreen(THREE: ThreeLib, options: ScreenOptions): Screen;
|
|
278
|
+
|
|
279
|
+
interface SwayOptions {
|
|
280
|
+
/** Strength of the idle flutter (wind) in meters per meter of hang, 0..1. Default 0.04. */
|
|
281
|
+
amplitude?: number;
|
|
282
|
+
/** Local y (geometry space) of the line the cloth hangs from. Default: top of the mesh's bounding box. */
|
|
283
|
+
anchorY?: number;
|
|
284
|
+
}
|
|
285
|
+
interface SwayUniforms {
|
|
286
|
+
time: {
|
|
287
|
+
value: number;
|
|
288
|
+
};
|
|
289
|
+
ax: {
|
|
290
|
+
value: number;
|
|
291
|
+
};
|
|
292
|
+
az: {
|
|
293
|
+
value: number;
|
|
294
|
+
};
|
|
295
|
+
anchorY: {
|
|
296
|
+
value: number;
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** Shared state of one swaying mesh: the three materials read the same uniforms. */
|
|
300
|
+
interface SwayHandle {
|
|
301
|
+
uniforms: SwayUniforms;
|
|
302
|
+
/** Sets the swing (radians around x and z) and, optionally, the clock for the idle flutter. */
|
|
303
|
+
set(ax: number, az: number, time?: number): void;
|
|
304
|
+
}
|
|
305
|
+
/** Patches a vertex shader so the vertices hang from the anchor line and swing with the shared uniforms. */
|
|
306
|
+
declare function patchSwayShader(shader: {
|
|
307
|
+
uniforms: Record<string, any>;
|
|
308
|
+
vertexShader: string;
|
|
309
|
+
}, uniforms: SwayUniforms, amplitude: number): void;
|
|
310
|
+
/**
|
|
311
|
+
* Clones `base` and patches it with a vertex-shader sway (uniforms `time`, `ax`, `az`, `anchorY`). The returned material carries
|
|
312
|
+
* `userData.sway` (a `SwayHandle`), `customDepthMaterial` and `customDistanceMaterial` patched with the same uniforms, so
|
|
313
|
+
* directional / spot shadow maps and point-light shadow cubes follow the animation. Assign it to a mesh, and set
|
|
314
|
+
* `mesh.customDepthMaterial` / `mesh.customDistanceMaterial` from the material (or just use `attachSway`).
|
|
315
|
+
*/
|
|
316
|
+
declare function swayMaterial(THREE: ThreeLib, base: ThreeNS.Material, options?: SwayOptions): ThreeNS.Material & {
|
|
317
|
+
userData: {
|
|
318
|
+
sway: SwayHandle;
|
|
319
|
+
depthMaterial: ThreeNS.MeshDepthMaterial;
|
|
320
|
+
distanceMaterial: ThreeNS.MeshDistanceMaterial;
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Makes a mesh sway: replaces its material with `swayMaterial(...)` and sets `customDepthMaterial` and `customDistanceMaterial`.
|
|
325
|
+
* `mesh.userData.sway` is the handle `syncObject` drives. Returns the handle.
|
|
326
|
+
*/
|
|
327
|
+
declare function attachSway(THREE: ThreeLib, mesh: ThreeNS.Mesh, options?: SwayOptions): SwayHandle;
|
|
328
|
+
|
|
329
|
+
interface ThreeAdapterOptions {
|
|
330
|
+
/** Default for `createMeshApi` / `createScreen` / `createEffects`. */
|
|
331
|
+
document?: CanvasFactory;
|
|
332
|
+
quality?: Quality;
|
|
333
|
+
materials?: MeshApiOptions['materials'];
|
|
334
|
+
/** `Reflector` and env map for `createMirrors`. */
|
|
335
|
+
Reflector?: MirrorOptions['Reflector'];
|
|
336
|
+
envMap?: MirrorOptions['envMap'];
|
|
337
|
+
}
|
|
338
|
+
/** The package functions with `THREE` and the common options already bound. */
|
|
339
|
+
interface ThreeAdapter {
|
|
340
|
+
THREE: ThreeLib;
|
|
341
|
+
createMeshApi(options?: MeshApiOptions): MeshApi;
|
|
342
|
+
applyPose<G extends ThreeNS.Object3D>(group: G, motion: Motion, t: number): G;
|
|
343
|
+
syncObject(world: World, objectId: string, parts: Record<string, ThreeNS.Object3D>, options?: SyncOptions): void;
|
|
344
|
+
swayMaterial(base: ThreeNS.Material, options?: SwayOptions): ReturnType<typeof swayMaterial>;
|
|
345
|
+
attachSway(mesh: ThreeNS.Mesh, options?: SwayOptions): SwayHandle;
|
|
346
|
+
createEffects(options: EffectsOptions): Effects;
|
|
347
|
+
/** @deprecated Pass `{ scene, camera, ... }`. */
|
|
348
|
+
createEffects(scene: ThreeNS.Object3D, options?: Partial<EffectsOptions>): Effects;
|
|
349
|
+
createScreen(options?: Partial<ScreenOptions>): Screen;
|
|
350
|
+
createMirrors(options?: MirrorOptions): Mirrors;
|
|
351
|
+
createCord(points: readonly Vec3[], radius: number, material: ThreeNS.Material): ThreeNS.Mesh;
|
|
352
|
+
catenary: typeof catenary;
|
|
353
|
+
}
|
|
354
|
+
/** Binds the host's `THREE` (and shared options) once, so the rest of the host code never passes it around. */
|
|
355
|
+
declare function createThreeAdapter(THREE: ThreeLib, options?: ThreeAdapterOptions): ThreeAdapter;
|
|
356
|
+
|
|
357
|
+
interface MergeOptions {
|
|
358
|
+
/** Idle flutter of merged cloth (see `SwayOptions.amplitude`). Default 0.04. */
|
|
359
|
+
swayAmplitude?: number;
|
|
360
|
+
}
|
|
361
|
+
/** Posed by `syncObject` in place of the part groups it replaced. */
|
|
362
|
+
interface PoseBatch {
|
|
363
|
+
kind: 'pose';
|
|
364
|
+
/** Part ids that moved into the batch. */
|
|
365
|
+
ids: string[];
|
|
366
|
+
/** Applies the motion at `t` to every vertex, each around its own pivot. */
|
|
367
|
+
apply(motion: Motion, t: number): void;
|
|
368
|
+
}
|
|
369
|
+
/** Cloth hanging from a line: every merged cloth swings with its own (ax, az). */
|
|
370
|
+
interface SwayBatch {
|
|
371
|
+
kind: 'sway';
|
|
372
|
+
/** Soft volume id -> index. */
|
|
373
|
+
index: Record<string, number>;
|
|
374
|
+
setAt(index: number, ax: number, az: number): void;
|
|
375
|
+
setTime(time: number): void;
|
|
376
|
+
}
|
|
377
|
+
type MergedGroup = ThreeNS.Group & {
|
|
378
|
+
userData: {
|
|
379
|
+
merged: PoseBatch | SwayBatch;
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* Merges parts after `finish()` to cut draw calls (the `mergeParts` option of `createMeshApi`):
|
|
384
|
+
*
|
|
385
|
+
* - parts of one `PartSpec.group` with the same motion (blind slats) become one mesh per material whose vertices carry their own pivot;
|
|
386
|
+
* `syncObject` poses them with one set of uniforms and the shadow materials follow;
|
|
387
|
+
* - `cloth` soft volumes (clothes on a line) become one mesh per material with a per-vertex cloth index and anchor height; each
|
|
388
|
+
* cloth keeps its own swing.
|
|
389
|
+
*
|
|
390
|
+
* The merged group is added to `root`, the part groups are removed, and `parts[id]` points at the merged group for every member.
|
|
391
|
+
* Parts that carry a mirror, that are turned (`ry`) or that stand alone are left as they are.
|
|
392
|
+
*/
|
|
393
|
+
declare function mergeParts(THREE: ThreeLib, root: ThreeNS.Group, parts: Record<string, ThreeNS.Group>, spec: ObjectSpec, options?: MergeOptions): void;
|
|
394
|
+
|
|
395
|
+
export { type CanvasFactory, type Effects, type EffectsOptions, type EffectsStats, type FinishOptions, type HouseholdApi, MATERIAL_KEYS, type MaterialKey, type MaterialMap, type MergeOptions, type MergedGroup, type MeshApi, type MeshApiOptions, type MeshResult, type MirrorCandidate, type MirrorOptions, type Mirrors, type PerspectiveLike, type PoolKind, type Pose, type PoseBatch, type Quality, type ReflectorConstructor, type Screen, type ScreenOptions, type SwayBatch, type SwayHandle, type SwayOptions, type SwayUniforms, type SyncOptions, type ThreeAdapter, type ThreeAdapterOptions, type ThreeLib, type Vec3, applyPose, attachSway, catenary, createCord, createDefaultMaterials, createEffects, createMeshApi, createMirrors, createScreen, createThreeAdapter, mergeParts, patchSwayShader, poseOf, rememberRest, swayMaterial, syncObject };
|