feat: 添加2D渲染系统核心组件

新增2D渲染系统核心组件,包括节点系统、场景管理、相机服务、窗口适配、动作系统等。主要变更如下:

1. 实现节点系统基础架构(Node, SceneGraph)
2. 添加多种渲染组件(Sprite, Text, Shape)
3. 实现相机系统(Camera, OrthoCam)及相机服务
4. 添加窗口适配服务(WindowAdapt)
5. 实现完整的动作系统(Act)及其派生类
6. 添加场景过渡效果(Trans)
7. 集成Vulkan渲染后端支持
8. 实现着色器管理系统(ShaderMgr)
9. 添加动态着色器支持(DynShader)
10. 完善数学工具类(MathExtended)
11. 更新构建系统支持Vulkan
12. 添加相关测试用例
This commit is contained in:
ChestnutYueyue 2026-02-21 06:00:02 +08:00
parent 6a12bb5e2e
commit d1d03520ff
60 changed files with 7515 additions and 4 deletions

View File

@ -0,0 +1,106 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/ref_counted.h>
#include <extra2d/core/math_types.h>
#include <functional>
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<f32(f32)>;
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;
};
}

View File

@ -0,0 +1,135 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <vector>
#include <initializer_list>
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<typename... As>
static Seq* create(As... as) {
std::initializer_list<Act*> acts = {static_cast<Act*>(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<Act*> acts_;
size_t curIdx_ = 0;
f32 split_ = 0.0f;
static Seq* createFromArray(std::initializer_list<Act*> acts);
};
class Spawn : public ActInterval {
public:
Spawn(Act* a1, Act* a2);
template<typename... As>
static Spawn* create(As... as) {
std::initializer_list<Act*> acts = {static_cast<Act*>(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<Act*> acts_;
static Spawn* createFromArray(std::initializer_list<Act*> 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<ActInterval*>(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<ActInterval*>(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;
};
}

View File

@ -0,0 +1,99 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <extra2d/node/renderer.h>
#include <extra2d/core/color.h>
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<Renderer>()) {
start_ = r->getOpacity();
}
}
}
void update(f32 t) override {
if (target_) {
if (auto* r = target_->get<Renderer>()) {
u8 opacity = static_cast<u8>(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<Renderer>()) {
}
}
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<int>(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;
};
}

View File

@ -0,0 +1,80 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <extra2d/core/color.h>
namespace extra2d {
class CallFunc : public ActInstant {
public:
explicit CallFunc(std::function<void()> 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<void()> 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;
};
}

View File

@ -0,0 +1,113 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <extra2d/core/color.h>
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_;
};
}

View File

@ -0,0 +1,61 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <extra2d/core/math_types.h>
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;
};
}

View File

@ -0,0 +1,57 @@
#pragma once
#include <extra2d/act/act.h>
#include <extra2d/node/node.h>
#include <extra2d/core/math_types.h>
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_;
};
}

View File

@ -1,6 +1,6 @@
#pragma once
#include <extra2d/asset/asset_types.h>
#include <extra2d/asset/asset.h>
#include <extra2d/core/types.h>
#include <memory>

View File

@ -0,0 +1,61 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/math_extended.h>
#include <glm/mat4x4.hpp>
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;
};
}

View File

@ -0,0 +1,121 @@
#pragma once
#include <extra2d/core/service_interface.h>
#include <extra2d/cam/cam.h>
#include <extra2d/cam/ortho_cam.h>
#include <string>
#include <unordered_map>
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<Camera> cam;
if (t == CamType::Ortho) {
cam = ptr::makeUnique<OrthoCam>();
} 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<std::string, Unique<Camera>> cameras_;
std::string mainName_;
};
}

View File

@ -0,0 +1,86 @@
#pragma once
#include <extra2d/cam/cam.h>
#include <glm/gtc/matrix_transform.hpp>
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;
};
}

View File

@ -0,0 +1,106 @@
#pragma once
#include <extra2d/core/types.h>
#include <glm/mat3x3.hpp>
#include <glm/mat4x4.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/vec4.hpp>
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;
}
}

View File

@ -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};
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <extra2d/core/types.h>
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;
};
}

