阶段五完成

This commit is contained in:
tianmo committed 2026-08-28 17:50:23 +08:00
1 parent 4e2e123269
commit f2fe653c04
6 files changed
+1423 -60

No files matched your search

+44 -13
View File
@@ -7,6 +7,7 @@
<label class="brand">粒子发射编辑器</label>
<label class="chk"><input type="checkbox" v-model="showEmitter" />发射点</label>
<label class="chk"><input type="checkbox" v-model="showBones" />骨骼连线</label>
<label class="chk"><input type="checkbox" v-model="showSkinMesh" />蒙皮网格</label>
<!-- 根变换工具:拖动(默认) / 位移 / 旋转 / 缩放 -->
<div class="tool-group">
@@ -63,7 +64,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { Application, Container, Graphics, Text, Texture } from 'pixi.js'
import { ParticleEmitter, ensureEmitterConfig } from '../core/particleEmitter'
import { ParticleEmitter, ensureEmitterConfig, type ParticleState } from '../core/particleEmitter'
import { useParticleStore } from '../store/particleStore'
const store = useParticleStore()
@@ -71,6 +72,7 @@ const holder = ref<HTMLElement | null>(null)
const time = ref(0)
const showEmitter = ref(true)
const showBones = ref(false)
const showSkinMesh = ref(false)
const info = ref<{ systems: number; particles: number } | null>(null)
const showGrid = ref(true)
const showSettings = ref(false)
@@ -92,12 +94,18 @@ let axisLabels: Text[] = []
let lastT = 0
let raf = 0
let dotTex: Texture<any> | null = null
let trailTex: Texture<any> | null = null
const emitterMap = new Map<number, ParticleEmitter>()
async function loadTexture() {
if (dotTex) return dotTex
dotTex = await Texture.fromURL('/particles/star.png')
if (dotTex && trailTex) return dotTex
const [particleTexture, trailTexture] = await Promise.all([
Texture.fromURL('/particles/star.png'),
Texture.fromURL('/trails/trail.png'),
])
dotTex = particleTexture
trailTex = trailTexture
return dotTex
}
@@ -395,6 +403,11 @@ function syncEmitters() {
// 新增系统若还没贴图(dotTex 已加载后),补上默认点纹理,否则无法渲染
if (dotTex && !sys.config.texture) sys.config.texture = dotTex
const cfg = ensureEmitterConfig(sys.config as any)
if (trailTex) {
for (const resource of cfg.trailResources) {
if (!resource.texture && resource.previewUrl === '/trails/trail.png') resource.texture = trailTex
}
}
if (dotTex) {
for (const resource of cfg.imageResources) {
if (!resource.texture && resource.textureName === 'star') resource.texture = dotTex
@@ -609,7 +622,18 @@ function drawGizmo() {
}
// 粒子→骨骼映射:每个活动粒子 = 一根挂在 root 下的骨骼
function drawBones(bones: { boneName: string; x: number; y: number }[]) {
type BonePreview = { boneName: string; x: number; y: number; parentX?: number; parentY?: number; trail?: boolean }
function appendStateBones(target: BonePreview[], state: ParticleState) {
target.push({ boneName: state.boneName, x: state.x, y: state.y })
if (!state.trailState?.bones.length) return
state.trailState.bones.forEach((bone, index, bones) => {
const parent = index > 0 ? bones[index - 1] : { x: state.x, y: state.y }
target.push({ boneName: bone.boneName, x: bone.x, y: bone.y, parentX: parent.x, parentY: parent.y, trail: true })
})
}
function drawBones(bones: BonePreview[]) {
if (!boneGfx) return
const g = boneGfx
g.clear()
@@ -617,11 +641,12 @@ function drawBones(bones: { boneName: string; x: number; y: number }[]) {
const cx = 0, cy = 0
// root → 每颗粒子骨骼 的连线
for (const b of bones) {
g.lineStyle(1, 0xff8c5a, 0.35)
g.moveTo(cx, cy)
const color = b.trail ? 0xff4d8d : 0xff8c5a
g.lineStyle(1, color, b.trail ? 0.65 : 0.35)
g.moveTo(b.parentX ?? cx, b.parentY ?? cy)
g.lineTo(b.x, b.y)
// 骨骼点
g.beginFill(0xff8c5a, 0.9)
g.beginFill(color, 0.9)
g.drawCircle(b.x, b.y, 2.5)
g.endFill()
}
@@ -650,7 +675,7 @@ function loop() {
em.scale.set(c.rootScaleX, c.rootScaleY)
}
let total = 0
const collected: { boneName: string; x: number; y: number }[] = []
const collected: BonePreview[] = []
const tl = store.timeline
const fps = tl.fps
const dt = 1 / fps // 固定帧步长,保证帧对齐
@@ -660,7 +685,7 @@ function loop() {
if (tl.frame === 0) { for (const [, em] of emitterMap) em.reset() }
for (const [, em] of emitterMap) {
const st = em.update(dt)
for (const s of st) if (s.active) collected.push({ boneName: s.boneName, x: s.x, y: s.y })
for (const s of st) if (s.active) appendStateBones(collected, s)
total += em.activeCount
}
for (const sys of store.systems) {
@@ -676,7 +701,7 @@ function loop() {
for (const sys of store.systems) {
const em = emitterMap.get(sys.id)
const fr = sys.frames?.[tl.frame]
if (em && fr) { em.apply(fr); for (const s of fr) if (s.active) { collected.push({ boneName: s.boneName, x: s.x, y: s.y }); total++ } }
if (em && fr) { em.apply(fr); for (const s of fr) if (s.active) { appendStateBones(collected, s); total++ } }
}
tl.frame++
if (tl.frame >= tl.totalFrames) { if (tl.loop) tl.frame = 0; else { tl.frame = tl.totalFrames - 1; tl.playing = false } }
@@ -688,14 +713,15 @@ function loop() {
const fr = sys.frames?.[tl.frame]
if (em && tl.recorded && fr) {
em.apply(fr)
for (const s of fr) if (s.active) { collected.push({ boneName: s.boneName, x: s.x, y: s.y }); total++ }
for (const s of fr) if (s.active) { appendStateBones(collected, s); total++ }
} else if (em) {
const st = em.update(0) // 冻结预览
for (const s of st) if (s.active) collected.push({ boneName: s.boneName, x: s.x, y: s.y })
for (const s of st) if (s.active) appendStateBones(collected, s)
total += em.activeCount
}
}
}
for (const [, emitter] of emitterMap) emitter.drawTrailMeshDebug(showSkinMesh.value)
drawOrigin()
drawShapeRange()
drawForceField()
@@ -716,6 +742,11 @@ onMounted(async () => {
for (const sys of store.systems) {
sys.config.texture = tex
const cfg = ensureEmitterConfig(sys.config as any)
if (trailTex) {
for (const resource of cfg.trailResources) {
if (!resource.texture && resource.previewUrl === '/trails/trail.png') resource.texture = trailTex
}
}
for (const resource of cfg.imageResources) {
if (!resource.texture && resource.textureName === 'star') resource.texture = tex
}
@@ -737,7 +768,7 @@ onMounted(async () => {
watch(
() =>
store.systems
.map((s) => JSON.stringify(s.config, (k, v) => (k === 'texture' || k === 'previewUrl' ? undefined : v)))
.map((s) => JSON.stringify(s.config, (k, v) => (k === 'texture' || k === 'trailTexture' || k === 'previewUrl' ? undefined : v)))
.join('|'),
() => {
const frames = store.recalcTotalFrames()