修改粒子命名规则

This commit is contained in:
tianmo committed 2026-10-14 18:35:26 +08:00
1 parent 1ecbb6fc48
commit aabf4ff693
10 files changed
+235 -97

No files matched your search

+45 -16
View File
@@ -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
}