From 887d77ef63287e42639264f653b9874737f8950d Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 19 Aug 2022 17:38:46 +0200 Subject: [PATCH] [flutter] skeleton drawable, vertices creation --- spine-cpp/spine-cpp/include/spine/Atlas.h | 3 +- spine-cpp/spine-cpp/src/spine/Atlas.cpp | 1 + spine-flutter/example/assets/skeleton.atlas | 6 + spine-flutter/example/assets/skeleton.json | 40 ++++ spine-flutter/example/assets/skeleton.png | Bin 0 -> 25772 bytes spine-flutter/example/lib/main.dart | 10 +- spine-flutter/lib/spine_flutter.dart | 42 ++++ .../lib/spine_flutter_bindings_generated.dart | 109 +++++++++++ spine-flutter/src/spine_flutter.cpp | 182 ++++++++++++++++++ spine-flutter/src/spine_flutter.h | 30 +++ 10 files changed, 419 insertions(+), 4 deletions(-) create mode 100644 spine-flutter/example/assets/skeleton.atlas create mode 100644 spine-flutter/example/assets/skeleton.json create mode 100644 spine-flutter/example/assets/skeleton.png diff --git a/spine-cpp/spine-cpp/include/spine/Atlas.h b/spine-cpp/spine-cpp/include/spine/Atlas.h index 820609ee3..2cdabe762 100644 --- a/spine-cpp/spine-cpp/include/spine/Atlas.h +++ b/spine-cpp/spine-cpp/include/spine/Atlas.h @@ -84,11 +84,12 @@ namespace spine { TextureWrap vWrap; int width, height; bool pma; + int index; explicit AtlasPage(const String &inName) : name(inName), format(Format_RGBA8888), minFilter(TextureFilter_Nearest), magFilter(TextureFilter_Nearest), uWrap(TextureWrap_ClampToEdge), - vWrap(TextureWrap_ClampToEdge), width(0), height(0), pma(false) { + vWrap(TextureWrap_ClampToEdge), width(0), height(0), pma(false), index(0) { } }; diff --git a/spine-cpp/spine-cpp/src/spine/Atlas.cpp b/spine-cpp/spine-cpp/src/spine/Atlas.cpp index 6930ef3dc..d13a0806c 100644 --- a/spine-cpp/spine-cpp/src/spine/Atlas.cpp +++ b/spine-cpp/spine-cpp/src/spine/Atlas.cpp @@ -286,6 +286,7 @@ void Atlas::load(const char *begin, int length, const char *dir, bool createText } else { page->texturePath = String(path, true); } + page->index = _pages.size(); _pages.add(page); } else { AtlasRegion *region = new (__FILE__, __LINE__) AtlasRegion(); diff --git a/spine-flutter/example/assets/skeleton.atlas b/spine-flutter/example/assets/skeleton.atlas new file mode 100644 index 000000000..24c816d99 --- /dev/null +++ b/spine-flutter/example/assets/skeleton.atlas @@ -0,0 +1,6 @@ +skeleton.png +size:188,198 +filter:Linear,Linear +pma:true +Screenshot 2022-08-19 at 13.45.53 +bounds:2,2,184,194 diff --git a/spine-flutter/example/assets/skeleton.json b/spine-flutter/example/assets/skeleton.json new file mode 100644 index 000000000..d526e9ced --- /dev/null +++ b/spine-flutter/example/assets/skeleton.json @@ -0,0 +1,40 @@ +{ + "skeleton": { + "hash": "EzIWQLMq9sw", + "spine": "4.1.08", + "x": -92, + "y": -97, + "width": 184, + "height": 194, + "images": "", + "audio": "" + }, + "bones": [ + { + "name": "root" + } + ], + "slots": [ + { + "name": "image", + "bone": "root", + "attachment": "Screenshot 2022-08-19 at 13.45.53" + } + ], + "skins": [ + { + "name": "default", + "attachments": { + "image": { + "Screenshot 2022-08-19 at 13.45.53": { + "width": 184, + "height": 194 + } + } + } + } + ], + "animations": { + "animation": {} + } +} \ No newline at end of file diff --git a/spine-flutter/example/assets/skeleton.png b/spine-flutter/example/assets/skeleton.png new file mode 100644 index 0000000000000000000000000000000000000000..abf13337994f17a1554d7a89767cc4b8ce44f7b3 GIT binary patch literal 25772 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 literal 0 HcmV?d00001 diff --git a/spine-flutter/example/lib/main.dart b/spine-flutter/example/lib/main.dart index 22c93c6c8..781421cd0 100644 --- a/spine-flutter/example/lib/main.dart +++ b/spine-flutter/example/lib/main.dart @@ -27,9 +27,13 @@ class _MyAppState extends State { } void loadSkeleton() async { - final atlas = await spine_flutter.loadAtlas(rootBundle, "assets/spineboy.atlas"); - final skeletonData = spine_flutter.loadSkeletonDataJson(atlas, await rootBundle.loadString("assets/spineboy-pro.json")); - final skeletonDataBinary = spine_flutter.loadSkeletonDataBinary(atlas, await rootBundle.load("assets/spineboy-pro.skel")); + final atlas = await spine_flutter.loadAtlas(rootBundle, "assets/skeleton.atlas"); + final skeletonData = spine_flutter.loadSkeletonDataJson(atlas, await rootBundle.loadString("assets/skeleton.json")); + // final skeletonDataBinary = spine_flutter.loadSkeletonDataBinary(atlas, await rootBundle.load("assets/spineboy-pro.skel")); + final skeletonDrawable = spine_flutter.createSkeletonDrawable(skeletonData); + spine_flutter.updateSkeletonDrawable(skeletonDrawable, 0.016); + final renderCommands = spine_flutter.renderSkeletonDrawable(skeletonDrawable); + print(renderCommands[0].vertices); } @override diff --git a/spine-flutter/lib/spine_flutter.dart b/spine-flutter/lib/spine_flutter.dart index 4205c3439..05d7cfd2b 100644 --- a/spine-flutter/lib/spine_flutter.dart +++ b/spine-flutter/lib/spine_flutter.dart @@ -1,6 +1,8 @@ import 'dart:ffi'; import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart'; @@ -60,6 +62,46 @@ Pointer loadSkeletonDataBinary(Pointer atlas, return skeletonData; } +Pointer createSkeletonDrawable(Pointer skeletonData) { + return _bindings.spine_skeleton_drawable_create(skeletonData); +} + +void updateSkeletonDrawable(Pointer drawable, double deltaTime) { + _bindings.spine_skeleton_drawable_update(drawable, deltaTime); +} + +class RenderCommand { + late Vertices vertices; + late int atlasPageIndex; + RenderCommand? next; + + RenderCommand(Pointer nativeCmd) { + atlasPageIndex = nativeCmd.ref.atlasPage; + int numVertices = nativeCmd.ref.numVertices; + int numIndices = nativeCmd.ref.numIndices; + // We pass the native data as views directly to Vertices.raw. According to the sources, the data + // is copied, so it doesn't matter that we free up the underlying memory on the next + // render call. See the implementation of Vertices.raw() here: + // https://github.com/flutter/engine/blob/5c60785b802ad2c8b8899608d949342d5c624952/lib/ui/painting/vertices.cc#L21 + vertices = Vertices.raw(VertexMode.triangles, + nativeCmd.ref.positions.asTypedList(numVertices * 2), + textureCoordinates: nativeCmd.ref.uvs.asTypedList(numVertices * 2), + colors: nativeCmd.ref.colors.asTypedList(numVertices), + indices: nativeCmd.ref.indices.asTypedList(numIndices)); + } +} + +List renderSkeletonDrawable(Pointer drawable) { + Pointer nativeCmd = _bindings.spine_skeleton_drawable_render(drawable); + List commands = []; + while(nativeCmd.address != nullptr.address) { + commands.add(RenderCommand(nativeCmd)); + nativeCmd = nativeCmd.ref.next; + } + return commands; + +} + const String _libName = 'spine_flutter'; final DynamicLibrary _dylib = () { diff --git a/spine-flutter/lib/spine_flutter_bindings_generated.dart b/spine-flutter/lib/spine_flutter_bindings_generated.dart index 63946fb60..fea84fb18 100644 --- a/spine-flutter/lib/spine_flutter_bindings_generated.dart +++ b/spine-flutter/lib/spine_flutter_bindings_generated.dart @@ -129,6 +129,76 @@ class SpineFlutterBindings { 'spine_skeleton_data_dispose'); late final _spine_skeleton_data_dispose = _spine_skeleton_data_disposePtr .asFunction)>(); + + ffi.Pointer spine_skeleton_drawable_create( + ffi.Pointer skeletonData, + ) { + return _spine_skeleton_drawable_create( + skeletonData, + ); + } + + late final _spine_skeleton_drawable_createPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>( + 'spine_skeleton_drawable_create'); + late final _spine_skeleton_drawable_create = + _spine_skeleton_drawable_createPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer)>(); + + void spine_skeleton_drawable_update( + ffi.Pointer drawable, + double deltaTime, + ) { + return _spine_skeleton_drawable_update( + drawable, + deltaTime, + ); + } + + late final _spine_skeleton_drawable_updatePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Float)>>('spine_skeleton_drawable_update'); + late final _spine_skeleton_drawable_update = + _spine_skeleton_drawable_updatePtr.asFunction< + void Function(ffi.Pointer, double)>(); + + ffi.Pointer spine_skeleton_drawable_render( + ffi.Pointer drawable, + ) { + return _spine_skeleton_drawable_render( + drawable, + ); + } + + late final _spine_skeleton_drawable_renderPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>( + 'spine_skeleton_drawable_render'); + late final _spine_skeleton_drawable_render = + _spine_skeleton_drawable_renderPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer)>(); + + void spine_skeleton_drawable_dispose( + ffi.Pointer drawable, + ) { + return _spine_skeleton_drawable_dispose( + drawable, + ); + } + + late final _spine_skeleton_drawable_disposePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>>( + 'spine_skeleton_drawable_dispose'); + late final _spine_skeleton_drawable_dispose = + _spine_skeleton_drawable_disposePtr + .asFunction)>(); } class spine_atlas extends ffi.Struct { @@ -147,3 +217,42 @@ class spine_skeleton_data extends ffi.Struct { external ffi.Pointer error; } + +abstract class spine_blend_mode { + static const int SPINE_BLEND_MODE_NORMAL = 0; + static const int SPINE_BLEND_MODE_ADDITIVE = 1; + static const int SPINE_BLEND_MODE_MULTIPLY = 2; + static const int SPINE_BLEND_MODE_SCREEN = 3; +} + +class spine_render_command extends ffi.Struct { + external ffi.Pointer positions; + + external ffi.Pointer uvs; + + external ffi.Pointer colors; + + @ffi.Int32() + external int numVertices; + + external ffi.Pointer indices; + + @ffi.Int32() + external int numIndices; + + @ffi.Int32() + external int atlasPage; + + @ffi.Int32() + external int blendMode; + + external ffi.Pointer next; +} + +class spine_skeleton_drawable extends ffi.Struct { + external ffi.Pointer skeleton; + + external ffi.Pointer animationState; + + external ffi.Pointer renderCommand; +} diff --git a/spine-flutter/src/spine_flutter.cpp b/spine-flutter/src/spine_flutter.cpp index f031a4191..09448b381 100644 --- a/spine-flutter/src/spine_flutter.cpp +++ b/spine-flutter/src/spine_flutter.cpp @@ -13,6 +13,7 @@ FFI_PLUGIN_EXPORT int32_t spine_minor_version() { } FFI_PLUGIN_EXPORT spine_atlas* spine_atlas_load(const char *atlasData) { + if (!atlasData) return nullptr; int length = strlen(atlasData); auto atlas = new Atlas(atlasData, length, "", (TextureLoader*)nullptr, false); spine_atlas *result = SpineExtension::calloc(1, __FILE__, __LINE__); @@ -36,6 +37,9 @@ FFI_PLUGIN_EXPORT void spine_atlas_dispose(spine_atlas *atlas) { } FFI_PLUGIN_EXPORT spine_skeleton_data *spine_skeleton_data_load_json(spine_atlas *atlas, const char *skeletonData) { + if (!atlas) return nullptr; + if (!atlas->atlas) return nullptr; + if (!skeletonData) return nullptr; SkeletonJson json((Atlas*)atlas->atlas); SkeletonData *data = json.readSkeletonData(skeletonData); spine_skeleton_data *result = SpineExtension::calloc(1, __FILE__, __LINE__); @@ -47,6 +51,10 @@ FFI_PLUGIN_EXPORT spine_skeleton_data *spine_skeleton_data_load_json(spine_atlas } FFI_PLUGIN_EXPORT spine_skeleton_data* spine_skeleton_data_load_binary(spine_atlas *atlas, const unsigned char *skeletonData, int32_t length) { + if (!atlas) return nullptr; + if (!atlas->atlas) return nullptr; + if (!skeletonData) return nullptr; + if (length <= 0) return nullptr; SkeletonBinary binary((Atlas*)atlas->atlas); SkeletonData *data = binary.readSkeletonData(skeletonData, length); spine_skeleton_data *result = SpineExtension::calloc(1, __FILE__, __LINE__); @@ -64,6 +72,180 @@ FFI_PLUGIN_EXPORT void spine_skeleton_data_dispose(spine_skeleton_data *skeleton SpineExtension::free(skeletonData, __FILE__, __LINE__); } +FFI_PLUGIN_EXPORT spine_skeleton_drawable *spine_skeleton_drawable_create(spine_skeleton_data *skeletonData) { + spine_skeleton_drawable *drawable = SpineExtension::calloc(1, __FILE__, __LINE__); + drawable->skeleton = new Skeleton((SkeletonData*)skeletonData->skeletonData); + drawable->animationState = new AnimationState(new AnimationStateData((SkeletonData*)skeletonData->skeletonData)); + return drawable; +} + +FFI_PLUGIN_EXPORT void spine_skeleton_drawable_update(spine_skeleton_drawable *drawable, float deltaTime) { + if (!drawable) return; + if (!drawable->skeleton) return; + if (!drawable->animationState) return; + + Skeleton *skeleton = (Skeleton*)drawable->skeleton; + AnimationState *animationState = (AnimationState*)drawable->animationState; + animationState->update(deltaTime); + animationState->apply(*skeleton); + skeleton->updateWorldTransform(); +} + +spine_render_command *spine_render_command_create(int32_t numVertices, int32_t numIndices, spine_blend_mode blendMode, int pageIndex) { + spine_render_command *cmd = SpineExtension::alloc(1, __FILE__, __LINE__); + cmd->positions = SpineExtension::alloc(numVertices * 2, __FILE__, __LINE__); + cmd->uvs = SpineExtension::alloc(numVertices * 2, __FILE__, __LINE__); + cmd->colors = SpineExtension::alloc(numVertices, __FILE__, __LINE__); + cmd->numVertices = numVertices; + cmd->indices = SpineExtension::alloc(numIndices, __FILE__, __LINE__); + cmd->numIndices = numIndices; + cmd->blendMode = blendMode; + cmd->atlasPage = pageIndex; + cmd->next = nullptr; + return cmd; +} + +void spine_render_command_dispose(spine_render_command *cmd) { + if (!cmd) return; + if (cmd->positions) SpineExtension::free(cmd->positions, __FILE__, __LINE__); + if (cmd->uvs) SpineExtension::free(cmd->uvs, __FILE__, __LINE__); + if (cmd->colors) SpineExtension::free(cmd->colors, __FILE__, __LINE__); + if (cmd->indices) SpineExtension::free(cmd->indices, __FILE__, __LINE__); + SpineExtension::free(cmd, __FILE__, __LINE__); +} + +FFI_PLUGIN_EXPORT spine_render_command *spine_skeleton_drawable_render(spine_skeleton_drawable *drawable) { + if (!drawable) return nullptr; + if (!drawable->skeleton) return nullptr; + + while (drawable->renderCommand) { + spine_render_command *cmd = drawable->renderCommand; + drawable->renderCommand = cmd->next; + spine_render_command_dispose(cmd); + } + + Vector quadIndices; + quadIndices.add(0); + quadIndices.add(1); + quadIndices.add(2); + quadIndices.add(2); + quadIndices.add(3); + quadIndices.add(0); + Vector worldVertices; + SkeletonClipping clipper; + Skeleton *skeleton = (Skeleton*)drawable->skeleton; + spine_render_command *lastCommand = nullptr; + + for (unsigned i = 0; i < skeleton->getSlots().size(); ++i) { + Slot &slot = *skeleton->getDrawOrder()[i]; + Attachment *attachment = slot.getAttachment(); + if (!attachment) continue; + + // Early out if the slot color is 0 or the bone is not active + if (slot.getColor().a == 0 || !slot.getBone().isActive()) { + clipper.clipEnd(slot); + continue; + } + + Vector *vertices = &worldVertices; + int verticesCount = 0; + Vector *uvs = NULL; + Vector *indices; + int indicesCount = 0; + Color *attachmentColor; + int pageIndex = -1; + + if (attachment->getRTTI().isExactly(RegionAttachment::rtti)) { + RegionAttachment *regionAttachment = (RegionAttachment *) attachment; + attachmentColor = ®ionAttachment->getColor(); + + // Early out if the slot color is 0 + if (attachmentColor->a == 0) { + clipper.clipEnd(slot); + continue; + } + + worldVertices.setSize(8, 0); + regionAttachment->computeWorldVertices(slot, worldVertices, 0, 2); + verticesCount = 4; + uvs = ®ionAttachment->getUVs(); + indices = &quadIndices; + indicesCount = 6; + pageIndex = ((AtlasRegion *) regionAttachment->getRendererObject())->page->index; + + } else if (attachment->getRTTI().isExactly(MeshAttachment::rtti)) { + MeshAttachment *mesh = (MeshAttachment *) attachment; + attachmentColor = &mesh->getColor(); + + // Early out if the slot color is 0 + if (attachmentColor->a == 0) { + clipper.clipEnd(slot); + continue; + } + + worldVertices.setSize(mesh->getWorldVerticesLength(), 0); + pageIndex = ((AtlasRegion *) mesh->getRendererObject())->page->index; + mesh->computeWorldVertices(slot, 0, mesh->getWorldVerticesLength(), worldVertices.buffer(), 0, 2); + verticesCount = mesh->getWorldVerticesLength() >> 1; + uvs = &mesh->getUVs(); + indices = &mesh->getTriangles(); + indicesCount = indices->size(); + + } else if (attachment->getRTTI().isExactly(ClippingAttachment::rtti)) { + ClippingAttachment *clip = (ClippingAttachment *) slot.getAttachment(); + clipper.clipStart(slot, clip); + continue; + } else + continue; + + uint8_t r = static_cast(skeleton->getColor().r * slot.getColor().r * attachmentColor->r * 255); + uint8_t g = static_cast(skeleton->getColor().g * slot.getColor().g * attachmentColor->g * 255); + uint8_t b = static_cast(skeleton->getColor().b * slot.getColor().b * attachmentColor->b * 255); + uint8_t a = static_cast(skeleton->getColor().a * slot.getColor().a * attachmentColor->a * 255); + uint32_t color = (a << 24) | (r << 16) | (g << 8) | b; + + if (clipper.isClipping()) { + clipper.clipTriangles(worldVertices, *indices, *uvs, 2); + vertices = &clipper.getClippedVertices(); + verticesCount = clipper.getClippedVertices().size() >> 1; + uvs = &clipper.getClippedUVs(); + indices = &clipper.getClippedTriangles(); + indicesCount = clipper.getClippedTriangles().size(); + } + + spine_render_command *cmd = spine_render_command_create(verticesCount, indicesCount, (spine_blend_mode)slot.getData().getBlendMode(), pageIndex); + + memcpy(cmd->positions, vertices->buffer(), (verticesCount << 2) * sizeof(float)); + memcpy(cmd->uvs, uvs->buffer(), (verticesCount << 2) * sizeof(float)); + for (int ii = 0; ii < verticesCount; ii++) cmd->colors[ii] = color; + memcpy(cmd->indices, indices->buffer(), indices->size() * sizeof(uint16_t)); + + if (!lastCommand) { + drawable->renderCommand = lastCommand = cmd; + } else { + lastCommand->next = cmd; + lastCommand = cmd; + } + + clipper.clipEnd(slot); + } + clipper.clipEnd(); + + return drawable->renderCommand; +} + +FFI_PLUGIN_EXPORT void spine_skeleton_drawable_dispose(spine_skeleton_drawable *drawable) { + if (!drawable) return; + if (drawable->skeleton) delete (Skeleton*)drawable->skeleton; + if (drawable->animationState) delete (AnimationState*)drawable->animationState; + while (drawable->renderCommand) { + spine_render_command *cmd = drawable->renderCommand; + drawable->renderCommand = cmd->next; + spine_render_command_dispose(cmd); + } + SpineExtension::free(drawable, __FILE__, __LINE__); +} + spine::SpineExtension *spine::getDefaultExtension() { return new spine::DefaultSpineExtension(); } diff --git a/spine-flutter/src/spine_flutter.h b/spine-flutter/src/spine_flutter.h index 030a668a9..135ba626e 100644 --- a/spine-flutter/src/spine_flutter.h +++ b/spine-flutter/src/spine_flutter.h @@ -45,3 +45,33 @@ FFI_PLUGIN_EXPORT spine_skeleton_data* spine_skeleton_data_load_json(spine_atlas FFI_PLUGIN_EXPORT spine_skeleton_data* spine_skeleton_data_load_binary(spine_atlas *atlas, const unsigned char *skeletonData, int32_t length); FFI_PLUGIN_EXPORT void spine_skeleton_data_dispose(spine_skeleton_data *skeletonData); +typedef enum spine_blend_mode { + SPINE_BLEND_MODE_NORMAL = 0, + SPINE_BLEND_MODE_ADDITIVE, + SPINE_BLEND_MODE_MULTIPLY, + SPINE_BLEND_MODE_SCREEN +} spine_blend_mode; + +typedef struct spine_render_command { + float *positions; + float *uvs; + int32_t *colors; + int32_t numVertices; + uint16_t *indices; + int32_t numIndices; + int32_t atlasPage; + spine_blend_mode blendMode; + struct spine_render_command *next; +} spine_render_command; + +typedef struct spine_skeleton_drawable { + void *skeleton; + void *animationState; + spine_render_command *renderCommand; +} spine_skeleton_drawable; + +FFI_PLUGIN_EXPORT spine_skeleton_drawable *spine_skeleton_drawable_create(spine_skeleton_data *skeletonData); +FFI_PLUGIN_EXPORT void spine_skeleton_drawable_update(spine_skeleton_drawable *drawable, float deltaTime); +FFI_PLUGIN_EXPORT spine_render_command *spine_skeleton_drawable_render(spine_skeleton_drawable *drawable); +FFI_PLUGIN_EXPORT void spine_skeleton_drawable_dispose(spine_skeleton_drawable *drawable); +