mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-06 07:14:55 +08:00
Merge branch '4.2-beta' of https://github.com/EsotericSoftware/spine-runtimes into 4.2-beta
This commit is contained in:
commit
3cf5e14453
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"]),
|
||||
|
||||
@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user