@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,213 @@
1
+ /**
2
+ * Lower parsed moc2 ModelImpl → CPU ModelProgram.
3
+ *
4
+ * Samples drawable keyforms at the given parameters (defaults if omitted),
5
+ * then applies the parent affine / mesh deformer chain into canvas space.
6
+ * Blend mode (color composition) and clipId → maskIndices are preserved.
7
+ */
8
+
9
+ import type {
10
+ DrawableProgram,
11
+ ModelProgram,
12
+ ParameterProgram,
13
+ } from "@doki-land/live2d-core";
14
+ import { FrameBlendMode } from "@doki-land/live2d-core";
15
+ import { decodeMoc2ColorComposition } from "./drawable-flags.js";
16
+ import { bakeDeformerOps, transformDrawablePositions } from "./moc2-deform.js";
17
+ import {
18
+ interpolateKeyforms,
19
+ interpolateScalarTable,
20
+ } from "./moc2-keyforms.js";
21
+ import type {
22
+ Moc2DrawableMesh,
23
+ Moc2ModelImpl,
24
+ Moc2ParamDef,
25
+ } from "./moc2-objects.js";
26
+
27
+ function buildParamGetter(
28
+ params: readonly Moc2ParamDef[],
29
+ overrides?: ReadonlyMap<string, number> | ((id: string) => number),
30
+ ): (id: string) => number {
31
+ if (typeof overrides === "function") return overrides;
32
+ const defaults = new Map<string, number>();
33
+ for (const p of params) defaults.set(p.id, p.defaultValue);
34
+ if (overrides) {
35
+ for (const [k, v] of overrides) defaults.set(k, v);
36
+ }
37
+ return (id: string) => defaults.get(id) ?? 0;
38
+ }
39
+
40
+ /** Build a parameter getter from a ModelProgram-aligned value array. */
41
+ export function moc2ParamGetterFromValues(
42
+ params: readonly ParameterProgram[] | readonly Moc2ParamDef[],
43
+ values: ArrayLike<number>,
44
+ ): (id: string) => number {
45
+ const byId = new Map<string, number>();
46
+ for (let i = 0; i < params.length; i++) {
47
+ byId.set(params[i]!.id, values[i] ?? params[i]!.defaultValue);
48
+ }
49
+ return (id: string) => byId.get(id) ?? 0;
50
+ }
51
+
52
+ function normalizePositions(
53
+ positions: Float32Array,
54
+ canvasWidth: number,
55
+ canvasHeight: number,
56
+ ): Float32Array {
57
+ // moc2 draw coords are canvas pixels with origin at the **top-left**
58
+ // (Y increases downward), matching Live2D Cubism 2 canvas space.
59
+ // Map to Y-up NDC so WebGL/WebGPU/Canvas2D share one convention:
60
+ // x: [0, w] → [-1, 1]
61
+ // y: [0, h] → [1, -1] (top of canvas → NDC +Y)
62
+ const w = canvasWidth > 0 ? canvasWidth : 1;
63
+ const h = canvasHeight > 0 ? canvasHeight : 1;
64
+ const out = new Float32Array(positions.length);
65
+ for (let i = 0; i + 1 < positions.length; i += 2) {
66
+ out[i] = (positions[i]! / w) * 2 - 1;
67
+ out[i + 1] = 1 - (positions[i + 1]! / h) * 2;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ interface DraftDrawable {
73
+ id: string;
74
+ textureIndex: number;
75
+ positions: Float32Array;
76
+ uvs: Float32Array;
77
+ indices: Uint16Array;
78
+ opacity: number;
79
+ renderOrder: number;
80
+ blendMode: number;
81
+ clipId: string | null;
82
+ visible: boolean;
83
+ }
84
+
85
+ function lowerDrawable(
86
+ mesh: Moc2DrawableMesh,
87
+ getParam: (id: string) => number,
88
+ ops: ReturnType<typeof bakeDeformerOps>,
89
+ canvasWidth: number,
90
+ canvasHeight: number,
91
+ partVisible: boolean,
92
+ ): DraftDrawable {
93
+ const floatCount = mesh.numPoints * 2;
94
+ const local = interpolateKeyforms(
95
+ mesh.keyforms,
96
+ mesh.pivotManager,
97
+ getParam,
98
+ floatCount,
99
+ );
100
+ const world = transformDrawablePositions(mesh, local, ops);
101
+ const positions = normalizePositions(world, canvasWidth, canvasHeight);
102
+ const opacity = interpolateScalarTable(
103
+ mesh.opacities,
104
+ mesh.pivotManager,
105
+ getParam,
106
+ 1,
107
+ );
108
+ const renderOrder = Math.round(
109
+ interpolateScalarTable(
110
+ mesh.drawOrders,
111
+ mesh.pivotManager,
112
+ getParam,
113
+ mesh.averageDrawOrder,
114
+ ),
115
+ );
116
+
117
+ const blendMode =
118
+ (mesh.optionFlags & 1) !== 0
119
+ ? decodeMoc2ColorComposition(mesh.colorComposition)
120
+ : FrameBlendMode.Normal;
121
+
122
+ return {
123
+ id: mesh.id,
124
+ textureIndex: mesh.textureIndex,
125
+ positions,
126
+ uvs: new Float32Array(mesh.uvs),
127
+ indices: new Uint16Array(mesh.indices),
128
+ opacity,
129
+ renderOrder,
130
+ blendMode,
131
+ clipId: mesh.clipId,
132
+ visible: partVisible,
133
+ };
134
+ }
135
+
136
+ export interface Moc2ToProgramOptions {
137
+ /** Override parameter values; defaults to each param's defaultValue. */
138
+ getParam?: (id: string) => number;
139
+ }
140
+
141
+ /** Convert a parsed moc2 model into a CPU ModelProgram at the given pose. */
142
+ export function moc2ModelToProgram(
143
+ model: Moc2ModelImpl,
144
+ options: Moc2ToProgramOptions = {},
145
+ ): ModelProgram {
146
+ const paramDefs = model.paramDefSet?.params ?? [];
147
+ const parameters: ParameterProgram[] = paramDefs.map((p) => ({
148
+ id: p.id,
149
+ min: p.min,
150
+ max: p.max,
151
+ defaultValue: p.defaultValue,
152
+ }));
153
+ const getParam = options.getParam ?? buildParamGetter(paramDefs);
154
+ const ops = bakeDeformerOps(model, getParam);
155
+
156
+ const drafts: DraftDrawable[] = [];
157
+ for (const part of model.parts) {
158
+ for (const mesh of part.drawData) {
159
+ drafts.push(
160
+ lowerDrawable(
161
+ mesh,
162
+ getParam,
163
+ ops,
164
+ model.canvasWidth,
165
+ model.canvasHeight,
166
+ part.visible,
167
+ ),
168
+ );
169
+ }
170
+ }
171
+
172
+ drafts.sort((a, b) => a.renderOrder - b.renderOrder);
173
+
174
+ const idToIndex = new Map<string, number>();
175
+ for (let i = 0; i < drafts.length; i++) {
176
+ idToIndex.set(drafts[i]!.id, i);
177
+ }
178
+
179
+ const drawables: DrawableProgram[] = drafts.map((d, index) => {
180
+ const maskIndices: number[] = [];
181
+ if (d.clipId) {
182
+ // clipId may be a single id or comma-separated list (Cubism 2).
183
+ for (const raw of d.clipId.split(",")) {
184
+ const id = raw.trim();
185
+ if (!id) continue;
186
+ const mapped = idToIndex.get(id);
187
+ if (mapped !== undefined) maskIndices.push(mapped);
188
+ }
189
+ }
190
+ return {
191
+ index,
192
+ textureIndex: d.textureIndex,
193
+ positions: d.positions,
194
+ uvs: d.uvs,
195
+ indices: d.indices,
196
+ opacity: d.opacity,
197
+ renderOrder: d.renderOrder,
198
+ blendMode: d.blendMode,
199
+ invertedMask: false,
200
+ maskIndices,
201
+ visible: d.visible,
202
+ deformParamIndex: -1,
203
+ deformDeltas: null,
204
+ };
205
+ });
206
+
207
+ return {
208
+ format: "moc2",
209
+ codec: "moc2",
210
+ parameters,
211
+ drawables,
212
+ };
213
+ }
@@ -0,0 +1,184 @@
1
+ import type {
2
+ FrameDrawable,
3
+ FrameSnapshot,
4
+ InternalModel,
5
+ ModelInstance,
6
+ ModelSettings,
7
+ } from "@doki-land/live2d-core";
8
+ import { detectModelSettingsFormat } from "@doki-land/live2d-core";
9
+ import {
10
+ createModelInstance,
11
+ evaluateFrame,
12
+ setParameterValue,
13
+ } from "../cpu/evaluate.js";
14
+ import type {
15
+ ModelBackend,
16
+ ModelBackendOptions,
17
+ ParameterBinding,
18
+ } from "../model-runtime.js";
19
+ import type { BlendMode, DrawableMesh } from "../types.js";
20
+ import { type Moc2ModelImpl, Moc2Parser } from "./moc2-objects.js";
21
+ import {
22
+ moc2ModelToProgram,
23
+ moc2ParamGetterFromValues,
24
+ } from "./moc2-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 Moc2State {
52
+ moc: Moc2ModelImpl;
53
+ instance: ModelInstance;
54
+ lastFrame: FrameSnapshot | null;
55
+ bakedFingerprint: string;
56
+ }
57
+
58
+ const stateByModel = new WeakMap<InternalModel, Moc2State>();
59
+
60
+ /** Re-bake moc2 geometry for current parameter values. */
61
+ function bakePose(state: Moc2State): void {
62
+ const values = Float32Array.from(state.instance.parameterValues);
63
+ const timeSeconds = state.instance.timeSeconds;
64
+ const fp = paramFingerprint(values);
65
+ if (fp === state.bakedFingerprint && state.lastFrame) {
66
+ return;
67
+ }
68
+
69
+ const program = moc2ModelToProgram(state.moc, {
70
+ getParam: moc2ParamGetterFromValues(
71
+ state.moc.paramDefSet.params,
72
+ values,
73
+ ),
74
+ });
75
+ const next = createModelInstance(program);
76
+ next.parameterValues.set(values);
77
+ next.timeSeconds = timeSeconds;
78
+ state.instance = next;
79
+ state.bakedFingerprint = fp;
80
+ state.lastFrame = evaluateFrame(next);
81
+ }
82
+
83
+ /** moc2 (`.moc`) model backend — pure-TS decode → CPU evaluate. */
84
+ export class Moc2Backend implements ModelBackend {
85
+ readonly format = "moc2" as const;
86
+
87
+ canHandle(json: unknown): boolean {
88
+ return detectModelSettingsFormat(json) === "moc2";
89
+ }
90
+
91
+ async createModel(
92
+ settings: ModelSettings,
93
+ options?: ModelBackendOptions,
94
+ ): Promise<InternalModel> {
95
+ let bytes = options?.mocBytes;
96
+ if (!bytes) {
97
+ if (!options?.resolver) {
98
+ throw new Error(
99
+ "@doki-land/live2d-renderer: Moc2Backend.createModel requires resolver or mocBytes",
100
+ );
101
+ }
102
+ bytes = await options.resolver.fetchBytes(settings.moc);
103
+ }
104
+
105
+ const moc = new Moc2Parser(bytes).parseModel();
106
+ const program = moc2ModelToProgram(moc);
107
+ const instance = createModelInstance(program);
108
+ const model: InternalModel = {
109
+ id: settings.name ?? settings.url,
110
+ settings,
111
+ format: "moc2",
112
+ };
113
+ stateByModel.set(model, {
114
+ moc,
115
+ instance,
116
+ lastFrame: null,
117
+ bakedFingerprint: paramFingerprint(instance.parameterValues),
118
+ });
119
+ return model;
120
+ }
121
+
122
+ updateModel(model: InternalModel, deltaTimeSeconds: number): void {
123
+ const state = stateByModel.get(model);
124
+ if (!state) return;
125
+ state.instance.timeSeconds += deltaTimeSeconds;
126
+ bakePose(state);
127
+ if (state.lastFrame) {
128
+ state.lastFrame = {
129
+ ...state.lastFrame,
130
+ timeSeconds: state.instance.timeSeconds,
131
+ };
132
+ }
133
+ }
134
+
135
+ getDrawables(model: InternalModel): DrawableMesh[] {
136
+ const state = stateByModel.get(model);
137
+ if (!state) return [];
138
+ bakePose(state);
139
+ const frame = state.lastFrame ?? evaluateFrame(state.instance);
140
+ state.lastFrame = frame;
141
+ return frame.drawables.map(toDrawableMesh);
142
+ }
143
+
144
+ captureFrame(model: InternalModel): FrameSnapshot | null {
145
+ const state = stateByModel.get(model);
146
+ if (!state) return null;
147
+ bakePose(state);
148
+ const frame = state.lastFrame ?? evaluateFrame(state.instance);
149
+ state.lastFrame = frame;
150
+ return frame;
151
+ }
152
+
153
+ setParameter(model: InternalModel, id: string, value: number): void {
154
+ const state = stateByModel.get(model);
155
+ if (!state) return;
156
+ setParameterValue(state.instance, id, value);
157
+ state.lastFrame = null;
158
+ state.bakedFingerprint = "";
159
+ }
160
+
161
+ listParameters(model: InternalModel): readonly ParameterBinding[] {
162
+ const state = stateByModel.get(model);
163
+ if (!state) return [];
164
+ return state.instance.program.parameters.map((p, i) => ({
165
+ id: p.id,
166
+ min: p.min,
167
+ max: p.max,
168
+ defaultValue: p.defaultValue,
169
+ value: state.instance.parameterValues[i] ?? p.defaultValue,
170
+ }));
171
+ }
172
+
173
+ hitTest(_model: InternalModel, _x: number, _y: number): string | null {
174
+ return null;
175
+ }
176
+
177
+ destroyModel(model: InternalModel): void {
178
+ stateByModel.delete(model);
179
+ }
180
+ }
181
+
182
+ export function createMoc2Backend(): Moc2Backend {
183
+ return new Moc2Backend();
184
+ }