@rig-c/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/NOTICE +5 -0
- package/README.md +108 -2
- package/dist/index.d.ts +81 -0
- package/dist/index.js +465 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rig-c
|
|
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/NOTICE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
@rig-c/three is MIT licensed. It requires Three.js and @rig-c/play as peer
|
|
2
|
+
dependencies. It reads rig-c model documents and links no Spine runtime.
|
|
3
|
+
Shared workspace helpers, when used, are bundled into host libraries; the private
|
|
4
|
+
@rig-c/adapters-shared workspace is not published. See LICENSE for this package's
|
|
5
|
+
licence and each dependency's licence for its own terms.
|
package/README.md
CHANGED
|
@@ -1,3 +1,109 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rig-c/three
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Draw a rig-c model in a native Three.js scene through `@rig-c/play`. `ThreeRig`
|
|
4
|
+
is a flat `Group`: each drawn slot is a `Mesh` with native float32 geometry,
|
|
5
|
+
with one shared shader material per page per instance. It poses no bones itself.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @rig-c/three three @rig-c/play
|
|
11
|
+
npm install --save-dev @types/three
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Minimal usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { loadModel } from '@rig-c/play';
|
|
18
|
+
import { OrthographicCamera, Scene, WebGLRenderer } from 'three';
|
|
19
|
+
import { ThreeRig } from '@rig-c/three';
|
|
20
|
+
|
|
21
|
+
const renderer = new WebGLRenderer({ alpha: true });
|
|
22
|
+
renderer.setSize(800, 600);
|
|
23
|
+
document.body.appendChild(renderer.domElement);
|
|
24
|
+
const scene = new Scene();
|
|
25
|
+
const camera = new OrthographicCamera(-400, 400, 300, -300, 0.1, 2000);
|
|
26
|
+
camera.position.z = 1000;
|
|
27
|
+
const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
|
|
28
|
+
const rig = new ThreeRig(assets, { renderer });
|
|
29
|
+
rig.play('walk');
|
|
30
|
+
scene.add(rig);
|
|
31
|
+
let previous: number | undefined;
|
|
32
|
+
function frame(now: number) {
|
|
33
|
+
if (previous !== undefined) rig.update((now - previous) / 1000);
|
|
34
|
+
previous = now;
|
|
35
|
+
renderer.render(scene, camera);
|
|
36
|
+
requestAnimationFrame(frame);
|
|
37
|
+
}
|
|
38
|
+
requestAnimationFrame(frame);
|
|
39
|
+
// On removal: rig.destroy(); assets.dispose(); renderer.dispose();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use the model document and image pages from `rigc build`, unchanged. Tested with
|
|
43
|
+
Three.js **0.186.1** and play **1.10.0**. The API is 0.x. See [`packages/three` in the repository](https://github.com/rig-c/adapters/tree/main/packages/three) for source and parity tests.
|
|
44
|
+
|
|
45
|
+
## Live object
|
|
46
|
+
|
|
47
|
+
`play(name, options)`, `seek(seconds)`, `advance(dt)` and `update(dt)` forward to
|
|
48
|
+
an independent `RigPlayer`; deltas are seconds, and the player's `timescale`
|
|
49
|
+
(alias `timeScale`) applies exactly once. The app owns the loop. `timescale = 0`
|
|
50
|
+
holds the drawn frame and uploads no geometry. `drawnFrame()` is the exact record
|
|
51
|
+
passed to the native renderer; `posedFrame(filter)`, `bone(name)` and `bounds()`
|
|
52
|
+
use play's readers. World bounds are in rig-local Y-up space, before this Group's
|
|
53
|
+
transform. `resourceCounts()` reports owned meshes, geometries, page materials,
|
|
54
|
+
clip textures and retained/shared pages. `destroy()` is idempotent and frees
|
|
55
|
+
only this object's resources; it neither destroys the application renderer nor
|
|
56
|
+
modifies another rig. Dispose the shared ModelAssets after all players using it
|
|
57
|
+
are destroyed; the adapter does not own the load's blob URLs. Mutating playback after destroy is refused.
|
|
58
|
+
|
|
59
|
+
Several rigs can share one `ModelAssets` load. Page textures are shared by image
|
|
60
|
+
identity until the last renderer releases them; instance geometry, clip data and
|
|
61
|
+
material uniforms remain separate. A page image cannot simultaneously declare
|
|
62
|
+
both straight and premultiplied pixels: conflicting `pma` records are refused.
|
|
63
|
+
|
|
64
|
+
## Native drawing and scene placement
|
|
65
|
+
|
|
66
|
+
`ThreeRenderer(root, assets, webglRenderer)` is available when the app owns the
|
|
67
|
+
player. Call `render(player.posedFrame())`, then render the scene normally.
|
|
68
|
+
No internal canvas, offscreen bitmap of the rig, or fallback renderer draws it.
|
|
69
|
+
Positions and page UVs come directly from the record; the full atlas page is
|
|
70
|
+
sampled, so packed rotation needs no separate rule. The root stays Y-up and may
|
|
71
|
+
be translated, rotated and scaled under an orthographic or perspective camera.
|
|
72
|
+
Use the Group's `renderOrder` to order separate flat rigs in overlapping scenes;
|
|
73
|
+
slot meshes have explicit internal draw order. Materials retain depth testing
|
|
74
|
+
for ordinary 3D occlusion and disable depth writing for translucent slots. They
|
|
75
|
+
are double-sided with a single draw pass. The adapter changes no global camera,
|
|
76
|
+
renderer colour space, sorting, depth or stencil state.
|
|
77
|
+
|
|
78
|
+
The shader matches play's premultiplied two-colour expression and explicit
|
|
79
|
+
Normal/Additive/Multiply/Screen RGB and alpha blend factors, without lighting,
|
|
80
|
+
tone mapping or an extra colour-space conversion. Straight pages are multiplied
|
|
81
|
+
once on upload; already-PMA pages are not. Texture UV orientation, linear
|
|
82
|
+
filtering and mipmap policy are explicit.
|
|
83
|
+
|
|
84
|
+
Clips are hard shader polygons in the same local plane as the geometry. Ordinary
|
|
85
|
+
concave polygons retain their shape; `convex: true` and inverse clips use the
|
|
86
|
+
convex hull, matching play's canvas reader. Per-slot point data is a float
|
|
87
|
+
DataTexture, never an array truncated to a guessed uniform limit. A clip with
|
|
88
|
+
more vertices than the actual renderer's `MAX_TEXTURE_SIZE` is refused as
|
|
89
|
+
`THREE_CLIP_CAPACITY` before any scene/resource change; malformed, crossing or
|
|
90
|
+
zero-area polygons are refused by slot and page as well. This is a WebGL2 host;
|
|
91
|
+
`WebGPURenderer` is not this package's renderer.
|
|
92
|
+
|
|
93
|
+
Geometry upload counters expose position, UV and triangle updates. Native
|
|
94
|
+
resources are reused when their arrays remain unchanged. Capacity changes dispose
|
|
95
|
+
the old geometry before replacing its attributes. The diagnostics
|
|
96
|
+
`alwaysReuploadGeometry` and `releasePageTexturesOnDispose` exist for planted
|
|
97
|
+
negative controls; keep their normal values `false` and `true` respectively.
|
|
98
|
+
|
|
99
|
+
## Demo and verification
|
|
100
|
+
|
|
101
|
+
From the repository, `bun run dev` serves `/three-demo.html`, including a flat
|
|
102
|
+
object in a perspective scene. Query parameters: `?rig=walk&anim=walk&time=0.5×cale=0&count=3&view=perspective`.
|
|
103
|
+
The default camera is orthographic; paused queries permit deterministic capture.
|
|
104
|
+
|
|
105
|
+
`bun run verify` discovers this host without workflow changes. It checks native
|
|
106
|
+
float storage and transformed geometry, same-browser CanvasRenderer pictures,
|
|
107
|
+
planted picture faults, exact pose parity, fixed updates, native held uploads,
|
|
108
|
+
shared page lifetime, buffer deletion, package contents and installed exports.
|
|
109
|
+
The release pipeline registers this package independently of the other hosts.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as three from 'three';
|
|
2
|
+
import { Group, WebGLRenderer, Texture, Mesh, BufferGeometry, ShaderMaterial, Camera } from 'three';
|
|
3
|
+
import { ModelAssets, PosedFrame, RigPlayer, SlotFilter, PlayOptions, BoneWorld, PoseBounds, PosedSlot } from '@rig-c/play';
|
|
4
|
+
|
|
5
|
+
declare const getLivePageTextureCount: () => number;
|
|
6
|
+
declare const getPageRefCount: (image: HTMLImageElement) => number;
|
|
7
|
+
declare const getSharedPageTexture: (image: HTMLImageElement) => Texture<unknown, three.TextureEventMap> | undefined;
|
|
8
|
+
interface ThreeSlotMesh extends Mesh<BufferGeometry, ShaderMaterial> {
|
|
9
|
+
posedSlot: PosedSlot;
|
|
10
|
+
}
|
|
11
|
+
interface ThreeRigResourceCounts {
|
|
12
|
+
meshes: number;
|
|
13
|
+
geometries: number;
|
|
14
|
+
materials: number;
|
|
15
|
+
clipTextures: number;
|
|
16
|
+
retainedPages: number;
|
|
17
|
+
livePageTextures: number;
|
|
18
|
+
}
|
|
19
|
+
/** Native meshes in Y-up local space. The application owns the WebGL renderer and camera. */
|
|
20
|
+
declare class ThreeRenderer {
|
|
21
|
+
readonly root: Group;
|
|
22
|
+
readonly assets: ModelAssets;
|
|
23
|
+
readonly host: WebGLRenderer;
|
|
24
|
+
positionUploadCount: number;
|
|
25
|
+
uvUploadCount: number;
|
|
26
|
+
triangleUploadCount: number;
|
|
27
|
+
alwaysReuploadGeometry: boolean;
|
|
28
|
+
/** Negative-control switch; normal disposal always releases retained page textures. */
|
|
29
|
+
releasePageTexturesOnDispose: boolean;
|
|
30
|
+
private disposed;
|
|
31
|
+
private readonly pool;
|
|
32
|
+
private readonly materials;
|
|
33
|
+
private readonly retained;
|
|
34
|
+
constructor(root: Group, assets: ModelAssets, host: WebGLRenderer);
|
|
35
|
+
get geometryUploadCount(): number;
|
|
36
|
+
get isDisposed(): boolean;
|
|
37
|
+
resourceCounts(): ThreeRigResourceCounts;
|
|
38
|
+
getPageTexture(name: string): Texture | undefined;
|
|
39
|
+
private prepare;
|
|
40
|
+
private material;
|
|
41
|
+
render(frame: PosedFrame): void;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
}
|
|
44
|
+
/** Read native float32 positions through the application's world and camera mapping. */
|
|
45
|
+
declare function readMeshScreenVertices(mesh: ThreeSlotMesh, camera: Camera, width: number, height: number): number[];
|
|
46
|
+
interface ThreeRigOptions {
|
|
47
|
+
renderer: WebGLRenderer;
|
|
48
|
+
skin?: string;
|
|
49
|
+
slots?: SlotFilter;
|
|
50
|
+
timescale?: number;
|
|
51
|
+
timeScale?: number;
|
|
52
|
+
}
|
|
53
|
+
/** A flat native Group. Advance from the app's loop using seconds, without an internal clock. */
|
|
54
|
+
declare class ThreeRig extends Group {
|
|
55
|
+
readonly assets: ModelAssets;
|
|
56
|
+
readonly player: RigPlayer;
|
|
57
|
+
readonly renderer: ThreeRenderer;
|
|
58
|
+
private lastFrame;
|
|
59
|
+
private destroyed;
|
|
60
|
+
constructor(assets: ModelAssets, options: ThreeRigOptions);
|
|
61
|
+
private active;
|
|
62
|
+
play(name: string, options?: PlayOptions): void;
|
|
63
|
+
seek(time: number): void;
|
|
64
|
+
advance(dt: number): void;
|
|
65
|
+
update(dt: number): void;
|
|
66
|
+
draw(): PosedFrame;
|
|
67
|
+
drawnFrame(): PosedFrame;
|
|
68
|
+
posedFrame(slots?: SlotFilter | null): PosedFrame;
|
|
69
|
+
bone(name: string): BoneWorld;
|
|
70
|
+
bounds(): PoseBounds | null;
|
|
71
|
+
get time(): number;
|
|
72
|
+
get current(): string | null;
|
|
73
|
+
get timescale(): number;
|
|
74
|
+
set timescale(value: number);
|
|
75
|
+
get timeScale(): number;
|
|
76
|
+
set timeScale(value: number);
|
|
77
|
+
resourceCounts(): ThreeRigResourceCounts;
|
|
78
|
+
destroy(): void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { ThreeRenderer, ThreeRig, type ThreeRigOptions, type ThreeRigResourceCounts, type ThreeSlotMesh, getLivePageTextureCount, getPageRefCount, getSharedPageTexture, readMeshScreenVertices };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// packages/three/src/three.ts
|
|
2
|
+
import { RigPlayer } from "@rig-c/play";
|
|
3
|
+
import {
|
|
4
|
+
AddEquation,
|
|
5
|
+
BufferAttribute,
|
|
6
|
+
BufferGeometry,
|
|
7
|
+
CustomBlending,
|
|
8
|
+
DataTexture,
|
|
9
|
+
DoubleSide,
|
|
10
|
+
DstColorFactor,
|
|
11
|
+
DynamicDrawUsage,
|
|
12
|
+
FloatType,
|
|
13
|
+
GLSL3,
|
|
14
|
+
Group,
|
|
15
|
+
LinearFilter,
|
|
16
|
+
Mesh,
|
|
17
|
+
NearestFilter,
|
|
18
|
+
NoColorSpace,
|
|
19
|
+
OneFactor,
|
|
20
|
+
OneMinusSrcAlphaFactor,
|
|
21
|
+
OneMinusSrcColorFactor,
|
|
22
|
+
RGBAFormat,
|
|
23
|
+
ShaderMaterial,
|
|
24
|
+
Texture,
|
|
25
|
+
Vector3
|
|
26
|
+
} from "three";
|
|
27
|
+
|
|
28
|
+
// packages/three/src/clip.ts
|
|
29
|
+
var cross = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
30
|
+
function hull(points) {
|
|
31
|
+
const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
32
|
+
const half = (items) => {
|
|
33
|
+
const result = [];
|
|
34
|
+
for (const p of items) {
|
|
35
|
+
while (result.length >= 2 && cross(result[result.length - 2], result[result.length - 1], p) <= 0) result.pop();
|
|
36
|
+
result.push(p);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
return [...half(sorted).slice(0, -1), ...half([...sorted].reverse()).slice(0, -1)];
|
|
41
|
+
}
|
|
42
|
+
function clipPolygon(clip, detail, capacity) {
|
|
43
|
+
const fail = (why) => {
|
|
44
|
+
throw new Error(`${detail} has invalid clip: ${why} (expected finite simple nonzero-area polygon, boolean inverse/convex, at most GPU MAX_TEXTURE_SIZE=${capacity} vertices)`);
|
|
45
|
+
};
|
|
46
|
+
if (!clip || typeof clip.inverse !== "boolean" || typeof clip.convex !== "boolean" || !Array.isArray(clip.polygon) || clip.polygon.length < 6 || clip.polygon.length % 2 !== 0) fail("invalid flags or polygon length");
|
|
47
|
+
if (clip.polygon.length / 2 > capacity) fail(`THREE_CLIP_CAPACITY: found ${clip.polygon.length / 2} vertices`);
|
|
48
|
+
const points = [];
|
|
49
|
+
for (let i = 0; i < clip.polygon.length; i += 2) {
|
|
50
|
+
const p = [clip.polygon[i], clip.polygon[i + 1]];
|
|
51
|
+
if (!p.every((v) => Number.isFinite(v) && Number.isFinite(Math.fround(v)))) fail(`vertex ${i / 2} contains ${p.join(",")}`);
|
|
52
|
+
const previous = points.at(-1);
|
|
53
|
+
if (!previous || previous[0] !== p[0] || previous[1] !== p[1]) points.push(p);
|
|
54
|
+
}
|
|
55
|
+
if (points.length > 1 && points[0][0] === points.at(-1)[0] && points[0][1] === points.at(-1)[1]) points.pop();
|
|
56
|
+
if (points.length < 3) fail("fewer than three distinct vertices");
|
|
57
|
+
for (let i = 0; i < points.length; i++) if (points.slice(0, i).some((p) => p[0] === points[i][0] && p[1] === points[i][1])) fail(`nonadjacent repeated vertex ${i}`);
|
|
58
|
+
const n = points.length;
|
|
59
|
+
let area = 0;
|
|
60
|
+
const on = (a, b, p) => cross(a, b, p) === 0 && p[0] >= Math.min(a[0], b[0]) && p[0] <= Math.max(a[0], b[0]) && p[1] >= Math.min(a[1], b[1]) && p[1] <= Math.max(a[1], b[1]);
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
const a = points[i], b = points[(i + 1) % n];
|
|
63
|
+
area += a[0] * b[1] - a[1] * b[0];
|
|
64
|
+
for (let j = i + 2; j < n; j++) {
|
|
65
|
+
if (i === 0 && j === n - 1) continue;
|
|
66
|
+
const c = points[j], d = points[(j + 1) % n];
|
|
67
|
+
if (cross(a, b, c) * cross(a, b, d) < 0 && cross(c, d, a) * cross(c, d, b) < 0 || on(a, b, c) || on(a, b, d) || on(c, d, a) || on(c, d, b)) fail(`crossing edges ${i}/${j}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (area === 0) fail("zero area");
|
|
71
|
+
return (clip.inverse || clip.convex ? hull(points) : points).flat();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// packages/three/src/three.ts
|
|
75
|
+
function isWebGLHost(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && "isWebGLRenderer" in value && value.isWebGLRenderer === true;
|
|
77
|
+
}
|
|
78
|
+
var pages = /* @__PURE__ */ new WeakMap();
|
|
79
|
+
var livePages = /* @__PURE__ */ new Set();
|
|
80
|
+
var getLivePageTextureCount = () => livePages.size;
|
|
81
|
+
var getPageRefCount = (image) => pages.get(image)?.holders.size ?? 0;
|
|
82
|
+
var getSharedPageTexture = (image) => pages.get(image)?.texture;
|
|
83
|
+
var REGION_INDICES = [0, 1, 2, 2, 3, 0];
|
|
84
|
+
var VERTEX = `
|
|
85
|
+
out vec2 vUV;
|
|
86
|
+
out vec2 vPosition;
|
|
87
|
+
void main() {
|
|
88
|
+
vUV = uv;
|
|
89
|
+
vPosition = position.xy;
|
|
90
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
91
|
+
}`;
|
|
92
|
+
var FRAGMENT = `
|
|
93
|
+
precision highp float;
|
|
94
|
+
uniform sampler2D page;
|
|
95
|
+
uniform sampler2D polygon;
|
|
96
|
+
uniform int points;
|
|
97
|
+
uniform bool inverse;
|
|
98
|
+
uniform vec4 light;
|
|
99
|
+
uniform vec3 dark;
|
|
100
|
+
in vec2 vUV;
|
|
101
|
+
in vec2 vPosition;
|
|
102
|
+
out vec4 outColor;
|
|
103
|
+
void main() {
|
|
104
|
+
if (points > 0) {
|
|
105
|
+
bool inside = false;
|
|
106
|
+
vec2 b = texelFetch(polygon, ivec2(points - 1, 0), 0).xy;
|
|
107
|
+
for (int i = 0; i < points; i++) {
|
|
108
|
+
vec2 a = texelFetch(polygon, ivec2(i, 0), 0).xy;
|
|
109
|
+
if ((a.y > vPosition.y) != (b.y > vPosition.y)) {
|
|
110
|
+
float x = (b.x - a.x) * (vPosition.y - a.y) / (b.y - a.y) + a.x;
|
|
111
|
+
if (vPosition.x < x) inside = !inside;
|
|
112
|
+
}
|
|
113
|
+
b = a;
|
|
114
|
+
}
|
|
115
|
+
if (inverse ? inside : !inside) discard;
|
|
116
|
+
}
|
|
117
|
+
vec4 t = texture(page, vUV);
|
|
118
|
+
outColor = vec4((t.rgb * light.rgb + (t.a - t.rgb) * dark) * light.a, t.a * light.a);
|
|
119
|
+
}`;
|
|
120
|
+
var ThreeRenderer = class {
|
|
121
|
+
constructor(root, assets, host) {
|
|
122
|
+
this.root = root;
|
|
123
|
+
this.assets = assets;
|
|
124
|
+
this.host = host;
|
|
125
|
+
if (!root?.isGroup || !assets || !(assets.pageImages instanceof Map) || !isWebGLHost(host)) throw new Error("ThreeRenderer: expected Group, ModelAssets.pageImages Map and WebGLRenderer");
|
|
126
|
+
}
|
|
127
|
+
root;
|
|
128
|
+
assets;
|
|
129
|
+
host;
|
|
130
|
+
positionUploadCount = 0;
|
|
131
|
+
uvUploadCount = 0;
|
|
132
|
+
triangleUploadCount = 0;
|
|
133
|
+
alwaysReuploadGeometry = false;
|
|
134
|
+
/** Negative-control switch; normal disposal always releases retained page textures. */
|
|
135
|
+
releasePageTexturesOnDispose = true;
|
|
136
|
+
disposed = false;
|
|
137
|
+
pool = /* @__PURE__ */ new Map();
|
|
138
|
+
materials = /* @__PURE__ */ new Map();
|
|
139
|
+
retained = /* @__PURE__ */ new Set();
|
|
140
|
+
get geometryUploadCount() {
|
|
141
|
+
return this.positionUploadCount + this.uvUploadCount + this.triangleUploadCount;
|
|
142
|
+
}
|
|
143
|
+
get isDisposed() {
|
|
144
|
+
return this.disposed;
|
|
145
|
+
}
|
|
146
|
+
resourceCounts() {
|
|
147
|
+
return { meshes: this.pool.size, geometries: this.pool.size, materials: this.materials.size, clipTextures: [...this.pool.values()].filter((e) => e.polygon !== null).length, retainedPages: this.retained.size, livePageTextures: livePages.size };
|
|
148
|
+
}
|
|
149
|
+
getPageTexture(name) {
|
|
150
|
+
const image = this.assets.pageImages.get(name);
|
|
151
|
+
return image && this.retained.has(image) ? pages.get(image)?.texture : void 0;
|
|
152
|
+
}
|
|
153
|
+
prepare(slot) {
|
|
154
|
+
const detail = `ThreeRenderer: slot "${slot?.slot ?? "<unnamed>"}" on page "${slot?.page ?? "<unnamed>"}"`;
|
|
155
|
+
const fail = (why) => {
|
|
156
|
+
throw new Error(`${detail}: ${why}`);
|
|
157
|
+
};
|
|
158
|
+
if (!slot || slot.kind !== "region" && slot.kind !== "mesh") fail(`kind=${String(slot && slot.kind)} (expected region or mesh)`);
|
|
159
|
+
if (typeof slot.slot !== "string" || slot.slot.length === 0 || typeof slot.page !== "string" || slot.page.length === 0) fail("expected non-empty slot and page names");
|
|
160
|
+
if (!["Normal", "Additive", "Multiply", "Screen"].includes(slot.blend)) fail(`blend=${slot.blend} (expected Normal, Additive, Multiply, Screen)`);
|
|
161
|
+
if (typeof slot.pma !== "boolean") fail(`pma=${String(slot.pma)} (expected boolean)`);
|
|
162
|
+
if (!Number.isFinite(slot.z)) fail(`z=${String(slot.z)} (expected finite draw-order index)`);
|
|
163
|
+
const image = this.assets.pageImages.get(slot.page);
|
|
164
|
+
if (!image || !(image.naturalWidth > 0 && image.naturalHeight > 0)) fail("page image absent or not decoded (expected positive naturalWidth/naturalHeight)");
|
|
165
|
+
const valid = (values, count, field) => {
|
|
166
|
+
if (!Array.isArray(values) || values.length !== count) fail(`${field} length=${values?.length} (expected ${count})`);
|
|
167
|
+
for (let i = 0; i < count; i++) if (!Number.isFinite(values[i]) || !Number.isFinite(Math.fround(values[i]))) fail(`${field}[${i}]=${String(values[i])} (expected finite float32)`);
|
|
168
|
+
};
|
|
169
|
+
valid(slot.tint, 4, "tint");
|
|
170
|
+
if (slot.dark !== void 0) valid(slot.dark, 3, "dark");
|
|
171
|
+
const coords = slot.kind === "region" ? slot.corners : slot.vertices;
|
|
172
|
+
if (!Array.isArray(coords) || coords.length < 6 || coords.length % 2 !== 0) fail(`positions length=${coords?.length} (expected even length >=6)`);
|
|
173
|
+
valid(coords, slot.kind === "region" ? 8 : coords.length, slot.kind === "region" ? "corners" : "vertices");
|
|
174
|
+
valid(slot.uvs, coords.length, "uvs");
|
|
175
|
+
const indices = slot.kind === "region" ? REGION_INDICES : slot.triangles;
|
|
176
|
+
if (!Array.isArray(indices) || indices.length < 3 || indices.length % 3 !== 0) fail(`triangles length=${indices?.length} (expected multiple of three >=3)`);
|
|
177
|
+
indices.forEach((v, i) => {
|
|
178
|
+
if (!Number.isInteger(v) || v < 0 || v >= coords.length / 2) fail(`triangles[${i}]=${v} (expected integer 0..${coords.length / 2 - 1})`);
|
|
179
|
+
});
|
|
180
|
+
if (slot.kind === "mesh" && !Number.isInteger(slot.sequence)) fail(`sequence=${slot.sequence} (expected integer)`);
|
|
181
|
+
return { slot, image, positions: coords, indices, polygon: slot.clip !== void 0 ? clipPolygon(slot.clip, detail, this.host.capabilities.maxTextureSize) : null };
|
|
182
|
+
}
|
|
183
|
+
material(item) {
|
|
184
|
+
let material = this.materials.get(item.image);
|
|
185
|
+
if (material) return material;
|
|
186
|
+
let page = pages.get(item.image);
|
|
187
|
+
if (!page) {
|
|
188
|
+
const texture = new Texture(item.image);
|
|
189
|
+
texture.flipY = false;
|
|
190
|
+
texture.premultiplyAlpha = !item.slot.pma;
|
|
191
|
+
texture.generateMipmaps = false;
|
|
192
|
+
texture.minFilter = texture.magFilter = LinearFilter;
|
|
193
|
+
texture.colorSpace = NoColorSpace;
|
|
194
|
+
texture.needsUpdate = true;
|
|
195
|
+
page = { texture, pma: item.slot.pma, holders: /* @__PURE__ */ new Set() };
|
|
196
|
+
pages.set(item.image, page);
|
|
197
|
+
livePages.add(texture);
|
|
198
|
+
}
|
|
199
|
+
page.holders.add(this);
|
|
200
|
+
this.retained.add(item.image);
|
|
201
|
+
material = new ShaderMaterial({
|
|
202
|
+
glslVersion: GLSL3,
|
|
203
|
+
vertexShader: VERTEX,
|
|
204
|
+
fragmentShader: FRAGMENT,
|
|
205
|
+
uniforms: { page: { value: page.texture }, polygon: { value: page.texture }, points: { value: 0 }, inverse: { value: false }, light: { value: [1, 1, 1, 1] }, dark: { value: [0, 0, 0] } },
|
|
206
|
+
transparent: true,
|
|
207
|
+
depthWrite: false,
|
|
208
|
+
depthTest: true,
|
|
209
|
+
side: DoubleSide,
|
|
210
|
+
forceSinglePass: true,
|
|
211
|
+
toneMapped: false,
|
|
212
|
+
blending: CustomBlending
|
|
213
|
+
});
|
|
214
|
+
this.materials.set(item.image, material);
|
|
215
|
+
return material;
|
|
216
|
+
}
|
|
217
|
+
render(frame) {
|
|
218
|
+
if (this.disposed) throw new Error("ThreeRenderer: render called after dispose (expected active renderer)");
|
|
219
|
+
if (!frame || !Array.isArray(frame.order)) throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
|
|
220
|
+
const prepared = frame.order.map((s) => this.prepare(s));
|
|
221
|
+
const conventions = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const item of prepared) {
|
|
223
|
+
const pma = conventions.get(item.image) ?? pages.get(item.image)?.pma;
|
|
224
|
+
if (pma !== void 0 && pma !== item.slot.pma) throw new Error(`ThreeRenderer: slot "${item.slot.slot}" page "${item.slot.page}" pma=${item.slot.pma} conflicts with retained pma=${pma} (expected one convention per page image)`);
|
|
225
|
+
conventions.set(item.image, item.slot.pma);
|
|
226
|
+
}
|
|
227
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
228
|
+
const active = [];
|
|
229
|
+
prepared.forEach((item, index) => {
|
|
230
|
+
const occurrence = occurrences.get(item.slot.slot) ?? 0;
|
|
231
|
+
occurrences.set(item.slot.slot, occurrence + 1);
|
|
232
|
+
const key = `${item.slot.slot}\0${occurrence}`;
|
|
233
|
+
let entry = this.pool.get(key);
|
|
234
|
+
if (!entry) {
|
|
235
|
+
const geometry = new BufferGeometry();
|
|
236
|
+
const mesh2 = new Mesh(geometry, this.material(item));
|
|
237
|
+
mesh2.frustumCulled = false;
|
|
238
|
+
mesh2.name = item.slot.slot;
|
|
239
|
+
entry = { mesh: mesh2, polygon: null, polygonValues: [], uvs: null, indices: null };
|
|
240
|
+
this.pool.set(key, entry);
|
|
241
|
+
}
|
|
242
|
+
const { mesh } = entry;
|
|
243
|
+
const oldPositions = mesh.geometry.getAttribute("position"), oldUVs = mesh.geometry.getAttribute("uv"), oldIndex = mesh.geometry.getIndex();
|
|
244
|
+
const needsWideIndex = item.indices.some((v) => v >= 65535);
|
|
245
|
+
if (oldPositions && oldPositions.array.length !== item.positions.length / 2 * 3 || oldUVs && oldUVs.array.length !== item.slot.uvs.length || oldIndex && (oldIndex.count !== item.indices.length || needsWideIndex && !(oldIndex.array instanceof Uint32Array))) {
|
|
246
|
+
mesh.geometry.dispose();
|
|
247
|
+
mesh.geometry = new BufferGeometry();
|
|
248
|
+
entry.uvs = entry.indices = null;
|
|
249
|
+
}
|
|
250
|
+
mesh.posedSlot = item.slot;
|
|
251
|
+
mesh.material = this.material(item);
|
|
252
|
+
mesh.renderOrder = index;
|
|
253
|
+
const update = (name, values, size, force) => {
|
|
254
|
+
let attribute = mesh.geometry.getAttribute(name);
|
|
255
|
+
let changed = force || !attribute || attribute.array.length !== values.length;
|
|
256
|
+
if (!attribute || attribute.array.length !== values.length) {
|
|
257
|
+
attribute = new BufferAttribute(new Float32Array(values.length), size).setUsage(DynamicDrawUsage);
|
|
258
|
+
mesh.geometry.setAttribute(name, attribute);
|
|
259
|
+
}
|
|
260
|
+
const array = attribute.array;
|
|
261
|
+
for (let i = 0; i < values.length; i++) if (array[i] !== Math.fround(values[i])) {
|
|
262
|
+
array[i] = values[i];
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
if (changed) {
|
|
266
|
+
attribute.needsUpdate = true;
|
|
267
|
+
if (name === "position") this.positionUploadCount++;
|
|
268
|
+
else this.uvUploadCount++;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const xyz = [];
|
|
272
|
+
for (let i = 0; i < item.positions.length; i += 2) xyz.push(item.positions[i], item.positions[i + 1], 0);
|
|
273
|
+
update("position", xyz, 3, this.alwaysReuploadGeometry);
|
|
274
|
+
if (this.alwaysReuploadGeometry || entry.uvs !== item.slot.uvs) {
|
|
275
|
+
update("uv", item.slot.uvs, 2, true);
|
|
276
|
+
entry.uvs = item.slot.uvs;
|
|
277
|
+
}
|
|
278
|
+
if (this.alwaysReuploadGeometry || entry.indices !== item.indices) {
|
|
279
|
+
const before = mesh.geometry.getIndex();
|
|
280
|
+
if (before && before.count === item.indices.length) {
|
|
281
|
+
for (let i = 0; i < item.indices.length; i++) before.setX(i, item.indices[i]);
|
|
282
|
+
before.needsUpdate = true;
|
|
283
|
+
} else mesh.geometry.setIndex([...item.indices]);
|
|
284
|
+
this.triangleUploadCount++;
|
|
285
|
+
entry.indices = item.indices;
|
|
286
|
+
}
|
|
287
|
+
if (item.polygon !== null && (entry.polygon === null || item.polygon.length !== entry.polygonValues.length || item.polygon.some((v, i) => v !== entry.polygonValues[i]))) {
|
|
288
|
+
const data = new Float32Array(item.polygon.length * 2);
|
|
289
|
+
for (let i = 0; i < item.polygon.length; i += 2) {
|
|
290
|
+
data[i * 2] = item.polygon[i];
|
|
291
|
+
data[i * 2 + 1] = item.polygon[i + 1];
|
|
292
|
+
}
|
|
293
|
+
entry.polygon?.dispose();
|
|
294
|
+
entry.polygon = new DataTexture(data, item.polygon.length / 2, 1, RGBAFormat, FloatType);
|
|
295
|
+
entry.polygon.minFilter = entry.polygon.magFilter = NearestFilter;
|
|
296
|
+
entry.polygon.needsUpdate = true;
|
|
297
|
+
entry.polygonValues = item.polygon;
|
|
298
|
+
} else if (item.polygon === null && entry.polygon !== null) {
|
|
299
|
+
entry.polygon.dispose();
|
|
300
|
+
entry.polygon = null;
|
|
301
|
+
entry.polygonValues = [];
|
|
302
|
+
}
|
|
303
|
+
const current = entry;
|
|
304
|
+
mesh.onBeforeRender = () => {
|
|
305
|
+
const material = mesh.material, slot = mesh.posedSlot;
|
|
306
|
+
material.uniforms.light.value = slot.tint;
|
|
307
|
+
material.uniforms.dark.value = slot.dark ?? [0, 0, 0];
|
|
308
|
+
material.uniforms.polygon.value = current.polygon ?? material.uniforms.page.value;
|
|
309
|
+
material.uniforms.points.value = current.polygon === null ? 0 : current.polygonValues.length / 2;
|
|
310
|
+
material.uniforms.inverse.value = slot.clip?.inverse ?? false;
|
|
311
|
+
material.blendEquation = material.blendEquationAlpha = AddEquation;
|
|
312
|
+
material.blendSrc = slot.blend === "Multiply" ? DstColorFactor : OneFactor;
|
|
313
|
+
material.blendDst = slot.blend === "Additive" ? OneFactor : slot.blend === "Screen" ? OneMinusSrcColorFactor : OneMinusSrcAlphaFactor;
|
|
314
|
+
material.blendSrcAlpha = OneFactor;
|
|
315
|
+
material.blendDstAlpha = material.blendDst;
|
|
316
|
+
material.uniformsNeedUpdate = true;
|
|
317
|
+
};
|
|
318
|
+
active.push(mesh);
|
|
319
|
+
});
|
|
320
|
+
for (const entry of this.pool.values()) if (entry.mesh.parent === this.root) this.root.remove(entry.mesh);
|
|
321
|
+
for (const mesh of active) this.root.add(mesh);
|
|
322
|
+
}
|
|
323
|
+
dispose() {
|
|
324
|
+
if (this.disposed) return;
|
|
325
|
+
this.disposed = true;
|
|
326
|
+
for (const e of this.pool.values()) {
|
|
327
|
+
e.mesh.removeFromParent();
|
|
328
|
+
e.mesh.geometry.dispose();
|
|
329
|
+
e.polygon?.dispose();
|
|
330
|
+
}
|
|
331
|
+
this.pool.clear();
|
|
332
|
+
for (const material of this.materials.values()) material.dispose();
|
|
333
|
+
this.materials.clear();
|
|
334
|
+
if (this.releasePageTexturesOnDispose) for (const image of this.retained) {
|
|
335
|
+
const page = pages.get(image);
|
|
336
|
+
if (!page) continue;
|
|
337
|
+
page.holders.delete(this);
|
|
338
|
+
if (page.holders.size === 0) {
|
|
339
|
+
page.texture.dispose();
|
|
340
|
+
livePages.delete(page.texture);
|
|
341
|
+
pages.delete(image);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.retained.clear();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
function readMeshScreenVertices(mesh, camera, width, height) {
|
|
348
|
+
mesh.updateWorldMatrix(true, false);
|
|
349
|
+
camera.updateWorldMatrix(true, false);
|
|
350
|
+
const positions = mesh.geometry.getAttribute("position"), result = [];
|
|
351
|
+
for (let i = 0; i < positions.count; i++) {
|
|
352
|
+
const p = new Vector3().fromBufferAttribute(positions, i).applyMatrix4(mesh.matrixWorld).project(camera);
|
|
353
|
+
result.push((p.x + 1) * width / 2, (1 - p.y) * height / 2);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
var ThreeRig = class extends Group {
|
|
358
|
+
constructor(assets, options) {
|
|
359
|
+
super();
|
|
360
|
+
this.assets = assets;
|
|
361
|
+
if (!assets?.document || !(assets.pageImages instanceof Map) || !isWebGLHost(options?.renderer)) throw new Error("ThreeRig: expected ModelAssets and options.renderer WebGLRenderer");
|
|
362
|
+
const scale = options.timescale ?? options.timeScale ?? 1;
|
|
363
|
+
if (!Number.isFinite(scale) || scale < 0) throw new Error(`ThreeRig: timescale=${scale} (expected finite number >=0)`);
|
|
364
|
+
this.player = new RigPlayer(document.createElement("div"), assets, { mode: assets.regions === "none" ? "canvas" : "dom", skin: options.skin, slots: options.slots });
|
|
365
|
+
this.renderer = new ThreeRenderer(this, assets, options.renderer);
|
|
366
|
+
this.timescale = scale;
|
|
367
|
+
try {
|
|
368
|
+
this.lastFrame = this.draw();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.renderer.dispose();
|
|
371
|
+
this.player.dispose();
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
assets;
|
|
376
|
+
player;
|
|
377
|
+
renderer;
|
|
378
|
+
lastFrame;
|
|
379
|
+
destroyed = false;
|
|
380
|
+
active() {
|
|
381
|
+
if (this.destroyed) throw new Error("ThreeRig: called after destroy (expected active rig)");
|
|
382
|
+
}
|
|
383
|
+
play(name, options = {}) {
|
|
384
|
+
this.active();
|
|
385
|
+
this.player.play(name, options);
|
|
386
|
+
this.draw();
|
|
387
|
+
}
|
|
388
|
+
seek(time) {
|
|
389
|
+
this.active();
|
|
390
|
+
if (!Number.isFinite(time)) throw new Error(`ThreeRig: time=${time} (expected finite seconds)`);
|
|
391
|
+
this.player.seek(time);
|
|
392
|
+
this.draw();
|
|
393
|
+
}
|
|
394
|
+
advance(dt) {
|
|
395
|
+
this.active();
|
|
396
|
+
if (!Number.isFinite(dt) || dt < 0) throw new Error(`ThreeRig: dt=${dt} (expected finite seconds >=0)`);
|
|
397
|
+
if (this.timescale === 0) return;
|
|
398
|
+
this.player.advance(dt);
|
|
399
|
+
this.draw();
|
|
400
|
+
}
|
|
401
|
+
update(dt) {
|
|
402
|
+
this.advance(dt);
|
|
403
|
+
}
|
|
404
|
+
draw() {
|
|
405
|
+
this.active();
|
|
406
|
+
const frame = this.player.posedFrame();
|
|
407
|
+
this.renderer.render(frame);
|
|
408
|
+
this.lastFrame = frame;
|
|
409
|
+
return frame;
|
|
410
|
+
}
|
|
411
|
+
drawnFrame() {
|
|
412
|
+
this.active();
|
|
413
|
+
return this.lastFrame;
|
|
414
|
+
}
|
|
415
|
+
posedFrame(slots) {
|
|
416
|
+
this.active();
|
|
417
|
+
return this.player.posedFrame(slots);
|
|
418
|
+
}
|
|
419
|
+
bone(name) {
|
|
420
|
+
this.active();
|
|
421
|
+
return this.player.bone(name);
|
|
422
|
+
}
|
|
423
|
+
bounds() {
|
|
424
|
+
this.active();
|
|
425
|
+
return this.player.bounds();
|
|
426
|
+
}
|
|
427
|
+
get time() {
|
|
428
|
+
return this.player.time;
|
|
429
|
+
}
|
|
430
|
+
get current() {
|
|
431
|
+
return this.player.current;
|
|
432
|
+
}
|
|
433
|
+
get timescale() {
|
|
434
|
+
return this.player.timeScale;
|
|
435
|
+
}
|
|
436
|
+
set timescale(value) {
|
|
437
|
+
this.active();
|
|
438
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`ThreeRig: timescale=${value} (expected finite number >=0)`);
|
|
439
|
+
this.player.timeScale = value;
|
|
440
|
+
}
|
|
441
|
+
get timeScale() {
|
|
442
|
+
return this.timescale;
|
|
443
|
+
}
|
|
444
|
+
set timeScale(value) {
|
|
445
|
+
this.timescale = value;
|
|
446
|
+
}
|
|
447
|
+
resourceCounts() {
|
|
448
|
+
return this.renderer.resourceCounts();
|
|
449
|
+
}
|
|
450
|
+
destroy() {
|
|
451
|
+
if (this.destroyed) return;
|
|
452
|
+
this.destroyed = true;
|
|
453
|
+
this.renderer.dispose();
|
|
454
|
+
this.player.dispose();
|
|
455
|
+
this.removeFromParent();
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
export {
|
|
459
|
+
ThreeRenderer,
|
|
460
|
+
ThreeRig,
|
|
461
|
+
getLivePageTextureCount,
|
|
462
|
+
getPageRefCount,
|
|
463
|
+
getSharedPageTexture,
|
|
464
|
+
readMeshScreenVertices
|
|
465
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/three",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Play rig-c model documents in a native Three.js scene through @rig-c/play.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rig-c/adapters.git",
|
|
10
|
+
"directory": "packages/three"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/rig-c/adapters/tree/main/packages/three#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rig-c/adapters/issues"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"NOTICE"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"three": "^0.186.1",
|
|
31
|
+
"@rig-c/play": "^1.10.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@rig-c/adapters-shared": "workspace:*",
|
|
35
|
+
"@types/three": "0.186.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "bun ../../scripts/build-packages.mjs --package packages/three"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|