@rig-c/core 2.72.0 → 2.74.0
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/README.md +10 -0
- package/docs/AUTHORING.md +106 -6
- package/docs/FACE.md +19 -0
- package/package.json +1 -1
- package/src/alpha_clip.ts +255 -0
- package/src/cli/core_commands.ts +2 -1
- package/src/cli/shared.ts +14 -1
- package/src/compile.ts +286 -3
- package/src/core/clipping.ts +5 -1
- package/src/mesh.ts +3 -1
- package/src/rig.ts +25 -3
- package/src/runtime/curves.ts +35 -0
- package/src/runtime/index.ts +2 -2
- package/src/runtime/model.ts +47 -14
- package/src/runtime/pose.ts +48 -7
- package/src/types.ts +26 -0
package/src/compile.ts
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
type DepthTone,
|
|
35
35
|
} from './depth.ts';
|
|
36
36
|
import { CompileError, NotImplementedError } from './errors.ts';
|
|
37
|
+
import { alphaClipOutline, clipCarriers, refuseClipCrossing, refuseClipDegeneracy } from './alpha_clip.ts';
|
|
38
|
+
import { regionCorners } from './core/vertices.ts';
|
|
37
39
|
import { parseJsonWithPosition } from './json-position.ts';
|
|
38
40
|
import { hypot2 } from './numeric.ts';
|
|
39
41
|
// The "did you mean" list on a missing atlas region, from the one implementation
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
type RigBone,
|
|
61
63
|
type RigBoundingBoxAttachment,
|
|
62
64
|
type RigClippingAttachment,
|
|
65
|
+
type RigAlphaClip,
|
|
63
66
|
type RigEvent,
|
|
64
67
|
type RigLinkedMeshAttachment,
|
|
65
68
|
type RigMeshAttachment,
|
|
@@ -2296,7 +2299,7 @@ function manifestShapeVisits(manifest: unknown): ShapeVisit[] {
|
|
|
2296
2299
|
*/
|
|
2297
2300
|
export function compile(opts: CompileOptions): CompileResult {
|
|
2298
2301
|
return withDroppedStates((droppedStates) => {
|
|
2299
|
-
const { model, header, atlasText, declaredDurations } = compileInto(opts, droppedStates);
|
|
2302
|
+
const { model, header, atlasText, declaredDurations, alphaClips } = compileInto(opts, droppedStates);
|
|
2300
2303
|
// The skeleton: the Spine emitter's one entry (issue #922), which writes
|
|
2301
2304
|
// every section from the model, in the order and under the spellings the
|
|
2302
2305
|
// file has, and runs the parser-default and key-order passes on the
|
|
@@ -2314,6 +2317,7 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
2314
2317
|
atlasText,
|
|
2315
2318
|
declaredDurations,
|
|
2316
2319
|
...carriedOf(model),
|
|
2320
|
+
...(alphaClips?.length ? { alphaClips } : {}),
|
|
2317
2321
|
model,
|
|
2318
2322
|
};
|
|
2319
2323
|
});
|
|
@@ -2391,6 +2395,7 @@ export function headerBoundsOf(model: CompiledModel, atlasText: string): { box:
|
|
|
2391
2395
|
* beside it to gate it, write its document and — if it asks — emit it.
|
|
2392
2396
|
*/
|
|
2393
2397
|
export interface CompiledBuild {
|
|
2398
|
+
alphaClips?: CompileResult['alphaClips'];
|
|
2394
2399
|
/** The compiled model (`src/model.ts`), `stage` and `editorOrder` included (issue #1026). */
|
|
2395
2400
|
model: CompiledModel;
|
|
2396
2401
|
/** The rest of the Spine header the model does not hold (`SkeletonHeader`): what `compile` hands the emitter beside the model. */
|
|
@@ -3012,6 +3017,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3012
3017
|
// `skin/slot/attachment` -> per-vertex z, for the attachments that named a
|
|
3013
3018
|
// depth map. Read at deform-key time; never emitted. See `AttachmentContext`.
|
|
3014
3019
|
const attachmentDepths = new Map<string, number[] | { z: number[]; geometry: DeformGeometry }>();
|
|
3020
|
+
const pendingAlphaClips: PendingAlphaClip[] = [];
|
|
3015
3021
|
// The same for a `soft` mask's per-vertex level, which a deform envelope's
|
|
3016
3022
|
// `mask` reads (issue #1413). Never emitted either.
|
|
3017
3023
|
const attachmentSoftMasks = new Map<string, DeformSoftMask>();
|
|
@@ -3212,6 +3218,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3212
3218
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
3213
3219
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
3214
3220
|
images,
|
|
3221
|
+
pendingAlphaClips,
|
|
3215
3222
|
bones,
|
|
3216
3223
|
transforms,
|
|
3217
3224
|
exactFrames,
|
|
@@ -3241,6 +3248,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3241
3248
|
// line a linked mesh is the one construct in a rig spec whose name has not been
|
|
3242
3249
|
// resolved yet.
|
|
3243
3250
|
resolveLinkedMeshes(pendingLinks, skinTables);
|
|
3251
|
+
const alphaClips = resolveAlphaClips(pendingAlphaClips, skinTables, rig, motion);
|
|
3244
3252
|
// 📐 The implicit budget of 0 is a statement about rigc's own GENERATORS:
|
|
3245
3253
|
// geometry rigc built is geometry rigc will not ship unmeasured, and
|
|
3246
3254
|
// `A13_MESH_BUDGET` has nothing to measure a generated mesh against until the
|
|
@@ -3920,6 +3928,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3920
3928
|
header,
|
|
3921
3929
|
atlasText,
|
|
3922
3930
|
declaredDurations,
|
|
3931
|
+
...(alphaClips.length ? { alphaClips } : {}),
|
|
3923
3932
|
};
|
|
3924
3933
|
}
|
|
3925
3934
|
|
|
@@ -4342,6 +4351,7 @@ function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<
|
|
|
4342
4351
|
// ---------------------------------------------------------------------------
|
|
4343
4352
|
|
|
4344
4353
|
interface AttachmentContext {
|
|
4354
|
+
pendingAlphaClips: PendingAlphaClip[];
|
|
4345
4355
|
images: CompiledImage[];
|
|
4346
4356
|
bones: ModelBone[];
|
|
4347
4357
|
transforms: Map<string, BoneTransform>;
|
|
@@ -4933,6 +4943,273 @@ function buildRigBoundingBox(
|
|
|
4933
4943
|
return out;
|
|
4934
4944
|
}
|
|
4935
4945
|
|
|
4946
|
+
interface PendingAlphaClip {
|
|
4947
|
+
spec: RigAlphaClip;
|
|
4948
|
+
out: ModelClippingAttachment;
|
|
4949
|
+
where: string;
|
|
4950
|
+
ctx: AttachmentContext;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
/** Resolve after all sources/links exist: draw order cannot decide whether a source can be measured. */
|
|
4954
|
+
function resolveAlphaClips(
|
|
4955
|
+
pending: PendingAlphaClip[],
|
|
4956
|
+
tables: Map<string, SkinTable>,
|
|
4957
|
+
rig: RigSpec,
|
|
4958
|
+
motion: MotionSpec,
|
|
4959
|
+
): NonNullable<CompileResult['alphaClips']> {
|
|
4960
|
+
const reports: NonNullable<CompileResult['alphaClips']> = [];
|
|
4961
|
+
for (const { spec, out, where, ctx } of pending) {
|
|
4962
|
+
const targetTable = tables.get(ctx.skinName)?.[ctx.slotName];
|
|
4963
|
+
const target = Object.entries(targetTable ?? {}).find(([, value]) => value === out)?.[0];
|
|
4964
|
+
// withStatedName can return a fresh object when name was declared.
|
|
4965
|
+
const targetKey = target ?? Object.entries(targetTable ?? {}).find(([, value]) => value.kind === 'clipping' && value.vertices === out.vertices)?.[0];
|
|
4966
|
+
if (targetKey === undefined) {
|
|
4967
|
+
throw new CompileError(
|
|
4968
|
+
`${where}: generated clipping target was not filed in its skin; required a resolved attachment.`,
|
|
4969
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4970
|
+
);
|
|
4971
|
+
}
|
|
4972
|
+
for (const key of ['slot', 'skin', 'attachment'] as const) {
|
|
4973
|
+
if (typeof spec[key] !== 'string' || spec[key].length === 0) {
|
|
4974
|
+
throw new CompileError(
|
|
4975
|
+
`${where}: alphaClip.${key} is ${JSON.stringify(spec[key])}; required an explicit nonempty source name.`,
|
|
4976
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4977
|
+
);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
const sourceWhere = `${spec.skin}/${spec.slot}/${spec.attachment}`;
|
|
4981
|
+
const label = `${where} alphaClip from "${sourceWhere}"`;
|
|
4982
|
+
const source = tables.get(spec.skin)?.[spec.slot]?.[spec.attachment];
|
|
4983
|
+
const sourceSlot = rig.slots.find((slot) => slot.name === spec.slot);
|
|
4984
|
+
if (!source || !sourceSlot) {
|
|
4985
|
+
throw new CompileError(
|
|
4986
|
+
`${label}: source attachment is absent; required a declared slot/skin/attachment.`,
|
|
4987
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4988
|
+
);
|
|
4989
|
+
}
|
|
4990
|
+
if (source.kind !== 'region' && source.kind !== 'mesh' && source.kind !== 'linkedmesh') {
|
|
4991
|
+
throw new CompileError(
|
|
4992
|
+
`${label}: source kind is ${source.kind}; required a region or resolved mesh with pixels.`,
|
|
4993
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4994
|
+
);
|
|
4995
|
+
}
|
|
4996
|
+
let geometry: SkinTableEntry = source;
|
|
4997
|
+
let depthKey = sourceWhere;
|
|
4998
|
+
if (geometry.kind === 'linkedmesh') {
|
|
4999
|
+
depthKey = `${geometry.skin}/${geometry.slot}/${geometry.source}`;
|
|
5000
|
+
const base = tables.get(geometry.skin)?.[geometry.slot]?.[geometry.source];
|
|
5001
|
+
if (!base || base.kind !== 'mesh') {
|
|
5002
|
+
throw new CompileError(
|
|
5003
|
+
`${label}: linked source resolves to ${base?.kind ?? 'nothing'}; required a resolved mesh.`,
|
|
5004
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
geometry = base;
|
|
5008
|
+
}
|
|
5009
|
+
if (geometry.kind !== 'region' && geometry.kind !== 'mesh') {
|
|
5010
|
+
throw new CompileError(
|
|
5011
|
+
`${label}: source is ${(geometry as SkinTableEntry).kind}; required region or mesh geometry with a plate (a clipping source would be a dependency cycle).`,
|
|
5012
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5013
|
+
);
|
|
5014
|
+
}
|
|
5015
|
+
if (source.sequence !== undefined) {
|
|
5016
|
+
throw new CompileError(
|
|
5017
|
+
`${label}: source has a ${source.sequence.count}-frame sequence; required one immutable source plate. Select a separate named attachment for the measured frame.`,
|
|
5018
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
const region = source.path ?? source.name ?? spec.attachment;
|
|
5022
|
+
const img = ctx.images.find((image) => image.region === region);
|
|
5023
|
+
if (!img) {
|
|
5024
|
+
throw new CompileError(
|
|
5025
|
+
`${label}: region "${region}" has no source plate; required image/atlas pixels.`,
|
|
5026
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
if (img.pageGrid !== undefined) {
|
|
5030
|
+
throw new CompileError(
|
|
5031
|
+
`${label}: source plate grid cannot be located. ${img.pageGrid.sentence}`,
|
|
5032
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5033
|
+
);
|
|
5034
|
+
}
|
|
5035
|
+
const plate = partPlate(img);
|
|
5036
|
+
const mask = { width: plate.width, height: plate.height, alpha: plateAlpha(plate) };
|
|
5037
|
+
const traced = alphaClipOutline(mask, spec, label, img.atlasScale ?? 1);
|
|
5038
|
+
const ring = traced.points.map(([x, y]): [number, number] => [x / plate.width, y / plate.height]);
|
|
5039
|
+
const anchor = ctx.transforms.get(sourceSlot.bone)!;
|
|
5040
|
+
let sourceBindings: ModelBinding[][];
|
|
5041
|
+
let uvs: number[];
|
|
5042
|
+
let triangles: number[];
|
|
5043
|
+
if (geometry.kind === 'region') {
|
|
5044
|
+
// Full restored plate, not trimmed page rectangle: source crop pixels remain the coordinates.
|
|
5045
|
+
const corners = regionCorners({ ...geometry, atlas: { width: img.width, height: img.height, originalWidth: img.width, originalHeight: img.height, offsetX: 0, offsetY: 0 } }, anchor);
|
|
5046
|
+
uvs = [0, 1, 0, 0, 1, 0, 1, 1];
|
|
5047
|
+
triangles = [0, 1, 2, 2, 3, 0];
|
|
5048
|
+
sourceBindings = Array.from({ length: 4 }, (_, i) => {
|
|
5049
|
+
const [x, y] = toBoneLocal(anchor, corners[2 * i], corners[2 * i + 1]);
|
|
5050
|
+
return [{ bone: sourceSlot.bone, x, y, weight: 1 }];
|
|
5051
|
+
});
|
|
5052
|
+
} else {
|
|
5053
|
+
uvs = geometry.uvs;
|
|
5054
|
+
triangles = geometry.triangles;
|
|
5055
|
+
const vertices = geometry.vertices;
|
|
5056
|
+
sourceBindings = vertices.weighted ? vertices.bindings :
|
|
5057
|
+
Array.from({ length: vertices.xy.length / 2 }, (_, i) => [{
|
|
5058
|
+
bone: sourceSlot.bone, x: vertices.xy[2 * i], y: vertices.xy[2 * i + 1], weight: 1,
|
|
5059
|
+
}]);
|
|
5060
|
+
}
|
|
5061
|
+
const carried = clipCarriers(ring, uvs, triangles, label);
|
|
5062
|
+
if (carried.points.length > spec.maxVertices) {
|
|
5063
|
+
throw new CompileError(
|
|
5064
|
+
`${label}: polygonoverdeclaredvertexbudget: ${carried.points.length} vertices after source carrier crossings (${traced.points.length} simplified), required <=${spec.maxVertices}. Raise the declared budget or simplify the source/outline.`,
|
|
5065
|
+
'CE_COMPILE_ALPHA_CLIP_VERTEX_BUDGET',
|
|
5066
|
+
);
|
|
5067
|
+
}
|
|
5068
|
+
// Per-bone first moments preserve Σβ Σw M(bind) for every bone matrix; no solver or new pose model.
|
|
5069
|
+
const bindings: ModelBinding[][] = carried.carriers.map((carrier) => {
|
|
5070
|
+
const moments = new Map<string, { weight: number; x: number; y: number }>();
|
|
5071
|
+
carrier.corners.forEach((index, corner) => {
|
|
5072
|
+
for (const binding of sourceBindings[index]) {
|
|
5073
|
+
const share = carrier.bary[corner] * binding.weight;
|
|
5074
|
+
const row = moments.get(binding.bone) ?? { weight: 0, x: 0, y: 0 };
|
|
5075
|
+
row.weight += share;
|
|
5076
|
+
row.x += share * binding.x;
|
|
5077
|
+
row.y += share * binding.y;
|
|
5078
|
+
moments.set(binding.bone, row);
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
5081
|
+
const result: ModelBinding[] = [];
|
|
5082
|
+
for (const [bone, row] of moments) {
|
|
5083
|
+
if (row.weight > 0) {
|
|
5084
|
+
result.push({ bone, weight: f32(row.weight), x: f32(row.x / row.weight), y: f32(row.y / row.weight) });
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
return result;
|
|
5088
|
+
});
|
|
5089
|
+
out.vertices = { weighted: true, bindings };
|
|
5090
|
+
out.vertexCount = bindings.length;
|
|
5091
|
+
const mappedWorld = setupWorldVertices(out.vertices, anchor, ctx.transforms, label)
|
|
5092
|
+
.map(([x, y]): [number, number] => [Math.fround(x), Math.fround(y)]);
|
|
5093
|
+
refuseClipDegeneracy(mappedWorld, `${label} final float32 world coordinates`);
|
|
5094
|
+
refuseClipCrossing(mappedWorld, label);
|
|
5095
|
+
const sourceWorld = sourceBindings.map((vertex): [number, number] => {
|
|
5096
|
+
let x = 0;
|
|
5097
|
+
let y = 0;
|
|
5098
|
+
for (const binding of vertex) {
|
|
5099
|
+
const [wx, wy] = toWorld(ctx.transforms.get(binding.bone)!, binding.x, binding.y);
|
|
5100
|
+
x += binding.weight * wx;
|
|
5101
|
+
y += binding.weight * wy;
|
|
5102
|
+
}
|
|
5103
|
+
return [x, y];
|
|
5104
|
+
});
|
|
5105
|
+
let maxBindingDeviationWorld = 0;
|
|
5106
|
+
carried.carriers.forEach((carrier, index) => {
|
|
5107
|
+
let x = 0;
|
|
5108
|
+
let y = 0;
|
|
5109
|
+
carrier.corners.forEach((corner, k) => {
|
|
5110
|
+
x += carrier.bary[k] * sourceWorld[corner][0];
|
|
5111
|
+
y += carrier.bary[k] * sourceWorld[corner][1];
|
|
5112
|
+
});
|
|
5113
|
+
maxBindingDeviationWorld = Math.max(
|
|
5114
|
+
maxBindingDeviationWorld,
|
|
5115
|
+
hypot2(mappedWorld[index][0] - x, mappedWorld[index][1] - y),
|
|
5116
|
+
);
|
|
5117
|
+
});
|
|
5118
|
+
const filed = targetTable![targetKey];
|
|
5119
|
+
if (filed.kind === 'clipping') {
|
|
5120
|
+
filed.vertices = out.vertices;
|
|
5121
|
+
filed.vertexCount = out.vertexCount;
|
|
5122
|
+
}
|
|
5123
|
+
// Authored depth turns use setup-world geometry, even for a rigid source.
|
|
5124
|
+
// Preserve that frame on the clip's own vertex/influence roster, not the source's.
|
|
5125
|
+
const sourceDepth = ctx.depths.get(depthKey);
|
|
5126
|
+
const z = sourceDepth === undefined || Array.isArray(sourceDepth) ? sourceDepth : sourceDepth.z;
|
|
5127
|
+
let clipDepthGeometry: DeformGeometry | undefined;
|
|
5128
|
+
if (z !== undefined) {
|
|
5129
|
+
const depths = carried.carriers.map((carrier) =>
|
|
5130
|
+
carrier.corners.reduce((sum, vertex, i) => sum + carrier.bary[i] * z[vertex], 0));
|
|
5131
|
+
if (sourceDepth !== undefined && !Array.isArray(sourceDepth)) {
|
|
5132
|
+
clipDepthGeometry = {
|
|
5133
|
+
...deformGeometryOf(out, label, ctx.transforms, ctx.transforms.get(ctx.anchorBone)!),
|
|
5134
|
+
depth: depths,
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${targetKey}`,
|
|
5138
|
+
clipDepthGeometry === undefined ? depths : { z: depths, geometry: clipDepthGeometry });
|
|
5139
|
+
}
|
|
5140
|
+
// The helper, not the number of distinct bone names, defines the frame.
|
|
5141
|
+
// Duplicate influences on one bone also take the world-space path.
|
|
5142
|
+
const frameOf = (attachment: ModelVertexAttachment, unweightedBone: string, depthGeometry: DeformGeometry | undefined,
|
|
5143
|
+
key: MotionDeformTrack['keys'][number] | undefined): {
|
|
5144
|
+
name: string; geometry: DeformGeometry;
|
|
5145
|
+
} => {
|
|
5146
|
+
// Match compileDeformTrack's per-key choice, including authored depth:true.
|
|
5147
|
+
const geometry = (key?.transform !== undefined && 'depth' in key.transform && key.transform.depth === true
|
|
5148
|
+
? depthGeometry : undefined) ?? deformGeometryOf(attachment, label, ctx.transforms);
|
|
5149
|
+
const vertices = attachment.vertices;
|
|
5150
|
+
const bone = vertices.weighted ? vertices.bindings[0]?.[0]?.bone : unweightedBone;
|
|
5151
|
+
return { name: geometry.influenceBones === null ? `bone:${bone}` : 'world', geometry };
|
|
5152
|
+
};
|
|
5153
|
+
for (const [animationName, animation] of Object.entries(motion.animations ?? {})) {
|
|
5154
|
+
const sourceIdentity = source.kind === 'linkedmesh' && source.timelines ? depthKey : sourceWhere;
|
|
5155
|
+
const targetIdentity = `${ctx.skinName}/${ctx.slotName}/${targetKey}`;
|
|
5156
|
+
const sourceTrack = animation.deform?.find((track) =>
|
|
5157
|
+
`${track.skin ?? 'default'}/${track.slot}/${track.attachment}` === sourceIdentity);
|
|
5158
|
+
const clipTrack = animation.deform?.find((track) =>
|
|
5159
|
+
`${track.skin ?? 'default'}/${track.slot}/${track.attachment}` === targetIdentity);
|
|
5160
|
+
if (!sourceTrack && !clipTrack) continue;
|
|
5161
|
+
const sourceDepthGeometry = sourceDepth !== undefined && !Array.isArray(sourceDepth) ? sourceDepth.geometry : undefined;
|
|
5162
|
+
const frames = (sourceTrack?.keys.length ? sourceTrack.keys : [undefined]).map((key, index) => ({
|
|
5163
|
+
source: geometry.kind === 'mesh' ? frameOf(geometry, sourceSlot.bone, sourceDepthGeometry, key) : null,
|
|
5164
|
+
clip: frameOf(out, ctx.anchorBone, clipDepthGeometry, clipTrack?.keys[index]),
|
|
5165
|
+
}));
|
|
5166
|
+
const compatible = frames.every(({ source, clip }) => source !== null && source.geometry.setup !== null &&
|
|
5167
|
+
clip.geometry.setup !== null && source.name === clip.name);
|
|
5168
|
+
const { source: sourceFrame, clip: clipFrame } = frames.find(({ source, clip }) =>
|
|
5169
|
+
source === null || source.geometry.setup === null || clip.geometry.setup === null || source.name !== clip.name) ?? frames[0];
|
|
5170
|
+
// With a world-space deform, varying per-vertex influence shares would
|
|
5171
|
+
// correlate the deform with moving bone matrices. Constant shares commute
|
|
5172
|
+
// with the affine/depth projection; no new deformation transfer is implied.
|
|
5173
|
+
const sharesOf = (vertex: ModelBinding[]): string => {
|
|
5174
|
+
const shares = new Map<string, number>();
|
|
5175
|
+
for (const binding of vertex) {
|
|
5176
|
+
shares.set(binding.bone, (shares.get(binding.bone) ?? 0) + binding.weight);
|
|
5177
|
+
}
|
|
5178
|
+
return JSON.stringify([...shares].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
5179
|
+
};
|
|
5180
|
+
const constantWorldShares = !frames.some(({ source }) => source?.name === 'world') ||
|
|
5181
|
+
sourceBindings.every((vertex) => sharesOf(vertex) === sharesOf(sourceBindings[0]));
|
|
5182
|
+
const shared = sourceTrack !== undefined && clipTrack !== undefined &&
|
|
5183
|
+
JSON.stringify(sourceTrack.keys) === JSON.stringify(clipTrack.keys);
|
|
5184
|
+
const linear = sourceTrack?.keys.every((key) => {
|
|
5185
|
+
const transform = key.transform;
|
|
5186
|
+
return transform !== undefined && transform.envelope === undefined &&
|
|
5187
|
+
(transform.kind === 'affine' || ((transform.kind === 'yaw' || transform.kind === 'pitch') &&
|
|
5188
|
+
transform.depth === true && z !== undefined));
|
|
5189
|
+
});
|
|
5190
|
+
if (!shared || !linear || !compatible || !constantWorldShares) {
|
|
5191
|
+
throw new CompileError(
|
|
5192
|
+
`${label}: animation "${animationName}" has ${sourceTrack?.keys.length ?? 0} source and ` +
|
|
5193
|
+
`${clipTrack?.keys.length ?? 0} clip keys; source frame=${sourceFrame?.name ?? 'no vertex geometry'}, ` +
|
|
5194
|
+
`clip frame=${clipFrame.name}, constant world influence shares=${constantWorldShares}. ` +
|
|
5195
|
+
'Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ' +
|
|
5196
|
+
'with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.',
|
|
5197
|
+
'CE_COMPILE_ALPHA_CLIP_DEFORM',
|
|
5198
|
+
);
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
reports.push({
|
|
5202
|
+
target: `${ctx.skinName}/${ctx.slotName}/${targetKey}`,
|
|
5203
|
+
source: sourceWhere,
|
|
5204
|
+
...traced.report,
|
|
5205
|
+
vertices: out.vertexCount,
|
|
5206
|
+
uvs: carried.points,
|
|
5207
|
+
maxBindingDeviationWorld,
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
return reports;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
4936
5213
|
function buildRigClipping(
|
|
4937
5214
|
att: RigClippingAttachment,
|
|
4938
5215
|
where: string,
|
|
@@ -4956,9 +5233,15 @@ function buildRigClipping(
|
|
|
4956
5233
|
...(att.end !== undefined ? { end: att.end } : {}),
|
|
4957
5234
|
...(att.convex !== undefined ? { convex: att.convex } : {}),
|
|
4958
5235
|
...(att.inverse !== undefined ? { inverse: att.inverse } : {}),
|
|
4959
|
-
vertexCount: att.vertexCount,
|
|
4960
|
-
vertices: buildVertexGeometry(att, where, ctx),
|
|
5236
|
+
vertexCount: att.vertexCount ?? 0,
|
|
5237
|
+
vertices: att.alphaClip === undefined ? buildVertexGeometry({ ...att, vertexCount: att.vertexCount! }, where, ctx) : { weighted: false, xy: [] },
|
|
4961
5238
|
};
|
|
5239
|
+
if (att.alphaClip !== undefined) {
|
|
5240
|
+
const fields = ['vertexCount', 'vertices', 'weights', 'boneIndexing'] as const;
|
|
5241
|
+
const present = fields.filter((key) => att[key] !== undefined);
|
|
5242
|
+
if (present.length || att.convex === true || att.inverse === true) throw new CompileError(`${where}: alphaClip conflicts with [${present.join(', ')}], convex=${att.convex}, inverse=${att.inverse}; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.`, 'CE_COMPILE_ALPHA_CLIP_CONFLICT');
|
|
5243
|
+
ctx.pendingAlphaClips.push({ spec: att.alphaClip, out, where, ctx });
|
|
5244
|
+
}
|
|
4962
5245
|
if (att.color !== undefined) out.editorColor = att.color;
|
|
4963
5246
|
return out;
|
|
4964
5247
|
}
|
package/src/core/clipping.ts
CHANGED
|
@@ -633,7 +633,11 @@ export function clipThrough(
|
|
|
633
633
|
clip: TriangleClipper = clipTriangles,
|
|
634
634
|
reading: { dropRegion?: number } = {},
|
|
635
635
|
): ClipResult | null {
|
|
636
|
-
|
|
636
|
+
let plan = clipShapeOf(shape);
|
|
637
|
+
// CO11/CL01/CN06 and portal's convex rows need the original world doubles.
|
|
638
|
+
// Two 66-point region/authored up_minus15 pieces witnesses went from 2 to 0
|
|
639
|
+
// rendered pixel differences with Float32 input; SDK clipper storage was not observed.
|
|
640
|
+
if (plan.kind === 'pieces') plan = clipShapeOf({ ...shape, polygon: shape.polygon.map(Math.fround) });
|
|
637
641
|
if (plan.kind === 'refused') return null;
|
|
638
642
|
if (plan.kind === 'convex') return clip(plan.polygon, vertices, triangles, uvs);
|
|
639
643
|
const f = Math.fround;
|
package/src/mesh.ts
CHANGED
|
@@ -930,7 +930,9 @@ export function segmentsMeet(
|
|
|
930
930
|
const o2 = o(a, b, d);
|
|
931
931
|
const o3 = o(c, d, a);
|
|
932
932
|
const o4 = o(c, d, b);
|
|
933
|
-
|
|
933
|
+
// An epsilon-zero orientation is not a strict side of a line. In that case
|
|
934
|
+
// only the endpoint tests below can establish contact (including the bounds).
|
|
935
|
+
if (o1 !== 0 && o2 !== 0 && o3 !== 0 && o4 !== 0 && o1 !== o2 && o3 !== o4) return true;
|
|
934
936
|
return onSegment(a, b, c) || onSegment(a, b, d) || onSegment(c, d, a) || onSegment(c, d, b);
|
|
935
937
|
}
|
|
936
938
|
|
package/src/rig.ts
CHANGED
|
@@ -1124,7 +1124,26 @@ export interface RigBoundingBoxAttachment extends RigVertexGeometry {
|
|
|
1124
1124
|
* never ends — it runs to the bottom of the draw order and takes every slot
|
|
1125
1125
|
* below it with it. rigc refuses a name the rig does not declare.
|
|
1126
1126
|
*/
|
|
1127
|
-
|
|
1127
|
+
/** A clipping outline measured from one named attachment's plate. No inferred parameters. */
|
|
1128
|
+
export interface RigAlphaClip {
|
|
1129
|
+
slot: string;
|
|
1130
|
+
skin: string;
|
|
1131
|
+
attachment: string;
|
|
1132
|
+
alpha: number;
|
|
1133
|
+
tolerance: number;
|
|
1134
|
+
maxVertices: number;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export interface RigClippingAttachment {
|
|
1138
|
+
/** Authored geometry uses the same named/raw contract as RigVertexGeometry. */
|
|
1139
|
+
vertices?: number[];
|
|
1140
|
+
weights?: RigMeshBinding[][];
|
|
1141
|
+
boneIndexing?: 'name' | 'raw';
|
|
1142
|
+
color?: string;
|
|
1143
|
+
/** Required for authored geometry; derived for alphaClip. */
|
|
1144
|
+
vertexCount?: number;
|
|
1145
|
+
/** Exclusive with authored vertexCount/vertices/weights/boneIndexing. */
|
|
1146
|
+
alphaClip?: RigAlphaClip;
|
|
1128
1147
|
type: 'clipping';
|
|
1129
1148
|
/** The runtime's attachment name — see `RigAttachmentName`. Absent, it is the placeholder. */
|
|
1130
1149
|
name?: RigAttachmentName;
|
|
@@ -2089,7 +2108,8 @@ export const RIG_KEYS = {
|
|
|
2089
2108
|
RigMeshBinding: ['bone', 'x', 'y', 'weight'],
|
|
2090
2109
|
RigSequence: ['count', 'start', 'digits', 'setup'],
|
|
2091
2110
|
RigBoundingBoxAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name'],
|
|
2092
|
-
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse'],
|
|
2111
|
+
RigClippingAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'end', 'convex', 'inverse', 'alphaClip'],
|
|
2112
|
+
RigAlphaClip: ['slot', 'skin', 'attachment', 'alpha', 'tolerance', 'maxVertices'],
|
|
2093
2113
|
RigPathAttachment: ['vertexCount', 'vertices', 'weights', 'boneIndexing', 'color', 'type', 'name', 'closed', 'constantSpeed', 'lengths'],
|
|
2094
2114
|
RigRingGenerator: ['kind', 'hull', 'center', 'inner', 'size', 'bias', 'controls'],
|
|
2095
2115
|
RigRibbonGenerator: ['kind', 'size', 'rows', 'chain'],
|
|
@@ -2218,8 +2238,9 @@ export const RIG_TYPES = {
|
|
|
2218
2238
|
},
|
|
2219
2239
|
RigClippingAttachment: {
|
|
2220
2240
|
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2221
|
-
end: 'string', convex: 'boolean', inverse: 'boolean',
|
|
2241
|
+
end: 'string', convex: 'boolean', inverse: 'boolean', alphaClip: 'object',
|
|
2222
2242
|
},
|
|
2243
|
+
RigAlphaClip: { slot: 'string', skin: 'string', attachment: 'string', alpha: 'number', tolerance: 'number', maxVertices: 'number' },
|
|
2223
2244
|
RigPathAttachment: {
|
|
2224
2245
|
vertexCount: 'number', vertices: 'number[]', weights: 'object[][]', boneIndexing: 'enum', color: 'string', type: 'enum', name: 'string',
|
|
2225
2246
|
closed: 'boolean', constantSpeed: 'boolean', lengths: 'number[]',
|
|
@@ -2550,6 +2571,7 @@ function checkRigSpecKeys(raw: Record<string, unknown>, where: string): Array<{
|
|
|
2550
2571
|
}
|
|
2551
2572
|
const here = [...base, slot, placeholder];
|
|
2552
2573
|
at(att, shape, `${who} (${type})`, here);
|
|
2574
|
+
if (type === 'clipping') at(att.alphaClip, 'RigAlphaClip', `${who} alphaClip`, [...here, 'alphaClip']);
|
|
2553
2575
|
if (att.sequence !== undefined) {
|
|
2554
2576
|
at(att.sequence, 'RigSequence', `${who} "sequence"`, [...here, 'sequence']);
|
|
2555
2577
|
checkRigSequence(att, who, where);
|
package/src/runtime/curves.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* Nothing here allocates once compiled.
|
|
26
26
|
*/
|
|
27
27
|
import { bezierPolyline, BEZIER_PIECES, type CoreKey } from '../core/animation.ts';
|
|
28
|
+
import { DEFORM_CURVE_END, type CoreDeformKey } from '../core/deform.ts';
|
|
28
29
|
|
|
29
30
|
/** A segment's curve, per key. */
|
|
30
31
|
export const CURVE_LINEAR = 0;
|
|
@@ -104,3 +105,37 @@ export function channelValue(curve: RuntimeCurve, i: number, c: number, t: numbe
|
|
|
104
105
|
const y0 = p[o + q - 1];
|
|
105
106
|
return y0 + ((t - x0) / (p[o + q] - x0)) * (p[o + q + 1] - y0);
|
|
106
107
|
}
|
|
108
|
+
|
|
109
|
+
/** Deform easing has one fraction channel, a distinct recurrence end and end-piece arithmetic (core/deform.ts). */
|
|
110
|
+
export function compileDeformCurve(keys: readonly CoreDeformKey[]): RuntimeCurve {
|
|
111
|
+
const curve = compileCurve(keys.map(k => ({ time: k.time, values: [0], curve: 'linear' as const, stated: { time: k.stated, values: [0] } })), 1);
|
|
112
|
+
for (let i = 0; i < keys.length; i++) {
|
|
113
|
+
const a = keys[i];
|
|
114
|
+
curve.kinds[i] = a.curve === 'linear' ? CURVE_LINEAR : a.curve === 'stepped' ? CURVE_STEPPED : CURVE_BEZIER;
|
|
115
|
+
if (typeof a.curve === 'string' || i + 1 === keys.length) continue;
|
|
116
|
+
const b = keys[i + 1];
|
|
117
|
+
const c = a.curve;
|
|
118
|
+
const inner = bezierPolyline(a.stated, 0, c[0], c[1], c[2], c[3], b.stated, DEFORM_CURVE_END);
|
|
119
|
+
const o = i * POLYLINE;
|
|
120
|
+
curve.polylines[o] = a.time;
|
|
121
|
+
curve.polylines[o + 1] = 0;
|
|
122
|
+
curve.polylines.set(inner, o + 2);
|
|
123
|
+
curve.polylines[o + POLYLINE - 2] = b.time;
|
|
124
|
+
curve.polylines[o + POLYLINE - 1] = 1;
|
|
125
|
+
}
|
|
126
|
+
return curve;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The oracle's deformPercent, with the polyline compiled once and product-before-division at both ends. */
|
|
130
|
+
export function deformFraction(curve: RuntimeCurve, i: number, t: number): number {
|
|
131
|
+
if (curve.kinds[i] === CURVE_STEPPED) return 0;
|
|
132
|
+
if (curve.kinds[i] === CURVE_LINEAR) return (t - curve.times[i]) / (curve.times[i + 1] - curve.times[i]);
|
|
133
|
+
const p = curve.polylines;
|
|
134
|
+
const o = i * POLYLINE;
|
|
135
|
+
let q = 2;
|
|
136
|
+
while (q < POLYLINE - 2 && p[o + q] < t) q += 2;
|
|
137
|
+
const x0 = p[o + q - 2], y0 = p[o + q - 1], x1 = p[o + q], y1 = p[o + q + 1];
|
|
138
|
+
if (q === 2) return (y1 * (t - x0)) / (x1 - x0);
|
|
139
|
+
if (q === POLYLINE - 2) return y0 + ((1 - y0) * (t - x0)) / (x1 - x0);
|
|
140
|
+
return y0 + ((t - x0) / (x1 - x0)) * (y1 - y0);
|
|
141
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* corpus through both at the oracle's samples and compares full doubles at
|
|
22
22
|
* tolerance 0 (*The runtime poser* there). The oracle is not changed for it.
|
|
23
23
|
*
|
|
24
|
-
* ## Stage
|
|
24
|
+
* ## Stage 2: what it poses, and what it refuses
|
|
25
25
|
*
|
|
26
26
|
* Bones and their five inherit modes; the rotate, translate, scale and shear
|
|
27
27
|
* timelines; a slot's attachment and colour (`rgba`, `rgb`, `alpha`)
|
|
28
|
-
* timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
28
|
+
* timelines; weighted/unweighted deform timelines; region, mesh, weighted mesh and linked mesh vertices; one- and
|
|
29
29
|
* two-bone ik and world-space, absolute transform constraints with their
|
|
30
30
|
* timelines; skin-required bones and constraints under the view's skin. One
|
|
31
31
|
* animation at full weight from the setup pose, at a time or stepped as a
|