View File

@ -6,9 +6,13 @@
// Core
#include <extra2d/core/color.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/math_extended.h>
#include <extra2d/core/module.h>
#include <extra2d/core/registry.h>
#include <extra2d/core/types.h>
#include <extra2d/core/ref_counted.h>
#include <extra2d/core/service_interface.h>
#include <extra2d/core/service_locator.h>
// Platform
#include <extra2d/platform/glfw/glfw_window.h>
@ -39,6 +43,45 @@
#include <extra2d/asset/asset_types.h>
#include <extra2d/asset/data_processor.h>
// Node
#include <extra2d/node/node.h>
#include <extra2d/node/scene_graph.h>
#include <extra2d/node/renderer.h>
#include <extra2d/node/sprite.h>
#include <extra2d/node/text.h>
#include <extra2d/node/shape.h>
// Camera
#include <extra2d/cam/cam.h>
#include <extra2d/cam/ortho_cam.h>
#include <extra2d/cam/cam_svc.h>
// Window
#include <extra2d/win/win_adapt.h>
#include <extra2d/win/win_adapt_svc.h>
// Render
#include <extra2d/render/render_types.h>
#include <extra2d/render/render_svc.h>
#include <extra2d/render/render_mod.h>
#include <extra2d/render/shader_mgr.h>
#include <extra2d/render/shader_sdf.h>
#include <extra2d/render/dyn_shader.h>
// Action
#include <extra2d/act/act.h>
#include <extra2d/act/act_instant.h>
#include <extra2d/act/act_move.h>
#include <extra2d/act/act_rotate.h>
#include <extra2d/act/act_scale.h>
#include <extra2d/act/act_fade.h>
#include <extra2d/act/act_composite.h>
// Scene
#include <extra2d/scene/scene.h>
#include <extra2d/scene/director.h>
#include <extra2d/scene/trans.h>
// Application
#include <extra2d/app/application.h>

View File

@ -0,0 +1,177 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/math_extended.h>
#include <algorithm>
#include <vector>
#include <memory>
#include <functional>
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<Node> {
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<typename T, typename... Args>
T* add(Args&&... args) {
auto c = std::make_unique<T>(std::forward<Args>(args)...);
T* p = c.get();
addInternal(std::move(c));
return p;
}
template<typename T>
T* get() {
for (auto& c : comps_) {
if (auto* p = dynamic_cast<T*>(c.get())) {
return p;
}
}
return nullptr;
}
template<typename T>
const T* get() const {
for (auto& c : comps_) {
if (auto* p = dynamic_cast<const T*>(c.get())) {
return p;
}
}
return nullptr;
}
template<typename T>
void remove() {
comps_.erase(
std::remove_if(comps_.begin(), comps_.end(),
[](const auto& c) { return dynamic_cast<T*>(c.get()) != nullptr; }),
comps_.end()
);
}
void updateComps(f32 dt);
void lateUpdateComps(f32 dt);
void addChild(Ref<Node> child);
void removeChild(Ref<Node> child);
void removeFromParent();
Node* parent() const { return parent_; }
const std::vector<Ref<Node>>& 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<Unique<Comp>> comps_;
Node* parent_ = nullptr;
std::vector<Ref<Node>> children_;
std::string name_;
bool visible_ = true;
i32 tag_ = 0;
std::vector<Act*> acts_;
void addInternal(Unique<Comp> c);
};
inline void Comp::setEnabled(bool e) {
if (enabled_ != e) {
enabled_ = e;
if (enabled_) {
onEnable();
} else {
onDisable();
}
}
}
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <extra2d/node/node.h>
#include <extra2d/core/math_extended.h>
#include <extra2d/render/render_types.h>
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));
}
};
}

View File

