[libgdx] Make sequence rendering thread-safe.

closes #2989
This commit is contained in:
Nathan Sweet 2026-03-08 18:47:28 -04:00
parent b920589757
commit 3789ec027d
17 changed files with 381 additions and 413 deletions

View File

@ -36,7 +36,6 @@ import com.badlogic.gdx.Files.FileType;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3FileHandle;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.Pool;
import com.esotericsoftware.spine.AnimationState.AnimationStateListener;
@ -53,11 +52,11 @@ import com.esotericsoftware.spine.attachments.Sequence;
/** Unit tests to ensure {@link AnimationState} is working as expected. */
public class AnimationStateTests {
final SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, Sequence sequence) {
return null;
}
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, Sequence sequence) {
return null;
}

View File

@ -30,7 +30,6 @@
package com.esotericsoftware.spine;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Animation.MixBlend;
import com.esotericsoftware.spine.Animation.MixDirection;
@ -48,11 +47,11 @@ public class BonePlotting {
static public void main (String[] args) throws Exception {
// Create a skeleton loader that doesn't use an atlas and doesn't create any attachments.
SkeletonJson json = new SkeletonJson(new AttachmentLoader() {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, Sequence sequence) {
return null;
}
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, Sequence sequence) {
return null;
}

View File

@ -49,7 +49,6 @@ import com.badlogic.gdx.physics.box2d.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.ScreenUtils;
import com.esotericsoftware.spine.Animation.MixBlend;
@ -88,12 +87,9 @@ public class Box2DExample extends ApplicationAdapter {
// This loader creates Box2dAttachments instead of RegionAttachments for an easy way to keep track of the Box2D body for
// each attachment.
AtlasAttachmentLoader atlasLoader = new AtlasAttachmentLoader(atlas) {
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
Box2dAttachment attachment = new Box2dAttachment(name);
AtlasRegion region = atlas.findRegion(attachment.getName());
if (region == null) throw new RuntimeException("Region not found in atlas: " + attachment);
attachment.setRegion(region);
return attachment;
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, Sequence sequence) {
findRegions(name, path, sequence);
return new Box2dAttachment(name, sequence);
}
};
SkeletonJson json = new SkeletonJson(atlasLoader);
@ -231,8 +227,8 @@ public class Box2DExample extends ApplicationAdapter {
static class Box2dAttachment extends RegionAttachment {
Body body;
public Box2dAttachment (String name) {
super(name);
public Box2dAttachment (String name, Sequence sequence) {
super(name, sequence);
}
}

View File

@ -4,7 +4,7 @@ package com.esotericsoftware.spine.utils;
import java.util.Locale;
public class JsonWriter {
private final StringBuffer buffer = new StringBuffer();
private final StringBuilder buffer = new StringBuilder();
private int depth = 0;
private boolean needsComma = false;

View File

@ -2479,10 +2479,12 @@ public class SkeletonSerializer {
json.writeValue("MeshAttachment");
json.writeName("region");
if (obj.getRegion() == null) {
Sequence sequence = obj.getSequence();
TextureRegion region = sequence.getRegion(sequence.getSetupIndex());
if (region == null) {
json.writeNull();
} else {
writeTextureRegion(obj.getRegion());
writeTextureRegion(region);
}
json.writeName("triangles");
@ -2501,8 +2503,10 @@ public class SkeletonSerializer {
json.writeName("uVs");
json.writeArrayStart();
for (float item : obj.getUVs()) {
json.writeValue(item);
float[] uvs = sequence.getUVs(sequence.getSetupIndex());
if (uvs != null) {
for (float item : uvs)
json.writeValue(item);
}
json.writeArrayEnd();
@ -2954,24 +2958,32 @@ public class SkeletonSerializer {
json.writeName("type");
json.writeValue("RegionAttachment");
Sequence sequence = obj.getSequence();
int setupIndex = sequence.getSetupIndex();
TextureRegion region = sequence.getRegion(setupIndex);
json.writeName("region");
if (obj.getRegion() == null) {
if (region == null) {
json.writeNull();
} else {
writeTextureRegion(obj.getRegion());
writeTextureRegion(region);
}
json.writeName("offset");
json.writeArrayStart();
for (float item : obj.getOffset()) {
json.writeValue(item);
float[] offset = sequence.getOffsets(setupIndex);
if (offset != null) {
for (float item : offset)
json.writeValue(item);
}
json.writeArrayEnd();
json.writeName("uVs");
json.writeArrayStart();
for (float item : obj.getUVs()) {
json.writeValue(item);
float[] uvs = sequence.getUVs(setupIndex);
if (uvs != null) {
for (float item : uvs)
json.writeValue(item);
}
json.writeArrayEnd();
@ -3003,11 +3015,7 @@ public class SkeletonSerializer {
json.writeValue(obj.getPath());
json.writeName("sequence");
if (obj.getSequence() == null) {
json.writeNull();
} else {
writeSequence(obj.getSequence());
}
writeSequence(obj.getSequence());
json.writeName("name");
json.writeValue(obj.getName());

View File

@ -43,7 +43,7 @@ import com.badlogic.gdx.utils.ObjectSet;
import com.esotericsoftware.spine.BoneData.Inherit;
import com.esotericsoftware.spine.attachments.Attachment;
import com.esotericsoftware.spine.attachments.HasTextureRegion;
import com.esotericsoftware.spine.attachments.HasSequence;
import com.esotericsoftware.spine.attachments.Sequence;
import com.esotericsoftware.spine.attachments.Sequence.SequenceMode;
import com.esotericsoftware.spine.attachments.VertexAttachment;
@ -1700,13 +1700,12 @@ public class Animation {
static private final int MODE = 1, DELAY = 2;
final int slotIndex;
final HasTextureRegion attachment;
final HasSequence attachment;
public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) {
super(frameCount,
Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId());
super(frameCount, Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasSequence)attachment).getSequence().getId());
this.slotIndex = slotIndex;
this.attachment = (HasTextureRegion)attachment;
this.attachment = (HasSequence)attachment;
}
public int getFrameEntries () {
@ -1743,8 +1742,6 @@ public class Animation {
if (!(slotAttachment instanceof VertexAttachment vertexAttachment)
|| vertexAttachment.getTimelineAttachment() != attachment) return;
}
Sequence sequence = ((HasTextureRegion)slotAttachment).getSequence();
if (sequence == null) return;
if (direction == out) {
if (blend == setup) pose.setSequenceIndex(-1);
@ -1762,7 +1759,7 @@ public class Animation {
int modeAndIndex = (int)frames[i + MODE];
float delay = frames[i + DELAY];
int index = modeAndIndex >> 4, count = sequence.getRegions().length;
int index = modeAndIndex >> 4, count = (((HasSequence)slotAttachment).getSequence()).getRegions().length;
SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf];
if (mode != SequenceMode.hold) {
index += (time - before) / delay + 0.0001f;

View File

@ -495,7 +495,7 @@ public class Skeleton {
if (attachment instanceof RegionAttachment region) {
verticesLength = 8;
vertices = temp.setSize(8);
region.computeWorldVertices(slot, vertices, 0, 2);
region.computeWorldVertices(slot, region.getOffsets(slot.applied), vertices, 0, 2);
triangles = quadTriangles;
} else if (attachment instanceof MeshAttachment mesh) {
verticesLength = mesh.getWorldVerticesLength();

View File

@ -475,7 +475,7 @@ public class SkeletonBinary extends SkeletonLoader {
if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
linkedMesh.mesh.setTimelineAttachment(linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh);
linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
if (linkedMesh.mesh.getRegion() == null) linkedMesh.mesh.updateRegion();
linkedMesh.mesh.updateSequence();
}
linkedMeshes.clear();
@ -561,7 +561,7 @@ public class SkeletonBinary extends SkeletonLoader {
case region -> {
String path = (flags & 16) != 0 ? input.readStringRef() : null;
int color = (flags & 32) != 0 ? input.readInt() : 0xffffffff;
Sequence sequence = (flags & 64) != 0 ? readSequence(input) : null;
Sequence sequence = readSequence(input, (flags & 64) != 0);
float rotation = (flags & 128) != 0 ? input.readFloat() : 0;
float x = input.readFloat();
float y = input.readFloat();
@ -582,8 +582,7 @@ public class SkeletonBinary extends SkeletonLoader {
region.setWidth(width * scale);
region.setHeight(height * scale);
Color.rgba8888ToColor(region.getColor(), color);
region.setSequence(sequence);
if (region.getRegion() != null) region.updateRegion();
region.updateSequence();
yield region;
}
case boundingbox -> {
@ -601,7 +600,7 @@ public class SkeletonBinary extends SkeletonLoader {
case mesh -> {
String path = (flags & 16) != 0 ? input.readStringRef() : name;
int color = (flags & 32) != 0 ? input.readInt() : 0xffffffff;
Sequence sequence = (flags & 64) != 0 ? readSequence(input) : null;
Sequence sequence = readSequence(input, (flags & 64) != 0);
int hullLength = input.readInt(true);
Vertices vertices = readVertices(input, (flags & 128) != 0);
float[] uvs = readFloatArray(input, vertices.length, 1);
@ -619,25 +618,24 @@ public class SkeletonBinary extends SkeletonLoader {
if (mesh == null) yield null;
mesh.setPath(path);
Color.rgba8888ToColor(mesh.getColor(), color);
mesh.setHullLength(hullLength << 1);
mesh.setBones(vertices.bones);
mesh.setVertices(vertices.vertices);
mesh.setWorldVerticesLength(vertices.length);
mesh.setTriangles(triangles);
mesh.setRegionUVs(uvs);
if (mesh.getRegion() != null) mesh.updateRegion();
mesh.setHullLength(hullLength << 1);
mesh.setSequence(sequence);
mesh.setTriangles(triangles);
if (nonessential) {
mesh.setEdges(edges);
mesh.setWidth(width * scale);
mesh.setHeight(height * scale);
}
mesh.updateSequence();
yield mesh;
}
case linkedmesh -> {
String path = (flags & 16) != 0 ? input.readStringRef() : name;
int color = (flags & 32) != 0 ? input.readInt() : 0xffffffff;
Sequence sequence = (flags & 64) != 0 ? readSequence(input) : null;
Sequence sequence = readSequence(input, (flags & 64) != 0);
boolean inheritTimelines = (flags & 128) != 0;
int skinIndex = input.readInt(true);
String parent = input.readStringRef();
@ -651,7 +649,6 @@ public class SkeletonBinary extends SkeletonLoader {
if (mesh == null) yield null;
mesh.setPath(path);
Color.rgba8888ToColor(mesh.getColor(), color);
mesh.setSequence(sequence);
if (nonessential) {
mesh.setWidth(width * scale);
mesh.setHeight(height * scale);
@ -711,8 +708,9 @@ public class SkeletonBinary extends SkeletonLoader {
};
}
private Sequence readSequence (SkeletonInput input) throws IOException {
var sequence = new Sequence(input.readInt(true));
private Sequence readSequence (SkeletonInput input, boolean hasPathSuffix) throws IOException {
if (!hasPathSuffix) return new Sequence(1, false);
var sequence = new Sequence(input.readInt(true), true);
sequence.setStart(input.readInt(true));
sequence.setDigits(input.readInt(true));
sequence.setSetupIndex(input.readInt(true));

View File

@ -492,7 +492,7 @@ public class SkeletonJson extends SkeletonLoader {
if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent);
linkedMesh.mesh.setTimelineAttachment(linkedMesh.inheritTimelines ? (VertexAttachment)parent : linkedMesh.mesh);
linkedMesh.mesh.setParentMesh((MeshAttachment)parent);
if (linkedMesh.mesh.getRegion() != null) linkedMesh.mesh.updateRegion();
linkedMesh.mesh.updateSequence();
}
linkedMeshes.clear();
@ -580,12 +580,11 @@ public class SkeletonJson extends SkeletonLoader {
region.setRotation(map.getFloat("rotation", 0));
region.setWidth(map.getFloat("width") * scale);
region.setHeight(map.getFloat("height") * scale);
region.setSequence(sequence);
String color = map.getString("color", null);
if (color != null) Color.valueOf(color, region.getColor());
if (region.getRegion() != null) region.updateRegion();
region.updateSequence();
yield region;
}
case boundingbox -> {
@ -609,7 +608,6 @@ public class SkeletonJson extends SkeletonLoader {
mesh.setWidth(map.getFloat("width", 0) * scale);
mesh.setHeight(map.getFloat("height", 0) * scale);
mesh.setSequence(sequence);
String parent = map.getString("parent", null);
if (parent != null) {
@ -622,10 +620,11 @@ public class SkeletonJson extends SkeletonLoader {
readVertices(map, mesh, uvs.length);
mesh.setTriangles(map.require("triangles").asShortArray());
mesh.setRegionUVs(uvs);
if (mesh.getRegion() != null) mesh.updateRegion();
if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() << 1);
if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray());
mesh.updateSequence();
yield mesh;
}
case path -> {
@ -680,8 +679,8 @@ public class SkeletonJson extends SkeletonLoader {
}
private Sequence readSequence (@Null JsonValue map) {
if (map == null) return null;
var sequence = new Sequence(map.getInt("count"));
if (map == null) return new Sequence(1, false);
var sequence = new Sequence(map.getInt("count"), true);
sequence.setStart(map.getInt("start", 1));
sequence.setDigits(map.getInt("digits", 0));
sequence.setSetupIndex(map.getInt("setup", 0));

View File

@ -41,6 +41,7 @@ import com.esotericsoftware.spine.attachments.Attachment;
import com.esotericsoftware.spine.attachments.ClippingAttachment;
import com.esotericsoftware.spine.attachments.MeshAttachment;
import com.esotericsoftware.spine.attachments.RegionAttachment;
import com.esotericsoftware.spine.attachments.Sequence;
import com.esotericsoftware.spine.attachments.SkeletonAttachment;
import com.esotericsoftware.spine.utils.SkeletonClipping;
import com.esotericsoftware.spine.utils.TwoColorPolygonBatch;
@ -83,7 +84,11 @@ public class SkeletonRenderer {
SlotPose pose = slot.applied;
Attachment attachment = pose.attachment;
if (attachment instanceof RegionAttachment region) {
region.computeWorldVertices(slot, vertices, 0, 5);
Sequence sequence = region.getSequence();
int sequenceIndex = sequence.resolveIndex(pose);
Texture texture = sequence.getRegion(sequenceIndex).getTexture();
float[] uvs = sequence.getUVs(sequenceIndex);
region.computeWorldVertices(slot, sequence.getOffsets(sequenceIndex), vertices, 0, 5);
Color color = region.getColor(), slotColor = pose.getColor();
float alpha = a * slotColor.a * color.a * 255;
float multiplier = pmaColors ? alpha : 255;
@ -102,14 +107,13 @@ public class SkeletonRenderer {
| (int)(b * slotColor.b * color.b * multiplier) << 16 //
| (int)(g * slotColor.g * color.g * multiplier) << 8 //
| (int)(r * slotColor.r * color.r * multiplier));
float[] uvs = region.getUVs();
for (int u = 0, v = 2; u < 8; u += 2, v += 5) {
vertices[v] = c;
vertices[v + 1] = uvs[u];
vertices[v + 2] = uvs[u + 1];
}
batch.draw(region.getRegion().getTexture(), vertices, 0, 20);
batch.draw(texture, vertices, 0, 20);
} else if (attachment instanceof ClippingAttachment) {
throw new RuntimeException(batch.getClass().getSimpleName()
@ -153,10 +157,12 @@ public class SkeletonRenderer {
if (attachment instanceof RegionAttachment region) {
verticesLength = 20;
vertices = this.vertices.items;
region.computeWorldVertices(slot, vertices, 0, 5);
Sequence sequence = region.getSequence();
int sequenceIndex = sequence.resolveIndex(pose);
region.computeWorldVertices(slot, sequence.getOffsets(sequenceIndex), vertices, 0, 5);
triangles = quadTriangles;
texture = region.getRegion().getTexture();
uvs = region.getUVs();
texture = sequence.getRegion(sequenceIndex).getTexture();
uvs = sequence.getUVs(sequenceIndex);
color = region.getColor();
} else if (attachment instanceof MeshAttachment mesh) {
@ -165,8 +171,10 @@ public class SkeletonRenderer {
vertices = this.vertices.setSize(verticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, count, vertices, 0, 5);
triangles = mesh.getTriangles();
texture = mesh.getRegion().getTexture();
uvs = mesh.getUVs();
Sequence sequence = mesh.getSequence();
int sequenceIndex = sequence.resolveIndex(pose);
texture = sequence.getRegion(sequenceIndex).getTexture();
uvs = sequence.getUVs(sequenceIndex);
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment clip) {
@ -248,10 +256,12 @@ public class SkeletonRenderer {
if (attachment instanceof RegionAttachment region) {
verticesLength = 24;
vertices = this.vertices.items;
region.computeWorldVertices(slot, vertices, 0, 6);
Sequence sequence = region.getSequence();
int sequenceIndex = sequence.resolveIndex(pose);
region.computeWorldVertices(slot, sequence.getOffsets(sequenceIndex), vertices, 0, 6);
triangles = quadTriangles;
texture = region.getRegion().getTexture();
uvs = region.getUVs();
texture = sequence.getRegion(sequenceIndex).getTexture();
uvs = sequence.getUVs(sequenceIndex);
color = region.getColor();
} else if (attachment instanceof MeshAttachment mesh) {
@ -260,8 +270,10 @@ public class SkeletonRenderer {
vertices = this.vertices.setSize(verticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, count, vertices, 0, 6);
triangles = mesh.getTriangles();
texture = mesh.getRegion().getTexture();
uvs = mesh.getUVs();
Sequence sequence = mesh.getSequence();
int sequenceIndex = sequence.resolveIndex(pose);
texture = sequence.getRegion(sequenceIndex).getTexture();
uvs = sequence.getUVs(sequenceIndex);
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment clip) {

View File

@ -126,7 +126,8 @@ public class SkeletonRendererDebug {
if (!slot.bone.active) continue;
if (slot.pose.attachment instanceof RegionAttachment region) {
float[] vertices = this.vertices.items;
region.computeWorldVertices(slot, vertices, 0, 2);
float[] offsets = region.getOffsets(slot.applied);
region.computeWorldVertices(slot, offsets, vertices, 0, 2);
shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);

View File

@ -32,7 +32,6 @@ package com.esotericsoftware.spine.attachments;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Skin;
@ -55,40 +54,27 @@ public class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions;
}
private void loadSequence (String name, String basePath, Sequence sequence) {
protected void findRegions (String name, String basePath, Sequence sequence) {
TextureRegion[] regions = sequence.getRegions();
for (int i = 0, n = regions.length; i < n; i++) {
String path = sequence.getPath(basePath, i);
regions[i] = atlas.findRegion(path);
if (regions[i] == null && !allowMissingRegions)
throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")");
}
for (int i = 0, n = regions.length; i < n; i++)
regions[i] = findRegion(name, sequence.getPath(basePath, i));
}
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
var attachment = new RegionAttachment(name);
if (sequence != null)
loadSequence(name, path, sequence);
else {
AtlasRegion region = atlas.findRegion(path);
if (region == null && !allowMissingRegions)
throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
attachment.setRegion(region);
}
return attachment;
protected AtlasRegion findRegion (String name, String path) {
AtlasRegion region = atlas.findRegion(path);
if (region == null && !allowMissingRegions)
throw new RuntimeException("Region not found in atlas: " + path + " (attachment: " + name + ")");
return region;
}
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
var attachment = new MeshAttachment(name);
if (sequence != null)
loadSequence(name, path, sequence);
else {
AtlasRegion region = atlas.findRegion(path);
if (region == null && !allowMissingRegions)
throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
attachment.setRegion(region);
}
return attachment;
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, Sequence sequence) {
findRegions(name, path, sequence);
return new RegionAttachment(name, sequence);
}
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, Sequence sequence) {
findRegions(name, path, sequence);
return new MeshAttachment(name, sequence);
}
public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {

View File

@ -39,10 +39,10 @@ import com.esotericsoftware.spine.Skin;
* Runtimes Guide. */
public interface AttachmentLoader {
/** @return May be null to not load the attachment. */
public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence);
public @Null RegionAttachment newRegionAttachment (Skin skin, String name, String path, Sequence sequence);
/** @return May be null to not load the attachment. In that case null should also be returned for child meshes. */
public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence);
public @Null MeshAttachment newMeshAttachment (Skin skin, String name, String path, Sequence sequence);
/** @return May be null to not load the attachment. */
public @Null BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name);

View File

@ -30,29 +30,16 @@
package com.esotericsoftware.spine.attachments;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
public interface HasTextureRegion {
/** The name used to find the {@link #getRegion()}. */
public interface HasSequence {
public String getPath ();
public void setPath (String path);
public TextureRegion getRegion ();
/** Sets the region used to draw the attachment. After setting the region or if the region's properties are changed,
* {@link #updateRegion()} must be called. */
public void setRegion (TextureRegion region);
/** Updates any values the attachment calculates using the {@link #getRegion()}. Must be called after setting the
* {@link #getRegion()} or if the region's properties are changed. */
public void updateRegion ();
/** The color to tint the attachment. */
public Color getColor ();
public @Null Sequence getSequence ();
/** Calls {@link Sequence#update(HasSequence)} on this attachment's sequence. */
public void updateSequence ();
public void setSequence (@Null Sequence sequence);
public Sequence getSequence ();
}

View File

@ -36,29 +36,27 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
* supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
* <p>
* See <a href="https://esotericsoftware.com/spine-meshes">Mesh attachments</a> in the Spine User Guide. */
public class MeshAttachment extends VertexAttachment implements HasTextureRegion {
private TextureRegion region;
private String path;
private float[] regionUVs, uvs;
public class MeshAttachment extends VertexAttachment implements HasSequence {
private final Sequence sequence;
float[] regionUVs;
private short[] triangles;
private final Color color = new Color(1, 1, 1, 1);
private int hullLength;
private String path;
private final Color color = new Color(1, 1, 1, 1);
private @Null MeshAttachment parentMesh;
private @Null Sequence sequence;
// Nonessential.
private @Null short[] edges;
private float width, height;
public MeshAttachment (String name) {
public MeshAttachment (String name, Sequence sequence) {
super(name);
if (sequence == null) throw new IllegalArgumentException("sequence cannot be null.");
this.sequence = sequence;
}
/** Copy constructor. Use {@link #newLinkedMesh()} if the other mesh is a linked mesh. */
@ -67,21 +65,17 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
if (parentMesh != null) throw new IllegalArgumentException("Use newLinkedMesh to copy a linked mesh.");
region = other.region;
path = other.path;
color.set(other.color);
regionUVs = new float[other.regionUVs.length];
arraycopy(other.regionUVs, 0, regionUVs, 0, regionUVs.length);
uvs = new float[other.uvs.length];
arraycopy(other.uvs, 0, uvs, 0, uvs.length);
triangles = new short[other.triangles.length];
arraycopy(other.triangles, 0, triangles, 0, triangles.length);
hullLength = other.hullLength;
sequence = other.sequence != null ? new Sequence(other.sequence) : null;
sequence = new Sequence(other.sequence);
// Nonessential.
if (other.edges != null) {
@ -92,99 +86,6 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
height = other.height;
}
public void setRegion (TextureRegion region) {
if (region == null) throw new IllegalArgumentException("region cannot be null.");
this.region = region;
}
public @Null TextureRegion getRegion () {
return region;
}
/** Calculates {@link #uvs} using the {@link #regionUVs} and region. Must be called if the region, the region's properties, or
* the {@link #regionUVs} are changed. */
public void updateRegion () {
float[] regionUVs = this.regionUVs;
if (this.uvs == null || this.uvs.length != regionUVs.length) this.uvs = new float[regionUVs.length];
float[] uvs = this.uvs;
int n = uvs.length;
float u, v, width, height;
if (region instanceof AtlasRegion region) {
u = region.getU();
v = region.getV();
float textureWidth = region.getTexture().getWidth(), textureHeight = region.getTexture().getHeight();
switch (region.degrees) {
case 90 -> {
u -= (region.originalHeight - region.offsetY - region.packedWidth) / textureWidth;
v -= (region.originalWidth - region.offsetX - region.packedHeight) / textureHeight;
width = region.originalHeight / textureWidth;
height = region.originalWidth / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + regionUVs[i + 1] * width;
uvs[i + 1] = v + (1 - regionUVs[i]) * height;
}
return;
}
case 180 -> {
u -= (region.originalWidth - region.offsetX - region.packedWidth) / textureWidth;
v -= region.offsetY / textureHeight;
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + (1 - regionUVs[i]) * width;
uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height;
}
return;
}
case 270 -> {
u -= region.offsetY / textureWidth;
v -= region.offsetX / textureHeight;
width = region.originalHeight / textureWidth;
height = region.originalWidth / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + (1 - regionUVs[i + 1]) * width;
uvs[i + 1] = v + regionUVs[i] * height;
}
return;
}
default -> {
u -= region.offsetX / textureWidth;
v -= (region.originalHeight - region.offsetY - region.packedHeight) / textureHeight;
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
}
}
} else if (region == null) {
u = v = 0;
width = height = 1;
} else {
u = region.getU();
v = region.getV();
width = region.getU2() - u;
height = region.getV2() - v;
}
for (int i = 0; i < n; i += 2) {
uvs[i] = u + regionUVs[i] * width;
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
/** If the attachment has a {@link #sequence}, the region may be changed. */
public void computeWorldVertices (Skeleton skeleton, Slot slot, int start, int count, float[] worldVertices, int offset,
int stride) {
if (sequence != null) sequence.apply(slot.getAppliedPose(), this);
super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride);
}
/** Triplets of vertex indices which describe the mesh's triangulation. */
public short[] getTriangles () {
return triangles;
}
public void setTriangles (short[] triangles) {
this.triangles = triangles;
}
/** The UV pair for each vertex, normalized within the texture region. */
public float[] getRegionUVs () {
return regionUVs;
@ -195,27 +96,13 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
this.regionUVs = regionUVs;
}
/** The UV pair for each vertex, normalized within the entire texture.
* <p>
* See {@link #updateRegion()}. */
public float[] getUVs () {
return uvs;
/** Triplets of vertex indices which describe the mesh's triangulation. */
public short[] getTriangles () {
return triangles;
}
public void setUVs (float[] uvs) {
this.uvs = uvs;
}
public Color getColor () {
return color;
}
public String getPath () {
return path;
}
public void setPath (String path) {
this.path = path;
public void setTriangles (short[] triangles) {
this.triangles = triangles;
}
/** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
@ -227,12 +114,32 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
this.hullLength = hullLength;
}
public Sequence getSequence () {
return sequence;
}
public void updateSequence () {
sequence.update(this);
}
public String getPath () {
return path;
}
public void setPath (String path) {
this.path = path;
}
public Color getColor () {
return color;
}
public void setEdges (short[] edges) {
this.edges = edges;
}
/** Vertex index pairs describing edges for controlling triangulation, or be null if nonessential data was not exported. Mesh
* triangles will never cross edges. Triangulation is not performed at runtime. */
* triangles never cross edges. Triangulation is not performed at runtime. */
public @Null short[] getEdges () {
return edges;
}
@ -255,14 +162,6 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
this.height = height;
}
public @Null Sequence getSequence () {
return sequence;
}
public void setSequence (@Null Sequence sequence) {
this.sequence = sequence;
}
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
* {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
* parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
@ -287,17 +186,81 @@ public class MeshAttachment extends VertexAttachment implements HasTextureRegion
/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. */
public MeshAttachment newLinkedMesh () {
var mesh = new MeshAttachment(name);
var mesh = new MeshAttachment(name, new Sequence(sequence));
mesh.timelineAttachment = timelineAttachment;
mesh.region = region;
mesh.path = path;
mesh.color.set(color);
mesh.setParentMesh(parentMesh != null ? parentMesh : this);
if (mesh.getRegion() != null) mesh.updateRegion();
mesh.updateSequence();
return mesh;
}
public MeshAttachment copy () {
return parentMesh != null ? newLinkedMesh() : new MeshAttachment(this);
}
/** Computes {@link Sequence#getUVs(int) UVs} for a mesh attachment.
* @param uvs Output array for the computed UVs, same length as regionUVs. */
static void computeUVs (@Null TextureRegion region, float[] regionUVs, float[] uvs) {
int n = uvs.length;
float u, v, width, height;
if (region instanceof AtlasRegion r) {
u = r.getU();
v = r.getV();
float textureWidth = r.getTexture().getWidth(), textureHeight = r.getTexture().getHeight();
switch (r.degrees) {
case 90 -> {
u -= (r.originalHeight - r.offsetY - r.packedWidth) / textureWidth;
v -= (r.originalWidth - r.offsetX - r.packedHeight) / textureHeight;
width = r.originalHeight / textureWidth;
height = r.originalWidth / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + regionUVs[i + 1] * width;
uvs[i + 1] = v + (1 - regionUVs[i]) * height;
}
return;
}
case 180 -> {
u -= (r.originalWidth - r.offsetX - r.packedWidth) / textureWidth;
v -= r.offsetY / textureHeight;
width = r.originalWidth / textureWidth;
height = r.originalHeight / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + (1 - regionUVs[i]) * width;
uvs[i + 1] = v + (1 - regionUVs[i + 1]) * height;
}
return;
}
case 270 -> {
u -= r.offsetY / textureWidth;
v -= r.offsetX / textureHeight;
width = r.originalHeight / textureWidth;
height = r.originalWidth / textureHeight;
for (int i = 0; i < n; i += 2) {
uvs[i] = u + (1 - regionUVs[i + 1]) * width;
uvs[i + 1] = v + regionUVs[i] * height;
}
return;
}
default -> {
u -= r.offsetX / textureWidth;
v -= (r.originalHeight - r.offsetY - r.packedHeight) / textureHeight;
width = r.originalWidth / textureWidth;
height = r.originalHeight / textureHeight;
}
}
} else if (region == null) {
u = v = 0;
width = height = 1;
} else {
u = region.getU();
v = region.getV();
width = region.getU2() - u;
height = region.getV2() - v;
}
for (int i = 0; i < n; i += 2) {
uvs[i] = u + regionUVs[i] * width;
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
}

View File

@ -38,32 +38,31 @@ import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.BonePose;
import com.esotericsoftware.spine.Slot;
import com.esotericsoftware.spine.SlotPose;
/** An attachment that displays a textured quadrilateral.
* <p>
* See <a href="https://esotericsoftware.com/spine-regions">Region attachments</a> in the Spine User Guide. */
public class RegionAttachment extends Attachment implements HasTextureRegion {
public class RegionAttachment extends Attachment implements HasSequence {
static public final int BLX = 0, BLY = 1;
static public final int ULX = 2, ULY = 3;
static public final int URX = 4, URY = 5;
static public final int BRX = 6, BRY = 7;
private TextureRegion region;
private final Sequence sequence;
float x, y, scaleX = 1, scaleY = 1, rotation, width, height;
private String path;
private float x, y, scaleX = 1, scaleY = 1, rotation, width, height;
private final float[] uvs = new float[8];
private final float[] offset = new float[8];
private final Color color = new Color(1, 1, 1, 1);
private @Null Sequence sequence;
public RegionAttachment (String name) {
public RegionAttachment (String name, Sequence sequence) {
super(name);
if (sequence == null) throw new IllegalArgumentException("sequence cannot be null.");
this.sequence = sequence;
}
/** Copy constructor. */
protected RegionAttachment (RegionAttachment other) {
super(other);
region = other.region;
path = other.path;
x = other.x;
y = other.y;
@ -72,148 +71,50 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
rotation = other.rotation;
width = other.width;
height = other.height;
arraycopy(other.uvs, 0, uvs, 0, 8);
arraycopy(other.offset, 0, offset, 0, 8);
color.set(other.color);
sequence = other.sequence != null ? new Sequence(other.sequence) : null;
sequence = new Sequence(other.sequence);
}
/** Calculates the {@link #offset} and {@link #uvs} using the region and the attachment's transform. Must be called if the
* region, the region's properties, or the transform are changed. */
public void updateRegion () {
float width = getWidth(), height = getHeight();
float localX2 = width / 2;
float localY2 = height / 2;
float localX = -localX2;
float localY = -localY2;
boolean rotated = false;
if (region instanceof AtlasRegion region) {
localX += region.offsetX / region.originalWidth * width;
localY += region.offsetY / region.originalHeight * height;
if (region.degrees == 90) {
rotated = true;
localX2 -= (region.originalWidth - region.offsetX - region.packedHeight) / region.originalWidth * width;
localY2 -= (region.originalHeight - region.offsetY - region.packedWidth) / region.originalHeight * height;
} else {
localX2 -= (region.originalWidth - region.offsetX - region.packedWidth) / region.originalWidth * width;
localY2 -= (region.originalHeight - region.offsetY - region.packedHeight) / region.originalHeight * height;
}
}
float scaleX = getScaleX(), scaleY = getScaleY();
localX *= scaleX;
localY *= scaleY;
localX2 *= scaleX;
localY2 *= scaleY;
float r = getRotation() * degRad, cos = cos(r), sin = sin(r);
float x = getX(), y = getY();
float localXCos = localX * cos + x;
float localXSin = localX * sin;
float localYCos = localY * cos + y;
float localYSin = localY * sin;
float localX2Cos = localX2 * cos + x;
float localX2Sin = localX2 * sin;
float localY2Cos = localY2 * cos + y;
float localY2Sin = localY2 * sin;
float[] offset = this.offset;
offset[BLX] = localXCos - localYSin;
offset[BLY] = localYCos + localXSin;
offset[ULX] = localXCos - localY2Sin;
offset[ULY] = localY2Cos + localXSin;
offset[URX] = localX2Cos - localY2Sin;
offset[URY] = localY2Cos + localX2Sin;
offset[BRX] = localX2Cos - localYSin;
offset[BRY] = localYCos + localX2Sin;
float[] uvs = this.uvs;
if (region == null) {
uvs[BLX] = 0;
uvs[BLY] = 0;
uvs[ULX] = 0;
uvs[ULY] = 1;
uvs[URX] = 1;
uvs[URY] = 1;
uvs[BRX] = 1;
uvs[BRY] = 0;
} else if (rotated) {
uvs[BLX] = region.getU2();
uvs[BLY] = region.getV();
uvs[ULX] = region.getU2();
uvs[ULY] = region.getV2();
uvs[URX] = region.getU();
uvs[URY] = region.getV2();
uvs[BRX] = region.getU();
uvs[BRY] = region.getV();
} else {
uvs[BLX] = region.getU2();
uvs[BLY] = region.getV2();
uvs[ULX] = region.getU();
uvs[ULY] = region.getV2();
uvs[URX] = region.getU();
uvs[URY] = region.getV();
uvs[BRX] = region.getU2();
uvs[BRY] = region.getV();
}
}
public void setRegion (TextureRegion region) {
if (region == null) throw new IllegalArgumentException("region cannot be null.");
this.region = region;
}
public @Null TextureRegion getRegion () {
return region;
}
/** Transforms the attachment's four vertices to world coordinates. If the attachment has a {@link #sequence}, the region may
* be changed.
/** Transforms the attachment's four vertices to world coordinates.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide.
* @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + 8.
* @param vertexOffsets The vertex {@link Sequence#getOffsets(int) offsets}.
* @param offset The <code>worldVertices</code> index to begin writing values.
* @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
public void computeWorldVertices (Slot slot, float[] worldVertices, int offset, int stride) {
if (sequence != null) sequence.apply(slot.getAppliedPose(), this);
float[] vertexOffset = this.offset;
public void computeWorldVertices (Slot slot, float[] vertexOffsets, float[] worldVertices, int offset, int stride) {
BonePose bone = slot.getBone().getAppliedPose();
float x = bone.getWorldX(), y = bone.getWorldY();
float a = bone.getA(), b = bone.getB(), c = bone.getC(), d = bone.getD();
float offsetX, offsetY;
offsetX = vertexOffset[BRX];
offsetY = vertexOffset[BRY];
float offsetX = vertexOffsets[BRX];
float offsetY = vertexOffsets[BRY];
worldVertices[offset] = offsetX * a + offsetY * b + x; // br
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[BLX];
offsetY = vertexOffset[BLY];
offsetX = vertexOffsets[BLX];
offsetY = vertexOffsets[BLY];
worldVertices[offset] = offsetX * a + offsetY * b + x; // bl
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[ULX];
offsetY = vertexOffset[ULY];
offsetX = vertexOffsets[ULX];
offsetY = vertexOffsets[ULY];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ul
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[URX];
offsetY = vertexOffset[URY];
offsetX = vertexOffsets[URX];
offsetY = vertexOffsets[URY];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ur
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
}
/** For each of the 4 vertices, a pair of <code>x,y</code> values that is the local position of the vertex.
* <p>
* See {@link #updateRegion()}. */
public float[] getOffset () {
return offset;
}
public float[] getUVs () {
return uvs;
/** Returns the vertex {@link Sequence#getOffsets(int) offsets} for the specified slot pose. */
public float[] getOffsets (SlotPose pose) {
return sequence.getOffsets(sequence.resolveIndex(pose));
}
/** The local x translation. */
@ -279,8 +180,12 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
this.height = height;
}
public Color getColor () {
return color;
public Sequence getSequence () {
return sequence;
}
public void updateSequence () {
sequence.update(this);
}
public String getPath () {
@ -291,15 +196,80 @@ public class RegionAttachment extends Attachment implements HasTextureRegion {
this.path = path;
}
public @Null Sequence getSequence () {
return sequence;
}
public void setSequence (@Null Sequence sequence) {
this.sequence = sequence;
public Color getColor () {
return color;
}
public RegionAttachment copy () {
return new RegionAttachment(this);
}
/** Computes {@link Sequence#getUVs(int) UVs} and {@link Sequence#getOffsets(int) offsets} for a region attachment.
* @param uvs Output array for the computed UVs, length of 8.
* @param offset Output array for the computed vertex offsets, length of 8. */
static void computeUVs (@Null TextureRegion region, float x, float y, float scaleX, float scaleY, float rotation, float width,
float height, float[] offset, float[] uvs) {
float localX2 = width / 2, localY2 = height / 2;
float localX = -localX2, localY = -localY2;
boolean rotated = false;
if (region instanceof AtlasRegion r) {
localX += r.offsetX / r.originalWidth * width;
localY += r.offsetY / r.originalHeight * height;
if (r.degrees == 90) {
rotated = true;
localX2 -= (r.originalWidth - r.offsetX - r.packedHeight) / r.originalWidth * width;
localY2 -= (r.originalHeight - r.offsetY - r.packedWidth) / r.originalHeight * height;
} else {
localX2 -= (r.originalWidth - r.offsetX - r.packedWidth) / r.originalWidth * width;
localY2 -= (r.originalHeight - r.offsetY - r.packedHeight) / r.originalHeight * height;
}
}
localX *= scaleX;
localY *= scaleY;
localX2 *= scaleX;
localY2 *= scaleY;
float r = rotation * degRad, cos = cos(r), sin = sin(r);
float localXCos = localX * cos + x;
float localXSin = localX * sin;
float localYCos = localY * cos + y;
float localYSin = localY * sin;
float localX2Cos = localX2 * cos + x;
float localX2Sin = localX2 * sin;
float localY2Cos = localY2 * cos + y;
float localY2Sin = localY2 * sin;
offset[BLX] = localXCos - localYSin;
offset[BLY] = localYCos + localXSin;
offset[ULX] = localXCos - localY2Sin;
offset[ULY] = localY2Cos + localXSin;
offset[URX] = localX2Cos - localY2Sin;
offset[URY] = localY2Cos + localX2Sin;
offset[BRX] = localX2Cos - localYSin;
offset[BRY] = localYCos + localX2Sin;
if (region == null) {
uvs[BLX] = 0;
uvs[BLY] = 0;
uvs[ULX] = 0;
uvs[ULY] = 1;
uvs[URX] = 1;
uvs[URY] = 1;
uvs[BRX] = 1;
uvs[BRY] = 0;
} else {
uvs[BLX] = region.getU2();
uvs[ULY] = region.getV2();
uvs[URX] = region.getU();
uvs[BRY] = region.getV();
if (rotated) {
uvs[BLY] = region.getV();
uvs[ULX] = region.getU2();
uvs[URY] = region.getV2();
uvs[BRX] = region.getU();
} else {
uvs[BLY] = region.getV2();
uvs[ULX] = region.getU();
uvs[URY] = region.getV();
uvs[BRX] = region.getU2();
}
}
}
}

View File

@ -35,46 +35,88 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.esotericsoftware.spine.SlotPose;
/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. {@link #getRegions() Regions} must
* be populated and {@link #update(HasSequence)} called before use. */
public class Sequence {
static private int nextID;
private final int id = nextID();
private final TextureRegion[] regions;
private final boolean pathSuffix;
private float[][] uvs, offsets;
private int start, digits, setupIndex;
public Sequence (int count) {
public Sequence (int count, boolean pathSuffix) {
regions = new TextureRegion[count];
this.pathSuffix = pathSuffix;
}
/** Copy constructor. */
protected Sequence (Sequence other) {
regions = new TextureRegion[other.regions.length];
arraycopy(other.regions, 0, regions, 0, regions.length);
int regionCount = other.regions.length;
regions = new TextureRegion[regionCount];
arraycopy(other.regions, 0, regions, 0, regionCount);
start = other.start;
digits = other.digits;
setupIndex = other.setupIndex;
}
pathSuffix = other.pathSuffix;
public void apply (SlotPose slot, HasTextureRegion attachment) {
int index = slot.getSequenceIndex();
if (index == -1) index = setupIndex;
if (index >= regions.length) index = regions.length - 1;
TextureRegion region = regions[index];
if (attachment.getRegion() != region) {
attachment.setRegion(region);
attachment.updateRegion();
if (other.uvs != null) {
int length = other.uvs[0].length;
uvs = new float[regionCount][length];
for (int i = 0; i < regionCount; i++)
arraycopy(other.uvs[i], 0, uvs[i], 0, length);
}
if (other.offsets != null) {
offsets = new float[regionCount][8];
for (int i = 0; i < regionCount; i++)
arraycopy(other.offsets[i], 0, offsets[i], 0, 8);
}
}
public String getPath (String basePath, int index) {
var buffer = new StringBuilder(basePath.length() + digits);
buffer.append(basePath);
String frame = Integer.toString(start + index);
for (int i = digits - frame.length(); i > 0; i--)
buffer.append('0');
buffer.append(frame);
return buffer.toString();
/** Computes UVs and offsets for the specified attachment. Must be called if the regions or attachment properties are
* changed. */
public void update (HasSequence attachment) {
int regionCount = regions.length;
if (attachment instanceof RegionAttachment region) {
uvs = new float[regionCount][8];
offsets = new float[regionCount][8];
for (int i = 0; i < regionCount; i++) {
RegionAttachment.computeUVs(regions[i], region.x, region.y, region.scaleX, region.scaleY, region.rotation,
region.width, region.height, offsets[i], uvs[i]);
}
} else if (attachment instanceof MeshAttachment mesh) {
float[] regionUVs = mesh.regionUVs;
uvs = new float[regionCount][regionUVs.length];
offsets = null;
for (int i = 0; i < regionCount; i++)
MeshAttachment.computeUVs(regions[i], regionUVs, uvs[i]);
}
}
public TextureRegion[] getRegions () {
return regions;
}
public int resolveIndex (SlotPose pose) {
int index = pose.getSequenceIndex();
if (index == -1) index = setupIndex;
if (index >= regions.length) index = regions.length - 1;
return index;
}
public TextureRegion getRegion (int index) {
return regions[index];
}
public float[] getUVs (int index) {
return uvs[index];
}
/** Returns vertex offsets from the center of a {@link RegionAttachment}. Invalid to call for a {@link MeshAttachment}. */
public float[] getOffsets (int index) {
return offsets[index];
}
public int getStart () {
@ -102,8 +144,19 @@ public class Sequence {
this.setupIndex = index;
}
public TextureRegion[] getRegions () {
return regions;
public boolean getPathSuffix () {
return pathSuffix;
}
public String getPath (String basePath, int index) {
if (!pathSuffix) return basePath;
var buffer = new StringBuilder(basePath.length() + digits);
buffer.append(basePath);
String frame = Integer.toString(start + index);
for (int i = digits - frame.length(); i > 0; i--)
buffer.append('0');
buffer.append(frame);
return buffer.toString();
}
/** Returns a unique ID for this attachment. */