This commit is contained in:
Harald Csaszar 2023-02-09 17:12:22 +01:00
commit 3cf5e14453
23 changed files with 4549 additions and 1986 deletions

View File

@ -113,6 +113,12 @@ namespace spine {
void
updateWorldTransform(float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY);
/// Computes the individual applied transform values from the world transform. This can be useful to perform processing using
/// the applied transform after the world transform has been modified directly (eg, by a constraint)..
///
/// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation.
void updateAppliedTransform();
void setToSetupPose();
void worldToLocal(float worldX, float worldY, float &outLocalX, float &outLocalY);
@ -260,12 +266,6 @@ namespace spine {
float _c, _d, _worldY;
bool _sorted;
bool _active;
/// Computes the individual applied transform values from the world transform. This can be useful to perform processing using
/// the applied transform after the world transform has been modified directly (eg, by a constraint)..
///
/// Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation.
void updateAppliedTransform();
};
}

View File

@ -33,7 +33,7 @@ class AnimationStateEvents extends StatelessWidget {
appBar: AppBar(title: const Text('Spineboy')),
body: Column(children: [
const Text("See output in console!"),
Expanded(child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller))
Expanded(child: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller))
]));
}
}

View File

@ -17,7 +17,7 @@ class DebugRendering extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: const Text('Debug Renderer')),
body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
);
}
}

View File

@ -29,7 +29,7 @@ class DressUpState extends State<DressUp> {
skeleton.setSkin(skin);
skeleton.setToSetupPose();
skeleton.updateWorldTransform();
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize);
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff);
_selectedSkins[skin.getName()] = false;
}
_toggleSkin("full-skins/girl");
@ -87,7 +87,7 @@ class DressUpState extends State<DressUp> {
}).toList()),
),
Expanded(
child: SpineWidget.drawable(
child: SpineWidget.fromDrawable(
_drawable,
controller,
boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"]),

View File

@ -25,7 +25,9 @@ class IkFollowingState extends State<IkFollowing> {
if (worldPosition == null) return;
var bone = controller.skeleton.findBone("crosshair");
if (bone == null) return;
var position = bone.getParent()?.worldToLocal(worldPosition.dx, worldPosition.dy) ?? Vec2(0, 0);
var parent = bone.getParent();
if (parent == null) return;
var position = parent.worldToLocal(worldPosition.dx, worldPosition.dy);
bone.setX(position.x);
bone.setY(position.y);
});
@ -44,7 +46,7 @@ class IkFollowingState extends State<IkFollowing> {
body: GestureDetector(
onPanDown: (drag) => _updateBonePosition(drag.localPosition),
onPanUpdate: (drag) => _updateBonePosition(drag.localPosition),
child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
child: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
));
}
}

View File