@ -0,0 +1,50 @@
#pragma once
#include <extra2d/node/node.h>
#include <extra2d/core/types.h>
#include <functional>
#include <string>
namespace extra2d {
class SceneGraph {
public:
SceneGraph();
~SceneGraph();
template<typename T, typename... Args>
Ref<T> create(Args&&... args) {
auto node = ptr::make<T>(std::forward<Args>(args)...);
node->onInit();
nodes_.push_back(node);
return node;
}
template<typename T>
Ref<T> create() {
auto node = ptr::make<T>();
node->onInit();
nodes_.push_back(node);
return node;
}
void add(Ref<Node> node);
void remove(Ref<Node> node);
void clear();
Node* find(const std::string& name) const;
Node* findByTag(i32 tag) const;
void traverse(const std::function<void(Node*)>& callback);
void traverseRecursive(Node* node, const std::function<void(Node*)>& callback);
void update(f32 dt);
const std::vector<Ref<Node>>& roots() const { return nodes_; }
size_t nodeCount() const { return nodes_.size(); }
private:
std::vector<Ref<Node>> nodes_;
};
}

View File

@ -0,0 +1,86 @@
#pragma once
#include <extra2d/node/renderer.h>
#include <extra2d/core/color.h>
#include <vector>
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<Vec2> 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<Vec2>& 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<Vertex2D> createCircleVertices(f32 cx, f32 cy, f32 r, u32 segments, const Color& c);
std::vector<u32> createCircleIndices(u32 baseIndex, u32 segments);
};
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <extra2d/node/renderer.h>
#include <extra2d/core/color.h>
#include <extra2d/asset/asset_handle.h>
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<TextureAsset> 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);
};
}

View File

@ -0,0 +1,55 @@
#pragma once
#include <extra2d/node/renderer.h>
#include <extra2d/core/color.h>
#include <extra2d/asset/asset_handle.h>
#include <string>
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<FontAsset> 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);
};
}

View File

@ -0,0 +1,80 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/render/render_svc.h>
#include <extra2d/render/backend/vulkan/vk_mem.h>
#include <vulkan/vulkan.h>
#include <vector>
#include <unordered_map>
namespace extra2d {
struct SwapchainSupport {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> 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<VkImage>& swapchainImages() const { return swapchainImages_; }
const std::vector<VkImageView>& 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<VkImage> swapchainImages_;
std::vector<VkImageView> swapchainViews_;
VkCommandPool cmdPool_ = VK_NULL_HANDLE;
std::vector<VkCommandBuffer> cmdBuffers_;
VkMemAlloc memAlloc_;
u32 graphicsFamily_ = 0;
u32 presentFamily_ = 0;
u32 frameIdx_ = 0;
SwapchainSupport querySwapchainSupport(VkPhysicalDevice device);
VkSurfaceFormatKHR chooseSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats);
VkPresentModeKHR choosePresentMode(const std::vector<VkPresentModeKHR>& modes);
VkExtent2D chooseExtent(const VkSurfaceCapabilitiesKHR& caps, u32 width, u32 height);
};
}

View File

@ -0,0 +1,117 @@
#pragma once
#include <extra2d/core/types.h>
#include <vulkan/vulkan.h>
#include <vma/vk_mem_alloc.h>
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_;
};
}

View File

@ -0,0 +1,71 @@
#pragma once
#include <extra2d/render/shader_mgr.h>
#include <extra2d/core/types.h>
#include <unordered_map>
#include <string>
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<std::string, MatShader> shaders_;
std::string cur_;
std::string prev_;
f32 blend_ = 1.0f;
f32 blendSpeed_ = 0.0f;
};
}

View File

@ -0,0 +1,41 @@
#pragma once
#include <extra2d/core/module.h>
#include <extra2d/render/render_svc.h>
#include <extra2d/render/render_types.h>
#include <extra2d/render/shader_mgr.h>
#include <extra2d/render/shader_sdf.h>
#include <extra2d/render/dyn_shader.h>
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_;
};
}

View File

@ -0,0 +1,51 @@
#pragma once
#include <extra2d/core/service_interface.h>
#include <extra2d/render/render_types.h>
#include <extra2d/core/math_extended.h>
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;
}
};
}

View File

