修改粒子命名规则
This commit is contained in:
1 parent
1ecbb6fc48
commit
aabf4ff693
10 files changed
+235
-97
No files matched your search
@@ -75,12 +75,13 @@ function stateIdentity(state: ParticleState, bonePool: boolean) {
|
||||
return bonePool ? state.boneName : `${state.spawnId ?? 'legacy'}_${state.boneName}`
|
||||
}
|
||||
|
||||
function collectTracks(frames: ParticleState[][], bonePool: boolean, usedNames: Set<string>) {
|
||||
function collectTracks(frames: ParticleState[][], bonePool: boolean, usedNames: Set<string>, prefix: string) {
|
||||
const keys = new Set<string>()
|
||||
for (const frame of frames) for (const state of frame) keys.add(stateIdentity(state, bonePool))
|
||||
return [...keys].sort().map((key, index): Track => ({
|
||||
key,
|
||||
name: uniqueName(`_particle_${String(index).padStart(3, '0')}`, usedNames),
|
||||
// 粒子骨骼命名:系统名_序号
|
||||
name: uniqueName(`${prefix}_${index}`, usedNames),
|
||||
states: frames.map((frame) => frame.find((state) => stateIdentity(state, bonePool) === key) || null),
|
||||
}))
|
||||
}
|
||||
@@ -249,6 +250,20 @@ function resourceAttachment(resource: ParticleImageResource, frameIndex: number,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片附件命名:默认使用系统名;同一插槽内存在序列帧或多张图片时追加序号尾缀。
|
||||
* 返回 key(资源id_帧号) -> 附件名,供蒙皮定义与动画时间轴共用,保证两边名字一致。
|
||||
*/
|
||||
function imageAttachmentNameMap(prefix: string, resources: ParticleImageResource[]) {
|
||||
const keys: string[] = []
|
||||
for (const resource of resources) {
|
||||
const count = resource.imageMode === 'sequence' ? Math.max(1, resource.sequenceFrames.length) : 1
|
||||
for (let frame = 0; frame < count; frame++) keys.push(`${resource.id}_${frame}`)
|
||||
}
|
||||
const multiple = keys.length > 1
|
||||
return new Map(keys.map((key, index) => [key, multiple ? `${prefix}_${index}` : prefix]))
|
||||
}
|
||||
|
||||
function rgbaTimeline(colors: number[][], attachments: Array<string | null>, fps: number, settings: SpineExportSettings, curveMode: TimelineCurveMode = settings.keyframeCurve, steppedFrames: Set<number> = new Set(), boundaryFrames: Set<number> = new Set()) {
|
||||
if (!colors.length) return []
|
||||
const forcedFrames = new Set<number>()
|
||||
@@ -321,7 +336,7 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
const skin = document.skins[0].attachments as SpineJsonMap
|
||||
const usedNames = new Set<string>(['root'])
|
||||
const animationNames = [...new Set([...(input.timeline.animations.length ? input.timeline.animations : ['animation']), ...visibleSystems.map((system) => system.animation || 'animation')])]
|
||||
const animationSystems = new Map<string, Array<{ system: ParticleSystem; tracks: Track[]; targetFrames: number; boundaryFrames: Set<number> }>>()
|
||||
const animationSystems = new Map<string, Array<{ system: ParticleSystem; tracks: Track[]; targetFrames: number; boundaryFrames: Set<number>; attachmentPrefix: string }>>()
|
||||
|
||||
for (const system of visibleSystems) {
|
||||
const sourceCount = Math.min(system.frames?.length || 0, particleDurationFrames(system, sourceFps))
|
||||
@@ -329,27 +344,36 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
if (!frames.length) warnings.push({ code: 'empty-cache', message: `“${system.config.name}”没有可用帧缓存。` })
|
||||
const prefix = uniqueName(safeName(system.config.name, `system_${system.id}`), usedNames)
|
||||
document.bones.push({ name: prefix, parent: 'root' })
|
||||
const tracks = collectTracks(frames, settings.bonePool, usedNames)
|
||||
const tracks = collectTracks(frames, settings.bonePool, usedNames, prefix)
|
||||
const duration = Math.max(0, (sourceCount - 1) / sourceFps)
|
||||
const targetFrames = Math.max(1, Math.ceil(duration * fps) + 1)
|
||||
// 循环持续:开始在/循环/结束的段边界帧,所有通道都要补关键帧。
|
||||
const boundaryFrames = loopSegmentBoundaryFrames(system, sourceFps, fps)
|
||||
const group = animationSystems.get(system.animation) || []
|
||||
group.push({ system, tracks, targetFrames, boundaryFrames })
|
||||
group.push({ system, tracks, targetFrames, boundaryFrames, attachmentPrefix: prefix })
|
||||
animationSystems.set(system.animation, group)
|
||||
|
||||
for (const track of tracks) {
|
||||
document.bones.push({ name: track.name, parent: prefix })
|
||||
const imageResources = trackImageResources(system, track)
|
||||
const usedImageBlends = new Set(imageResources.map((item) => item.blend))
|
||||
const imageSlotNames: string[] = []
|
||||
const trailSlotNames: string[] = []
|
||||
for (const blend of usedImageBlends) {
|
||||
const slotName = `${track.name}_image_${blend}`
|
||||
// 插槽命名:系统名_序号_颜色模式(轨道名已经是“系统名_序号”)
|
||||
const slotName = `${track.name}_${blend}`
|
||||
imageSlotNames.push(slotName)
|
||||
document.slots.push({ name: slotName, bone: track.name, color: 'ffffff00', blend: BLENDS[blend] })
|
||||
const slotSkin = skin[slotName] ||= {}
|
||||
const blendResources = imageResources.filter((item) => item.blend === blend)
|
||||
const attachmentNames = imageAttachmentNameMap(prefix, blendResources)
|
||||
// 只挂该轨道真正用到的图片;序列帧资源包含全部帧(每帧都会被播放到)。
|
||||
for (const resource of imageResources.filter((item) => item.blend === blend)) {
|
||||
for (const resource of blendResources) {
|
||||
const count = resource.imageMode === 'sequence' ? Math.max(1, resource.sequenceFrames.length) : 1
|
||||
for (let frame = 0; frame < count; frame++) slotSkin[`image_${resource.id}_${frame}`] = resourceAttachment(resource, frame, warnings)
|
||||
for (let frame = 0; frame < count; frame++) {
|
||||
const name = attachmentNames.get(`${resource.id}_${frame}`)
|
||||
if (name) slotSkin[name] = resourceAttachment(resource, frame, warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (system.config.trail) {
|
||||
@@ -366,6 +390,7 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
})
|
||||
}
|
||||
const slotName = `${track.name}_trail_${resource.id}`
|
||||
trailSlotNames.push(slotName)
|
||||
document.slots.push({ name: slotName, bone: trailNames[0], color: 'ffffff00', blend: BLENDS[resource.blend] })
|
||||
const indices = trailNames.map((name) => document.bones.findIndex((bone) => bone.name === name))
|
||||
const path = normalizePath('', resource.textureName || `trail_${resource.id}`)
|
||||
@@ -374,12 +399,14 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
}
|
||||
}
|
||||
// Spine 按 slots 数组顺序绘制。保持每个粒子的拖尾在图片下方。
|
||||
const imagePrefix = `${track.name}_image_`
|
||||
const trailPrefix = `${track.name}_trail_`
|
||||
const ownSlots = document.slots.filter((slot) => String(slot.name).startsWith(imagePrefix) || String(slot.name).startsWith(trailPrefix))
|
||||
const ownNames = new Set([...imageSlotNames, ...trailSlotNames])
|
||||
const ownSlots = document.slots.filter((slot) => ownNames.has(String(slot.name)))
|
||||
if (ownSlots.length) {
|
||||
document.slots = document.slots.filter((slot) => !ownSlots.includes(slot))
|
||||
document.slots.push(...ownSlots.filter((slot) => String(slot.name).startsWith(trailPrefix)), ...ownSlots.filter((slot) => String(slot.name).startsWith(imagePrefix)))
|
||||
document.slots.push(
|
||||
...ownSlots.filter((slot) => trailSlotNames.includes(String(slot.name))),
|
||||
...ownSlots.filter((slot) => imageSlotNames.includes(String(slot.name))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +415,7 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
const entries = animationSystems.get(animationName) || []
|
||||
const animation: SpineJsonMap = { bones: {}, slots: {} }
|
||||
let hasContent = false
|
||||
for (const { system, tracks, targetFrames, boundaryFrames } of entries) {
|
||||
for (const { system, tracks, targetFrames, boundaryFrames, attachmentPrefix } of entries) {
|
||||
for (const track of tracks) {
|
||||
const states = Array.from({ length: targetFrames }, (_, frame) => sourceStateAt(track, frame, sourceFps, fps))
|
||||
const active = states.map((state) => !!state?.active)
|
||||
@@ -418,6 +445,8 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
const imageResources = trackImageResources(system, track)
|
||||
const usedBlends = new Set(imageResources.map((item) => item.blend))
|
||||
for (const blend of usedBlends) {
|
||||
const blendResources = imageResources.filter((item) => item.blend === blend)
|
||||
const attachmentNames = imageAttachmentNameMap(attachmentPrefix, blendResources)
|
||||
const attachments: Array<string | null> = [], colors: number[][] = []
|
||||
let lastColor = 0xffffff
|
||||
for (let frame = 0; frame < targetFrames; frame++) {
|
||||
@@ -425,17 +454,17 @@ export function buildSpineJson(input: ExportInput): SpineExportResult {
|
||||
const resource = state ? system.config.imageResources.find((item) => item.id === state.resourceId) : undefined
|
||||
const visible = !!state && active[frame] && state.imageVisible !== false && resource?.blend === blend
|
||||
if (state && resource?.blend === blend) lastColor = state.colorHex
|
||||
attachments.push(visible ? `image_${resource!.id}_${state!.imageFrameIndex ?? 0}` : null)
|
||||
const attachmentName = visible ? attachmentNames.get(`${resource!.id}_${state!.imageFrameIndex ?? 0}`) : undefined
|
||||
attachments.push(attachmentName ?? null)
|
||||
colors.push(rgbaValues(lastColor, visible ? state!.alpha : 0))
|
||||
}
|
||||
const blendResources = imageResources.filter((item) => item.blend === blend)
|
||||
const requestedModes = blendResources
|
||||
.filter((item) => item.independentAlpha === 'curve')
|
||||
.map((item) => resolvedTimelineCurve(item.alphaCurveMode, settings))
|
||||
const rgbaCurveMode = requestedModes.length
|
||||
? (requestedModes.includes('bezier') ? 'bezier' : 'linear')
|
||||
: resolvedTimelineCurve(system.config.alphaMode === 'curve' ? system.config.curveMode : undefined, settings)
|
||||
addSlotAnimation(animation, `${track.name}_image_${blend}`, attachments, colors, fps, settings, rgbaCurveMode, stepped, boundaryFrames)
|
||||
addSlotAnimation(animation, `${track.name}_${blend}`, attachments, colors, fps, settings, rgbaCurveMode, stepped, boundaryFrames)
|
||||
if (attachments.some(Boolean)) hasContent = true
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user