Skip to content

Instantly share code, notes, and snippets.

@hsdk123
Created October 1, 2025 13:55
Show Gist options
  • Select an option

  • Save hsdk123/6b32fc70b92b5ac9301ffd0acf92d782 to your computer and use it in GitHub Desktop.

Select an option

Save hsdk123/6b32fc70b92b5ac9301ffd0acf92d782 to your computer and use it in GitHub Desktop.
#include "NSprite_Spine.h"
#include <spine/Extension.h>
#include <spine/TextureLoader.h>
#include <spine/SkeletonJson.h>
#include <spine/SkeletonBinary.h>
#include <spine/Vertices.h>
#include <spine/Bone.h>
using namespace lvn;
spine::SpineExtension* spine::getDefaultExtension() {
return new spine::DefaultSpineExtension();
}
namespace {
class MyTextureLoader : public spine::TextureLoader {
public:
MyTextureLoader() {}
~MyTextureLoader() override {}
// Called when the atlas loads the texture of a page.
void load(spine::AtlasPage& page, const spine::String& path) override {
//Texture* texture = engine_loadTexture(path);
const auto filepath = NHelper::s2ws(path.buffer());
const auto resource = CTextureMgr::GetInstance().GetResource(filepath);
if (!resource) {
return;
}
_cache[resource->getFilepath().string()] = resource;
// store the Texture on the rendererObject so we can
// retrieve it later for rendering.
page.texture = resource.get();
}
// Called when the atlas is disposed and itself disposes its atlas pages.
void unload(void* texture_) override
{
if (!texture_) {
return;
}
auto texture = static_cast<lvn::Texture*>(texture_);
_cache.erase(texture->getFilepath().string());
}
private:
// cache s.t. texturemgr doesn't release
std::unordered_map<tstring, std::shared_ptr<lvn::Texture>> _cache;
};
lvn::BlendMode to_lvn_blendmode(const spine::BlendMode& mode_)
{
if (mode_ == spine::BlendMode::BlendMode_Normal) {
return lvn::BlendMode::BlendAlpha;
}
if (mode_ == spine::BlendMode::BlendMode_Additive) {
return lvn::BlendMode::BlendAdd;
}
if (mode_ == spine::BlendMode::BlendMode_Multiply) {
return lvn::BlendMode::BlendMultiply;
// the equations in the spine link don't seem correct...
/*return lvn::BlendMode(
BlendMode::Factor::DestinationColor, BlendMode::Factor::OneMinusSourceAlpha, BlendMode::Equation::Add,
BlendMode::Factor::OneMinusSourceAlpha, BlendMode::Factor::OneMinusSourceAlpha, BlendMode::Equation::Add
);*/
}
if (mode_ == spine::BlendMode::BlendMode_Screen) {
return lvn::BlendMode::BlendScreen;
/*return lvn::BlendMode(
BlendMode::Factor::One, BlendMode::Factor::OneMinusSourceColor, BlendMode::Equation::Add,
BlendMode::Factor::OneMinusSourceColor, BlendMode::Factor::OneMinusSourceColor, BlendMode::Equation::Add
);*/
}
CLOG_ERROR(L"to_lvn_blendmode unhandled: {}", static_cast<int>(mode_));
return lvn::BlendMode::BlendAlpha;
}
}
NSprite_Spine::NSprite_Spine( const tstring& name ) :
StateObj( NType::extern_spine_cg, name )
{
}
NSprite_Spine::~NSprite_Spine()
{
// in this order (due to dependencies on one another)
_spine_skeleton.reset();
_spine_animation_state.reset();
_spine_atlas.reset();
_spine_skeleton_data.reset();
_spine_animation_state_data.reset();
}
std::shared_ptr<NSprite_Spine> lvn::NSprite_Spine::Create(
const tstring& name,
const tstring& filepath_skeleton_,
const tstring& filepath_atlas_,
const tstring& start_animation,
const bool repeat
)
{
using namespace std;
auto obj = make_unique<NSprite_Spine>(name);
{
auto& state = obj->GetState();
auto& obj_data = obj->getData();
{
obj_data.filepath_skeleton = filepath_skeleton_;
obj_data.filepath_atlas = filepath_atlas_;
}
const auto filepath_atlas = NFilepath::EvalLvnPath(NConstants::imageDir.first, filepath_atlas_).string();
const auto texture_directory = filepath_atlas.substr(0, filepath_atlas.find_last_of(L'/'));
// static state
{
{
const auto stream = CStreamLoadMgr::GetInstance().get_stream_createIfNone(filepath_atlas);
if (!stream) {
CLOG_ERROR(L"filepath: {} does not exist", filepath_atlas);
return nullptr;
}
std::string resource_directory = NHelper::ws2s(texture_directory);
obj->_texture_loader = std::make_shared<MyTextureLoader>();
obj->_spine_atlas = std::make_shared<spine::Atlas>(
stream->getData().data(), stream->getData().length(), resource_directory.c_str(), obj->_texture_loader.get());
}
{
const auto filepath_skeleton = NFilepath::EvalLvnPath(NConstants::imageDir.first, filepath_skeleton_).string();
const auto stream = CStreamLoadMgr::GetInstance().get_stream_createIfNone(filepath_skeleton);
if (!stream) {
CLOG_ERROR(L"filepath: {} does not exist", filepath_skeleton);
return nullptr;
}
if (filepath_skeleton.find(L"json") != tstring::npos) {
spine::SkeletonJson json(obj->_spine_atlas.get());
const auto p_skeleton_data = json.readSkeletonData(stream->getData().data());
if (!p_skeleton_data) {
CLOG_ERROR(L"skeleton data: {} could not be loaded", filepath_skeleton);
return nullptr;
}
obj->_spine_skeleton_data = std::shared_ptr<spine::SkeletonData>(p_skeleton_data);
}
else {
spine::SkeletonBinary binary(obj->_spine_atlas.get());
const auto p_skeleton_data = binary.readSkeletonData(
reinterpret_cast<const unsigned char*>(stream->getData().data()), stream->getData().size());
if (!p_skeleton_data) {
CLOG_ERROR(L"skeleton data: {} could not be loaded", filepath_skeleton);
return nullptr;
}
obj->_spine_skeleton_data = std::shared_ptr<spine::SkeletonData>(p_skeleton_data);
}
}
{
auto& state_data = obj->_spine_animation_state_data;
state_data = std::make_shared<spine::AnimationStateData>(obj->_spine_skeleton_data.get());
state_data->setDefaultMix(0.5f);
}
}
// dynamic state
{
obj->_spine_skeleton = std::make_shared< spine::Skeleton>(obj->_spine_skeleton_data.get());
obj->_spine_animation_state = std::make_shared<spine::AnimationState>(obj->_spine_animation_state_data.get());
const auto start_animation_ = NHelper::ws2s(start_animation);
{
state.GetComponent<CState_Animation>()._state_name = start_animation;
state.GetComponent<CState_Animation>()._repeat = repeat;
if (!obj->SyncState()) {
return nullptr;
}
}
}
// spine's y axis is upside down to us.
spine::Bone::setYDown(true);
// update once
obj->Update(0);
}
return obj;
}
// reference:
// https://en.esotericsoftware.com/spine-cpp#Putting-it-all-together
void NSprite_Spine::Update(const lvn::TimeUnit delta_ms)
{
StateObj::Update(delta_ms);
auto& state = GetState();
// sync with playable.
if (state.GetComponent<CPlayable>().GetPlayState() == PlayState::paused) {
return;
}
auto& data = getData();
{
// update to new animation if needed
bool animation_updated = false;
{
auto& state_animation = state.GetComponent<CState_Animation>();
if (state_animation._state_name != data.animation_state_name) {
animation_updated = SyncState();
}
}
const auto delta_secs = animation_updated ? 0.f : (delta_ms / 1000.f);
// update spine states
// https://en.esotericsoftware.com/spine-cpp#Updating-and-applying
{
_spine_animation_state->update(delta_secs);
_spine_animation_state->apply(*_spine_skeleton);
_spine_skeleton->update(delta_secs);
_spine_skeleton->updateWorldTransform(spine::Physics_Update);
}
}
}
bool NSprite_Spine::SyncState()
{
auto& state = GetState();
auto& data = getData();
auto& state_animation = state.GetComponent<CState_Animation>();
const auto initial_sync = (data.animation_state_name.empty());
{
const auto new_animation_state = NHelper::ws2s(state_animation._state_name);
if (!_spine_skeleton_data->findAnimation(new_animation_state.c_str())) {
CLOG_ERROR(L"animation: {} does not exist", state_animation._state_name);
// reset external component info.
state_animation._state_name = data.animation_state_name;
return false;
}
// update internal info.
data.animation_state_name = state_animation._state_name;
// update animation to new.
/*if (initial_sync) {*/
if (true) {
_spine_animation_state->setAnimation(0, new_animation_state.c_str(), state_animation._repeat);
}
else {
_spine_animation_state->addAnimation(0, new_animation_state.c_str(), state_animation._repeat,
_spine_animation_state_data->getDefaultMix());
}
// _spine_animation_state->addAnimation(0, new_animation_state.c_str(), true, 0);
return true;
}
}
void NSprite_Spine::Draw(const lvn::TimeUnit delta_ms)
{
auto& state = GetState();
if (!_buffer) {
_buffer = (CVertexArrayBuffer::Create());
}
const auto custom_shaders = state.GetComponent<CShaderCtrl>().GetSpriteShaders();
// read spine render commands
{
auto p_command = _skeleton_renderer.render(*_spine_skeleton);
while (p_command)
{
auto& command = *p_command;
const auto positions = command.positions;
const auto uvs = command.uvs;
const auto colors = command.colors;
const auto indices = command.indices;
std::vector<CVertexArrayBuffer::Vertex> data_vertices;
{
//for (int ii = 0; ii < (command.numVertices << 1); ii += 2)
for (int i = 0, j = 0; i < (command.numVertices); i++, j += 2)
{
CVertexArrayBuffer::Vertex vertex;
vertex.x = positions[j];
vertex.y = positions[j + 1];
vertex.u = uvs[j];
vertex.v = (1.f - uvs[j + 1]);
// colour
{
const auto& colour = colors[i];
vertex.rgba.r = (colour >> 24) & 0xFF;
vertex.rgba.g = (colour >> 16) & 0xFF;
vertex.rgba.b = (colour >> 8) & 0xFF;
vertex.rgba.a = (colour) & 0xFF;
}
data_vertices.emplace_back(vertex);
}
}
std::vector<uint16_t> data_indices;
{
for (int ii = 0; ii < command.numIndices; ii++) {
data_indices.emplace_back(indices[ii]);
}
}
CVertexArrayBuffer::RenderConfiguration config;
{
config.texture = static_cast<lvn::Texture*>(command.texture);
config.blend_mode = to_lvn_blendmode(command.blendMode);
//state.GetComponent<CSpriteAnimationCtrl>().
}
// render
{
_buffer->Render(*this,
data_vertices,
data_indices,
config,
custom_shaders
);
}
//engine_drawMesh(vertices.buffer(), command.indices, command.numIndices, texture, blendMode);
p_command = command.next;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment