@doki-land/live2d-renderer 0.0.0 → 0.0.11

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,215 @@
1
+ /**
2
+ * Parse official MOC3 bytes into typed section tables (no deform evaluation).
3
+ */
4
+
5
+ import {
6
+ MOC3_COUNT_MAX,
7
+ MOC3_HEADER_SIZE,
8
+ MOC3_MAGIC,
9
+ MOC3_SECTION_LAYOUT,
10
+ MOC3_SOT_COUNT,
11
+ type SectionEntry,
12
+ } from "./moc3-layout.js";
13
+
14
+ export interface Moc3CanvasInfo {
15
+ readonly pixelsPerUnit: number;
16
+ readonly originX: number;
17
+ readonly originY: number;
18
+ readonly canvasWidth: number;
19
+ readonly canvasHeight: number;
20
+ readonly flags: number;
21
+ }
22
+
23
+ export interface Moc3Document {
24
+ readonly version: number;
25
+ readonly littleEndian: boolean;
26
+ readonly counts: Int32Array;
27
+ readonly canvas: Moc3CanvasInfo;
28
+ readonly sections: ReadonlyMap<string, unknown>;
29
+ }
30
+
31
+ function readMagic(bytes: ArrayBuffer): string {
32
+ if (bytes.byteLength < 4) return "";
33
+ return String.fromCharCode(...new Uint8Array(bytes, 0, 4));
34
+ }
35
+
36
+ function sectionCount(counts: Int32Array, entry: SectionEntry): number {
37
+ if (entry.countIdx < 0 || entry.countIdx >= counts.length) return 0;
38
+ return Math.max(0, counts[entry.countIdx]!);
39
+ }
40
+
41
+ function readI32Array(
42
+ view: DataView,
43
+ offset: number,
44
+ count: number,
45
+ le: boolean,
46
+ ): Int32Array {
47
+ const out = new Int32Array(count);
48
+ for (let i = 0; i < count; i++) {
49
+ out[i] = view.getInt32(offset + i * 4, le);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function readF32Array(
55
+ view: DataView,
56
+ offset: number,
57
+ count: number,
58
+ le: boolean,
59
+ ): Float32Array {
60
+ const out = new Float32Array(count);
61
+ for (let i = 0; i < count; i++) {
62
+ out[i] = view.getFloat32(offset + i * 4, le);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function readI16Array(
68
+ view: DataView,
69
+ offset: number,
70
+ count: number,
71
+ le: boolean,
72
+ ): Int16Array {
73
+ const out = new Int16Array(count);
74
+ for (let i = 0; i < count; i++) {
75
+ out[i] = view.getInt16(offset + i * 2, le);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function readStr64Array(
81
+ bytes: Uint8Array,
82
+ offset: number,
83
+ count: number,
84
+ ): string[] {
85
+ const out: string[] = [];
86
+ for (let i = 0; i < count; i++) {
87
+ const start = offset + i * 64;
88
+ const slice = bytes.subarray(start, start + 64);
89
+ let end = slice.indexOf(0);
90
+ if (end < 0) end = 64;
91
+ out.push(String.fromCharCode(...slice.subarray(0, end)));
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function readSection(
97
+ bytes: Uint8Array,
98
+ view: DataView,
99
+ offset: number,
100
+ entry: SectionEntry,
101
+ count: number,
102
+ le: boolean,
103
+ ): unknown {
104
+ if (count <= 0 || offset <= 0 || offset >= bytes.byteLength) {
105
+ return entry.elemType === "str64" ? [] : new Int32Array(0);
106
+ }
107
+ switch (entry.elemType) {
108
+ case "runtime":
109
+ return bytes.subarray(offset, offset + count * 8);
110
+ case "str64":
111
+ return readStr64Array(bytes, offset, count);
112
+ case "i32":
113
+ case "bool":
114
+ return readI32Array(view, offset, count, le);
115
+ case "f32":
116
+ return readF32Array(view, offset, count, le);
117
+ case "i16":
118
+ return readI16Array(view, offset, count, le);
119
+ case "u8":
120
+ return bytes.subarray(offset, offset + count);
121
+ default:
122
+ return new Int32Array(0);
123
+ }
124
+ }
125
+
126
+ /** Parse MOC3 binary into section tables. */
127
+ export function parseMoc3Document(buffer: ArrayBuffer): Moc3Document {
128
+ if (buffer.byteLength < MOC3_HEADER_SIZE + MOC3_SOT_COUNT * 4) {
129
+ throw new Error("@doki-land/live2d-renderer: truncated MOC3 header");
130
+ }
131
+ const magic = readMagic(buffer);
132
+ if (magic !== MOC3_MAGIC) {
133
+ throw new Error(
134
+ `@doki-land/live2d-renderer: unrecognized moc3 bytes (magic=${JSON.stringify(magic)})`,
135
+ );
136
+ }
137
+
138
+ const bytes = new Uint8Array(buffer);
139
+ const version = bytes[4]!;
140
+ const littleEndian = bytes[5] === 0;
141
+ const view = new DataView(buffer);
142
+
143
+ const sot = new Int32Array(MOC3_SOT_COUNT);
144
+ for (let i = 0; i < MOC3_SOT_COUNT; i++) {
145
+ sot[i] = view.getInt32(MOC3_HEADER_SIZE + i * 4, littleEndian);
146
+ }
147
+
148
+ const countInfoOff = sot[0]!;
149
+ if (
150
+ countInfoOff <= 0 ||
151
+ countInfoOff + MOC3_COUNT_MAX * 4 > buffer.byteLength
152
+ ) {
153
+ throw new Error(
154
+ "@doki-land/live2d-renderer: invalid MOC3 count info offset",
155
+ );
156
+ }
157
+ const counts = readI32Array(
158
+ view,
159
+ countInfoOff,
160
+ MOC3_COUNT_MAX,
161
+ littleEndian,
162
+ );
163
+
164
+ const canvasOff = sot[1]!;
165
+ if (canvasOff <= 0 || canvasOff + 24 > buffer.byteLength) {
166
+ throw new Error(
167
+ "@doki-land/live2d-renderer: invalid MOC3 canvas info offset",
168
+ );
169
+ }
170
+ const canvas: Moc3CanvasInfo = {
171
+ pixelsPerUnit: view.getFloat32(canvasOff, littleEndian),
172
+ originX: view.getFloat32(canvasOff + 4, littleEndian),
173
+ originY: view.getFloat32(canvasOff + 8, littleEndian),
174
+ canvasWidth: view.getFloat32(canvasOff + 12, littleEndian),
175
+ canvasHeight: view.getFloat32(canvasOff + 16, littleEndian),
176
+ flags: bytes[canvasOff + 20]!,
177
+ };
178
+
179
+ const sections = new Map<string, unknown>();
180
+ for (let i = 0; i < MOC3_SECTION_LAYOUT.length; i++) {
181
+ const entry = MOC3_SECTION_LAYOUT[i]!;
182
+ const sotIdx = i + 2;
183
+ const offset = sotIdx < sot.length ? sot[sotIdx]! : 0;
184
+ const count = sectionCount(counts, entry);
185
+ sections.set(
186
+ entry.name,
187
+ readSection(bytes, view, offset, entry, count, littleEndian),
188
+ );
189
+ }
190
+
191
+ return { version, littleEndian, counts, canvas, sections };
192
+ }
193
+
194
+ export function moc3SectionI32(doc: Moc3Document, name: string): Int32Array {
195
+ const v = doc.sections.get(name);
196
+ return v instanceof Int32Array ? v : new Int32Array(0);
197
+ }
198
+
199
+ export function moc3SectionF32(doc: Moc3Document, name: string): Float32Array {
200
+ const v = doc.sections.get(name);
201
+ return v instanceof Float32Array ? v : new Float32Array(0);
202
+ }
203
+
204
+ export function moc3SectionI16(doc: Moc3Document, name: string): Int16Array {
205
+ const v = doc.sections.get(name);
206
+ return v instanceof Int16Array ? v : new Int16Array(0);
207
+ }
208
+
209
+ export function moc3SectionStrings(
210
+ doc: Moc3Document,
211
+ name: string,
212
+ ): readonly string[] {
213
+ const v = doc.sections.get(name);
214
+ return Array.isArray(v) ? (v as string[]) : [];
215
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Lower parsed MOC3 document → ModelProgram.
3
+ *
4
+ * Samples art-mesh keyforms at the given parameters, applies the parent
5
+ * warp / rotation deformer chain, then moc3 Glue (seam pull), then normalize.
6
+ * Constant flags (blend / invert-mask) and drawable mask tables are preserved.
7
+ */
8
+
9
+ import type {
10
+ DrawableProgram,
11
+ ModelProgram,
12
+ ParameterProgram,
13
+ } from "@doki-land/live2d-core";
14
+ import { decodeMoc3DrawableFlags } from "./drawable-flags.js";
15
+ import {
16
+ applyParentToPoints,
17
+ bakeMoc3Deformers,
18
+ loadMoc3KeyTables,
19
+ } from "./moc3-deform.js";
20
+ import { applyMoc3Glues, loadMoc3Glues } from "./moc3-glue.js";
21
+ import {
22
+ blendKeyformFloats,
23
+ blendKeyformScalar,
24
+ resolveMoc3KeyformBlend,
25
+ } from "./moc3-keyforms.js";
26
+ import { CountIdx } from "./moc3-layout.js";
27
+ import {
28
+ type Moc3Document,
29
+ moc3SectionF32,
30
+ moc3SectionI16,
31
+ moc3SectionI32,
32
+ moc3SectionStrings,
33
+ } from "./moc3-reader.js";
34
+
35
+ function normalizePositions(
36
+ positions: Float32Array,
37
+ canvasWidth: number,
38
+ canvasHeight: number,
39
+ pixelsPerUnit: number,
40
+ ): Float32Array {
41
+ // Cubism deformer output is in logical units (canvas / ppu), center origin.
42
+ // Authoring Y grows upward in the editor, but the runtime draw path that
43
+ // matches Cubism Web samples presents with Y flipped into our shared
44
+ // Y-up NDC (head / bowl toward +Y). Negate Y once here — same contract as
45
+ // moc2 after top-left pixel normalization.
46
+ const ppu = pixelsPerUnit > 0 ? pixelsPerUnit : 1;
47
+ const hw = canvasWidth > 0 ? (canvasWidth / ppu) * 0.5 : 0.5;
48
+ const hh = canvasHeight > 0 ? (canvasHeight / ppu) * 0.5 : 0.5;
49
+ const out = new Float32Array(positions.length);
50
+ for (let i = 0; i + 1 < positions.length; i += 2) {
51
+ out[i] = positions[i]! / hw;
52
+ out[i + 1] = -positions[i + 1]! / hh;
53
+ }
54
+ return out;
55
+ }
56
+
57
+ export interface Moc3ToProgramOptions {
58
+ /** Parameter values by index; defaults to each param's defaultValue. */
59
+ getParamByIndex?: (index: number) => number;
60
+ }
61
+
62
+ interface DraftDrawable {
63
+ artMeshIndex: number;
64
+ textureIndex: number;
65
+ positions: Float32Array;
66
+ uvs: Float32Array;
67
+ indices: Uint16Array;
68
+ opacity: number;
69
+ renderOrder: number;
70
+ blendMode: number;
71
+ invertedMask: boolean;
72
+ rawMaskArtMeshes: number[];
73
+ visible: boolean;
74
+ }
75
+
76
+ /** Convert a parsed MOC3 document into a ModelProgram at the given pose. */
77
+ export function moc3DocumentToProgram(
78
+ doc: Moc3Document,
79
+ options: Moc3ToProgramOptions = {},
80
+ ): ModelProgram {
81
+ const meshCount = doc.counts[CountIdx.ART_MESHES] ?? 0;
82
+ const paramCount = doc.counts[CountIdx.PARAMETERS] ?? 0;
83
+
84
+ const paramIds = moc3SectionStrings(doc, "parameter.ids");
85
+ const maxValues = moc3SectionF32(doc, "parameter.max_values");
86
+ const minValues = moc3SectionF32(doc, "parameter.min_values");
87
+ const defaultValues = moc3SectionF32(doc, "parameter.default_values");
88
+
89
+ const parameters: ParameterProgram[] = [];
90
+ for (let i = 0; i < paramCount; i++) {
91
+ parameters.push({
92
+ id: paramIds[i] || `param_${i}`,
93
+ min: minValues[i] ?? 0,
94
+ max: maxValues[i] ?? 0,
95
+ defaultValue: defaultValues[i] ?? 0,
96
+ });
97
+ }
98
+
99
+ const getParamByIndex =
100
+ options.getParamByIndex ??
101
+ ((index: number) => defaultValues[index] ?? 0);
102
+
103
+ const keyTables = loadMoc3KeyTables(doc);
104
+ const deformers = bakeMoc3Deformers(doc, keyTables, getParamByIndex);
105
+ const glues = loadMoc3Glues(doc);
106
+ const glueIntensities = moc3SectionF32(doc, "glue_keyform.intensities");
107
+
108
+ const visibles = moc3SectionI32(doc, "art_mesh.visibles");
109
+ const enables = moc3SectionI32(doc, "art_mesh.enables");
110
+ const textureIndices = moc3SectionI32(doc, "art_mesh.texture_indices");
111
+ const drawableFlags = moc3SectionI32(doc, "art_mesh.drawable_flags");
112
+ const vertexCounts = moc3SectionI32(doc, "art_mesh.vertex_counts");
113
+ const uvBegins = moc3SectionI32(doc, "art_mesh.uv_begin_indices");
114
+ const indexBegins = moc3SectionI32(
115
+ doc,
116
+ "art_mesh.position_index_begin_indices",
117
+ );
118
+ const indexCounts = moc3SectionI32(doc, "art_mesh.position_index_counts");
119
+ const keyformBegins = moc3SectionI32(doc, "art_mesh.keyform_begin_indices");
120
+ const keyformCounts = moc3SectionI32(doc, "art_mesh.keyform_counts");
121
+ const bandIndices = moc3SectionI32(
122
+ doc,
123
+ "art_mesh.keyform_binding_band_indices",
124
+ );
125
+ const parentDeformers = moc3SectionI32(
126
+ doc,
127
+ "art_mesh.parent_deformer_indices",
128
+ );
129
+ const maskBegins = moc3SectionI32(doc, "art_mesh.mask_begin_indices");
130
+ const maskCounts = moc3SectionI32(doc, "art_mesh.mask_counts");
131
+ const maskArtMeshes = moc3SectionI32(doc, "drawable_mask.art_mesh_indices");
132
+
133
+ const keyformOpacities = moc3SectionF32(doc, "art_mesh_keyform.opacities");
134
+ const keyformDrawOrders = moc3SectionF32(
135
+ doc,
136
+ "art_mesh_keyform.draw_orders",
137
+ );
138
+ const keyformPosBegins = moc3SectionI32(
139
+ doc,
140
+ "art_mesh_keyform.keyform_position_begin_indices",
141
+ );
142
+
143
+ const keyformPositions = moc3SectionF32(doc, "keyform_position.xys");
144
+ const uvsAll = moc3SectionF32(doc, "uv.xys");
145
+ const indicesAll = moc3SectionI16(doc, "position_index.indices");
146
+
147
+ const cw = doc.canvas.canvasWidth;
148
+ const ch = doc.canvas.canvasHeight;
149
+ const ppu = doc.canvas.pixelsPerUnit;
150
+
151
+ // World positions for every mesh that participates in glue or draw.
152
+ const worldByMesh = new Map<number, Float32Array>();
153
+ const meshMeta = new Map<
154
+ number,
155
+ {
156
+ textureIndex: number;
157
+ uvs: Float32Array;
158
+ indices: Uint16Array;
159
+ opacity: number;
160
+ renderOrder: number;
161
+ blendMode: number;
162
+ invertedMask: boolean;
163
+ rawMaskArtMeshes: number[];
164
+ visible: boolean;
165
+ }
166
+ >();
167
+
168
+ for (let i = 0; i < meshCount; i++) {
169
+ if ((enables[i] ?? 1) === 0) continue;
170
+
171
+ const vertexCount = vertexCounts[i] ?? 0;
172
+ const indexCount = indexCounts[i] ?? 0;
173
+ if (vertexCount <= 0 || indexCount < 3) continue;
174
+
175
+ const kfBegin = keyformBegins[i] ?? 0;
176
+ const kfCount = keyformCounts[i] ?? 0;
177
+ if (kfCount <= 0) continue;
178
+
179
+ const band = bandIndices[i] ?? -1;
180
+ const blend = resolveMoc3KeyformBlend(keyTables, band, getParamByIndex);
181
+
182
+ const local = blendKeyformFloats(
183
+ keyformPositions,
184
+ keyformPosBegins,
185
+ kfBegin,
186
+ kfCount,
187
+ vertexCount * 2,
188
+ blend,
189
+ );
190
+
191
+ const parentIndex = parentDeformers[i] ?? -1;
192
+ const parent =
193
+ parentIndex >= 0 ? (deformers[parentIndex] ?? null) : null;
194
+ const world = parent ? applyParentToPoints(local, parent) : local;
195
+ worldByMesh.set(i, world);
196
+
197
+ const uvBegin = uvBegins[i] ?? 0;
198
+ const uvs = new Float32Array(vertexCount * 2);
199
+ for (let v = 0; v < vertexCount * 2; v++) {
200
+ uvs[v] = uvsAll[uvBegin + v] ?? 0;
201
+ }
202
+
203
+ const indexBegin = indexBegins[i] ?? 0;
204
+ const indices = new Uint16Array(indexCount);
205
+ for (let t = 0; t < indexCount; t++) {
206
+ indices[t] = indicesAll[indexBegin + t] ?? 0;
207
+ }
208
+
209
+ const opacity = blendKeyformScalar(
210
+ keyformOpacities,
211
+ kfBegin,
212
+ kfCount,
213
+ blend,
214
+ 1,
215
+ );
216
+ const renderOrder = Math.round(
217
+ blendKeyformScalar(keyformDrawOrders, kfBegin, kfCount, blend, i),
218
+ );
219
+
220
+ const flags = decodeMoc3DrawableFlags(drawableFlags[i] ?? 0);
221
+ const maskCount = maskCounts[i] ?? 0;
222
+ const maskBegin = maskBegins[i] ?? 0;
223
+ const rawMasks: number[] = [];
224
+ for (let m = 0; m < maskCount; m++) {
225
+ const mi = maskArtMeshes[maskBegin + m];
226
+ if (mi !== undefined && mi >= 0) rawMasks.push(mi);
227
+ }
228
+
229
+ meshMeta.set(i, {
230
+ textureIndex: Math.max(0, textureIndices[i] ?? 0),
231
+ uvs,
232
+ indices,
233
+ opacity,
234
+ renderOrder,
235
+ blendMode: flags.blendMode,
236
+ invertedMask: flags.invertedMask,
237
+ rawMaskArtMeshes: rawMasks,
238
+ visible: (visibles[i] ?? 1) !== 0,
239
+ });
240
+ }
241
+
242
+ applyMoc3Glues(
243
+ worldByMesh,
244
+ glues,
245
+ keyTables,
246
+ getParamByIndex,
247
+ glueIntensities,
248
+ );
249
+
250
+ const drafts: DraftDrawable[] = [];
251
+ for (const [artMeshIndex, world] of worldByMesh) {
252
+ const meta = meshMeta.get(artMeshIndex);
253
+ if (!meta) continue;
254
+ drafts.push({
255
+ artMeshIndex,
256
+ textureIndex: meta.textureIndex,
257
+ positions: normalizePositions(world, cw, ch, ppu),
258
+ uvs: meta.uvs,
259
+ indices: meta.indices,
260
+ opacity: meta.opacity,
261
+ renderOrder: meta.renderOrder,
262
+ blendMode: meta.blendMode,
263
+ invertedMask: meta.invertedMask,
264
+ rawMaskArtMeshes: meta.rawMaskArtMeshes,
265
+ visible: meta.visible,
266
+ });
267
+ }
268
+
269
+ drafts.sort((a, b) => a.renderOrder - b.renderOrder);
270
+
271
+ const artToProgram = new Map<number, number>();
272
+ for (let i = 0; i < drafts.length; i++) {
273
+ artToProgram.set(drafts[i]!.artMeshIndex, i);
274
+ }
275
+
276
+ const drawables: DrawableProgram[] = drafts.map((d, index) => {
277
+ const maskIndices: number[] = [];
278
+ for (const art of d.rawMaskArtMeshes) {
279
+ const mapped = artToProgram.get(art);
280
+ if (mapped !== undefined) maskIndices.push(mapped);
281
+ }
282
+ return {
283
+ index,
284
+ textureIndex: d.textureIndex,
285
+ positions: d.positions,
286
+ uvs: d.uvs,
287
+ indices: d.indices,
288
+ opacity: d.opacity,
289
+ renderOrder: d.renderOrder,
290
+ blendMode: d.blendMode,
291
+ invertedMask: d.invertedMask,
292
+ maskIndices,
293
+ visible: d.visible,
294
+ deformParamIndex: -1,
295
+ deformDeltas: null,
296
+ };
297
+ });
298
+
299
+ return {
300
+ format: "moc3",
301
+ codec: "moc3",
302
+ parameters,
303
+ drawables,
304
+ };
305
+ }