@ -0,0 +1,121 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/color.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/math_extended.h>
#include <extra2d/node/node.h>
#include <glm/mat4x4.hpp>
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<u8> 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<Vertex2D>& verts) {
u32 offset = static_cast<u32>(vertices_.size());
vertices_.insert(vertices_.end(), verts.begin(), verts.end());
return offset;
}
u32 addIndices(const std::vector<u32>& indices) {
u32 offset = static_cast<u32>(indices_.size());
indices_.insert(indices_.end(), indices.begin(), indices.end());
return offset;
}
const std::vector<RenderCmd>& cmds() const { return cmds_; }
const std::vector<Vertex2D>& vertices() const { return vertices_; }
const std::vector<u32>& indices() const { return indices_; }
std::vector<RenderCmd>& cmds() { return cmds_; }
std::vector<Vertex2D>& vertices() { return vertices_; }
std::vector<u32>& indices() { return indices_; }
private:
std::vector<RenderCmd> cmds_;
std::vector<Vertex2D> vertices_;
std::vector<u32> 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<u32> bindingIndices;
std::vector<u32> descriptorCounts;
};
}

View File

@ -0,0 +1,100 @@
#pragma once
#include <extra2d/core/types.h>
#include <string>
#include <vector>
#include <unordered_map>
#include <functional>
#include <filesystem>
namespace extra2d {
enum class ShaderType {
Vert,
Frag,
Geom,
Comp
};
struct ShaderDef {
std::string name;
ShaderType type;
std::string entry = "main";
std::vector<u8> spirv;
std::string src;
std::string path;
};
using ShaderReloadCb = std::function<void(const std::string& name)>;
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<u8>& 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<u8>& getSpirv(const std::string& name) const {
static std::vector<u8> 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<std::string, ShaderDef> shaders_;
std::unordered_map<std::string, std::filesystem::file_time_type> 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();
}
};
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/color.h>
#include <string>
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;
}
}

View File

@ -0,0 +1,174 @@
#pragma once
#include <extra2d/core/service_interface.h>
#include <extra2d/scene/scene.h>
#include <stack>
namespace extra2d {
class Trans;
class IDirector : public IService {
public:
virtual void run(Ref<Scene> s) = 0;
virtual void replace(Ref<Scene> s) = 0;
virtual void push(Ref<Scene> s) = 0;
virtual void pop() = 0;
virtual void popToRoot() = 0;
virtual void popTo(const std::string& n) = 0;
virtual void replaceWith(Ref<Scene> s, Ref<Scene> t) = 0;
virtual void pushWith(Ref<Scene> s, Ref<Scene> 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<Scene> s) override {
if (curScene_) {
curScene_->onExit();
curScene_->setRunning(false);
}
sceneStack_.clear();
curScene_ = s;
if (curScene_) {
curScene_->onEnter();
curScene_->setRunning(true);
}
}
void replace(Ref<Scene> 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<Scene> 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<Scene> s, Ref<Scene> t) override {
transScene_ = t;
nextScene_ = s;
}
void pushWith(Ref<Scene> s, Ref<Scene> 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<Ref<Scene>> sceneStack_;
Ref<Scene> curScene_;
Ref<Scene> nextScene_;
Ref<Scene> transScene_;
bool isPush_ = false;
};
}

View File

@ -0,0 +1,50 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/node/scene_graph.h>
#include <extra2d/cam/cam.h>
#include <extra2d/cam/ortho_cam.h>
#include <extra2d/render/render_types.h>
#include <string>
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<SceneGraph> graph_;
Unique<Camera> cam_;
std::string name_;
bool running_ = false;
Scene() {
graph_ = ptr::makeUnique<SceneGraph>();
cam_ = ptr::makeUnique<OrthoCam>();
}
};
}

View File

@ -0,0 +1,117 @@
#pragma once
#include <extra2d/scene/scene.h>
#include <extra2d/core/color.h>
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<Scene> inScene, Ref<Scene> outScene) {
duration_ = duration;
inScene_ = inScene;
outScene_ = outScene;
}
protected:
Ref<Scene> inScene_;
Ref<Scene> outScene_;
f32 duration_ = 0.0f;
f32 progress_ = 0.0f;
bool inOnTop_ = false;
};
class TransFade : public Trans {
public:
void init(f32 duration, Ref<Scene> inScene, Ref<Scene> 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;
};
}

View File

