阶段五完成
This commit is contained in:
1 parent
4e2e123269
commit
f2fe653c04
6 files changed
+1423
-60
No files matched your search
+44
-13
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user