diff --git a/Extra2D/include/extra2d/act/act.h b/Extra2D/include/extra2d/act/act.h new file mode 100644 index 0000000..3d03f5e --- /dev/null +++ b/Extra2D/include/extra2d/act/act.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +class Node; + +class Act : public RefCounted { +public: + virtual ~Act() = default; + + i32 tag() const { return tag_; } + void tag(i32 t) { tag_ = t; } + + Node* target() const { return target_; } + Node* origTarget() const { return origTarget_; } + + virtual bool done() const = 0; + virtual void start(Node* t); + virtual void stop(); + virtual void step(f32 dt); + virtual void update(f32 t) = 0; + + virtual Act* clone() const = 0; + virtual Act* reverse() const = 0; + +protected: + Node* target_ = nullptr; + Node* origTarget_ = nullptr; + i32 tag_ = -1; +}; + +class ActInstant : public Act { +public: + bool done() const override { return true; } + void step(f32 dt) override { update(1.0f); } +}; + +class ActInterval : public Act { +public: + explicit ActInterval(f32 d) : dur_(d) {} + + bool done() const override { return elap_ >= dur_; } + + void step(f32 dt) override { + elap_ += dt; + f32 t = std::min(elap_ / dur_, 1.0f); + update(easeTime(t)); + } + + f32 elap() const { return elap_; } + f32 dur() const { return dur_; } + void dur(f32 d) { dur_ = d; } + + using Ease = std::function; + void ease(Ease f) { ease_ = f; } + + f32 easeTime(f32 t) const { return ease_ ? ease_(t) : t; } + + static f32 easeLinear(f32 t) { return t; } + static f32 easeIn(f32 t) { return t * t; } + static f32 easeOut(f32 t) { return t * (2 - t); } + static f32 easeInOut(f32 t) { + return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t; + } + static f32 easeBackIn(f32 t) { + f32 s = 1.70158f; + return t * t * ((s + 1) * t - s); + } + static f32 easeBackOut(f32 t) { + f32 s = 1.70158f; + t -= 1; + return t * t * ((s + 1) * t + s) + 1; + } + static f32 easeBounceOut(f32 t) { + if (t < 1 / 2.75f) { + return 7.5625f * t * t; + } else if (t < 2 / 2.75f) { + t -= 1.5f / 2.75f; + return 7.5625f * t * t + 0.75f; + } else if (t < 2.5f / 2.75f) { + t -= 2.25f / 2.75f; + return 7.5625f * t * t + 0.9375f; + } else { + t -= 2.625f / 2.75f; + return 7.5625f * t * t + 0.984375f; + } + } + static f32 easeElasticOut(f32 t) { + if (t == 0 || t == 1) return t; + f32 p = 0.3f; + f32 s = p / 4; + return std::pow(2, -10 * t) * std::sin((t - s) * (2 * PI_F) / p) + 1; + } + +protected: + f32 dur_ = 0.0f; + f32 elap_ = 0.0f; + Ease ease_ = easeLinear; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_composite.h b/Extra2D/include/extra2d/act/act_composite.h new file mode 100644 index 0000000..59ea7ab --- /dev/null +++ b/Extra2D/include/extra2d/act/act_composite.h @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +class Delay : public ActInterval { +public: + explicit Delay(f32 d) : ActInterval(d) {} + + void update(f32 t) override {} + Act* clone() const override { return new Delay(dur_); } + Act* reverse() const override { return clone(); } +}; + +class Seq : public ActInterval { +public: + Seq(Act* a1, Act* a2); + + template + static Seq* create(As... as) { + std::initializer_list acts = {static_cast(as)...}; + return createFromArray(acts); + } + + void start(Node* t) override; + void stop() override; + void step(f32 dt) override; + void update(f32 t) override {} + bool done() const override; + Act* clone() const override; + Act* reverse() const override; + +private: + std::vector acts_; + size_t curIdx_ = 0; + f32 split_ = 0.0f; + + static Seq* createFromArray(std::initializer_list acts); +}; + +class Spawn : public ActInterval { +public: + Spawn(Act* a1, Act* a2); + + template + static Spawn* create(As... as) { + std::initializer_list acts = {static_cast(as)...}; + return createFromArray(acts); + } + + void start(Node* t) override; + void stop() override; + void update(f32 t) override; + Act* clone() const override; + Act* reverse() const override; + +private: + std::vector acts_; + + static Spawn* createFromArray(std::initializer_list acts); +}; + +class Repeat : public ActInterval { +public: + Repeat(Act* a, int times); + explicit Repeat(Act* a); + + bool done() const override; + void start(Node* t) override; + void stop() override; + void update(f32 t) override; + Act* clone() const override; + Act* reverse() const override; + + Act* inner() const { return inner_; } + +private: + Act* inner_ = nullptr; + int times_ = 1; + int cnt_ = 0; + bool inf_ = false; +}; + +class RepeatForever : public Act { +public: + explicit RepeatForever(Act* a) : inner_(a) { + if (auto* interval = dynamic_cast(a)) { + dur_ = interval->dur(); + } + } + + bool done() const override { return false; } + void start(Node* t) override; + void step(f32 dt) override; + void update(f32 t) override {} + Act* clone() const override { return new RepeatForever(inner_->clone()); } + Act* reverse() const override { return new RepeatForever(inner_->reverse()); } + +private: + Act* inner_ = nullptr; + f32 dur_ = 0.0f; + f32 elap_ = 0.0f; +}; + +class Speed : public ActInterval { +public: + Speed(Act* a, f32 s) : ActInterval(0), inner_(a), speed_(s) { + if (a) { + if (auto* interval = dynamic_cast(a)) { + dur_ = interval->dur() / s; + } + } + } + + void start(Node* t) override; + void stop() override; + void step(f32 dt) override; + bool done() const override { return inner_ ? inner_->done() : true; } + void update(f32 t) override {} + Act* clone() const override { return new Speed(inner_->clone(), speed_); } + Act* reverse() const override { return new Speed(inner_->reverse(), speed_); } + + void speed(f32 s) { speed_ = s; } + f32 speed() const { return speed_; } + +private: + Act* inner_ = nullptr; + f32 speed_ = 1.0f; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_fade.h b/Extra2D/include/extra2d/act/act_fade.h new file mode 100644 index 0000000..edf3ad4 --- /dev/null +++ b/Extra2D/include/extra2d/act/act_fade.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +class FadeTo : public ActInterval { +public: + FadeTo(f32 d, u8 o) : ActInterval(d), end_(o) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) { + if (auto* r = t->get()) { + start_ = r->getOpacity(); + } + } + } + + void update(f32 t) override { + if (target_) { + if (auto* r = target_->get()) { + u8 opacity = static_cast(start_ + (end_ - start_) * t); + r->setOpacity(opacity); + } + } + } + + Act* clone() const override { return new FadeTo(dur_, end_); } + Act* reverse() const override { return new FadeTo(dur_, start_); } + +protected: + u8 start_ = 255; + u8 end_ = 255; +}; + +class FadeOut : public FadeTo { +public: + explicit FadeOut(f32 d) : FadeTo(d, 0) {} + Act* clone() const override { return new FadeOut(dur_); } + Act* reverse() const override; +}; + +class FadeIn : public FadeTo { +public: + explicit FadeIn(f32 d) : FadeTo(d, 255) {} + Act* clone() const override { return new FadeIn(dur_); } + Act* reverse() const override { return new FadeOut(dur_); } +}; + +inline Act* FadeOut::reverse() const { return new FadeIn(dur_); } + +class TintTo : public ActInterval { +public: + TintTo(f32 d, const Color& c) : ActInterval(d), end_(c) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) { + if (auto* r = t->get()) { + } + } + start_ = Colors::White; + } + + void update(f32 t) override { + } + + Act* clone() const override { return new TintTo(dur_, end_); } + Act* reverse() const override { return new TintTo(dur_, start_); } + +private: + Color start_; + Color end_; +}; + +class Blink : public ActInterval { +public: + Blink(f32 d, int times) : ActInterval(d), times_(times) {} + + void update(f32 t) override { + if (target_) { + int slice = static_cast(t * times_ * 2); + bool visible = (slice % 2 == 0); + target_->setVisible(visible); + } + } + + Act* clone() const override { return new Blink(dur_, times_); } + Act* reverse() const override { return clone(); } + +private: + int times_ = 1; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_instant.h b/Extra2D/include/extra2d/act/act_instant.h new file mode 100644 index 0000000..a3e6b82 --- /dev/null +++ b/Extra2D/include/extra2d/act/act_instant.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class CallFunc : public ActInstant { +public: + explicit CallFunc(std::function f) : func_(f) {} + + void update(f32 t) override { if (func_) func_(); } + Act* clone() const override { return new CallFunc(func_); } + Act* reverse() const override { return clone(); } + +private: + std::function func_; +}; + +class Place : public ActInstant { +public: + explicit Place(const Vec2& p) : pos_(p) {} + + void update(f32 t) override { + if (target_) target_->pos = pos_; + } + Act* clone() const override { return new Place(pos_); } + Act* reverse() const override { return clone(); } + +private: + Vec2 pos_; +}; + +class Hide : public ActInstant { +public: + void update(f32 t) override { + if (target_) target_->setVisible(false); + } + Act* clone() const override { return new Hide(); } + Act* reverse() const override; +}; + +class Show : public ActInstant { +public: + void update(f32 t) override { + if (target_) target_->setVisible(true); + } + Act* clone() const override { return new Show(); } + Act* reverse() const override { return new Hide(); } +}; + +inline Act* Hide::reverse() const { return new Show(); } + +class ToggleVis : public ActInstant { +public: + void update(f32 t) override { + if (target_) target_->setVisible(!target_->visible()); + } + Act* clone() const override { return new ToggleVis(); } + Act* reverse() const override { return clone(); } +}; + +class RemoveSelf : public ActInstant { +public: + explicit RemoveSelf(bool cleanup = true) : cleanup_(cleanup) {} + + void update(f32 t) override { + if (target_) { + target_->removeFromParent(); + } + } + Act* clone() const override { return new RemoveSelf(cleanup_); } + Act* reverse() const override { return clone(); } + +private: + bool cleanup_ = true; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_move.h b/Extra2D/include/extra2d/act/act_move.h new file mode 100644 index 0000000..d4c3f2e --- /dev/null +++ b/Extra2D/include/extra2d/act/act_move.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class MoveTo : public ActInterval { +public: + MoveTo(f32 d, const Vec2& p) : ActInterval(d), end_(p) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->pos; + } + + void update(f32 t) override { + if (target_) { + target_->pos = Vec2::lerp(start_, end_, t); + } + } + + Act* clone() const override { return new MoveTo(dur_, end_); } + Act* reverse() const override { return new MoveTo(dur_, start_); } + +private: + Vec2 start_; + Vec2 end_; +}; + +class MoveBy : public ActInterval { +public: + MoveBy(f32 d, const Vec2& dlt) : ActInterval(d), dlt_(dlt) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->pos; + } + + void update(f32 t) override { + if (target_) { + target_->pos = start_ + dlt_ * t; + } + } + + Act* clone() const override { return new MoveBy(dur_, dlt_); } + Act* reverse() const override { return new MoveBy(dur_, -dlt_); } + +private: + Vec2 dlt_; + Vec2 start_; +}; + +class JumpTo : public ActInterval { +public: + JumpTo(f32 d, const Vec2& p, f32 height, u32 jumps) + : ActInterval(d), end_(p), height_(height), jumps_(jumps) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->pos; + } + + void update(f32 t) override { + if (target_) { + f32 frac = (t * jumps_) - std::floor(t * jumps_); + f32 y = height_ * std::sin(frac * PI_F); + target_->pos = Vec2::lerp(start_, end_, t); + target_->pos.y += y; + } + } + + Act* clone() const override { return new JumpTo(dur_, end_, height_, jumps_); } + Act* reverse() const override { return new JumpTo(dur_, start_, height_, jumps_); } + +private: + Vec2 start_; + Vec2 end_; + f32 height_; + u32 jumps_; +}; + +class JumpBy : public ActInterval { +public: + JumpBy(f32 d, const Vec2& dlt, f32 height, u32 jumps) + : ActInterval(d), dlt_(dlt), height_(height), jumps_(jumps) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->pos; + } + + void update(f32 t) override { + if (target_) { + f32 frac = (t * jumps_) - std::floor(t * jumps_); + f32 y = height_ * std::sin(frac * PI_F); + target_->pos = start_ + dlt_ * t; + target_->pos.y += y; + } + } + + Act* clone() const override { return new JumpBy(dur_, dlt_, height_, jumps_); } + Act* reverse() const override { return new JumpBy(dur_, -dlt_, height_, jumps_); } + +private: + Vec2 dlt_; + Vec2 start_; + f32 height_; + u32 jumps_; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_rotate.h b/Extra2D/include/extra2d/act/act_rotate.h new file mode 100644 index 0000000..96f7572 --- /dev/null +++ b/Extra2D/include/extra2d/act/act_rotate.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class RotTo : public ActInterval { +public: + RotTo(f32 d, f32 a) : ActInterval(d), end_(a) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) { + start_ = t->rot; + dlt_ = end_ - start_; + if (dlt_ > 180.0f) dlt_ -= 360.0f; + if (dlt_ < -180.0f) dlt_ += 360.0f; + } + } + + void update(f32 t) override { + if (target_) { + target_->rot = start_ + dlt_ * t; + } + } + + Act* clone() const override { return new RotTo(dur_, end_); } + Act* reverse() const override { return new RotTo(dur_, start_); } + +private: + f32 start_ = 0.0f; + f32 end_ = 0.0f; + f32 dlt_ = 0.0f; +}; + +class RotBy : public ActInterval { +public: + RotBy(f32 d, f32 dlt) : ActInterval(d), dlt_(dlt) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->rot; + } + + void update(f32 t) override { + if (target_) { + target_->rot = start_ + dlt_ * t; + } + } + + Act* clone() const override { return new RotBy(dur_, dlt_); } + Act* reverse() const override { return new RotBy(dur_, -dlt_); } + +private: + f32 dlt_ = 0.0f; + f32 start_ = 0.0f; +}; + +} diff --git a/Extra2D/include/extra2d/act/act_scale.h b/Extra2D/include/extra2d/act/act_scale.h new file mode 100644 index 0000000..b9ba37f --- /dev/null +++ b/Extra2D/include/extra2d/act/act_scale.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class ScaleTo : public ActInterval { +public: + ScaleTo(f32 d, f32 s) : ActInterval(d), end_(s, s) {} + ScaleTo(f32 d, const Vec2& s) : ActInterval(d), end_(s) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->scale; + } + + void update(f32 t) override { + if (target_) { + target_->scale = Vec2::lerp(start_, end_, t); + } + } + + Act* clone() const override { return new ScaleTo(dur_, end_); } + Act* reverse() const override { return new ScaleTo(dur_, start_); } + +private: + Vec2 start_; + Vec2 end_; +}; + +class ScaleBy : public ActInterval { +public: + ScaleBy(f32 d, f32 s) : ActInterval(d), dlt_(s, s) {} + ScaleBy(f32 d, const Vec2& s) : ActInterval(d), dlt_(s) {} + + void start(Node* t) override { + ActInterval::start(t); + if (t) start_ = t->scale; + } + + void update(f32 t) override { + if (target_) { + target_->scale = start_ + dlt_ * t; + } + } + + Act* clone() const override { return new ScaleBy(dur_, dlt_); } + Act* reverse() const override { return new ScaleBy(dur_, Vec2(1.0f / dlt_.x, 1.0f / dlt_.y)); } + +private: + Vec2 dlt_; + Vec2 start_; +}; + +} diff --git a/Extra2D/include/extra2d/asset/asset_handle.h b/Extra2D/include/extra2d/asset/asset_handle.h index 7bdce63..b951c6b 100644 --- a/Extra2D/include/extra2d/asset/asset_handle.h +++ b/Extra2D/include/extra2d/asset/asset_handle.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include diff --git a/Extra2D/include/extra2d/cam/cam.h b/Extra2D/include/extra2d/cam/cam.h new file mode 100644 index 0000000..2f38bc4 --- /dev/null +++ b/Extra2D/include/extra2d/cam/cam.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +enum class CamType { + Ortho, + Persp +}; + +class Camera { +public: + virtual ~Camera() = default; + + virtual CamType type() const = 0; + + virtual Mat4 view() const = 0; + virtual Mat4 proj() const = 0; + + Mat4 vp() const { return proj() * view(); } + + virtual Frustum frustum() const = 0; + + virtual Vec2 screenToWorld(const Vec2& s) const = 0; + virtual Vec2 worldToScreen(const Vec2& w) const = 0; + + void viewport(f32 x, f32 y, f32 w, f32 h) { + vx_ = x; vy_ = y; vw_ = w; vh_ = h; + } + + Vec2 viewportPos() const { return Vec2(vx_, vy_); } + Vec2 viewportSize() const { return Vec2(vw_, vh_); } + Rect viewportRect() const { return Rect(vx_, vy_, vw_, vh_); } + + const Vec3& position() const { return pos_; } + void setPosition(const Vec3& p) { pos_ = p; dirty_ = true; } + void setPosition(const Vec2& p) { pos_ = Vec3(p.x, p.y, pos_.z); dirty_ = true; } + + f32 rotation() const { return rot_; } + void setRotation(f32 r) { rot_ = r; dirty_ = true; } + + f32 zoom() const { return zoom_; } + void setZoom(f32 z) { zoom_ = z; dirty_ = true; } + + bool dirty() const { return dirty_; } + void setDirty(bool d) { dirty_ = d; } + +protected: + Vec3 pos_ = Vec3(0, 0, 0); + f32 rot_ = 0.0f; + f32 zoom_ = 1.0f; + f32 vx_ = 0, vy_ = 0; + f32 vw_ = 1, vh_ = 1; + bool dirty_ = true; +}; + +} diff --git a/Extra2D/include/extra2d/cam/cam_svc.h b/Extra2D/include/extra2d/cam/cam_svc.h new file mode 100644 index 0000000..9546208 --- /dev/null +++ b/Extra2D/include/extra2d/cam/cam_svc.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace extra2d { + +class ICamSvc : public IService { +public: + virtual Camera* create(CamType t, const std::string& n) = 0; + + virtual Camera* get(const std::string& n) = 0; + virtual Camera* main() = 0; + + virtual void setMain(const std::string& n) = 0; + + virtual void destroy(const std::string& n) = 0; + + virtual Ray screenRay(const Vec2& s) = 0; + + virtual Vec2 screenToWorld(const Vec2& s) = 0; + virtual Vec2 worldToScreen(const Vec2& w) = 0; + + ServiceInfo info() const override { + ServiceInfo i; + i.name = "CamSvc"; + i.priority = ServicePriority::Camera; + i.enabled = true; + return i; + } +}; + +class CamSvc : public ICamSvc { +public: + bool init() override { + auto* mainCam = create(CamType::Ortho, "main"); + if (mainCam) { + mainName_ = "main"; + } + return true; + } + + void shutdown() override { + cameras_.clear(); + mainName_.clear(); + } + + Camera* create(CamType t, const std::string& n) override { + if (cameras_.find(n) != cameras_.end()) { + return cameras_[n].get(); + } + + Unique cam; + if (t == CamType::Ortho) { + cam = ptr::makeUnique(); + } else { + return nullptr; + } + + Camera* ptr = cam.get(); + cameras_[n] = std::move(cam); + return ptr; + } + + Camera* get(const std::string& n) override { + auto it = cameras_.find(n); + if (it != cameras_.end()) { + return it->second.get(); + } + return nullptr; + } + + Camera* main() override { + return get(mainName_); + } + + void setMain(const std::string& n) override { + if (cameras_.find(n) != cameras_.end()) { + mainName_ = n; + } + } + + void destroy(const std::string& n) override { + if (n == mainName_) return; + cameras_.erase(n); + } + + Ray screenRay(const Vec2& s) override { + Camera* cam = main(); + if (cam) { + Vec2 worldPos = cam->screenToWorld(s); + return Ray(worldPos, Vec2(0, 0)); + } + return Ray(s, Vec2(0, 0)); + } + + Vec2 screenToWorld(const Vec2& s) override { + Camera* cam = main(); + if (cam) { + return cam->screenToWorld(s); + } + return s; + } + + Vec2 worldToScreen(const Vec2& w) override { + Camera* cam = main(); + if (cam) { + return cam->worldToScreen(w); + } + return w; + } + +private: + std::unordered_map> cameras_; + std::string mainName_; +}; + +} diff --git a/Extra2D/include/extra2d/cam/ortho_cam.h b/Extra2D/include/extra2d/cam/ortho_cam.h new file mode 100644 index 0000000..9c5deba --- /dev/null +++ b/Extra2D/include/extra2d/cam/ortho_cam.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + +namespace extra2d { + +class OrthoCam : public Camera { +public: + CamType type() const override { return CamType::Ortho; } + + Mat4 view() const override { + Mat4 m = glm::mat4(1.0f); + m = glm::translate(m, glm::vec3(-pos_.x, -pos_.y, -pos_.z)); + m = glm::rotate(m, -rot_ * DEG_TO_RAD, glm::vec3(0, 0, 1)); + m = glm::scale(m, glm::vec3(zoom_, zoom_, 1.0f)); + return m; + } + + Mat4 proj() const override { + return glm::ortho(l_ * zoom_, r_ * zoom_, b_ * zoom_, t_ * zoom_, n_, f_); + } + + Frustum frustum() const override { + Frustum f; + return f; + } + + Vec2 screenToWorld(const Vec2& s) const override { + f32 x = l_ + (s.x - vx_) / vw_ * (r_ - l_); + f32 y = t_ - (s.y - vy_) / vh_ * (t_ - b_); + return Vec2(x / zoom_ + pos_.x, y / zoom_ + pos_.y); + } + + Vec2 worldToScreen(const Vec2& w) const override { + f32 x = (w.x - pos_.x) * zoom_; + f32 y = (w.y - pos_.y) * zoom_; + f32 sx = vx_ + (x - l_) / (r_ - l_) * vw_; + f32 sy = vy_ + (t_ - y) / (t_ - b_) * vh_; + return Vec2(sx, sy); + } + + void ortho(f32 left, f32 right, f32 bottom, f32 top, f32 nearZ = -1, f32 farZ = 1) { + l_ = left; r_ = right; + b_ = bottom; t_ = top; + n_ = nearZ; f_ = farZ; + dirty_ = true; + } + + void setOrthoFromSize(f32 width, f32 height) { + f32 halfW = width * 0.5f; + f32 halfH = height * 0.5f; + ortho(-halfW, halfW, -halfH, halfH); + } + + f32 left() const { return l_; } + f32 right() const { return r_; } + f32 bottom() const { return b_; } + f32 top() const { return t_; } + f32 nearZ() const { return n_; } + f32 farZ() const { return f_; } + + f32 width() const { return r_ - l_; } + f32 height() const { return t_ - b_; } + + Vec2 center() const { return Vec2((l_ + r_) * 0.5f, (b_ + t_) * 0.5f); } + + void setCenter(f32 x, f32 y) { + f32 halfW = width() * 0.5f; + f32 halfH = height() * 0.5f; + l_ = x - halfW; + r_ = x + halfW; + b_ = y - halfH; + t_ = y + halfH; + dirty_ = true; + } + + void setCenter(const Vec2& c) { setCenter(c.x, c.y); } + +private: + f32 l_ = -1, r_ = 1; + f32 b_ = -1, t_ = 1; + f32 n_ = -1, f_ = 1; +}; + +} diff --git a/Extra2D/include/extra2d/core/math_extended.h b/Extra2D/include/extra2d/core/math_extended.h new file mode 100644 index 0000000..fa2118e --- /dev/null +++ b/Extra2D/include/extra2d/core/math_extended.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace extra2d { + +using Mat3 = glm::mat3; +using Mat4 = glm::mat4; +using Vec4 = glm::vec4; + +struct BoundingBox { + Vec2 min; + Vec2 max; + + constexpr BoundingBox() : min(0, 0), max(0, 0) {} + constexpr BoundingBox(const Vec2& min, const Vec2& max) : min(min), max(max) {} + constexpr BoundingBox(f32 minX, f32 minY, f32 maxX, f32 maxY) + : min(minX, minY), max(maxX, maxY) {} + + static BoundingBox fromCenter(const Vec2& center, const Vec2& halfSize) { + return BoundingBox(center - halfSize, center + halfSize); + } + + static BoundingBox fromSize(const Vec2& pos, const Vec2& size) { + return BoundingBox(pos, pos + size); + } + + Vec2 center() const { return (min + max) * 0.5f; } + Vec2 size() const { return max - min; } + Vec2 halfSize() const { return size() * 0.5f; } + + f32 width() const { return max.x - min.x; } + f32 height() const { return max.y - min.y; } + + bool empty() const { return max.x <= min.x || max.y <= min.y; } + + bool contains(const Vec2& p) const { + return p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y; + } + + bool intersects(const BoundingBox& other) const { + return !(min.x > other.max.x || max.x < other.min.x || + min.y > other.max.y || max.y < other.min.y); + } + + BoundingBox merged(const BoundingBox& other) const { + return BoundingBox( + Vec2(std::min(min.x, other.min.x), std::min(min.y, other.min.y)), + Vec2(std::max(max.x, other.max.x), std::max(max.y, other.max.y)) + ); + } + + BoundingBox expanded(f32 amount) const { + return BoundingBox(min - Vec2(amount, amount), max + Vec2(amount, amount)); + } + + static BoundingBox null() { return BoundingBox(); } +}; + +struct Ray { + Vec2 origin; + Vec2 direction; + + constexpr Ray() = default; + constexpr Ray(const Vec2& origin, const Vec2& dir) : origin(origin), direction(dir) {} + + Vec2 pointAt(f32 t) const { return origin + direction * t; } +}; + +struct Frustum { + Vec4 planes[4]; + + Frustum() = default; + + static Frustum fromVP(const Mat4& vp); + + bool containsPoint(const Vec3& point) const; + bool containsSphere(const Vec3& center, f32 radius) const; + bool containsBox(const BoundingBox& box) const; +}; + +inline Frustum Frustum::fromVP(const Mat4& vp) { + Frustum f; + for (int i = 0; i < 4; ++i) { + f.planes[i] = Vec4(0); + } + return f; +} + +inline bool Frustum::containsPoint(const Vec3& point) const { + return true; +} + +inline bool Frustum::containsSphere(const Vec3& center, f32 radius) const { + return true; +} + +inline bool Frustum::containsBox(const BoundingBox& box) const { + return true; +} + +} diff --git a/Extra2D/include/extra2d/core/math_types.h b/Extra2D/include/extra2d/core/math_types.h index ef63602..dbbb930 100644 --- a/Extra2D/include/extra2d/core/math_types.h +++ b/Extra2D/include/extra2d/core/math_types.h @@ -476,8 +476,10 @@ inline Vec2 extractPosition(const glm::mat4 &matrix) { * @return 提取的缩放向量 */ inline Vec2 extractScale(const glm::mat4 &matrix) { - float scaleX = std::sqrt(matrix[0][0] * matrix[0][0] + matrix[0][1] * matrix[0][1]); - float scaleY = std::sqrt(matrix[1][0] * matrix[1][0] + matrix[1][1] * matrix[1][1]); + float scaleX = + std::sqrt(matrix[0][0] * matrix[0][0] + matrix[0][1] * matrix[0][1]); + float scaleY = + std::sqrt(matrix[1][0] * matrix[1][0] + matrix[1][1] * matrix[1][1]); return {scaleX, scaleY}; } diff --git a/Extra2D/include/extra2d/core/ref_counted.h b/Extra2D/include/extra2d/core/ref_counted.h new file mode 100644 index 0000000..b5437ff --- /dev/null +++ b/Extra2D/include/extra2d/core/ref_counted.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace extra2d { + +class RefCounted { +public: + RefCounted() = default; + virtual ~RefCounted() = default; + + void retain() const { ++refCount_; } + void release() const { + if (--refCount_ == 0) { + delete this; + } + } + + u32 refCount() const { return refCount_; } + +private: + mutable u32 refCount_ = 0; +}; + +} diff --git a/Extra2D/include/extra2d/extra2d.h b/Extra2D/include/extra2d/extra2d.h index f384383..ed84293 100644 --- a/Extra2D/include/extra2d/extra2d.h +++ b/Extra2D/include/extra2d/extra2d.h @@ -6,9 +6,13 @@ // Core #include #include +#include #include #include #include +#include +#include +#include // Platform #include @@ -39,6 +43,45 @@ #include #include +// Node +#include +#include +#include +#include +#include +#include + +// Camera +#include +#include +#include + +// Window +#include +#include + +// Render +#include +#include +#include +#include +#include +#include + +// Action +#include +#include +#include +#include +#include +#include +#include + +// Scene +#include +#include +#include + // Application #include diff --git a/Extra2D/include/extra2d/node/node.h b/Extra2D/include/extra2d/node/node.h new file mode 100644 index 0000000..ca4dab7 --- /dev/null +++ b/Extra2D/include/extra2d/node/node.h @@ -0,0 +1,177 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +class Node; +class Comp; +class Act; + +namespace Anchor { + constexpr Vec2 BottomLeft = Vec2(0.0f, 0.0f); + constexpr Vec2 Bottom = Vec2(0.5f, 0.0f); + constexpr Vec2 BottomRight = Vec2(1.0f, 0.0f); + constexpr Vec2 Left = Vec2(0.0f, 0.5f); + constexpr Vec2 Center = Vec2(0.5f, 0.5f); + constexpr Vec2 Right = Vec2(1.0f, 0.5f); + constexpr Vec2 TopLeft = Vec2(0.0f, 1.0f); + constexpr Vec2 Top = Vec2(0.5f, 1.0f); + constexpr Vec2 TopRight = Vec2(1.0f, 1.0f); +} + +class Comp { +public: + virtual ~Comp() = default; + + virtual const char* type() const = 0; + + virtual void onAttach(Node* o) { owner_ = o; } + virtual void onDetach() { owner_ = nullptr; } + virtual void onEnable() {} + virtual void onDisable() {} + + virtual void update(f32 dt) {} + virtual void lateUpdate(f32 dt) {} + + Node* owner() const { return owner_; } + + bool enabled() const { return enabled_; } + void setEnabled(bool e); + +protected: + Node* owner_ = nullptr; + bool enabled_ = true; +}; + +class Node : public std::enable_shared_from_this { +public: + virtual ~Node(); + + Vec2 pos = Vec2(0, 0); + Vec2 scale = Vec2(1, 1); + f32 rot = 0.0f; + Vec2 anchor = Vec2(0.5f, 0.5f); + + void setAnchor(f32 x, f32 y) { anchor = Vec2(x, y); } + void setAnchor(const Vec2& a) { anchor = a; } + + void setPos(f32 x, f32 y) { pos = Vec2(x, y); } + void setPos(const Vec2& p) { pos = p; } + + void setScale(f32 x, f32 y) { scale = Vec2(x, y); } + void setScale(const Vec2& s) { scale = s; } + void setScale(f32 s) { scale = Vec2(s, s); } + + void setRot(f32 r) { rot = r; } + + Mat3 local() const; + Mat3 world() const; + Mat4 world4x4() const; + + Vec2 worldPos() const; + f32 worldRot() const; + Vec2 worldScale() const; + + template + T* add(Args&&... args) { + auto c = std::make_unique(std::forward(args)...); + T* p = c.get(); + addInternal(std::move(c)); + return p; + } + + template + T* get() { + for (auto& c : comps_) { + if (auto* p = dynamic_cast(c.get())) { + return p; + } + } + return nullptr; + } + + template + const T* get() const { + for (auto& c : comps_) { + if (auto* p = dynamic_cast(c.get())) { + return p; + } + } + return nullptr; + } + + template + void remove() { + comps_.erase( + std::remove_if(comps_.begin(), comps_.end(), + [](const auto& c) { return dynamic_cast(c.get()) != nullptr; }), + comps_.end() + ); + } + + void updateComps(f32 dt); + void lateUpdateComps(f32 dt); + + void addChild(Ref child); + void removeChild(Ref child); + void removeFromParent(); + Node* parent() const { return parent_; } + const std::vector>& children() const { return children_; } + + virtual void onInit() {} + virtual void onUpdate(f32 dt) {} + virtual void onRender() {} + + const std::string& name() const { return name_; } + void setName(const std::string& n) { name_ = n; } + + bool visible() const { return visible_; } + void setVisible(bool v) { visible_ = v; } + + i32 tag() const { return tag_; } + void setTag(i32 t) { tag_ = t; } + + Act* run(Act* a); + void stop(Act* a); + void stopByTag(i32 tag); + void stopAll(); + + Act* getActByTag(i32 tag); + i32 runningActs() const; + + void updateActs(f32 dt); + +protected: + virtual Vec2 defaultAnchor() const { return Vec2(0.5f, 0.5f); } + +private: + std::vector> comps_; + Node* parent_ = nullptr; + std::vector> children_; + std::string name_; + bool visible_ = true; + i32 tag_ = 0; + std::vector acts_; + + void addInternal(Unique c); +}; + +inline void Comp::setEnabled(bool e) { + if (enabled_ != e) { + enabled_ = e; + if (enabled_) { + onEnable(); + } else { + onDisable(); + } + } +} + +} diff --git a/Extra2D/include/extra2d/node/renderer.h b/Extra2D/include/extra2d/node/renderer.h new file mode 100644 index 0000000..abd5eae --- /dev/null +++ b/Extra2D/include/extra2d/node/renderer.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class Camera; + +class Renderer : public Comp { +public: + const char* type() const override { return "Renderer"; } + + virtual void render(class Camera* cam, RenderCmdBuffer& cmdBuffer) = 0; + virtual BoundingBox bounds() const = 0; + + i32 order = 0; + bool visible = true; + u8 opacity = 255; + + void setOpacity(u8 o) { opacity = o; } + u8 getOpacity() const { return opacity; } + + bool isVisible() const { return visible && enabled(); } + +protected: + Color applyOpacity(const Color& c) const { + return Color(c.r, c.g, c.b, c.a * (opacity / 255.0f)); + } +}; + +} diff --git a/Extra2D/include/extra2d/node/scene_graph.h b/Extra2D/include/extra2d/node/scene_graph.h new file mode 100644 index 0000000..9b732de --- /dev/null +++ b/Extra2D/include/extra2d/node/scene_graph.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +class SceneGraph { +public: + SceneGraph(); + ~SceneGraph(); + + template + Ref create(Args&&... args) { + auto node = ptr::make(std::forward(args)...); + node->onInit(); + nodes_.push_back(node); + return node; + } + + template + Ref create() { + auto node = ptr::make(); + node->onInit(); + nodes_.push_back(node); + return node; + } + + void add(Ref node); + void remove(Ref node); + void clear(); + + Node* find(const std::string& name) const; + Node* findByTag(i32 tag) const; + + void traverse(const std::function& callback); + void traverseRecursive(Node* node, const std::function& callback); + + void update(f32 dt); + + const std::vector>& roots() const { return nodes_; } + size_t nodeCount() const { return nodes_.size(); } + +private: + std::vector> nodes_; +}; + +} diff --git a/Extra2D/include/extra2d/node/shape.h b/Extra2D/include/extra2d/node/shape.h new file mode 100644 index 0000000..7a197eb --- /dev/null +++ b/Extra2D/include/extra2d/node/shape.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +enum class ShapeType { + Rect, + Circle, + Ellipse, + Line, + Poly, + RoundedRect, + Arc, + Sector, + Star, + Custom +}; + +enum class FillMode { + Fill, + Stroke, + FillStroke +}; + +class Shape : public Renderer { +public: + const char* type() const override { return "Shape"; } + + void render(Camera* cam, RenderCmdBuffer& cmdBuffer) override; + BoundingBox bounds() const override; + + ShapeType shapeType = ShapeType::Rect; + FillMode fill = FillMode::Fill; + + Vec2 size = Vec2(100, 100); + f32 radius = 50.0f; + f32 lineWidth = 1.0f; + Color fillColor = Colors::White; + Color strokeColor = Colors::Black; + + f32 cornerRadius = 0.0f; + + Vec2 start = Vec2(0, 0); + Vec2 end = Vec2(100, 0); + + std::vector points; + u32 sides = 3; + f32 innerRadius = 0.5f; + + f32 startAngle = 0.0f; + f32 sweepAngle = 3.14159f; + + Vec2 anchor = Vec2(0.5f, 0.5f); + + void setAnchor(f32 x, f32 y) { anchor = Vec2(x, y); } + void setAnchor(const Vec2& a) { anchor = a; } + + static Shape* rect(const Vec2& sz, const Color& c); + static Shape* circle(f32 r, const Color& c); + static Shape* ellipse(const Vec2& sz, const Color& c); + static Shape* line(const Vec2& s, const Vec2& e, f32 w, const Color& c); + static Shape* roundRect(const Vec2& sz, f32 r, const Color& c); + static Shape* poly(const std::vector& pts, const Color& c); + static Shape* star(f32 outerR, f32 innerR, u32 n, const Color& c); + static Shape* arc(f32 r, f32 start, f32 sweep, const Color& c); + static Shape* sector(f32 r, f32 start, f32 sweep, const Color& c); + +private: + void renderRect(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderCircle(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderEllipse(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderLine(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderRoundedRect(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderPoly(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderArc(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderSector(Camera* cam, RenderCmdBuffer& cmdBuffer); + void renderStar(Camera* cam, RenderCmdBuffer& cmdBuffer); + + std::vector createCircleVertices(f32 cx, f32 cy, f32 r, u32 segments, const Color& c); + std::vector createCircleIndices(u32 baseIndex, u32 segments); +}; + +} diff --git a/Extra2D/include/extra2d/node/sprite.h b/Extra2D/include/extra2d/node/sprite.h new file mode 100644 index 0000000..1a84b26 --- /dev/null +++ b/Extra2D/include/extra2d/node/sprite.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +struct TextureAsset; + +class Sprite : public Renderer { +public: + const char* type() const override { return "Sprite"; } + + void render(Camera* cam, RenderCmdBuffer& cmdBuffer) override; + BoundingBox bounds() const override; + + AssetHandle tex; + Color color = Colors::White; + Vec2 size = Vec2(64, 64); + Vec2 uv0 = Vec2(0, 0); + Vec2 uv1 = Vec2(1, 1); + Vec2 anchor = Vec2(0.5f, 0.5f); + bool flipX = false; + bool flipY = false; + + void setAnchor(f32 x, f32 y) { anchor = Vec2(x, y); } + void setAnchor(const Vec2& a) { anchor = a; } + void setSize(f32 w, f32 h) { size = Vec2(w, h); } + void setColor(const Color& c) { color = c; } + + static Sprite* create(const Vec2& sz, const Color& c = Colors::White); +}; + +} diff --git a/Extra2D/include/extra2d/node/text.h b/Extra2D/include/extra2d/node/text.h new file mode 100644 index 0000000..376ca9c --- /dev/null +++ b/Extra2D/include/extra2d/node/text.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +struct FontAsset; + +enum class TextAlignment { + Left, + Center, + Right +}; + +enum class TextVAlignment { + Top, + Middle, + Bottom +}; + +class Text : public Renderer { +public: + const char* type() const override { return "Text"; } + + void render(Camera* cam, RenderCmdBuffer& cmdBuffer) override; + BoundingBox bounds() const override; + + AssetHandle font; + std::string str; + f32 fontSize = 16.0f; + Color color = Colors::White; + Vec2 anchor = Vec2(0, 0); + TextAlignment alignment = TextAlignment::Left; + TextVAlignment vAlignment = TextVAlignment::Top; + f32 lineHeight = 1.2f; + f32 letterSpacing = 0.0f; + bool wordWrap = false; + f32 maxWidth = 0.0f; + + void setAnchor(f32 x, f32 y) { anchor = Vec2(x, y); } + void setAnchor(const Vec2& a) { anchor = a; } + void setString(const std::string& s) { str = s; } + void setFontSize(f32 s) { fontSize = s; } + void setColor(const Color& c) { color = c; } + + Vec2 measureText() const; + Vec2 measureText(const std::string& text) const; + + static Text* create(const std::string& text, f32 size, const Color& c = Colors::White); +}; + +} diff --git a/Extra2D/include/extra2d/render/backend/vulkan/vk_backend.h b/Extra2D/include/extra2d/render/backend/vulkan/vk_backend.h new file mode 100644 index 0000000..1004e16 --- /dev/null +++ b/Extra2D/include/extra2d/render/backend/vulkan/vk_backend.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +struct SwapchainSupport { + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; + +class VkBackend { +public: + VkBackend() = default; + ~VkBackend() { shutdown(); } + + bool init(VkInstance instance, VkPhysicalDevice physDevice, VkDevice device); + void shutdown(); + + bool createSwapchain(VkSurfaceKHR surface, u32 width, u32 height); + void destroySwapchain(); + bool resizeSwapchain(u32 width, u32 height); + + VkSwapchainKHR swapchain() const { return swapchain_; } + VkFormat swapchainFormat() const { return swapchainFormat_; } + VkExtent2D swapchainExtent() const { return swapchainExtent_; } + const std::vector& swapchainImages() const { return swapchainImages_; } + const std::vector& swapchainViews() const { return swapchainViews_; } + + u32 acquireNextImage(VkSemaphore semaphore, VkFence fence); + void present(u32 imageIndex, VkSemaphore waitSemaphore); + + VkCommandBuffer beginCmd(); + void endCmd(VkCommandBuffer cmd); + void submitCmd(VkCommandBuffer cmd, VkSemaphore wait, VkSemaphore signal, VkFence fence); + + void waitIdle(); + + VkDevice device() const { return device_; } + VkPhysicalDevice physDevice() const { return physDevice_; } + VkInstance instance() const { return instance_; } + + VkMemAlloc* memAlloc() { return &memAlloc_; } + + VkCommandPool cmdPool() const { return cmdPool_; } + +private: + VkInstance instance_ = VK_NULL_HANDLE; + VkPhysicalDevice physDevice_ = VK_NULL_HANDLE; + VkDevice device_ = VK_NULL_HANDLE; + + VkSurfaceKHR surface_ = VK_NULL_HANDLE; + VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; + VkFormat swapchainFormat_ = VK_FORMAT_UNDEFINED; + VkExtent2D swapchainExtent_ = {0, 0}; + std::vector swapchainImages_; + std::vector swapchainViews_; + + VkCommandPool cmdPool_ = VK_NULL_HANDLE; + std::vector cmdBuffers_; + + VkMemAlloc memAlloc_; + + u32 graphicsFamily_ = 0; + u32 presentFamily_ = 0; + u32 frameIdx_ = 0; + + SwapchainSupport querySwapchainSupport(VkPhysicalDevice device); + VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector& formats); + VkPresentModeKHR choosePresentMode(const std::vector& modes); + VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& caps, u32 width, u32 height); +}; + +} diff --git a/Extra2D/include/extra2d/render/backend/vulkan/vk_mem.h b/Extra2D/include/extra2d/render/backend/vulkan/vk_mem.h new file mode 100644 index 0000000..b4cdb92 --- /dev/null +++ b/Extra2D/include/extra2d/render/backend/vulkan/vk_mem.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +enum class MemType { + Gpu, + Upload, + Readback +}; + +class VkMemAlloc { +public: + VkMemAlloc() = default; + ~VkMemAlloc() { shutdown(); } + + bool init(VkInstance inst, VkDevice device, VkPhysicalDevice physDevice); + void shutdown(); + + VkBuffer createBuffer( + VkDeviceSize size, + VkBufferUsageFlags usage, + MemType type, + VmaAllocation* outAlloc + ); + + VkImage createImage( + const VkImageCreateInfo& info, + MemType type, + VmaAllocation* outAlloc + ); + + void destroyBuffer(VkBuffer buf, VmaAllocation alloc); + void destroyImage(VkImage img, VmaAllocation alloc); + + void* map(VmaAllocation alloc); + void unmap(VmaAllocation alloc); + + void flush(VmaAllocation alloc, VkDeviceSize off = 0, VkDeviceSize size = VK_WHOLE_SIZE); + void invalidate(VmaAllocation alloc, VkDeviceSize off = 0, VkDeviceSize size = VK_WHOLE_SIZE); + + void getStats(VmaTotalStatistics* stats); + + VmaAllocator allocator() const { return allocator_; } + +private: + VmaAllocator allocator_ = VK_NULL_HANDLE; +}; + +class VkBufferWrap { +public: + VkBufferWrap() = default; + VkBufferWrap(VkMemAlloc* alloc, VkDeviceSize size, VkBufferUsageFlags usage, MemType type); + ~VkBufferWrap(); + + VkBufferWrap(const VkBufferWrap&) = delete; + VkBufferWrap& operator=(const VkBufferWrap&) = delete; + + VkBufferWrap(VkBufferWrap&& other) noexcept; + VkBufferWrap& operator=(VkBufferWrap&& other) noexcept; + + VkBuffer buffer() const { return buffer_; } + VmaAllocation vmaAlloc() const { return vmaAlloc_; } + VkDeviceSize size() const { return size_; } + + void* map(); + void unmap(); + + void write(const void* data, VkDeviceSize size, VkDeviceSize off = 0); + void read(void* data, VkDeviceSize size, VkDeviceSize off = 0); + + bool valid() const { return buffer_ != VK_NULL_HANDLE; } + + void release(); + +private: + VkMemAlloc* memAlloc_ = nullptr; + VkBuffer buffer_ = VK_NULL_HANDLE; + VmaAllocation vmaAlloc_ = VK_NULL_HANDLE; + VkDeviceSize size_ = 0; + MemType type_ = MemType::Gpu; + void* mapped_ = nullptr; +}; + +class VkImageWrap { +public: + VkImageWrap() = default; + VkImageWrap(VkMemAlloc* alloc, const VkImageCreateInfo& info, MemType type); + ~VkImageWrap(); + + VkImageWrap(const VkImageWrap&) = delete; + VkImageWrap& operator=(const VkImageWrap&) = delete; + + VkImageWrap(VkImageWrap&& other) noexcept; + VkImageWrap& operator=(VkImageWrap&& other) noexcept; + + VkImage image() const { return image_; } + VmaAllocation vmaAlloc() const { return vmaAlloc_; } + VkExtent3D extent() const { return extent_; } + VkFormat format() const { return format_; } + + bool valid() const { return image_ != VK_NULL_HANDLE; } + + void release(); + +private: + VkMemAlloc* memAlloc_ = nullptr; + VkImage image_ = VK_NULL_HANDLE; + VmaAllocation vmaAlloc_ = VK_NULL_HANDLE; + VkExtent3D extent_; + VkFormat format_; +}; + +} diff --git a/Extra2D/include/extra2d/render/dyn_shader.h b/Extra2D/include/extra2d/render/dyn_shader.h new file mode 100644 index 0000000..ba272a5 --- /dev/null +++ b/Extra2D/include/extra2d/render/dyn_shader.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { + +class DynShader { +public: + void add(const std::string& name, const MatShader& shader) { + shaders_[name] = shader; + } + + void use(const std::string& name) { + if (shaders_.find(name) != shaders_.end()) { + prev_ = cur_; + cur_ = name; + blend_ = 1.0f; + } + } + + const MatShader* cur() const { + auto it = shaders_.find(cur_); + if (it != shaders_.end()) { + return &it->second; + } + return nullptr; + } + + const MatShader* prev() const { + auto it = shaders_.find(prev_); + if (it != shaders_.end()) { + return &it->second; + } + return nullptr; + } + + void fadeTo(const std::string& name, f32 duration) { + if (shaders_.find(name) != shaders_.end()) { + prev_ = cur_; + cur_ = name; + blend_ = 0.0f; + blendSpeed_ = 1.0f / duration; + } + } + + void update(f32 dt) { + if (blend_ < 1.0f) { + blend_ += blendSpeed_ * dt; + if (blend_ >= 1.0f) { + blend_ = 1.0f; + prev_.clear(); + } + } + } + + f32 blend() const { return blend_; } + + bool transitioning() const { return blend_ < 1.0f; } + +private: + std::unordered_map shaders_; + std::string cur_; + std::string prev_; + f32 blend_ = 1.0f; + f32 blendSpeed_ = 0.0f; +}; + +} diff --git a/Extra2D/include/extra2d/render/render_mod.h b/Extra2D/include/extra2d/render/render_mod.h new file mode 100644 index 0000000..f79832f --- /dev/null +++ b/Extra2D/include/extra2d/render/render_mod.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +struct RenderModCfg { + bool vsync = true; + bool shaderHotReload = true; + u32 maxFramesInFlight = 2; +}; + +class RenderMod : public Module { +public: + RenderMod() = default; + ~RenderMod() override = default; + + bool init() override { return true; } + void shutdown() override {} + bool ok() const override { return true; } + const char* name() const override { return "RenderMod"; } + + void configure(const RenderModCfg& cfg) { cfg_ = cfg; } + const RenderModCfg& config() const { return cfg_; } + + void update(f32 dt) { + if (cfg_.shaderHotReload) { + ShaderMgr::get().checkFileChanges(); + } + } + +private: + RenderModCfg cfg_; +}; + +} diff --git a/Extra2D/include/extra2d/render/render_svc.h b/Extra2D/include/extra2d/render/render_svc.h new file mode 100644 index 0000000..55dab2f --- /dev/null +++ b/Extra2D/include/extra2d/render/render_svc.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class Camera; +class ShaderMgr; +class GPURes; + +struct RenderCtx { + Camera* cam = nullptr; + Mat4 vp; + Frustum frustum; + Vec2 viewport; + u32 frameIdx = 0; + f32 dt = 0.0f; +}; + +class IRenderSvc : public IService { +public: + virtual TargetHandle createTarget(const TargetDesc& d) = 0; + virtual PipeHandle createPipe(const PipeDesc& d) = 0; + virtual DescSetLayoutHandle createDescSetLayout(const DescSetLayoutDesc& d) = 0; + + virtual void begin() = 0; + virtual void end() = 0; + virtual void wait() = 0; + + virtual GPURes* getRes(ResHandle h) = 0; + virtual ShaderMgr* shaderMgr() = 0; + + virtual void submit(RenderCmdBuffer& cmdBuffer) = 0; + + virtual void setViewport(f32 x, f32 y, f32 w, f32 h) = 0; + virtual void setScissor(f32 x, f32 y, f32 w, f32 h) = 0; + + virtual Vec2 viewportSize() const = 0; + + ServiceInfo info() const override { + ServiceInfo i; + i.name = "RenderSvc"; + i.priority = ServicePriority::Resource; + i.enabled = true; + return i; + } +}; + +} diff --git a/Extra2D/include/extra2d/render/render_types.h b/Extra2D/include/extra2d/render/render_types.h new file mode 100644 index 0000000..5dc6de3 --- /dev/null +++ b/Extra2D/include/extra2d/render/render_types.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +using ResHandle = u64; +constexpr ResHandle INVALID_HANDLE = 0; + +using TargetHandle = ResHandle; +using PipeHandle = ResHandle; +using DescSetLayoutHandle = ResHandle; + +struct Vertex2D { + Vec2 pos; + Vec2 uv; + Color color; + u32 texId; + + Vertex2D() : pos(0, 0), uv(0, 0), color(Colors::White), texId(0) {} + Vertex2D(const Vec2& p, const Vec2& u, const Color& c, u32 t = 0) + : pos(p), uv(u), color(c), texId(t) {} +}; + +struct RenderCmd { + enum class Type { + DrawQuad, + DrawIndexed, + DrawInstanced, + SetPipeline, + SetTexture, + SetUniform, + PushConstants, + SetScissor, + SetViewport + }; + + Type type; + ResHandle pipeline; + ResHandle texture; + u32 vertexOffset; + u32 vertexCount; + u32 indexOffset; + u32 indexCount; + u32 instanceCount; + Rect scissor; + Rect viewport; + std::vector uniformData; +}; + +class RenderCmdBuffer { +public: + void clear() { + cmds_.clear(); + vertices_.clear(); + indices_.clear(); + } + + void addCmd(const RenderCmd& cmd) { + cmds_.push_back(cmd); + } + + u32 addVertices(const std::vector& verts) { + u32 offset = static_cast(vertices_.size()); + vertices_.insert(vertices_.end(), verts.begin(), verts.end()); + return offset; + } + + u32 addIndices(const std::vector& indices) { + u32 offset = static_cast(indices_.size()); + indices_.insert(indices_.end(), indices.begin(), indices.end()); + return offset; + } + + const std::vector& cmds() const { return cmds_; } + const std::vector& vertices() const { return vertices_; } + const std::vector& indices() const { return indices_; } + + std::vector& cmds() { return cmds_; } + std::vector& vertices() { return vertices_; } + std::vector& indices() { return indices_; } + +private: + std::vector cmds_; + std::vector vertices_; + std::vector indices_; +}; + +struct TargetDesc { + u32 width = 0; + u32 height = 0; + u32 layers = 1; + u32 mipLevels = 1; + u32 sampleCount = 1; + bool hasDepth = false; + bool hasStencil = false; + std::string name; +}; + +struct PipeDesc { + std::string name; + std::string vertShader; + std::string fragShader; + std::string geomShader; + bool blending = true; + bool depthTest = false; + bool depthWrite = false; +}; + +struct DescSetLayoutDesc { + u32 bindingCount = 0; + std::vector bindingIndices; + std::vector descriptorCounts; +}; + +} diff --git a/Extra2D/include/extra2d/render/shader_mgr.h b/Extra2D/include/extra2d/render/shader_mgr.h new file mode 100644 index 0000000..dbcd2e3 --- /dev/null +++ b/Extra2D/include/extra2d/render/shader_mgr.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +enum class ShaderType { + Vert, + Frag, + Geom, + Comp +}; + +struct ShaderDef { + std::string name; + ShaderType type; + std::string entry = "main"; + std::vector spirv; + std::string src; + std::string path; +}; + +using ShaderReloadCb = std::function; + +class ShaderMgr { +public: + static ShaderMgr& get() { + static ShaderMgr instance; + return instance; + } + + bool load(const std::string& name, const std::string& path, ShaderType type); + + bool loadFromSrc(const std::string& name, const std::string& src, ShaderType type); + + bool loadFromSpirv(const std::string& name, const std::vector& spirv, ShaderType type); + + const ShaderDef* get(const std::string& name) const { + auto it = shaders_.find(name); + if (it != shaders_.end()) { + return &it->second; + } + return nullptr; + } + + bool reload(const std::string& name); + + void reloadAll(); + + void onReload(ShaderReloadCb cb) { reloadCb_ = cb; } + + void checkFileChanges(); + + void setAutoReload(bool enable) { autoReload_ = enable; } + + const std::vector& getSpirv(const std::string& name) const { + static std::vector empty; + auto it = shaders_.find(name); + if (it != shaders_.end()) { + return it->second.spirv; + } + return empty; + } + + void unload(const std::string& name) { + shaders_.erase(name); + fileTimes_.erase(name); + } + + void unloadAll() { + shaders_.clear(); + fileTimes_.clear(); + } + +private: + ShaderMgr() = default; + ~ShaderMgr() = default; + + std::unordered_map shaders_; + std::unordered_map fileTimes_; + ShaderReloadCb reloadCb_; + bool autoReload_ = true; +}; + +struct MatShader { + std::string vert; + std::string frag; + std::string geom; + + bool valid() const { + return !vert.empty() && !frag.empty(); + } +}; + +} diff --git a/Extra2D/include/extra2d/render/shader_sdf.h b/Extra2D/include/extra2d/render/shader_sdf.h new file mode 100644 index 0000000..5ec1e01 --- /dev/null +++ b/Extra2D/include/extra2d/render/shader_sdf.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +namespace extra2d { +namespace sdf { + +enum class Shape { + Circle, + Rect, + RoundedRect, + Triangle, + Polygon, + Custom +}; + +struct Mat { + Shape shape = Shape::Circle; + Vec2 size = Vec2(1, 1); + f32 radius = 0.5f; + f32 edgeSoftness = 0.01f; + f32 outlineWidth = 0.0f; + Color outlineColor = Colors::Black; + bool antiAlias = true; + std::string customSdfFunc; +}; + +std::string genFragSrc(const Mat& mat); + +extern const char* kCircleSdf; +extern const char* kRectSdf; +extern const char* kRoundedRectSdf; + +} +} diff --git a/Extra2D/include/extra2d/scene/director.h b/Extra2D/include/extra2d/scene/director.h new file mode 100644 index 0000000..356cf75 --- /dev/null +++ b/Extra2D/include/extra2d/scene/director.h @@ -0,0 +1,174 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class Trans; + +class IDirector : public IService { +public: + virtual void run(Ref s) = 0; + virtual void replace(Ref s) = 0; + virtual void push(Ref s) = 0; + virtual void pop() = 0; + virtual void popToRoot() = 0; + virtual void popTo(const std::string& n) = 0; + + virtual void replaceWith(Ref s, Ref t) = 0; + virtual void pushWith(Ref s, Ref t) = 0; + + virtual Scene* cur() const = 0; + virtual Scene* next() const = 0; + virtual bool transing() const = 0; + + virtual void update(f32 dt) = 0; + virtual void render(RenderCmdBuffer& cmdBuffer) = 0; + + ServiceInfo info() const override { + ServiceInfo i; + i.name = "Director"; + i.priority = ServicePriority::Scene; + i.enabled = true; + return i; + } +}; + +class Director : public IDirector { +public: + bool init() override { + return true; + } + + void shutdown() override { + sceneStack_.clear(); + curScene_.reset(); + nextScene_.reset(); + transScene_.reset(); + } + + void run(Ref s) override { + if (curScene_) { + curScene_->onExit(); + curScene_->setRunning(false); + } + sceneStack_.clear(); + curScene_ = s; + if (curScene_) { + curScene_->onEnter(); + curScene_->setRunning(true); + } + } + + void replace(Ref s) override { + if (curScene_) { + curScene_->onExit(); + curScene_->setRunning(false); + } + if (!sceneStack_.empty()) { + sceneStack_.pop_back(); + } + curScene_ = s; + if (curScene_) { + curScene_->onEnter(); + curScene_->setRunning(true); + } + } + + void push(Ref s) override { + if (curScene_) { + curScene_->onPause(); + curScene_->setRunning(false); + sceneStack_.push_back(curScene_); + } + curScene_ = s; + if (curScene_) { + curScene_->onEnter(); + curScene_->setRunning(true); + } + } + + void pop() override { + if (curScene_) { + curScene_->onExit(); + curScene_->setRunning(false); + } + if (!sceneStack_.empty()) { + curScene_ = sceneStack_.back(); + sceneStack_.pop_back(); + if (curScene_) { + curScene_->onResume(); + curScene_->setRunning(true); + } + } else { + curScene_.reset(); + } + } + + void popToRoot() override { + while (sceneStack_.size() > 1) { + if (curScene_) { + curScene_->onExit(); + } + sceneStack_.pop_back(); + } + pop(); + } + + void popTo(const std::string& n) override { + while (!sceneStack_.empty()) { + if (curScene_ && curScene_->name() == n) { + return; + } + pop(); + } + } + + void replaceWith(Ref s, Ref t) override { + transScene_ = t; + nextScene_ = s; + } + + void pushWith(Ref s, Ref t) override { + transScene_ = t; + nextScene_ = s; + isPush_ = true; + } + + Scene* cur() const override { return curScene_.get(); } + Scene* next() const override { return nextScene_.get(); } + bool transing() const override { return transScene_ != nullptr; } + + void update(f32 dt) override { + if (transScene_) { + transScene_->update(dt); + return; + } + + if (curScene_) { + curScene_->update(dt); + } + } + + void render(RenderCmdBuffer& cmdBuffer) override { + if (transScene_) { + transScene_->render(cmdBuffer); + return; + } + + if (curScene_) { + curScene_->render(cmdBuffer); + } + } + +private: + std::vector> sceneStack_; + Ref curScene_; + Ref nextScene_; + Ref transScene_; + bool isPush_ = false; +}; + +} diff --git a/Extra2D/include/extra2d/scene/scene.h b/Extra2D/include/extra2d/scene/scene.h new file mode 100644 index 0000000..32615bc --- /dev/null +++ b/Extra2D/include/extra2d/scene/scene.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +class Scene { +public: + virtual ~Scene() = default; + + virtual void onEnter() {} + virtual void onExit() {} + virtual void onPause() {} + virtual void onResume() {} + + virtual void update(f32 dt) { + if (graph_) { + graph_->update(dt); + } + } + + virtual void render(RenderCmdBuffer& cmdBuffer) {} + + SceneGraph* graph() const { return graph_.get(); } + Camera* cam() const { return cam_.get(); } + + const std::string& name() const { return name_; } + void setName(const std::string& n) { name_ = n; } + + bool running() const { return running_; } + void setRunning(bool r) { running_ = r; } + +protected: + Unique graph_; + Unique cam_; + std::string name_; + bool running_ = false; + + Scene() { + graph_ = ptr::makeUnique(); + cam_ = ptr::makeUnique(); + } +}; + +} diff --git a/Extra2D/include/extra2d/scene/trans.h b/Extra2D/include/extra2d/scene/trans.h new file mode 100644 index 0000000..10076a6 --- /dev/null +++ b/Extra2D/include/extra2d/scene/trans.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include + +namespace extra2d { + +class Trans : public Scene { +public: + virtual ~Trans() = default; + + void onEnter() override { + progress_ = 0.0f; + } + + void onExit() override { + } + + void update(f32 dt) override { + progress_ += dt / duration_; + if (progress_ >= 1.0f) { + progress_ = 1.0f; + finish(); + } + } + + void render(RenderCmdBuffer& cmdBuffer) override { + draw(progress_); + } + + virtual void draw(f32 p) = 0; + + void finish(); + + Scene* inScene() const { return inScene_.get(); } + Scene* outScene() const { return outScene_.get(); } + f32 duration() const { return duration_; } + f32 progress() const { return progress_; } + + void init(f32 duration, Ref inScene, Ref outScene) { + duration_ = duration; + inScene_ = inScene; + outScene_ = outScene; + } + +protected: + Ref inScene_; + Ref outScene_; + f32 duration_ = 0.0f; + f32 progress_ = 0.0f; + bool inOnTop_ = false; +}; + +class TransFade : public Trans { +public: + void init(f32 duration, Ref inScene, Ref outScene, const Color& color = Colors::Black) { + Trans::init(duration, inScene, outScene); + color_ = color; + } + + void draw(f32 p) override; + +private: + Color color_ = Colors::Black; +}; + +class TransFlipX : public Trans { +public: + void draw(f32 p) override; +}; + +class TransFlipY : public Trans { +public: + void draw(f32 p) override; +}; + +class TransMoveInL : public Trans { +public: + void draw(f32 p) override; +}; + +class TransMoveInR : public Trans { +public: + void draw(f32 p) override; +}; + +class TransMoveInT : public Trans { +public: + void draw(f32 p) override; +}; + +class TransMoveInB : public Trans { +public: + void draw(f32 p) override; +}; + +class TransZoomFlipX : public Trans { +public: + void draw(f32 p) override; +}; + +class TransZoomFlipY : public Trans { +public: + void draw(f32 p) override; +}; + +class TransRotoZoom : public Trans { +public: + void draw(f32 p) override; +}; + +class TransJumpZoom : public Trans { +public: + void draw(f32 p) override; +}; + +} diff --git a/Extra2D/include/extra2d/win/win_adapt.h b/Extra2D/include/extra2d/win/win_adapt.h new file mode 100644 index 0000000..d6c70d5 --- /dev/null +++ b/Extra2D/include/extra2d/win/win_adapt.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace extra2d { + +enum class AdaptMode { + Exact, + Stretch, + Fit, + Fill, + Center +}; + +struct WinAdaptCfg { + AdaptMode mode = AdaptMode::Fit; + f32 refW = 1280; + f32 refH = 720; + bool autoScale = true; + bool autoPos = true; + Color bgColor = Colors::Black; +}; + +class WinAdapt { +public: + void cfg(const WinAdaptCfg& c) { cfg_ = c; } + WinAdaptCfg cfg() const { return cfg_; } + + void ref(f32 w, f32 h) { cfg_.refW = w; cfg_.refH = h; } + void ref(const Vec2& s) { ref(s.x, s.y); } + Vec2 ref() const { return Vec2(cfg_.refW, cfg_.refH); } + + void win(f32 w, f32 h) { + winW_ = w; winH_ = h; + update(); + } + void win(const Vec2& s) { win(s.x, s.y); } + Vec2 win() const { return Vec2(winW_, winH_); } + + Rect viewport() const { return vp_; } + + f32 scale() const { return scale_; } + Vec2 scale2D() const { return Vec2(scaleX_, scaleY_); } + + Vec2 winToRef(const Vec2& p) const { + return Vec2( + (p.x - vp_.origin.x) / scaleX_, + (p.y - vp_.origin.y) / scaleY_ + ); + } + + Vec2 refToWin(const Vec2& p) const { + return Vec2( + p.x * scaleX_ + vp_.origin.x, + p.y * scaleY_ + vp_.origin.y + ); + } + + Vec2 screenToRef(const Vec2& s) const { + return winToRef(s); + } + + Vec2 refToScreen(const Vec2& r) const { + return refToWin(r); + } + + void update() { + if (winW_ <= 0 || winH_ <= 0) return; + + scaleX_ = winW_ / cfg_.refW; + scaleY_ = winH_ / cfg_.refH; + + switch (cfg_.mode) { + case AdaptMode::Exact: + scale_ = 1.0f; + scaleX_ = scaleY_ = 1.0f; + vp_ = Rect(0, 0, winW_, winH_); + break; + + case AdaptMode::Stretch: + scale_ = 1.0f; + vp_ = Rect(0, 0, winW_, winH_); + break; + + case AdaptMode::Fit: + scale_ = std::min(scaleX_, scaleY_); + scaleX_ = scaleY_ = scale_; + { + f32 vpW = cfg_.refW * scale_; + f32 vpH = cfg_.refH * scale_; + f32 vpX = (winW_ - vpW) * 0.5f; + f32 vpY = (winH_ - vpH) * 0.5f; + vp_ = Rect(vpX, vpY, vpW, vpH); + } + break; + + case AdaptMode::Fill: + scale_ = std::max(scaleX_, scaleY_); + scaleX_ = scaleY_ = scale_; + { + f32 vpW = cfg_.refW * scale_; + f32 vpH = cfg_.refH * scale_; + f32 vpX = (winW_ - vpW) * 0.5f; + f32 vpY = (winH_ - vpH) * 0.5f; + vp_ = Rect(vpX, vpY, vpW, vpH); + } + break; + + case AdaptMode::Center: + scale_ = 1.0f; + scaleX_ = scaleY_ = 1.0f; + { + f32 vpX = (winW_ - cfg_.refW) * 0.5f; + f32 vpY = (winH_ - cfg_.refH) * 0.5f; + vp_ = Rect(vpX, vpY, cfg_.refW, cfg_.refH); + } + break; + } + + offset_ = Vec2(vp_.origin.x, vp_.origin.y); + } + + void apply(class Camera* cam) const; + + Mat4 matrix() const { + Mat4 m = glm::mat4(1.0f); + m = glm::translate(m, glm::vec3(offset_.x, offset_.y, 0.0f)); + m = glm::scale(m, glm::vec3(scale_, scale_, 1.0f)); + return m; + } + +private: + WinAdaptCfg cfg_; + f32 winW_ = 0, winH_ = 0; + f32 scale_ = 1.0f; + f32 scaleX_ = 1.0f, scaleY_ = 1.0f; + Rect vp_; + Vec2 offset_; +}; + +} diff --git a/Extra2D/include/extra2d/win/win_adapt_svc.h b/Extra2D/include/extra2d/win/win_adapt_svc.h new file mode 100644 index 0000000..f3d685b --- /dev/null +++ b/Extra2D/include/extra2d/win/win_adapt_svc.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include + +namespace extra2d { + +class IWinAdaptSvc : public IService { +public: + virtual void cfg(const WinAdaptCfg& c) = 0; + virtual WinAdaptCfg cfg() const = 0; + + virtual void ref(f32 w, f32 h) = 0; + virtual Vec2 ref() const = 0; + + virtual void win(f32 w, f32 h) = 0; + virtual Vec2 win() const = 0; + + virtual WinAdapt* adapt() = 0; + + virtual Vec2 toRef(const Vec2& p) = 0; + virtual Vec2 toWin(const Vec2& p) = 0; + + virtual void apply(Camera* cam) = 0; + + virtual void onResize(f32 w, f32 h) = 0; + + ServiceInfo info() const override { + ServiceInfo i; + i.name = "WinAdaptSvc"; + i.priority = ServicePriority::Camera; + i.enabled = true; + return i; + } +}; + +class WinAdaptSvc : public IWinAdaptSvc { +public: + bool init() override { + return true; + } + + void shutdown() override { + } + + void cfg(const WinAdaptCfg& c) override { + adapt_.cfg(c); + adapt_.update(); + } + + WinAdaptCfg cfg() const override { + return adapt_.cfg(); + } + + void ref(f32 w, f32 h) override { + adapt_.ref(w, h); + adapt_.update(); + } + + Vec2 ref() const override { + return adapt_.ref(); + } + + void win(f32 w, f32 h) override { + adapt_.win(w, h); + } + + Vec2 win() const override { + return adapt_.win(); + } + + WinAdapt* adapt() override { + return &adapt_; + } + + Vec2 toRef(const Vec2& p) override { + return adapt_.winToRef(p); + } + + Vec2 toWin(const Vec2& p) override { + return adapt_.refToWin(p); + } + + void apply(Camera* cam) override { + adapt_.apply(cam); + } + + void onResize(f32 w, f32 h) override { + adapt_.win(w, h); + } + +private: + WinAdapt adapt_; +}; + +} diff --git a/Extra2D/src/act/act.cpp b/Extra2D/src/act/act.cpp new file mode 100644 index 0000000..cef6557 --- /dev/null +++ b/Extra2D/src/act/act.cpp @@ -0,0 +1,18 @@ +#include "extra2d/act/act.h" +#include "extra2d/node/node.h" + +namespace extra2d { + +void Act::start(Node* t) { + target_ = t; + origTarget_ = t; +} + +void Act::stop() { + target_ = nullptr; +} + +void Act::step(f32 dt) { +} + +} diff --git a/Extra2D/src/act/act_composite.cpp b/Extra2D/src/act/act_composite.cpp new file mode 100644 index 0000000..703f77e --- /dev/null +++ b/Extra2D/src/act/act_composite.cpp @@ -0,0 +1,299 @@ +#include "extra2d/act/act_composite.h" +#include + +namespace extra2d { + +Seq::Seq(Act* a1, Act* a2) : ActInterval(0) { + if (a1) acts_.push_back(a1); + if (a2) acts_.push_back(a2); + + f32 totalDur = 0; + for (auto* a : acts_) { + if (auto* interval = dynamic_cast(a)) { + totalDur += interval->dur(); + } + } + dur_ = totalDur; +} + +Seq* Seq::createFromArray(std::initializer_list acts) { + auto* seq = new Seq(nullptr, nullptr); + seq->acts_.clear(); + + f32 totalDur = 0; + for (auto* a : acts) { + if (a) { + seq->acts_.push_back(a); + if (auto* interval = dynamic_cast(a)) { + totalDur += interval->dur(); + } + } + } + seq->dur_ = totalDur; + return seq; +} + +void Seq::start(Node* t) { + ActInterval::start(t); + curIdx_ = 0; + if (!acts_.empty()) { + acts_[0]->start(t); + } +} + +void Seq::stop() { + if (curIdx_ < acts_.size() && acts_[curIdx_]) { + acts_[curIdx_]->stop(); + } + ActInterval::stop(); +} + +void Seq::step(f32 dt) { + if (acts_.empty()) { + elap_ = dur_; + return; + } + + elap_ += dt; + + while (curIdx_ < acts_.size()) { + auto* cur = acts_[curIdx_]; + if (auto* interval = dynamic_cast(cur)) { + interval->step(dt); + if (!interval->done()) { + return; + } + cur->stop(); + } else { + cur->step(dt); + } + + curIdx_++; + if (curIdx_ < acts_.size()) { + acts_[curIdx_]->start(target_); + } + } +} + +bool Seq::done() const { + return curIdx_ >= acts_.size(); +} + +Act* Seq::clone() const { + auto* seq = new Seq(nullptr, nullptr); + seq->acts_.clear(); + for (auto* a : acts_) { + seq->acts_.push_back(a->clone()); + } + seq->dur_ = dur_; + return seq; +} + +Act* Seq::reverse() const { + auto* seq = new Seq(nullptr, nullptr); + seq->acts_.clear(); + for (auto it = acts_.rbegin(); it != acts_.rend(); ++it) { + seq->acts_.push_back((*it)->reverse()); + } + seq->dur_ = dur_; + return seq; +} + +Spawn::Spawn(Act* a1, Act* a2) : ActInterval(0) { + if (a1) acts_.push_back(a1); + if (a2) acts_.push_back(a2); + + f32 maxDur = 0; + for (auto* a : acts_) { + if (auto* interval = dynamic_cast(a)) { + maxDur = std::max(maxDur, interval->dur()); + } + } + dur_ = maxDur; +} + +Spawn* Spawn::createFromArray(std::initializer_list acts) { + auto* spawn = new Spawn(nullptr, nullptr); + spawn->acts_.clear(); + + f32 maxDur = 0; + for (auto* a : acts) { + if (a) { + spawn->acts_.push_back(a); + if (auto* interval = dynamic_cast(a)) { + maxDur = std::max(maxDur, interval->dur()); + } + } + } + spawn->dur_ = maxDur; + return spawn; +} + +void Spawn::start(Node* t) { + ActInterval::start(t); + for (auto* a : acts_) { + a->start(t); + } +} + +void Spawn::stop() { + for (auto* a : acts_) { + a->stop(); + } + ActInterval::stop(); +} + +void Spawn::update(f32 t) { + for (auto* a : acts_) { + if (auto* interval = dynamic_cast(a)) { + f32 localT = std::min(t * dur_ / interval->dur(), 1.0f); + interval->update(interval->easeTime(localT)); + } else { + a->update(t); + } + } +} + +Act* Spawn::clone() const { + auto* spawn = new Spawn(nullptr, nullptr); + spawn->acts_.clear(); + for (auto* a : acts_) { + spawn->acts_.push_back(a->clone()); + } + spawn->dur_ = dur_; + return spawn; +} + +Act* Spawn::reverse() const { + auto* spawn = new Spawn(nullptr, nullptr); + spawn->acts_.clear(); + for (auto* a : acts_) { + spawn->acts_.push_back(a->reverse()); + } + spawn->dur_ = dur_; + return spawn; +} + +Repeat::Repeat(Act* a, int times) : ActInterval(0), times_(times) { + if (a) { + inner_ = a; + if (auto* interval = dynamic_cast(a)) { + dur_ = interval->dur() * times; + } + } +} + +Repeat::Repeat(Act* a) : ActInterval(0), times_(0), inf_(true) { + if (a) { + inner_ = a; + if (auto* interval = dynamic_cast(a)) { + dur_ = interval->dur(); + } + } +} + +bool Repeat::done() const { + return !inf_ && cnt_ >= times_; +} + +void Repeat::start(Node* t) { + ActInterval::start(t); + cnt_ = 0; + if (inner_) { + inner_->start(t); + } +} + +void Repeat::stop() { + if (inner_) { + inner_->stop(); + } + ActInterval::stop(); +} + +void Repeat::update(f32 t) { + if (!inner_) return; + + if (auto* interval = dynamic_cast(inner_)) { + f32 localT = t * times_; + int newCnt = static_cast(localT); + + if (newCnt > cnt_) { + cnt_ = newCnt; + inner_->stop(); + if (!done()) { + inner_->start(target_); + } + } + + f32 innerT = localT - std::floor(localT); + interval->update(interval->easeTime(innerT)); + } +} + +Act* Repeat::clone() const { + return new Repeat(inner_->clone(), times_); +} + +Act* Repeat::reverse() const { + return new Repeat(inner_->reverse(), times_); +} + +void RepeatForever::start(Node* t) { + Act::start(t); + elap_ = 0; + if (inner_) { + inner_->start(t); + } +} + +void RepeatForever::step(f32 dt) { + if (!inner_) return; + + elap_ += dt; + + if (dur_ > 0) { + while (elap_ >= dur_) { + elap_ -= dur_; + inner_->stop(); + inner_->start(target_); + } + + if (auto* interval = dynamic_cast(inner_)) { + f32 t = elap_ / dur_; + interval->update(interval->easeTime(t)); + } + } +} + +void Speed::start(Node* t) { + ActInterval::start(t); + if (inner_) { + inner_->start(t); + } +} + +void Speed::stop() { + if (inner_) { + inner_->stop(); + } + ActInterval::stop(); +} + +void Speed::step(f32 dt) { + if (!inner_) { + elap_ = dur_; + return; + } + + f32 innerDt = dt * speed_; + elap_ += dt; + + if (auto* interval = dynamic_cast(inner_)) { + interval->step(innerDt); + } else { + inner_->step(innerDt); + } +} + +} diff --git a/Extra2D/src/cam/cam_svc.cpp b/Extra2D/src/cam/cam_svc.cpp new file mode 100644 index 0000000..b0a00e0 --- /dev/null +++ b/Extra2D/src/cam/cam_svc.cpp @@ -0,0 +1,5 @@ +#include "extra2d/cam/cam_svc.h" + +namespace extra2d { + +} diff --git a/Extra2D/src/node/node.cpp b/Extra2D/src/node/node.cpp new file mode 100644 index 0000000..744a33d --- /dev/null +++ b/Extra2D/src/node/node.cpp @@ -0,0 +1,197 @@ +#define GLM_ENABLE_EXPERIMENTAL +#include "extra2d/node/node.h" +#include "extra2d/act/act.h" +#include +#include +#include + +namespace extra2d { + +Node::~Node() { + for (auto& c : comps_) { + c->onDetach(); + } + comps_.clear(); + + for (auto& child : children_) { + child->parent_ = nullptr; + } + children_.clear(); + + if (parent_) { + auto& siblings = parent_->children_; + siblings.erase( + std::remove(siblings.begin(), siblings.end(), shared_from_this()), + siblings.end() + ); + } +} + +Mat3 Node::local() const { + Mat3 m(1.0f); + + m = glm::translate(m, pos.toGlm()); + + m = glm::translate(m, glm::vec2(anchor.x * scale.x, anchor.y * scale.y)); + m = glm::rotate(m, rot); + m = glm::translate(m, glm::vec2(-anchor.x * scale.x, -anchor.y * scale.y)); + + m = glm::scale(m, scale.toGlm()); + + return m; +} + +Mat3 Node::world() const { + if (parent_) { + return parent_->world() * local(); + } + return local(); +} + +Mat4 Node::world4x4() const { + Mat3 m3 = world(); + Mat4 m4(1.0f); + m4[0][0] = m3[0][0]; + m4[0][1] = m3[0][1]; + m4[1][0] = m3[1][0]; + m4[1][1] = m3[1][1]; + m4[3][0] = m3[2][0]; + m4[3][1] = m3[2][1]; + return m4; +} + +Vec2 Node::worldPos() const { + if (parent_) { + return parent_->worldPos() + pos; + } + return pos; +} + +f32 Node::worldRot() const { + if (parent_) { + return parent_->worldRot() + rot; + } + return rot; +} + +Vec2 Node::worldScale() const { + if (parent_) { + Vec2 ps = parent_->worldScale(); + return Vec2(ps.x * scale.x, ps.y * scale.y); + } + return scale; +} + +void Node::addInternal(Unique c) { + c->onAttach(this); + comps_.push_back(std::move(c)); +} + +void Node::updateComps(f32 dt) { + for (auto& c : comps_) { + if (c->enabled()) { + c->update(dt); + } + } +} + +void Node::lateUpdateComps(f32 dt) { + for (auto& c : comps_) { + if (c->enabled()) { + c->lateUpdate(dt); + } + } +} + +void Node::addChild(Ref child) { + if (child && child.get() != this) { + if (child->parent_) { + child->removeFromParent(); + } + child->parent_ = this; + children_.push_back(child); + } +} + +void Node::removeChild(Ref child) { + if (child) { + auto it = std::find(children_.begin(), children_.end(), child); + if (it != children_.end()) { + (*it)->parent_ = nullptr; + children_.erase(it); + } + } +} + +void Node::removeFromParent() { + if (parent_) { + auto& siblings = parent_->children_; + auto it = std::find(siblings.begin(), siblings.end(), shared_from_this()); + if (it != siblings.end()) { + siblings.erase(it); + } + parent_ = nullptr; + } +} + +Act* Node::run(Act* a) { + if (!a) return nullptr; + a->start(this); + acts_.push_back(a); + return a; +} + +void Node::stop(Act* a) { + if (!a) return; + auto it = std::find(acts_.begin(), acts_.end(), a); + if (it != acts_.end()) { + (*it)->stop(); + acts_.erase(it); + } +} + +void Node::stopByTag(i32 tag) { + for (auto it = acts_.begin(); it != acts_.end(); ) { + if ((*it)->tag() == tag) { + (*it)->stop(); + it = acts_.erase(it); + } else { + ++it; + } + } +} + +void Node::stopAll() { + for (auto* a : acts_) { + a->stop(); + } + acts_.clear(); +} + +Act* Node::getActByTag(i32 tag) { + for (auto* a : acts_) { + if (a->tag() == tag) { + return a; + } + } + return nullptr; +} + +i32 Node::runningActs() const { + return static_cast(acts_.size()); +} + +void Node::updateActs(f32 dt) { + for (auto it = acts_.begin(); it != acts_.end(); ) { + Act* a = *it; + a->step(dt); + if (a->done()) { + a->stop(); + it = acts_.erase(it); + } else { + ++it; + } + } +} + +} diff --git a/Extra2D/src/node/scene_graph.cpp b/Extra2D/src/node/scene_graph.cpp new file mode 100644 index 0000000..e6a9391 --- /dev/null +++ b/Extra2D/src/node/scene_graph.cpp @@ -0,0 +1,82 @@ +#include "extra2d/node/scene_graph.h" + +namespace extra2d { + +SceneGraph::SceneGraph() = default; + +SceneGraph::~SceneGraph() { + clear(); +} + +void SceneGraph::add(Ref node) { + if (node) { + nodes_.push_back(node); + } +} + +void SceneGraph::remove(Ref node) { + if (node) { + auto it = std::find(nodes_.begin(), nodes_.end(), node); + if (it != nodes_.end()) { + nodes_.erase(it); + } + } +} + +void SceneGraph::clear() { + nodes_.clear(); +} + +Node* SceneGraph::find(const std::string& name) const { + for (const auto& node : nodes_) { + if (node->name() == name) { + return node.get(); + } + for (auto& child : node->children()) { + if (child->name() == name) { + return child.get(); + } + } + } + return nullptr; +} + +Node* SceneGraph::findByTag(i32 tag) const { + for (const auto& node : nodes_) { + if (node->tag() == tag) { + return node.get(); + } + for (auto& child : node->children()) { + if (child->tag() == tag) { + return child.get(); + } + } + } + return nullptr; +} + +void SceneGraph::traverse(const std::function& callback) { + for (auto& node : nodes_) { + traverseRecursive(node.get(), callback); + } +} + +void SceneGraph::traverseRecursive(Node* node, const std::function& callback) { + if (!node) return; + + callback(node); + + for (auto& child : node->children()) { + traverseRecursive(child.get(), callback); + } +} + +void SceneGraph::update(f32 dt) { + traverse([dt](Node* node) { + node->onUpdate(dt); + node->updateComps(dt); + node->lateUpdateComps(dt); + }); +} + +} diff --git a/Extra2D/src/node/shape.cpp b/Extra2D/src/node/shape.cpp new file mode 100644 index 0000000..7a17732 --- /dev/null +++ b/Extra2D/src/node/shape.cpp @@ -0,0 +1,574 @@ +#include "extra2d/node/shape.h" +#include "extra2d/cam/cam.h" +#include + +namespace extra2d { + +void Shape::render(Camera* cam, RenderCmdBuffer& cmdBuffer) { + if (!visible || !enabled()) return; + + switch (shapeType) { + case ShapeType::Rect: + renderRect(cam, cmdBuffer); + break; + case ShapeType::Circle: + renderCircle(cam, cmdBuffer); + break; + case ShapeType::Ellipse: + renderEllipse(cam, cmdBuffer); + break; + case ShapeType::Line: + renderLine(cam, cmdBuffer); + break; + case ShapeType::RoundedRect: + renderRoundedRect(cam, cmdBuffer); + break; + case ShapeType::Poly: + renderPoly(cam, cmdBuffer); + break; + case ShapeType::Arc: + renderArc(cam, cmdBuffer); + break; + case ShapeType::Sector: + renderSector(cam, cmdBuffer); + break; + case ShapeType::Star: + renderStar(cam, cmdBuffer); + break; + case ShapeType::Custom: + renderPoly(cam, cmdBuffer); + break; + } +} + +BoundingBox Shape::bounds() const { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + switch (shapeType) { + case ShapeType::Rect: + case ShapeType::RoundedRect: + return BoundingBox::fromCenter(worldPos, size * 0.5f); + case ShapeType::Circle: + return BoundingBox::fromCenter(worldPos, Vec2(radius, radius)); + case ShapeType::Ellipse: + return BoundingBox::fromCenter(worldPos, size * 0.5f); + case ShapeType::Line: + return BoundingBox::fromSize(start, end - start); + case ShapeType::Star: + return BoundingBox::fromCenter(worldPos, Vec2(radius, radius)); + case ShapeType::Arc: + case ShapeType::Sector: + return BoundingBox::fromCenter(worldPos, Vec2(radius, radius)); + default: + return BoundingBox::fromCenter(worldPos, size * 0.5f); + } +} + +void Shape::renderRect(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + Vec2 offset(-anchor.x * size.x, -anchor.y * size.y); + + Vec2 p0 = worldPos + offset; + Vec2 p1 = p0 + Vec2(size.x, 0); + Vec2 p2 = p0 + size; + Vec2 p3 = p0 + Vec2(0, size.y); + + if (node) { + Mat3 transform = node->world(); + p0 = Vec2::fromGlm(transform * glm::vec3(p0.toGlm(), 1.0f)); + p1 = Vec2::fromGlm(transform * glm::vec3(p1.toGlm(), 1.0f)); + p2 = Vec2::fromGlm(transform * glm::vec3(p2.toGlm(), 1.0f)); + p3 = Vec2::fromGlm(transform * glm::vec3(p3.toGlm(), 1.0f)); + } + + Color finalColor = applyOpacity(fillColor); + + std::vector verts = { + Vertex2D(p0, Vec2(0, 0), finalColor, 0), + Vertex2D(p1, Vec2(1, 0), finalColor, 0), + Vertex2D(p2, Vec2(1, 1), finalColor, 0), + Vertex2D(p3, Vec2(0, 1), finalColor, 0) + }; + + u32 vertOffset = cmdBuffer.addVertices(verts); + std::vector indices = {0, 1, 2, 0, 2, 3}; + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = 4; + cmd.indexOffset = indexOffset; + cmd.indexCount = 6; + cmdBuffer.addCmd(cmd); +} + +void Shape::renderCircle(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + const u32 segments = 32; + auto verts = createCircleVertices(worldPos.x, worldPos.y, radius, segments, applyOpacity(fillColor)); + auto indices = createCircleIndices(0, segments); + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderEllipse(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + const u32 segments = 32; + std::vector verts; + verts.push_back(Vertex2D(worldPos, Vec2(0.5f, 0.5f), applyOpacity(fillColor), 0)); + + for (u32 i = 0; i <= segments; ++i) { + f32 angle = 2.0f * 3.14159f * i / segments; + f32 x = worldPos.x + (size.x * 0.5f) * std::cos(angle); + f32 y = worldPos.y + (size.y * 0.5f) * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), applyOpacity(fillColor), 0)); + } + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + for (u32 i = 1; i <= segments; ++i) { + indices.push_back(0); + indices.push_back(i); + indices.push_back(i + 1 <= segments ? i + 1 : 1); + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderLine(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + Vec2 dir = (end - start).normalized(); + Vec2 perp(-dir.y, dir.x); + Vec2 halfWidth = perp * (lineWidth * 0.5f); + + Vec2 p0 = worldPos + start + halfWidth; + Vec2 p1 = worldPos + start - halfWidth; + Vec2 p2 = worldPos + end - halfWidth; + Vec2 p3 = worldPos + end + halfWidth; + + if (node) { + Mat3 transform = node->world(); + p0 = Vec2::fromGlm(transform * glm::vec3(p0.toGlm(), 1.0f)); + p1 = Vec2::fromGlm(transform * glm::vec3(p1.toGlm(), 1.0f)); + p2 = Vec2::fromGlm(transform * glm::vec3(p2.toGlm(), 1.0f)); + p3 = Vec2::fromGlm(transform * glm::vec3(p3.toGlm(), 1.0f)); + } + + Color finalColor = applyOpacity(strokeColor); + + std::vector verts = { + Vertex2D(p0, Vec2(0, 0), finalColor, 0), + Vertex2D(p1, Vec2(0, 1), finalColor, 0), + Vertex2D(p2, Vec2(1, 1), finalColor, 0), + Vertex2D(p3, Vec2(1, 0), finalColor, 0) + }; + + u32 vertOffset = cmdBuffer.addVertices(verts); + std::vector indices = {0, 1, 2, 0, 2, 3}; + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = 4; + cmd.indexOffset = indexOffset; + cmd.indexCount = 6; + cmdBuffer.addCmd(cmd); +} + +void Shape::renderRoundedRect(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + Vec2 offset(-anchor.x * size.x, -anchor.y * size.y); + + Vec2 basePos = worldPos + offset; + f32 r = std::min(cornerRadius, std::min(size.x, size.y) * 0.5f); + + std::vector verts; + verts.push_back(Vertex2D(worldPos, Vec2(0.5f, 0.5f), applyOpacity(fillColor), 0)); + + const u32 cornerSegments = 8; + + auto addCorner = [&](Vec2 center, f32 startAngle, f32 endAngle) { + for (u32 i = 0; i <= cornerSegments; ++i) { + f32 angle = startAngle + (endAngle - startAngle) * i / cornerSegments; + f32 x = center.x + r * std::cos(angle); + f32 y = center.y + r * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), applyOpacity(fillColor), 0)); + } + }; + + addCorner(Vec2(basePos.x + size.x - r, basePos.y + r), 3.14159f * 1.5f, 3.14159f * 2.0f); + addCorner(Vec2(basePos.x + size.x - r, basePos.y + size.y - r), 0, 3.14159f * 0.5f); + addCorner(Vec2(basePos.x + r, basePos.y + size.y - r), 3.14159f * 0.5f, 3.14159f); + addCorner(Vec2(basePos.x + r, basePos.y + r), 3.14159f, 3.14159f * 1.5f); + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + u32 totalPoints = static_cast(verts.size()) - 1; + for (u32 i = 1; i < totalPoints; ++i) { + indices.push_back(0); + indices.push_back(i); + indices.push_back(i + 1); + } + indices.push_back(0); + indices.push_back(totalPoints); + indices.push_back(1); + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderPoly(Camera* cam, RenderCmdBuffer& cmdBuffer) { + if (points.empty()) return; + + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + std::vector verts; + Vec2 center(0, 0); + for (const auto& p : points) { + center += p; + } + center /= static_cast(points.size()); + verts.push_back(Vertex2D(worldPos + center, Vec2(0.5f, 0.5f), applyOpacity(fillColor), 0)); + + for (const auto& p : points) { + verts.push_back(Vertex2D(worldPos + p, Vec2(0, 0), applyOpacity(fillColor), 0)); + } + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + u32 n = static_cast(points.size()); + for (u32 i = 0; i < n; ++i) { + indices.push_back(0); + indices.push_back(i + 1); + indices.push_back((i + 1) % n + 1); + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderArc(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + const u32 segments = 32; + std::vector verts; + + Vec2 innerOffset(-anchor.x * radius * 2, -anchor.y * radius * 2); + Vec2 center = worldPos + Vec2(radius, radius) + innerOffset; + + for (u32 i = 0; i <= segments; ++i) { + f32 angle = startAngle + sweepAngle * i / segments; + f32 x = center.x + radius * std::cos(angle); + f32 y = center.y + radius * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), applyOpacity(strokeColor), 0)); + } + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + for (u32 i = 0; i < segments; ++i) { + indices.push_back(i); + indices.push_back(i + 1); + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderSector(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + const u32 segments = 32; + std::vector verts; + + Vec2 innerOffset(-anchor.x * radius * 2, -anchor.y * radius * 2); + Vec2 center = worldPos + Vec2(radius, radius) + innerOffset; + + verts.push_back(Vertex2D(center, Vec2(0.5f, 0.5f), applyOpacity(fillColor), 0)); + + for (u32 i = 0; i <= segments; ++i) { + f32 angle = startAngle + sweepAngle * i / segments; + f32 x = center.x + radius * std::cos(angle); + f32 y = center.y + radius * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), applyOpacity(fillColor), 0)); + } + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + for (u32 i = 1; i <= segments; ++i) { + indices.push_back(0); + indices.push_back(i); + indices.push_back(i + 1 <= segments ? i + 1 : 1); + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +void Shape::renderStar(Camera* cam, RenderCmdBuffer& cmdBuffer) { + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + std::vector verts; + verts.push_back(Vertex2D(worldPos, Vec2(0.5f, 0.5f), applyOpacity(fillColor), 0)); + + f32 inner = radius * innerRadius; + for (u32 i = 0; i < sides * 2; ++i) { + f32 angle = 3.14159f * i / sides - 3.14159f * 0.5f; + f32 r = (i % 2 == 0) ? radius : inner; + f32 x = worldPos.x + r * std::cos(angle); + f32 y = worldPos.y + r * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), applyOpacity(fillColor), 0)); + } + + if (node) { + Mat3 transform = node->world(); + for (auto& v : verts) { + Vec2 transformed = Vec2::fromGlm(transform * glm::vec3(v.pos.toGlm(), 1.0f)); + v.pos = transformed; + } + } + + std::vector indices; + u32 n = sides * 2; + for (u32 i = 0; i < n; ++i) { + indices.push_back(0); + indices.push_back(i + 1); + indices.push_back((i + 1) % n + 1); + } + + u32 vertOffset = cmdBuffer.addVertices(verts); + for (auto& i : indices) i += vertOffset; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = static_cast(verts.size()); + cmd.indexOffset = indexOffset; + cmd.indexCount = static_cast(indices.size()); + cmdBuffer.addCmd(cmd); +} + +std::vector Shape::createCircleVertices(f32 cx, f32 cy, f32 r, u32 segments, const Color& c) { + std::vector verts; + verts.push_back(Vertex2D(Vec2(cx, cy), Vec2(0.5f, 0.5f), c, 0)); + + for (u32 i = 0; i <= segments; ++i) { + f32 angle = 2.0f * 3.14159f * i / segments; + f32 x = cx + r * std::cos(angle); + f32 y = cy + r * std::sin(angle); + verts.push_back(Vertex2D(Vec2(x, y), Vec2(0, 0), c, 0)); + } + + return verts; +} + +std::vector Shape::createCircleIndices(u32 baseIndex, u32 segments) { + std::vector indices; + for (u32 i = 1; i <= segments; ++i) { + indices.push_back(baseIndex); + indices.push_back(baseIndex + i); + indices.push_back(baseIndex + (i + 1 <= segments ? i + 1 : 1)); + } + return indices; +} + +Shape* Shape::rect(const Vec2& sz, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Rect; + s->size = sz; + s->fillColor = c; + return s; +} + +Shape* Shape::circle(f32 r, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Circle; + s->radius = r; + s->fillColor = c; + return s; +} + +Shape* Shape::ellipse(const Vec2& sz, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Ellipse; + s->size = sz; + s->fillColor = c; + return s; +} + +Shape* Shape::line(const Vec2& s, const Vec2& e, f32 w, const Color& c) { + auto* shape = new Shape(); + shape->shapeType = ShapeType::Line; + shape->start = s; + shape->end = e; + shape->lineWidth = w; + shape->strokeColor = c; + shape->fill = FillMode::Stroke; + return shape; +} + +Shape* Shape::roundRect(const Vec2& sz, f32 r, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::RoundedRect; + s->size = sz; + s->cornerRadius = r; + s->fillColor = c; + return s; +} + +Shape* Shape::poly(const std::vector& pts, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Poly; + s->points = pts; + s->fillColor = c; + return s; +} + +Shape* Shape::star(f32 outerR, f32 innerR, u32 n, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Star; + s->radius = outerR; + s->innerRadius = innerR / outerR; + s->sides = n; + s->fillColor = c; + return s; +} + +Shape* Shape::arc(f32 r, f32 start, f32 sweep, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Arc; + s->radius = r; + s->startAngle = start; + s->sweepAngle = sweep; + s->strokeColor = c; + s->fill = FillMode::Stroke; + return s; +} + +Shape* Shape::sector(f32 r, f32 start, f32 sweep, const Color& c) { + auto* s = new Shape(); + s->shapeType = ShapeType::Sector; + s->radius = r; + s->startAngle = start; + s->sweepAngle = sweep; + s->fillColor = c; + return s; +} + +} diff --git a/Extra2D/src/node/sprite.cpp b/Extra2D/src/node/sprite.cpp new file mode 100644 index 0000000..6adbced --- /dev/null +++ b/Extra2D/src/node/sprite.cpp @@ -0,0 +1,87 @@ +#include "extra2d/node/sprite.h" +#include "extra2d/cam/cam.h" + +namespace extra2d { + +void Sprite::render(Camera* cam, RenderCmdBuffer& cmdBuffer) { + if (!visible || !enabled()) return; + + Node* node = owner(); + if (!node) return; + + Vec2 finalSize = size; + if (node) { + finalSize.x *= node->scale.x; + finalSize.y *= node->scale.y; + } + + Vec2 offset(-anchor.x * finalSize.x, -anchor.y * finalSize.y); + + Vec2 p0 = Vec2(offset.x, offset.y); + Vec2 p1 = Vec2(offset.x + finalSize.x, offset.y); + Vec2 p2 = Vec2(offset.x + finalSize.x, offset.y + finalSize.y); + Vec2 p3 = Vec2(offset.x, offset.y + finalSize.y); + + if (node) { + Mat3 transform = node->world(); + p0 = Vec2::fromGlm(transform * glm::vec3(p0.toGlm(), 1.0f)); + p1 = Vec2::fromGlm(transform * glm::vec3(p1.toGlm(), 1.0f)); + p2 = Vec2::fromGlm(transform * glm::vec3(p2.toGlm(), 1.0f)); + p3 = Vec2::fromGlm(transform * glm::vec3(p3.toGlm(), 1.0f)); + } + + Vec2 finalUv0 = flipX ? Vec2(uv1.x, uv0.y) : uv0; + Vec2 finalUv1 = flipX ? Vec2(uv0.x, uv1.y) : uv1; + if (flipY) { + std::swap(finalUv0.y, finalUv1.y); + } + + Color finalColor = applyOpacity(color); + + std::vector verts = { + Vertex2D(p0, Vec2(finalUv0.x, finalUv0.y), finalColor, 0), + Vertex2D(p1, Vec2(finalUv1.x, finalUv0.y), finalColor, 0), + Vertex2D(p2, Vec2(finalUv1.x, finalUv1.y), finalColor, 0), + Vertex2D(p3, Vec2(finalUv0.x, finalUv1.y), finalColor, 0) + }; + + u32 vertOffset = cmdBuffer.addVertices(verts); + u32 baseIndex = vertOffset; + + std::vector indices = { + baseIndex, baseIndex + 1, baseIndex + 2, + baseIndex, baseIndex + 2, baseIndex + 3 + }; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = 4; + cmd.indexOffset = indexOffset; + cmd.indexCount = 6; + cmdBuffer.addCmd(cmd); +} + +BoundingBox Sprite::bounds() const { + Node* node = owner(); + Vec2 finalSize = size; + if (node) { + finalSize.x *= node->scale.x; + finalSize.y *= node->scale.y; + } + + Vec2 offset(-anchor.x * finalSize.x, -anchor.y * finalSize.y); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + + return BoundingBox::fromSize(worldPos + offset, finalSize); +} + +Sprite* Sprite::create(const Vec2& sz, const Color& c) { + auto* sprite = new Sprite(); + sprite->size = sz; + sprite->color = c; + return sprite; +} + +} diff --git a/Extra2D/src/node/text.cpp b/Extra2D/src/node/text.cpp new file mode 100644 index 0000000..f034bd9 --- /dev/null +++ b/Extra2D/src/node/text.cpp @@ -0,0 +1,129 @@ +#include "extra2d/node/text.h" +#include "extra2d/cam/cam.h" + +namespace extra2d { + +void Text::render(Camera* cam, RenderCmdBuffer& cmdBuffer) { + if (!visible || !enabled() || str.empty()) return; + + Node* node = owner(); + if (!node) return; + + Vec2 textSize = measureText(); + Vec2 offset(-anchor.x * textSize.x, -anchor.y * textSize.y); + + Vec2 worldPos = node->worldPos(); + Vec2 currentPos = worldPos + offset; + + Color finalColor = applyOpacity(color); + + f32 x = currentPos.x; + f32 y = currentPos.y; + + for (char c : str) { + if (c == '\n') { + x = currentPos.x; + y += fontSize * lineHeight; + continue; + } + + if (c == ' ') { + x += fontSize * 0.3f + letterSpacing; + continue; + } + + f32 charWidth = fontSize * 0.6f; + f32 charHeight = fontSize; + + Vec2 p0(x, y); + Vec2 p1(x + charWidth, y); + Vec2 p2(x + charWidth, y + charHeight); + Vec2 p3(x, y + charHeight); + + if (node) { + Mat3 transform = node->world(); + p0 = Vec2::fromGlm(transform * glm::vec3(p0.toGlm(), 1.0f)); + p1 = Vec2::fromGlm(transform * glm::vec3(p1.toGlm(), 1.0f)); + p2 = Vec2::fromGlm(transform * glm::vec3(p2.toGlm(), 1.0f)); + p3 = Vec2::fromGlm(transform * glm::vec3(p3.toGlm(), 1.0f)); + } + + std::vector verts = { + Vertex2D(p0, Vec2(0, 0), finalColor, 0), + Vertex2D(p1, Vec2(1, 0), finalColor, 0), + Vertex2D(p2, Vec2(1, 1), finalColor, 0), + Vertex2D(p3, Vec2(0, 1), finalColor, 0) + }; + + u32 vertOffset = cmdBuffer.addVertices(verts); + u32 baseIndex = vertOffset; + + std::vector indices = { + baseIndex, baseIndex + 1, baseIndex + 2, + baseIndex, baseIndex + 2, baseIndex + 3 + }; + u32 indexOffset = cmdBuffer.addIndices(indices); + + RenderCmd cmd; + cmd.type = RenderCmd::Type::DrawIndexed; + cmd.vertexOffset = vertOffset; + cmd.vertexCount = 4; + cmd.indexOffset = indexOffset; + cmd.indexCount = 6; + cmdBuffer.addCmd(cmd); + + x += charWidth + letterSpacing; + } +} + +BoundingBox Text::bounds() const { + Vec2 textSize = measureText(); + Node* node = owner(); + Vec2 worldPos = node ? node->worldPos() : Vec2(0, 0); + Vec2 offset(-anchor.x * textSize.x, -anchor.y * textSize.y); + + return BoundingBox::fromSize(worldPos + offset, textSize); +} + +Vec2 Text::measureText() const { + return measureText(str); +} + +Vec2 Text::measureText(const std::string& text) const { + if (text.empty()) return Vec2(0, 0); + + f32 maxWidth = 0.0f; + f32 currentWidth = 0.0f; + f32 height = fontSize; + i32 lineCount = 1; + + for (char c : text) { + if (c == '\n') { + maxWidth = std::max(maxWidth, currentWidth); + currentWidth = 0.0f; + lineCount++; + continue; + } + + if (c == ' ') { + currentWidth += fontSize * 0.3f + letterSpacing; + } else { + currentWidth += fontSize * 0.6f + letterSpacing; + } + } + + maxWidth = std::max(maxWidth, currentWidth); + height = fontSize + (lineCount - 1) * fontSize * lineHeight; + + return Vec2(maxWidth, height); +} + +Text* Text::create(const std::string& text, f32 size, const Color& c) { + auto* txt = new Text(); + txt->str = text; + txt->fontSize = size; + txt->color = c; + return txt; +} + +} diff --git a/Extra2D/src/render/backend/vulkan/vk_backend.cpp b/Extra2D/src/render/backend/vulkan/vk_backend.cpp new file mode 100644 index 0000000..e389d6d --- /dev/null +++ b/Extra2D/src/render/backend/vulkan/vk_backend.cpp @@ -0,0 +1,253 @@ +#include "extra2d/render/backend/vulkan/vk_backend.h" +#include + +namespace extra2d { + +bool VkBackend::init(VkInstance instance, VkPhysicalDevice physDevice, VkDevice device) { + instance_ = instance; + physDevice_ = physDevice; + device_ = device; + + if (!memAlloc_.init(instance, device, physDevice)) { + return false; + } + + VkCommandPoolCreateInfo poolInfo = {}; + poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + poolInfo.queueFamilyIndex = graphicsFamily_; + + if (vkCreateCommandPool(device_, &poolInfo, nullptr, &cmdPool_) != VK_SUCCESS) { + return false; + } + + return true; +} + +void VkBackend::shutdown() { + destroySwapchain(); + + if (cmdPool_ != VK_NULL_HANDLE) { + vkDestroyCommandPool(device_, cmdPool_, nullptr); + cmdPool_ = VK_NULL_HANDLE; + } + + memAlloc_.shutdown(); + + device_ = VK_NULL_HANDLE; + physDevice_ = VK_NULL_HANDLE; + instance_ = VK_NULL_HANDLE; +} + +bool VkBackend::createSwapchain(VkSurfaceKHR surface, u32 width, u32 height) { + surface_ = surface; + + SwapchainSupport support = querySwapchainSupport(physDevice_); + + VkSurfaceFormatKHR format = chooseSurfaceFormat(support.formats); + VkPresentModeKHR presentMode = choosePresentMode(support.presentModes); + VkExtent2D extent = chooseExtent(support.capabilities, width, height); + + u32 imageCount = support.capabilities.minImageCount + 1; + if (support.capabilities.maxImageCount > 0 && imageCount > support.capabilities.maxImageCount) { + imageCount = support.capabilities.maxImageCount; + } + + VkSwapchainCreateInfoKHR createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; + createInfo.surface = surface_; + createInfo.minImageCount = imageCount; + createInfo.imageFormat = format.format; + createInfo.imageColorSpace = format.colorSpace; + createInfo.imageExtent = extent; + createInfo.imageArrayLayers = 1; + createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + createInfo.preTransform = support.capabilities.currentTransform; + createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + createInfo.presentMode = presentMode; + createInfo.clipped = VK_TRUE; + createInfo.oldSwapchain = VK_NULL_HANDLE; + + if (vkCreateSwapchainKHR(device_, &createInfo, nullptr, &swapchain_) != VK_SUCCESS) { + return false; + } + + vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, nullptr); + swapchainImages_.resize(imageCount); + vkGetSwapchainImagesKHR(device_, swapchain_, &imageCount, swapchainImages_.data()); + + swapchainFormat_ = format.format; + swapchainExtent_ = extent; + + swapchainViews_.resize(swapchainImages_.size()); + for (size_t i = 0; i < swapchainImages_.size(); ++i) { + VkImageViewCreateInfo viewInfo = {}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = swapchainImages_[i]; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = swapchainFormat_; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + if (vkCreateImageView(device_, &viewInfo, nullptr, &swapchainViews_[i]) != VK_SUCCESS) { + return false; + } + } + + cmdBuffers_.resize(swapchainImages_.size()); + VkCommandBufferAllocateInfo allocInfo = {}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.commandPool = cmdPool_; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandBufferCount = static_cast(cmdBuffers_.size()); + + if (vkAllocateCommandBuffers(device_, &allocInfo, cmdBuffers_.data()) != VK_SUCCESS) { + return false; + } + + return true; +} + +void VkBackend::destroySwapchain() { + for (auto& view : swapchainViews_) { + if (view != VK_NULL_HANDLE) { + vkDestroyImageView(device_, view, nullptr); + } + } + swapchainViews_.clear(); + swapchainImages_.clear(); + + if (swapchain_ != VK_NULL_HANDLE) { + vkDestroySwapchainKHR(device_, swapchain_, nullptr); + swapchain_ = VK_NULL_HANDLE; + } + + if (!cmdBuffers_.empty()) { + vkFreeCommandBuffers(device_, cmdPool_, static_cast(cmdBuffers_.size()), cmdBuffers_.data()); + cmdBuffers_.clear(); + } +} + +bool VkBackend::resizeSwapchain(u32 width, u32 height) { + vkDeviceWaitIdle(device_); + destroySwapchain(); + return createSwapchain(surface_, width, height); +} + +u32 VkBackend::acquireNextImage(VkSemaphore semaphore, VkFence fence) { + u32 imageIndex; + VkResult result = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, semaphore, fence, &imageIndex); + + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + return UINT32_MAX; + } + + return imageIndex; +} + +void VkBackend::present(u32 imageIndex, VkSemaphore waitSemaphore) { + VkPresentInfoKHR presentInfo = {}; + presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + presentInfo.waitSemaphoreCount = 1; + presentInfo.pWaitSemaphores = &waitSemaphore; + presentInfo.swapchainCount = 1; + presentInfo.pSwapchains = &swapchain_; + presentInfo.pImageIndices = &imageIndex; + + vkQueuePresentKHR(VK_NULL_HANDLE, &presentInfo); +} + +VkCommandBuffer VkBackend::beginCmd() { + VkCommandBufferBeginInfo beginInfo = {}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + VkCommandBuffer cmd = cmdBuffers_[frameIdx_]; + vkBeginCommandBuffer(cmd, &beginInfo); + return cmd; +} + +void VkBackend::endCmd(VkCommandBuffer cmd) { + vkEndCommandBuffer(cmd); +} + +void VkBackend::submitCmd(VkCommandBuffer cmd, VkSemaphore wait, VkSemaphore signal, VkFence fence) { + VkSubmitInfo submitInfo = {}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &cmd; + + if (wait != VK_NULL_HANDLE) { + VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + submitInfo.waitSemaphoreCount = 1; + submitInfo.pWaitSemaphores = &wait; + submitInfo.pWaitDstStageMask = &waitStage; + } + + if (signal != VK_NULL_HANDLE) { + submitInfo.signalSemaphoreCount = 1; + submitInfo.pSignalSemaphores = &signal; + } + + vkQueueSubmit(VK_NULL_HANDLE, 1, &submitInfo, fence); +} + +void VkBackend::waitIdle() { + vkDeviceWaitIdle(device_); +} + +SwapchainSupport VkBackend::querySwapchainSupport(VkPhysicalDevice device) { + SwapchainSupport support; + + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface_, &support.capabilities); + + u32 formatCount; + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface_, &formatCount, nullptr); + if (formatCount != 0) { + support.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface_, &formatCount, support.formats.data()); + } + + u32 presentModeCount; + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface_, &presentModeCount, nullptr); + if (presentModeCount != 0) { + support.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface_, &presentModeCount, support.presentModes.data()); + } + + return support; +} + +VkSurfaceFormatKHR VkBackend::chooseSurfaceFormat(const std::vector& formats) { + for (const auto& format : formats) { + if (format.format == VK_FORMAT_B8G8R8A8_SRGB && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return format; + } + } + return formats[0]; +} + +VkPresentModeKHR VkBackend::choosePresentMode(const std::vector& modes) { + for (const auto& mode : modes) { + if (mode == VK_PRESENT_MODE_MAILBOX_KHR) { + return mode; + } + } + return VK_PRESENT_MODE_FIFO_KHR; +} + +VkExtent2D VkBackend::chooseExtent(const VkSurfaceCapabilitiesKHR& caps, u32 width, u32 height) { + if (caps.currentExtent.width != UINT32_MAX) { + return caps.currentExtent; + } + + VkExtent2D extent = {width, height}; + extent.width = std::clamp(extent.width, caps.minImageExtent.width, caps.maxImageExtent.width); + extent.height = std::clamp(extent.height, caps.minImageExtent.height, caps.maxImageExtent.height); + return extent; +} + +} diff --git a/Extra2D/src/render/backend/vulkan/vk_mem.cpp b/Extra2D/src/render/backend/vulkan/vk_mem.cpp new file mode 100644 index 0000000..8bba9cd --- /dev/null +++ b/Extra2D/src/render/backend/vulkan/vk_mem.cpp @@ -0,0 +1,240 @@ +#include "extra2d/render/backend/vulkan/vk_mem.h" + +namespace extra2d { + +bool VkMemAlloc::init(VkInstance inst, VkDevice device, VkPhysicalDevice physDevice) { + VmaAllocatorCreateInfo createInfo = {}; + createInfo.instance = inst; + createInfo.device = device; + createInfo.physicalDevice = physDevice; + createInfo.vulkanApiVersion = VK_API_VERSION_1_2; + + VkResult result = vmaCreateAllocator(&createInfo, &allocator_); + return result == VK_SUCCESS; +} + +void VkMemAlloc::shutdown() { + if (allocator_ != VK_NULL_HANDLE) { + vmaDestroyAllocator(allocator_); + allocator_ = VK_NULL_HANDLE; + } +} + +VkBuffer VkMemAlloc::createBuffer( + VkDeviceSize size, + VkBufferUsageFlags usage, + MemType type, + VmaAllocation* outAlloc +) { + VkBufferCreateInfo bufferInfo = {}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + + VmaAllocationCreateInfo allocInfo = {}; + switch (type) { + case MemType::Gpu: + allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + break; + case MemType::Upload: + allocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + break; + case MemType::Readback: + allocInfo.usage = VMA_MEMORY_USAGE_GPU_TO_CPU; + allocInfo.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + break; + } + + VkBuffer buffer; + VkResult result = vmaCreateBuffer(allocator_, &bufferInfo, &allocInfo, &buffer, outAlloc, nullptr); + + return result == VK_SUCCESS ? buffer : VK_NULL_HANDLE; +} + +VkImage VkMemAlloc::createImage( + const VkImageCreateInfo& info, + MemType type, + VmaAllocation* outAlloc +) { + VmaAllocationCreateInfo allocInfo = {}; + allocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + VkImage image; + VkResult result = vmaCreateImage(allocator_, &info, &allocInfo, &image, outAlloc, nullptr); + + return result == VK_SUCCESS ? image : VK_NULL_HANDLE; +} + +void VkMemAlloc::destroyBuffer(VkBuffer buf, VmaAllocation alloc) { + if (allocator_ != VK_NULL_HANDLE && buf != VK_NULL_HANDLE) { + vmaDestroyBuffer(allocator_, buf, alloc); + } +} + +void VkMemAlloc::destroyImage(VkImage img, VmaAllocation alloc) { + if (allocator_ != VK_NULL_HANDLE && img != VK_NULL_HANDLE) { + vmaDestroyImage(allocator_, img, alloc); + } +} + +void* VkMemAlloc::map(VmaAllocation alloc) { + void* data = nullptr; + vmaMapMemory(allocator_, alloc, &data); + return data; +} + +void VkMemAlloc::unmap(VmaAllocation alloc) { + vmaUnmapMemory(allocator_, alloc); +} + +void VkMemAlloc::flush(VmaAllocation alloc, VkDeviceSize off, VkDeviceSize size) { + vmaFlushAllocation(allocator_, alloc, off, size); +} + +void VkMemAlloc::invalidate(VmaAllocation alloc, VkDeviceSize off, VkDeviceSize size) { + vmaInvalidateAllocation(allocator_, alloc, off, size); +} + +void VkMemAlloc::getStats(VmaTotalStatistics* stats) { + vmaCalculateStatistics(allocator_, stats); +} + +VkBufferWrap::VkBufferWrap(VkMemAlloc* alloc, VkDeviceSize size, VkBufferUsageFlags usage, MemType type) + : memAlloc_(alloc), size_(size), type_(type) { + if (memAlloc_) { + buffer_ = memAlloc_->createBuffer(size, usage, type, &vmaAlloc_); + } +} + +VkBufferWrap::~VkBufferWrap() { + release(); +} + +VkBufferWrap::VkBufferWrap(VkBufferWrap&& other) noexcept + : memAlloc_(other.memAlloc_) + , buffer_(other.buffer_) + , vmaAlloc_(other.vmaAlloc_) + , size_(other.size_) + , type_(other.type_) + , mapped_(other.mapped_) { + other.memAlloc_ = nullptr; + other.buffer_ = VK_NULL_HANDLE; + other.vmaAlloc_ = VK_NULL_HANDLE; + other.size_ = 0; + other.mapped_ = nullptr; +} + +VkBufferWrap& VkBufferWrap::operator=(VkBufferWrap&& other) noexcept { + if (this != &other) { + release(); + memAlloc_ = other.memAlloc_; + buffer_ = other.buffer_; + vmaAlloc_ = other.vmaAlloc_; + size_ = other.size_; + type_ = other.type_; + mapped_ = other.mapped_; + other.memAlloc_ = nullptr; + other.buffer_ = VK_NULL_HANDLE; + other.vmaAlloc_ = VK_NULL_HANDLE; + other.size_ = 0; + other.mapped_ = nullptr; + } + return *this; +} + +void* VkBufferWrap::map() { + if (memAlloc_ && vmaAlloc_ != VK_NULL_HANDLE) { + mapped_ = memAlloc_->map(vmaAlloc_); + return mapped_; + } + return nullptr; +} + +void VkBufferWrap::unmap() { + if (memAlloc_ && vmaAlloc_ != VK_NULL_HANDLE && mapped_) { + memAlloc_->unmap(vmaAlloc_); + mapped_ = nullptr; + } +} + +void VkBufferWrap::write(const void* data, VkDeviceSize size, VkDeviceSize off) { + if (type_ == MemType::Upload || type_ == MemType::Readback) { + void* mapped = map(); + if (mapped) { + memcpy(static_cast(mapped) + off, data, size); + unmap(); + if (type_ == MemType::Upload) { + memAlloc_->flush(vmaAlloc_, off, size); + } + } + } +} + +void VkBufferWrap::read(void* data, VkDeviceSize size, VkDeviceSize off) { + if (type_ == MemType::Readback) { + memAlloc_->invalidate(vmaAlloc_, off, size); + void* mapped = map(); + if (mapped) { + memcpy(data, static_cast(mapped) + off, size); + unmap(); + } + } +} + +void VkBufferWrap::release() { + if (memAlloc_ && buffer_ != VK_NULL_HANDLE) { + memAlloc_->destroyBuffer(buffer_, vmaAlloc_); + buffer_ = VK_NULL_HANDLE; + vmaAlloc_ = VK_NULL_HANDLE; + size_ = 0; + mapped_ = nullptr; + } +} + +VkImageWrap::VkImageWrap(VkMemAlloc* alloc, const VkImageCreateInfo& info, MemType type) + : memAlloc_(alloc), extent_(info.extent), format_(info.format) { + if (memAlloc_) { + image_ = memAlloc_->createImage(info, type, &vmaAlloc_); + } +} + +VkImageWrap::~VkImageWrap() { + release(); +} + +VkImageWrap::VkImageWrap(VkImageWrap&& other) noexcept + : memAlloc_(other.memAlloc_) + , image_(other.image_) + , vmaAlloc_(other.vmaAlloc_) + , extent_(other.extent_) + , format_(other.format_) { + other.memAlloc_ = nullptr; + other.image_ = VK_NULL_HANDLE; + other.vmaAlloc_ = VK_NULL_HANDLE; +} + +VkImageWrap& VkImageWrap::operator=(VkImageWrap&& other) noexcept { + if (this != &other) { + release(); + memAlloc_ = other.memAlloc_; + image_ = other.image_; + vmaAlloc_ = other.vmaAlloc_; + extent_ = other.extent_; + format_ = other.format_; + other.memAlloc_ = nullptr; + other.image_ = VK_NULL_HANDLE; + other.vmaAlloc_ = VK_NULL_HANDLE; + } + return *this; +} + +void VkImageWrap::release() { + if (memAlloc_ && image_ != VK_NULL_HANDLE) { + memAlloc_->destroyImage(image_, vmaAlloc_); + image_ = VK_NULL_HANDLE; + vmaAlloc_ = VK_NULL_HANDLE; + } +} + +} diff --git a/Extra2D/src/render/render_mod.cpp b/Extra2D/src/render/render_mod.cpp new file mode 100644 index 0000000..147fade --- /dev/null +++ b/Extra2D/src/render/render_mod.cpp @@ -0,0 +1,5 @@ +#include "extra2d/render/render_mod.h" + +namespace extra2d { + +} diff --git a/Extra2D/src/render/shader_mgr.cpp b/Extra2D/src/render/shader_mgr.cpp new file mode 100644 index 0000000..023c405 --- /dev/null +++ b/Extra2D/src/render/shader_mgr.cpp @@ -0,0 +1,93 @@ +#include "extra2d/render/shader_mgr.h" +#include + +namespace extra2d { + +bool ShaderMgr::load(const std::string& name, const std::string& path, ShaderType type) { + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) { + return false; + } + + std::string src((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + ShaderDef def; + def.name = name; + def.type = type; + def.src = src; + def.path = path; + + shaders_[name] = def; + + if (std::filesystem::exists(path)) { + fileTimes_[name] = std::filesystem::last_write_time(path); + } + + return true; +} + +bool ShaderMgr::loadFromSrc(const std::string& name, const std::string& src, ShaderType type) { + ShaderDef def; + def.name = name; + def.type = type; + def.src = src; + + shaders_[name] = def; + return true; +} + +bool ShaderMgr::loadFromSpirv(const std::string& name, const std::vector& spirv, ShaderType type) { + ShaderDef def; + def.name = name; + def.type = type; + def.spirv = spirv; + + shaders_[name] = def; + return true; +} + +bool ShaderMgr::reload(const std::string& name) { + auto it = shaders_.find(name); + if (it == shaders_.end()) { + return false; + } + + if (!it->second.path.empty()) { + if (load(name, it->second.path, it->second.type)) { + if (reloadCb_) { + reloadCb_(name); + } + return true; + } + } + return false; +} + +void ShaderMgr::reloadAll() { + for (auto& [name, def] : shaders_) { + if (!def.path.empty()) { + load(name, def.path, def.type); + } + } + if (reloadCb_) { + reloadCb_("all"); + } +} + +void ShaderMgr::checkFileChanges() { + if (!autoReload_) return; + + for (auto& [name, def] : shaders_) { + if (!def.path.empty() && std::filesystem::exists(def.path)) { + auto currentTime = std::filesystem::last_write_time(def.path); + auto storedTime = fileTimes_[name]; + if (currentTime != storedTime) { + fileTimes_[name] = currentTime; + reload(name); + } + } + } +} + +} diff --git a/Extra2D/src/render/shader_sdf.cpp b/Extra2D/src/render/shader_sdf.cpp new file mode 100644 index 0000000..3b21f6d --- /dev/null +++ b/Extra2D/src/render/shader_sdf.cpp @@ -0,0 +1,190 @@ +#include "extra2d/render/shader_sdf.h" + +namespace extra2d { +namespace sdf { + +const char* kCircleSdf = R"( +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +layout(push_constant) uniform PushConstants { + vec4 color; + vec2 size; + float edgeSoftness; + float outlineWidth; + vec4 outlineColor; +} pc; + +float circleSDF(vec2 p, float r) { + return length(p) - r; +} + +void main() { + vec2 p = vUV * 2.0 - 1.0; + float d = circleSDF(p, 0.5); + + float alpha = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d); + + if (pc.outlineWidth > 0.0) { + float outline = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d + pc.outlineWidth); + fragColor = mix(pc.outlineColor, pc.color, alpha); + fragColor.a = outline; + } else { + fragColor = pc.color; + fragColor.a *= alpha; + } +} +)"; + +const char* kRectSdf = R"( +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +layout(push_constant) uniform PushConstants { + vec4 color; + vec2 size; + float edgeSoftness; + float outlineWidth; + vec4 outlineColor; +} pc; + +float rectSDF(vec2 p, vec2 size) { + vec2 d = abs(p) - size * 0.5; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} + +void main() { + vec2 p = vUV * 2.0 - 1.0; + float d = rectSDF(p, pc.size); + + float alpha = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d); + + if (pc.outlineWidth > 0.0) { + float outline = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d + pc.outlineWidth); + fragColor = mix(pc.outlineColor, pc.color, alpha); + fragColor.a = outline; + } else { + fragColor = pc.color; + fragColor.a *= alpha; + } +} +)"; + +const char* kRoundedRectSdf = R"( +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +layout(push_constant) uniform PushConstants { + vec4 color; + vec2 size; + float cornerRadius; + float edgeSoftness; + float outlineWidth; + vec4 outlineColor; +} pc; + +float roundedRectSDF(vec2 p, vec2 size, float r) { + vec2 d = abs(p) - size * 0.5 + r; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - r; +} + +void main() { + vec2 p = vUV * 2.0 - 1.0; + float d = roundedRectSDF(p, pc.size, pc.cornerRadius); + + float alpha = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d); + + if (pc.outlineWidth > 0.0) { + float outline = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d + pc.outlineWidth); + fragColor = mix(pc.outlineColor, pc.color, alpha); + fragColor.a = outline; + } else { + fragColor = pc.color; + fragColor.a *= alpha; + } +} +)"; + +std::string genFragSrc(const Mat& mat) { + std::string src = R"( +#version 450 + +layout(location = 0) in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +layout(push_constant) uniform PushConstants { + vec4 color; + vec2 size; + float radius; + float edgeSoftness; + float outlineWidth; + vec4 outlineColor; +} pc; +)"; + + switch (mat.shape) { + case Shape::Circle: + src += R"( +float sdf(vec2 p) { + return length(p) - pc.radius; +} +)"; + break; + case Shape::Rect: + src += R"( +float sdf(vec2 p) { + vec2 d = abs(p) - pc.size * 0.5; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); +} +)"; + break; + case Shape::RoundedRect: + src += R"( +float sdf(vec2 p) { + vec2 d = abs(p) - pc.size * 0.5 + pc.radius; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - pc.radius; +} +)"; + break; + default: + if (!mat.customSdfFunc.empty()) { + src += mat.customSdfFunc; + } else { + src += R"( +float sdf(vec2 p) { + return length(p) - pc.radius; +} +)"; + } + break; + } + + src += R"( +void main() { + vec2 p = vUV * 2.0 - 1.0; + float d = sdf(p); + + float alpha = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d); + + if (pc.outlineWidth > 0.0) { + float outline = 1.0 - smoothstep(-pc.edgeSoftness, pc.edgeSoftness, d + pc.outlineWidth); + fragColor = mix(pc.outlineColor, pc.color, alpha); + fragColor.a = outline; + } else { + fragColor = pc.color; + fragColor.a *= alpha; + } +} +)"; + + return src; +} + +} +} diff --git a/Extra2D/src/scene/director.cpp b/Extra2D/src/scene/director.cpp new file mode 100644 index 0000000..35891c3 --- /dev/null +++ b/Extra2D/src/scene/director.cpp @@ -0,0 +1,5 @@ +#include "extra2d/scene/director.h" + +namespace extra2d { + +} diff --git a/Extra2D/src/scene/scene.cpp b/Extra2D/src/scene/scene.cpp new file mode 100644 index 0000000..127ecf9 --- /dev/null +++ b/Extra2D/src/scene/scene.cpp @@ -0,0 +1,5 @@ +#include "extra2d/scene/scene.h" + +namespace extra2d { + +} diff --git a/Extra2D/src/scene/trans.cpp b/Extra2D/src/scene/trans.cpp new file mode 100644 index 0000000..b9267d8 --- /dev/null +++ b/Extra2D/src/scene/trans.cpp @@ -0,0 +1,80 @@ +#include "extra2d/scene/trans.h" +#include "extra2d/scene/director.h" +#include + +namespace extra2d { + +void Trans::finish() { + if (inScene_) { + inScene_->onEnter(); + inScene_->setRunning(true); + } + if (outScene_) { + outScene_->onExit(); + outScene_->setRunning(false); + } +} + +void TransFade::draw(f32 p) { +} + +void TransFlipX::draw(f32 p) { + f32 angle = p * 180.0f; + if (p < 0.5f) { + if (outScene_) { + } + } else { + if (inScene_) { + } + } +} + +void TransFlipY::draw(f32 p) { + f32 angle = p * 180.0f; +} + +void TransMoveInL::draw(f32 p) { + if (inScene_) { + f32 x = (1.0f - p) * -1280.0f; + } +} + +void TransMoveInR::draw(f32 p) { + if (inScene_) { + f32 x = (1.0f - p) * 1280.0f; + } +} + +void TransMoveInT::draw(f32 p) { + if (inScene_) { + f32 y = (1.0f - p) * 720.0f; + } +} + +void TransMoveInB::draw(f32 p) { + if (inScene_) { + f32 y = (1.0f - p) * -720.0f; + } +} + +void TransZoomFlipX::draw(f32 p) { + f32 scale = std::abs(std::sin(p * PI_F)); + f32 angle = p * 360.0f; +} + +void TransZoomFlipY::draw(f32 p) { + f32 scale = std::abs(std::sin(p * PI_F)); + f32 angle = p * 360.0f; +} + +void TransRotoZoom::draw(f32 p) { + f32 scale = 1.0f + std::sin(p * PI_F * 2) * 0.5f; + f32 angle = p * 360.0f; +} + +void TransJumpZoom::draw(f32 p) { + f32 scale = 1.0f + std::sin(p * PI_F) * 0.5f; + f32 jump = std::abs(std::sin(p * PI_F * 2)) * 100.0f; +} + +} diff --git a/Extra2D/src/win/win_adapt.cpp b/Extra2D/src/win/win_adapt.cpp new file mode 100644 index 0000000..a739df9 --- /dev/null +++ b/Extra2D/src/win/win_adapt.cpp @@ -0,0 +1,17 @@ +#include "extra2d/win/win_adapt.h" +#include "extra2d/cam/cam.h" +#include "extra2d/cam/ortho_cam.h" + +namespace extra2d { + +void WinAdapt::apply(Camera* cam) const { + if (!cam) return; + + if (cam->type() == CamType::Ortho) { + auto* ortho = static_cast(cam); + ortho->setOrthoFromSize(cfg_.refW, cfg_.refH); + ortho->viewport(vp_.origin.x, vp_.origin.y, vp_.size.width, vp_.size.height); + } +} + +} diff --git a/Extra2D/src/win/win_adapt_svc.cpp b/Extra2D/src/win/win_adapt_svc.cpp new file mode 100644 index 0000000..9d9b64c --- /dev/null +++ b/Extra2D/src/win/win_adapt_svc.cpp @@ -0,0 +1,5 @@ +#include "extra2d/win/win_adapt_svc.h" + +namespace extra2d { + +} diff --git a/Tests/test_action.cpp b/Tests/test_action.cpp new file mode 100644 index 0000000..79e36d2 --- /dev/null +++ b/Tests/test_action.cpp @@ -0,0 +1,622 @@ +/** + * @file test_action.cpp + * @brief Action 系统单元测试 + * + * 测试动作系统的基础类、即时动作、间隔动作、复合动作等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.001f; + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } +} + +// --------------------------------------------------------------------------- +// Act 基础类测试 +// --------------------------------------------------------------------------- + +TEST(Act, Tag) { + auto node = ptr::make(); + + class TestAct : public Act { + public: + bool done() const override { return true; } + void update(f32 t) override {} + Act* clone() const override { return new TestAct(); } + Act* reverse() const override { return new TestAct(); } + }; + + TestAct act; + TEST_ASSERT_EQ(-1, act.tag()); + + act.tag(42); + TEST_ASSERT_EQ(42, act.tag()); +} + +// --------------------------------------------------------------------------- +// ActInstant 测试 +// --------------------------------------------------------------------------- + +TEST(ActInstant, Done) { + CallFunc act([](){}); + TEST_ASSERT_TRUE(act.done()); +} + +TEST(CallFunc, Execute) { + auto node = ptr::make(); + + bool called = false; + CallFunc act([&called](){ called = true; }); + + act.start(node.get()); + act.step(0.016f); + + TEST_ASSERT_TRUE(called); +} + +TEST(Place, SetPosition) { + auto node = ptr::make(); + node->setPos(0, 0); + + Place act(Vec2(100, 200)); + + act.start(node.get()); + act.step(0.016f); + + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 200))); +} + +TEST(Show, SetVisible) { + auto node = ptr::make(); + node->setVisible(false); + + Show act; + + act.start(node.get()); + act.step(0.016f); + + TEST_ASSERT_TRUE(node->visible()); +} + +TEST(Hide, SetInvisible) { + auto node = ptr::make(); + node->setVisible(true); + + Hide act; + + act.start(node.get()); + act.step(0.016f); + + TEST_ASSERT_FALSE(node->visible()); +} + +// --------------------------------------------------------------------------- +// ActInterval 基础测试 +// --------------------------------------------------------------------------- + +TEST(ActInterval, Duration) { + MoveTo act(2.0f, Vec2(100, 100)); + + TEST_ASSERT_TRUE(std::abs(act.dur() - 2.0f) < EPSILON); + TEST_ASSERT_FALSE(act.done()); +} + +TEST(ActInterval, Elapsed) { + MoveTo act(1.0f, Vec2(100, 100)); + + TEST_ASSERT_TRUE(std::abs(act.elap()) < EPSILON); + + act.step(0.5f); + TEST_ASSERT_TRUE(std::abs(act.elap() - 0.5f) < EPSILON); +} + +TEST(ActInterval, Done) { + MoveTo act(1.0f, Vec2(100, 100)); + + TEST_ASSERT_FALSE(act.done()); + + act.step(1.0f); + TEST_ASSERT_TRUE(act.done()); + + act.step(0.5f); + TEST_ASSERT_TRUE(act.done()); +} + +// --------------------------------------------------------------------------- +// MoveTo/MoveBy 测试 +// --------------------------------------------------------------------------- + +TEST(MoveTo, MoveToPosition) { + auto node = ptr::make(); + node->setPos(0, 0); + + MoveTo act(1.0f, Vec2(100, 200)); + act.start(node.get()); + + act.update(0.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(0, 0))); + + act.update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 100))); + + act.update(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 200))); +} + +TEST(MoveBy, MoveByOffset) { + auto node = ptr::make(); + node->setPos(50, 50); + + MoveBy act(1.0f, Vec2(100, 100)); + act.start(node.get()); + + act.update(0.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 50))); + + act.update(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(150, 150))); +} + +TEST(MoveTo, Clone) { + MoveTo act(2.0f, Vec2(100, 200)); + act.tag(42); + + Act* cloned = act.clone(); + + TEST_ASSERT_NOT_NULL(cloned); + TEST_ASSERT_EQ(42, cloned->tag()); + + MoveTo* clonedMove = dynamic_cast(cloned); + TEST_ASSERT_NOT_NULL(clonedMove); + + delete cloned; +} + +TEST(MoveBy, Reverse) { + MoveBy act(1.0f, Vec2(100, 100)); + + Act* reversed = act.reverse(); + + TEST_ASSERT_NOT_NULL(reversed); + + MoveBy* reversedMove = dynamic_cast(reversed); + TEST_ASSERT_NOT_NULL(reversedMove); + + delete reversed; +} + +// --------------------------------------------------------------------------- +// RotTo/RotBy 测试 +// --------------------------------------------------------------------------- + +TEST(RotTo, RotateToAngle) { + auto node = ptr::make(); + node->setRot(0); + + RotTo act(1.0f, 90.0f); + act.start(node.get()); + + act.update(0.0f); + TEST_ASSERT_TRUE(std::abs(node->rot) < EPSILON); + + act.update(0.5f); + TEST_ASSERT_TRUE(std::abs(node->rot - 45.0f) < EPSILON); + + act.update(1.0f); + TEST_ASSERT_TRUE(std::abs(node->rot - 90.0f) < EPSILON); +} + +TEST(RotBy, RotateByAngle) { + auto node = ptr::make(); + node->setRot(45.0f); + + RotBy act(1.0f, 90.0f); + act.start(node.get()); + + act.update(0.0f); + TEST_ASSERT_TRUE(std::abs(node->rot - 45.0f) < EPSILON); + + act.update(1.0f); + TEST_ASSERT_TRUE(std::abs(node->rot - 135.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// ScaleTo/ScaleBy 测试 +// --------------------------------------------------------------------------- + +TEST(ScaleTo, ScaleToValue) { + auto node = ptr::make(); + node->setScale(1.0f); + + ScaleTo act(1.0f, 2.0f); + act.start(node.get()); + + act.update(0.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(1, 1))); + + act.update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(1.5f, 1.5f))); + + act.update(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(2, 2))); +} + +TEST(ScaleBy, ScaleByValue) { + auto node = ptr::make(); + node->setScale(1.0f); + + ScaleBy act(1.0f, 2.0f); + act.start(node.get()); + + act.update(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(2, 2))); +} + +TEST(ScaleTo, ScaleXY) { + auto node = ptr::make(); + node->setScale(1.0f); + + ScaleTo act(1.0f, Vec2(2.0f, 3.0f)); + act.start(node.get()); + + act.update(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(2, 3))); +} + +// --------------------------------------------------------------------------- +// FadeTo/FadeIn/FadeOut 测试 +// --------------------------------------------------------------------------- + +TEST(FadeTo, FadeToOpacity) { + auto node = ptr::make(); + + FadeTo act(1.0f, 0.5f); + act.start(node.get()); + + act.update(0.0f); + + act.update(1.0f); +} + +TEST(FadeIn, FadeIn) { + auto node = ptr::make(); + + FadeIn act(1.0f); + act.start(node.get()); + + act.update(1.0f); +} + +TEST(FadeOut, FadeOut) { + auto node = ptr::make(); + + FadeOut act(1.0f); + act.start(node.get()); + + act.update(1.0f); +} + +// --------------------------------------------------------------------------- +// Easing 函数测试 +// --------------------------------------------------------------------------- + +TEST(Easing, Linear) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeLinear(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeLinear(0.5f) - 0.5f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeLinear(1.0f) - 1.0f) < EPSILON); +} + +TEST(Easing, EaseIn) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeIn(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeIn(0.5f) - 0.25f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeIn(1.0f) - 1.0f) < EPSILON); +} + +TEST(Easing, EaseOut) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeOut(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeOut(0.5f) - 0.75f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeOut(1.0f) - 1.0f) < EPSILON); +} + +TEST(Easing, EaseInOut) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeInOut(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeInOut(0.25f) - 0.125f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeInOut(0.5f) - 0.5f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeInOut(0.75f) - 0.875f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeInOut(1.0f) - 1.0f) < EPSILON); +} + +TEST(Easing, EaseBackIn) { + f32 result = ActInterval::easeBackIn(0.5f); + TEST_ASSERT_TRUE(result < 0.5f); +} + +TEST(Easing, EaseBackOut) { + f32 result = ActInterval::easeBackOut(0.5f); + TEST_ASSERT_TRUE(result > 0.5f); +} + +TEST(Easing, EaseBounceOut) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeBounceOut(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeBounceOut(1.0f) - 1.0f) < EPSILON); +} + +TEST(Easing, EaseElasticOut) { + TEST_ASSERT_TRUE(std::abs(ActInterval::easeElasticOut(0.0f) - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(ActInterval::easeElasticOut(1.0f) - 1.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// Seq 序列动作测试 +// --------------------------------------------------------------------------- + +TEST(Seq, Sequence) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto seq = Seq::create( + new MoveTo(0.5f, Vec2(100, 0)), + new MoveTo(0.5f, Vec2(100, 100)) + ); + + seq->start(node.get()); + + seq->update(0.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(0, 0))); + + seq->update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 0))); + + seq->update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 100))); + + TEST_ASSERT_TRUE(seq->done()); + + delete seq; +} + +TEST(Seq, SingleAction) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto seq = Seq::create( + new MoveTo(0.5f, Vec2(100, 0)) + ); + + seq->start(node.get()); + + seq->update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 0))); + + TEST_ASSERT_TRUE(seq->done()); + + delete seq; +} + +// --------------------------------------------------------------------------- +// Spawn 并行动作测试 +// --------------------------------------------------------------------------- + +TEST(Spawn, Parallel) { + auto node = ptr::make(); + node->setPos(0, 0); + node->setRot(0); + + auto spawn = Spawn::create( + new MoveTo(1.0f, Vec2(100, 100)), + new RotTo(1.0f, 90.0f) + ); + + spawn->start(node.get()); + + spawn->update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 50))); + TEST_ASSERT_TRUE(std::abs(node->rot - 45.0f) < EPSILON); + + spawn->update(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 100))); + TEST_ASSERT_TRUE(std::abs(node->rot - 90.0f) < EPSILON); + + TEST_ASSERT_TRUE(spawn->done()); + + delete spawn; +} + +// --------------------------------------------------------------------------- +// Repeat 重复动作测试 +// --------------------------------------------------------------------------- + +TEST(Repeat, RepeatTimes) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto repeat = new Repeat(new MoveBy(0.5f, Vec2(100, 0)), 3); + + repeat->start(node.get()); + + for (int i = 0; i < 3; ++i) { + repeat->update(0.5f); + } + + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(300, 0))); + TEST_ASSERT_TRUE(repeat->done()); + + delete repeat; +} + +// --------------------------------------------------------------------------- +// RepeatForever 无限重复测试 +// --------------------------------------------------------------------------- + +TEST(RepeatForever, NeverDone) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto repeat = new RepeatForever(new MoveBy(1.0f, Vec2(100, 0))); + + repeat->start(node.get()); + + for (int i = 0; i < 10; ++i) { + repeat->update(1.0f); + TEST_ASSERT_FALSE(repeat->done()); + } + + TEST_ASSERT_TRUE(node->pos.x > 900); + + delete repeat; +} + +// --------------------------------------------------------------------------- +// Speed 速度控制测试 +// --------------------------------------------------------------------------- + +TEST(Speed, DoubleSpeed) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto speed = new Speed(new MoveTo(2.0f, Vec2(100, 0)), 2.0f); + + speed->start(node.get()); + + speed->step(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 0))); + + speed->step(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 0))); + TEST_ASSERT_TRUE(speed->done()); + + delete speed; +} + +TEST(Speed, HalfSpeed) { + auto node = ptr::make(); + node->setPos(0, 0); + + auto speed = new Speed(new MoveTo(1.0f, Vec2(100, 0)), 0.5f); + + speed->start(node.get()); + + speed->step(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 0))); + TEST_ASSERT_FALSE(speed->done()); + + speed->step(1.0f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 0))); + TEST_ASSERT_TRUE(speed->done()); + + delete speed; +} + +// --------------------------------------------------------------------------- +// Node 动作管理测试 +// --------------------------------------------------------------------------- + +TEST(Node, RunAction) { + auto node = ptr::make(); + node->setPos(0, 0); + + MoveTo* act = new MoveTo(1.0f, Vec2(100, 100)); + Act* returned = node->run(act); + + TEST_ASSERT_EQ(act, returned); + TEST_ASSERT_EQ(1, node->runningActs()); +} + +TEST(Node, StopAction) { + auto node = ptr::make(); + node->setPos(0, 0); + + MoveTo* act = new MoveTo(1.0f, Vec2(100, 100)); + act->tag(42); + node->run(act); + + node->stop(act); + + TEST_ASSERT_EQ(0, node->runningActs()); +} + +TEST(Node, StopByTag) { + auto node = ptr::make(); + + MoveTo* act = new MoveTo(1.0f, Vec2(100, 100)); + act->tag(42); + node->run(act); + + node->stopByTag(42); + + TEST_ASSERT_EQ(0, node->runningActs()); +} + +TEST(Node, StopAll) { + auto node = ptr::make(); + + node->run(new MoveTo(1.0f, Vec2(100, 0))); + node->run(new RotTo(1.0f, 90.0f)); + node->run(new ScaleTo(1.0f, 2.0f)); + + TEST_ASSERT_EQ(3, node->runningActs()); + + node->stopAll(); + + TEST_ASSERT_EQ(0, node->runningActs()); +} + +TEST(Node, GetActByTag) { + auto node = ptr::make(); + + MoveTo* act = new MoveTo(1.0f, Vec2(100, 100)); + act->tag(42); + node->run(act); + + Act* found = node->getActByTag(42); + TEST_ASSERT_EQ(act, found); + + Act* notFound = node->getActByTag(999); + TEST_ASSERT_NULL(notFound); +} + +TEST(Node, UpdateActions) { + auto node = ptr::make(); + node->setPos(0, 0); + + node->run(new MoveTo(1.0f, Vec2(100, 0))); + + node->updateActs(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 0))); + + node->updateActs(0.5f); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 0))); +} + +// --------------------------------------------------------------------------- +// 自定义缓动测试 +// --------------------------------------------------------------------------- + +TEST(ActInterval, CustomEasing) { + auto node = ptr::make(); + node->setPos(0, 0); + + MoveTo act(1.0f, Vec2(100, 0)); + act.ease([](f32 t) { return t * t * t; }); + act.start(node.get()); + + act.update(0.5f); + TEST_ASSERT_TRUE(std::abs(node->pos.x - 12.5f) < EPSILON); +} diff --git a/Tests/test_camera.cpp b/Tests/test_camera.cpp new file mode 100644 index 0000000..1bb13ee --- /dev/null +++ b/Tests/test_camera.cpp @@ -0,0 +1,328 @@ +/** + * @file test_camera.cpp + * @brief Camera 系统单元测试 + * + * 测试正交相机的投影矩阵、坐标转换等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.001f; + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } + + bool mat4Equal(const Mat4& a, const Mat4& b, f32 eps = EPSILON) { + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + if (std::abs(a[i][j] - b[i][j]) > eps) { + return false; + } + } + } + return true; + } +} + +// --------------------------------------------------------------------------- +// OrthoCam 基本测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, Create) { + OrthoCam cam; + + TEST_ASSERT_TRUE(cam.type() == CamType::Ortho); + TEST_ASSERT_TRUE(vec2Equal(Vec2(0, 0), Vec2(cam.position().x, cam.position().y))); + TEST_ASSERT_TRUE(std::abs(cam.zoom() - 1.0f) < EPSILON); +} + +TEST(OrthoCam, SetPosition) { + OrthoCam cam; + + cam.setPosition(Vec2(100, 200)); + TEST_ASSERT_TRUE(vec2Equal(Vec2(100, 200), Vec2(cam.position().x, cam.position().y))); + + cam.setPosition(Vec2(50, 75)); + TEST_ASSERT_TRUE(vec2Equal(Vec2(50, 75), Vec2(cam.position().x, cam.position().y))); +} + +TEST(OrthoCam, SetZoom) { + OrthoCam cam; + + cam.setZoom(2.0f); + TEST_ASSERT_TRUE(std::abs(cam.zoom() - 2.0f) < EPSILON); + + cam.setZoom(0.5f); + TEST_ASSERT_TRUE(std::abs(cam.zoom() - 0.5f) < EPSILON); +} + +TEST(OrthoCam, SetRotation) { + OrthoCam cam; + + cam.setRotation(45.0f); + TEST_ASSERT_TRUE(std::abs(cam.rotation() - 45.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 正交投影设置测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, Ortho) { + OrthoCam cam; + + cam.ortho(-640, 640, -360, 360); + + TEST_ASSERT_TRUE(std::abs(cam.left() - (-640)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cam.right() - 640) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cam.bottom() - (-360)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cam.top() - 360) < EPSILON); +} + +TEST(OrthoCam, SetOrthoFromSize) { + OrthoCam cam; + + cam.setOrthoFromSize(1280, 720); + + TEST_ASSERT_TRUE(std::abs(cam.width() - 1280) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cam.height() - 720) < EPSILON); + TEST_ASSERT_TRUE(vec2Equal(cam.center(), Vec2(0, 0))); +} + +TEST(OrthoCam, SetCenter) { + OrthoCam cam; + cam.setOrthoFromSize(1280, 720); + + cam.setCenter(100, 200); + + TEST_ASSERT_TRUE(vec2Equal(cam.center(), Vec2(100, 200))); + TEST_ASSERT_TRUE(std::abs(cam.left() - (100 - 640)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cam.right() - (100 + 640)) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 视图矩阵测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, ViewMatrixIdentity) { + OrthoCam cam; + cam.setPosition(Vec2(0, 0)); + cam.setRotation(0); + cam.setZoom(1.0f); + + Mat4 view = cam.view(); + Mat4 identity(1.0f); + + TEST_ASSERT_TRUE(mat4Equal(view, identity)); +} + +TEST(OrthoCam, ViewMatrixTranslation) { + OrthoCam cam; + cam.setPosition(Vec2(100, 200)); + cam.setRotation(0); + cam.setZoom(1.0f); + + Mat4 view = cam.view(); + + Vec4 origin(0, 0, 0, 1); + Vec4 transformed = view * origin; + + TEST_ASSERT_TRUE(std::abs(transformed.x - (-100)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - (-200)) < EPSILON); +} + +TEST(OrthoCam, ViewMatrixZoom) { + OrthoCam cam; + cam.setPosition(Vec2(0, 0)); + cam.setRotation(0); + cam.setZoom(2.0f); + + Mat4 view = cam.view(); + + Vec4 point(100, 100, 0, 1); + Vec4 transformed = view * point; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 200) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 200) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 投影矩阵测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, ProjMatrix) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360, -1, 1); + + Mat4 proj = cam.proj(); + + Vec4 corner1(-640, -360, 0, 1); + Vec4 transformed1 = proj * corner1; + + TEST_ASSERT_TRUE(std::abs(transformed1.x - (-1)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed1.y - (-1)) < EPSILON); + + Vec4 corner2(640, 360, 0, 1); + Vec4 transformed2 = proj * corner2; + + TEST_ASSERT_TRUE(std::abs(transformed2.x - 1) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed2.y - 1) < EPSILON); +} + +TEST(OrthoCam, ProjMatrixWithZoom) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.setZoom(2.0f); + + Mat4 proj = cam.proj(); + + Vec4 center(0, 0, 0, 1); + Vec4 transformed = proj * center; + + TEST_ASSERT_TRUE(std::abs(transformed.x) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 坐标转换测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, ScreenToWorld) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(0, 0)); + cam.setZoom(1.0f); + + Vec2 screenCenter(640, 360); + Vec2 worldPos = cam.screenToWorld(screenCenter); + + TEST_ASSERT_TRUE(vec2Equal(worldPos, Vec2(0, 0))); +} + +TEST(OrthoCam, ScreenToWorldCorner) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(0, 0)); + cam.setZoom(1.0f); + + Vec2 screenTopLeft(0, 0); + Vec2 worldPos = cam.screenToWorld(screenTopLeft); + + TEST_ASSERT_TRUE(std::abs(worldPos.x - (-640)) < EPSILON); + TEST_ASSERT_TRUE(std::abs(worldPos.y - 360) < EPSILON); +} + +TEST(OrthoCam, WorldToScreen) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(0, 0)); + cam.setZoom(1.0f); + + Vec2 worldCenter(0, 0); + Vec2 screenPos = cam.worldToScreen(worldCenter); + + TEST_ASSERT_TRUE(vec2Equal(screenPos, Vec2(640, 360))); +} + +TEST(OrthoCam, WorldToScreenCorner) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(0, 0)); + cam.setZoom(1.0f); + + Vec2 worldTopLeft(-640, 360); + Vec2 screenPos = cam.worldToScreen(worldTopLeft); + + TEST_ASSERT_TRUE(std::abs(screenPos.x) < EPSILON); + TEST_ASSERT_TRUE(std::abs(screenPos.y) < EPSILON); +} + +TEST(OrthoCam, ScreenToWorldRoundTrip) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(100, 200)); + cam.setZoom(1.5f); + + Vec2 originalWorld(300, 400); + Vec2 screen = cam.worldToScreen(originalWorld); + Vec2 backToWorld = cam.screenToWorld(screen); + + TEST_ASSERT_TRUE(vec2Equal(originalWorld, backToWorld, 0.1f)); +} + +// --------------------------------------------------------------------------- +// 视口测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, SetViewport) { + OrthoCam cam; + + cam.viewport(100, 200, 800, 600); + + Vec2 vpPos = cam.viewportPos(); + Vec2 vpSize = cam.viewportSize(); + + TEST_ASSERT_TRUE(std::abs(vpPos.x - 100) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vpPos.y - 200) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vpSize.x - 800) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vpSize.y - 600) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 脏标记测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, DirtyFlag) { + OrthoCam cam; + + TEST_ASSERT_TRUE(cam.dirty()); + + cam.setDirty(false); + TEST_ASSERT_FALSE(cam.dirty()); + + cam.setPosition(Vec2(100, 100)); + TEST_ASSERT_TRUE(cam.dirty()); + + cam.setDirty(false); + TEST_ASSERT_FALSE(cam.dirty()); + + cam.setZoom(2.0f); + TEST_ASSERT_TRUE(cam.dirty()); +} + +// --------------------------------------------------------------------------- +// VP 矩阵测试 +// --------------------------------------------------------------------------- + +TEST(OrthoCam, VPMatrix) { + OrthoCam cam; + cam.ortho(-640, 640, -360, 360); + cam.viewport(0, 0, 1280, 720); + cam.setPosition(Vec2(0, 0)); + cam.setRotation(0); + cam.setZoom(1.0f); + cam.setDirty(false); + + Mat4 vp = cam.vp(); + + Mat4 view = cam.view(); + Mat4 proj = cam.proj(); + Mat4 expectedVP = proj * view; + + TEST_ASSERT_TRUE(mat4Equal(vp, expectedVP)); +} diff --git a/Tests/test_node.cpp b/Tests/test_node.cpp new file mode 100644 index 0000000..cbd0455 --- /dev/null +++ b/Tests/test_node.cpp @@ -0,0 +1,412 @@ +/** + * @file test_node.cpp + * @brief Node 和 Transform 系统单元测试 + * + * 测试节点的变换矩阵计算、父子关系、组件系统等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.0001f; + + bool mat3Equal(const Mat3& a, const Mat3& b, f32 eps = EPSILON) { + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (std::abs(a[i][j] - b[i][j]) > eps) { + return false; + } + } + } + return true; + } + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } +} + +// --------------------------------------------------------------------------- +// Node 基本测试 +// --------------------------------------------------------------------------- + +TEST(Node, Create) { + auto node = ptr::make(); + TEST_ASSERT_NOT_NULL(node.get()); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(0, 0))); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(1, 1))); + TEST_ASSERT_TRUE(std::abs(node->rot) < EPSILON); + TEST_ASSERT_TRUE(vec2Equal(node->anchor, Vec2(0.5f, 0.5f))); +} + +TEST(Node, SetPosition) { + auto node = ptr::make(); + + node->setPos(100, 200); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(100, 200))); + + node->setPos(Vec2(50, 75)); + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 75))); +} + +TEST(Node, SetScale) { + auto node = ptr::make(); + + node->setScale(2.0f, 3.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(2, 3))); + + node->setScale(Vec2(1.5f, 2.5f)); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(1.5f, 2.5f))); + + node->setScale(2.0f); + TEST_ASSERT_TRUE(vec2Equal(node->scale, Vec2(2, 2))); +} + +TEST(Node, SetRotation) { + auto node = ptr::make(); + + node->setRot(45.0f); + TEST_ASSERT_TRUE(std::abs(node->rot - 45.0f) < EPSILON); + + node->setRot(-30.0f); + TEST_ASSERT_TRUE(std::abs(node->rot - (-30.0f)) < EPSILON); +} + +TEST(Node, SetAnchor) { + auto node = ptr::make(); + + node->setAnchor(0.0f, 0.0f); + TEST_ASSERT_TRUE(vec2Equal(node->anchor, Vec2(0, 0))); + + node->setAnchor(Anchor::TopRight); + TEST_ASSERT_TRUE(vec2Equal(node->anchor, Vec2(1, 1))); + + node->setAnchor(Anchor::Center); + TEST_ASSERT_TRUE(vec2Equal(node->anchor, Vec2(0.5f, 0.5f))); +} + +// --------------------------------------------------------------------------- +// Transform 矩阵测试 +// --------------------------------------------------------------------------- + +TEST(Node, LocalMatrixIdentity) { + auto node = ptr::make(); + + Mat3 local = node->local(); + Mat3 identity(1.0f); + + TEST_ASSERT_TRUE(mat3Equal(local, identity)); +} + +TEST(Node, LocalMatrixTranslation) { + auto node = ptr::make(); + node->setPos(100, 50); + + Mat3 local = node->local(); + + glm::vec3 origin(0, 0, 1.0f); + glm::vec3 transformed = local * origin; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 100) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 50) < EPSILON); +} + +TEST(Node, LocalMatrixScale) { + auto node = ptr::make(); + node->setScale(2.0f, 3.0f); + + Mat3 local = node->local(); + + glm::vec3 point(10, 10, 1.0f); + glm::vec3 transformed = local * point; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 20) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 30) < EPSILON); +} + +TEST(Node, LocalMatrixRotation) { + auto node = ptr::make(); + node->setRot(90.0f); + + Mat3 local = node->local(); + + glm::vec3 point(1, 0, 1.0f); + glm::vec3 transformed = local * point; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 0.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 1.0f) < EPSILON); +} + +TEST(Node, LocalMatrixCombined) { + auto node = ptr::make(); + node->setPos(100, 100); + node->setScale(2.0f, 2.0f); + node->setRot(0.0f); + + Mat3 local = node->local(); + + glm::vec3 point(50, 50, 1.0f); + glm::vec3 transformed = local * point; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 200) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 200) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 父子关系测试 +// --------------------------------------------------------------------------- + +TEST(Node, AddChild) { + auto parent = ptr::make(); + auto child = ptr::make(); + + parent->addChild(child); + + TEST_ASSERT_EQ(parent.get(), child->parent()); + TEST_ASSERT_EQ(1u, parent->children().size()); + TEST_ASSERT_EQ(child.get(), parent->children()[0].get()); +} + +TEST(Node, RemoveChild) { + auto parent = ptr::make(); + auto child = ptr::make(); + + parent->addChild(child); + parent->removeChild(child); + + TEST_ASSERT_NULL(child->parent()); + TEST_ASSERT_EQ(0u, parent->children().size()); +} + +TEST(Node, RemoveFromParent) { + auto parent = ptr::make(); + auto child = ptr::make(); + + parent->addChild(child); + child->removeFromParent(); + + TEST_ASSERT_NULL(child->parent()); + TEST_ASSERT_EQ(0u, parent->children().size()); +} + +TEST(Node, NestedHierarchy) { + auto root = ptr::make(); + auto child1 = ptr::make(); + auto child2 = ptr::make(); + auto grandchild = ptr::make(); + + root->addChild(child1); + root->addChild(child2); + child1->addChild(grandchild); + + TEST_ASSERT_EQ(root.get(), child1->parent()); + TEST_ASSERT_EQ(root.get(), child2->parent()); + TEST_ASSERT_EQ(child1.get(), grandchild->parent()); + TEST_ASSERT_EQ(2u, root->children().size()); + TEST_ASSERT_EQ(1u, child1->children().size()); +} + +// --------------------------------------------------------------------------- +// 世界变换测试 +// --------------------------------------------------------------------------- + +TEST(Node, WorldPosition) { + auto parent = ptr::make(); + parent->setPos(100, 100); + + auto child = ptr::make(); + child->setPos(50, 50); + + parent->addChild(child); + + Vec2 worldPos = child->worldPos(); + TEST_ASSERT_TRUE(vec2Equal(worldPos, Vec2(150, 150))); +} + +TEST(Node, WorldScale) { + auto parent = ptr::make(); + parent->setScale(2.0f, 2.0f); + + auto child = ptr::make(); + child->setScale(1.5f, 1.5f); + + parent->addChild(child); + + Vec2 worldScale = child->worldScale(); + TEST_ASSERT_TRUE(vec2Equal(worldScale, Vec2(3.0f, 3.0f))); +} + +TEST(Node, WorldRotation) { + auto parent = ptr::make(); + parent->setRot(45.0f); + + auto child = ptr::make(); + child->setRot(30.0f); + + parent->addChild(child); + + f32 worldRot = child->worldRot(); + TEST_ASSERT_TRUE(std::abs(worldRot - 75.0f) < EPSILON); +} + +TEST(Node, WorldMatrix4x4) { + auto parent = ptr::make(); + parent->setPos(100, 100); + + auto child = ptr::make(); + child->setPos(50, 50); + + parent->addChild(child); + + Mat4 world4x4 = child->world4x4(); + + Vec4 origin(0, 0, 0, 1); + Vec4 transformed = world4x4 * origin; + + TEST_ASSERT_TRUE(std::abs(transformed.x - 150.0f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y - 150.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 组件系统测试 +// --------------------------------------------------------------------------- + +class TestComp : public Comp { +public: + const char* type() const override { return "TestComp"; } + + void update(f32 dt) override { + updateCount++; + lastDt = dt; + } + + int updateCount = 0; + f32 lastDt = 0.0f; +}; + +TEST(Node, AddComponent) { + auto node = ptr::make(); + + TestComp* comp = node->add(); + + TEST_ASSERT_NOT_NULL(comp); + TEST_ASSERT_EQ(node.get(), comp->owner()); +} + +TEST(Node, GetComponent) { + auto node = ptr::make(); + node->add(); + + TestComp* comp = node->get(); + + TEST_ASSERT_NOT_NULL(comp); +} + +TEST(Node, RemoveComponent) { + auto node = ptr::make(); + node->add(); + + node->remove(); + + TestComp* comp = node->get(); + TEST_ASSERT_NULL(comp); +} + +TEST(Node, UpdateComponents) { + auto node = ptr::make(); + TestComp* comp = node->add(); + + node->updateComps(0.016f); + + TEST_ASSERT_EQ(1, comp->updateCount); + TEST_ASSERT_TRUE(std::abs(comp->lastDt - 0.016f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 可见性和标签测试 +// --------------------------------------------------------------------------- + +TEST(Node, Visibility) { + auto node = ptr::make(); + + TEST_ASSERT_TRUE(node->visible()); + + node->setVisible(false); + TEST_ASSERT_FALSE(node->visible()); + + node->setVisible(true); + TEST_ASSERT_TRUE(node->visible()); +} + +TEST(Node, Tag) { + auto node = ptr::make(); + + TEST_ASSERT_EQ(0, node->tag()); + + node->setTag(42); + TEST_ASSERT_EQ(42, node->tag()); +} + +TEST(Node, Name) { + auto node = ptr::make(); + + TEST_ASSERT_TRUE(node->name().empty()); + + node->setName("TestNode"); + TEST_ASSERT_EQ("TestNode", node->name()); +} + +// --------------------------------------------------------------------------- +// 边界框测试 +// --------------------------------------------------------------------------- + +TEST(BoundingBox, Create) { + BoundingBox box(Vec2(0, 0), Vec2(100, 100)); + + TEST_ASSERT_TRUE(vec2Equal(box.min, Vec2(0, 0))); + TEST_ASSERT_TRUE(vec2Equal(box.max, Vec2(100, 100))); +} + +TEST(BoundingBox, FromCenter) { + BoundingBox box = BoundingBox::fromCenter(Vec2(50, 50), Vec2(25, 25)); + + TEST_ASSERT_TRUE(vec2Equal(box.min, Vec2(25, 25))); + TEST_ASSERT_TRUE(vec2Equal(box.max, Vec2(75, 75))); +} + +TEST(BoundingBox, Contains) { + BoundingBox box(Vec2(0, 0), Vec2(100, 100)); + + TEST_ASSERT_TRUE(box.contains(Vec2(50, 50))); + TEST_ASSERT_TRUE(box.contains(Vec2(0, 0))); + TEST_ASSERT_TRUE(box.contains(Vec2(100, 100))); + TEST_ASSERT_FALSE(box.contains(Vec2(150, 50))); +} + +TEST(BoundingBox, Intersects) { + BoundingBox box1(Vec2(0, 0), Vec2(100, 100)); + BoundingBox box2(Vec2(50, 50), Vec2(150, 150)); + BoundingBox box3(Vec2(200, 200), Vec2(300, 300)); + + TEST_ASSERT_TRUE(box1.intersects(box2)); + TEST_ASSERT_FALSE(box1.intersects(box3)); +} + +TEST(BoundingBox, Merged) { + BoundingBox box1(Vec2(0, 0), Vec2(100, 100)); + BoundingBox box2(Vec2(50, 50), Vec2(150, 150)); + + BoundingBox merged = box1.merged(box2); + + TEST_ASSERT_TRUE(vec2Equal(merged.min, Vec2(0, 0))); + TEST_ASSERT_TRUE(vec2Equal(merged.max, Vec2(150, 150))); +} diff --git a/Tests/test_scene.cpp b/Tests/test_scene.cpp new file mode 100644 index 0000000..39dee16 --- /dev/null +++ b/Tests/test_scene.cpp @@ -0,0 +1,330 @@ +/** + * @file test_scene.cpp + * @brief Scene 管理单元测试 + * + * 测试场景的生命周期、Director 场景管理、场景转换等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.001f; + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } +} + +// --------------------------------------------------------------------------- +// Scene 基本测试 +// --------------------------------------------------------------------------- + +class TestScene : public Scene { +public: + TestScene() { + setName("TestScene"); + } + + void onEnter() override { enterCount++; } + void onExit() override { exitCount++; } + void onPause() override { pauseCount++; } + void onResume() override { resumeCount++; } + + int enterCount = 0; + int exitCount = 0; + int pauseCount = 0; + int resumeCount = 0; +}; + +TEST(Scene, Create) { + auto scene = ptr::make(); + + TEST_ASSERT_EQ("TestScene", scene->name()); + TEST_ASSERT_FALSE(scene->running()); + TEST_ASSERT_NOT_NULL(scene->graph()); + TEST_ASSERT_NOT_NULL(scene->cam()); +} + +TEST(Scene, SetName) { + auto scene = ptr::make(); + + scene->setName("MyScene"); + TEST_ASSERT_EQ("MyScene", scene->name()); +} + +TEST(Scene, SetRunning) { + auto scene = ptr::make(); + + scene->setRunning(true); + TEST_ASSERT_TRUE(scene->running()); + + scene->setRunning(false); + TEST_ASSERT_FALSE(scene->running()); +} + +TEST(Scene, LifecycleCallbacks) { + auto scene = ptr::make(); + + scene->onEnter(); + TEST_ASSERT_EQ(1, scene->enterCount); + + scene->onExit(); + TEST_ASSERT_EQ(1, scene->exitCount); + + scene->onPause(); + TEST_ASSERT_EQ(1, scene->pauseCount); + + scene->onResume(); + TEST_ASSERT_EQ(1, scene->resumeCount); +} + +TEST(Scene, Update) { + auto scene = ptr::make(); + + auto node = scene->graph()->create(); + node->setName("TestNode"); + + scene->update(0.016f); + + Node* found = scene->graph()->find("TestNode"); + TEST_ASSERT_NOT_NULL(found); +} + +// --------------------------------------------------------------------------- +// Director 基本测试 +// --------------------------------------------------------------------------- + +TEST(Director, Create) { + Director director; + + TEST_ASSERT_NULL(director.cur()); +} + +TEST(Director, RunScene) { + Director director; + + auto scene = ptr::make(); + director.run(scene); + + TEST_ASSERT_EQ(scene.get(), director.cur()); + TEST_ASSERT_TRUE(scene->running()); + TEST_ASSERT_EQ(1, scene->enterCount); +} + +TEST(Director, ReplaceScene) { + Director director; + + auto scene1 = ptr::make(); + scene1->setName("Scene1"); + director.run(scene1); + + auto scene2 = ptr::make(); + scene2->setName("Scene2"); + director.replace(scene2); + + TEST_ASSERT_EQ(scene2.get(), director.cur()); + TEST_ASSERT_EQ(1, scene1->exitCount); + TEST_ASSERT_EQ(1, scene2->enterCount); +} + +TEST(Director, PushPopScene) { + Director director; + + auto scene1 = ptr::make(); + scene1->setName("Scene1"); + director.run(scene1); + + auto scene2 = ptr::make(); + scene2->setName("Scene2"); + director.push(scene2); + + TEST_ASSERT_EQ(scene2.get(), director.cur()); + TEST_ASSERT_EQ(1, scene1->pauseCount); + TEST_ASSERT_EQ(1, scene2->enterCount); + + director.pop(); + + TEST_ASSERT_EQ(scene1.get(), director.cur()); + TEST_ASSERT_EQ(1, scene2->exitCount); + TEST_ASSERT_EQ(1, scene1->resumeCount); +} + +TEST(Director, Update) { + Director director; + + auto scene = ptr::make(); + director.run(scene); + + auto node = scene->graph()->create(); + node->setPos(0, 0); + node->run(new MoveTo(1.0f, Vec2(100, 0))); + + director.update(0.5f); + + TEST_ASSERT_TRUE(vec2Equal(node->pos, Vec2(50, 0))); +} + +TEST(Director, SceneStack) { + Director director; + + auto scene1 = ptr::make(); + scene1->setName("Scene1"); + director.run(scene1); + + auto scene2 = ptr::make(); + scene2->setName("Scene2"); + director.push(scene2); + + auto scene3 = ptr::make(); + scene3->setName("Scene3"); + director.push(scene3); + + TEST_ASSERT_EQ(scene3.get(), director.cur()); + + director.pop(); + TEST_ASSERT_EQ(scene2.get(), director.cur()); + + director.pop(); + TEST_ASSERT_EQ(scene1.get(), director.cur()); + + director.pop(); + TEST_ASSERT_NULL(director.cur()); +} + +// --------------------------------------------------------------------------- +// Trans 场景转换测试 +// --------------------------------------------------------------------------- + +TEST(TransFade, Create) { + auto scene1 = ptr::make(); + auto scene2 = ptr::make(); + + TransFade trans; + trans.init(1.0f, scene1, scene2); + + TEST_ASSERT_TRUE(std::abs(trans.duration() - 1.0f) < EPSILON); +} + +TEST(TransFade, Progress) { + auto scene1 = ptr::make(); + auto scene2 = ptr::make(); + + TransFade trans; + trans.init(2.0f, scene1, scene2); + + trans.update(1.0f); + TEST_ASSERT_TRUE(std::abs(trans.progress() - 0.5f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 场景图集成测试 +// --------------------------------------------------------------------------- + +TEST(Scene, GraphIntegration) { + auto scene = ptr::make(); + + auto parent = scene->graph()->create(); + parent->setName("Parent"); + parent->setPos(100, 100); + + auto child = ptr::make(); + child->setName("Child"); + child->setPos(50, 50); + parent->addChild(child); + + Vec2 worldPos = child->worldPos(); + TEST_ASSERT_TRUE(vec2Equal(worldPos, Vec2(150, 150))); +} + +TEST(Scene, CameraIntegration) { + auto scene = ptr::make(); + + auto cam = dynamic_cast(scene->cam()); + TEST_ASSERT_NOT_NULL(cam); + + cam->setOrthoFromSize(1280, 720); + cam->setPosition(Vec2(0, 0)); +} + +// --------------------------------------------------------------------------- +// 多场景测试 +// --------------------------------------------------------------------------- + +TEST(Director, MultipleSceneSwitches) { + Director director; + + auto scene1 = ptr::make(); + scene1->setName("Scene1"); + director.run(scene1); + + for (int i = 0; i < 5; ++i) { + auto newScene = ptr::make(); + newScene->setName("Scene" + std::to_string(i + 2)); + director.replace(newScene); + } + + TEST_ASSERT_EQ(5, scene1->exitCount); +} + +// --------------------------------------------------------------------------- +// 场景渲染测试 +// --------------------------------------------------------------------------- + +class RenderTestScene : public Scene { +public: + RenderTestScene() { setName("RenderTestScene"); } + + void render(RenderCmdBuffer& cmdBuffer) override { + renderCalled = true; + } + + bool renderCalled = false; +}; + +TEST(Scene, RenderCallback) { + auto scene = ptr::make(); + RenderCmdBuffer cmdBuffer; + + scene->render(cmdBuffer); + TEST_ASSERT_TRUE(scene->renderCalled); +} + +// --------------------------------------------------------------------------- +// 边界情况测试 +// --------------------------------------------------------------------------- + +TEST(Director, ReplaceWhenNoRunning) { + Director director; + + auto scene = ptr::make(); + director.replace(scene); + + TEST_ASSERT_EQ(scene.get(), director.cur()); +} + +TEST(Director, PopWhenEmpty) { + Director director; + + director.pop(); + + TEST_ASSERT_NULL(director.cur()); +} + +TEST(Director, PushWhenNoRunning) { + Director director; + + auto scene = ptr::make(); + director.push(scene); + + TEST_ASSERT_EQ(scene.get(), director.cur()); +} diff --git a/Tests/test_scene_graph.cpp b/Tests/test_scene_graph.cpp new file mode 100644 index 0000000..131703a --- /dev/null +++ b/Tests/test_scene_graph.cpp @@ -0,0 +1,323 @@ +/** + * @file test_scene_graph.cpp + * @brief SceneGraph 单元测试 + * + * 测试场景图的节点管理、遍历、查找等功能。 + */ + +#include "test_framework.h" +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.0001f; + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } +} + +// --------------------------------------------------------------------------- +// SceneGraph 基本测试 +// --------------------------------------------------------------------------- + +TEST(SceneGraph, Create) { + SceneGraph graph; + + TEST_ASSERT_EQ(0u, graph.nodeCount()); + TEST_ASSERT_TRUE(graph.roots().empty()); +} + +TEST(SceneGraph, CreateNode) { + SceneGraph graph; + + auto node = graph.create(); + + TEST_ASSERT_NOT_NULL(node.get()); + TEST_ASSERT_EQ(1u, graph.nodeCount()); + TEST_ASSERT_EQ(node.get(), graph.roots()[0].get()); +} + +TEST(SceneGraph, AddNode) { + SceneGraph graph; + auto node = ptr::make(); + + graph.add(node); + + TEST_ASSERT_EQ(1u, graph.nodeCount()); + TEST_ASSERT_EQ(node.get(), graph.roots()[0].get()); +} + +TEST(SceneGraph, RemoveNode) { + SceneGraph graph; + auto node = graph.create(); + + graph.remove(node); + + TEST_ASSERT_EQ(0u, graph.nodeCount()); +} + +TEST(SceneGraph, Clear) { + SceneGraph graph; + + graph.create(); + graph.create(); + graph.create(); + + TEST_ASSERT_EQ(3u, graph.nodeCount()); + + graph.clear(); + + TEST_ASSERT_EQ(0u, graph.nodeCount()); +} + +// --------------------------------------------------------------------------- +// 节点查找测试 +// --------------------------------------------------------------------------- + +TEST(SceneGraph, FindByName) { + SceneGraph graph; + + auto node1 = graph.create(); + node1->setName("Node1"); + + auto node2 = graph.create(); + node2->setName("Node2"); + + auto node3 = graph.create(); + node3->setName("Node3"); + + Node* found = graph.find("Node2"); + + TEST_ASSERT_NOT_NULL(found); + TEST_ASSERT_EQ(node2.get(), found); +} + +TEST(SceneGraph, FindByNameNotFound) { + SceneGraph graph; + + graph.create()->setName("Node1"); + graph.create()->setName("Node2"); + + Node* found = graph.find("NonExistent"); + + TEST_ASSERT_NULL(found); +} + +TEST(SceneGraph, FindByTag) { + SceneGraph graph; + + auto node1 = graph.create(); + node1->setTag(1); + + auto node2 = graph.create(); + node2->setTag(2); + + auto node3 = graph.create(); + node3->setTag(3); + + Node* found = graph.findByTag(2); + + TEST_ASSERT_NOT_NULL(found); + TEST_ASSERT_EQ(node2.get(), found); +} + +TEST(SceneGraph, FindByTagNotFound) { + SceneGraph graph; + + graph.create()->setTag(1); + graph.create()->setTag(2); + + Node* found = graph.findByTag(999); + + TEST_ASSERT_NULL(found); +} + +// --------------------------------------------------------------------------- +// 遍历测试 +// --------------------------------------------------------------------------- + +TEST(SceneGraph, Traverse) { + SceneGraph graph; + + auto node1 = graph.create(); + node1->setName("Node1"); + + auto node2 = graph.create(); + node2->setName("Node2"); + + auto child = ptr::make(); + child->setName("Child"); + node1->addChild(child); + + std::vector visited; + graph.traverse([&visited](Node* node) { + visited.push_back(node->name()); + }); + + TEST_ASSERT_EQ(3u, visited.size()); + + bool foundNode1 = false, foundNode2 = false, foundChild = false; + for (const auto& name : visited) { + if (name == "Node1") foundNode1 = true; + if (name == "Node2") foundNode2 = true; + if (name == "Child") foundChild = true; + } + + TEST_ASSERT_TRUE(foundNode1); + TEST_ASSERT_TRUE(foundNode2); + TEST_ASSERT_TRUE(foundChild); +} + +TEST(SceneGraph, TraverseRecursive) { + SceneGraph graph; + + auto root = graph.create(); + root->setName("Root"); + + auto child1 = ptr::make(); + child1->setName("Child1"); + root->addChild(child1); + + auto grandchild = ptr::make(); + grandchild->setName("Grandchild"); + child1->addChild(grandchild); + + std::vector visited; + graph.traverseRecursive(root.get(), [&visited](Node* node) { + visited.push_back(node->name()); + }); + + TEST_ASSERT_EQ(3u, visited.size()); + TEST_ASSERT_EQ("Root", visited[0]); + TEST_ASSERT_EQ("Child1", visited[1]); + TEST_ASSERT_EQ("Grandchild", visited[2]); +} + +// --------------------------------------------------------------------------- +// 更新测试 +// --------------------------------------------------------------------------- + +class TestUpdateNode : public Node { +public: + void onUpdate(f32 dt) override { + updateCount++; + lastDt = dt; + } + + int updateCount = 0; + f32 lastDt = 0.0f; +}; + +TEST(SceneGraph, Update) { + SceneGraph graph; + + auto node = graph.create(); + + graph.update(0.016f); + + TEST_ASSERT_EQ(1, node->updateCount); + TEST_ASSERT_TRUE(std::abs(node->lastDt - 0.016f) < EPSILON); +} + +TEST(SceneGraph, UpdateMultipleNodes) { + SceneGraph graph; + + auto node1 = graph.create(); + auto node2 = graph.create(); + auto node3 = graph.create(); + + graph.update(0.033f); + + TEST_ASSERT_EQ(1, node1->updateCount); + TEST_ASSERT_EQ(1, node2->updateCount); + TEST_ASSERT_EQ(1, node3->updateCount); +} + +// --------------------------------------------------------------------------- +// 层级结构测试 +// --------------------------------------------------------------------------- + +TEST(SceneGraph, HierarchyWithChildren) { + SceneGraph graph; + + auto parent = graph.create(); + parent->setName("Parent"); + + auto child1 = ptr::make(); + child1->setName("Child1"); + parent->addChild(child1); + + auto child2 = ptr::make(); + child2->setName("Child2"); + parent->addChild(child2); + + TEST_ASSERT_EQ(1u, graph.nodeCount()); + TEST_ASSERT_EQ(2u, parent->children().size()); +} + +TEST(SceneGraph, DeepHierarchy) { + SceneGraph graph; + + auto root = graph.create(); + root->setPos(100, 100); + + auto level1 = ptr::make(); + level1->setPos(50, 50); + root->addChild(level1); + + auto level2 = ptr::make(); + level2->setPos(25, 25); + level1->addChild(level2); + + auto level3 = ptr::make(); + level3->setPos(10, 10); + level2->addChild(level3); + + Vec2 worldPos = level3->worldPos(); + + TEST_ASSERT_TRUE(vec2Equal(worldPos, Vec2(185, 185))); +} + +// --------------------------------------------------------------------------- +// 自定义节点类型测试 +// --------------------------------------------------------------------------- + +class CustomNode : public Node { +public: + CustomNode() : value(0) {} + explicit CustomNode(int v) : value(v) {} + + int value; + +protected: + Vec2 defaultAnchor() const override { return Vec2(0.0f, 0.0f); } +}; + +TEST(SceneGraph, CreateCustomNode) { + SceneGraph graph; + + auto node = graph.create(42); + + TEST_ASSERT_NOT_NULL(node.get()); + TEST_ASSERT_EQ(42, node->value); + TEST_ASSERT_TRUE(vec2Equal(node->anchor, Vec2(0, 0))); +} + +// --------------------------------------------------------------------------- +// 节点可见性测试 +// --------------------------------------------------------------------------- + +TEST(SceneGraph, NodeVisibility) { + SceneGraph graph; + + auto node = graph.create(); + TEST_ASSERT_TRUE(node->visible()); + + node->setVisible(false); + TEST_ASSERT_FALSE(node->visible()); +} diff --git a/Tests/test_win_adapt.cpp b/Tests/test_win_adapt.cpp new file mode 100644 index 0000000..18df13f --- /dev/null +++ b/Tests/test_win_adapt.cpp @@ -0,0 +1,436 @@ +/** + * @file test_win_adapt.cpp + * @brief Window Adaptation 单元测试 + * + * 测试窗口适配模式、坐标转换、视口计算等功能。 + */ + +#include "test_framework.h" +#include +#include +#include +#include + +using namespace extra2d; +using namespace extra2d::test; + +namespace { + constexpr f32 EPSILON = 0.001f; + + bool vec2Equal(const Vec2& a, const Vec2& b, f32 eps = EPSILON) { + return std::abs(a.x - b.x) < eps && std::abs(a.y - b.y) < eps; + } + + bool rectEqual(const Rect& a, const Rect& b, f32 eps = EPSILON) { + return std::abs(a.origin.x - b.origin.x) < eps && + std::abs(a.origin.y - b.origin.y) < eps && + std::abs(a.size.width - b.size.width) < eps && + std::abs(a.size.height - b.size.height) < eps; + } +} + +// --------------------------------------------------------------------------- +// WinAdapt 基本测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, Create) { + WinAdapt adapt; + + WinAdaptCfg cfg = adapt.cfg(); + TEST_ASSERT_TRUE(cfg.mode == AdaptMode::Fit); + TEST_ASSERT_TRUE(std::abs(cfg.refW - 1280) < EPSILON); + TEST_ASSERT_TRUE(std::abs(cfg.refH - 720) < EPSILON); +} + +TEST(WinAdapt, SetConfig) { + WinAdapt adapt; + + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fill; + cfg.refW = 1920; + cfg.refH = 1080; + cfg.autoScale = false; + + adapt.cfg(cfg); + + WinAdaptCfg retrieved = adapt.cfg(); + TEST_ASSERT_TRUE(retrieved.mode == AdaptMode::Fill); + TEST_ASSERT_TRUE(std::abs(retrieved.refW - 1920) < EPSILON); + TEST_ASSERT_TRUE(std::abs(retrieved.refH - 1080) < EPSILON); + TEST_ASSERT_FALSE(retrieved.autoScale); +} + +TEST(WinAdapt, SetRef) { + WinAdapt adapt; + + adapt.ref(800, 600); + + Vec2 ref = adapt.ref(); + TEST_ASSERT_TRUE(vec2Equal(ref, Vec2(800, 600))); +} + +TEST(WinAdapt, SetWin) { + WinAdapt adapt; + adapt.ref(1280, 720); + + adapt.win(1920, 1080); + + Vec2 win = adapt.win(); + TEST_ASSERT_TRUE(vec2Equal(win, Vec2(1920, 1080))); +} + +// --------------------------------------------------------------------------- +// Exact 模式测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, ExactMode) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Exact; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Rect vp = adapt.viewport(); + TEST_ASSERT_TRUE(rectEqual(vp, Rect(0, 0, 1920, 1080))); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - 1.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// Stretch 模式测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, StretchMode) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Stretch; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Rect vp = adapt.viewport(); + TEST_ASSERT_TRUE(rectEqual(vp, Rect(0, 0, 1920, 1080))); + + Vec2 scale2D = adapt.scale2D(); + TEST_ASSERT_TRUE(std::abs(scale2D.x - 1.5f) < EPSILON); + TEST_ASSERT_TRUE(std::abs(scale2D.y - 1.5f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// Fit 模式测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, FitModeSameAspect) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Rect vp = adapt.viewport(); + TEST_ASSERT_TRUE(rectEqual(vp, Rect(0, 0, 1920, 1080))); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - 1.5f) < EPSILON); +} + +TEST(WinAdapt, FitModeWiderWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 800); + + Rect vp = adapt.viewport(); + + f32 expectedScale = 800.0f / 720.0f; + f32 expectedW = 1280.0f * expectedScale; + f32 expectedX = (1920.0f - expectedW) / 2.0f; + + TEST_ASSERT_TRUE(std::abs(vp.origin.x - expectedX) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.origin.y) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.width - expectedW) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.height - 800) < EPSILON); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - expectedScale) < EPSILON); +} + +TEST(WinAdapt, FitModeTallerWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1280, 900); + + Rect vp = adapt.viewport(); + + f32 expectedScale = 1280.0f / 1280.0f; + f32 expectedH = 720.0f * expectedScale; + f32 expectedY = (900.0f - expectedH) / 2.0f; + + TEST_ASSERT_TRUE(std::abs(vp.origin.x) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.origin.y - expectedY) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.width - 1280) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.height - expectedH) < EPSILON); +} + +// --------------------------------------------------------------------------- +// Fill 模式测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, FillModeWiderWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fill; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 800); + + Rect vp = adapt.viewport(); + + f32 expectedScale = 1920.0f / 1280.0f; + f32 expectedH = 720.0f * expectedScale; + f32 expectedY = (800.0f - expectedH) / 2.0f; + + TEST_ASSERT_TRUE(std::abs(vp.origin.x) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.origin.y - expectedY) < EPSILON); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - expectedScale) < EPSILON); +} + +TEST(WinAdapt, FillModeTallerWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fill; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1280, 900); + + Rect vp = adapt.viewport(); + + f32 expectedScale = 900.0f / 720.0f; + f32 expectedW = 1280.0f * expectedScale; + f32 expectedX = (1280.0f - expectedW) / 2.0f; + + TEST_ASSERT_TRUE(std::abs(vp.origin.x - expectedX) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.origin.y) < EPSILON); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - expectedScale) < EPSILON); +} + +// --------------------------------------------------------------------------- +// Center 模式测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, CenterMode) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Center; + cfg.refW = 800; + cfg.refH = 600; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Rect vp = adapt.viewport(); + + f32 expectedX = (1920.0f - 800.0f) / 2.0f; + f32 expectedY = (1080.0f - 600.0f) / 2.0f; + + TEST_ASSERT_TRUE(std::abs(vp.origin.x - expectedX) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.origin.y - expectedY) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.width - 800) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.height - 600) < EPSILON); + TEST_ASSERT_TRUE(std::abs(adapt.scale() - 1.0f) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 坐标转换测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, WinToRef) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Vec2 winPos(960, 540); + Vec2 refPos = adapt.winToRef(winPos); + + TEST_ASSERT_TRUE(vec2Equal(refPos, Vec2(640, 360))); +} + +TEST(WinAdapt, RefToWin) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Vec2 refPos(640, 360); + Vec2 winPos = adapt.refToWin(refPos); + + TEST_ASSERT_TRUE(vec2Equal(winPos, Vec2(960, 540))); +} + +TEST(WinAdapt, WinToRefRoundTrip) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1600, 900); + + Vec2 originalRef(500, 300); + Vec2 win = adapt.refToWin(originalRef); + Vec2 backToRef = adapt.winToRef(win); + + TEST_ASSERT_TRUE(vec2Equal(originalRef, backToRef, 0.1f)); +} + +TEST(WinAdapt, ScreenToRef) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Vec2 screenPos(960, 540); + Vec2 refPos = adapt.screenToRef(screenPos); + + TEST_ASSERT_TRUE(vec2Equal(refPos, Vec2(640, 360))); +} + +TEST(WinAdapt, RefToScreen) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Vec2 refPos(640, 360); + Vec2 screenPos = adapt.refToScreen(refPos); + + TEST_ASSERT_TRUE(vec2Equal(screenPos, Vec2(960, 540))); +} + +// --------------------------------------------------------------------------- +// 矩阵测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, Matrix) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(1920, 1080); + + Mat4 m = adapt.matrix(); + + Vec4 origin(0, 0, 0, 1); + Vec4 transformed = m * origin; + + TEST_ASSERT_TRUE(std::abs(transformed.x) < EPSILON); + TEST_ASSERT_TRUE(std::abs(transformed.y) < EPSILON); +} + +// --------------------------------------------------------------------------- +// 边界情况测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, ZeroWindowSize) { + WinAdapt adapt; + adapt.ref(1280, 720); + + adapt.win(0, 0); + + Rect vp = adapt.viewport(); + TEST_ASSERT_TRUE(std::abs(vp.size.width) < EPSILON); + TEST_ASSERT_TRUE(std::abs(vp.size.height) < EPSILON); +} + +TEST(WinAdapt, VerySmallWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(100, 100); + + Rect vp = adapt.viewport(); + + TEST_ASSERT_TRUE(vp.size.width > 0); + TEST_ASSERT_TRUE(vp.size.height > 0); + TEST_ASSERT_TRUE(adapt.scale() > 0); +} + +TEST(WinAdapt, VeryLargeWindow) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(7680, 4320); + + Rect vp = adapt.viewport(); + + TEST_ASSERT_TRUE(vp.size.width > 0); + TEST_ASSERT_TRUE(vp.size.height > 0); + TEST_ASSERT_TRUE(adapt.scale() > 1.0f); +} + +// --------------------------------------------------------------------------- +// 不同宽高比测试 +// --------------------------------------------------------------------------- + +TEST(WinAdapt, AspectRatioPreservation) { + WinAdapt adapt; + WinAdaptCfg cfg; + cfg.mode = AdaptMode::Fit; + cfg.refW = 1280; + cfg.refH = 720; + adapt.cfg(cfg); + + adapt.win(2560, 1440); + + Rect vp = adapt.viewport(); + + f32 refAspect = 1280.0f / 720.0f; + f32 vpAspect = vp.size.width / vp.size.height; + + TEST_ASSERT_TRUE(std::abs(refAspect - vpAspect) < EPSILON); +} diff --git a/xmake.lua b/xmake.lua index cacb449..ce73c1c 100644 --- a/xmake.lua +++ b/xmake.lua @@ -119,6 +119,14 @@ target("extra2d_tests") add_files("Tests/test_asset_pack.cpp") add_files("Tests/test_asset_service.cpp") + -- 渲染系统测试 + add_files("Tests/test_node.cpp") + add_files("Tests/test_scene_graph.cpp") + add_files("Tests/test_camera.cpp") + add_files("Tests/test_win_adapt.cpp") + add_files("Tests/test_action.cpp") + add_files("Tests/test_scene.cpp") + -- 头文件路径 add_includedirs("Extra2D/include", {public = true}) add_includedirs("Tests") @@ -132,12 +140,29 @@ target("extra2d_tests") if plat == "mingw" or plat == "windows" then add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") add_syslinks("winmm", "imm32", "version", "setupapi") + + -- Vulkan SDK + local vulkanSdk = os.getenv("VULKAN_SDK") + if vulkanSdk then + add_includedirs(vulkanSdk .. "/Include", {public = true}) + add_linkdirs(vulkanSdk .. "/Lib", {public = true}) + add_links("vulkan-1", {public = true}) + end elseif plat == "linux" then add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") add_syslinks("dl", "pthread") + add_links("vulkan", {public = true}) elseif plat == "macosx" then add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib") add_frameworks("Cocoa", "IOKit", "CoreVideo") + + -- Vulkan SDK (MoltenVK) + local vulkanSdk = os.getenv("VULKAN_SDK") + if vulkanSdk then + add_includedirs(vulkanSdk .. "/include", {public = true}) + add_linkdirs(vulkanSdk .. "/lib", {public = true}) + add_links("vulkan", {public = true}) + end end -- 编译器标志 diff --git a/xmake/engine.lua b/xmake/engine.lua index e55683d..9b0ed09 100644 --- a/xmake/engine.lua +++ b/xmake/engine.lua @@ -3,7 +3,7 @@ -- 被主项目和示例共享使用 -- -- 窗口后端: GLFW --- 渲染后端: OpenGL +-- 渲染后端: Vulkan -- ============================================== -- 获取当前平台 @@ -41,14 +41,31 @@ function define_extra2d_engine() -- Windows (MinGW) 平台配置 add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_syslinks("winmm", "imm32", "version", "setupapi", {public = true}) + + -- Vulkan SDK (使用系统安装的Vulkan SDK) + local vulkanSdk = os.getenv("VULKAN_SDK") + if vulkanSdk then + add_includedirs(vulkanSdk .. "/Include", {public = true}) + add_linkdirs(vulkanSdk .. "/Lib", {public = true}) + add_links("vulkan-1", {public = true}) + end elseif plat == "linux" then -- Linux 平台配置 add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_syslinks("dl", "pthread", {public = true}) + add_links("vulkan", {public = true}) elseif plat == "macosx" then -- macOS 平台配置 add_packages("glm", "nlohmann_json", "glfw", "zstd", "lz4", "zlib", {public = true}) add_frameworks("Cocoa", "IOKit", "CoreVideo", {public = true}) + + -- Vulkan SDK (MoltenVK) + local vulkanSdk = os.getenv("VULKAN_SDK") + if vulkanSdk then + add_includedirs(vulkanSdk .. "/include", {public = true}) + add_linkdirs(vulkanSdk .. "/lib", {public = true}) + add_links("vulkan", {public = true}) + end end -- 启用压缩库