@ -0,0 +1,146 @@
#pragma once
#include <extra2d/core/types.h>
#include <extra2d/core/color.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/math_extended.h>
#include <glm/mat4x4.hpp>
#include <glm/gtc/matrix_transform.hpp>
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_;
};
}

View File

@ -0,0 +1,97 @@
#pragma once
#include <extra2d/core/service_interface.h>
#include <extra2d/win/win_adapt.h>
#include <extra2d/cam/cam.h>
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_;
};
}

18
Extra2D/src/act/act.cpp Normal file
View File

@ -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) {
}
}

View File

@ -0,0 +1,299 @@
#include "extra2d/act/act_composite.h"
#include <algorithm>
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<ActInterval*>(a)) {
totalDur += interval->dur();
}
}
dur_ = totalDur;
}
Seq* Seq::createFromArray(std::initializer_list<Act*> 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<ActInterval*>(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<ActInterval*>(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<ActInterval*>(a)) {
maxDur = std::max(maxDur, interval->dur());
}
}
dur_ = maxDur;
}
Spawn* Spawn::createFromArray(std::initializer_list<Act*> 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<ActInterval*>(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<ActInterval*>(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<ActInterval*>(a)) {
dur_ = interval->dur() * times;
}
}
}
Repeat::Repeat(Act* a) : ActInterval(0), times_(0), inf_(true) {
if (a) {
inner_ = a;
if (auto* interval = dynamic_cast<ActInterval*>(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<ActInterval*>(inner_)) {
f32 localT = t * times_;
int newCnt = static_cast<int>(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<ActInterval*>(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<ActInterval*>(inner_)) {
interval->step(innerDt);
} else {
inner_->step(innerDt);
}
}
}

View File

@ -0,0 +1,5 @@
#include "extra2d/cam/cam_svc.h"
namespace extra2d {
}

197
Extra2D/src/node/node.cpp Normal file
View File

@ -0,0 +1,197 @@
#define GLM_ENABLE_EXPERIMENTAL
#include "extra2d/node/node.h"
#include "extra2d/act/act.h"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/matrix_transform_2d.hpp>
#include <algorithm>
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<Comp> 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<Node> child) {
if (child && child.get() != this) {
if (child->parent_) {
child->removeFromParent();
}
child->parent_ = this;
children_.push_back(child);
}
}
void Node::removeChild(Ref<Node> 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<i32>(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;
}
}
}
}

View File

@ -0,0 +1,82 @@
#include "extra2d/node/scene_graph.h"
namespace extra2d {
SceneGraph::SceneGraph() = default;
SceneGraph::~SceneGraph() {
clear();
}
void SceneGraph::add(Ref<Node> node) {
if (node) {
nodes_.push_back(node);
}
}
void SceneGraph::remove(Ref<Node> 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<void(Node*)>& callback) {
for (auto& node : nodes_) {
traverseRecursive(node.get(), callback);
}
}
void SceneGraph::traverseRecursive(Node* node, const std::function<void(Node*)>& 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);
});
}
}

574
Extra2D/src/node/shape.cpp Normal file
View File

@ -0,0 +1,574 @@
#include "extra2d/node/shape.h"
#include "extra2d/cam/cam.h"
#include <cmath>
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<Vertex2D> 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<u32> 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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> 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<u32> 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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> 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<u32> 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<Vertex2D> 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<u32> indices;
u32 totalPoints = static_cast<u32>(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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> verts;
Vec2 center(0, 0);
for (const auto& p : points) {
center += p;
}
center /= static_cast<f32>(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<u32> indices;
u32 n = static_cast<u32>(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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> 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<u32> 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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> 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<u32> 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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(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<Vertex2D> 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<u32> 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<u32>(verts.size());
cmd.indexOffset = indexOffset;
cmd.indexCount = static_cast<u32>(indices.size());
cmdBuffer.addCmd(cmd);
}
std::vector<Vertex2D> Shape::createCircleVertices(f32 cx, f32 cy, f32 r, u32 segments, const Color& c) {
std::vector<Vertex2D> 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<u32> Shape::createCircleIndices(u32 baseIndex, u32 segments) {
std::vector<u32> 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<Vec2>& 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;
}
}

View File

@ -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<Vertex2D> 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<u32> 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;
}
}

129
Extra2D/src/node/text.cpp Normal file
View File

@ -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<Vertex2D> 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<u32> 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;
}
}

