Skip to content

Commit c8c661d

Browse files
committed
implement "baked animations" setting in plugin export
1 parent 2992686 commit c8c661d

1 file changed

Lines changed: 128 additions & 65 deletions

File tree

src/systems/pluginCompiler.ts

Lines changed: 128 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -506,96 +506,161 @@ function serializeTexture(texture: Texture): PluginTexture {
506506
} satisfies PluginTexture)
507507
}
508508

509-
function serializeAnimation(options: {
509+
function buildLoopMode(animation: IRenderedAnimation): LoopMode {
510+
switch (animation.loop_mode) {
511+
case 'loop':
512+
return { type: 'loop', loop_delay: String(animation.loop_delay ?? 0) }
513+
case 'hold':
514+
return { type: 'hold' }
515+
default:
516+
return { type: 'once' }
517+
}
518+
}
519+
520+
function pad3<T>(arr: ArrayLike<T>, fill: T): [T, T, T] {
521+
return [arr[0] ?? fill, arr[1] ?? fill, arr[2] ?? fill]
522+
}
523+
524+
function keyframeInterpolation(kf: _Keyframe): TransformationKeyframeInterpolation {
525+
switch (kf.interpolation) {
526+
case 'bezier':
527+
return {
528+
type: 'bezier',
529+
left_handle_time: pad3(kf.bezier_left_time, 0),
530+
left_handle_value: pad3(kf.bezier_left_value, 0),
531+
right_handle_time: pad3(kf.bezier_right_time, 0),
532+
right_handle_value: pad3(kf.bezier_right_value, 0),
533+
}
534+
case 'catmullrom':
535+
return { type: 'catmullrom' }
536+
case 'step':
537+
return { type: 'step' }
538+
default: {
539+
const out: TransformationKeyframeInterpolation = {
540+
type: 'linear',
541+
easing: kf.easing ?? 'linear',
542+
}
543+
if (kf.easingArgs?.length) out.easing_arguments = kf.easingArgs.slice()
544+
return out
545+
}
546+
}
547+
}
548+
549+
function keyframeDataPoint(kf: _Keyframe, index: number): [string, string, string] {
550+
return [
551+
String(kf.get('x', index)),
552+
String(kf.get('y', index)),
553+
String(kf.get('z', index)),
554+
]
555+
}
556+
557+
function serializeRawAnimation(options: {
510558
animation: IRenderedAnimation
511559
nodeUuidToId: Map<string, string>
512560
paletteIds: string[]
513561
}): PluginAnimation {
514-
const { animation, nodeUuidToId } = options
562+
const { animation, nodeUuidToId, paletteIds } = options
563+
const bbAnimation = Blockbench.Animation.all.find(a => a.uuid === animation.uuid)
564+
if (!bbAnimation) {
565+
throw new IntentionalExportError(
566+
`Could not locate source animation for <code>${animation.name}</code>.`
567+
)
568+
}
515569

516570
// eslint-disable-next-line @typescript-eslint/naming-convention
517-
const loop_mode: LoopMode =
518-
animation.loop_mode === 'loop'
519-
? { type: 'loop', loop_delay: String(animation.loop_delay ?? 0) }
520-
: animation.loop_mode === 'hold'
521-
? { type: 'hold' }
522-
: { type: 'once' }
571+
const node_keyframes: NonNullable<PluginAnimation['node_keyframes']> = {}
572+
// eslint-disable-next-line @typescript-eslint/naming-convention
573+
let global_keyframes: PluginAnimation['global_keyframes']
574+
575+
for (const [animatorUuid, animator] of Object.entries(bbAnimation.animators)) {
576+
// @ts-expect-error - broken bb types
577+
const keyframes: _Keyframe[] | undefined = animator?.keyframes
578+
if (!keyframes?.length) continue
579+
580+
const nodeId = nodeUuidToId.get(animatorUuid)
581+
582+
for (const kf of keyframes) {
583+
const timeKey = formatTimestamp(kf.time)
584+
585+
if (kf.channel === 'position' || kf.channel === 'rotation' || kf.channel === 'scale') {
586+
if (!nodeId) continue
587+
const channels = (node_keyframes[nodeId] ??= {})
588+
const bucket = (channels[kf.channel] ??= {})
589+
const entry: TransformationKeyframe = {
590+
value: keyframeDataPoint(kf, 0),
591+
interpolation: keyframeInterpolation(kf),
592+
}
593+
if (kf.data_points.length === 2) entry.post = keyframeDataPoint(kf, 1)
594+
bucket[timeKey] = entry
595+
} else if (kf.channel === 'variant' && paletteIds.length && kf.variant) {
596+
global_keyframes ??= {}
597+
const texture = (global_keyframes.texture ??= {})
598+
const slot = (texture[timeKey] ??= {})
599+
for (const paletteId of paletteIds) slot[paletteId] = kf.variant.name
600+
}
601+
}
602+
}
523603

524-
const maxTime = animation.frames.at(-1)?.time ?? 0
604+
return scrubUndefined({
605+
loop_mode: buildLoopMode(animation),
606+
blend_weight: '1',
607+
start_delay: '0',
608+
length: bbAnimation.length,
609+
global_keyframes,
610+
node_keyframes,
611+
} satisfies PluginAnimation)
612+
}
613+
614+
function serializeBakedAnimation(options: {
615+
animation: IRenderedAnimation
616+
nodeUuidToId: Map<string, string>
617+
paletteIds: string[]
618+
}): PluginAnimation {
619+
const { animation, nodeUuidToId, paletteIds } = options
525620

526621
// eslint-disable-next-line @typescript-eslint/naming-convention
527622
const node_keyframes: NonNullable<PluginAnimation['node_keyframes']> = {}
623+
// eslint-disable-next-line @typescript-eslint/naming-convention
624+
let global_keyframes: PluginAnimation['global_keyframes']
528625

529626
for (const frame of animation.frames) {
530627
const timeKey = formatTimestamp(frame.time)
628+
531629
for (const [uuid, transform] of Object.entries(frame.node_transforms)) {
532630
const nodeId = nodeUuidToId.get(uuid)
533631
if (!nodeId) continue
534-
node_keyframes[nodeId] ??= {}
535632

536-
const createInterpolation = (): TransformationKeyframeInterpolation =>
633+
const interpolation: TransformationKeyframeInterpolation =
537634
transform.interpolation === 'step' || transform.interpolation === 'pre-post'
538635
? { type: 'step' }
539636
: { type: 'linear', easing: 'linear' }
540637

541-
node_keyframes[nodeId].position ??= {}
542-
node_keyframes[nodeId].rotation ??= {}
543-
node_keyframes[nodeId].scale ??= {}
544-
545-
node_keyframes[nodeId].position![timeKey] = {
546-
value: [
547-
toMolangNumber(transform.pos[0]),
548-
toMolangNumber(transform.pos[1]),
549-
toMolangNumber(transform.pos[2]),
550-
],
551-
interpolation: createInterpolation(),
552-
}
553-
node_keyframes[nodeId].rotation![timeKey] = {
554-
value: [
555-
toMolangNumber(transform.rot[0]),
556-
toMolangNumber(transform.rot[1]),
557-
toMolangNumber(transform.rot[2]),
558-
],
559-
interpolation: createInterpolation(),
560-
}
561-
node_keyframes[nodeId].scale![timeKey] = {
562-
value: [
563-
toMolangNumber(transform.scale[0]),
564-
toMolangNumber(transform.scale[1]),
565-
toMolangNumber(transform.scale[2]),
566-
],
567-
interpolation: createInterpolation(),
568-
}
569-
}
570-
}
638+
const channels = (node_keyframes[nodeId] ??= {})
639+
const position = (channels.position ??= {})
640+
const rotation = (channels.rotation ??= {})
641+
const scale = (channels.scale ??= {})
571642

572-
// eslint-disable-next-line @typescript-eslint/naming-convention
573-
let global_keyframes: NonNullable<PluginAnimation['global_keyframes']> | undefined
643+
position[timeKey] = { value: transform.pos.map(toMolangNumber) as [string, string, string], interpolation }
644+
rotation[timeKey] = { value: transform.rot.map(toMolangNumber) as [string, string, string], interpolation }
645+
scale[timeKey] = { value: transform.scale.map(toMolangNumber) as [string, string, string], interpolation }
646+
}
574647

575-
// map the baked variant for each frame into the texture keyframes
576-
if (options.paletteIds.length) {
577-
const textureKeyframes: Record<string, Record<string, string>> = {}
578-
for (const frame of animation.frames) {
579-
if (!frame.variants?.length) continue
648+
if (paletteIds.length && frame.variants?.length) {
580649
const variant = Variant.getByUUID(frame.variants[0])
581-
if (!variant) continue
582-
const timeKey = formatTimestamp(frame.time)
583-
textureKeyframes[timeKey] ??= {}
584-
for (const paletteId of options.paletteIds) {
585-
textureKeyframes[timeKey][paletteId] = variant.name
650+
if (variant) {
651+
global_keyframes ??= {}
652+
const texture = (global_keyframes.texture ??= {})
653+
const slot = (texture[timeKey] ??= {})
654+
for (const paletteId of paletteIds) slot[paletteId] = variant.name
586655
}
587656
}
588-
if (Object.keys(textureKeyframes).length) {
589-
global_keyframes ??= {}
590-
global_keyframes.texture = textureKeyframes
591-
}
592657
}
593658

594659
return scrubUndefined({
595-
loop_mode,
660+
loop_mode: buildLoopMode(animation),
596661
blend_weight: '1',
597662
start_delay: '0',
598-
length: maxTime,
663+
length: animation.frames.at(-1)?.time ?? 0,
599664
global_keyframes,
600665
node_keyframes,
601666
} satisfies PluginAnimation)
@@ -642,13 +707,11 @@ export function exportPluginBlueprint(options: {
642707
}
643708

644709
const animations: Record<string, PluginAnimation> = {}
710+
const usedAnimationKeys = new Set<string>()
711+
const serialize = aj.baked_animations ? serializeBakedAnimation : serializeRawAnimation
645712
for (const animation of options.animations) {
646-
const key = ensureUniqueKey(animation.storage_name, new Set(Object.keys(animations)))
647-
animations[key] = serializeAnimation({
648-
animation,
649-
nodeUuidToId,
650-
paletteIds,
651-
})
713+
const key = ensureUniqueKey(animation.storage_name, usedAnimationKeys)
714+
animations[key] = serialize({ animation, nodeUuidToId, paletteIds })
652715
}
653716

654717
const blueprint: PluginBlueprintJson = scrubUndefined({

0 commit comments

Comments
 (0)