From 93d07e547f6a9b1d5bd7ac4f0b263013c48f1192 Mon Sep 17 00:00:00 2001 From: ChestnutYueyue <952134128@qq.com> Date: Sat, 14 Feb 2026 18:58:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=A7=86=E5=8F=A3=E9=80=82=E9=85=8D):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=86=E5=8F=A3=E9=80=82=E9=85=8D=E5=99=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E9=9B=86=E6=88=90=E5=88=B0=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E5=92=8C=E8=BE=93=E5=85=A5=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现视口适配器功能,支持多种适配模式(宽高比、拉伸、居中、自定义) 将视口适配器集成到相机系统,实现坐标转换和视口自动调整 将视口适配器集成到输入系统,支持逻辑坐标转换 移除不再使用的字符串转换工具类 优化相机矩阵计算,支持旋转和缩放 添加数学工具函数,包括角度处理和坐标转换 --- Extra2D/include/extra2d/app/application.h | 8 + Extra2D/include/extra2d/core/math_types.h | 215 ++++++++++++ Extra2D/include/extra2d/extra2d.h | 1 + Extra2D/include/extra2d/graphics/camera.h | 18 + .../extra2d/graphics/viewport_adapter.h | 332 ++++++++++++++++++ Extra2D/include/extra2d/platform/input.h | 32 ++ Extra2D/src/app/application.cpp | 41 ++- Extra2D/src/core/string.cpp | 48 --- Extra2D/src/graphics/camera.cpp | 89 ++++- Extra2D/src/graphics/opengl/gl_font_atlas.cpp | 13 +- Extra2D/src/graphics/opengl/gl_renderer.cpp | 4 +- Extra2D/src/graphics/viewport_adapter.cpp | 321 +++++++++++++++++ Extra2D/src/platform/input.cpp | 39 +- 13 files changed, 1091 insertions(+), 70 deletions(-) create mode 100644 Extra2D/include/extra2d/graphics/viewport_adapter.h delete mode 100644 Extra2D/src/core/string.cpp create mode 100644 Extra2D/src/graphics/viewport_adapter.cpp diff --git a/Extra2D/include/extra2d/app/application.h b/Extra2D/include/extra2d/app/application.h index 09c4ab7..6c6f666 100644 --- a/Extra2D/include/extra2d/app/application.h +++ b/Extra2D/include/extra2d/app/application.h @@ -15,6 +15,7 @@ class TimerManager; class EventQueue; class EventDispatcher; class Camera; +class ViewportAdapter; // ============================================================================ // Application 配置 @@ -80,6 +81,12 @@ public: EventDispatcher &eventDispatcher(); Camera &camera(); + /** + * @brief 获取视口适配器 + * @return 视口适配器引用 + */ + ViewportAdapter &viewportAdapter(); + // ------------------------------------------------------------------------ // 便捷方法 // ------------------------------------------------------------------------ @@ -115,6 +122,7 @@ private: UniquePtr eventQueue_; UniquePtr eventDispatcher_; UniquePtr camera_; + UniquePtr viewportAdapter_; // 状态 bool initialized_ = false; diff --git a/Extra2D/include/extra2d/core/math_types.h b/Extra2D/include/extra2d/core/math_types.h index 2c4d68e..ef63602 100644 --- a/Extra2D/include/extra2d/core/math_types.h +++ b/Extra2D/include/extra2d/core/math_types.h @@ -330,6 +330,221 @@ inline float degrees(float radians) { return radians * RAD_TO_DEG; } 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 ¢er, 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 ¢er1, float radius1, + const Vec2 ¢er2, 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 extra2d diff --git a/Extra2D/include/extra2d/extra2d.h b/Extra2D/include/extra2d/extra2d.h index d0eb408..e8a6d5c 100644 --- a/Extra2D/include/extra2d/extra2d.h +++ b/Extra2D/include/extra2d/extra2d.h @@ -20,6 +20,7 @@ #include #include +#include #include // Scene diff --git a/Extra2D/include/extra2d/graphics/camera.h b/Extra2D/include/extra2d/graphics/camera.h index 2b56423..5c92804 100644 --- a/Extra2D/include/extra2d/graphics/camera.h +++ b/Extra2D/include/extra2d/graphics/camera.h @@ -7,6 +7,8 @@ namespace extra2d { +class ViewportAdapter; + // ============================================================================ // 2D 正交相机 // ============================================================================ @@ -65,6 +67,20 @@ public: void clearBounds(); void clampToBounds(); + // ------------------------------------------------------------------------ + // 视口适配器 + // ------------------------------------------------------------------------ + /** + * @brief 设置视口适配器 + * @param adapter 视口适配器指针 + */ + void setViewportAdapter(ViewportAdapter* adapter); + + /** + * @brief 根据视口适配器自动设置视口 + */ + void applyViewportAdapter(); + // ------------------------------------------------------------------------ // 快捷方法:看向某点 // ------------------------------------------------------------------------ @@ -83,6 +99,8 @@ private: Rect bounds_; bool hasBounds_ = false; + ViewportAdapter* viewportAdapter_ = nullptr; + mutable glm::mat4 viewMatrix_; mutable glm::mat4 projMatrix_; mutable glm::mat4 vpMatrix_; diff --git a/Extra2D/include/extra2d/graphics/viewport_adapter.h b/Extra2D/include/extra2d/graphics/viewport_adapter.h new file mode 100644 index 0000000..db0ac4d --- /dev/null +++ b/Extra2D/include/extra2d/graphics/viewport_adapter.h @@ -0,0 +1,332 @@ +#pragma once + +#include +#include +#include +#include + +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(screenWidth_), + static_cast(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 diff --git a/Extra2D/include/extra2d/platform/input.h b/Extra2D/include/extra2d/platform/input.h index ff16b13..b0d8b8b 100644 --- a/Extra2D/include/extra2d/platform/input.h +++ b/Extra2D/include/extra2d/platform/input.h @@ -9,6 +9,8 @@ namespace extra2d { +class ViewportAdapter; + // ============================================================================ // 鼠标按钮枚举 // ============================================================================ @@ -81,6 +83,33 @@ public: Vec2 getTouchPosition() const { return touchPosition_; } 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_; int touchCount_; + // 视口适配器 + ViewportAdapter* viewportAdapter_; + // 映射键盘 keyCode 到 SDL GameController 按钮 (Switch 兼容模式) SDL_GameControllerButton mapKeyToButton(int keyCode) const; diff --git a/Extra2D/src/app/application.cpp b/Extra2D/src/app/application.cpp index 6763b11..29545f6 100644 --- a/Extra2D/src/app/application.cpp +++ b/Extra2D/src/app/application.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -139,12 +140,32 @@ bool Application::init(const AppConfig &config) { camera_ = makeUnique(0, static_cast(window_->getWidth()), static_cast(window_->getHeight()), 0); + // 创建视口适配器 + viewportAdapter_ = makeUnique(); + ViewportConfig vpConfig; + vpConfig.logicWidth = static_cast(config.width); + vpConfig.logicHeight = static_cast(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) { - if (camera_) { - camera_->setViewport(0, static_cast(width), - static_cast(height), 0); + // 更新视口适配器 + if (viewportAdapter_) { + viewportAdapter_->update(width, height); } + + if (camera_) { + camera_->applyViewportAdapter(); + } + if (sceneManager_) { auto currentScene = sceneManager_->getCurrentScene(); if (currentScene) { @@ -204,6 +225,7 @@ void Application::shutdown() { // ======================================== sceneManager_.reset(); // 场景持有纹理引用 resourceManager_.reset(); // 纹理缓存持有 GPU 纹理 + viewportAdapter_.reset(); // 视口适配器 camera_.reset(); // 相机可能持有渲染目标 // ======================================== @@ -356,7 +378,16 @@ void Application::render() { return; } - renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight()); + // 应用视口适配器 + if (viewportAdapter_) { + const auto &vp = viewportAdapter_->getViewport(); + renderer_->setViewport(static_cast(vp.origin.x), + static_cast(vp.origin.y), + static_cast(vp.size.width), + static_cast(vp.size.height)); + } else { + renderer_->setViewport(0, 0, window_->getWidth(), window_->getHeight()); + } if (sceneManager_) { sceneManager_->render(*renderer_); @@ -383,6 +414,8 @@ EventDispatcher &Application::eventDispatcher() { return *eventDispatcher_; } Camera &Application::camera() { return *camera_; } +ViewportAdapter &Application::viewportAdapter() { return *viewportAdapter_; } + void Application::enterScene(Ptr scene) { enterScene(scene, nullptr); } void Application::enterScene(Ptr scene, diff --git a/Extra2D/src/core/string.cpp b/Extra2D/src/core/string.cpp deleted file mode 100644 index 95e05b2..0000000 --- a/Extra2D/src/core/string.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include - -#ifdef _WIN32 -#include - -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 diff --git a/Extra2D/src/graphics/camera.cpp b/Extra2D/src/graphics/camera.cpp index 1d982d2..f8d62d9 100644 --- a/Extra2D/src/graphics/camera.cpp +++ b/Extra2D/src/graphics/camera.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include namespace extra2d { @@ -55,12 +57,34 @@ Rect Camera::getViewport() const { return Rect(left_, top_, right_ - left_, bottom_ - top_); } +/** + * @brief 获取视图矩阵 + * @return 视图矩阵 + * + * 变换顺序:平移 -> 旋转 -> 缩放(逆序应用) + * View = T(-position) × R(-rotation) × S(1/zoom) + */ glm::mat4 Camera::getViewMatrix() const { if (viewDirty_) { viewMatrix_ = glm::mat4(1.0f); - // 对于2D相机,我们只需要平移(注意Y轴方向) - viewMatrix_ = glm::translate(viewMatrix_, + + // 1. 平移(最后应用) + viewMatrix_ = glm::translate(viewMatrix_, 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; } return viewMatrix_; @@ -81,20 +105,49 @@ glm::mat4 Camera::getProjectionMatrix() const { return projMatrix_; } +/** + * @brief 获取视图-投影矩阵 + * @return 视图-投影矩阵 + */ glm::mat4 Camera::getViewProjectionMatrix() const { - // 对于2D相机,我们主要依赖投影矩阵 - // 视口变换已经处理了坐标系转换 - return getProjectionMatrix(); + return getProjectionMatrix() * getViewMatrix(); } +/** + * @brief 将屏幕坐标转换为世界坐标 + * @param screenPos 屏幕坐标 + * @return 世界坐标 + */ Vec2 Camera::screenToWorld(const Vec2 &screenPos) const { - // 屏幕坐标直接映射到世界坐标(在2D中通常相同) - return screenPos; + Vec2 logicPos = 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 { - // 世界坐标直接映射到屏幕坐标(在2D中通常相同) - return worldPos; + glm::vec4 world(worldPos.x, worldPos.y, 0.0f, 1.0f); + 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 { @@ -155,4 +208,22 @@ void Camera::lookAt(const Vec2 &target) { 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 diff --git a/Extra2D/src/graphics/opengl/gl_font_atlas.cpp b/Extra2D/src/graphics/opengl/gl_font_atlas.cpp index 6fd4dd4..037765c 100644 --- a/Extra2D/src/graphics/opengl/gl_font_atlas.cpp +++ b/Extra2D/src/graphics/opengl/gl_font_atlas.cpp @@ -1,12 +1,12 @@ #include -#include #include #include #define STB_TRUETYPE_IMPLEMENTATION #include #define STB_RECT_PACK_IMPLEMENTATION -#include #include +#include + namespace extra2d { @@ -75,7 +75,8 @@ Vec2 GLFontAtlas::measureText(const std::string &text) { float height = getAscent() - getDescent(); float currentWidth = 0.0f; - for (char32_t codepoint : utf8ToUtf32(text)) { + for (char c : text) { + char32_t codepoint = static_cast(static_cast(c)); if (codepoint == '\n') { width = std::max(width, currentWidth); currentWidth = 0.0f; @@ -108,7 +109,7 @@ void GLFontAtlas::createAtlas() { packNodes_.resize(ATLAS_WIDTH); stbrp_init_target(&packContext_, ATLAS_WIDTH, ATLAS_HEIGHT, packNodes_.data(), ATLAS_WIDTH); - + // 预分配字形缓冲区 // 假设最大字形尺寸为 fontSize * fontSize * 4 (RGBA) size_t maxGlyphSize = static_cast(fontSize_ * fontSize_ * 4 * 4); @@ -218,8 +219,8 @@ void GLFontAtlas::cacheGlyph(char32_t codepoint) const { // 使用预分配缓冲区 size_t pixelCount = static_cast(w) * static_cast(h); glyphBitmapCache_.resize(pixelCount); - stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w, scale_, scale_, - static_cast(codepoint)); + stbtt_MakeCodepointBitmap(&fontInfo_, glyphBitmapCache_.data(), w, h, w, + scale_, scale_, static_cast(codepoint)); // 使用 stb_rect_pack 打包矩形 stbrp_rect rect; diff --git a/Extra2D/src/graphics/opengl/gl_renderer.cpp b/Extra2D/src/graphics/opengl/gl_renderer.cpp index 2be034c..c7784fe 100644 --- a/Extra2D/src/graphics/opengl/gl_renderer.cpp +++ b/Extra2D/src/graphics/opengl/gl_renderer.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -448,7 +447,8 @@ void GLRenderer::drawText(const FontAtlas &font, const std::string &text, std::vector sprites; sprites.reserve(text.size()); // 预分配空间 - for (char32_t codepoint : utf8ToUtf32(text)) { + for (char c : text) { + char32_t codepoint = static_cast(static_cast(c)); if (codepoint == '\n') { cursorX = x; cursorY += font.getLineHeight(); diff --git a/Extra2D/src/graphics/viewport_adapter.cpp b/Extra2D/src/graphics/viewport_adapter.cpp new file mode 100644 index 0000000..14e5738 --- /dev/null +++ b/Extra2D/src/graphics/viewport_adapter.cpp @@ -0,0 +1,321 @@ +#include +#include +#include + +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(screenWidth_) / screenHeight_; + + if (screenAspect > logicAspect) { + result_.uniformScale = static_cast(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(screenHeight_); + result_.offset.x = (screenWidth_ - result_.viewport.size.width) / 2.0f; + result_.offset.y = 0.0f; + } else { + result_.uniformScale = static_cast(screenWidth_) / config_.logicWidth; + result_.scaleX = result_.uniformScale; + result_.scaleY = result_.uniformScale; + result_.viewport.size.width = static_cast(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(screenWidth_) - result_.viewport.size.width, + static_cast(screenHeight_) - result_.viewport.size.height); + + calculateLetterbox(); +} + +void ViewportAdapter::calculateStretch() { + result_.scaleX = static_cast(screenWidth_) / config_.logicWidth; + result_.scaleY = static_cast(screenHeight_) / config_.logicHeight; + result_.uniformScale = std::min(result_.scaleX, result_.scaleY); + + result_.viewport.origin = Vec2::Zero(); + result_.viewport.size.width = static_cast(screenWidth_); + result_.viewport.size.height = static_cast(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(screenWidth_) / config_.logicWidth; + float scaleY = static_cast(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(screenWidth_) - displayWidth, + static_cast(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(screenWidth_); + float screenH = static_cast(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 diff --git a/Extra2D/src/platform/input.cpp b/Extra2D/src/platform/input.cpp index 3b7f0c9..aa90808 100644 --- a/Extra2D/src/platform/input.cpp +++ b/Extra2D/src/platform/input.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -9,7 +10,8 @@ Input::Input() leftStickX_(0.0f), leftStickY_(0.0f), rightStickX_(0.0f), rightStickY_(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); @@ -446,4 +448,39 @@ bool Input::isAnyMouseDown() const { #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