View File

@ -0,0 +1,253 @@
#include "extra2d/render/backend/vulkan/vk_backend.h"
#include <algorithm>
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<u32>(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<u32>(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<VkSurfaceFormatKHR>& 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<VkPresentModeKHR>& 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;
}
}

View File

@ -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<char*>(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<const char*>(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;
}
}
}

View File

@ -0,0 +1,5 @@
#include "extra2d/render/render_mod.h"
namespace extra2d {
}

View File

@ -0,0 +1,93 @@
#include "extra2d/render/shader_mgr.h"
#include <fstream>
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<char>(file)),
std::istreambuf_iterator<char>());
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<u8>& 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);
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,5 @@
#include "extra2d/scene/director.h"
namespace extra2d {
}

View File

@ -0,0 +1,5 @@
#include "extra2d/scene/scene.h"
namespace extra2d {
}

View File

@ -0,0 +1,80 @@
#include "extra2d/scene/trans.h"
#include "extra2d/scene/director.h"
#include <cmath>
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;
}
}

View File

@ -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<OrthoCam*>(cam);
ortho->setOrthoFromSize(cfg_.refW, cfg_.refH);
ortho->viewport(vp_.origin.x, vp_.origin.y, vp_.size.width, vp_.size.height);
}
}
}

View File

@ -0,0 +1,5 @@
#include "extra2d/win/win_adapt_svc.h"
namespace extra2d {
}

622
Tests/test_action.cpp Normal file
View File

@ -0,0 +1,622 @@
/**
* @file test_action.cpp
* @brief Action
*
*
*/
#include "test_framework.h"
#include <extra2d/act/act.h>
#include <extra2d/act/act_instant.h>
#include <extra2d/act/act_move.h>
#include <extra2d/act/act_rotate.h>
#include <extra2d/act/act_scale.h>
#include <extra2d/act/act_fade.h>
#include <extra2d/act/act_composite.h>
#include <extra2d/node/node.h>
#include <extra2d/core/math_extended.h>
#include <cmath>
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<Node>();
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<Node>();
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>();
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>();
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>();
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>();
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>();
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<MoveTo*>(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<MoveBy*>(reversed);
TEST_ASSERT_NOT_NULL(reversedMove);
delete reversed;
}
// ---------------------------------------------------------------------------
// RotTo/RotBy 测试
// ---------------------------------------------------------------------------
TEST(RotTo, RotateToAngle) {
auto node = ptr::make<Node>();
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>();
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>();
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>();
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>();
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<Node>();
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<Node>();
FadeIn act(1.0f);
act.start(node.get());
act.update(1.0f);
}
TEST(FadeOut, FadeOut) {
auto node = ptr::make<Node>();
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>();
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>();
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>();
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>();
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>();
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>();
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>();
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>();
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>();
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<Node>();
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>();
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<Node>();
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>();
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>();
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);
}

328
Tests/test_camera.cpp Normal file
View File

@ -0,0 +1,328 @@
/**
* @file test_camera.cpp
* @brief Camera
*
*
*/
#include "test_framework.h"
#include <extra2d/cam/cam.h>
#include <extra2d/cam/ortho_cam.h>
#include <extra2d/core/math_extended.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
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));
}

412
Tests/test_node.cpp Normal file
View File

