载入中...
搜索中...
未找到
SpineAnim.cpp
浏览该文件的文档.
2
6#include "common/Exception.h"
8#include "graphics/Graphics.h"
10#include "graphics/Texture.h"
11
12#include <algorithm>
13#include <cmath>
14
15#ifndef M_PI
16#define M_PI 3.14159265358979323846
17#endif
18
19namespace eve::animation {
20
21SpineAnim::SpineAnim(SpineSkeleton *skeleton) : skeleton_(skeleton) {
22 if (!skeleton_) throw Exception("SpineAnim: skeleton is null");
23}
24
26 if (owner_) owner_->unregisterSpineAnim(this);
27}
28
30 atlas_ = atlas;
31 pageTextures_.clear();
32 if (atlas_) pageTextures_.resize(static_cast<size_t>(atlas_->getPageCount()), nullptr);
33}
34
35void SpineAnim::setPageTexture(int pageIndex, graphics::Texture *texture) {
36 if (!atlas_) throw Exception("SpineAnim.setPageTexture: atlas is null");
37 if (pageIndex < 0 || pageIndex >= atlas_->getPageCount())
38 throw Exception("SpineAnim.setPageTexture: page index %d out of range", pageIndex);
39 if (pageTextures_.size() < static_cast<size_t>(atlas_->getPageCount()))
40 pageTextures_.resize(static_cast<size_t>(atlas_->getPageCount()), nullptr);
41 pageTextures_[static_cast<size_t>(pageIndex)] = texture;
42}
43
44void SpineAnim::setPageTextureByName(const std::string &pageName, graphics::Texture *texture) {
45 if (!atlas_) throw Exception("SpineAnim.setPageTextureByName: atlas is null");
46 for (int i = 0; i < atlas_->getPageCount(); ++i) {
47 if (atlas_->getPageName(i) == pageName) {
48 setPageTexture(i, texture);
49 return;
50 }
51 }
52 throw Exception("SpineAnim.setPageTextureByName: unknown page '%s'", pageName.c_str());
53}
54
56 if (pageIndex < 0 || pageIndex >= static_cast<int>(pageTextures_.size())) return nullptr;
57 return pageTextures_[static_cast<size_t>(pageIndex)];
58}
59
60bool SpineAnim::play(const std::string &animationName) {
61 if (!skeleton_ || !skeleton_->getData()) return false;
62 int idx = skeleton_->getData()->findAnimation(animationName);
63 if (idx < 0) return false;
64 animIndex_ = idx;
65 animName_ = animationName;
66 time_ = 0.f;
67 playing_ = true;
68 paused_ = false;
69 finished_ = false;
70 apply();
71 return true;
72}
73
75 playing_ = false;
76 paused_ = false;
77 finished_ = false;
78 time_ = 0.f;
79 animIndex_ = -1;
80 animName_.clear();
81}
82
84 if (playing_) paused_ = true;
85}
86
88 if (playing_ && paused_) paused_ = false;
89}
90
91void SpineAnim::setSpeed(float speed) {
92 if (speed < 0.f) throw Exception("SpineAnim.setSpeed: speed must be >= 0");
93 speed_ = speed;
94}
95
96void SpineAnim::setTime(float seconds) {
97 if (seconds < 0.f) throw Exception("SpineAnim.setTime: time must be >= 0");
98 time_ = seconds;
99 if (playing_) apply();
100}
101
103 if (animIndex_ < 0 || !skeleton_ || !skeleton_->getData()) return 0.f;
104 return skeleton_->getData()->getAnimationDuration(animIndex_);
105}
106
107void SpineAnim::setPosition(float x, float y) {
108 x_ = x;
109 y_ = y;
110}
111
112void SpineAnim::setScale(float sx, float sy) {
113 scaleX_ = sx;
114 scaleY_ = sy;
115}
116
117void SpineAnim::setColor(float r, float g, float b, float a) {
118 r_ = r;
119 g_ = g;
120 b_ = b;
121 a_ = a;
122}
123
124float SpineAnim::sampleFloat(const std::vector<SpineSkeletonData::FloatKey> &keys, float time,
125 float fallback) {
126 if (keys.empty()) return fallback;
127 if (time <= keys.front().time) return keys.front().value;
128 if (time >= keys.back().time) return keys.back().value;
129 for (size_t i = 0; i + 1 < keys.size(); ++i) {
130 const auto &a = keys[i];
131 const auto &b = keys[i + 1];
132 if (time > b.time) continue;
133 if (a.stepped || b.time <= a.time) return a.value;
134 float t = (time - a.time) / (b.time - a.time);
135 return a.value + (b.value - a.value) * t;
136 }
137 return keys.back().value;
138}
139
140void SpineAnim::sampleTranslate(const std::vector<SpineSkeletonData::TranslateKey> &keys,
141 float time, float &x, float &y) {
142 if (keys.empty()) return;
143 if (time <= keys.front().time) {
144 x = keys.front().x;
145 y = keys.front().y;
146 return;
147 }
148 if (time >= keys.back().time) {
149 x = keys.back().x;
150 y = keys.back().y;
151 return;
152 }
153 for (size_t i = 0; i + 1 < keys.size(); ++i) {
154 const auto &a = keys[i];
155 const auto &b = keys[i + 1];
156 if (time > b.time) continue;
157 if (a.stepped || b.time <= a.time) {
158 x = a.x;
159 y = a.y;
160 return;
161 }
162 float t = (time - a.time) / (b.time - a.time);
163 x = a.x + (b.x - a.x) * t;
164 y = a.y + (b.y - a.y) * t;
165 return;
166 }
167 x = keys.back().x;
168 y = keys.back().y;
169}
170
171void SpineAnim::sampleScale(const std::vector<SpineSkeletonData::ScaleKey> &keys, float time,
172 float &x, float &y) {
173 if (keys.empty()) return;
174 if (time <= keys.front().time) {
175 x = keys.front().x;
176 y = keys.front().y;
177 return;
178 }
179 if (time >= keys.back().time) {
180 x = keys.back().x;
181 y = keys.back().y;
182 return;
183 }
184 for (size_t i = 0; i + 1 < keys.size(); ++i) {
185 const auto &a = keys[i];
186 const auto &b = keys[i + 1];
187 if (time > b.time) continue;
188 if (a.stepped || b.time <= a.time) {
189 x = a.x;
190 y = a.y;
191 return;
192 }
193 float t = (time - a.time) / (b.time - a.time);
194 x = a.x + (b.x - a.x) * t;
195 y = a.y + (b.y - a.y) * t;
196 return;
197 }
198 x = keys.back().x;
199 y = keys.back().y;
200}
201
202std::string SpineAnim::sampleAttachment(const std::vector<SpineSkeletonData::AttachmentKey> &keys,
203 float time, const std::string &fallback) {
204 if (keys.empty()) return fallback;
205 std::string cur = fallback;
206 for (const auto &k : keys) {
207 if (k.time > time) break;
208 cur = k.name;
209 }
210 return cur;
211}
212
214 if (!skeleton_ || !skeleton_->getData()) return;
215 skeleton_->setToSetupPose();
216
217 if (animIndex_ >= 0) {
218 const auto &anim = skeleton_->getData()->animation(animIndex_);
219 float t = time_;
220 const float dur = anim.duration;
221 if (loop_ && dur > 0.f) {
222 t = std::fmod(t, dur);
223 if (t < 0.f) t += dur;
224 } else if (!loop_ && dur > 0.f && t > dur) {
225 t = dur;
226 }
227
228 for (const auto &bt : anim.bones) {
229 float x = skeleton_->getBoneLocalX(bt.boneIndex);
230 float y = skeleton_->getBoneLocalY(bt.boneIndex);
231 float r = skeleton_->getBoneLocalRotation(bt.boneIndex);
232 float sx = skeleton_->getBoneLocalScaleX(bt.boneIndex);
233 float sy = skeleton_->getBoneLocalScaleY(bt.boneIndex);
234
235 // Spine translate/rotate/scale timelines are typically relative to setup pose
236 // for rotate (additive angle) and absolute overrides for translate offset from setup.
237 // We treat keys as absolute local values when present (common export style stores
238 // absolute). Translate keys are offsets added to setup in official runtime;
239 // many JSON exports store absolute. Use: setup + key for translate/scale deviation
240 // matching spine-runtimes Timeline apply (translate is relative to setup).
241 const auto &setup = skeleton_->getData()->bone(bt.boneIndex);
242 if (!bt.translate.empty()) {
243 float tx = 0.f, ty = 0.f;
244 sampleTranslate(bt.translate, t, tx, ty);
245 x = setup.x + tx;
246 y = setup.y + ty;
247 }
248 if (!bt.rotate.empty()) {
249 float angle = sampleFloat(bt.rotate, t, 0.f);
250 r = setup.rotation + angle;
251 }
252 if (!bt.scale.empty()) {
253 float kx = 1.f, ky = 1.f;
254 sampleScale(bt.scale, t, kx, ky);
255 sx = setup.scaleX * kx;
256 sy = setup.scaleY * ky;
257 }
258 skeleton_->setBoneLocalX(bt.boneIndex, x);
259 skeleton_->setBoneLocalY(bt.boneIndex, y);
260 skeleton_->setBoneLocalRotation(bt.boneIndex, r);
261 skeleton_->setBoneLocalScaleX(bt.boneIndex, sx);
262 skeleton_->setBoneLocalScaleY(bt.boneIndex, sy);
263 }
264
265 for (const auto &st : anim.slots) {
266 std::string setupName = skeleton_->getData()->slot(st.slotIndex).attachment;
267 std::string name = sampleAttachment(st.attachment, t, setupName);
268 skeleton_->setSlotAttachmentName(st.slotIndex, name);
269 }
270 }
271
272 skeleton_->updateWorldTransform();
273 rebuildDrawSlots();
274}
275
276bool SpineAnim::update(float dt) {
277 if (dt < 0.f) throw Exception("SpineAnim.update: dt must be >= 0");
278 if (!playing_ || paused_) return playing_ || paused_;
279
280 time_ += dt * speed_;
281 const float dur = getAnimationDuration();
282 if (!loop_ && dur > 0.f && time_ >= dur) {
283 time_ = dur;
284 finished_ = true;
285 playing_ = false;
286 apply();
287 return false;
288 }
289 apply();
290 return true;
291}
292
294 if (!gfx) return;
295 std::vector<graphics::DrawItem2D> items;
296 collectDrawItems(items);
297 graphics::RenderSystem::drawItems(*gfx, items, false);
298}
299
300void SpineAnim::rebuildDrawSlots() {
301 drawSlots_.clear();
302 if (!skeleton_) return;
303
304 for (int si = 0; si < skeleton_->getSlotCount(); ++si) {
305 const auto *att = skeleton_->getSlotRegion(si);
306 if (!att) continue;
307 int boneIndex = skeleton_->getData()->slot(si).bone;
308 const auto &bone = skeleton_->bones_[static_cast<size_t>(boneIndex)];
309
310 DrawSlot ds;
311 ds.regionName = att->path.empty() ? att->name : att->path;
312
313 // Atlas region packed dimensions + original dimensions + rotation flag.
314 int regionIndex = -1;
315 float regionW = att->width, regionH = att->height;
316 float origW = att->width, origH = att->height;
317 bool rotated = false;
318 if (atlas_) {
319 regionIndex = atlas_->findRegion(ds.regionName);
320 ds.region = regionIndex;
321 if (regionIndex >= 0) {
322 ds.page = atlas_->getRegionPage(regionIndex);
323 rotated = atlas_->getRegionRotate(regionIndex);
324 ds.rotated = rotated;
325 regionW = static_cast<float>(atlas_->getRegionWidth(regionIndex));
326 regionH = static_cast<float>(atlas_->getRegionHeight(regionIndex));
327 origW = static_cast<float>(atlas_->getRegionOriginalWidth(regionIndex));
328 origH = static_cast<float>(atlas_->getRegionOriginalHeight(regionIndex));
329 int tw = atlas_->getPageWidth(ds.page);
330 int th = atlas_->getPageHeight(ds.page);
331 auto *tex = getPageTexture(ds.page);
332 if (tex) {
333 tw = tex->getWidth();
334 th = tex->getHeight();
335 }
336 if (tw > 0 && th > 0)
337 atlas_->getRegionUV(regionIndex, tw, th, ds.u0, ds.v0, ds.u1, ds.v1);
338 }
339 }
340 if (origW <= 0.f) origW = att->width;
341 if (origH <= 0.f) origH = att->height;
342
343 // Official spine RegionAttachment: scale the attachment rect to the
344 // atlas region's PACKED size (regionWidth/regionHeight; for a rotated
345 // region these already span origH×origW), rotate the corner offsets
346 // around the BONE ORIGIN by the attachment rotation, then translate by
347 // the attachment anchor (x, y) and map through the bone matrix.
348 const float regionScaleX = (origW > 0.f) ? (att->width / origW) * att->scaleX : att->scaleX;
349 const float regionScaleY = (origH > 0.f) ? (att->height / origH) * att->scaleY : att->scaleY;
350
351 const float localX = -att->width * 0.5f * att->scaleX;
352 const float localY = -att->height * 0.5f * att->scaleY;
353 const float localX2 = localX + regionW * regionScaleX;
354 const float localY2 = localY + regionH * regionScaleY;
355
356 const float ar = att->rotation * static_cast<float>(M_PI / 180.0);
357 const float ac = std::cos(ar);
358 const float as = std::sin(ar);
359
360 // Corner (lx, ly) → bone-space → spine world → screen (Y-down).
361 auto toScreen = [&](float lx, float ly) -> std::pair<float, float> {
362 const float rx = lx * ac - ly * as + att->x;
363 const float ry = lx * as + ly * ac + att->y;
364 const float wx = bone.a * rx + bone.b * ry + bone.worldX;
365 const float wy = bone.c * rx + bone.d * ry + bone.worldY;
366 return {x_ + wx * scaleX_, y_ + (flipY_ ? -wy : wy) * scaleY_};
367 };
368
369 const auto bl = toScreen(localX, localY);
370 const auto br = toScreen(localX2, localY);
371 const auto tl = toScreen(localX, localY2);
372 const auto tr = toScreen(localX2, localY2);
373
374 // Reconstruct the axis-aligned-then-rotated quad the 2D batcher draws:
375 // center = average of corners, w = length of the top edge (UL→UR) in
376 // screen space, h = length of the left edge (UL→BL). The batcher maps
377 // (u0,v0)→top-left of the quad, which after the Spine Y-up → Y-down
378 // flip corresponds to Spine's UL corner; so the rotation angle must be
379 // the direction of the top edge, otherwise the sprite is upside down.
380 ds.x = (bl.first + br.first + tl.first + tr.first) * 0.25f;
381 ds.y = (bl.second + br.second + tl.second + tr.second) * 0.25f;
382 const float dx = tr.first - tl.first;
383 const float dy = tr.second - tl.second;
384 ds.w = std::sqrt(dx * dx + dy * dy);
385 const float hx = bl.first - tl.first;
386 const float hy = bl.second - tl.second;
387 ds.h = std::sqrt(hx * hx + hy * hy);
388 ds.rotation = (ds.w > 1e-6f) ? std::atan2(dy, dx) * static_cast<float>(180.0 / M_PI) : 0.f;
389 // Slot declaration order == Spine back-to-front draw order.
390 ds.order = si;
391
392 drawSlots_.push_back(ds);
393 }
394}
395
396void SpineAnim::collectDrawItems(std::vector<graphics::DrawItem2D> &out) {
397 apply();
398 for (const DrawSlot &ds : drawSlots_) {
400 // Centered quad at attachment world position
401 item.x = ds.x - ds.w * 0.5f;
402 item.y = ds.y - ds.h * 0.5f;
403 item.w = ds.w;
404 item.h = ds.h;
405 item.depthY = ds.y + ds.h * 0.5f;
406 item.order = ds.order;
407 item.hasOrder = true;
408 item.rotation = ds.rotation;
409 item.color = {r_, g_, b_, a_};
410 item.layer = layer_;
411 item.receiveLight = false;
412 item.texture = getPageTexture(ds.page);
413 item.hasUV = true;
414 item.rotatedUV = ds.rotated;
415 item.u0 = ds.u0;
416 item.v0 = ds.v0;
417 item.u1 = ds.u1;
418 item.v1 = ds.v1;
419 // A reflected skeleton cannot be represented by rotation plus positive
420 // quad extents alone. Reconstructing the reflected quad adds a 180-degree
421 // rotation, so regular regions need a V flip to leave only the requested
422 // horizontal reflection. Rotated atlas regions exchange the UV axes.
423 if (scaleX_ < 0.f) {
424 if (ds.rotated)
425 std::swap(item.u0, item.u1);
426 else
427 std::swap(item.v0, item.v1);
428 }
429 out.push_back(item);
430 }
431}
432
433void SpineAnim::checkDrawSlot(int index) const {
434 if (index < 0 || index >= getDrawSlotCount())
435 throw Exception("SpineAnim: draw slot index %d out of range (count=%d)", index,
437}
438
439int SpineAnim::getDrawSlotCount() const { return static_cast<int>(drawSlots_.size()); }
440
441float SpineAnim::getDrawSlotX(int index) const {
442 checkDrawSlot(index);
443 return drawSlots_[static_cast<size_t>(index)].x;
444}
445float SpineAnim::getDrawSlotY(int index) const {
446 checkDrawSlot(index);
447 return drawSlots_[static_cast<size_t>(index)].y;
448}
449float SpineAnim::getDrawSlotWidth(int index) const {
450 checkDrawSlot(index);
451 return drawSlots_[static_cast<size_t>(index)].w;
452}
453float SpineAnim::getDrawSlotHeight(int index) const {
454 checkDrawSlot(index);
455 return drawSlots_[static_cast<size_t>(index)].h;
456}
457float SpineAnim::getDrawSlotRotation(int index) const {
458 checkDrawSlot(index);
459 return drawSlots_[static_cast<size_t>(index)].rotation;
460}
461int SpineAnim::getDrawSlotPage(int index) const {
462 checkDrawSlot(index);
463 return drawSlots_[static_cast<size_t>(index)].page;
464}
465std::string SpineAnim::getDrawSlotRegion(int index) const {
466 checkDrawSlot(index);
467 return drawSlots_[static_cast<size_t>(index)].regionName;
468}
469
470} // namespace eve::animation
int y
Definition Grass.cpp:135
int x
Definition Grass.cpp:135
int h
int w
uint32_t a
uint32_t b
float tr
int idx
const char * name
Definition RockMesh.cpp:21
#define M_PI
Definition SpineAnim.cpp:16
void apply()
Apply current animation time to skeleton and update world transforms.
void setPageTexture(int pageIndex, graphics::Texture *texture)
Bind a GPU texture to an atlas page (by index or page image name).
Definition SpineAnim.cpp:35
graphics::Texture * getPageTexture(int pageIndex) const
Definition SpineAnim.cpp:55
float getDrawSlotHeight(int index) const
void setSpeed(float speed)
Definition SpineAnim.cpp:91
void setPageTextureByName(const std::string &pageName, graphics::Texture *texture)
Definition SpineAnim.cpp:44
float getDrawSlotX(int index) const
void setAtlas(SpineAtlas *atlas)
Definition SpineAnim.cpp:29
void setScale(float sx, float sy)
int getDrawSlotCount() const
Query posed draw slots without textures (for unit tests). Returns number of visible region attachment...
float getAnimationDuration() const
float getDrawSlotRotation(int index) const
float getDrawSlotY(int index) const
void collectDrawItems(std::vector< graphics::DrawItem2D > &out)
Append region attachment quads into the shared 2D draw queue.
void setPosition(float x, float y)
bool play(const std::string &animationName)
Definition SpineAnim.cpp:60
float getDrawSlotWidth(int index) const
SpineAnim(SpineSkeleton *skeleton)
Definition SpineAnim.cpp:21
std::string getDrawSlotRegion(int index) const
void setColor(float r, float g, float b, float a=1.f)
void setTime(float seconds)
Definition SpineAnim.cpp:96
void draw(graphics::Graphics *gfx)
Draw the current pose into an existing frame without clearing or presenting it.
int getDrawSlotPage(int index) const
Esoteric Spine .atlas text parser (region rectangles + page metadata). Does not load image pixels — b...
Definition SpineAtlas.h:14
int getPageHeight(int pageIndex) const
int getRegionOriginalWidth(int index) const
std::string getPageName(int pageIndex) const
int getPageWidth(int pageIndex) const
void getRegionUV(int index, int texW, int texH, float &u0, float &v0, float &u1, float &v1) const
Normalized UVs for a texture of texW×texH (usually page size).
int getRegionOriginalHeight(int index) const
bool getRegionRotate(int index) const
int getRegionPage(int index) const
int getRegionWidth(int index) const
int getRegionHeight(int index) const
int findRegion(const std::string &name) const
float getAnimationDuration(int index) const
const AnimationData & animation(int i) const
int findAnimation(const std::string &name) const
const SlotData & slot(int i) const
const BoneData & bone(int i) const
Runtime Spine skeleton pose (local + world bone transforms, slot attachments). Script type: SpineSkel...
void setBoneLocalScaleY(int index, float sy)
float getBoneLocalScaleY(int index) const
void setBoneLocalScaleX(int index, float sx)
float getBoneLocalScaleX(int index) const
float getBoneLocalX(int index) const
const SpineSkeletonData::RegionAttachment * getSlotRegion(int slotIndex) const
float getBoneLocalRotation(int index) const
void setBoneLocalRotation(int index, float degrees)
void setBoneLocalX(int index, float x)
SpineSkeletonData * getData() const
void setSlotAttachmentName(int slotIndex, const std::string &name)
float getBoneLocalY(int index) const
void setBoneLocalY(int index, float y)
static void drawItems(Graphics &gfx, std::vector< DrawItem2D > &items, bool present)
Sort and draw items. If present=true, calls gfx.present() at the end. Map::render uses present=false ...
GPU texture created via Graphics::newTexture. Owns GPU resources through an opaque backend handle.
Definition Texture.h:17
Shared 2D draw queue item (sprites + tiles).
Definition DrawItem2D.h:18
bool rotatedUV
Atlas-packed rotated region: corner UVs are remapped (rotated 90°).
Definition DrawItem2D.h:44
float rotation
Degrees, clockwise, around the rectangle center (screen Y-down).
Definition DrawItem2D.h:25