feat(动画): 添加角色动作合成纹理功能

实现角色动作的离屏渲染合成功能,支持获取合成纹理及其相关信息:
1. 新增CanvasActor用于离屏渲染
2. 新增RenderTexture封装FBO和纹理
3. 扩展Renderer支持离屏渲染到纹理
4. 为CharacterAnimation添加合成纹理生成逻辑
5. 在调试界面添加合成纹理预览功能
This commit is contained in:
2026-04-07 06:17:49 +08:00
parent 6684abd131
commit 808431f92c
14 changed files with 1207 additions and 22 deletions

View File

@@ -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