@ -0,0 +1,412 @@
/**
* @file test_node.cpp
* @brief Node Transform
*
*
*/
#include "test_framework.h"
#include <extra2d/node/node.h>
#include <extra2d/node/scene_graph.h>
#include <extra2d/core/math_extended.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
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<Node>();
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>();
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>();
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>();
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>();
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<Node>();
Mat3 local = node->local();
Mat3 identity(1.0f);
TEST_ASSERT_TRUE(mat3Equal(local, identity));
}
TEST(Node, LocalMatrixTranslation) {
auto node = ptr::make<Node>();
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>();
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>();
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>();
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<Node>();
auto child = ptr::make<Node>();
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<Node>();
auto child = ptr::make<Node>();
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<Node>();
auto child = ptr::make<Node>();
parent->addChild(child);
child->removeFromParent();
TEST_ASSERT_NULL(child->parent());
TEST_ASSERT_EQ(0u, parent->children().size());
}
TEST(Node, NestedHierarchy) {
auto root = ptr::make<Node>();
auto child1 = ptr::make<Node>();
auto child2 = ptr::make<Node>();
auto grandchild = ptr::make<Node>();
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<Node>();
parent->setPos(100, 100);
auto child = ptr::make<Node>();
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<Node>();
parent->setScale(2.0f, 2.0f);
auto child = ptr::make<Node>();
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<Node>();
parent->setRot(45.0f);
auto child = ptr::make<Node>();
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<Node>();
parent->setPos(100, 100);
auto child = ptr::make<Node>();
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<Node>();
TestComp* comp = node->add<TestComp>();
TEST_ASSERT_NOT_NULL(comp);
TEST_ASSERT_EQ(node.get(), comp->owner());
}
TEST(Node, GetComponent) {
auto node = ptr::make<Node>();
node->add<TestComp>();
TestComp* comp = node->get<TestComp>();
TEST_ASSERT_NOT_NULL(comp);
}
TEST(Node, RemoveComponent) {
auto node = ptr::make<Node>();
node->add<TestComp>();
node->remove<TestComp>();
TestComp* comp = node->get<TestComp>();
TEST_ASSERT_NULL(comp);
}
TEST(Node, UpdateComponents) {
auto node = ptr::make<Node>();
TestComp* comp = node->add<TestComp>();
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<Node>();
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<Node>();
TEST_ASSERT_EQ(0, node->tag());
node->setTag(42);
TEST_ASSERT_EQ(42, node->tag());
}
TEST(Node, Name) {
auto node = ptr::make<Node>();
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)));
}

330
Tests/test_scene.cpp Normal file
View File