@ -10,7 +10,6 @@ class PlayPauseAnimation extends StatefulWidget {
class PlayPauseAnimationState extends State<PlayPauseAnimation> {
late SpineWidgetController controller;
late bool isPlaying;
@override
void initState() {
@ -18,12 +17,14 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
controller = SpineWidgetController(onInitialized: (controller) {
controller.animationState.setAnimationByName(0, "flying", true);
});
isPlaying = true;
}
void _togglePlay() {
isPlaying = !isPlaying;
controller.animationState.setTimeScale(isPlaying ? 1 : 0);
if (controller.isPlaying) {
controller.pause();
} else {
controller.resume();
}
setState(() {});
}
@ -33,7 +34,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
return Scaffold(
appBar: AppBar(title: const Text('Play/Pause')),
body: SpineWidget.asset(
body: SpineWidget.fromAsset(
"assets/dragon.atlas",
"assets/dragon-ess.skel",
controller,
@ -41,7 +42,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
child: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
child: Icon(controller.isPlaying ? Icons.pause : Icons.play_arrow),
),
);
}

View File

@ -14,9 +14,7 @@ class SimpleAnimation extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: const Text('Simple Animation')),
body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
// body: SpineWidget.file( "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy.atlas", "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy-pro.skel", controller),
// body: const SpineWidget.http("https://marioslab.io/dump/spineboy/spineboy.atlas", "https://marioslab.io/dump/spineboy/spineboy-pro.json"),
body: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller)
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,44 @@ import 'package:flutter/widgets.dart';
import 'spine_flutter.dart';
/// Controls how the skeleton of a [SpineWidget] is animated and rendered.
///
/// Upon initialization of a [SpineWidget] the provided [onInitialized] callback method is called once. This method can be used
/// to setup the initial animation(s) of the skeleton, among other things.
///
/// After initialization is complete, the [SpineWidget] is rendered at the screen refresh rate. In each frame,
/// the [AnimationState] is updated and applied to the [Skeleton].
///
/// Next the optionally provided method [onBeforeUpdateWorldTransforms] is called, which can modify the
/// skeleton before its current pose is calculated using [Skeleton.updateWorldTransforms]. After
/// [Skeleton.updateWorldTransforms] has completed, the optional [onAfterUpdateWorldTransforms] method is
/// called, which can modify the current pose before rendering the skeleton.
///
/// Before the skeleton's current pose is rendered by the [SpineWidget] the optional [onBeforePaint] is called,
/// which allows rendering backgrounds or other objects that should go behind the skeleton on the [Canvas]. The
/// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which
/// can render additional objects on top of the skeleton.
///
/// The underlying [Atlas], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawable]
/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
/// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet.
///
/// By default, the widget updates and renders the skeleton every frame. The [pause] method can be used to pause updating
/// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter
/// reports the current state.
class SpineWidgetController {
SkeletonDrawable? _drawable;
double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1;
bool _isPlaying = true;
_SpineRenderObject? _renderObject = null;
final void Function(SpineWidgetController controller)? onInitialized;
final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms;
final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms;
final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint;
final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommand> commands)? onAfterPaint;
/// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on
/// the optional arguments.
SpineWidgetController(
{this.onInitialized, this.onBeforeUpdateWorldTransforms, this.onAfterUpdateWorldTransforms, this.onBeforePaint, this.onAfterPaint});
@ -25,31 +54,38 @@ class SpineWidgetController {
onInitialized?.call(this);
}
/// The [Atlas] from which images to render the skeleton are sourced.
Atlas get atlas {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.atlas;
}
/// The setup-pose data used by the skeleton.
SkeletonData get skeletonData {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.skeletonData;
}
/// The mixing information used by the [AnimationState]
AnimationStateData get animationStateData {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.animationStateData;
}
/// The [AnimationState] used to manage animations that are being applied to the
/// skeleton.
AnimationState get animationState {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.animationState;
}
/// The [Skeleton]
Skeleton get skeleton {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!.skeleton;
}
/// The [SkeletonDrawable]
SkeletonDrawable get drawable {
if (_drawable == null) throw Exception("Controller is not initialized yet.");
return _drawable!;
@ -62,21 +98,49 @@ class SpineWidgetController {
_scaleY = scaleY;
}
void _setRenderObject(_SpineRenderObject? renderObject) {
_renderObject = renderObject;
}
/// Transforms the coordinates given in the [SpineWidget] coordinate system in [position] to
/// the skeleton coordinate system. See the `ik_following.dart` example how to use this
/// to move a bone based on user touch input.
Offset toSkeletonCoordinates(Offset position) {
var x = position.dx;
var y = position.dy;
return Offset(x / _scaleX - _offsetX, y / _scaleY - _offsetY);
}
/// Pauses updating and rendering the skeleton.
void pause() {
_isPlaying = false;
}
/// Resumes updating and rendering the skeleton.
void resume() {
_isPlaying = true;
_renderObject?._stopwatch.reset();
_renderObject?._stopwatch.start();
_renderObject?._scheduleFrame();
}
bool get isPlaying {
return _isPlaying;
}
}
enum AssetType { asset, file, http, drawable }
enum _AssetType { asset, file, http, drawable }
/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box
/// used to scale and fit a skeleton inside the bounds of a [SpineWidget].
abstract class BoundsProvider {
const BoundsProvider();
Bounds computeBounds(SkeletonDrawable drawable);
}
/// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible
/// attachments in the setup pose.
class SetupPoseBounds extends BoundsProvider {
const SetupPoseBounds();
@ -86,6 +150,7 @@ class SetupPoseBounds extends BoundsProvider {
}
}
/// A [BoundsProvider] that returns fixed bounds.
class RawBounds extends BoundsProvider {
final double x, y, width, height;
@ -97,11 +162,17 @@ class RawBounds extends BoundsProvider {
}
}
/// A [BoundsProvider] that calculates the bounding box needed for a combination of skins
/// and an animation.
class SkinAndAnimationBounds extends BoundsProvider {
final List<String> skins;
final String? animation;
final double stepTime;
/// Constructs a new provider that will use the given [skins] and [animation] to calculate
/// the bounding box of the skeleton. If no skins are given, the default skin is used.
/// The [stepTime], given in seconds, defines at what interval the bounds should be sampled
/// across the entire animation.
SkinAndAnimationBounds({List<String>? skins, this.animation, this.stepTime = 0.1})
: skins = skins == null || skins.isEmpty ? ["default"] : skins;
@ -151,15 +222,15 @@ class SkinAndAnimationBounds extends BoundsProvider {
}
}
class ComputedBounds extends BoundsProvider {
@override
Bounds computeBounds(SkeletonDrawable drawable) {
return Bounds(0, 0, 0, 0);
}
}
/// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset],
/// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawable] ([SpineWidget.fromDrawable]).
///
/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
///
/// The size of the widget can be derived from the bounds provided by a [BoundsProvider]. If the widget is not sized by the bounds
/// computed by the [BoundsProvider], the widget will use the computed bounds to fit the skeleton inside the widget's dimensions.
class SpineWidget extends StatefulWidget {
final AssetType _assetType;
final _AssetType _assetType;
final AssetBundle? _bundle;
final String? _skeletonFile;
final String? _atlasFile;
@ -170,9 +241,21 @@ class SpineWidget extends StatefulWidget {
final BoundsProvider _boundsProvider;
final bool _sizedByBounds;
SpineWidget.asset(this._atlasFile, this._skeletonFile, this._controller,
/// Constructs a new [SpineWidget] from files in the root bundle or the optionally specified [bundle]. The [_atlasFile] specifies the
/// `.atlas` file to be loaded for the images used to render the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or
/// `.skel` file containing the skeleton data.
///
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
/// modifying how the skeleton inside the widget is animated and rendered.
///
/// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
/// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
/// are used.
///
/// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
SpineWidget.fromAsset(this._atlasFile, this._skeletonFile, this._controller,
{AssetBundle? bundle, BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.asset,
: _assetType = _AssetType.asset,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
@ -180,9 +263,20 @@ class SpineWidget extends StatefulWidget {
_drawable = null,
_bundle = bundle ?? rootBundle;
const SpineWidget.file(this._atlasFile, this._skeletonFile, this._controller,
/// Constructs a new [SpineWidget] from files. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render
/// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
///
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
/// modifying how the skeleton inside the widget is animated and rendered.
///
/// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
/// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
/// are used.
///
/// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
const SpineWidget.fromFile(this._atlasFile, this._skeletonFile, this._controller,
{BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.file,
: _assetType = _AssetType.file,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
@ -190,9 +284,20 @@ class SpineWidget extends StatefulWidget {
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
const SpineWidget.http(this._atlasFile, this._skeletonFile, this._controller,
/// Constructs a new [SpineWidget] from HTTP URLs. The [_atlasFile] specifies the `.atlas` file to be loaded for the images used to render
/// the skeleton. The [_skeletonFile] specifies either a Skeleton `.json` or `.skel` file containing the skeleton data.
///
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
/// modifying how the skeleton inside the widget is animated and rendered.
///
/// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
/// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
/// are used.
///
/// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
const SpineWidget.fromHttp(this._atlasFile, this._skeletonFile, this._controller,
{BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.http,
: _assetType = _AssetType.http,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
@ -200,9 +305,19 @@ class SpineWidget extends StatefulWidget {
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
const SpineWidget.drawable(this._drawable, this._controller,
/// Constructs a new [SpineWidget] from a [SkeletonDrawable].
///
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
/// modifying how the skeleton inside the widget is animated and rendered.
///
/// The skeleton is fitted and aligned inside the widget as per the [fit] and [alignment] arguments. For this purpose, the skeleton
/// bounds must be computed via a [BoundsProvider]. By default, [BoxFit.contain], [Alignment.center], and a [SetupPoseBounds] provider
/// are used.
///
/// The widget can optionally by sized by the bounds provided by the [BoundsProvider] by passing `true` for [sizedByBounds].
const SpineWidget.fromDrawable(this._drawable, this._controller,
{BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.drawable,
: _assetType = _AssetType.drawable,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
@ -222,7 +337,7 @@ class _SpineWidgetState extends State<SpineWidget> {
@override
void initState() {
super.initState();
if (widget._assetType == AssetType.drawable) {
if (widget._assetType == _AssetType.drawable) {
loadDrawable(widget._drawable!);
} else {
loadFromAsset(widget._bundle, widget._atlasFile!, widget._skeletonFile!, widget._assetType);
@ -236,18 +351,18 @@ class _SpineWidgetState extends State<SpineWidget> {
setState(() {});
}
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, AssetType assetType) async {
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async {
switch (assetType) {
case AssetType.asset:
case _AssetType.asset:
loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle));
break;
case AssetType.file:
case _AssetType.file:
loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile));
break;
case AssetType.http:
case _AssetType.http:
loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile));
break;
case AssetType.drawable:
case _AssetType.drawable:
throw Exception("Drawable can not be loaded via loadFromAsset().");
}
}
@ -303,6 +418,7 @@ class _SpineRenderObject extends RenderBox {
Alignment _alignment;
Bounds _bounds;
bool _sizedByBounds;
bool _disposed = false;
_SpineRenderObject(this._skeletonDrawable, this._controller, this._fit, this._alignment, this._bounds, this._sizedByBounds);
@ -405,22 +521,39 @@ class _SpineRenderObject extends RenderBox {
void attach(rendering.PipelineOwner owner) {
super.attach(owner);
_stopwatch.start();
SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);
_controller._setRenderObject(this);
}
@override
void detach() {
_stopwatch.stop();
super.detach();
_controller._setRenderObject(null);
}
@override
void dispose() {
super.dispose();
_disposed = true;
}
void _scheduleFrame() {
SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);
}
void _beginFrame(Duration duration) {
if (_disposed) return;
_deltaTime = _stopwatch.elapsedTicks / _stopwatch.frequency;
_stopwatch.reset();
_stopwatch.start();
_controller.onBeforeUpdateWorldTransforms?.call(_controller);
_skeletonDrawable.update(_deltaTime);
_controller.onAfterUpdateWorldTransforms?.call(_controller);
markNeedsPaint();
if (_controller.isPlaying) {
_controller.onBeforeUpdateWorldTransforms?.call(_controller);
_skeletonDrawable.update(_deltaTime);
_controller.onAfterUpdateWorldTransforms?.call(_controller);
markNeedsPaint();
_scheduleFrame();
}
}
void _setCanvasTransform(Canvas canvas, Offset offset) {
@ -480,6 +613,5 @@ class _SpineRenderObject extends RenderBox {
_controller.onAfterPaint?.call(_controller, canvas, commands);
canvas.restore();
SchedulerBinding.instance.scheduleFrameCallback(_beginFrame);
}
}

View File

@ -2115,6 +2115,12 @@ void spine_bone_update_world_transform_with(spine_bone bone, float x, float y, f
_bone->updateWorldTransform(x, y, rotation, scaleX, scaleY, shearX, shearY);
}
void spine_bone_update_applied_transform(spine_bone bone) {
if (bone == nullptr) return;
Bone *_bone = (Bone *) bone;
_bone->updateAppliedTransform();
}
void spine_bone_set_to_setup_pose(spine_bone bone) {
if (bone == nullptr) return;
Bone *_bone = (Bone *) bone;

View File

@ -489,6 +489,7 @@ SPINE_FLUTTER_EXPORT int32_t spine_bone_get_is_y_down();
SPINE_FLUTTER_EXPORT void spine_bone_update(spine_bone bone);
SPINE_FLUTTER_EXPORT void spine_bone_update_world_transform(spine_bone bone);
SPINE_FLUTTER_EXPORT void spine_bone_update_world_transform_with(spine_bone bone, float x, float y, float rotation, float scaleX, float scaleY, float shearX, float shearY);
SPINE_FLUTTER_EXPORT void spine_bone_update_applied_transform(spine_bone bone);
SPINE_FLUTTER_EXPORT void spine_bone_set_to_setup_pose(spine_bone bone);
SPINE_FLUTTER_EXPORT spine_vector spine_bone_world_to_local(spine_bone bone, float worldX, float worldY);
SPINE_FLUTTER_EXPORT spine_vector spine_bone_local_to_world(spine_bone bone, float localX, float localY);

View File

@ -51,7 +51,7 @@ void SpineAnimationMix::_bind_methods() {
SpineAnimationMix::SpineAnimationMix() : from(""), to(""), mix(0) {
}
void SpineAnimationMix::set_from(const StringName &_from) {
void SpineAnimationMix::set_from(const String &_from) {
this->from = _from;
}
@ -59,7 +59,7 @@ String SpineAnimationMix::get_from() {
return from;
}
void SpineAnimationMix::set_to(const StringName &_to) {
void SpineAnimationMix::set_to(const String &_to) {
this->to = _to;
}

View File

@ -24,11 +24,11 @@ protected:
public:
SpineAnimationMix();
void set_from(const StringName &from);
void set_from(const String &from);
String get_from();
void set_to(const StringName &to);
void set_to(const String &to);
String get_to();

View File

@ -53,9 +53,72 @@ static int sprite_count = 0;
static spine::Vector<unsigned short> quad_indices;
static spine::Vector<float> scratch_vertices;
static Vector<Vector2> scratch_points;
static Vector<Vector2> scratch_uvs;
static Vector<Color> scratch_colors;
static Vector<int> scratch_indices;
static void clear_triangles(SpineMesh2D *mesh_instance) {
#if VERSION_MAJOR > 3
RenderingServer::get_singleton()->canvas_item_clear(mesh_instance->get_canvas_item());
#else
VisualServer::get_singleton()->canvas_item_clear(mesh_instance->get_canvas_item());
#endif
}
static void add_triangles(SpineMesh2D *mesh_instance,
const Vector<Point2> &vertices,
const Vector<Point2> &uvs,
const Vector<Color> &colors,
const Vector<int> &indices,
SpineRendererObject *renderer_object) {
#if VERSION_MAJOR > 3
RenderingServer::get_singleton()->canvas_item_add_triangle_array(mesh_instance->get_canvas_item(),
indices,
vertices,
colors,
uvs,
Vector<int>(),
Vector<float>(),
renderer_object->canvas_texture.is_valid() ? renderer_object->canvas_texture->get_rid() : RID(),
-1);
#else
auto texture = renderer_object->texture;
auto normal_map = renderer_object->normal_map;
VisualServer::get_singleton()->canvas_item_add_triangle_array(mesh_instance->get_canvas_item(),
indices,
vertices,
colors,
uvs,
Vector<int>(),
Vector<float>(),
texture.is_null() ? RID() : texture->get_rid(),
-1,
normal_map.is_null() ? RID() : normal_map->get_rid());
#endif
}
void SpineMesh2D::_notification(int what) {
switch (what) {
case NOTIFICATION_READY: {
set_process_internal(true);
break;
}
case NOTIFICATION_INTERNAL_PROCESS:
#if VERSION_MAJOR > 3
queue_redraw();
#else
update();
#endif
break;
case NOTIFICATION_DRAW:
//clear_triangles(this);
if (renderer_object)
add_triangles(this, vertices, uvs, colors, indices, renderer_object);
break;
default:
break;
}
}
void SpineMesh2D::_bind_methods() {
}
void SpineSprite::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_skeleton_data_res", "skeleton_data_res"), &SpineSprite::set_skeleton_data_res);
@ -259,7 +322,7 @@ void SpineSprite::on_skeleton_data_changed() {
void SpineSprite::generate_meshes_for_slots(Ref<SpineSkeleton> skeleton_ref) {
auto skeleton = skeleton_ref->get_spine_object();
for (int i = 0, n = (int) skeleton->getSlots().size(); i < n; i++) {
auto mesh_instance = memnew(MeshInstance2D);
auto mesh_instance = memnew(SpineMesh2D);
mesh_instance->set_position(Vector2(0, 0));
mesh_instance->set_material(default_materials[spine::BlendMode_Normal]);
// Needed so that debug drawables are rendered in front of attachments
@ -490,54 +553,14 @@ void SpineSprite::update_skeleton(float delta) {
#endif
}
static void clear_mesh_instance(MeshInstance2D *mesh_instance) {
#if VERSION_MAJOR > 3
RenderingServer::get_singleton()->canvas_item_clear(mesh_instance->get_canvas_item());
#else
VisualServer::get_singleton()->canvas_item_clear(mesh_instance->get_canvas_item());
#endif
}
static void add_triangles(MeshInstance2D *mesh_instance,
const Vector<Point2> &vertices,
const Vector<Point2> &uvs,
const Vector<Color> &colors,
const Vector<int> &indices,
SpineRendererObject *renderer_object) {
#if VERSION_MAJOR > 3
RenderingServer::get_singleton()->canvas_item_add_triangle_array(mesh_instance->get_canvas_item(),
indices,
vertices,
colors,
uvs,
Vector<int>(),
Vector<float>(),
renderer_object->canvas_texture.is_valid() ? renderer_object->canvas_texture->get_rid() : RID(),
-1);
#else
auto texture = renderer_object->texture;
auto normal_map = renderer_object->normal_map;
VisualServer::get_singleton()->canvas_item_add_triangle_array(mesh_instance->get_canvas_item(),
indices,
vertices,
colors,
uvs,
Vector<int>(),
Vector<float>(),
texture.is_null() ? RID() : texture->get_rid(),
-1,
normal_map.is_null() ? RID() : normal_map->get_rid());
#endif
}
void SpineSprite::update_meshes(Ref<SpineSkeleton> skeleton_ref) {
spine::Skeleton *skeleton = skeleton_ref->get_spine_object();
for (int i = 0, n = (int) skeleton->getSlots().size(); i < n; ++i) {
spine::Slot *slot = skeleton->getDrawOrder()[i];
spine::Attachment *attachment = slot->getAttachment();
MeshInstance2D *mesh_instance = mesh_instances[i];
SpineMesh2D *mesh_instance = mesh_instances[i];
mesh_instance->renderer_object = nullptr;
mesh_instance->set_light_mask(get_light_mask());
clear_mesh_instance(mesh_instance);
if (!attachment) {
skeleton_clipper->clipEnd(*slot);
continue;
@ -607,21 +630,20 @@ void SpineSprite::update_meshes(Ref<SpineSkeleton> skeleton_ref) {
if (indices->size() > 0) {
// Set the mesh
size_t num_vertices = vertices->size() / 2;
scratch_points.resize((int) num_vertices);
memcpy(scratch_points.ptrw(), vertices->buffer(), num_vertices * 2 * sizeof(float));
scratch_uvs.resize((int) num_vertices);
memcpy(scratch_uvs.ptrw(), uvs->buffer(), num_vertices * 2 * sizeof(float));
scratch_colors.resize((int) num_vertices);
mesh_instance->vertices.resize((int) num_vertices);
memcpy(mesh_instance->vertices.ptrw(), vertices->buffer(), num_vertices * 2 * sizeof(float));
mesh_instance->uvs.resize((int) num_vertices);
memcpy(mesh_instance->uvs.ptrw(), uvs->buffer(), num_vertices * 2 * sizeof(float));
mesh_instance->colors.resize((int) num_vertices);
for (int j = 0; j < (int) num_vertices; j++) {
scratch_colors.set(j, Color(tint.r, tint.g, tint.b, tint.a));
mesh_instance->colors.set(j, Color(tint.r, tint.g, tint.b, tint.a));
}
scratch_indices.resize((int) indices->size());
mesh_instance->indices.resize((int) indices->size());
for (int j = 0; j < (int) indices->size(); ++j) {
scratch_indices.set(j, indices->buffer()[j]);
mesh_instance->indices.set(j, indices->buffer()[j]);
}
add_triangles(mesh_instance, scratch_points, scratch_uvs, scratch_colors, scratch_indices, renderer_object);
mesh_instance->renderer_object = renderer_object;
spine::BlendMode blend_mode = slot->getData().getBlendMode();
Ref<Material> custom_material;
@ -676,9 +698,9 @@ void SpineSprite::update_meshes(Ref<SpineSkeleton> skeleton_ref) {
}
void SpineSprite::draw() {
if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_collisions_hint()) return;
if (!animation_state.is_valid() && !skeleton.is_valid()) return;
if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_collisions_hint()) return;
auto mouse_position = get_local_mouse_position();
spine::Slot *hovered_slot = nullptr;

View File

@ -32,11 +32,35 @@
#include "SpineSkeleton.h"
#include "SpineAnimationState.h"
#include "scene/2d/node_2d.h"
#include "scene/2d/mesh_instance_2d.h"
class SpineSlotNode;
class SpineSprite : public Node2D, public spine::AnimationStateListenerObject {
struct SpineRendererObject;
class SpineSprite;
class SpineMesh2D : public Node2D {
GDCLASS(SpineMesh2D, Node2D);
friend class SpineSprite;
protected:
void _notification(int what);
static void _bind_methods();
Vector<Vector2> vertices;
Vector<Vector2> uvs;
Vector<Color> colors;
Vector<int> indices;
SpineRendererObject *renderer_object;
public:
SpineMesh2D() : renderer_object(nullptr){};
~SpineMesh2D(){};
};
class SpineSprite : public Node2D,
public spine::AnimationStateListenerObject {
GDCLASS(SpineSprite, Node2D)
friend class SpineBone;
@ -67,7 +91,7 @@ protected:
Color debug_clipping_color;
spine::Vector<spine::Vector<SpineSlotNode *>> slot_nodes;
Vector<MeshInstance2D *> mesh_instances;
Vector<SpineMesh2D *> mesh_instances;
static Ref<CanvasItemMaterial> default_materials[4];
Ref<Material> normal_material;
Ref<Material> additive_material;

View File

@ -153,13 +153,8 @@ void uninitialize_spine_godot_module(ModuleInitializationLevel level) {
#else
void unregister_spine_godot_types() {
#endif
/*ResourceLoader::remove_resource_format_loader(atlas_loader);
ResourceLoader::remove_resource_format_loader(atlas_loader);
ResourceSaver::remove_resource_format_saver(atlas_saver);
ResourceLoader::remove_resource_format_loader(skeleton_file_loader);
ResourceSaver::remove_resource_format_saver(skeleton_file_saver);*/
/*memdelete(atlas_loader);
memdelete(atlas_saver);
memdelete(skeleton_file_saver);
memdelete(skeleton_file_loader);*/
ResourceSaver::remove_resource_format_saver(skeleton_file_saver);
}

View File

@ -847,7 +847,7 @@ public class AnimationState {
this.timeScale = timeScale;
}
/** The AnimationStateData to look up mix durations. */
/** The {@link AnimationStateData} to look up mix durations. */
public AnimationStateData getData () {
return data;
}
@ -1199,13 +1199,13 @@ public class AnimationState {
}
/** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no
* mixing is currently occuring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
* mixing is currently occurring. When mixing from multiple animations, <code>mixingFrom</code> makes up a linked list. */
public @Null TrackEntry getMixingFrom () {
return mixingFrom;
}
/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
* currently occuring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
* currently occurring. When mixing to multiple animations, <code>mixingTo</code> makes up a linked list. */
public @Null TrackEntry getMixingTo () {
return mixingTo;
}

View File

@ -115,7 +115,7 @@ public class BoneData {
this.y = y;
}
/** The local rotation. */
/** The local rotation in degrees, counter clockwise. */
public float getRotation () {
return rotation;
}

View File

@ -331,6 +331,25 @@ bool USpineWidget::HasBone(const FString BoneName) {
return false;
}
FTransform USpineWidget::GetBoneTransform(const FString& BoneName) {
CheckState();
if (skeleton) {
Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
if (!bone) return FTransform();
FMatrix localTransform;
localTransform.SetIdentity();
localTransform.SetAxis(2, FVector(bone->getA(), 0, bone->getC()));
localTransform.SetAxis(0, FVector(bone->getB(), 0, bone->getD()));
localTransform.SetOrigin(FVector(bone->getWorldX(), 0, bone->getWorldY()));
FTransform result;
result.SetFromMatrix(localTransform);
return result;
}
return FTransform();
}
void USpineWidget::GetSlots(TArray<FString> &Slots) {
CheckState();
if (skeleton) {

View File

@ -132,6 +132,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasBone(const FString BoneName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
FTransform GetBoneTransform(const FString &BoneName);
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetSlots(TArray<FString> &Slots);

View File

@ -36,5 +36,5 @@ See the [Spine Runtimes documentation](http://esotericsoftware.com/spine-documen
The Spine UE4 example works on all platforms supported by Unreal Engine. The samples require Unreal Engine 4.25+.
1. Copy the `spine-cpp` folder from this repositories root directory to your `Plugins/SpinePlugin/Sources/SpinePlugin/Public/` directory.
1. Copy the `spine-cpp` folder from this repositories root directory to your `Plugins/SpinePlugin/Sources/SpinePlugin/Public/` directory. You can run the `setup.bat` or `setup.sh` scripts to accomplish this.
2. Open the SpineUE4.uproject file with Unreal Editor