feat(动画): 添加角色动作合成纹理功能
实现角色动作的离屏渲染合成功能,支持获取合成纹理及其相关信息: 1. 新增CanvasActor用于离屏渲染 2. 新增RenderTexture封装FBO和纹理 3. 扩展Renderer支持离屏渲染到纹理 4. 为CharacterAnimation添加合成纹理生成逻辑 5. 在调试界面添加合成纹理预览功能
This commit is contained in:
64
Frostbite2D/include/frostbite2D/2d/canvas_actor.h
Normal file
64
Frostbite2D/include/frostbite2D/2d/canvas_actor.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
#include <frostbite2D/graphics/camera.h>
|
||||
#include <frostbite2D/graphics/render_texture.h>
|
||||
#include <functional>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
/// @brief 离屏画布节点。
|
||||
///
|
||||
/// 子节点通过 `AddCanvasChild()` 挂到内部画布树,只会在重绘到离屏纹理时参与渲染,
|
||||
/// 不会像普通场景子节点那样直接显示到屏幕。
|
||||
class CanvasActor : public Sprite {
|
||||
public:
|
||||
CanvasActor();
|
||||
~CanvasActor() override = default;
|
||||
|
||||
bool Init(int width, int height);
|
||||
bool SetCanvasSize(int width, int height);
|
||||
Vec2 GetCanvasSize() const {
|
||||
return Vec2(static_cast<float>(canvasWidth_), static_cast<float>(canvasHeight_));
|
||||
}
|
||||
|
||||
void SetClearColor(const Color& color);
|
||||
const Color& GetClearColor() const { return clearColor_; }
|
||||
|
||||
void SetDirty();
|
||||
bool IsDirty() const { return dirty_; }
|
||||
bool Redraw();
|
||||
void SetCustomDrawCallback(std::function<void()> callback);
|
||||
void ClearCustomDrawCallback();
|
||||
|
||||
void AddCanvasChild(RefPtr<Actor> child);
|
||||
void RemoveCanvasChild(RefPtr<Actor> child);
|
||||
void RemoveAllCanvasChildren();
|
||||
Actor* GetCanvasRoot() const { return canvasRoot_.Get(); }
|
||||
ActorList& GetCanvasChildren() { return canvasRoot_->GetChildren(); }
|
||||
const ActorList& GetCanvasChildren() const { return canvasRoot_->GetChildren(); }
|
||||
|
||||
Ptr<Texture> GetOutputTexture() const;
|
||||
Ptr<RenderTexture> GetRenderTexture() const { return renderTexture_; }
|
||||
bool IsCanvasReady() const { return renderTexture_ && renderTexture_->IsValid(); }
|
||||
Camera& GetCanvasCamera() { return canvasCamera_; }
|
||||
const Camera& GetCanvasCamera() const { return canvasCamera_; }
|
||||
|
||||
void OnUpdate(float deltaTime) override;
|
||||
void Render() override;
|
||||
|
||||
private:
|
||||
bool redrawInternal();
|
||||
void syncCanvasResources();
|
||||
|
||||
RefPtr<Actor> canvasRoot_;
|
||||
Ptr<RenderTexture> renderTexture_ = nullptr;
|
||||
Camera canvasCamera_;
|
||||
Color clearColor_ = Colors::Transparent;
|
||||
int canvasWidth_ = 0;
|
||||
int canvasHeight_ = 0;
|
||||
bool dirty_ = true;
|
||||
std::function<void()> customDrawCallback_;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -44,6 +44,9 @@ public:
|
||||
void InterpolationLogic();
|
||||
|
||||
Vec2 GetMaxSize() const;
|
||||
bool GetStaticLocalBounds(Rect& outBounds) const;
|
||||
bool GetStaticLocalBounds(int direction, Rect& outBounds) const;
|
||||
uint64 GetRenderSignature() const;
|
||||
|
||||
bool IsUsable() const { return usable_; }
|
||||
void SetUsable(bool usable) { usable_ = usable; }
|
||||
|
||||
34
Frostbite2D/include/frostbite2D/graphics/render_texture.h
Normal file
34
Frostbite2D/include/frostbite2D/graphics/render_texture.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/base/RefObject.h>
|
||||
#include <frostbite2D/graphics/texture.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class Renderer;
|
||||
|
||||
/// @brief 2D 离屏渲染目标,封装颜色纹理和对应的 framebuffer。
|
||||
class RenderTexture : public RefObject {
|
||||
public:
|
||||
RenderTexture() = default;
|
||||
~RenderTexture() override;
|
||||
|
||||
bool Init(int width, int height);
|
||||
bool Resize(int width, int height);
|
||||
void Reset();
|
||||
|
||||
bool IsValid() const { return framebufferID_ != 0 && texture_ != nullptr; }
|
||||
int GetWidth() const { return width_; }
|
||||
int GetHeight() const { return height_; }
|
||||
Ptr<Texture> GetTexture() const { return texture_; }
|
||||
|
||||
private:
|
||||
Ptr<Texture> texture_ = nullptr;
|
||||
uint32 framebufferID_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
|
||||
friend class Renderer;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -9,9 +9,12 @@
|
||||
#include <frostbite2D/graphics/texture.h>
|
||||
#include <frostbite2D/graphics/types.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class RenderTexture;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
static Renderer& get();
|
||||
@@ -22,6 +25,10 @@ public:
|
||||
void beginFrame();
|
||||
void endFrame();
|
||||
void flush();
|
||||
bool beginRenderToTexture(RenderTexture& target, Camera* camera,
|
||||
const Color& clearColor = Color(0.0f, 0.0f, 0.0f, 0.0f),
|
||||
bool clear = true);
|
||||
bool endRenderToTexture();
|
||||
|
||||
void setViewport(int x, int y, int width, int height);
|
||||
void setWindowSize(int width, int height, float contentScaleX = 1.0f,
|
||||
@@ -61,6 +68,9 @@ public:
|
||||
const RenderResolutionState& getResolutionState() const {
|
||||
return resolutionState_;
|
||||
}
|
||||
bool isInitialized() const { return initialized_; }
|
||||
bool isFrameActive() const { return frameActive_; }
|
||||
bool isRenderingToTexture() const { return !renderTargetStack_.empty(); }
|
||||
Vec2 screenToVirtual(const Vec2& screenPos) const;
|
||||
Vec2 virtualToScreen(const Vec2& virtualPos) const;
|
||||
|
||||
@@ -107,6 +117,17 @@ private:
|
||||
void recalculateResolutionState();
|
||||
void syncCameraViewport();
|
||||
|
||||
struct RenderTargetState {
|
||||
uint32 framebuffer = 0;
|
||||
int viewportX = 0;
|
||||
int viewportY = 0;
|
||||
int viewportWidth = 1;
|
||||
int viewportHeight = 1;
|
||||
float clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
Camera* camera = nullptr;
|
||||
bool startedStandaloneBatch = false;
|
||||
};
|
||||
|
||||
ShaderManager shaderManager_;
|
||||
Batch batch_;
|
||||
Camera* camera_ = nullptr;
|
||||
@@ -123,6 +144,8 @@ private:
|
||||
RenderResolutionState resolutionState_;
|
||||
|
||||
bool initialized_ = false;
|
||||
bool frameActive_ = false;
|
||||
std::vector<RenderTargetState> renderTargetStack_;
|
||||
|
||||
Renderer(const Renderer&) = delete;
|
||||
Renderer& operator=(const Renderer&) = delete;
|
||||
|
||||
141
Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp
Normal file
141
Frostbite2D/src/frostbite2D/2d/canvas_actor.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include <frostbite2D/2d/canvas_actor.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
CanvasActor::CanvasActor() {
|
||||
canvasRoot_ = MakePtr<Actor>();
|
||||
canvasRoot_->SetName("canvasRoot");
|
||||
}
|
||||
|
||||
bool CanvasActor::Init(int width, int height) {
|
||||
return SetCanvasSize(width, height);
|
||||
}
|
||||
|
||||
bool CanvasActor::SetCanvasSize(int width, int height) {
|
||||
width = std::max(width, 1);
|
||||
height = std::max(height, 1);
|
||||
|
||||
if (!renderTexture_) {
|
||||
renderTexture_ = MakePtr<RenderTexture>();
|
||||
}
|
||||
|
||||
if (!renderTexture_->Resize(width, height)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
canvasWidth_ = width;
|
||||
canvasHeight_ = height;
|
||||
canvasCamera_.setPosition(Vec2::Zero());
|
||||
canvasCamera_.setZoom(1.0f);
|
||||
canvasCamera_.setViewport(canvasWidth_, canvasHeight_);
|
||||
|
||||
syncCanvasResources();
|
||||
dirty_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CanvasActor::SetClearColor(const Color& color) {
|
||||
clearColor_ = color;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void CanvasActor::SetDirty() {
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
bool CanvasActor::Redraw() {
|
||||
dirty_ = true;
|
||||
return redrawInternal();
|
||||
}
|
||||
|
||||
void CanvasActor::SetCustomDrawCallback(std::function<void()> callback) {
|
||||
customDrawCallback_ = std::move(callback);
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void CanvasActor::ClearCustomDrawCallback() {
|
||||
if (!customDrawCallback_) {
|
||||
return;
|
||||
}
|
||||
|
||||
customDrawCallback_ = nullptr;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void CanvasActor::AddCanvasChild(RefPtr<Actor> child) {
|
||||
if (!child || child.Get() == this) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasRoot_->AddChild(child);
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void CanvasActor::RemoveCanvasChild(RefPtr<Actor> child) {
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvasRoot_->RemoveChild(child);
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
void CanvasActor::RemoveAllCanvasChildren() {
|
||||
canvasRoot_->RemoveAllChildren();
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
Ptr<Texture> CanvasActor::GetOutputTexture() const {
|
||||
return renderTexture_ ? renderTexture_->GetTexture() : nullptr;
|
||||
}
|
||||
|
||||
void CanvasActor::OnUpdate(float deltaTime) {
|
||||
if (canvasRoot_) {
|
||||
canvasRoot_->Update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void CanvasActor::Render() {
|
||||
if (dirty_) {
|
||||
redrawInternal();
|
||||
}
|
||||
|
||||
Sprite::Render();
|
||||
}
|
||||
|
||||
bool CanvasActor::redrawInternal() {
|
||||
if (!renderTexture_ || !renderTexture_->IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Renderer& renderer = Renderer::get();
|
||||
if (!renderer.beginRenderToTexture(*renderTexture_, &canvasCamera_, clearColor_)) {
|
||||
dirty_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canvasRoot_) {
|
||||
canvasRoot_->Render();
|
||||
}
|
||||
if (customDrawCallback_) {
|
||||
customDrawCallback_();
|
||||
}
|
||||
|
||||
renderer.endRenderToTexture();
|
||||
dirty_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void CanvasActor::syncCanvasResources() {
|
||||
Ptr<Texture> outputTexture = renderTexture_ ? renderTexture_->GetTexture() : nullptr;
|
||||
if (outputTexture) {
|
||||
outputTexture->setSampling(TextureSampling::PixelArt);
|
||||
}
|
||||
|
||||
SetTexture(outputTexture);
|
||||
SetSize(static_cast<float>(canvasWidth_), static_cast<float>(canvasHeight_));
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -12,6 +12,46 @@ using namespace frostbite2D::animation;
|
||||
namespace frostbite2D {
|
||||
namespace {
|
||||
|
||||
struct BoundsAccumulator {
|
||||
bool valid = false;
|
||||
float minX = 0.0f;
|
||||
float minY = 0.0f;
|
||||
float maxX = 0.0f;
|
||||
float maxY = 0.0f;
|
||||
|
||||
void includePoint(const Vec2& point) {
|
||||
if (!valid) {
|
||||
minX = maxX = point.x;
|
||||
minY = maxY = point.y;
|
||||
valid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
minX = std::min(minX, point.x);
|
||||
minY = std::min(minY, point.y);
|
||||
maxX = std::max(maxX, point.x);
|
||||
maxY = std::max(maxY, point.y);
|
||||
}
|
||||
|
||||
void includeRect(const Rect& rect) {
|
||||
if (rect.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
includePoint(Vec2(rect.left(), rect.top()));
|
||||
includePoint(Vec2(rect.right(), rect.top()));
|
||||
includePoint(Vec2(rect.left(), rect.bottom()));
|
||||
includePoint(Vec2(rect.right(), rect.bottom()));
|
||||
}
|
||||
|
||||
Rect toRect() const {
|
||||
if (!valid) {
|
||||
return Rect();
|
||||
}
|
||||
return Rect(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
};
|
||||
|
||||
Vec2 resolveFramePosition(const AniFrame& frameInfo, const Vec2& imageRate) {
|
||||
return Vec2(frameInfo.imgPos.x * imageRate.x, frameInfo.imgPos.y * imageRate.y);
|
||||
}
|
||||
@@ -37,6 +77,64 @@ BlendMode resolveBlendMode(const AniFrame& frameInfo) {
|
||||
return BlendMode::Normal;
|
||||
}
|
||||
|
||||
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
|
||||
BoundsAccumulator bounds;
|
||||
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.top())));
|
||||
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.top())));
|
||||
bounds.includePoint(transform.transformPoint(Vec2(rect.left(), rect.bottom())));
|
||||
bounds.includePoint(transform.transformPoint(Vec2(rect.right(), rect.bottom())));
|
||||
return bounds.toRect();
|
||||
}
|
||||
|
||||
bool computeAnimationFrameBounds(const Animation& animation,
|
||||
int frameIndex,
|
||||
int direction,
|
||||
Rect& outBounds) {
|
||||
if (frameIndex < 0 ||
|
||||
frameIndex >= static_cast<int>(animation.frames_.size()) ||
|
||||
frameIndex >= static_cast<int>(animation.spriteFrames_.size()) ||
|
||||
frameIndex >= static_cast<int>(animation.spriteFrameOffsets_.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const AniFrame& frameInfo = animation.frames_[frameIndex];
|
||||
const auto& sprite = animation.spriteFrames_[frameIndex];
|
||||
if (!sprite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec2 spriteSize = sprite->GetSize();
|
||||
if (spriteSize.x <= 0.0f || spriteSize.y <= 0.0f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vec2 imageRate = resolveImageRate(frameInfo);
|
||||
float rotation = resolveFrameRotation(frameInfo);
|
||||
Vec2 framePos = resolveFramePosition(frameInfo, imageRate);
|
||||
Vec2 offset = animation.spriteFrameOffsets_[frameIndex];
|
||||
float mirroredRotation = rotation;
|
||||
if (direction < 0) {
|
||||
float visualWidth = spriteSize.x * std::abs(imageRate.x);
|
||||
framePos.x = -framePos.x;
|
||||
offset.x = -offset.x - visualWidth;
|
||||
mirroredRotation = -rotation;
|
||||
}
|
||||
|
||||
Transform2D transform =
|
||||
Transform2D::translation(offset) *
|
||||
Transform2D::translation(framePos) *
|
||||
Transform2D::rotation(mirroredRotation) *
|
||||
Transform2D::scaling(imageRate.x, imageRate.y);
|
||||
outBounds = transformRectBounds(Rect(0.0f, 0.0f, spriteSize.x, spriteSize.y),
|
||||
transform);
|
||||
return !outBounds.empty();
|
||||
}
|
||||
|
||||
void combineHash(uint64& seed, uint64 value) {
|
||||
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
|
||||
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Animation::Animation() {
|
||||
@@ -346,6 +444,66 @@ Vec2 Animation::GetMaxSize() const {
|
||||
return maxSize_;
|
||||
}
|
||||
|
||||
bool Animation::GetStaticLocalBounds(Rect& outBounds) const {
|
||||
BoundsAccumulator bounds;
|
||||
Rect directionalBounds;
|
||||
if (GetStaticLocalBounds(1, directionalBounds)) {
|
||||
bounds.includeRect(directionalBounds);
|
||||
}
|
||||
if (GetStaticLocalBounds(-1, directionalBounds)) {
|
||||
bounds.includeRect(directionalBounds);
|
||||
}
|
||||
|
||||
outBounds = bounds.toRect();
|
||||
return bounds.valid;
|
||||
}
|
||||
|
||||
bool Animation::GetStaticLocalBounds(int direction, Rect& outBounds) const {
|
||||
BoundsAccumulator bounds;
|
||||
|
||||
for (int i = 0; i < static_cast<int>(frames_.size()); ++i) {
|
||||
Rect frameBounds;
|
||||
if (computeAnimationFrameBounds(*this, i, direction, frameBounds)) {
|
||||
bounds.includeRect(frameBounds);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& child : GetChildren()) {
|
||||
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
|
||||
if (!subAnimation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect childBounds;
|
||||
if (!subAnimation->GetStaticLocalBounds(direction, childBounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bounds.includeRect(transformRectBounds(childBounds, subAnimation->GetLocalTransform()));
|
||||
}
|
||||
|
||||
outBounds = bounds.toRect();
|
||||
return bounds.valid;
|
||||
}
|
||||
|
||||
uint64 Animation::GetRenderSignature() const {
|
||||
uint64 signature = 1469598103934665603ULL;
|
||||
combineHash(signature, static_cast<uint64>(usable_));
|
||||
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
|
||||
combineHash(signature, static_cast<uint64>(std::max(currentFrameIndex_, 0)));
|
||||
combineHash(signature, static_cast<uint64>(std::max(totalFrameCount_, 0)));
|
||||
|
||||
for (const auto& child : GetChildren()) {
|
||||
auto* subAnimation = dynamic_cast<Animation*>(child.Get());
|
||||
if (!subAnimation) {
|
||||
continue;
|
||||
}
|
||||
combineHash(signature, subAnimation->GetRenderSignature());
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
void Animation::ApplyFramePresentation(const Vec2& framePos,
|
||||
const Vec2& imageRate,
|
||||
float rotation,
|
||||
|
||||
67
Frostbite2D/src/frostbite2D/graphics/render_texture.cpp
Normal file
67
Frostbite2D/src/frostbite2D/graphics/render_texture.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include <frostbite2D/graphics/render_texture.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
RenderTexture::~RenderTexture() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
bool RenderTexture::Init(int width, int height) {
|
||||
return Resize(width, height);
|
||||
}
|
||||
|
||||
bool RenderTexture::Resize(int width, int height) {
|
||||
width = std::max(width, 1);
|
||||
height = std::max(height, 1);
|
||||
|
||||
if (framebufferID_ != 0 && texture_ && width_ == width && height_ == height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Reset();
|
||||
|
||||
texture_ = Texture::createEmpty(width, height);
|
||||
if (!texture_) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"RenderTexture: failed to create color texture %dx%d",
|
||||
width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenFramebuffers(1, &framebufferID_);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID_);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
texture_->getID(), 0);
|
||||
|
||||
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"RenderTexture: framebuffer incomplete (status=0x%x)",
|
||||
static_cast<unsigned int>(status));
|
||||
Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderTexture::Reset() {
|
||||
texture_.Reset();
|
||||
|
||||
if (framebufferID_ != 0) {
|
||||
glDeleteFramebuffers(1, &framebufferID_);
|
||||
framebufferID_ = 0;
|
||||
}
|
||||
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/graphics/render_texture.h>
|
||||
#include <glad/glad.h>
|
||||
#include <glm/gtc/type_ptr.hpp>
|
||||
|
||||
@@ -60,9 +61,11 @@ bool Renderer::init() {
|
||||
}
|
||||
|
||||
void Renderer::shutdown() {
|
||||
renderTargetStack_.clear();
|
||||
batch_.shutdown();
|
||||
shaderManager_.shutdown();
|
||||
initialized_ = false;
|
||||
frameActive_ = false;
|
||||
}
|
||||
|
||||
void Renderer::beginFrame() {
|
||||
@@ -74,16 +77,109 @@ void Renderer::beginFrame() {
|
||||
|
||||
updateUniforms();
|
||||
batch_.begin();
|
||||
frameActive_ = true;
|
||||
}
|
||||
|
||||
void Renderer::endFrame() {
|
||||
while (!renderTargetStack_.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Renderer: endFrame() auto-closing unfinished render target scope");
|
||||
endRenderToTexture();
|
||||
}
|
||||
batch_.end();
|
||||
frameActive_ = false;
|
||||
}
|
||||
|
||||
void Renderer::flush() {
|
||||
batch_.flush();
|
||||
}
|
||||
|
||||
bool Renderer::beginRenderToTexture(RenderTexture& target, Camera* camera,
|
||||
const Color& clearColor, bool clear) {
|
||||
if (!initialized_) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Renderer: beginRenderToTexture called before renderer init");
|
||||
return false;
|
||||
}
|
||||
if (!target.IsValid()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Renderer: beginRenderToTexture called with invalid target");
|
||||
return false;
|
||||
}
|
||||
|
||||
Camera* activeCamera = camera ? camera : camera_;
|
||||
if (!activeCamera) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Renderer: beginRenderToTexture requires an active camera");
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderTargetState state;
|
||||
GLint framebufferBinding = 0;
|
||||
GLint viewport[4] = {};
|
||||
GLfloat clearColorValue[4] = {};
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebufferBinding);
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
glGetFloatv(GL_COLOR_CLEAR_VALUE, clearColorValue);
|
||||
state.framebuffer = static_cast<uint32>(framebufferBinding);
|
||||
state.viewportX = viewport[0];
|
||||
state.viewportY = viewport[1];
|
||||
state.viewportWidth = std::max(viewport[2], 1);
|
||||
state.viewportHeight = std::max(viewport[3], 1);
|
||||
state.clearColor[0] = clearColorValue[0];
|
||||
state.clearColor[1] = clearColorValue[1];
|
||||
state.clearColor[2] = clearColorValue[2];
|
||||
state.clearColor[3] = clearColorValue[3];
|
||||
state.camera = camera_;
|
||||
|
||||
if (frameActive_ || !renderTargetStack_.empty()) {
|
||||
batch_.flush();
|
||||
} else {
|
||||
batch_.begin();
|
||||
state.startedStandaloneBatch = true;
|
||||
}
|
||||
|
||||
renderTargetStack_.push_back(state);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebufferID_);
|
||||
glViewport(0, 0, std::max(target.width_, 1), std::max(target.height_, 1));
|
||||
if (clear) {
|
||||
glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
camera_ = activeCamera;
|
||||
updateUniforms();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Renderer::endRenderToTexture() {
|
||||
if (renderTargetStack_.empty()) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Renderer: endRenderToTexture called without matching begin");
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderTargetState state = renderTargetStack_.back();
|
||||
renderTargetStack_.pop_back();
|
||||
|
||||
if (state.startedStandaloneBatch) {
|
||||
batch_.end();
|
||||
} else {
|
||||
batch_.flush();
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer);
|
||||
glViewport(state.viewportX, state.viewportY, state.viewportWidth,
|
||||
state.viewportHeight);
|
||||
glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
|
||||
state.clearColor[3]);
|
||||
|
||||
camera_ = state.camera;
|
||||
updateUniforms();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::setViewport(int x, int y, int width, int height) {
|
||||
resolutionState_.viewportX = x;
|
||||
resolutionState_.viewportY = y;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "character/CharacterDataLoader.h"
|
||||
#include "character/CharacterEquipmentManager.h"
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/2d/canvas_actor.h>
|
||||
#include <frostbite2D/animation/animation.h>
|
||||
#include <frostbite2D/base/RefPtr.h>
|
||||
#include <functional>
|
||||
@@ -21,9 +22,20 @@ public:
|
||||
using ActionFrameFlagCallback = std::function<void(int)>;
|
||||
using ActionEndCallback = std::function<void()>;
|
||||
|
||||
struct CompositeFrameInfo {
|
||||
bool valid = false;
|
||||
Rect localBounds;
|
||||
Vec2 originInTexture = Vec2::Zero();
|
||||
Vec2 groundAnchorInTexture = Vec2::Zero();
|
||||
int width = 1;
|
||||
int height = 1;
|
||||
};
|
||||
|
||||
bool Init(CharacterObject* parent,
|
||||
const character::CharacterConfig& config,
|
||||
const CharacterEquipmentManager& equipmentManager);
|
||||
void Update(float deltaTime) override;
|
||||
void Render() override;
|
||||
|
||||
bool SetAction(const std::string& actionName);
|
||||
void SetDirection(int direction);
|
||||
@@ -39,6 +51,12 @@ public:
|
||||
animation::AniFrame GetCurrentFrameInfo() const;
|
||||
void SetActionFrameFlagCallback(ActionFrameFlagCallback callback);
|
||||
void SetActionEndCallback(ActionEndCallback callback);
|
||||
bool HasCompositeTexture() const;
|
||||
Ptr<Texture> GetCompositeTexture() const;
|
||||
Vec2 GetCompositeTextureSize() const;
|
||||
Vec2 GetCompositeOriginInTexture() const;
|
||||
Vec2 GetCompositeGroundAnchorInTexture() const;
|
||||
uint64 GetCompositeVersion() const { return compositeVersion_; }
|
||||
|
||||
private:
|
||||
static std::string FormatImgPath(std::string path, Animation::ReplaceData data);
|
||||
@@ -46,6 +64,15 @@ private:
|
||||
Animation* GetCurrentPrimaryAnimation() const;
|
||||
void RefreshRuntimeCallbacks();
|
||||
bool shouldSkipMissingAnimation(const std::string& aniPath);
|
||||
void BuildCompositeActionFrames();
|
||||
CompositeFrameInfo ComputeCompositeFrameInfo(const std::vector<RefPtr<Animation>>& animations,
|
||||
int direction) const;
|
||||
CompositeFrameInfo GetCurrentCompositeFrameInfo() const;
|
||||
uint64 CaptureCurrentRenderSignature() const;
|
||||
void MarkCompositeDirty();
|
||||
void UpdateCompositeCamera();
|
||||
void RefreshCompositeTextureIfNeeded();
|
||||
void RenderCurrentActionToCompositeCanvas();
|
||||
|
||||
void CreateAnimationBySlot(const std::string& actionName,
|
||||
const std::string& slotName,
|
||||
@@ -61,6 +88,11 @@ private:
|
||||
ActionEndCallback actionEndCallback_;
|
||||
std::unordered_set<std::string> missingAnimationPaths_;
|
||||
size_t skippedMissingAnimationCount_ = 0;
|
||||
RefPtr<CanvasActor> compositeCanvas_ = nullptr;
|
||||
std::map<std::string, CompositeFrameInfo> compositeFrameInfoByAction_;
|
||||
bool compositeDirty_ = true;
|
||||
uint64 compositeVersion_ = 0;
|
||||
uint64 lastCompositeSignature_ = 0;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -141,6 +141,12 @@ public:
|
||||
int GetCurrentAnimationFrameCount() const;
|
||||
float GetCurrentAnimationProgressNormalized() const;
|
||||
animation::AniFrame GetCurrentAnimationFrameInfo() const;
|
||||
bool HasCompositeTexture() const;
|
||||
Ptr<Texture> GetCompositeTexture() const;
|
||||
Vec2 GetCompositeTextureSize() const;
|
||||
Vec2 GetCompositeOriginInTexture() const;
|
||||
Vec2 GetCompositeGroundAnchorInTexture() const;
|
||||
uint64 GetCompositeTextureVersion() const;
|
||||
|
||||
/// @brief 绑定当前动作主动画的运行时回调,供后续脚本系统接入。
|
||||
void SetAnimationFrameFlagCallback(CharacterAnimation::ActionFrameFlagCallback callback);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <frostbite2D/2d/actor.h>
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
|
||||
namespace frostbite2D {
|
||||
|
||||
class CharacterObject;
|
||||
class GameMap;
|
||||
class NineSliceActor;
|
||||
class Sprite;
|
||||
class TextSprite;
|
||||
|
||||
/**
|
||||
@@ -33,6 +35,11 @@ public:
|
||||
private:
|
||||
void initOverlay();
|
||||
void updateOverlay();
|
||||
void updateMapDebugHighlight();
|
||||
void updateCompositePreview();
|
||||
Vec2 computeScreenMatchedPreviewSize(const Vec2& textureSize) const;
|
||||
void updateCompositePreviewMarker(const Vec2& textureSize, const Vec2& previewSize,
|
||||
const Vec2& originInTexture);
|
||||
void setOverlayVisible(bool visible);
|
||||
|
||||
GameDebugActor();
|
||||
@@ -42,6 +49,13 @@ private:
|
||||
CharacterObject* trackedCharacter_ = nullptr;
|
||||
RefPtr<NineSliceActor> background_;
|
||||
RefPtr<TextSprite> coordText_;
|
||||
RefPtr<TextSprite> actionText_;
|
||||
RefPtr<TextSprite> compositeText_;
|
||||
RefPtr<Sprite> compositePreview_;
|
||||
RefPtr<Actor> compositeOriginMarker_;
|
||||
CharacterObject* previewCharacter_ = nullptr;
|
||||
uint64 previewCompositeVersion_ = 0;
|
||||
bool previewTextureAvailable_ = false;
|
||||
};
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "character/CharacterAnimation.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/resource/pvf_archive.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
@@ -32,6 +34,82 @@ std::string actionTail(const std::string& path) {
|
||||
return path.substr(slashPos);
|
||||
}
|
||||
|
||||
struct BoundsAccumulator {
|
||||
bool valid = false;
|
||||
float minX = 0.0f;
|
||||
float minY = 0.0f;
|
||||
float maxX = 0.0f;
|
||||
float maxY = 0.0f;
|
||||
|
||||
void includeRect(const Rect& rect) {
|
||||
if (rect.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
minX = rect.left();
|
||||
minY = rect.top();
|
||||
maxX = rect.right();
|
||||
maxY = rect.bottom();
|
||||
valid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
minX = std::min(minX, rect.left());
|
||||
minY = std::min(minY, rect.top());
|
||||
maxX = std::max(maxX, rect.right());
|
||||
maxY = std::max(maxY, rect.bottom());
|
||||
}
|
||||
};
|
||||
|
||||
Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
|
||||
Vec2 p0 = transform.transformPoint(Vec2(rect.left(), rect.top()));
|
||||
Vec2 p1 = transform.transformPoint(Vec2(rect.right(), rect.top()));
|
||||
Vec2 p2 = transform.transformPoint(Vec2(rect.left(), rect.bottom()));
|
||||
Vec2 p3 = transform.transformPoint(Vec2(rect.right(), rect.bottom()));
|
||||
|
||||
float minX = std::min(std::min(p0.x, p1.x), std::min(p2.x, p3.x));
|
||||
float minY = std::min(std::min(p0.y, p1.y), std::min(p2.y, p3.y));
|
||||
float maxX = std::max(std::max(p0.x, p1.x), std::max(p2.x, p3.x));
|
||||
float maxY = std::max(std::max(p0.y, p1.y), std::max(p2.y, p3.y));
|
||||
return Rect(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
void combineHash(uint64& seed, uint64 value) {
|
||||
constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
|
||||
seed ^= value + kOffset + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
CharacterAnimation::CompositeFrameInfo combineCompositeFrameInfo(
|
||||
const CharacterAnimation::CompositeFrameInfo& lhs,
|
||||
const CharacterAnimation::CompositeFrameInfo& rhs) {
|
||||
if (!lhs.valid) {
|
||||
return rhs;
|
||||
}
|
||||
if (!rhs.valid) {
|
||||
return lhs;
|
||||
}
|
||||
|
||||
float left = std::floor(std::min(lhs.localBounds.left(), rhs.localBounds.left()));
|
||||
float top = std::floor(std::min(lhs.localBounds.top(), rhs.localBounds.top()));
|
||||
float right = std::ceil(std::max(lhs.localBounds.right(), rhs.localBounds.right()));
|
||||
float bottom =
|
||||
std::ceil(std::max(lhs.localBounds.bottom(), rhs.localBounds.bottom()));
|
||||
|
||||
CharacterAnimation::CompositeFrameInfo result;
|
||||
result.valid = true;
|
||||
result.localBounds =
|
||||
Rect(left, top, std::max(right - left, 1.0f), std::max(bottom - top, 1.0f));
|
||||
result.width =
|
||||
std::max(static_cast<int>(std::lround(result.localBounds.width())), 1);
|
||||
result.height =
|
||||
std::max(static_cast<int>(std::lround(result.localBounds.height())), 1);
|
||||
result.originInTexture =
|
||||
Vec2(-result.localBounds.left(), -result.localBounds.top());
|
||||
result.groundAnchorInTexture = result.originInTexture;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CharacterAnimation::Init(CharacterObject* parent,
|
||||
@@ -45,6 +123,21 @@ bool CharacterAnimation::Init(CharacterObject* parent,
|
||||
direction_ = 1;
|
||||
actionFrameFlagCallback_ = nullptr;
|
||||
actionEndCallback_ = nullptr;
|
||||
compositeCanvas_ = MakePtr<CanvasActor>();
|
||||
compositeFrameInfoByAction_.clear();
|
||||
compositeDirty_ = true;
|
||||
compositeVersion_ = 0;
|
||||
lastCompositeSignature_ = 0;
|
||||
|
||||
if (!compositeCanvas_->Init(1, 1)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CharacterAnimation: failed to initialize composite canvas");
|
||||
compositeCanvas_.Reset();
|
||||
} else {
|
||||
compositeCanvas_->SetCustomDrawCallback([this]() {
|
||||
RenderCurrentActionToCompositeCanvas();
|
||||
});
|
||||
}
|
||||
|
||||
for (const auto& [actionName, actionPath] : config.animationPath) {
|
||||
for (const char* slotName : kAvatarParts) {
|
||||
@@ -65,9 +158,27 @@ bool CharacterAnimation::Init(CharacterObject* parent,
|
||||
config.jobId);
|
||||
}
|
||||
|
||||
BuildCompositeActionFrames();
|
||||
return true;
|
||||
}
|
||||
|
||||
void CharacterAnimation::Update(float deltaTime) {
|
||||
Actor::Update(deltaTime);
|
||||
|
||||
uint64 renderSignature = CaptureCurrentRenderSignature();
|
||||
if (renderSignature != lastCompositeSignature_) {
|
||||
compositeDirty_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterAnimation::Render() {
|
||||
if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) {
|
||||
RefreshCompositeTextureIfNeeded();
|
||||
}
|
||||
|
||||
Actor::Render();
|
||||
}
|
||||
|
||||
std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) {
|
||||
size_t pos = path.find("%04d");
|
||||
if (pos != std::string::npos) {
|
||||
@@ -262,6 +373,7 @@ bool CharacterAnimation::SetAction(const std::string& actionName) {
|
||||
currentActionTag_ = actionName;
|
||||
RefreshRuntimeCallbacks();
|
||||
SetDirection(direction_);
|
||||
MarkCompositeDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -278,6 +390,8 @@ void CharacterAnimation::SetDirection(int direction) {
|
||||
animation->SetDirection(direction_);
|
||||
}
|
||||
}
|
||||
|
||||
MarkCompositeDirty();
|
||||
}
|
||||
|
||||
bool CharacterAnimation::IsCurrentActionFinished() const {
|
||||
@@ -327,4 +441,158 @@ void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
|
||||
RefreshRuntimeCallbacks();
|
||||
}
|
||||
|
||||
bool CharacterAnimation::HasCompositeTexture() const {
|
||||
return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
|
||||
GetCurrentCompositeFrameInfo().valid;
|
||||
}
|
||||
|
||||
Ptr<Texture> CharacterAnimation::GetCompositeTexture() const {
|
||||
return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr;
|
||||
}
|
||||
|
||||
Vec2 CharacterAnimation::GetCompositeTextureSize() const {
|
||||
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
|
||||
return Vec2(static_cast<float>(info.width), static_cast<float>(info.height));
|
||||
}
|
||||
|
||||
Vec2 CharacterAnimation::GetCompositeOriginInTexture() const {
|
||||
return GetCurrentCompositeFrameInfo().originInTexture;
|
||||
}
|
||||
|
||||
Vec2 CharacterAnimation::GetCompositeGroundAnchorInTexture() const {
|
||||
return GetCurrentCompositeFrameInfo().groundAnchorInTexture;
|
||||
}
|
||||
|
||||
CharacterAnimation::CompositeFrameInfo CharacterAnimation::ComputeCompositeFrameInfo(
|
||||
const std::vector<RefPtr<Animation>>& animations, int direction) const {
|
||||
CompositeFrameInfo info;
|
||||
BoundsAccumulator bounds;
|
||||
|
||||
for (const auto& animation : animations) {
|
||||
if (!animation) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Rect animationBounds;
|
||||
if (!animation->GetStaticLocalBounds(direction, animationBounds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bounds.includeRect(
|
||||
transformRectBounds(animationBounds, animation->GetLocalTransform()));
|
||||
}
|
||||
|
||||
if (!bounds.valid) {
|
||||
return info;
|
||||
}
|
||||
|
||||
float left = std::floor(bounds.minX);
|
||||
float top = std::floor(bounds.minY);
|
||||
float right = std::ceil(bounds.maxX);
|
||||
float bottom = std::ceil(bounds.maxY);
|
||||
|
||||
info.valid = true;
|
||||
info.localBounds = Rect(left, top, std::max(right - left, 1.0f),
|
||||
std::max(bottom - top, 1.0f));
|
||||
info.width = std::max(static_cast<int>(std::lround(info.localBounds.width())), 1);
|
||||
info.height = std::max(static_cast<int>(std::lround(info.localBounds.height())), 1);
|
||||
info.originInTexture = Vec2(-info.localBounds.left(), -info.localBounds.top());
|
||||
info.groundAnchorInTexture = info.originInTexture;
|
||||
return info;
|
||||
}
|
||||
|
||||
void CharacterAnimation::BuildCompositeActionFrames() {
|
||||
compositeFrameInfoByAction_.clear();
|
||||
for (const auto& [actionName, animations] : actionAnimations_) {
|
||||
CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(animations, 1);
|
||||
CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(animations, -1);
|
||||
compositeFrameInfoByAction_[actionName] =
|
||||
combineCompositeFrameInfo(rightInfo, leftInfo);
|
||||
}
|
||||
}
|
||||
|
||||
CharacterAnimation::CompositeFrameInfo CharacterAnimation::GetCurrentCompositeFrameInfo() const {
|
||||
auto it = compositeFrameInfoByAction_.find(currentActionTag_);
|
||||
if (it == compositeFrameInfoByAction_.end()) {
|
||||
return CompositeFrameInfo();
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
uint64 CharacterAnimation::CaptureCurrentRenderSignature() const {
|
||||
uint64 signature = 1469598103934665603ULL;
|
||||
combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
|
||||
combineHash(signature, static_cast<uint64>(std::hash<std::string>{}(currentActionTag_)));
|
||||
|
||||
auto currentIt = actionAnimations_.find(currentActionTag_);
|
||||
if (currentIt == actionAnimations_.end()) {
|
||||
return signature;
|
||||
}
|
||||
|
||||
for (const auto& animation : currentIt->second) {
|
||||
if (!animation) {
|
||||
continue;
|
||||
}
|
||||
combineHash(signature, animation->GetRenderSignature());
|
||||
}
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
void CharacterAnimation::MarkCompositeDirty() {
|
||||
compositeDirty_ = true;
|
||||
if (compositeCanvas_) {
|
||||
compositeCanvas_->SetDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterAnimation::UpdateCompositeCamera() {
|
||||
if (!compositeCanvas_) {
|
||||
return;
|
||||
}
|
||||
|
||||
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
|
||||
Camera& camera = compositeCanvas_->GetCanvasCamera();
|
||||
camera.setViewport(std::max(info.width, 1), std::max(info.height, 1));
|
||||
camera.setZoom(1.0f);
|
||||
Vec2 cameraOrigin = GetWorldTransform().transformPoint(
|
||||
Vec2(info.localBounds.left(), info.localBounds.top()));
|
||||
camera.setPosition(cameraOrigin);
|
||||
}
|
||||
|
||||
void CharacterAnimation::RefreshCompositeTextureIfNeeded() {
|
||||
if (!compositeDirty_ || !compositeCanvas_) {
|
||||
return;
|
||||
}
|
||||
|
||||
CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
|
||||
if (!info.valid) {
|
||||
compositeDirty_ = false;
|
||||
lastCompositeSignature_ = CaptureCurrentRenderSignature();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!compositeCanvas_->SetCanvasSize(std::max(info.width, 1),
|
||||
std::max(info.height, 1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateCompositeCamera();
|
||||
if (!compositeCanvas_->Redraw()) {
|
||||
return;
|
||||
}
|
||||
|
||||
compositeDirty_ = false;
|
||||
lastCompositeSignature_ = CaptureCurrentRenderSignature();
|
||||
++compositeVersion_;
|
||||
}
|
||||
|
||||
void CharacterAnimation::RenderCurrentActionToCompositeCanvas() {
|
||||
if (actionAnimations_.find(currentActionTag_) == actionAnimations_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
RenderChildren();
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
@@ -404,6 +404,32 @@ animation::AniFrame CharacterObject::GetCurrentAnimationFrameInfo() const {
|
||||
return animationManager_ ? animationManager_->GetCurrentFrameInfo() : animation::AniFrame();
|
||||
}
|
||||
|
||||
bool CharacterObject::HasCompositeTexture() const {
|
||||
return animationManager_ && animationManager_->HasCompositeTexture();
|
||||
}
|
||||
|
||||
Ptr<Texture> CharacterObject::GetCompositeTexture() const {
|
||||
return animationManager_ ? animationManager_->GetCompositeTexture() : nullptr;
|
||||
}
|
||||
|
||||
Vec2 CharacterObject::GetCompositeTextureSize() const {
|
||||
return animationManager_ ? animationManager_->GetCompositeTextureSize() : Vec2::Zero();
|
||||
}
|
||||
|
||||
Vec2 CharacterObject::GetCompositeOriginInTexture() const {
|
||||
return animationManager_ ? animationManager_->GetCompositeOriginInTexture()
|
||||
: Vec2::Zero();
|
||||
}
|
||||
|
||||
Vec2 CharacterObject::GetCompositeGroundAnchorInTexture() const {
|
||||
return animationManager_ ? animationManager_->GetCompositeGroundAnchorInTexture()
|
||||
: Vec2::Zero();
|
||||
}
|
||||
|
||||
uint64 CharacterObject::GetCompositeTextureVersion() const {
|
||||
return animationManager_ ? animationManager_->GetCompositeVersion() : 0;
|
||||
}
|
||||
|
||||
void CharacterObject::SetAnimationFrameFlagCallback(
|
||||
CharacterAnimation::ActionFrameFlagCallback callback) {
|
||||
if (animationManager_) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "common/debug/GameDebugActor.h"
|
||||
#include "common/debug/GameDebugActor.h"
|
||||
#include "character/CharacterObject.h"
|
||||
#include "map/GameMap.h"
|
||||
#include "ui/NineSliceActor.h"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <frostbite2D/2d/sprite.h>
|
||||
#include <frostbite2D/2d/text_sprite.h>
|
||||
#include <frostbite2D/graphics/renderer.h>
|
||||
#include <frostbite2D/scene/scene.h>
|
||||
#include <limits>
|
||||
|
||||
@@ -15,8 +19,38 @@ constexpr float kDebugHudMarginX = 12.0f;
|
||||
constexpr float kDebugHudMarginY = 12.0f;
|
||||
constexpr float kDebugHudPaddingX = 8.0f;
|
||||
constexpr float kDebugHudPaddingY = 6.0f;
|
||||
constexpr float kDebugHudLineGap = 4.0f;
|
||||
constexpr float kDebugHudPreviewGap = 8.0f;
|
||||
constexpr float kDebugHudOriginMarkerHalfExtent = 3.0f;
|
||||
constexpr char kDebugHudPopupImg[] = "sprite/interface/newstyle/windows/popup/popup.img";
|
||||
|
||||
class DebugCrosshairActor : public Actor {
|
||||
public:
|
||||
void Render() override {
|
||||
if (!IsVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Renderer& renderer = Renderer::get();
|
||||
Vec2 center = GetWorldTransform().transformPoint(Vec2::Zero());
|
||||
Color color = Colors::Yellow;
|
||||
color.a *= GetWorldOpacity();
|
||||
if (color.a <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
float lineLength = kDebugHudOriginMarkerHalfExtent * 2.0f + 1.0f;
|
||||
renderer.drawQuad(
|
||||
Rect(center.x - kDebugHudOriginMarkerHalfExtent, center.y, lineLength,
|
||||
1.0f),
|
||||
color);
|
||||
renderer.drawQuad(
|
||||
Rect(center.x, center.y - kDebugHudOriginMarkerHalfExtent, 1.0f,
|
||||
lineLength),
|
||||
color);
|
||||
}
|
||||
};
|
||||
|
||||
void ConfigureTextLine(RefPtr<TextSprite> textSprite, const char* name,
|
||||
int zOrder) {
|
||||
if (!textSprite) {
|
||||
@@ -83,6 +117,11 @@ void GameDebugActor::SetDebugMap(GameMap* map) {
|
||||
}
|
||||
|
||||
void GameDebugActor::SetTrackedCharacter(CharacterObject* character) {
|
||||
if (trackedCharacter_ != character) {
|
||||
previewCharacter_ = nullptr;
|
||||
previewCompositeVersion_ = 0;
|
||||
previewTextureAvailable_ = false;
|
||||
}
|
||||
trackedCharacter_ = character;
|
||||
updateOverlay();
|
||||
}
|
||||
@@ -93,6 +132,16 @@ void GameDebugActor::ClearDebugContext() {
|
||||
}
|
||||
debugMap_ = nullptr;
|
||||
trackedCharacter_ = nullptr;
|
||||
previewCharacter_ = nullptr;
|
||||
previewCompositeVersion_ = 0;
|
||||
previewTextureAvailable_ = false;
|
||||
if (compositePreview_) {
|
||||
compositePreview_->SetTexture(nullptr);
|
||||
compositePreview_->SetSize(0.0f, 0.0f);
|
||||
}
|
||||
if (compositeOriginMarker_) {
|
||||
compositeOriginMarker_->SetVisible(false);
|
||||
}
|
||||
setOverlayVisible(false);
|
||||
}
|
||||
|
||||
@@ -102,7 +151,7 @@ void GameDebugActor::OnUpdate(float deltaTime) {
|
||||
}
|
||||
|
||||
void GameDebugActor::initOverlay() {
|
||||
if (background_ || coordText_) {
|
||||
if (background_ || coordText_ || actionText_ || compositeText_ || compositePreview_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,43 +169,233 @@ void GameDebugActor::initOverlay() {
|
||||
ConfigureTextLine(coordText_, "debugCoordText", 1);
|
||||
AddChild(coordText_);
|
||||
|
||||
actionText_ = TextSprite::create();
|
||||
ConfigureTextLine(actionText_, "debugActionText", 1);
|
||||
AddChild(actionText_);
|
||||
|
||||
compositeText_ = TextSprite::create();
|
||||
ConfigureTextLine(compositeText_, "debugCompositeText", 1);
|
||||
AddChild(compositeText_);
|
||||
|
||||
compositePreview_ = MakePtr<Sprite>();
|
||||
compositePreview_->SetName("debugCompositePreview");
|
||||
compositePreview_->SetZOrder(1);
|
||||
AddChild(compositePreview_);
|
||||
|
||||
compositeOriginMarker_ = MakePtr<DebugCrosshairActor>();
|
||||
compositeOriginMarker_->SetName("debugCompositeOriginMarker");
|
||||
compositeOriginMarker_->SetZOrder(2);
|
||||
compositeOriginMarker_->SetVisible(false);
|
||||
AddChild(compositeOriginMarker_);
|
||||
|
||||
setOverlayVisible(false);
|
||||
}
|
||||
|
||||
void GameDebugActor::updateOverlay() {
|
||||
if (!coordText_ || !debugMap_ || !trackedCharacter_ ||
|
||||
!debugMap_->IsDebugModeEnabled()) {
|
||||
if (debugMap_) {
|
||||
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
|
||||
}
|
||||
if (!coordText_ || !actionText_ || !compositeText_ || !compositePreview_) {
|
||||
setOverlayVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateMapDebugHighlight();
|
||||
if (!trackedCharacter_) {
|
||||
if (compositePreview_) {
|
||||
compositePreview_->SetTexture(nullptr);
|
||||
compositePreview_->SetSize(0.0f, 0.0f);
|
||||
}
|
||||
if (compositeOriginMarker_) {
|
||||
compositeOriginMarker_->SetVisible(false);
|
||||
}
|
||||
previewCharacter_ = nullptr;
|
||||
previewCompositeVersion_ = 0;
|
||||
previewTextureAvailable_ = false;
|
||||
setOverlayVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
|
||||
|
||||
char coordTextBuffer[96];
|
||||
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
|
||||
worldPosition.x, worldPosition.y, worldPosition.z);
|
||||
coordText_->SetText(coordTextBuffer);
|
||||
|
||||
int frameCount = std::max(trackedCharacter_->GetCurrentAnimationFrameCount(), 1);
|
||||
int frameIndex = std::clamp(trackedCharacter_->GetCurrentAnimationFrameIndex(), 0,
|
||||
frameCount - 1);
|
||||
char actionTextBuffer[128];
|
||||
SDL_snprintf(actionTextBuffer, sizeof(actionTextBuffer),
|
||||
"Action: %s dir:%c frame:%d/%d",
|
||||
trackedCharacter_->GetCurrentAction().empty()
|
||||
? "<none>"
|
||||
: trackedCharacter_->GetCurrentAction().c_str(),
|
||||
trackedCharacter_->GetDirection() >= 0 ? 'R' : 'L', frameIndex + 1,
|
||||
frameCount);
|
||||
actionText_->SetText(actionTextBuffer);
|
||||
|
||||
updateCompositePreview();
|
||||
|
||||
Vec2 coordTextSize = coordText_->GetTextSize();
|
||||
Vec2 actionTextSize = actionText_->GetTextSize();
|
||||
Vec2 compositeTextSize = compositeText_->GetTextSize();
|
||||
Vec2 previewSize = compositePreview_->IsVisible() ? compositePreview_->GetSize()
|
||||
: Vec2::Zero();
|
||||
|
||||
float textBlockWidth = std::max(coordTextSize.x,
|
||||
std::max(actionTextSize.x, compositeTextSize.x));
|
||||
float panelWidth = std::max(textBlockWidth, previewSize.x) + kDebugHudPaddingX * 2.0f;
|
||||
|
||||
float contentHeight = coordTextSize.y;
|
||||
if (actionText_->IsVisible()) {
|
||||
contentHeight += kDebugHudLineGap + actionTextSize.y;
|
||||
}
|
||||
if (compositeText_->IsVisible()) {
|
||||
contentHeight += kDebugHudLineGap + compositeTextSize.y;
|
||||
}
|
||||
if (compositePreview_->IsVisible()) {
|
||||
contentHeight += kDebugHudPreviewGap + previewSize.y;
|
||||
}
|
||||
|
||||
float panelHeight = contentHeight + kDebugHudPaddingY * 2.0f;
|
||||
Vec2 panelSize(panelWidth, panelHeight);
|
||||
if (background_) {
|
||||
background_->SetSize(panelSize);
|
||||
}
|
||||
|
||||
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
|
||||
SetScale(1.0f);
|
||||
|
||||
float currentY = kDebugHudPaddingY;
|
||||
coordText_->SetPosition(kDebugHudPaddingX, currentY);
|
||||
currentY += coordTextSize.y;
|
||||
|
||||
if (actionText_->IsVisible()) {
|
||||
currentY += kDebugHudLineGap;
|
||||
actionText_->SetPosition(kDebugHudPaddingX, currentY);
|
||||
currentY += actionTextSize.y;
|
||||
}
|
||||
|
||||
if (compositeText_->IsVisible()) {
|
||||
currentY += kDebugHudLineGap;
|
||||
compositeText_->SetPosition(kDebugHudPaddingX, currentY);
|
||||
currentY += compositeTextSize.y;
|
||||
}
|
||||
|
||||
if (compositePreview_->IsVisible()) {
|
||||
currentY += kDebugHudPreviewGap;
|
||||
compositePreview_->SetPosition(kDebugHudPaddingX, currentY);
|
||||
updateCompositePreviewMarker(trackedCharacter_->GetCompositeTextureSize(),
|
||||
compositePreview_->GetSize(),
|
||||
trackedCharacter_->GetCompositeOriginInTexture());
|
||||
}
|
||||
|
||||
setOverlayVisible(true);
|
||||
}
|
||||
|
||||
void GameDebugActor::updateMapDebugHighlight() {
|
||||
if (!debugMap_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackedCharacter_ || !debugMap_->IsDebugModeEnabled()) {
|
||||
debugMap_->SetDebugHighlightedMoveAreaIndex(GameMap::kInvalidMoveAreaIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
const CharacterWorldPosition& worldPosition = trackedCharacter_->GetWorldPosition();
|
||||
Vec3 currentWorldPos(static_cast<float>(worldPosition.x),
|
||||
static_cast<float>(worldPosition.y),
|
||||
static_cast<float>(worldPosition.z));
|
||||
size_t moveAreaIndex = debugMap_->FindMoveAreaIndex(currentWorldPos);
|
||||
debugMap_->SetDebugHighlightedMoveAreaIndex(moveAreaIndex);
|
||||
}
|
||||
|
||||
char coordTextBuffer[96];
|
||||
SDL_snprintf(coordTextBuffer, sizeof(coordTextBuffer), "角色坐标: (%d, %d, %d)",
|
||||
worldPosition.x, worldPosition.y, worldPosition.z);
|
||||
coordText_->SetText(coordTextBuffer);
|
||||
|
||||
Vec2 coordTextSize = coordText_->GetTextSize();
|
||||
float panelWidth = coordTextSize.x + kDebugHudPaddingX * 2.0f;
|
||||
float panelHeight = coordTextSize.y + kDebugHudPaddingY * 2.0f;
|
||||
Vec2 panelSize(panelWidth, panelHeight);
|
||||
if (background_) {
|
||||
background_->SetSize(panelSize);
|
||||
void GameDebugActor::updateCompositePreview() {
|
||||
if (!trackedCharacter_ || !compositePreview_ || !compositeText_) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetPosition(kDebugHudMarginX, kDebugHudMarginY);
|
||||
SetScale(1.0f);
|
||||
coordText_->SetPosition(kDebugHudPaddingX, kDebugHudPaddingY);
|
||||
setOverlayVisible(true);
|
||||
Ptr<Texture> compositeTexture = trackedCharacter_->GetCompositeTexture();
|
||||
bool textureAvailable =
|
||||
trackedCharacter_->HasCompositeTexture() && compositeTexture != nullptr;
|
||||
uint64 compositeVersion =
|
||||
textureAvailable ? trackedCharacter_->GetCompositeTextureVersion() : 0;
|
||||
bool needsRefresh = previewCharacter_ != trackedCharacter_ ||
|
||||
previewCompositeVersion_ != compositeVersion ||
|
||||
previewTextureAvailable_ != textureAvailable;
|
||||
|
||||
if (needsRefresh) {
|
||||
previewCharacter_ = trackedCharacter_;
|
||||
previewCompositeVersion_ = compositeVersion;
|
||||
previewTextureAvailable_ = textureAvailable;
|
||||
|
||||
if (textureAvailable) {
|
||||
compositePreview_->SetTexture(compositeTexture);
|
||||
} else {
|
||||
compositePreview_->SetTexture(nullptr);
|
||||
compositePreview_->SetSize(0.0f, 0.0f);
|
||||
compositePreview_->SetVisible(false);
|
||||
if (compositeOriginMarker_) {
|
||||
compositeOriginMarker_->SetVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!textureAvailable) {
|
||||
compositeText_->SetText("Composite: unavailable");
|
||||
compositeText_->SetVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Vec2 textureSize = trackedCharacter_->GetCompositeTextureSize();
|
||||
Vec2 previewSize = computeScreenMatchedPreviewSize(textureSize);
|
||||
Vec2 origin = trackedCharacter_->GetCompositeOriginInTexture();
|
||||
Vec2 groundAnchor = trackedCharacter_->GetCompositeGroundAnchorInTexture();
|
||||
compositePreview_->SetSize(previewSize);
|
||||
compositePreview_->SetVisible(true);
|
||||
updateCompositePreviewMarker(textureSize, previewSize, origin);
|
||||
|
||||
char compositeTextBuffer[256];
|
||||
SDL_snprintf(
|
||||
compositeTextBuffer, sizeof(compositeTextBuffer),
|
||||
"Composite: tex %.0fx%.0f preview %.0fx%.0f origin(%.0f, %.0f) ground(%.0f, %.0f) ver %llu",
|
||||
textureSize.x, textureSize.y, previewSize.x, previewSize.y, origin.x,
|
||||
origin.y, groundAnchor.x, groundAnchor.y,
|
||||
static_cast<unsigned long long>(trackedCharacter_->GetCompositeTextureVersion()));
|
||||
compositeText_->SetText(compositeTextBuffer);
|
||||
compositeText_->SetVisible(true);
|
||||
}
|
||||
|
||||
Vec2 GameDebugActor::computeScreenMatchedPreviewSize(const Vec2& textureSize) const {
|
||||
if (!trackedCharacter_ || textureSize.x <= 0.0f || textureSize.y <= 0.0f) {
|
||||
return Vec2::Zero();
|
||||
}
|
||||
|
||||
Renderer& renderer = Renderer::get();
|
||||
Camera* worldCamera = renderer.getCamera();
|
||||
float zoom = worldCamera ? worldCamera->getZoom() : 1.0f;
|
||||
Vec2 worldScale = math::extractScale(trackedCharacter_->GetWorldTransform().matrix);
|
||||
return Vec2(std::max(textureSize.x * std::abs(worldScale.x) * zoom, 1.0f),
|
||||
std::max(textureSize.y * std::abs(worldScale.y) * zoom, 1.0f));
|
||||
}
|
||||
|
||||
void GameDebugActor::updateCompositePreviewMarker(const Vec2& textureSize,
|
||||
const Vec2& previewSize,
|
||||
const Vec2& originInTexture) {
|
||||
if (!compositeOriginMarker_ || !compositePreview_ || textureSize.x <= 0.0f ||
|
||||
textureSize.y <= 0.0f || previewSize.x <= 0.0f || previewSize.y <= 0.0f) {
|
||||
if (compositeOriginMarker_) {
|
||||
compositeOriginMarker_->SetVisible(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float scaleX = previewSize.x / textureSize.x;
|
||||
float scaleY = previewSize.y / textureSize.y;
|
||||
Vec2 previewTopLeft = compositePreview_->GetPosition();
|
||||
compositeOriginMarker_->SetPosition(previewTopLeft.x + originInTexture.x * scaleX,
|
||||
previewTopLeft.y + originInTexture.y * scaleY);
|
||||
compositeOriginMarker_->SetVisible(compositePreview_->IsVisible());
|
||||
}
|
||||
|
||||
void GameDebugActor::setOverlayVisible(bool visible) {
|
||||
@@ -166,6 +405,20 @@ void GameDebugActor::setOverlayVisible(bool visible) {
|
||||
if (coordText_) {
|
||||
coordText_->SetVisible(visible);
|
||||
}
|
||||
if (actionText_) {
|
||||
actionText_->SetVisible(visible);
|
||||
}
|
||||
if (compositeText_) {
|
||||
compositeText_->SetVisible(visible);
|
||||
}
|
||||
if (compositePreview_) {
|
||||
compositePreview_->SetVisible(visible && previewTextureAvailable_);
|
||||
}
|
||||
if (compositeOriginMarker_) {
|
||||
compositeOriginMarker_->SetVisible(visible && previewTextureAvailable_ &&
|
||||
compositePreview_ &&
|
||||
compositePreview_->IsVisible());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace frostbite2D
|
||||
|
||||
Reference in New Issue
Block a user