feat(视口适配): 添加视口适配器功能并集成到相机和输入系统

实现视口适配器功能,支持多种适配模式(宽高比、拉伸、居中、自定义)
将视口适配器集成到相机系统,实现坐标转换和视口自动调整
将视口适配器集成到输入系统,支持逻辑坐标转换
移除不再使用的字符串转换工具类
优化相机矩阵计算,支持旋转和缩放
添加数学工具函数,包括角度处理和坐标转换
This commit is contained in:
ChestnutYueyue 2026-02-14 18:58:24 +08:00
parent 2767d64bf8
commit 93d07e547f
13 changed files with 1091 additions and 70 deletions

View File

@ -15,6 +15,7 @@ class TimerManager;
class EventQueue; class EventQueue;
class EventDispatcher; class EventDispatcher;
class Camera; class Camera;
class ViewportAdapter;
// ============================================================================ // ============================================================================
// Application 配置 // Application 配置
@ -80,6 +81,12 @@ public:
EventDispatcher &eventDispatcher(); EventDispatcher &eventDispatcher();
Camera &camera(); Camera &camera();
/**
* @brief
* @return
*/
ViewportAdapter &viewportAdapter();
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 便捷方法 // 便捷方法
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -115,6 +122,7 @@ private:
UniquePtr<EventQueue> eventQueue_; UniquePtr<EventQueue> eventQueue_;
UniquePtr<EventDispatcher> eventDispatcher_; UniquePtr<EventDispatcher> eventDispatcher_;
UniquePtr<Camera> camera_; UniquePtr<Camera> camera_;
UniquePtr<ViewportAdapter> viewportAdapter_;
// 状态 // 状态
bool initialized_ = false; bool initialized_ = false;

View File

@ -330,6 +330,221 @@ inline float degrees(float radians) { return radians * RAD_TO_DEG; }
inline float radians(float degrees) { return degrees * DEG_TO_RAD; } inline float radians(float degrees) { return degrees * DEG_TO_RAD; }
// ---------------------------------------------------------------------------
// 角度工具函数
// ---------------------------------------------------------------------------
/**
* @brief [0, 360)
* @param degrees
* @return [0, 360)
*/
inline float normalizeAngle360(float degrees) {
degrees = std::fmod(degrees, 360.0f);
if (degrees < 0.0f) {
degrees += 360.0f;
}
return degrees;
}
/**
* @brief [-180, 180)
* @param degrees
* @return [-180, 180)
*/
inline float normalizeAngle180(float degrees) {
degrees = std::fmod(degrees + 180.0f, 360.0f);
if (degrees < 0.0f) {
degrees += 360.0f;
}
return degrees - 180.0f;
}
/**
* @brief
* @param from
* @param to
* @return from to [-180, 180]
*/
inline float angleDifference(float from, float to) {
float diff = normalizeAngle360(to - from);
if (diff > 180.0f) {
diff -= 360.0f;
}
return diff;
}
/**
* @brief 线
* @param from
* @param to
* @param t [0, 1]
* @return
*/
inline float lerpAngle(float from, float to, float t) {
return from + angleDifference(from, to) * t;
}
// ---------------------------------------------------------------------------
// 向量工具函数
// ---------------------------------------------------------------------------
/**
* @brief from to
* @param from
* @param to
* @return
*/
inline Vec2 direction(const Vec2 &from, const Vec2 &to) {
return (to - from).normalized();
}
/**
* @brief
* @param from
* @param to
* @return [-180, 180]
*/
inline float angleBetween(const Vec2 &from, const Vec2 &to) {
Vec2 dir = to - from;
return std::atan2(dir.y, dir.x) * RAD_TO_DEG;
}
/**
* @brief
* @param degrees 0
* @return
*/
inline Vec2 angleToVector(float degrees) {
float rad = degrees * DEG_TO_RAD;
return {std::cos(rad), std::sin(rad)};
}
/**
* @brief
* @param v
* @param degrees
* @return
*/
inline Vec2 rotateVector(const Vec2 &v, float degrees) {
float rad = degrees * DEG_TO_RAD;
float cosA = std::cos(rad);
float sinA = std::sin(rad);
return {v.x * cosA - v.y * sinA, v.x * sinA + v.y * cosA};
}
// ---------------------------------------------------------------------------
// 坐标系转换工具
// ---------------------------------------------------------------------------
/**
* @brief Y轴向上坐标转Y轴向下坐标
* @param pos Y轴向上坐标系中的位置
* @param height /
* @return Y轴向下坐标系中的位置
*/
inline Vec2 flipY(const Vec2 &pos, float height) {
return {pos.x, height - pos.y};
}
/**
* @brief Y轴向下坐标转Y轴向上坐标
* @param pos Y轴向下坐标系中的位置
* @param height /
* @return Y轴向上坐标系中的位置
*/
inline Vec2 unflipY(const Vec2 &pos, float height) {
return {pos.x, height - pos.y};
}
// ---------------------------------------------------------------------------
// 矩阵工具函数
// ---------------------------------------------------------------------------
/**
* @brief
* @param matrix 4x4变换矩阵
* @return
*/
inline Vec2 extractPosition(const glm::mat4 &matrix) {
return {matrix[3][0], matrix[3][1]};
}
/**
* @brief
* @param matrix 4x4变换矩阵
* @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]);
return {scaleX, scaleY};
}
/**
* @brief
* @param matrix 4x4变换矩阵
* @return
*/
inline float extractRotation(const glm::mat4 &matrix) {
return std::atan2(matrix[0][1], matrix[0][0]) * RAD_TO_DEG;
}
// ---------------------------------------------------------------------------
// 碰撞检测工具
// ---------------------------------------------------------------------------
/**
* @brief
* @param point
* @param rect
* @return true false
*/
inline bool pointInRect(const Vec2 &point, const Rect &rect) {
return point.x >= rect.left() && point.x <= rect.right() &&
point.y >= rect.top() && point.y <= rect.bottom();
}
/**
* @brief
* @param point
* @param center
* @param radius
* @return true false
*/
inline bool pointInCircle(const Vec2 &point, const Vec2 &center, float radius) {
float dx = point.x - center.x;
float dy = point.y - center.y;
return (dx * dx + dy * dy) <= (radius * radius);
}
/**
* @brief
* @param a
* @param b
* @return true false
*/
inline bool rectsIntersect(const Rect &a, const Rect &b) {
return a.intersects(b);
}
/**
* @brief
* @param center1
* @param radius1
* @param center2
* @param radius2
* @return true false
*/
inline bool circlesIntersect(const Vec2 &center1, float radius1,
const Vec2 &center2, float radius2) {
float dx = center2.x - center1.x;
float dy = center2.y - center1.y;
float distSq = dx * dx + dy * dy;
float radiusSum = radius1 + radius2;
return distSq <= (radiusSum * radiusSum);
}
} // namespace math } // namespace math
} // namespace extra2d } // namespace extra2d

