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