452 lines
12 KiB
C++
452 lines
12 KiB
C++
#include <extra2d/graphics/camera/viewport_adapter.h>
|
||
#include <algorithm>
|
||
#include <glm/gtc/matrix_transform.hpp>
|
||
|
||
namespace extra2d {
|
||
|
||
/**
|
||
* @brief 默认构造函数
|
||
*
|
||
* 创建一个未配置的视口适配器
|
||
*/
|
||
ViewportAdapter::ViewportAdapter() = default;
|
||
|
||
/**
|
||
* @brief 构造函数
|
||
* @param logicWidth 逻辑宽度
|
||
* @param logicHeight 逻辑高度
|
||
*
|
||
* 创建一个指定逻辑尺寸的视口适配器
|
||
*/
|
||
ViewportAdapter::ViewportAdapter(float logicWidth, float logicHeight) {
|
||
config_.logicWidth = logicWidth;
|
||
config_.logicHeight = logicHeight;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置视口配置
|
||
* @param config 视口配置
|
||
*
|
||
* 设置完整的视口配置并标记矩阵为脏
|
||
*/
|
||
void ViewportAdapter::setConfig(const ViewportConfig &config) {
|
||
config_ = config;
|
||
matrixDirty_ = true;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置逻辑尺寸
|
||
* @param width 逻辑宽度
|
||
* @param height 逻辑高度
|
||
*
|
||
* 设置视口的逻辑尺寸并标记矩阵为脏
|
||
*/
|
||
void ViewportAdapter::setLogicSize(float width, float height) {
|
||
config_.logicWidth = width;
|
||
config_.logicHeight = height;
|
||
matrixDirty_ = true;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置视口模式
|
||
* @param mode 视口模式
|
||
*
|
||
* 设置视口适配模式(宽高比、拉伸、居中等)
|
||
*/
|
||
void ViewportAdapter::setMode(ViewportMode mode) {
|
||
config_.mode = mode;
|
||
matrixDirty_ = true;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置黑边位置
|
||
* @param position 黑边位置
|
||
*
|
||
* 设置黑边相对于视口的位置
|
||
*/
|
||
void ViewportAdapter::setLetterboxPosition(LetterboxPosition position) {
|
||
config_.letterboxPosition = position;
|
||
matrixDirty_ = true;
|
||
}
|
||
|
||
/**
|
||
* @brief 设置黑边颜色
|
||
* @param color 黑边颜色
|
||
*
|
||
* 设置黑边区域的填充颜色
|
||
*/
|
||
void ViewportAdapter::setLetterboxColor(const Color &color) {
|
||
config_.letterboxColor = color;
|
||
}
|
||
|
||
/**
|
||
* @brief 更新视口适配
|
||
* @param screenWidth 屏幕宽度
|
||
* @param screenHeight 屏幕高度
|
||
*
|
||
* 根据屏幕尺寸和配置计算视口参数
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 计算宽高比适配模式
|
||
*
|
||
* 保持逻辑宽高比,根据屏幕尺寸计算缩放和偏移
|
||
*/
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* @brief 计算拉伸模式
|
||
*
|
||
* 拉伸逻辑视口以填满整个屏幕
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* @brief 计算居中模式
|
||
*
|
||
* 将逻辑视口居中显示,可选自动缩放
|
||
*/
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* @brief 计算自定义模式
|
||
*
|
||
* 使用自定义缩放和偏移参数
|
||
*/
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* @brief 计算黑边区域
|
||
*
|
||
* 根据视口偏移计算上下左右黑边矩形
|
||
*/
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 应用黑边位置
|
||
* @param extraWidth 额外宽度
|
||
* @param extraHeight 额外高度
|
||
*
|
||
* 根据配置调整视口偏移以实现不同的黑边位置
|
||
*/
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* @brief 将屏幕坐标转换为逻辑坐标
|
||
* @param screenPos 屏幕坐标
|
||
* @return 逻辑坐标
|
||
*
|
||
* 根据当前缩放和偏移计算对应的逻辑坐标
|
||
*/
|
||
Vec2 ViewportAdapter::screenToLogic(const Vec2 &screenPos) const {
|
||
return Vec2((screenPos.x - result_.offset.x) / result_.scaleX,
|
||
(screenPos.y - result_.offset.y) / result_.scaleY);
|
||
}
|
||
|
||
/**
|
||
* @brief 将逻辑坐标转换为屏幕坐标
|
||
* @param logicPos 逻辑坐标
|
||
* @return 屏幕坐标
|
||
*
|
||
* 根据当前缩放和偏移计算对应的屏幕坐标
|
||
*/
|
||
Vec2 ViewportAdapter::logicToScreen(const Vec2 &logicPos) const {
|
||
return Vec2(logicPos.x * result_.scaleX + result_.offset.x,
|
||
logicPos.y * result_.scaleY + result_.offset.y);
|
||
}
|
||
|
||
/**
|
||
* @brief 将屏幕坐标转换为逻辑坐标
|
||
* @param x 屏幕X坐标
|
||
* @param y 屏幕Y坐标
|
||
* @return 逻辑坐标
|
||
*/
|
||
Vec2 ViewportAdapter::screenToLogic(float x, float y) const {
|
||
return screenToLogic(Vec2(x, y));
|
||
}
|
||
|
||
/**
|
||
* @brief 将逻辑坐标转换为屏幕坐标
|
||
* @param x 逻辑X坐标
|
||
* @param y 逻辑Y坐标
|
||
* @return 屏幕坐标
|
||
*/
|
||
Vec2 ViewportAdapter::logicToScreen(float x, float y) const {
|
||
return logicToScreen(Vec2(x, y));
|
||
}
|
||
|
||
/**
|
||
* @brief 获取视口变换矩阵
|
||
* @return 4x4变换矩阵
|
||
*
|
||
* 返回用于将逻辑坐标转换为屏幕坐标的变换矩阵
|
||
*/
|
||
glm::mat4 ViewportAdapter::getMatrix() 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_;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取视口逆变换矩阵
|
||
* @return 4x4逆变换矩阵
|
||
*
|
||
* 返回用于将屏幕坐标转换为逻辑坐标的逆变换矩阵
|
||
*/
|
||
glm::mat4 ViewportAdapter::getInvMatrix() const {
|
||
if (matrixDirty_) {
|
||
getMatrix();
|
||
}
|
||
inverseViewportMatrix_ = glm::inverse(viewportMatrix_);
|
||
return inverseViewportMatrix_;
|
||
}
|
||
|
||
/**
|
||
* @brief 检查屏幕坐标是否在视口内
|
||
* @param screenPos 屏幕坐标
|
||
* @return 在视口内返回true,否则返回false
|
||
*/
|
||
bool ViewportAdapter::isInViewport(const Vec2 &screenPos) const {
|
||
return result_.viewport.containsPoint(screenPos);
|
||
}
|
||
|
||
/**
|
||
* @brief 检查屏幕坐标是否在黑边区域内
|
||
* @param screenPos 屏幕坐标
|
||
* @return 在黑边区域内返回true,否则返回false
|
||
*/
|
||
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
|