@ -0,0 +1,330 @@
/**
* @file test_scene.cpp
* @brief Scene
*
* Director
*/
#include "test_framework.h"
#include <extra2d/scene/scene.h>
#include <extra2d/scene/director.h>
#include <extra2d/scene/trans.h>
#include <extra2d/node/node.h>
#include <extra2d/cam/ortho_cam.h>
#include <extra2d/act/act_move.h>
#include <extra2d/core/math_extended.h>
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<TestScene>();
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<TestScene>();
scene->setName("MyScene");
TEST_ASSERT_EQ("MyScene", scene->name());
}
TEST(Scene, SetRunning) {
auto scene = ptr::make<TestScene>();
scene->setRunning(true);
TEST_ASSERT_TRUE(scene->running());
scene->setRunning(false);
TEST_ASSERT_FALSE(scene->running());
}
TEST(Scene, LifecycleCallbacks) {
auto scene = ptr::make<TestScene>();
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<TestScene>();
auto node = scene->graph()->create<Node>();
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<TestScene>();
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<TestScene>();
scene1->setName("Scene1");
director.run(scene1);
auto scene2 = ptr::make<TestScene>();
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<TestScene>();
scene1->setName("Scene1");
director.run(scene1);
auto scene2 = ptr::make<TestScene>();
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<TestScene>();
director.run(scene);
auto node = scene->graph()->create<Node>();
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<TestScene>();
scene1->setName("Scene1");
director.run(scene1);
auto scene2 = ptr::make<TestScene>();
scene2->setName("Scene2");
director.push(scene2);
auto scene3 = ptr::make<TestScene>();
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<TestScene>();
auto scene2 = ptr::make<TestScene>();
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<TestScene>();
auto scene2 = ptr::make<TestScene>();
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<TestScene>();
auto parent = scene->graph()->create<Node>();
parent->setName("Parent");
parent->setPos(100, 100);
auto child = ptr::make<Node>();
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<TestScene>();
auto cam = dynamic_cast<OrthoCam*>(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<TestScene>();
scene1->setName("Scene1");
director.run(scene1);
for (int i = 0; i < 5; ++i) {
auto newScene = ptr::make<TestScene>();
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<RenderTestScene>();
RenderCmdBuffer cmdBuffer;
scene->render(cmdBuffer);
TEST_ASSERT_TRUE(scene->renderCalled);
}
// ---------------------------------------------------------------------------
// 边界情况测试
// ---------------------------------------------------------------------------
TEST(Director, ReplaceWhenNoRunning) {
Director director;
auto scene = ptr::make<TestScene>();
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<TestScene>();
director.push(scene);
TEST_ASSERT_EQ(scene.get(), director.cur());
}

323
Tests/test_scene_graph.cpp Normal file
View File

@ -0,0 +1,323 @@
/**
* @file test_scene_graph.cpp
* @brief SceneGraph
*
*
*/
#include "test_framework.h"
#include <extra2d/node/node.h>
#include <extra2d/node/scene_graph.h>
#include <extra2d/core/math_extended.h>
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<Node>();
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<Node>();
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<Node>();
graph.remove(node);
TEST_ASSERT_EQ(0u, graph.nodeCount());
}
TEST(SceneGraph, Clear) {
SceneGraph graph;
graph.create<Node>();
graph.create<Node>();
graph.create<Node>();
TEST_ASSERT_EQ(3u, graph.nodeCount());
graph.clear();
TEST_ASSERT_EQ(0u, graph.nodeCount());
}
// ---------------------------------------------------------------------------
// 节点查找测试
// ---------------------------------------------------------------------------
TEST(SceneGraph, FindByName) {
SceneGraph graph;
auto node1 = graph.create<Node>();
node1->setName("Node1");
auto node2 = graph.create<Node>();
node2->setName("Node2");
auto node3 = graph.create<Node>();
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<Node>()->setName("Node1");
graph.create<Node>()->setName("Node2");
Node* found = graph.find("NonExistent");
TEST_ASSERT_NULL(found);
}
TEST(SceneGraph, FindByTag) {
SceneGraph graph;
auto node1 = graph.create<Node>();
node1->setTag(1);
auto node2 = graph.create<Node>();
node2->setTag(2);
auto node3 = graph.create<Node>();
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<Node>()->setTag(1);
graph.create<Node>()->setTag(2);
Node* found = graph.findByTag(999);
TEST_ASSERT_NULL(found);
}
// ---------------------------------------------------------------------------
// 遍历测试
// ---------------------------------------------------------------------------
TEST(SceneGraph, Traverse) {
SceneGraph graph;
auto node1 = graph.create<Node>();
node1->setName("Node1");
auto node2 = graph.create<Node>();
node2->setName("Node2");
auto child = ptr::make<Node>();
child->setName("Child");
node1->addChild(child);
std::vector<std::string> 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<Node>();
root->setName("Root");
auto child1 = ptr::make<Node>();
child1->setName("Child1");
root->addChild(child1);
auto grandchild = ptr::make<Node>();
grandchild->setName("Grandchild");
child1->addChild(grandchild);
std::vector<std::string> 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<TestUpdateNode>();
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<TestUpdateNode>();
auto node2 = graph.create<TestUpdateNode>();
auto node3 = graph.create<TestUpdateNode>();
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<Node>();
parent->setName("Parent");
auto child1 = ptr::make<Node>();
child1->setName("Child1");
parent->addChild(child1);
auto child2 = ptr::make<Node>();
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<Node>();
root->setPos(100, 100);
auto level1 = ptr::make<Node>();
level1->setPos(50, 50);
root->addChild(level1);
auto level2 = ptr::make<Node>();
level2->setPos(25, 25);
level1->addChild(level2);
auto level3 = ptr::make<Node>();
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<CustomNode>(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<Node>();
TEST_ASSERT_TRUE(node->visible());
node->setVisible(false);
TEST_ASSERT_FALSE(node->visible());
}

436
Tests/test_win_adapt.cpp Normal file
View File

@ -0,0 +1,436 @@
/**
* @file test_win_adapt.cpp
* @brief Window Adaptation
*
*
*/
#include "test_framework.h"
#include <extra2d/win/win_adapt.h>
#include <extra2d/core/math_extended.h>
#include <glm/glm.hpp>
#include <cmath>
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);
}

View File

@ -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
-- 编译器标志

View File

@ -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
-- 启用压缩库