From 875af43f88ba6e8e5e5b2b8c43fc348b5b2ee5f2 Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Tue, 7 Apr 2026 00:15:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93):=20=E5=AE=9E=E7=8E=B02?= =?UTF-8?q?D=E6=B8=B2=E6=9F=93=E9=A3=8E=E6=A0=BC=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加渲染风格预设配置,支持像素风、平滑2D和混合模式 新增纹理采样控制、顶点像素对齐和UV收缩优化 为相机和场景添加渲染风格覆盖功能 --- .../include/frostbite2D/core/application.h | 8 +++ .../include/frostbite2D/graphics/camera.h | 23 +++--- .../frostbite2D/graphics/render_style.h | 72 +++++++++++++++++++ .../include/frostbite2D/graphics/renderer.h | 47 ++++++++++++ .../include/frostbite2D/graphics/texture.h | 16 ++++- .../include/frostbite2D/graphics/types.h | 39 ++++++++-- Frostbite2D/include/frostbite2D/scene/scene.h | 18 ++++- Frostbite2D/src/frostbite2D/2d/sprite.cpp | 9 ++- .../src/frostbite2D/2d/text_sprite.cpp | 1 + .../src/frostbite2D/core/application.cpp | 5 +- .../src/frostbite2D/graphics/batch.cpp | 18 ++++- .../src/frostbite2D/graphics/camera.cpp | 69 ++++++++++++------ .../src/frostbite2D/graphics/renderer.cpp | 45 +++++++++++- .../src/frostbite2D/graphics/texture.cpp | 42 +++++++++-- Frostbite2D/src/frostbite2D/scene/scene.cpp | 15 ++++ .../src/frostbite2D/scene/scene_manager.cpp | 35 +++++++++ Game/src/bootstrap/main.cpp | 1 + Game/src/map/GameMap.cpp | 2 +- 18 files changed, 414 insertions(+), 51 deletions(-) create mode 100644 Frostbite2D/include/frostbite2D/graphics/render_style.h diff --git a/Frostbite2D/include/frostbite2D/core/application.h b/Frostbite2D/include/frostbite2D/core/application.h index 84afb43..a30cc16 100644 --- a/Frostbite2D/include/frostbite2D/core/application.h +++ b/Frostbite2D/include/frostbite2D/core/application.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -58,6 +59,12 @@ struct AppConfig { */ ResolutionScaleMode resolutionMode = ResolutionScaleMode::Fit; + /** + * @brief 默认 2D 渲染风格预设 + * 启动时作为世界/UI 渲染的基础风格,Scene 可按需覆盖 + */ + RenderStyleProfileId renderStyleProfile = RenderStyleProfileId::Hybrid2D; + /** * @brief 创建默认配置 * @return 默认的应用配置实例 @@ -69,6 +76,7 @@ struct AppConfig { config.organization = "frostbite"; config.targetPlatform = PlatformType::Auto; config.windowConfig = WindowConfig(); + config.renderStyleProfile = RenderStyleProfileId::Hybrid2D; return config; } }; diff --git a/Frostbite2D/include/frostbite2D/graphics/camera.h b/Frostbite2D/include/frostbite2D/graphics/camera.h index 8f0a7fd..e4a8c80 100644 --- a/Frostbite2D/include/frostbite2D/graphics/camera.h +++ b/Frostbite2D/include/frostbite2D/graphics/camera.h @@ -7,32 +7,37 @@ namespace frostbite2D { class Camera { public: Camera(); - + void setPosition(const Vec2& pos); void setZoom(float zoom); void setViewport(int width, int height); - void setFlipY(bool flip) { flipY_ = flip; } // 调试用:Y轴翻转开关 - + void setFlipY(bool flip) { flipY_ = flip; } // Debug Y-axis flip. + void setPixelSnapEnabled(bool enabled) { pixelSnapEnabled_ = enabled; } + const Vec2& getPosition() const { return position_; } + Vec2 getRenderPosition() const; + Vec2 snapWorldPosition(const Vec2& position) const; float getZoom() const { return zoom_; } int getViewportWidth() const { return viewportWidth_; } int getViewportHeight() const { return viewportHeight_; } float getVisibleWidth() const; float getVisibleHeight() const; - + bool isPixelSnapEnabled() const { return pixelSnapEnabled_; } + void lookAt(const Vec2& target); void move(const Vec2& delta); void zoomAt(float factor, const Vec2& screenPos); - + glm::mat4 getViewMatrix() const; glm::mat4 getProjectionMatrix() const; - - private: + +private: Vec2 position_; float zoom_ = 1.0f; int viewportWidth_ = 1280; int viewportHeight_ = 720; - bool flipY_ = false; // 调试用:Y轴翻转开关 + bool flipY_ = false; // Debug Y-axis flip. + bool pixelSnapEnabled_ = false; }; -} +} // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/graphics/render_style.h b/Frostbite2D/include/frostbite2D/graphics/render_style.h new file mode 100644 index 0000000..fdba974 --- /dev/null +++ b/Frostbite2D/include/frostbite2D/graphics/render_style.h @@ -0,0 +1,72 @@ +#pragma once + +namespace frostbite2D { + +/** + * @brief 2D 渲染风格预设 ID + */ +enum class RenderStyleProfileId { + PixelArt2D, ///< 世界与 UI 都按像素风策略渲染 + Smooth2D, ///< 世界与 UI 都按平滑 2D 策略渲染 + Hybrid2D ///< 世界走像素风,UI 走平滑策略 +}; + +/** + * @brief 渲染风格作用层级 + */ +enum class RenderStyleLayerRole { + World, ///< 世界层,如地图、角色、场景特效 + UI ///< UI 层,如 HUD、菜单、文字 +}; + +/** + * @brief 单个层级的渲染风格开关集合 + */ +struct RenderStyleLayerSettings { + bool cameraPixelSnap = false; ///< 是否让相机位置吸附到像素网格 + bool vertexPixelSnap = false; ///< 是否让世界顶点在提交前做像素对齐 + bool pixelArtSampling = false; ///< 是否优先使用像素风采样(如 GL_NEAREST) + bool shrinkSubTextureUVs = false; ///< 是否对子纹理 UV 做向内收缩,减轻图集边缘闪烁 +}; + +/** + * @brief 一套完整的 2D 渲染风格设置 + */ +struct RenderStyleSettings { + RenderStyleLayerSettings world; ///< 世界层使用的渲染风格 + RenderStyleLayerSettings ui; ///< UI 层使用的渲染风格 + + /** + * @brief 根据预设 ID 生成对应的渲染风格 + */ + static RenderStyleSettings FromProfile(RenderStyleProfileId profile) { + RenderStyleSettings settings; + + switch (profile) { + case RenderStyleProfileId::PixelArt2D: + settings.world = {true, true, true, true}; + settings.ui = {true, true, true, true}; + break; + case RenderStyleProfileId::Smooth2D: + settings.world = {false, false, false, false}; + settings.ui = {false, false, false, false}; + break; + case RenderStyleProfileId::Hybrid2D: + default: + settings.world = {true, true, true, true}; + settings.ui = {false, false, false, false}; + break; + } + + return settings; + } + + /** + * @brief 获取指定层级对应的风格设置 + */ + const RenderStyleLayerSettings& layer(RenderStyleLayerRole role) const { + return role == RenderStyleLayerRole::UI ? ui : world; + } +}; + +} // namespace frostbite2D diff --git a/Frostbite2D/include/frostbite2D/graphics/renderer.h b/Frostbite2D/include/frostbite2D/graphics/renderer.h index a13e6b7..a50f21f 100644 --- a/Frostbite2D/include/frostbite2D/graphics/renderer.h +++ b/Frostbite2D/include/frostbite2D/graphics/renderer.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,26 @@ public: void setClearColor(float r, float g, float b, float a = 1.0f); void setClearColor(const Color& color); void clear(uint32_t flags); + /** + * @brief 设置应用默认渲染风格预设 + */ + void setDefaultRenderStyleProfile(RenderStyleProfileId profile); + /** + * @brief 获取应用默认渲染风格预设 + */ + RenderStyleProfileId getDefaultRenderStyleProfile() const { + return defaultRenderStyleProfile_; + } + /** + * @brief 切换当前正在使用的渲染风格上下文 + */ + void setActiveRenderStyleProfile(RenderStyleProfileId profile, + RenderStyleLayerRole role); + /** + * @brief 将指定风格作用到某个相机 + */ + void applyRenderStyleToCamera(Camera* camera, RenderStyleProfileId profile, + RenderStyleLayerRole role) const; void setCamera(Camera* camera); Camera* getCamera() { return camera_; } @@ -53,6 +74,28 @@ public: Ptr texture, const Color& color = Color(1, 1, 1, 1)); void setupBlendMode(BlendMode mode); + /** + * @brief 获取当前激活的层级风格设置 + */ + const RenderStyleLayerSettings& getActiveRenderStyle() const { + return activeRenderStyle_; + } + /** + * @brief 当前风格是否要求顶点像素对齐 + */ + bool shouldSnapVertices() const { return activeRenderStyle_.vertexPixelSnap; } + /** + * @brief 当前风格是否优先使用像素风采样 + */ + bool shouldUsePixelArtSampling() const { + return activeRenderStyle_.pixelArtSampling; + } + /** + * @brief 当前风格是否需要对子纹理 UV 做向内收缩 + */ + bool shouldShrinkSubTextureUVs() const { + return activeRenderStyle_.shrinkSubTextureUVs; + } ShaderManager& getShaderManager() { return shaderManager_; } Batch& getBatch() { return batch_; } @@ -67,6 +110,10 @@ private: ShaderManager shaderManager_; Batch batch_; Camera* camera_ = nullptr; + RenderStyleProfileId defaultRenderStyleProfile_ = RenderStyleProfileId::Hybrid2D; + RenderStyleProfileId activeRenderStyleProfile_ = RenderStyleProfileId::Hybrid2D; + RenderStyleLayerRole activeRenderStyleRole_ = RenderStyleLayerRole::World; + RenderStyleLayerSettings activeRenderStyle_; uint32_t clearColor_[4] = {0, 0, 0, 255}; bool useVirtualResolution_ = false; diff --git a/Frostbite2D/include/frostbite2D/graphics/texture.h b/Frostbite2D/include/frostbite2D/graphics/texture.h index 8ee11e7..a8bbd47 100644 --- a/Frostbite2D/include/frostbite2D/graphics/texture.h +++ b/Frostbite2D/include/frostbite2D/graphics/texture.h @@ -8,6 +8,11 @@ namespace frostbite2D { +enum class TextureSampling { + Linear, + PixelArt +}; + class Texture : public RefObject { public: static Ptr loadFromFile(const std::string& path); @@ -16,11 +21,13 @@ public: ~Texture(); - void bind(uint32_t slot = 0); + void bind(uint32_t slot = 0, bool preferPixelArtSampling = false); void unbind(); void setWrapMode(uint32_t wrapS, uint32_t wrapT); void setFilterMode(uint32_t minFilter, uint32_t magFilter); + void setSampling(TextureSampling sampling); + TextureSampling getSampling() const { return sampling_; } int getWidth() const { return width_; } int getHeight() const { return height_; } @@ -29,16 +36,21 @@ public: private: Texture(int width, int height, uint32_t id); + void applyFilter(uint32_t minFilter, uint32_t magFilter); + void applySampling(bool preferPixelArtSampling); uint32_t textureID_ = 0; int width_ = 0; int height_ = 0; int channels_ = 0; std::string path_; + TextureSampling sampling_ = TextureSampling::Linear; + uint32_t appliedMinFilter_ = 0; + uint32_t appliedMagFilter_ = 0; Texture() = default; Texture(const Texture&) = delete; Texture& operator=(const Texture&) = delete; }; -} \ No newline at end of file +} diff --git a/Frostbite2D/include/frostbite2D/graphics/types.h b/Frostbite2D/include/frostbite2D/graphics/types.h index 322db79..54f70db 100644 --- a/Frostbite2D/include/frostbite2D/graphics/types.h +++ b/Frostbite2D/include/frostbite2D/graphics/types.h @@ -57,7 +57,8 @@ struct Quad { static Quad createTextured(const Rect& destRect, const Rect& srcRect, const Vec2& texSize, float cr = 1.0f, - float cg = 1.0f, float cb = 1.0f, float ca = 1.0f) { + float cg = 1.0f, float cb = 1.0f, float ca = 1.0f, + bool shrinkSubTextureUVs = false) { Quad q; Vec2 bl(destRect.left(), destRect.bottom()); @@ -65,10 +66,38 @@ struct Quad { Vec2 tl(destRect.left(), destRect.top()); Vec2 tr(destRect.right(), destRect.top()); - Vec2 uvBL(srcRect.left() / texSize.x, srcRect.bottom() / texSize.y); - Vec2 uvBR(srcRect.right() / texSize.x, srcRect.bottom() / texSize.y); - Vec2 uvTL(srcRect.left() / texSize.x, srcRect.top() / texSize.y); - Vec2 uvTR(srcRect.right() / texSize.x, srcRect.top() / texSize.y); + auto resolveUvX = [&srcRect, &texSize, shrinkSubTextureUVs](bool useRightEdge) { + float pixelInset = 0.0f; + if (!shrinkSubTextureUVs) { + return (useRightEdge ? srcRect.right() : srcRect.left()) / texSize.x; + } + if (useRightEdge && srcRect.right() < texSize.x) { + pixelInset = -0.5f; + } else if (!useRightEdge && srcRect.left() > 0.0f) { + pixelInset = 0.5f; + } + return (useRightEdge ? srcRect.right() : srcRect.left()) / texSize.x + + pixelInset / texSize.x; + }; + + auto resolveUvY = [&srcRect, &texSize, shrinkSubTextureUVs](bool useBottomEdge) { + float pixelInset = 0.0f; + if (!shrinkSubTextureUVs) { + return (useBottomEdge ? srcRect.bottom() : srcRect.top()) / texSize.y; + } + if (useBottomEdge && srcRect.bottom() < texSize.y) { + pixelInset = -0.5f; + } else if (!useBottomEdge && srcRect.top() > 0.0f) { + pixelInset = 0.5f; + } + return (useBottomEdge ? srcRect.bottom() : srcRect.top()) / texSize.y + + pixelInset / texSize.y; + }; + + Vec2 uvBL(resolveUvX(false), resolveUvY(true)); + Vec2 uvBR(resolveUvX(true), resolveUvY(true)); + Vec2 uvTL(resolveUvX(false), resolveUvY(false)); + Vec2 uvTR(resolveUvX(true), resolveUvY(false)); q.vertices[0] = Vertex(bl, uvBL, cr, cg, cb, ca); q.vertices[1] = Vertex(br, uvBR, cr, cg, cb, ca); diff --git a/Frostbite2D/include/frostbite2D/scene/scene.h b/Frostbite2D/include/frostbite2D/scene/scene.h index d49e94c..01c9e48 100644 --- a/Frostbite2D/include/frostbite2D/scene/scene.h +++ b/Frostbite2D/include/frostbite2D/scene/scene.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace frostbite2D { @@ -18,6 +19,20 @@ public: void Render() override; bool OnEvent(const Event& event) override; + /** + * @brief 为当前场景指定渲染风格预设 + */ + void SetRenderStyleProfile(RenderStyleProfileId profile); + /** + * @brief 清除场景级渲染风格覆盖,回退到应用默认值 + */ + void ClearRenderStyleProfileOverride(); + bool HasRenderStyleProfileOverride() const { return hasRenderStyleProfileOverride_; } + /** + * @brief 解析当前场景最终应使用的渲染风格 + */ + RenderStyleProfileId ResolveRenderStyleProfile( + RenderStyleProfileId defaultProfile) const; static Scene* GetCurrent(); @@ -25,9 +40,10 @@ private: bool dispatchToChildren(const Event& event); static Scene* current_; + bool hasRenderStyleProfileOverride_ = false; + RenderStyleProfileId renderStyleProfileOverride_ = RenderStyleProfileId::Hybrid2D; friend class SceneManager; }; } - diff --git a/Frostbite2D/src/frostbite2D/2d/sprite.cpp b/Frostbite2D/src/frostbite2D/2d/sprite.cpp index 74d3026..a319087 100644 --- a/Frostbite2D/src/frostbite2D/2d/sprite.cpp +++ b/Frostbite2D/src/frostbite2D/2d/sprite.cpp @@ -25,6 +25,7 @@ Ptr Sprite::createFromFile(const std::string &path) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load texture: %s", path.c_str()); return nullptr; } + texture->setSampling(TextureSampling::PixelArt); auto sprite = MakePtr(texture); sprite->SetSizeToTexture(); @@ -38,6 +39,7 @@ Ptr Sprite::createFromMemory(const uint8* data, int width, int height, i "Failed to create texture from memory"); return nullptr; } + texture->setSampling(TextureSampling::PixelArt); auto sprite = MakePtr(texture); sprite->SetSizeToTexture(); @@ -73,7 +75,8 @@ void Sprite::Render() { Quad quad = Quad::createTextured( destRect, srcRect, Vec2((float)texture_->getWidth(), (float)texture_->getHeight()), - color_.r, color_.g, color_.b, color_.a * worldOpacity + color_.r, color_.g, color_.b, color_.a * worldOpacity, + renderer.shouldShrinkSubTextureUVs() ); if (flippedX_) { @@ -180,6 +183,7 @@ Shader* Sprite::getActiveShader() const { } Quad Sprite::createQuad() const { + Renderer& renderer = Renderer::get(); Rect srcRect = srcRect_; if (srcRect.empty()) { srcRect = Rect(0, 0, texture_->getWidth(), texture_->getHeight()); @@ -201,7 +205,8 @@ Quad Sprite::createQuad() const { float worldOpacity = GetWorldOpacity(); Quad quad = Quad::createTextured( destRect, srcRect, texSize, - color_.r, color_.g, color_.b, color_.a * worldOpacity + color_.r, color_.g, color_.b, color_.a * worldOpacity, + renderer.shouldShrinkSubTextureUVs() ); if (flippedX_) { diff --git a/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp b/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp index ec8c561..7a12da6 100644 --- a/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp +++ b/Frostbite2D/src/frostbite2D/2d/text_sprite.cpp @@ -148,6 +148,7 @@ void TextSprite::RenderText() { ); if (texture) { + texture->setSampling(TextureSampling::Linear); SetTexture(texture); SetSizeToTexture(); } diff --git a/Frostbite2D/src/frostbite2D/core/application.cpp b/Frostbite2D/src/frostbite2D/core/application.cpp index ec9b0b2..8a06c6b 100644 --- a/Frostbite2D/src/frostbite2D/core/application.cpp +++ b/Frostbite2D/src/frostbite2D/core/application.cpp @@ -201,6 +201,7 @@ bool Application::initCoreModules() { // 设置窗口清除颜色和视口 renderer_->setClearColor(0.0f, 0.0f, 0.0f); + renderer_->setDefaultRenderStyleProfile(config_.renderStyleProfile); renderer_->setVirtualResolutionEnabled(config_.useVirtualResolution); renderer_->setResolutionScaleMode(config_.resolutionMode); if (config_.virtualWidth > 0 && config_.virtualHeight > 0) { @@ -214,7 +215,9 @@ bool Application::initCoreModules() { ScopedStartupTrace stageTrace("Camera setup"); camera_ = new Camera(); camera_->setViewport(renderer_->getVirtualWidth(), renderer_->getVirtualHeight()); - camera_->setFlipY(true); // 启用 Y 轴翻转,(0,0) 在左上角 + camera_->setFlipY(true); // Use top-left as (0,0). + renderer_->applyRenderStyleToCamera( + camera_, config_.renderStyleProfile, RenderStyleLayerRole::World); renderer_->setCamera(camera_); } diff --git a/Frostbite2D/src/frostbite2D/graphics/batch.cpp b/Frostbite2D/src/frostbite2D/graphics/batch.cpp index acbf1c8..510ae05 100644 --- a/Frostbite2D/src/frostbite2D/graphics/batch.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/batch.cpp @@ -98,15 +98,25 @@ void Batch::submitQuad(const Quad& quad, const Transform2D& transform, flushIfNeeded(newKey, shader, texture); + Renderer& renderer = Renderer::get(); glm::mat4 mat = transform.matrix; + Camera* camera = renderer.getCamera(); + Vec2 snapOffset = Vec2::Zero(); + if (camera && renderer.shouldSnapVertices()) { + glm::vec4 referencePos(quad.vertices[0].position.x, + quad.vertices[0].position.y, 0.0f, 1.0f); + glm::vec4 transformedReference = mat * referencePos; + Vec2 referenceWorld(transformedReference.x, transformedReference.y); + snapOffset = camera->snapWorldPosition(referenceWorld) - referenceWorld; + } uint16_t indexOffset = static_cast(currentBatch_.vertices.size()); for (int i = 0; i < 4; i++) { Vertex v = quad.vertices[i]; glm::vec4 pos(v.position.x, v.position.y, 0.0f, 1.0f); glm::vec4 transformed = mat * pos; - v.position.x = transformed.x; - v.position.y = transformed.y; + v.position.x = transformed.x + snapOffset.x; + v.position.y = transformed.y + snapOffset.y; currentBatch_.vertices.push_back(v); } @@ -161,7 +171,9 @@ void Batch::flushCurrentBatch() { currentBatch_.shader->setTexture("u_texture", 0); } if (currentBatch_.texture) { - currentBatch_.texture->bind(0); + Renderer& renderer = Renderer::get(); + bool preferPixelArtSampling = renderer.shouldUsePixelArtSampling(); + currentBatch_.texture->bind(0, preferPixelArtSampling); } glBindBuffer(GL_ARRAY_BUFFER, vbo_); diff --git a/Frostbite2D/src/frostbite2D/graphics/camera.cpp b/Frostbite2D/src/frostbite2D/graphics/camera.cpp index 309e535..49adaeb 100644 --- a/Frostbite2D/src/frostbite2D/graphics/camera.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/camera.cpp @@ -1,47 +1,80 @@ #include -#include #include +#include +#include namespace frostbite2D { +namespace { + +constexpr float kMinZoom = 0.01f; + +float clampZoom(float zoom) { + return std::max(zoom, kMinZoom); +} + +Vec2 snapPositionToPixelGrid(const Vec2& position, float zoom) { + float safeZoom = clampZoom(zoom); + return Vec2(std::round(position.x * safeZoom) / safeZoom, + std::round(position.y * safeZoom) / safeZoom); +} + +} // namespace + Camera::Camera() : position_(0, 0), zoom_(1.0f) {} -void Camera::setPosition(const Vec2 &pos) { position_ = pos; } +void Camera::setPosition(const Vec2& pos) { position_ = pos; } -void Camera::setZoom(float zoom) { zoom_ = zoom; } +void Camera::setZoom(float zoom) { zoom_ = clampZoom(zoom); } void Camera::setViewport(int width, int height) { viewportWidth_ = width; viewportHeight_ = height; } +Vec2 Camera::getRenderPosition() const { + return snapWorldPosition(position_); +} + +Vec2 Camera::snapWorldPosition(const Vec2& position) const { + if (!pixelSnapEnabled_) { + return position; + } + return snapPositionToPixelGrid(position, zoom_); +} + float Camera::getVisibleWidth() const { - float safeZoom = std::max(zoom_, 0.01f); + float safeZoom = clampZoom(zoom_); return static_cast(viewportWidth_) / safeZoom; } float Camera::getVisibleHeight() const { - float safeZoom = std::max(zoom_, 0.01f); + float safeZoom = clampZoom(zoom_); return static_cast(viewportHeight_) / safeZoom; } -void Camera::lookAt(const Vec2 &target) { position_ = target; } +void Camera::lookAt(const Vec2& target) { position_ = target; } -void Camera::move(const Vec2 &delta) { position_ = position_ + delta; } +void Camera::move(const Vec2& delta) { position_ = position_ + delta; } -void Camera::zoomAt(float factor, const Vec2 &screenPos) { - Vec2 worldBefore = screenPos - position_; - zoom_ *= factor; - Vec2 worldAfter = screenPos - position_; +void Camera::zoomAt(float factor, const Vec2& screenPos) { + float previousZoom = clampZoom(zoom_); + Vec2 worldBefore(position_.x + screenPos.x / previousZoom, + position_.y + screenPos.y / previousZoom); + zoom_ = clampZoom(zoom_ * factor); + Vec2 worldAfter(position_.x + screenPos.x / zoom_, + position_.y + screenPos.y / zoom_); position_ = position_ + (worldBefore - worldAfter); } glm::mat4 Camera::getViewMatrix() const { + float safeZoom = clampZoom(zoom_); + Vec2 renderPosition = getRenderPosition(); + glm::mat4 view = glm::mat4(1.0f); - view[3][0] = -position_.x; - view[3][1] = -position_.y; - view[0][0] = zoom_; - view[1][1] = zoom_; + view = glm::scale(view, glm::vec3(safeZoom, safeZoom, 1.0f)); + view = glm::translate(view, + glm::vec3(-renderPosition.x, -renderPosition.y, 0.0f)); return view; } @@ -49,15 +82,11 @@ glm::mat4 Camera::getProjectionMatrix() const { float left = 0.0f; float right = static_cast(viewportWidth_); float bottom, top; - + if (flipY_) { - // Y 轴向下:(0,0) 在左上角(2D 游戏常用) - // glm::ortho(left, right, bottom, top, ...) - // 这里 bottom 是屏幕底部(值大),top 是屏幕顶部(值小) bottom = static_cast(viewportHeight_); top = 0.0f; } else { - // Y 轴向上:(0,0) 在左下角(OpenGL 默认) bottom = 0.0f; top = static_cast(viewportHeight_); } diff --git a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp index ee43336..238e189 100644 --- a/Frostbite2D/src/frostbite2D/graphics/renderer.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/renderer.cpp @@ -16,6 +16,11 @@ int roundToInt(float value) { return static_cast(std::lround(value)); } +RenderStyleLayerSettings resolveRenderStyleLayer(RenderStyleProfileId profile, + RenderStyleLayerRole role) { + return RenderStyleSettings::FromProfile(profile).layer(role); +} + } // namespace Renderer& Renderer::get() { @@ -23,7 +28,10 @@ Renderer& Renderer::get() { return instance; } -Renderer::Renderer() = default; +Renderer::Renderer() { + activeRenderStyle_ = + resolveRenderStyleLayer(defaultRenderStyleProfile_, activeRenderStyleRole_); +} bool Renderer::init() { if (initialized_) { @@ -157,6 +165,34 @@ void Renderer::clear(uint32_t flags) { glClear(flags); } +void Renderer::setDefaultRenderStyleProfile(RenderStyleProfileId profile) { + defaultRenderStyleProfile_ = profile; + setActiveRenderStyleProfile(profile, activeRenderStyleRole_); +} + +void Renderer::setActiveRenderStyleProfile(RenderStyleProfileId profile, + RenderStyleLayerRole role) { + if (initialized_ && + (activeRenderStyleProfile_ != profile || activeRenderStyleRole_ != role)) { + batch_.flush(); + } + + activeRenderStyleProfile_ = profile; + activeRenderStyleRole_ = role; + activeRenderStyle_ = resolveRenderStyleLayer(profile, role); +} + +void Renderer::applyRenderStyleToCamera(Camera* camera, + RenderStyleProfileId profile, + RenderStyleLayerRole role) const { + if (!camera) { + return; + } + + RenderStyleLayerSettings settings = resolveRenderStyleLayer(profile, role); + camera->setPixelSnapEnabled(settings.cameraPixelSnap); +} + void Renderer::setCamera(Camera* camera) { if (initialized_ && camera_ != camera) { batch_.flush(); @@ -412,7 +448,9 @@ void Renderer::drawQuad(const Vec2& pos, const Size& size, float cr, float cg, void Renderer::drawQuad(const Vec2& pos, const Size& size, Ptr texture) { Rect rect(pos.x, pos.y, size.width, size.height); Quad quad = Quad::createTextured(rect, Rect(0, 0, size.width, size.height), - Vec2(size.width, size.height)); + Vec2(size.width, size.height), + 1.0f, 1.0f, 1.0f, 1.0f, + shouldShrinkSubTextureUVs()); Transform2D transform = Transform2D::identity(); auto* shader = shaderManager_.getShader("sprite"); @@ -437,7 +475,8 @@ void Renderer::drawSprite(const Vec2& pos, const Rect& srcRect, const Color& color) { Rect destRect(pos.x, pos.y, srcRect.width(), srcRect.height()); Quad quad = Quad::createTextured(destRect, srcRect, texSize, color.r, color.g, - color.b, color.a); + color.b, color.a, + shouldShrinkSubTextureUVs()); Transform2D transform = Transform2D::identity(); auto* shader = shaderManager_.getShader("sprite"); diff --git a/Frostbite2D/src/frostbite2D/graphics/texture.cpp b/Frostbite2D/src/frostbite2D/graphics/texture.cpp index b219d53..cc28b8a 100644 --- a/Frostbite2D/src/frostbite2D/graphics/texture.cpp +++ b/Frostbite2D/src/frostbite2D/graphics/texture.cpp @@ -84,6 +84,8 @@ Ptr Texture::loadFromFile(const std::string& path) { auto texture = Ptr(new Texture(width, height, textureID)); texture->path_ = resolvedPath; texture->channels_ = channels; + texture->appliedMinFilter_ = GL_LINEAR; + texture->appliedMagFilter_ = GL_LINEAR; return texture; } @@ -111,6 +113,8 @@ Ptr Texture::createFromMemory(const uint8* data, int width, int height, auto texture = Ptr(new Texture(width, height, textureID)); texture->channels_ = channels; + texture->appliedMinFilter_ = GL_LINEAR; + texture->appliedMagFilter_ = GL_LINEAR; return texture; } @@ -129,12 +133,16 @@ Ptr Texture::createEmpty(int width, int height) { glBindTexture(GL_TEXTURE_2D, 0); - return Ptr(new Texture(width, height, textureID)); + auto texture = Ptr(new Texture(width, height, textureID)); + texture->appliedMinFilter_ = GL_LINEAR; + texture->appliedMagFilter_ = GL_LINEAR; + return texture; } -void Texture::bind(uint32_t slot) { +void Texture::bind(uint32_t slot, bool preferPixelArtSampling) { glActiveTexture(GL_TEXTURE0 + slot); glBindTexture(GL_TEXTURE_2D, textureID_); + applySampling(preferPixelArtSampling); } void Texture::unbind() { @@ -150,9 +158,35 @@ void Texture::setWrapMode(uint32_t wrapS, uint32_t wrapT) { void Texture::setFilterMode(uint32_t minFilter, uint32_t magFilter) { bind(); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter); + applyFilter(minFilter, magFilter); + sampling_ = (minFilter == GL_NEAREST && magFilter == GL_NEAREST) + ? TextureSampling::PixelArt + : TextureSampling::Linear; unbind(); } +void Texture::setSampling(TextureSampling sampling) { + sampling_ = sampling; + appliedMinFilter_ = 0; + appliedMagFilter_ = 0; +} + +void Texture::applyFilter(uint32_t minFilter, uint32_t magFilter) { + if (appliedMinFilter_ == minFilter && appliedMagFilter_ == magFilter) { + return; + } + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter); + appliedMinFilter_ = minFilter; + appliedMagFilter_ = magFilter; +} + +void Texture::applySampling(bool preferPixelArtSampling) { + bool useNearest = + sampling_ == TextureSampling::PixelArt && preferPixelArtSampling; + applyFilter(useNearest ? GL_NEAREST : GL_LINEAR, + useNearest ? GL_NEAREST : GL_LINEAR); +} + } diff --git a/Frostbite2D/src/frostbite2D/scene/scene.cpp b/Frostbite2D/src/frostbite2D/scene/scene.cpp index ae07dab..eb9907a 100644 --- a/Frostbite2D/src/frostbite2D/scene/scene.cpp +++ b/Frostbite2D/src/frostbite2D/scene/scene.cpp @@ -43,6 +43,21 @@ bool Scene::OnEvent(const Event& event) { return false; } +void Scene::SetRenderStyleProfile(RenderStyleProfileId profile) { + renderStyleProfileOverride_ = profile; + hasRenderStyleProfileOverride_ = true; +} + +void Scene::ClearRenderStyleProfileOverride() { + hasRenderStyleProfileOverride_ = false; +} + +RenderStyleProfileId Scene::ResolveRenderStyleProfile( + RenderStyleProfileId defaultProfile) const { + return hasRenderStyleProfileOverride_ ? renderStyleProfileOverride_ + : defaultProfile; +} + bool Scene::dispatchToChildren(const Event& event) { if (event.isPropagationStopped()) { return false; diff --git a/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp b/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp index 1f88563..0a63edc 100644 --- a/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp +++ b/Frostbite2D/src/frostbite2D/scene/scene_manager.cpp @@ -5,6 +5,18 @@ namespace frostbite2D { +namespace { + +RenderStyleProfileId resolveSceneRenderStyleProfile(const Scene* scene, + const Renderer& renderer) { + if (!scene) { + return renderer.getDefaultRenderStyleProfile(); + } + return scene->ResolveRenderStyleProfile(renderer.getDefaultRenderStyleProfile()); +} + +} // namespace + SceneManager& SceneManager::get() { static SceneManager instance; return instance; @@ -123,13 +135,23 @@ void SceneManager::ClearAll() { void SceneManager::Update(float deltaTime) { if (!sceneStack_.empty()) { + Renderer& renderer = Renderer::get(); + RenderStyleProfileId profile = + resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer); + renderer.applyRenderStyleToCamera(renderer.getCamera(), profile, + RenderStyleLayerRole::World); sceneStack_.back()->Update(deltaTime); } } void SceneManager::UpdateUI(float deltaTime) { + Renderer& renderer = Renderer::get(); for (auto& scene : uiSceneStack_) { if (scene) { + RenderStyleProfileId profile = + resolveSceneRenderStyleProfile(scene.Get(), renderer); + renderer.applyRenderStyleToCamera(scene->GetCamera(), profile, + RenderStyleLayerRole::UI); scene->Update(deltaTime); } } @@ -138,8 +160,15 @@ void SceneManager::UpdateUI(float deltaTime) { void SceneManager::Render() { Renderer& renderer = Renderer::get(); Camera* worldCamera = renderer.getCamera(); + RenderStyleProfileId worldProfile = renderer.getDefaultRenderStyleProfile(); if (!sceneStack_.empty()) { + worldProfile = + resolveSceneRenderStyleProfile(sceneStack_.back().Get(), renderer); + renderer.applyRenderStyleToCamera(worldCamera, worldProfile, + RenderStyleLayerRole::World); + renderer.setActiveRenderStyleProfile(worldProfile, + RenderStyleLayerRole::World); sceneStack_.back()->Render(); } @@ -148,6 +177,11 @@ void SceneManager::Render() { continue; } + RenderStyleProfileId uiProfile = + resolveSceneRenderStyleProfile(scene.Get(), renderer); + renderer.applyRenderStyleToCamera(scene->GetCamera(), uiProfile, + RenderStyleLayerRole::UI); + renderer.setActiveRenderStyleProfile(uiProfile, RenderStyleLayerRole::UI); renderer.setCamera(scene->GetCamera()); scene->Render(); } @@ -155,6 +189,7 @@ void SceneManager::Render() { if (renderer.getCamera() != worldCamera) { renderer.setCamera(worldCamera); } + renderer.setActiveRenderStyleProfile(worldProfile, RenderStyleLayerRole::World); } bool SceneManager::DispatchEvent(const Event& event) { diff --git a/Game/src/bootstrap/main.cpp b/Game/src/bootstrap/main.cpp index 6e114f5..9617901 100644 --- a/Game/src/bootstrap/main.cpp +++ b/Game/src/bootstrap/main.cpp @@ -46,6 +46,7 @@ int main(int argc, char **argv) { config.virtualWidth = 1066; config.virtualHeight = 600; config.resolutionMode = ResolutionScaleMode::FitHeight; + config.renderStyleProfile = RenderStyleProfileId::PixelArt2D; Application &app = Application::get(); { diff --git a/Game/src/map/GameMap.cpp b/Game/src/map/GameMap.cpp index 8086ab7..4d06623 100644 --- a/Game/src/map/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -381,7 +381,7 @@ void GameMap::updateLayerPositions(const Vec2 &cameraFocus) { return; } - Vec2 cameraPos = camera->getPosition(); + Vec2 cameraPos = camera->getRenderPosition(); // 主视图平移完全交给底层 // Camera,这里只保留地图层自己的静态校准和少量视差偏移。