@doki-land/live2d-renderer 0.0.0 → 0.0.12

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.
@@ -0,0 +1,222 @@
1
+ import type {
2
+ FrameDrawable,
3
+ FrameSnapshot,
4
+ InternalModel,
5
+ ModelInstance,
6
+ ModelProgram,
7
+ ModelSettings,
8
+ } from "@doki-land/live2d-core";
9
+ import { detectModelSettingsFormat } from "@doki-land/live2d-core";
10
+ import { isCpuProgramBytes, parseCpuProgram } from "../cpu/cpu-program.js";
11
+ import {
12
+ createModelInstance,
13
+ evaluateFrame,
14
+ setParameterValue,
15
+ } from "../cpu/evaluate.js";
16
+ import type {
17
+ ModelBackend,
18
+ ModelBackendOptions,
19
+ ParameterBinding,
20
+ } from "../model-runtime.js";
21
+ import type { BlendMode, DrawableMesh } from "../types.js";
22
+ import { cascadedPartOpacity, readMoc3PartTables } from "./moc3-parts.js";
23
+ import { type Moc3Document, parseMoc3Document } from "./moc3-reader.js";
24
+ import { moc3DocumentToProgram } from "./moc3-to-program.js";
25
+
26
+ function toDrawableMesh(d: FrameDrawable): DrawableMesh {
27
+ return {
28
+ index: d.index,
29
+ textureIndex: d.textureIndex,
30
+ vertexPositions: d.positions,
31
+ uvs: d.uvs,
32
+ indices: d.indices,
33
+ opacity: d.opacity,
34
+ blendMode: d.blendMode as BlendMode,
35
+ invertedMask: d.invertedMask,
36
+ renderOrder: d.renderOrder,
37
+ dynamicFlag: true,
38
+ maskIndices: [...d.maskIndices],
39
+ visible: d.visible,
40
+ };
41
+ }
42
+
43
+ function paramFingerprint(values: ArrayLike<number>): string {
44
+ let s = "";
45
+ for (let i = 0; i < values.length; i++) {
46
+ s += `${values[i]?.toFixed(5)},`;
47
+ }
48
+ return s;
49
+ }
50
+
51
+ interface Moc3State {
52
+ /** Present for official MOC3; null for cpu-program fixtures. */
53
+ doc: Moc3Document | null;
54
+ instance: ModelInstance;
55
+ lastFrame: FrameSnapshot | null;
56
+ bakedFingerprint: string;
57
+ /** Runtime PartOpacity overrides (id → 0..1). */
58
+ partOpacity: Map<string, number>;
59
+ }
60
+
61
+ const stateByModel = new WeakMap<InternalModel, Moc3State>();
62
+
63
+ function bakePose(state: Moc3State): void {
64
+ if (!state.doc) {
65
+ state.lastFrame = evaluateFrame(state.instance);
66
+ return;
67
+ }
68
+
69
+ const values = Float32Array.from(state.instance.parameterValues);
70
+ const timeSeconds = state.instance.timeSeconds;
71
+ const fp = paramFingerprint(values);
72
+ if (fp === state.bakedFingerprint && state.lastFrame) return;
73
+
74
+ const program = moc3DocumentToProgram(state.doc, {
75
+ getParamByIndex: (i) => values[i] ?? 0,
76
+ });
77
+ const next = createModelInstance(program);
78
+ next.parameterValues.set(values);
79
+ next.timeSeconds = timeSeconds;
80
+ state.instance = next;
81
+ state.bakedFingerprint = fp;
82
+ state.lastFrame = evaluateFrame(next);
83
+ }
84
+
85
+ /** moc3 model backend — official `.moc3` or CPU `.program.json` fixture. */
86
+ export class Moc3Backend implements ModelBackend {
87
+ readonly format = "moc3" as const;
88
+
89
+ canHandle(json: unknown): boolean {
90
+ return detectModelSettingsFormat(json) === "moc3";
91
+ }
92
+
93
+ async createModel(
94
+ settings: ModelSettings,
95
+ options?: ModelBackendOptions,
96
+ ): Promise<InternalModel> {
97
+ let bytes = options?.mocBytes;
98
+ if (!bytes) {
99
+ if (!options?.resolver) {
100
+ throw new Error(
101
+ "@doki-land/live2d-renderer: Moc3Backend.createModel requires resolver or mocBytes",
102
+ );
103
+ }
104
+ bytes = await options.resolver.fetchBytes(settings.moc);
105
+ }
106
+
107
+ const isCpu =
108
+ settings.moc.toLowerCase().endsWith(".program.json") ||
109
+ isCpuProgramBytes(bytes);
110
+
111
+ let doc: Moc3Document | null = null;
112
+ let program: ModelProgram;
113
+ if (isCpu) {
114
+ program = parseCpuProgram(bytes);
115
+ } else {
116
+ doc = parseMoc3Document(bytes);
117
+ program = moc3DocumentToProgram(doc);
118
+ }
119
+
120
+ const instance = createModelInstance(program);
121
+ const model: InternalModel = {
122
+ id: settings.name ?? settings.url,
123
+ settings,
124
+ format: "moc3",
125
+ };
126
+ stateByModel.set(model, {
127
+ doc,
128
+ instance,
129
+ lastFrame: null,
130
+ bakedFingerprint: paramFingerprint(instance.parameterValues),
131
+ partOpacity: new Map(),
132
+ });
133
+ return model;
134
+ }
135
+
136
+ updateModel(model: InternalModel, deltaTimeSeconds: number): void {
137
+ const state = stateByModel.get(model);
138
+ if (!state) return;
139
+ state.instance.timeSeconds += deltaTimeSeconds;
140
+ bakePose(state);
141
+ if (state.lastFrame) {
142
+ state.lastFrame = {
143
+ ...state.lastFrame,
144
+ timeSeconds: state.instance.timeSeconds,
145
+ };
146
+ }
147
+ }
148
+
149
+ getDrawables(model: InternalModel): DrawableMesh[] {
150
+ const state = stateByModel.get(model);
151
+ if (!state) return [];
152
+ bakePose(state);
153
+ const frame = state.lastFrame ?? evaluateFrame(state.instance);
154
+ state.lastFrame = frame;
155
+ const tables = state.doc ? readMoc3PartTables(state.doc) : null;
156
+ return frame.drawables.map((d) => {
157
+ const mesh = toDrawableMesh(d);
158
+ if (!tables || state.partOpacity.size === 0) return mesh;
159
+ const mul = cascadedPartOpacity(tables, d.index, state.partOpacity);
160
+ return { ...mesh, opacity: mesh.opacity * mul };
161
+ });
162
+ }
163
+
164
+ captureFrame(model: InternalModel): FrameSnapshot | null {
165
+ const state = stateByModel.get(model);
166
+ if (!state) return null;
167
+ bakePose(state);
168
+ const frame = state.lastFrame ?? evaluateFrame(state.instance);
169
+ state.lastFrame = frame;
170
+ const tables = state.doc ? readMoc3PartTables(state.doc) : null;
171
+ if (!tables || state.partOpacity.size === 0) return frame;
172
+ return {
173
+ ...frame,
174
+ drawables: frame.drawables.map((d) => ({
175
+ ...d,
176
+ opacity:
177
+ d.opacity *
178
+ cascadedPartOpacity(tables, d.index, state.partOpacity),
179
+ })),
180
+ };
181
+ }
182
+
183
+ setParameter(model: InternalModel, id: string, value: number): void {
184
+ const state = stateByModel.get(model);
185
+ if (!state) return;
186
+ setParameterValue(state.instance, id, value);
187
+ state.lastFrame = null;
188
+ state.bakedFingerprint = "";
189
+ }
190
+
191
+ setPartOpacity(model: InternalModel, id: string, value: number): void {
192
+ const state = stateByModel.get(model);
193
+ if (!state) return;
194
+ const v = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
195
+ state.partOpacity.set(id, v);
196
+ // Opacity is applied in getDrawables; no need to rebake deform.
197
+ }
198
+
199
+ listParameters(model: InternalModel): readonly ParameterBinding[] {
200
+ const state = stateByModel.get(model);
201
+ if (!state) return [];
202
+ return state.instance.program.parameters.map((p, i) => ({
203
+ id: p.id,
204
+ min: p.min,
205
+ max: p.max,
206
+ defaultValue: p.defaultValue,
207
+ value: state.instance.parameterValues[i] ?? p.defaultValue,
208
+ }));
209
+ }
210
+
211
+ hitTest(_model: InternalModel, _x: number, _y: number): string | null {
212
+ return null;
213
+ }
214
+
215
+ destroyModel(model: InternalModel): void {
216
+ stateByModel.delete(model);
217
+ }
218
+ }
219
+
220
+ export function createMoc3Backend(): Moc3Backend {
221
+ return new Moc3Backend();
222
+ }
@@ -0,0 +1,73 @@
1
+ import type {
2
+ AssetResolver,
3
+ FrameSnapshot,
4
+ InternalModel,
5
+ ModelFormat,
6
+ ModelSettings,
7
+ } from "@doki-land/live2d-core";
8
+ import type { DrawableMesh, Renderer } from "./types.js";
9
+
10
+ /**
11
+ * Model-format runtime contract (moc2 / moc3 / cpu-program).
12
+ * Not a graphics backend — those live under `backends/`.
13
+ */
14
+ export interface ModelBackendOptions {
15
+ /** Bound graphics renderer, if the runtime needs GPU handles. */
16
+ renderer?: Renderer | null;
17
+ /** Asset resolver for moc / textures. */
18
+ resolver?: AssetResolver;
19
+ /** Preloaded moc bytes (tests). */
20
+ mocBytes?: ArrayBuffer;
21
+ }
22
+
23
+ /** Live parameter binding for inspectors / playground. */
24
+ export interface ParameterBinding {
25
+ readonly id: string;
26
+ readonly min: number;
27
+ readonly max: number;
28
+ readonly defaultValue: number;
29
+ readonly value: number;
30
+ }
31
+
32
+ /** moc2 / moc3 model runtime. */
33
+ export interface ModelBackend {
34
+ readonly format: ModelFormat;
35
+
36
+ canHandle(json: unknown): boolean;
37
+
38
+ createModel(
39
+ settings: ModelSettings,
40
+ options?: ModelBackendOptions,
41
+ ): Promise<InternalModel>;
42
+
43
+ updateModel(model: InternalModel, deltaTimeSeconds: number): void;
44
+
45
+ getDrawables(model: InternalModel): DrawableMesh[];
46
+
47
+ hitTest(model: InternalModel, x: number, y: number): string | null;
48
+
49
+ destroyModel(model: InternalModel): void;
50
+
51
+ /** Optional CPU snapshot (moc3 CPU path). */
52
+ captureFrame?(model: InternalModel): FrameSnapshot | null;
53
+
54
+ setParameter?(model: InternalModel, id: string, value: number): void;
55
+
56
+ /** Optional PartOpacity override (moc3 parts / pose / motion). */
57
+ setPartOpacity?(model: InternalModel, id: string, value: number): void;
58
+
59
+ listParameters?(model: InternalModel): readonly ParameterBinding[];
60
+ }
61
+
62
+ export function selectModelBackend(
63
+ backends: ModelBackend[],
64
+ json: unknown,
65
+ ): ModelBackend {
66
+ const hit = backends.find((b) => b.canHandle(json));
67
+ if (!hit) {
68
+ throw new Error(
69
+ "@doki-land/live2d-renderer: no ModelBackend can handle this model JSON",
70
+ );
71
+ }
72
+ return hit;
73
+ }
@@ -0,0 +1,33 @@
1
+ /** Shared untextured preview colors (Canvas2D / WebGL2 / WebGPU parity). */
2
+
3
+ export const PREVIEW_FILL = {
4
+ r: 0x5b / 255,
5
+ g: 0x8d / 255,
6
+ b: 0xef / 255,
7
+ a: 1,
8
+ } as const;
9
+
10
+ export const PREVIEW_STROKE = {
11
+ r: 0x1b / 255,
12
+ g: 0x3a / 255,
13
+ b: 0x6b / 255,
14
+ a: 1,
15
+ } as const;
16
+
17
+ /** Expand triangle indices into a line-list (each edge twice-wound). */
18
+ export function triangleEdgesToLineList(indices: Uint16Array): Uint16Array {
19
+ const out = new Uint16Array(Math.floor(indices.length / 3) * 6);
20
+ let o = 0;
21
+ for (let i = 0; i + 2 < indices.length; i += 3) {
22
+ const a = indices[i]!;
23
+ const b = indices[i + 1]!;
24
+ const c = indices[i + 2]!;
25
+ out[o++] = a;
26
+ out[o++] = b;
27
+ out[o++] = b;
28
+ out[o++] = c;
29
+ out[o++] = c;
30
+ out[o++] = a;
31
+ }
32
+ return out;
33
+ }
package/src/types.ts ADDED
@@ -0,0 +1,71 @@
1
+ /** Implemented renderer kinds. */
2
+ export type RendererKind = "webgpu" | "webgl2" | "canvas2d";
3
+
4
+ /** Blend modes for Live2D drawable compositing. */
5
+ export const BlendMode = {
6
+ Normal: 0,
7
+ Additive: 1,
8
+ Multiplicative: 2,
9
+ } as const;
10
+
11
+ export type BlendMode = (typeof BlendMode)[keyof typeof BlendMode];
12
+
13
+ /** A single drawable mesh submitted to the GPU backend. */
14
+ export interface DrawableMesh {
15
+ index: number;
16
+ textureIndex: number;
17
+ vertexPositions: Float32Array;
18
+ uvs: Float32Array;
19
+ indices: Uint16Array;
20
+ opacity: number;
21
+ blendMode: BlendMode;
22
+ invertedMask: boolean;
23
+ renderOrder: number;
24
+ dynamicFlag: boolean;
25
+ maskIndices: number[];
26
+ visible: boolean;
27
+ }
28
+
29
+ export interface TextureData {
30
+ index: number;
31
+ image: HTMLImageElement | ImageBitmap;
32
+ width: number;
33
+ height: number;
34
+ }
35
+
36
+ export interface ModelDrawPass {
37
+ setTextures(textures: TextureData[]): void;
38
+
39
+ draw(drawables: DrawableMesh[], modelMatrix: Float32Array): void;
40
+
41
+ destroy(): void;
42
+ }
43
+
44
+ /** Top-level renderer bound to a canvas. */
45
+ export interface Renderer {
46
+ readonly kind: RendererKind;
47
+
48
+ initialize(canvas: HTMLCanvasElement): Promise<void>;
49
+
50
+ createModelDrawPass(): ModelDrawPass;
51
+
52
+ beginFrame(): void;
53
+
54
+ endFrame(): void;
55
+
56
+ resize(width: number, height: number): void;
57
+
58
+ destroy(): void;
59
+ }
60
+
61
+ export interface WebGpuRenderer extends Renderer {
62
+ readonly kind: "webgpu";
63
+
64
+ getDevice(): GPUDevice | null;
65
+ }
66
+
67
+ export interface WebGl2Renderer extends Renderer {
68
+ readonly kind: "webgl2";
69
+
70
+ getGL(): WebGL2RenderingContext | null;
71
+ }