@doki-land/live2d-renderer 0.0.18 → 0.0.20
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/dist/index.d.ts +63 -7
- package/dist/index.js +985 -339
- package/package.json +2 -2
- package/src/backends/canvas2d.ts +6 -7
- package/src/backends/webgl2.ts +54 -33
- package/src/backends/webgpu-mesh-cache.ts +206 -0
- package/src/backends/webgpu.ts +158 -186
- package/src/cpu/evaluate.ts +63 -4
- package/src/index.ts +9 -0
- package/src/moc/moc2-to-program.ts +89 -3
- package/src/moc/moc2.ts +151 -75
- package/src/moc/moc3-to-program.ts +193 -3
- package/src/moc/moc3.ts +160 -81
- package/src/render/clipping-plan.ts +140 -0
- package/src/render/clipping.ts +22 -6
- package/src/render/mesh-interleave.ts +28 -0
- package/src/runtime/model-runtime.ts +6 -0
- package/src/types.ts +1 -1
|
@@ -49,11 +49,12 @@ export function moc2ParamGetterFromValues(
|
|
|
49
49
|
return (id: string) => byId.get(id) ?? 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function normalizePositionsInto(
|
|
53
53
|
positions: Float32Array,
|
|
54
54
|
canvasWidth: number,
|
|
55
55
|
canvasHeight: number,
|
|
56
|
-
|
|
56
|
+
out: Float32Array,
|
|
57
|
+
): void {
|
|
57
58
|
// moc2 draw coords are canvas pixels with origin at the **top-left**
|
|
58
59
|
// (Y increases downward), matching Live2D Cubism 2 canvas space.
|
|
59
60
|
// Map to Y-up NDC so WebGL/WebGPU/Canvas2D share one convention:
|
|
@@ -61,14 +62,99 @@ function normalizePositions(
|
|
|
61
62
|
// y: [0, h] → [1, -1] (top of canvas → NDC +Y)
|
|
62
63
|
const w = canvasWidth > 0 ? canvasWidth : 1;
|
|
63
64
|
const h = canvasHeight > 0 ? canvasHeight : 1;
|
|
64
|
-
const out = new Float32Array(positions.length);
|
|
65
65
|
for (let i = 0; i + 1 < positions.length; i += 2) {
|
|
66
66
|
out[i] = (positions[i]! / w) * 2 - 1;
|
|
67
67
|
out[i + 1] = 1 - (positions[i + 1]! / h) * 2;
|
|
68
68
|
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizePositions(
|
|
72
|
+
positions: Float32Array,
|
|
73
|
+
canvasWidth: number,
|
|
74
|
+
canvasHeight: number,
|
|
75
|
+
): Float32Array {
|
|
76
|
+
const out = new Float32Array(positions.length);
|
|
77
|
+
normalizePositionsInto(positions, canvasWidth, canvasHeight, out);
|
|
69
78
|
return out;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Evaluate moc2 keyforms/deformers into resident {@link DrawableMesh} buffers.
|
|
83
|
+
* Mesh object identity and shared `uvs`/`indices`/`maskIndices` are preserved
|
|
84
|
+
* when vertex counts are unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export function evaluateMoc2PoseInto(
|
|
87
|
+
model: Moc2ModelImpl,
|
|
88
|
+
getParam: (id: string) => number,
|
|
89
|
+
byId: ReadonlyMap<string, import("../types.js").DrawableMesh>,
|
|
90
|
+
): void {
|
|
91
|
+
const ops = bakeDeformerOps(model, getParam);
|
|
92
|
+
const w = model.canvasWidth;
|
|
93
|
+
const h = model.canvasHeight;
|
|
94
|
+
for (const part of model.parts) {
|
|
95
|
+
for (const mesh of part.drawData) {
|
|
96
|
+
const sink = byId.get(mesh.id);
|
|
97
|
+
if (!sink) continue;
|
|
98
|
+
const floatCount = mesh.numPoints * 2;
|
|
99
|
+
const local = interpolateKeyforms(
|
|
100
|
+
mesh.keyforms,
|
|
101
|
+
mesh.pivotManager,
|
|
102
|
+
getParam,
|
|
103
|
+
floatCount,
|
|
104
|
+
);
|
|
105
|
+
const world = transformDrawablePositions(mesh, local, ops);
|
|
106
|
+
let pos = sink.vertexPositions;
|
|
107
|
+
if (pos.length !== world.length) {
|
|
108
|
+
pos = new Float32Array(world.length);
|
|
109
|
+
sink.vertexPositions = pos;
|
|
110
|
+
}
|
|
111
|
+
normalizePositionsInto(world, w, h, pos);
|
|
112
|
+
sink.opacity = interpolateScalarTable(
|
|
113
|
+
mesh.opacities,
|
|
114
|
+
mesh.pivotManager,
|
|
115
|
+
getParam,
|
|
116
|
+
1,
|
|
117
|
+
);
|
|
118
|
+
sink.renderOrder = Math.round(
|
|
119
|
+
interpolateScalarTable(
|
|
120
|
+
mesh.drawOrders,
|
|
121
|
+
mesh.pivotManager,
|
|
122
|
+
getParam,
|
|
123
|
+
mesh.averageDrawOrder,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
sink.visible = part.visible;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Collect drawable ids in the same order {@link moc2ModelToProgram} assigns indices. */
|
|
132
|
+
export function moc2DrawableIdsInProgramOrder(
|
|
133
|
+
model: Moc2ModelImpl,
|
|
134
|
+
getParam?: (id: string) => number,
|
|
135
|
+
): string[] {
|
|
136
|
+
const paramDefs = model.paramDefSet?.params ?? [];
|
|
137
|
+
const gp = getParam ?? buildParamGetter(paramDefs);
|
|
138
|
+
const drafts: { id: string; renderOrder: number }[] = [];
|
|
139
|
+
for (const part of model.parts) {
|
|
140
|
+
for (const mesh of part.drawData) {
|
|
141
|
+
drafts.push({
|
|
142
|
+
id: mesh.id,
|
|
143
|
+
renderOrder: Math.round(
|
|
144
|
+
interpolateScalarTable(
|
|
145
|
+
mesh.drawOrders,
|
|
146
|
+
mesh.pivotManager,
|
|
147
|
+
gp,
|
|
148
|
+
mesh.averageDrawOrder,
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
drafts.sort((a, b) => a.renderOrder - b.renderOrder);
|
|
155
|
+
return drafts.map((d) => d.id);
|
|
156
|
+
}
|
|
157
|
+
|
|
72
158
|
interface DraftDrawable {
|
|
73
159
|
id: string;
|
|
74
160
|
textureIndex: number;
|
package/src/moc/moc2.ts
CHANGED
|
@@ -6,11 +6,7 @@ import type {
|
|
|
6
6
|
ModelSettings,
|
|
7
7
|
} from "@doki-land/live2d-core";
|
|
8
8
|
import { detectModelSettingsFormat } from "@doki-land/live2d-core";
|
|
9
|
-
import {
|
|
10
|
-
createModelInstance,
|
|
11
|
-
evaluateFrame,
|
|
12
|
-
setParameterValue,
|
|
13
|
-
} from "../cpu/evaluate.js";
|
|
9
|
+
import { createModelInstance, setParameterValue } from "../cpu/evaluate.js";
|
|
14
10
|
import type {
|
|
15
11
|
ModelBackend,
|
|
16
12
|
ModelBackendOptions,
|
|
@@ -19,65 +15,127 @@ import type {
|
|
|
19
15
|
import type { BlendMode, DrawableMesh } from "../types.js";
|
|
20
16
|
import { type Moc2ModelImpl, Moc2Parser } from "./moc2-objects.js";
|
|
21
17
|
import {
|
|
18
|
+
evaluateMoc2PoseInto,
|
|
19
|
+
moc2DrawableIdsInProgramOrder,
|
|
22
20
|
moc2ModelToProgram,
|
|
23
21
|
moc2ParamGetterFromValues,
|
|
24
22
|
} from "./moc2-to-program.js";
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
maskIndices: [...d.maskIndices],
|
|
39
|
-
visible: d.visible,
|
|
40
|
-
};
|
|
24
|
+
interface Moc2State {
|
|
25
|
+
moc: Moc2ModelImpl;
|
|
26
|
+
/** Stable program + instance (never replaced after load). */
|
|
27
|
+
instance: ModelInstance;
|
|
28
|
+
/** Resident meshes; objects and topology buffers stay identity-stable. */
|
|
29
|
+
meshes: DrawableMesh[];
|
|
30
|
+
/** Draw-order view (same mesh refs, re-sorted when pose changes). */
|
|
31
|
+
drawView: DrawableMesh[];
|
|
32
|
+
byId: Map<string, DrawableMesh>;
|
|
33
|
+
poseDirty: boolean;
|
|
34
|
+
bindings: ParameterBinding[];
|
|
35
|
+
paramIndexById: Map<string, number>;
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
const stateByModel = new WeakMap<InternalModel, Moc2State>();
|
|
39
|
+
|
|
40
|
+
function allocateMeshesFromProgram(
|
|
41
|
+
program: ReturnType<typeof moc2ModelToProgram>,
|
|
42
|
+
drawableIds: readonly string[],
|
|
43
|
+
): { meshes: DrawableMesh[]; byId: Map<string, DrawableMesh> } {
|
|
44
|
+
const meshes: DrawableMesh[] = [];
|
|
45
|
+
const byId = new Map<string, DrawableMesh>();
|
|
46
|
+
for (let i = 0; i < program.drawables.length; i++) {
|
|
47
|
+
const d = program.drawables[i]!;
|
|
48
|
+
const mesh: DrawableMesh = {
|
|
49
|
+
index: d.index,
|
|
50
|
+
textureIndex: d.textureIndex,
|
|
51
|
+
vertexPositions: new Float32Array(d.positions),
|
|
52
|
+
uvs: d.uvs,
|
|
53
|
+
indices: d.indices,
|
|
54
|
+
opacity: d.opacity,
|
|
55
|
+
blendMode: d.blendMode as BlendMode,
|
|
56
|
+
invertedMask: d.invertedMask,
|
|
57
|
+
renderOrder: d.renderOrder,
|
|
58
|
+
dynamicFlag: true,
|
|
59
|
+
maskIndices: [...d.maskIndices],
|
|
60
|
+
visible: d.visible,
|
|
61
|
+
};
|
|
62
|
+
meshes.push(mesh);
|
|
63
|
+
const id = drawableIds[i];
|
|
64
|
+
if (id) byId.set(id, mesh);
|
|
47
65
|
}
|
|
48
|
-
return
|
|
66
|
+
return { meshes, byId };
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
function buildBindings(instance: ModelInstance): {
|
|
70
|
+
bindings: ParameterBinding[];
|
|
71
|
+
paramIndexById: Map<string, number>;
|
|
72
|
+
} {
|
|
73
|
+
const bindings: ParameterBinding[] = [];
|
|
74
|
+
const paramIndexById = new Map<string, number>();
|
|
75
|
+
for (let i = 0; i < instance.program.parameters.length; i++) {
|
|
76
|
+
const p = instance.program.parameters[i]!;
|
|
77
|
+
const binding: ParameterBinding = {
|
|
78
|
+
id: p.id,
|
|
79
|
+
min: p.min,
|
|
80
|
+
max: p.max,
|
|
81
|
+
defaultValue: p.defaultValue,
|
|
82
|
+
value: instance.parameterValues[i] ?? p.defaultValue,
|
|
83
|
+
};
|
|
84
|
+
bindings.push(binding);
|
|
85
|
+
paramIndexById.set(p.id, i);
|
|
86
|
+
}
|
|
87
|
+
return { bindings, paramIndexById };
|
|
56
88
|
}
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
function syncBindingValues(state: Moc2State): void {
|
|
91
|
+
for (let i = 0; i < state.bindings.length; i++) {
|
|
92
|
+
const b = state.bindings[i]!;
|
|
93
|
+
(b as { value: number }).value =
|
|
94
|
+
state.instance.parameterValues[i] ?? b.defaultValue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function refreshDrawView(state: Moc2State): void {
|
|
99
|
+
const view = state.drawView;
|
|
100
|
+
view.length = 0;
|
|
101
|
+
for (const m of state.meshes) view.push(m);
|
|
102
|
+
view.sort((a, b) => a.renderOrder - b.renderOrder || a.index - b.index);
|
|
103
|
+
}
|
|
59
104
|
|
|
60
|
-
/** Re-bake moc2 geometry for current parameter values. */
|
|
105
|
+
/** Re-bake moc2 geometry for current parameter values into resident meshes. */
|
|
61
106
|
function bakePose(state: Moc2State): void {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
if (!state.poseDirty) return;
|
|
108
|
+
const getParam = moc2ParamGetterFromValues(
|
|
109
|
+
state.instance.program.parameters,
|
|
110
|
+
state.instance.parameterValues,
|
|
111
|
+
);
|
|
112
|
+
evaluateMoc2PoseInto(state.moc, getParam, state.byId);
|
|
113
|
+
refreshDrawView(state);
|
|
114
|
+
syncBindingValues(state);
|
|
115
|
+
state.poseDirty = false;
|
|
116
|
+
}
|
|
68
117
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
118
|
+
function frameFromMeshes(
|
|
119
|
+
meshes: readonly DrawableMesh[],
|
|
120
|
+
timeSeconds: number,
|
|
121
|
+
): FrameSnapshot {
|
|
122
|
+
const drawables: FrameDrawable[] = [];
|
|
123
|
+
for (const m of meshes) {
|
|
124
|
+
drawables.push({
|
|
125
|
+
index: m.index,
|
|
126
|
+
textureIndex: m.textureIndex,
|
|
127
|
+
positions: m.vertexPositions,
|
|
128
|
+
uvs: m.uvs,
|
|
129
|
+
indices: m.indices,
|
|
130
|
+
opacity: m.opacity,
|
|
131
|
+
blendMode: m.blendMode,
|
|
132
|
+
renderOrder: m.renderOrder,
|
|
133
|
+
visible: m.visible,
|
|
134
|
+
invertedMask: m.invertedMask,
|
|
135
|
+
maskIndices: m.maskIndices,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return { timeSeconds, drawables };
|
|
81
139
|
}
|
|
82
140
|
|
|
83
141
|
/** moc2 (`.moc`) model backend — pure-TS decode → CPU evaluate. */
|
|
@@ -108,17 +166,30 @@ export class Moc2Backend implements ModelBackend {
|
|
|
108
166
|
new Moc2Parser(bytes).parseModel();
|
|
109
167
|
const program = moc2ModelToProgram(moc);
|
|
110
168
|
const instance = createModelInstance(program);
|
|
169
|
+
const drawableIds = moc2DrawableIdsInProgramOrder(moc);
|
|
170
|
+
const { meshes, byId } = allocateMeshesFromProgram(
|
|
171
|
+
program,
|
|
172
|
+
drawableIds,
|
|
173
|
+
);
|
|
174
|
+
const { bindings, paramIndexById } = buildBindings(instance);
|
|
175
|
+
const drawView: DrawableMesh[] = [];
|
|
111
176
|
const model: InternalModel = {
|
|
112
177
|
id: settings.name ?? settings.url,
|
|
113
178
|
settings,
|
|
114
179
|
format: "moc2",
|
|
115
180
|
};
|
|
116
|
-
|
|
181
|
+
const state: Moc2State = {
|
|
117
182
|
moc,
|
|
118
183
|
instance,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
meshes,
|
|
185
|
+
drawView,
|
|
186
|
+
byId,
|
|
187
|
+
poseDirty: true,
|
|
188
|
+
bindings,
|
|
189
|
+
paramIndexById,
|
|
190
|
+
};
|
|
191
|
+
bakePose(state);
|
|
192
|
+
stateByModel.set(model, state);
|
|
122
193
|
return model;
|
|
123
194
|
}
|
|
124
195
|
|
|
@@ -127,50 +198,50 @@ export class Moc2Backend implements ModelBackend {
|
|
|
127
198
|
if (!state) return;
|
|
128
199
|
state.instance.timeSeconds += deltaTimeSeconds;
|
|
129
200
|
bakePose(state);
|
|
130
|
-
if (state.lastFrame) {
|
|
131
|
-
state.lastFrame = {
|
|
132
|
-
...state.lastFrame,
|
|
133
|
-
timeSeconds: state.instance.timeSeconds,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
201
|
}
|
|
137
202
|
|
|
138
203
|
getDrawables(model: InternalModel): DrawableMesh[] {
|
|
139
204
|
const state = stateByModel.get(model);
|
|
140
205
|
if (!state) return [];
|
|
141
206
|
bakePose(state);
|
|
142
|
-
|
|
143
|
-
state.lastFrame = frame;
|
|
144
|
-
return frame.drawables.map(toDrawableMesh);
|
|
207
|
+
return state.drawView;
|
|
145
208
|
}
|
|
146
209
|
|
|
147
210
|
captureFrame(model: InternalModel): FrameSnapshot | null {
|
|
148
211
|
const state = stateByModel.get(model);
|
|
149
212
|
if (!state) return null;
|
|
150
213
|
bakePose(state);
|
|
151
|
-
|
|
152
|
-
state.lastFrame = frame;
|
|
153
|
-
return frame;
|
|
214
|
+
return frameFromMeshes(state.drawView, state.instance.timeSeconds);
|
|
154
215
|
}
|
|
155
216
|
|
|
156
217
|
setParameter(model: InternalModel, id: string, value: number): void {
|
|
157
218
|
const state = stateByModel.get(model);
|
|
158
219
|
if (!state) return;
|
|
159
220
|
setParameterValue(state.instance, id, value);
|
|
160
|
-
|
|
161
|
-
|
|
221
|
+
const index = state.paramIndexById.get(id);
|
|
222
|
+
if (index !== undefined) {
|
|
223
|
+
const binding = state.bindings[index];
|
|
224
|
+
if (binding) {
|
|
225
|
+
(binding as { value: number }).value =
|
|
226
|
+
state.instance.parameterValues[index] ??
|
|
227
|
+
binding.defaultValue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
state.poseDirty = true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
resolveParameter(model: InternalModel, id: string): number | undefined {
|
|
234
|
+
const state = stateByModel.get(model);
|
|
235
|
+
if (!state) return undefined;
|
|
236
|
+
return state.paramIndexById.get(id);
|
|
162
237
|
}
|
|
163
238
|
|
|
164
239
|
listParameters(model: InternalModel): readonly ParameterBinding[] {
|
|
165
240
|
const state = stateByModel.get(model);
|
|
166
241
|
if (!state) return [];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
max: p.max,
|
|
171
|
-
defaultValue: p.defaultValue,
|
|
172
|
-
value: state.instance.parameterValues[i] ?? p.defaultValue,
|
|
173
|
-
}));
|
|
242
|
+
bakePose(state);
|
|
243
|
+
syncBindingValues(state);
|
|
244
|
+
return state.bindings;
|
|
174
245
|
}
|
|
175
246
|
|
|
176
247
|
hitTest(_model: InternalModel, _x: number, _y: number): string | null {
|
|
@@ -180,6 +251,11 @@ export class Moc2Backend implements ModelBackend {
|
|
|
180
251
|
destroyModel(model: InternalModel): void {
|
|
181
252
|
stateByModel.delete(model);
|
|
182
253
|
}
|
|
254
|
+
|
|
255
|
+
/** Test/harness: stable program + instance refs. */
|
|
256
|
+
getResidentInstance(model: InternalModel): ModelInstance | null {
|
|
257
|
+
return stateByModel.get(model)?.instance ?? null;
|
|
258
|
+
}
|
|
183
259
|
}
|
|
184
260
|
|
|
185
261
|
export function createMoc2Backend(): Moc2Backend {
|
|
@@ -32,23 +32,39 @@ import {
|
|
|
32
32
|
moc3SectionStrings,
|
|
33
33
|
} from "./moc3-reader.js";
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function normalizePositionsInto(
|
|
36
36
|
positions: Float32Array,
|
|
37
37
|
canvasWidth: number,
|
|
38
38
|
canvasHeight: number,
|
|
39
39
|
pixelsPerUnit: number,
|
|
40
|
-
|
|
40
|
+
out: Float32Array,
|
|
41
|
+
): void {
|
|
41
42
|
// Deformer output uses logical canvas/ppu units around the model center.
|
|
42
43
|
// Convert the decoded authoring orientation once into the renderer's shared
|
|
43
44
|
// Y-up NDC contract, matching the moc2 path after top-left normalization.
|
|
44
45
|
const ppu = pixelsPerUnit > 0 ? pixelsPerUnit : 1;
|
|
45
46
|
const hw = canvasWidth > 0 ? (canvasWidth / ppu) * 0.5 : 0.5;
|
|
46
47
|
const hh = canvasHeight > 0 ? (canvasHeight / ppu) * 0.5 : 0.5;
|
|
47
|
-
const out = new Float32Array(positions.length);
|
|
48
48
|
for (let i = 0; i + 1 < positions.length; i += 2) {
|
|
49
49
|
out[i] = positions[i]! / hw;
|
|
50
50
|
out[i + 1] = -positions[i + 1]! / hh;
|
|
51
51
|
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizePositions(
|
|
55
|
+
positions: Float32Array,
|
|
56
|
+
canvasWidth: number,
|
|
57
|
+
canvasHeight: number,
|
|
58
|
+
pixelsPerUnit: number,
|
|
59
|
+
): Float32Array {
|
|
60
|
+
const out = new Float32Array(positions.length);
|
|
61
|
+
normalizePositionsInto(
|
|
62
|
+
positions,
|
|
63
|
+
canvasWidth,
|
|
64
|
+
canvasHeight,
|
|
65
|
+
pixelsPerUnit,
|
|
66
|
+
out,
|
|
67
|
+
);
|
|
52
68
|
return out;
|
|
53
69
|
}
|
|
54
70
|
|
|
@@ -301,3 +317,177 @@ export function moc3DocumentToProgram(
|
|
|
301
317
|
drawables,
|
|
302
318
|
};
|
|
303
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Art-mesh indices in the same order {@link moc3DocumentToProgram} assigns drawable indices. */
|
|
322
|
+
export function moc3ArtMeshIndicesInProgramOrder(
|
|
323
|
+
doc: Moc3Document,
|
|
324
|
+
getParamByIndex?: (index: number) => number,
|
|
325
|
+
): number[] {
|
|
326
|
+
const meshCount = doc.counts[CountIdx.ART_MESHES] ?? 0;
|
|
327
|
+
const defaultValues = moc3SectionF32(doc, "parameter.default_values");
|
|
328
|
+
const gp =
|
|
329
|
+
getParamByIndex ?? ((index: number) => defaultValues[index] ?? 0);
|
|
330
|
+
const keyTables = loadMoc3KeyTables(doc);
|
|
331
|
+
const enables = moc3SectionI32(doc, "art_mesh.enables");
|
|
332
|
+
const vertexCounts = moc3SectionI32(doc, "art_mesh.vertex_counts");
|
|
333
|
+
const indexCounts = moc3SectionI32(doc, "art_mesh.position_index_counts");
|
|
334
|
+
const keyformBegins = moc3SectionI32(doc, "art_mesh.keyform_begin_indices");
|
|
335
|
+
const keyformCounts = moc3SectionI32(doc, "art_mesh.keyform_counts");
|
|
336
|
+
const bandIndices = moc3SectionI32(
|
|
337
|
+
doc,
|
|
338
|
+
"art_mesh.keyform_binding_band_indices",
|
|
339
|
+
);
|
|
340
|
+
const keyformDrawOrders = moc3SectionF32(
|
|
341
|
+
doc,
|
|
342
|
+
"art_mesh_keyform.draw_orders",
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const drafts: { artMeshIndex: number; renderOrder: number }[] = [];
|
|
346
|
+
for (let i = 0; i < meshCount; i++) {
|
|
347
|
+
if ((enables[i] ?? 1) === 0) continue;
|
|
348
|
+
const vertexCount = vertexCounts[i] ?? 0;
|
|
349
|
+
const indexCount = indexCounts[i] ?? 0;
|
|
350
|
+
if (vertexCount <= 0 || indexCount < 3) continue;
|
|
351
|
+
const kfBegin = keyformBegins[i] ?? 0;
|
|
352
|
+
const kfCount = keyformCounts[i] ?? 0;
|
|
353
|
+
if (kfCount <= 0) continue;
|
|
354
|
+
const band = bandIndices[i] ?? -1;
|
|
355
|
+
const blend = resolveMoc3KeyformBlend(keyTables, band, gp);
|
|
356
|
+
drafts.push({
|
|
357
|
+
artMeshIndex: i,
|
|
358
|
+
renderOrder: Math.round(
|
|
359
|
+
blendKeyformScalar(
|
|
360
|
+
keyformDrawOrders,
|
|
361
|
+
kfBegin,
|
|
362
|
+
kfCount,
|
|
363
|
+
blend,
|
|
364
|
+
i,
|
|
365
|
+
),
|
|
366
|
+
),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
drafts.sort((a, b) => a.renderOrder - b.renderOrder);
|
|
370
|
+
return drafts.map((d) => d.artMeshIndex);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Evaluate moc3 keyforms/deformers/glue into resident meshes keyed by art-mesh index.
|
|
375
|
+
* Topology buffers (`uvs`/`indices`/`maskIndices`) stay shared; positions are written in place.
|
|
376
|
+
*/
|
|
377
|
+
export function evaluateMoc3PoseInto(
|
|
378
|
+
doc: Moc3Document,
|
|
379
|
+
getParamByIndex: (index: number) => number,
|
|
380
|
+
byArtMesh: ReadonlyMap<number, import("../types.js").DrawableMesh>,
|
|
381
|
+
poseOpacity: Float32Array,
|
|
382
|
+
): void {
|
|
383
|
+
const meshCount = doc.counts[CountIdx.ART_MESHES] ?? 0;
|
|
384
|
+
const keyTables = loadMoc3KeyTables(doc);
|
|
385
|
+
const deformers = bakeMoc3Deformers(doc, keyTables, getParamByIndex);
|
|
386
|
+
const glues = loadMoc3Glues(doc);
|
|
387
|
+
const glueIntensities = moc3SectionF32(doc, "glue_keyform.intensities");
|
|
388
|
+
|
|
389
|
+
const visibles = moc3SectionI32(doc, "art_mesh.visibles");
|
|
390
|
+
const enables = moc3SectionI32(doc, "art_mesh.enables");
|
|
391
|
+
const vertexCounts = moc3SectionI32(doc, "art_mesh.vertex_counts");
|
|
392
|
+
const indexCounts = moc3SectionI32(doc, "art_mesh.position_index_counts");
|
|
393
|
+
const keyformBegins = moc3SectionI32(doc, "art_mesh.keyform_begin_indices");
|
|
394
|
+
const keyformCounts = moc3SectionI32(doc, "art_mesh.keyform_counts");
|
|
395
|
+
const bandIndices = moc3SectionI32(
|
|
396
|
+
doc,
|
|
397
|
+
"art_mesh.keyform_binding_band_indices",
|
|
398
|
+
);
|
|
399
|
+
const parentDeformers = moc3SectionI32(
|
|
400
|
+
doc,
|
|
401
|
+
"art_mesh.parent_deformer_indices",
|
|
402
|
+
);
|
|
403
|
+
const keyformOpacities = moc3SectionF32(doc, "art_mesh_keyform.opacities");
|
|
404
|
+
const keyformDrawOrders = moc3SectionF32(
|
|
405
|
+
doc,
|
|
406
|
+
"art_mesh_keyform.draw_orders",
|
|
407
|
+
);
|
|
408
|
+
const keyformPosBegins = moc3SectionI32(
|
|
409
|
+
doc,
|
|
410
|
+
"art_mesh_keyform.keyform_position_begin_indices",
|
|
411
|
+
);
|
|
412
|
+
const keyformPositions = moc3SectionF32(doc, "keyform_position.xys");
|
|
413
|
+
|
|
414
|
+
const cw = doc.canvas.canvasWidth;
|
|
415
|
+
const ch = doc.canvas.canvasHeight;
|
|
416
|
+
const ppu = doc.canvas.pixelsPerUnit;
|
|
417
|
+
|
|
418
|
+
const worldByMesh = new Map<number, Float32Array>();
|
|
419
|
+
const opacityByMesh = new Map<number, number>();
|
|
420
|
+
const orderByMesh = new Map<number, number>();
|
|
421
|
+
const visibleByMesh = new Map<number, boolean>();
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < meshCount; i++) {
|
|
424
|
+
if (!byArtMesh.has(i)) continue;
|
|
425
|
+
if ((enables[i] ?? 1) === 0) continue;
|
|
426
|
+
const vertexCount = vertexCounts[i] ?? 0;
|
|
427
|
+
const indexCount = indexCounts[i] ?? 0;
|
|
428
|
+
if (vertexCount <= 0 || indexCount < 3) continue;
|
|
429
|
+
const kfBegin = keyformBegins[i] ?? 0;
|
|
430
|
+
const kfCount = keyformCounts[i] ?? 0;
|
|
431
|
+
if (kfCount <= 0) continue;
|
|
432
|
+
|
|
433
|
+
const band = bandIndices[i] ?? -1;
|
|
434
|
+
const blend = resolveMoc3KeyformBlend(keyTables, band, getParamByIndex);
|
|
435
|
+
const local = blendKeyformFloats(
|
|
436
|
+
keyformPositions,
|
|
437
|
+
keyformPosBegins,
|
|
438
|
+
kfBegin,
|
|
439
|
+
kfCount,
|
|
440
|
+
vertexCount * 2,
|
|
441
|
+
blend,
|
|
442
|
+
);
|
|
443
|
+
const parentIndex = parentDeformers[i] ?? -1;
|
|
444
|
+
const parent =
|
|
445
|
+
parentIndex >= 0 ? (deformers[parentIndex] ?? null) : null;
|
|
446
|
+
const world = parent ? applyParentToPoints(local, parent) : local;
|
|
447
|
+
worldByMesh.set(i, world);
|
|
448
|
+
opacityByMesh.set(
|
|
449
|
+
i,
|
|
450
|
+
blendKeyformScalar(keyformOpacities, kfBegin, kfCount, blend, 1),
|
|
451
|
+
);
|
|
452
|
+
orderByMesh.set(
|
|
453
|
+
i,
|
|
454
|
+
Math.round(
|
|
455
|
+
blendKeyformScalar(
|
|
456
|
+
keyformDrawOrders,
|
|
457
|
+
kfBegin,
|
|
458
|
+
kfCount,
|
|
459
|
+
blend,
|
|
460
|
+
i,
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
visibleByMesh.set(i, (visibles[i] ?? 1) !== 0);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
applyMoc3Glues(
|
|
468
|
+
worldByMesh,
|
|
469
|
+
glues,
|
|
470
|
+
keyTables,
|
|
471
|
+
getParamByIndex,
|
|
472
|
+
glueIntensities,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
for (const [artMeshIndex, world] of worldByMesh) {
|
|
476
|
+
const sink = byArtMesh.get(artMeshIndex);
|
|
477
|
+
if (!sink) continue;
|
|
478
|
+
let pos = sink.vertexPositions;
|
|
479
|
+
if (pos.length !== world.length) {
|
|
480
|
+
pos = new Float32Array(world.length);
|
|
481
|
+
sink.vertexPositions = pos;
|
|
482
|
+
}
|
|
483
|
+
normalizePositionsInto(world, cw, ch, ppu, pos);
|
|
484
|
+
const opacity = opacityByMesh.get(artMeshIndex) ?? 1;
|
|
485
|
+
sink.renderOrder = orderByMesh.get(artMeshIndex) ?? sink.renderOrder;
|
|
486
|
+
sink.visible = visibleByMesh.get(artMeshIndex) ?? true;
|
|
487
|
+
const slot = sink.index;
|
|
488
|
+
if (slot >= 0 && slot < poseOpacity.length) {
|
|
489
|
+
poseOpacity[slot] = opacity;
|
|
490
|
+
}
|
|
491
|
+
sink.opacity = opacity;
|
|
492
|
+
}
|
|
493
|
+
}
|