View File

@ -20,6 +20,7 @@
#include <extra2d/graphics/texture.h> #include <extra2d/graphics/texture.h>
#include <extra2d/graphics/render_target.h> #include <extra2d/graphics/render_target.h>
#include <extra2d/graphics/viewport_adapter.h>
#include <extra2d/graphics/vram_manager.h> #include <extra2d/graphics/vram_manager.h>
// Scene // Scene

View File

@ -7,6 +7,8 @@
namespace extra2d { namespace extra2d {
class ViewportAdapter;
// ============================================================================ // ============================================================================
// 2D 正交相机 // 2D 正交相机
// ============================================================================ // ============================================================================
@ -65,6 +67,20 @@ public:
void clearBounds(); void clearBounds();
void clampToBounds(); void clampToBounds();
// ------------------------------------------------------------------------
// 视口适配器
// ------------------------------------------------------------------------
/**
* @brief
* @param adapter
*/
void setViewportAdapter(ViewportAdapter* adapter);
/**
* @brief
*/
void applyViewportAdapter();
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 快捷方法:看向某点 // 快捷方法:看向某点
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -83,6 +99,8 @@ private:
Rect bounds_; Rect bounds_;
bool hasBounds_ = false; bool hasBounds_ = false;
ViewportAdapter* viewportAdapter_ = nullptr;
mutable glm::mat4 viewMatrix_; mutable glm::mat4 viewMatrix_;
mutable glm::mat4 projMatrix_; mutable glm::mat4 projMatrix_;
mutable glm::mat4 vpMatrix_; mutable glm::mat4 vpMatrix_;

View File

@ -0,0 +1,332 @@
#pragma once
#include <extra2d/core/color.h>
#include <extra2d/core/math_types.h>
#include <extra2d/core/types.h>
#include <glm/mat4x4.hpp>
namespace extra2d {
// ============================================================================
// 视口适配模式枚举
// ============================================================================
enum class ViewportMode {
AspectRatio,
Stretch,
Center,
Custom
};
// ============================================================================
// 黑边位置枚举
// ============================================================================
enum class LetterboxPosition {
Center,
LeftTop,
RightTop,
LeftBottom,
RightBottom
};
// ============================================================================
// 视口配置结构体
// ============================================================================
struct ViewportConfig {
float logicWidth = 1920.0f;
float logicHeight = 1080.0f;
ViewportMode mode = ViewportMode::AspectRatio;
LetterboxPosition letterboxPosition = LetterboxPosition::Center;
Color letterboxColor = Colors::Black;
bool autoScaleInCenterMode = true;
float customScale = 1.0f;
Vec2 customOffset = Vec2::Zero();
Rect customViewport = Rect::Zero();
};
// ============================================================================
// 视口计算结果结构体
// ============================================================================
struct ViewportResult {
Rect viewport;
float scaleX = 1.0f;
float scaleY = 1.0f;
float uniformScale = 1.0f;
Vec2 offset;
bool hasLetterbox = false;
struct Letterbox {
Rect top;
Rect bottom;
Rect left;
Rect right;
} letterbox;
};
// ============================================================================
// 视口适配器类
// ============================================================================
class ViewportAdapter {
public:
ViewportAdapter();
ViewportAdapter(float logicWidth, float logicHeight);
~ViewportAdapter() = default;
// ------------------------------------------------------------------------
// 配置设置
// ------------------------------------------------------------------------
/**
* @brief
* @param config
*/
void setConfig(const ViewportConfig &config);
/**
* @brief
* @return
*/
const ViewportConfig &getConfig() const { return config_; }
/**
* @brief
* @param width
* @param height
*/
void setLogicSize(float width, float height);
/**
* @brief
* @param mode
*/
void setMode(ViewportMode mode);
/**
* @brief
* @param position
*/
void setLetterboxPosition(LetterboxPosition position);
/**
* @brief
* @param color
*/
void setLetterboxColor(const Color &color);
// ------------------------------------------------------------------------
// 更新和计算
// ------------------------------------------------------------------------
/**
* @brief
* @param screenWidth
* @param screenHeight
*/
void update(int screenWidth, int screenHeight);
/**
* @brief
* @return
*/
const ViewportResult &getResult() const { return result_; }
// ------------------------------------------------------------------------
// 坐标转换
// ------------------------------------------------------------------------
/**
* @brief
* @param screenPos
* @return
*/
Vec2 screenToLogic(const Vec2 &screenPos) const;
/**
* @brief
* @param logicPos
* @return
*/
Vec2 logicToScreen(const Vec2 &logicPos) const;
/**
* @brief
* @param x X坐标
* @param y Y坐标
* @return
*/
Vec2 screenToLogic(float x, float y) const;
/**
* @brief
* @param x X坐标
* @param y Y坐标
* @return
*/
Vec2 logicToScreen(float x, float y) const;
// ------------------------------------------------------------------------
// 矩阵获取
// ------------------------------------------------------------------------
/**
* @brief
* @return
*/
glm::mat4 getViewportMatrix() const;
/**
* @brief
* @return
*/
glm::mat4 getInverseViewportMatrix() const;
// ------------------------------------------------------------------------
// 区域检测
// ------------------------------------------------------------------------
/**
* @brief
* @param screenPos
* @return true
*/
bool isInViewport(const Vec2 &screenPos) const;
/**
* @brief
* @param screenPos
* @return true
*/
bool isInLetterbox(const Vec2 &screenPos) const;
// ------------------------------------------------------------------------
// Getter 方法
// ------------------------------------------------------------------------
/**
* @brief
* @return
*/
float getLogicWidth() const { return config_.logicWidth; }
/**
* @brief
* @return
*/
float getLogicHeight() const { return config_.logicHeight; }
/**
* @brief
* @return
*/
Size getLogicSize() const {
return Size(config_.logicWidth, config_.logicHeight);
}
/**
* @brief
* @return
*/
int getScreenWidth() const { return screenWidth_; }
/**
* @brief
* @return
*/
int getScreenHeight() const { return screenHeight_; }
/**
* @brief
* @return
*/
Size getScreenSize() const {
return Size(static_cast<float>(screenWidth_),
static_cast<float>(screenHeight_));
}
/**
* @brief X方向缩放比例
* @return X方向缩放比例
*/
float getScaleX() const { return result_.scaleX; }
/**
* @brief Y方向缩放比例
* @return Y方向缩放比例
*/
float getScaleY() const { return result_.scaleY; }
/**
* @brief
* @return
*/
float getUniformScale() const { return result_.uniformScale; }
/**
* @brief
* @return
*/
Vec2 getOffset() const { return result_.offset; }
/**
* @brief
* @return
*/
Rect getViewport() const { return result_.viewport; }
/**
* @brief
* @return true
*/
bool hasLetterbox() const { return result_.hasLetterbox; }
/**
* @brief
* @return
*/
const ViewportResult::Letterbox &getLetterbox() const {
return result_.letterbox;
}
private:
/**
* @brief
*/
void calculateAspectRatio();
/**
* @brief
*/
void calculateStretch();
/**
* @brief
*/
void calculateCenter();
/**
* @brief
*/
void calculateCustom();
/**
* @brief
*/
void calculateLetterbox();
/**
* @brief
* @param extraWidth
* @param extraHeight
*/
void applyLetterboxPosition(float extraWidth, float extraHeight);
ViewportConfig config_;
ViewportResult result_;
int screenWidth_ = 0;
int screenHeight_ = 0;
mutable glm::mat4 viewportMatrix_;
mutable glm::mat4 inverseViewportMatrix_;
mutable bool matrixDirty_ = true;
};
} // namespace extra2d

View File

@ -9,6 +9,8 @@
namespace extra2d { namespace extra2d {
class ViewportAdapter;
// ============================================================================ // ============================================================================
// 鼠标按钮枚举 // 鼠标按钮枚举
// ============================================================================ // ============================================================================
@ -81,6 +83,33 @@ public:
Vec2 getTouchPosition() const { return touchPosition_; } Vec2 getTouchPosition() const { return touchPosition_; }
int getTouchCount() const { return touchCount_; } int getTouchCount() const { return touchCount_; }
// ------------------------------------------------------------------------
// 视口适配器
// ------------------------------------------------------------------------
/**
* @brief
* @param adapter
*/
void setViewportAdapter(ViewportAdapter* adapter);
/**
* @brief
* @return
*/
Vec2 getMousePositionLogic() const;
/**
* @brief
* @return
*/
Vec2 getTouchPositionLogic() const;
/**
* @brief
* @return
*/
Vec2 getMouseDeltaLogic() const;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 便捷方法 // 便捷方法
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -122,6 +151,9 @@ private:
Vec2 prevTouchPosition_; Vec2 prevTouchPosition_;
int touchCount_; int touchCount_;
// 视口适配器
ViewportAdapter* viewportAdapter_;
// 映射键盘 keyCode 到 SDL GameController 按钮 (Switch 兼容模式) // 映射键盘 keyCode 到 SDL GameController 按钮 (Switch 兼容模式)
SDL_GameControllerButton mapKeyToButton(int keyCode) const; SDL_GameControllerButton mapKeyToButton(int keyCode) const;

View File

@ -4,6 +4,7 @@
#include <extra2d/event/event_queue.h> #include <extra2d/event/event_queue.h>
#include <extra2d/graphics/camera.h> #include <extra2d/graphics/camera.h>
#include <extra2d/graphics/render_backend.h> #include <extra2d/graphics/render_backend.h>
#include <extra2d/graphics/viewport_adapter.h>
#include <extra2d/graphics/vram_manager.h> #include <extra2d/graphics/vram_manager.h>
#include <extra2d/platform/input.h> #include <extra2d/platform/input.h>
#include <extra2d/platform/window.h> #include <extra2d/platform/window.h>
@ -139,12 +140,32 @@ bool Application::init(const AppConfig &config) {
camera_ = makeUnique<Camera>(0, static_cast<float>(window_->getWidth()), camera_ = makeUnique<Camera>(0, static_cast<float>(window_->getWidth()),
static_cast<float>(window_->getHeight()), 0); static_cast<float>(window_->getHeight()), 0);
// 创建视口适配器
viewportAdapter_ = makeUnique<ViewportAdapter>();
ViewportConfig vpConfig;
vpConfig.logicWidth = static_cast<float>(config.width);
vpConfig.logicHeight = static_cast<float>(config.height);
vpConfig.mode = ViewportMode::AspectRatio;
viewportAdapter_->setConfig(vpConfig);
// 关联到各子系统
camera_->setViewportAdapter(viewportAdapter_.get());
input().setViewportAdapter(viewportAdapter_.get());
// 初始更新
viewportAdapter_->update(window_->getWidth(), window_->getHeight());
// 窗口大小回调 // 窗口大小回调
window_->setResizeCallback([this](int width, int height) { window_->setResizeCallback([this](int width, int height) {
if (camera_) { // 更新视口适配器
camera_->setViewport(0, static_cast<float>(width), if (viewportAdapter_) {
static_cast<float>(height), 0); viewportAdapter_->update(width, height);
} }
if (camera_) {
camera_->applyViewportAdapter();
}
if (sceneManager_) { if (sceneManager_) {
auto currentScene = sceneManager_->getCurrentScene(); auto currentScene = sceneManager_->getCurrentScene();
if (currentScene) { if (currentScene) {
@ -204,6 +225,7 @@ void Application::shutdown() {
// ======================================== // ========================================
sceneManager_.reset(); // 场景持有纹理引用 sceneManager_.reset(); // 场景持有纹理引用
resourceManager_.reset(); // 纹理缓存持有 GPU 纹理 resourceManager_.reset(); // 纹理缓存持有 GPU 纹理
viewportAdapter_.reset(); // 视口适配器
camera_.reset(); // 相机可能持有渲染目标 camera_.reset(); // 相机可能持有渲染目标
// ======================================== // ========================================
@ -356,7 +378,16 @@ void Application::render() {
return; return;
} }
renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight()); // 应用视口适配器
if (viewportAdapter_) {
const auto &vp = viewportAdapter_->getViewport();
renderer_->setViewport(static_cast<int>(vp.origin.x),
static_cast<int>(vp.origin.y),
static_cast<int>(vp.size.width),
static_cast<int>(vp.size.height));
} else {
renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight());
}
if (sceneManager_) { if (sceneManager_) {
sceneManager_->render(*renderer_); sceneManager_->render(*renderer_);
@ -383,6 +414,8 @@ EventDispatcher &Application::eventDispatcher() { return *eventDispatcher_; }
Camera &Application::camera() { return *camera_; } Camera &Application::camera() { return *camera_; }
ViewportAdapter &Application::viewportAdapter() { return *viewportAdapter_; }
void Application::enterScene(Ptr<Scene> scene) { enterScene(scene, nullptr); } void Application::enterScene(Ptr<Scene> scene) { enterScene(scene, nullptr); }
void Application::enterScene(Ptr<Scene> scene, void Application::enterScene(Ptr<Scene> scene,

View File

@ -1,48 +0,0 @@
#include <extra2d/core/string.h>
#ifdef _WIN32
#include <windows.h>
namespace extra2d {
std::string utf8ToGbkImpl(const std::string& utf8) {
if (utf8.empty()) return std::string();
// UTF-8 → Wide → GBK
int wideLen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
if (wideLen <= 0) return std::string();
std::wstring wide(wideLen - 1, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &wide[0], wideLen);
int gbkLen = WideCharToMultiByte(CP_ACP, 0, wide.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (gbkLen <= 0) return std::string();
std::string gbk(gbkLen - 1, 0);
WideCharToMultiByte(CP_ACP, 0, wide.c_str(), -1, &gbk[0], gbkLen, nullptr, nullptr);
return gbk;
}
std::string gbkToUtf8Impl(const std::string& gbk) {
if (gbk.empty()) return std::string();
// GBK → Wide → UTF-8
int wideLen = MultiByteToWideChar(CP_ACP, 0, gbk.c_str(), -1, nullptr, 0);
if (wideLen <= 0) return std::string();
std::wstring wide(wideLen - 1, 0);
MultiByteToWideChar(CP_ACP, 0, gbk.c_str(), -1, &wide[0], wideLen);
int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (utf8Len <= 0) return std::string();
std::string utf8(utf8Len - 1, 0);
WideCharToMultiByte(CP_UTF8, 0, wide.c_str(), -1, &utf8[0], utf8Len, nullptr, nullptr);
return utf8;
}
} // namespace extra2d
#endif // _WIN32

View File

@ -1,6 +1,8 @@
#include <algorithm> #include <algorithm>
#include <extra2d/graphics/camera.h> #include <extra2d/graphics/camera.h>
#include <extra2d/graphics/viewport_adapter.h>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/matrix_inverse.hpp>
namespace extra2d { namespace extra2d {
@ -55,12 +57,34 @@ Rect Camera::getViewport() const {
return Rect(left_, top_, right_ - left_, bottom_ - top_); return Rect(left_, top_, right_ - left_, bottom_ - top_);
} }
/**
* @brief
* @return
*
* -> ->
* View = T(-position) × R(-rotation) × S(1/zoom)
*/
glm::mat4 Camera::getViewMatrix() const { glm::mat4 Camera::getViewMatrix() const {
if (viewDirty_) { if (viewDirty_) {
viewMatrix_ = glm::mat4(1.0f); viewMatrix_ = glm::mat4(1.0f);
// 对于2D相机我们只需要平移注意Y轴方向
// 1. 平移(最后应用)
viewMatrix_ = glm::translate(viewMatrix_, viewMatrix_ = glm::translate(viewMatrix_,
glm::vec3(-position_.x, -position_.y, 0.0f)); glm::vec3(-position_.x, -position_.y, 0.0f));
// 2. 旋转(中间应用)
if (rotation_ != 0.0f) {
viewMatrix_ = glm::rotate(viewMatrix_,
-rotation_ * DEG_TO_RAD,
glm::vec3(0.0f, 0.0f, 1.0f));
}
// 3. 缩放(最先应用)
if (zoom_ != 1.0f) {
viewMatrix_ = glm::scale(viewMatrix_,
glm::vec3(1.0f / zoom_, 1.0f / zoom_, 1.0f));
}
viewDirty_ = false; viewDirty_ = false;
} }
return viewMatrix_; return viewMatrix_;
@ -81,20 +105,49 @@ glm::mat4 Camera::getProjectionMatrix() const {
return projMatrix_; return projMatrix_;
} }
/**
* @brief -
* @return -
*/
glm::mat4 Camera::getViewProjectionMatrix() const { glm::mat4 Camera::getViewProjectionMatrix() const {
// 对于2D相机我们主要依赖投影矩阵 return getProjectionMatrix() * getViewMatrix();
// 视口变换已经处理了坐标系转换
return getProjectionMatrix();
} }
/**
* @brief
* @param screenPos
* @return
*/
Vec2 Camera::screenToWorld(const Vec2 &screenPos) const { Vec2 Camera::screenToWorld(const Vec2 &screenPos) const {
// 屏幕坐标直接映射到世界坐标在2D中通常相同 Vec2 logicPos = screenPos;
return screenPos;
// 如果有视口适配器,先转换到逻辑坐标
if (viewportAdapter_) {
logicPos = viewportAdapter_->screenToLogic(screenPos);
}
// 使用逆视图-投影矩阵转换
glm::mat4 invVP = glm::inverse(getViewProjectionMatrix());
glm::vec4 ndc(logicPos.x, logicPos.y, 0.0f, 1.0f);
glm::vec4 world = invVP * ndc;
return Vec2(world.x, world.y);
} }
/**
* @brief
* @param worldPos
* @return
*/
Vec2 Camera::worldToScreen(const Vec2 &worldPos) const { Vec2 Camera::worldToScreen(const Vec2 &worldPos) const {
// 世界坐标直接映射到屏幕坐标在2D中通常相同 glm::vec4 world(worldPos.x, worldPos.y, 0.0f, 1.0f);
return worldPos; glm::vec4 screen = getViewProjectionMatrix() * world;
Vec2 logicPos(screen.x, screen.y);
// 如果有视口适配器,转换到屏幕坐标
if (viewportAdapter_) {
return viewportAdapter_->logicToScreen(logicPos);
}
return logicPos;
} }
Vec2 Camera::screenToWorld(float x, float y) const { Vec2 Camera::screenToWorld(float x, float y) const {
@ -155,4 +208,22 @@ void Camera::lookAt(const Vec2 &target) {
viewDirty_ = true; viewDirty_ = true;
} }
/**
* @brief
* @param adapter
*/
void Camera::setViewportAdapter(ViewportAdapter* adapter) {
viewportAdapter_ = adapter;
}
/**
* @brief
*/
void Camera::applyViewportAdapter() {
if (viewportAdapter_) {
const auto& config = viewportAdapter_->getConfig();
setViewport(0.0f, config.logicWidth, config.logicHeight, 0.0f);
}
}
} // namespace extra2d } // namespace extra2d

View File

@ -1,12 +1,12 @@
#include <extra2d/graphics/opengl/gl_font_atlas.h> #include <extra2d/graphics/opengl/gl_font_atlas.h>
#include <extra2d/core/string.h>
#include <extra2d/utils/logger.h> #include <extra2d/utils/logger.h>
#include <fstream> #include <fstream>
#define STB_TRUETYPE_IMPLEMENTATION #define STB_TRUETYPE_IMPLEMENTATION
#include <stb/stb_truetype.h> #include <stb/stb_truetype.h>
#define STB_RECT_PACK_IMPLEMENTATION #define STB_RECT_PACK_IMPLEMENTATION
#include <stb/stb_rect_pack.h>
#include <algorithm> #include <algorithm>
#include <stb/stb_rect_pack.h>
namespace extra2d { namespace extra2d {
@ -75,7 +75,8 @@ Vec2 GLFontAtlas::measureText(const std::string &text) {
float height = getAscent() - getDescent(); float height = getAscent() - getDescent();
float currentWidth = 0.0f; float currentWidth = 0.0f;
for (char32_t codepoint : utf8ToUtf32(text)) { for (char c : text) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
if (codepoint == '\n') { if (codepoint == '\n') {
width = std::max(width, currentWidth); width = std::max(width, currentWidth);
currentWidth = 0.0f; currentWidth = 0.0f;
@ -218,8 +219,8 @@ void GLFontAtlas::cacheGlyph(char32_t codepoint) const {
// 使用预分配缓冲区 // 使用预分配缓冲区
size_t pixelCount = static_cast<size_t>(w) * static_cast<size_t>(h); size_t pixelCount = static_cast<size_t>(w) * static_cast<size_t>(h);
glyphBitmapCache_.resize(pixelCount); glyphBitmapCache_.resize(pixelCount);
stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w, scale_, scale_, stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w,
static_cast<int>(codepoint)); scale_, scale_, static_cast<int>(codepoint));
// 使用 stb_rect_pack 打包矩形 // 使用 stb_rect_pack 打包矩形
stbrp_rect rect; stbrp_rect rect;

View File

@ -2,7 +2,6 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstring> #include <cstring>
#include <extra2d/core/string.h>
#include <extra2d/graphics/gpu_context.h> #include <extra2d/graphics/gpu_context.h>
#include <extra2d/graphics/opengl/gl_font_atlas.h> #include <extra2d/graphics/opengl/gl_font_atlas.h>
#include <extra2d/graphics/opengl/gl_renderer.h> #include <extra2d/graphics/opengl/gl_renderer.h>
@ -448,7 +447,8 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text,
std::vector<GLSpriteBatch::SpriteData> sprites; std::vector<GLSpriteBatch::SpriteData> sprites;
sprites.reserve(text.size()); // 预分配空间 sprites.reserve(text.size()); // 预分配空间
for (char32_t codepoint : utf8ToUtf32(text)) { for (char c : text) {
char32_t codepoint = static_cast<char32_t>(static_cast<unsigned char>(c));
if (codepoint == '\n') { if (codepoint == '\n') {
cursorX = x; cursorX = x;
cursorY += font.getLineHeight(); cursorY += font.getLineHeight();

View File

@ -0,0 +1,321 @@
#include <extra2d/graphics/viewport_adapter.h>
#include <algorithm>
#include <glm/gtc/matrix_transform.hpp>
namespace extra2d {
ViewportAdapter::ViewportAdapter() = default;
ViewportAdapter::ViewportAdapter(float logicWidth, float logicHeight) {
config_.logicWidth = logicWidth;
config_.logicHeight = logicHeight;
}
void ViewportAdapter::setConfig(const ViewportConfig &config) {
config_ = config;
matrixDirty_ = true;
}
void ViewportAdapter::setLogicSize(float width, float height) {
config_.logicWidth = width;
config_.logicHeight = height;
matrixDirty_ = true;
}
void ViewportAdapter::setMode(ViewportMode mode) {
config_.mode = mode;
matrixDirty_ = true;
}
void ViewportAdapter::setLetterboxPosition(LetterboxPosition position) {
config_.letterboxPosition = position;
matrixDirty_ = true;
}
void ViewportAdapter::setLetterboxColor(const Color &color) {
config_.letterboxColor = color;
}
void ViewportAdapter::update(int screenWidth, int screenHeight) {
if (screenWidth_ == screenWidth && screenHeight_ == screenHeight &&
!matrixDirty_) {
return;
}
screenWidth_ = screenWidth;
screenHeight_ = screenHeight;
matrixDirty_ = true;
result_.hasLetterbox = false;
result_.letterbox.top = Rect::Zero();
result_.letterbox.bottom = Rect::Zero();
result_.letterbox.left = Rect::Zero();
result_.letterbox.right = Rect::Zero();
switch (config_.mode) {
case ViewportMode::AspectRatio:
calculateAspectRatio();
break;
case ViewportMode::Stretch:
calculateStretch();
break;
case ViewportMode::Center:
calculateCenter();
break;
case ViewportMode::Custom:
calculateCustom();
break;
}
}
void ViewportAdapter::calculateAspectRatio() {
if (config_.logicHeight <= 0.0f || screenHeight_ <= 0) {
result_ = ViewportResult();
return;
}
float logicAspect = config_.logicWidth / config_.logicHeight;
float screenAspect = static_cast<float>(screenWidth_) / screenHeight_;
if (screenAspect > logicAspect) {
result_.uniformScale = static_cast<float>(screenHeight_) / config_.logicHeight;
result_.scaleX = result_.uniformScale;
result_.scaleY = result_.uniformScale;
result_.viewport.size.width = config_.logicWidth * result_.uniformScale;
result_.viewport.size.height = static_cast<float>(screenHeight_);
result_.offset.x = (screenWidth_ - result_.viewport.size.width) / 2.0f;
result_.offset.y = 0.0f;
} else {
result_.uniformScale = static_cast<float>(screenWidth_) / config_.logicWidth;
result_.scaleX = result_.uniformScale;
result_.scaleY = result_.uniformScale;
result_.viewport.size.width = static_cast<float>(screenWidth_);
result_.viewport.size.height = config_.logicHeight * result_.uniformScale;
result_.offset.x = 0.0f;
result_.offset.y = (screenHeight_ - result_.viewport.size.height) / 2.0f;
}
result_.viewport.origin = result_.offset;
applyLetterboxPosition(
static_cast<float>(screenWidth_) - result_.viewport.size.width,
static_cast<float>(screenHeight_) - result_.viewport.size.height);
calculateLetterbox();
}
void ViewportAdapter::calculateStretch() {
result_.scaleX = static_cast<float>(screenWidth_) / config_.logicWidth;
result_.scaleY = static_cast<float>(screenHeight_) / config_.logicHeight;
result_.uniformScale = std::min(result_.scaleX, result_.scaleY);
result_.viewport.origin = Vec2::Zero();
result_.viewport.size.width = static_cast<float>(screenWidth_);
result_.viewport.size.height = static_cast<float>(screenHeight_);
result_.offset = Vec2::Zero();
result_.hasLetterbox = false;
}
void ViewportAdapter::calculateCenter() {
float displayWidth = config_.logicWidth;
float displayHeight = config_.logicHeight;
if (config_.autoScaleInCenterMode) {
float scaleX = static_cast<float>(screenWidth_) / config_.logicWidth;
float scaleY = static_cast<float>(screenHeight_) / config_.logicHeight;
result_.uniformScale = std::min(scaleX, scaleY);
if (result_.uniformScale < 1.0f) {
displayWidth = config_.logicWidth * result_.uniformScale;
displayHeight = config_.logicHeight * result_.uniformScale;
} else {
result_.uniformScale = 1.0f;
}
result_.scaleX = result_.uniformScale;
result_.scaleY = result_.uniformScale;
} else {
result_.scaleX = 1.0f;
result_.scaleY = 1.0f;
result_.uniformScale = 1.0f;
}
result_.offset.x = (screenWidth_ - displayWidth) / 2.0f;
result_.offset.y = (screenHeight_ - displayHeight) / 2.0f;
result_.viewport.origin = result_.offset;
result_.viewport.size.width = displayWidth;
result_.viewport.size.height = displayHeight;
applyLetterboxPosition(
static_cast<float>(screenWidth_) - displayWidth,
static_cast<float>(screenHeight_) - displayHeight);
calculateLetterbox();
}
void ViewportAdapter::calculateCustom() {
result_.scaleX = config_.customScale;
result_.scaleY = config_.customScale;
result_.uniformScale = config_.customScale;
if (config_.customViewport.empty()) {
float displayWidth = config_.logicWidth * config_.customScale;
float displayHeight = config_.logicHeight * config_.customScale;
result_.offset = config_.customOffset;
result_.viewport.origin = result_.offset;
result_.viewport.size.width = displayWidth;
result_.viewport.size.height = displayHeight;
} else {
result_.viewport = config_.customViewport;
result_.offset = config_.customViewport.origin;
}
calculateLetterbox();
}
void ViewportAdapter::calculateLetterbox() {
result_.hasLetterbox = false;
float screenW = static_cast<float>(screenWidth_);
float screenH = static_cast<float>(screenHeight_);
if (result_.offset.y > 0.0f) {
result_.hasLetterbox = true;
result_.letterbox.top =
Rect(0.0f, 0.0f, screenW, result_.offset.y);
result_.letterbox.bottom =
Rect(0.0f, result_.offset.y + result_.viewport.size.height, screenW,
result_.offset.y);
}
if (result_.offset.x > 0.0f) {
result_.hasLetterbox = true;
result_.letterbox.left =
Rect(0.0f, 0.0f, result_.offset.x, screenH);
result_.letterbox.right =
Rect(result_.offset.x + result_.viewport.size.width, 0.0f,
result_.offset.x, screenH);
}
}
void ViewportAdapter::applyLetterboxPosition(float extraWidth,
float extraHeight) {
if (extraWidth <= 0.0f && extraHeight <= 0.0f) {
return;
}
switch (config_.letterboxPosition) {
case LetterboxPosition::Center:
break;
case LetterboxPosition::LeftTop:
if (extraWidth > 0.0f) {
result_.offset.x = 0.0f;
}
if (extraHeight > 0.0f) {
result_.offset.y = 0.0f;
}
break;
case LetterboxPosition::RightTop:
if (extraWidth > 0.0f) {
result_.offset.x = extraWidth;
}
if (extraHeight > 0.0f) {
result_.offset.y = 0.0f;
}
break;
case LetterboxPosition::LeftBottom:
if (extraWidth > 0.0f) {
result_.offset.x = 0.0f;
}
if (extraHeight > 0.0f) {
result_.offset.y = extraHeight;
}
break;
case LetterboxPosition::RightBottom:
if (extraWidth > 0.0f) {
result_.offset.x = extraWidth;
}
if (extraHeight > 0.0f) {
result_.offset.y = extraHeight;
}
break;
}
result_.viewport.origin = result_.offset;
}
Vec2 ViewportAdapter::screenToLogic(const Vec2 &screenPos) const {
return Vec2((screenPos.x - result_.offset.x) / result_.scaleX,
(screenPos.y - result_.offset.y) / result_.scaleY);
}
Vec2 ViewportAdapter::logicToScreen(const Vec2 &logicPos) const {
return Vec2(logicPos.x * result_.scaleX + result_.offset.x,
logicPos.y * result_.scaleY + result_.offset.y);
}
Vec2 ViewportAdapter::screenToLogic(float x, float y) const {
return screenToLogic(Vec2(x, y));
}
Vec2 ViewportAdapter::logicToScreen(float x, float y) const {
return logicToScreen(Vec2(x, y));
}
glm::mat4 ViewportAdapter::getViewportMatrix() const {
if (matrixDirty_) {
viewportMatrix_ = glm::mat4(1.0f);
viewportMatrix_ = glm::translate(viewportMatrix_,
glm::vec3(result_.offset.x, result_.offset.y, 0.0f));
viewportMatrix_ = glm::scale(viewportMatrix_,
glm::vec3(result_.scaleX, result_.scaleY, 1.0f));
matrixDirty_ = false;
}
return viewportMatrix_;
}
glm::mat4 ViewportAdapter::getInverseViewportMatrix() const {
if (matrixDirty_) {
getViewportMatrix();
}
inverseViewportMatrix_ = glm::inverse(viewportMatrix_);
return inverseViewportMatrix_;
}
bool ViewportAdapter::isInViewport(const Vec2 &screenPos) const {
return result_.viewport.containsPoint(screenPos);
}
bool ViewportAdapter::isInLetterbox(const Vec2 &screenPos) const {
if (!result_.hasLetterbox) {
return false;
}
if (!result_.letterbox.top.empty() &&
result_.letterbox.top.containsPoint(screenPos)) {
return true;
}
if (!result_.letterbox.bottom.empty() &&
result_.letterbox.bottom.containsPoint(screenPos)) {
return true;
}
if (!result_.letterbox.left.empty() &&
result_.letterbox.left.containsPoint(screenPos)) {
return true;
}
if (!result_.letterbox.right.empty() &&
result_.letterbox.right.containsPoint(screenPos)) {
return true;
}
return false;
}
} // namespace extra2d

View File

@ -1,4 +1,5 @@
#include <extra2d/event/input_codes.h> #include <extra2d/event/input_codes.h>
#include <extra2d/graphics/viewport_adapter.h>
#include <extra2d/platform/input.h> #include <extra2d/platform/input.h>
#include <extra2d/utils/logger.h> #include <extra2d/utils/logger.h>
@ -9,7 +10,8 @@ Input::Input()
leftStickX_(0.0f), leftStickY_(0.0f), leftStickX_(0.0f), leftStickY_(0.0f),
rightStickX_(0.0f), rightStickY_(0.0f), rightStickX_(0.0f), rightStickY_(0.0f),
mouseScroll_(0.0f), prevMouseScroll_(0.0f), mouseScroll_(0.0f), prevMouseScroll_(0.0f),
touching_(false), prevTouching_(false), touchCount_(0) { touching_(false), prevTouching_(false), touchCount_(0),
viewportAdapter_(nullptr) {
// 初始化所有状态数组 // 初始化所有状态数组
keysDown_.fill(false); keysDown_.fill(false);
@ -446,4 +448,39 @@ bool Input::isAnyMouseDown() const {
#endif #endif
} }
// ============================================================================
// 视口适配器
// ============================================================================
void Input::setViewportAdapter(ViewportAdapter* adapter) {
viewportAdapter_ = adapter;
}
Vec2 Input::getMousePositionLogic() const {
Vec2 screenPos = getMousePosition();
if (viewportAdapter_) {
return viewportAdapter_->screenToLogic(screenPos);
}
return screenPos;
}
Vec2 Input::getTouchPositionLogic() const {
Vec2 screenPos = getTouchPosition();
if (viewportAdapter_) {
return viewportAdapter_->screenToLogic(screenPos);
}
return screenPos;
}
Vec2 Input::getMouseDeltaLogic() const {
Vec2 delta = getMouseDelta();
if (viewportAdapter_) {
float scale = viewportAdapter_->getUniformScale();
if (scale > 0.0f) {
return delta / scale;
}
}
return delta;
}
} // namespace extra2d } // namespace extra2d