[spine-android] Clean-up, batching renderer, clipping TBD

This commit is contained in:
Mario Zechner 2024-04-26 18:00:16 +02:00
parent f8ebef5715
commit 4f32c5bd1b
6 changed files with 471 additions and 378 deletions

View File

@ -34,7 +34,6 @@ fun AppContent() {
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
Box { Box {
BackgroundImage()
SpineViewComposable() SpineViewComposable()
} }
} }
@ -52,13 +51,3 @@ fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) {
modifier = modifier modifier = modifier
) )
} }
@Composable
fun BackgroundImage() {
val image: Painter = painterResource(id = com.esotericsoftware.spine.R.drawable.img) // Replace with your image resource
Image(
painter = image,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}

View File

@ -29,11 +29,9 @@
package com.esotericsoftware.spine.android; package com.esotericsoftware.spine.android;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Null; import com.badlogic.gdx.utils.Null;
import com.esotericsoftware.spine.Skin; import com.esotericsoftware.spine.Skin;
import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.AttachmentLoader;
import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.BoundingBoxAttachment;
@ -50,61 +48,61 @@ import com.esotericsoftware.spine.attachments.Sequence;
* Spine Runtimes Guide. */ * Spine Runtimes Guide. */
@SuppressWarnings("javadoc") @SuppressWarnings("javadoc")
public class AndroidAtlasAttachmentLoader implements AttachmentLoader { public class AndroidAtlasAttachmentLoader implements AttachmentLoader {
private AndroidTextureAtlas atlas; private AndroidTextureAtlas atlas;
public AndroidAtlasAttachmentLoader (AndroidTextureAtlas atlas) { public AndroidAtlasAttachmentLoader (AndroidTextureAtlas atlas) {
if (atlas == null) throw new IllegalArgumentException("atlas cannot be null."); if (atlas == null) throw new IllegalArgumentException("atlas cannot be null.");
this.atlas = atlas; this.atlas = atlas;
} }
private void loadSequence (String name, String basePath, Sequence sequence) { private void loadSequence (String name, String basePath, Sequence sequence) {
TextureRegion[] regions = sequence.getRegions(); TextureRegion[] regions = sequence.getRegions();
for (int i = 0, n = regions.length; i < n; i++) { for (int i = 0, n = regions.length; i < n; i++) {
String path = sequence.getPath(basePath, i); String path = sequence.getPath(basePath, i);
regions[i] = atlas.findRegion(path); regions[i] = atlas.findRegion(path);
if (regions[i] == null) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")"); if (regions[i] == null) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")");
} }
} }
public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) { public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
RegionAttachment attachment = new RegionAttachment(name); RegionAttachment attachment = new RegionAttachment(name);
if (sequence != null) if (sequence != null)
loadSequence(name, path, sequence); loadSequence(name, path, sequence);
else { else {
AtlasRegion region = atlas.findRegion(path); AtlasRegion region = atlas.findRegion(path);
if (region == null) if (region == null)
throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")"); throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
attachment.setRegion(region); attachment.setRegion(region);
} }
return attachment; return attachment;
} }
public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) { public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) {
MeshAttachment attachment = new MeshAttachment(name); MeshAttachment attachment = new MeshAttachment(name);
if (sequence != null) if (sequence != null)
loadSequence(name, path, sequence); loadSequence(name, path, sequence);
else { else {
AtlasRegion region = atlas.findRegion(path); AtlasRegion region = atlas.findRegion(path);
if (region == null) if (region == null)
throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
attachment.setRegion(region); attachment.setRegion(region);
} }
return attachment; return attachment;
} }
public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) { public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) {
return new BoundingBoxAttachment(name); return new BoundingBoxAttachment(name);
} }
public ClippingAttachment newClippingAttachment (Skin skin, String name) { public ClippingAttachment newClippingAttachment (Skin skin, String name) {
return new ClippingAttachment(name); return new ClippingAttachment(name);
} }
public PathAttachment newPathAttachment (Skin skin, String name) { public PathAttachment newPathAttachment (Skin skin, String name) {
return new PathAttachment(name); return new PathAttachment(name);
} }
public PointAttachment newPointAttachment (Skin skin, String name) { public PointAttachment newPointAttachment (Skin skin, String name) {
return new PointAttachment(name); return new PointAttachment(name);
} }
} }

View File

@ -1,45 +1,70 @@
package com.esotericsoftware.spine.android; package com.esotericsoftware.spine.android;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.utils.ObjectMap;
import com.esotericsoftware.spine.BlendMode;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapShader; import android.graphics.BitmapShader;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader; import android.graphics.Shader;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.TextureData;
public class AndroidTexture extends Texture { public class AndroidTexture extends Texture {
private Bitmap bitmap; private Bitmap bitmap;
private Paint paint; private ObjectMap<BlendMode, Paint> paints = new ObjectMap<>();
protected AndroidTexture(Bitmap bitmap) { protected AndroidTexture (Bitmap bitmap) {
super(); super();
this.bitmap = bitmap; this.bitmap = bitmap;
this.paint = new Paint(); for (BlendMode blendMode : BlendMode.values()) {
BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); Paint paint = new Paint();
paint.setShader(shader); BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
} paint.setShader(shader);
public Bitmap getBitmap() { switch (blendMode) {
return bitmap; case normal:
} paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
break;
case multiply:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
break;
case additive:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
break;
case screen:
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
break;
default:
break;
}
public Paint getPaint() { paints.put(blendMode, paint);
return paint; }
} }
@Override public Bitmap getBitmap () {
public int getWidth() { return bitmap;
return bitmap.getWidth(); }
}
@Override public Paint getPaint (BlendMode blendMode) {
public int getHeight() { return paints.get(blendMode);
return bitmap.getHeight(); }
}
@Override @Override
public void dispose() { public int getWidth () {
bitmap.recycle(); return bitmap.getWidth();
} }
@Override
public int getHeight () {
return bitmap.getHeight();
}
@Override
public void dispose () {
bitmap.recycle();
}
} }

View File

@ -1,98 +1,98 @@
package com.esotericsoftware.spine.android; package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Null;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.Buffer;
public class AndroidTextureAtlas { public class AndroidTextureAtlas {
private static interface BitmapLoader { private static interface BitmapLoader {
Bitmap load(String path); Bitmap load (String path);
} }
private Array<AndroidTexture> textures = new Array<>(); private Array<AndroidTexture> textures = new Array<>();
private Array<AtlasRegion> regions = new Array<>(); private Array<AtlasRegion> regions = new Array<>();
private AndroidTextureAtlas(TextureAtlasData data, BitmapLoader bitmapLoader) {
for (TextureAtlasData.Page page: data.getPages()) {
page.texture = new AndroidTexture(bitmapLoader.load(page.textureFile.path()));
textures.add((AndroidTexture) page.texture);
}
for (TextureAtlasData.Region region : data.getRegions()) { private AndroidTextureAtlas (TextureAtlasData data, BitmapLoader bitmapLoader) {
AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, // for (TextureAtlasData.Page page : data.getPages()) {
region.rotate ? region.height : region.width, // page.texture = new AndroidTexture(bitmapLoader.load(page.textureFile.path()));
region.rotate ? region.width : region.height); textures.add((AndroidTexture)page.texture);
atlasRegion.index = region.index; }
atlasRegion.name = region.name;
atlasRegion.offsetX = region.offsetX;
atlasRegion.offsetY = region.offsetY;
atlasRegion.originalHeight = region.originalHeight;
atlasRegion.originalWidth = region.originalWidth;
atlasRegion.rotate = region.rotate;
atlasRegion.degrees = region.degrees;
atlasRegion.names = region.names;
atlasRegion.values = region.values;
if (region.flip) atlasRegion.flip(false, true);
regions.add(atlasRegion);
}
}
/** Returns the first region found with the specified name. This method uses string comparison to find the region, so the for (TextureAtlasData.Region region : data.getRegions()) {
* result should be cached rather than calling this method multiple times. */ AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, //
public @Null AtlasRegion findRegion (String name) { region.rotate ? region.height : region.width, //
for (int i = 0, n = regions.size; i < n; i++) region.rotate ? region.width : region.height);
if (regions.get(i).name.equals(name)) return regions.get(i); atlasRegion.index = region.index;
return null; atlasRegion.name = region.name;
} atlasRegion.offsetX = region.offsetX;
atlasRegion.offsetY = region.offsetY;
atlasRegion.originalHeight = region.originalHeight;
atlasRegion.originalWidth = region.originalWidth;
atlasRegion.rotate = region.rotate;
atlasRegion.degrees = region.degrees;
atlasRegion.names = region.names;
atlasRegion.values = region.values;
if (region.flip) atlasRegion.flip(false, true);
regions.add(atlasRegion);
}
}
public Array<AndroidTexture> getTextures() { /** Returns the first region found with the specified name. This method uses string comparison to find the region, so the
return textures; * result should be cached rather than calling this method multiple times. */
} public @Null AtlasRegion findRegion (String name) {
for (int i = 0, n = regions.size; i < n; i++)
if (regions.get(i).name.equals(name)) return regions.get(i);
return null;
}
public Array<AtlasRegion> getRegions() { public Array<AndroidTexture> getTextures () {
return regions; return textures;
} }
static public AndroidTextureAtlas loadFromAssets(String atlasFile, AssetManager assetManager) { public Array<AtlasRegion> getRegions () {
TextureAtlasData data = new TextureAtlasData(); return regions;
}
try { static public AndroidTextureAtlas loadFromAssets (String atlasFile, AssetManager assetManager) {
FileHandle inputFile = new FileHandle() { TextureAtlasData data = new TextureAtlasData();
@Override
public InputStream read() {
try {
return assetManager.open(atlasFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
data.load(inputFile, new FileHandle(atlasFile).parent(), false);
} catch (Throwable t) {
throw new RuntimeException(t);
}
return new AndroidTextureAtlas(data, new BitmapLoader() { try {
@Override FileHandle inputFile = new FileHandle() {
public Bitmap load(String path) { @Override
path = path.startsWith("/") ? path.substring(1) : path; public InputStream read () {
try (InputStream in = new BufferedInputStream(assetManager.open(path))) { try {
return BitmapFactory.decodeStream(in); return assetManager.open(atlasFile);
} catch (Throwable t) { } catch (IOException e) {
throw new RuntimeException(t); throw new RuntimeException(e);
} }
} }
}); };
} data.load(inputFile, new FileHandle(atlasFile).parent(), false);
} catch (Throwable t) {
throw new RuntimeException(t);
}
return new AndroidTextureAtlas(data, new BitmapLoader() {
@Override
public Bitmap load (String path) {
path = path.startsWith("/") ? path.substring(1) : path;
try (InputStream in = new BufferedInputStream(assetManager.open(path))) {
return BitmapFactory.decodeStream(in);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
});
}
} }

View File

@ -0,0 +1,191 @@
package com.esotericsoftware.spine.android;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.IntArray;
import com.badlogic.gdx.utils.Pool;
import com.badlogic.gdx.utils.ShortArray;
import com.esotericsoftware.spine.BlendMode;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.Slot;
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.utils.SkeletonClipping;
import android.graphics.Canvas;
public class SkeletonRenderer {
public static class RenderCommand implements Pool.Poolable {
FloatArray vertices = new FloatArray(32);
FloatArray uvs = new FloatArray(32);
IntArray colors = new IntArray(32);
ShortArray indices = new ShortArray(32);
BlendMode blendMode;
AndroidTexture texture;
@Override
public void reset () {
vertices.setSize(0);
uvs.setSize(0);
colors.setSize(0);
indices.setSize(0);
blendMode = null;
texture = null;
}
}
static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0};
private final SkeletonClipping clipper = new SkeletonClipping();
private final Pool<RenderCommand> commandPool = new Pool<RenderCommand>(10) {
@Override
protected RenderCommand newObject () {
return new RenderCommand();
}
};
private final Array<RenderCommand> commandList = new Array<RenderCommand>();
public Array<RenderCommand> render (Skeleton skeleton) {
Color color = null, skeletonColor = skeleton.getColor();
float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
commandPool.freeAll(commandList);
commandList.clear();
RenderCommand command = commandPool.obtain();
commandList.add(command);
int vertexStart = 0;
Object[] drawOrder = skeleton.getDrawOrder().items;
for (int i = 0, n = skeleton.getDrawOrder().size; i < n; i++) {
Slot slot = (Slot)drawOrder[i];
if (!slot.getBone().isActive()) {
clipper.clipEnd(slot);
continue;
}
int verticesLength = 0;
int vertexSize = 2;
float[] uvs = null;
short[] indices = null;
Attachment attachment = slot.getAttachment();
if (attachment == null) {
continue;
}
if (attachment instanceof RegionAttachment) {
RegionAttachment region = (RegionAttachment)attachment;
verticesLength = vertexSize << 2;
AndroidTexture texture = (AndroidTexture)region.getRegion().getTexture();
BlendMode blendMode = slot.getData().getBlendMode();
if (command.blendMode == null && command.texture == null) {
command.blendMode = blendMode;
command.texture = texture;
}
if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
command = commandPool.obtain();
commandList.add(command);
vertexStart = 0;
command.blendMode = blendMode;
command.texture = texture;
}
command.vertices.setSize(command.vertices.size + verticesLength);
region.computeWorldVertices(slot, command.vertices.items, vertexStart, vertexSize);
uvs = region.getUVs();
indices = quadTriangles;
color = region.getColor();
} else if (attachment instanceof MeshAttachment) {
MeshAttachment mesh = (MeshAttachment)attachment;
verticesLength = mesh.getWorldVerticesLength();
AndroidTexture texture = (AndroidTexture)mesh.getRegion().getTexture();
BlendMode blendMode = slot.getData().getBlendMode();
if (command.blendMode == null && command.texture == null) {
command.blendMode = blendMode;
command.texture = texture;
}
if (command.blendMode != blendMode || command.texture != texture || command.vertices.size + verticesLength > 64000) {
command = commandPool.obtain();
commandList.add(command);
vertexStart = 0;
command.blendMode = blendMode;
command.texture = texture;
}
command.vertices.setSize(command.vertices.size + verticesLength);
mesh.computeWorldVertices(slot, 0, verticesLength, command.vertices.items, vertexStart, vertexSize);
uvs = mesh.getUVs();
indices = mesh.getTriangles();
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment) {
ClippingAttachment clip = (ClippingAttachment)attachment;
clipper.clipStart(slot, clip);
continue;
} else {
continue;
}
Color slotColor = slot.getColor();
int c = (int)(a * slotColor.a * color.a * 255) << 24 //
| (int)(r * slotColor.r * color.r * 255) << 16 //
| (int)(g * slotColor.g * color.g * 255) << 8 //
| (int)(b * slotColor.b * color.b * 255);
if (clipper.isClipping()) {
// FIXME
throw new RuntimeException("Not implemented, need to split positions, uvs, colors");
// clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false);
// FloatArray clippedVertices = clipper.getClippedVertices();
// ShortArray clippedTriangles = clipper.getClippedTriangles();
// batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0,
// clippedTriangles.size);
} else {
command.uvs.addAll(uvs);
float[] uvsArray = command.uvs.items;
for (int ii = vertexStart, w = command.texture.getWidth(), h = command.texture.getHeight(),
nn = vertexStart + verticesLength; ii < nn; ii += 2) {
uvsArray[ii] = uvsArray[ii] * w;
uvsArray[ii + 1] = uvsArray[ii + 1] * h;
}
command.colors.setSize(command.colors.size + (verticesLength >> 1));
int[] colorsArray = command.colors.items;
for (int ii = vertexStart >> 1, nn = (vertexStart >> 1) + (verticesLength >> 1); ii < nn; ii++) {
colorsArray[ii] = c;
}
int indicesStart = command.indices.size;
command.indices.addAll(indices);
int firstIndex = vertexStart >> 1;
short[] indicesArray = command.indices.items;
for (int ii = indicesStart, nn = indicesStart + indices.length; ii < nn; ii++) {
indicesArray[ii] += firstIndex;
}
}
// FIXME wrt clipping
vertexStart += verticesLength;
clipper.clipEnd(slot);
}
clipper.clipEnd();
return commandList;
}
public void render (Canvas canvas, Skeleton skeleton, float x, float y) {
canvas.save();
canvas.translate(x, y);
Array<RenderCommand> commands = render(skeleton);
for (int i = 0; i < commands.size; i++) {
RenderCommand command = commands.get(i);
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, command.vertices.size, command.vertices.items, 0, command.uvs.items, 0,
command.colors.items, 0, command.indices.items, 0, command.indices.size, command.texture.getPaint(command.blendMode));
}
canvas.restore();
}
}

View File

@ -1,5 +1,19 @@
package com.esotericsoftware.spine.android; package com.esotericsoftware.spine.android;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.SkeletonBinary;
import com.esotericsoftware.spine.SkeletonData;
import android.content.Context; import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.graphics.Canvas; import android.graphics.Canvas;
@ -9,223 +23,99 @@ import android.util.AttributeSet;
import android.view.Choreographer; import android.view.Choreographer;
import android.view.View; import android.view.View;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.IntArray;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.BlendMode;
import com.esotericsoftware.spine.Skeleton;
import com.esotericsoftware.spine.SkeletonBinary;
import com.esotericsoftware.spine.SkeletonData;
import com.esotericsoftware.spine.Slot;
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.utils.SkeletonClipping;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
public class SpineView extends View implements Choreographer.FrameCallback { public class SpineView extends View implements Choreographer.FrameCallback {
private long lastTime = 0; private long lastTime = 0;
private long delta = 0; private float delta = 0;
private Paint textPaint; private Paint textPaint;
int instances = 100; int instances = 1;
Vector2[] coords = new Vector2[instances]; Vector2[] coords = new Vector2[instances];
AndroidTextureAtlas atlas; AndroidTextureAtlas atlas;
SkeletonData data; SkeletonData data;
Array<Skeleton> skeletons = new Array<>(); Array<Skeleton> skeletons = new Array<>();
Array<AnimationState> states = new Array<>();
SkeletonRenderer renderer = new SkeletonRenderer();
Array<AnimationState> states = new Array<>(); public SpineView (Context context) {
super(context);
init();
}
public SpineView(Context context) { public SpineView (Context context, AttributeSet attrs) {
super(context); super(context, attrs);
init(); init();
} }
public SpineView(Context context, AttributeSet attrs) { public SpineView (Context context, AttributeSet attrs, int defStyle) {
super(context, attrs); super(context, attrs, defStyle);
init(); init();
} }
public SpineView(Context context, AttributeSet attrs, int defStyle) { private void loadSkeleton () {
super(context, attrs, defStyle); String skel = "spineboy-pro.skel";
init(); String atlasFile = "spineboy.atlas";
}
private void loadSkeleton() { AssetManager assetManager = this.getContext().getAssets();
AssetManager assetManager = this.getContext().getAssets(); atlas = AndroidTextureAtlas.loadFromAssets(atlasFile, assetManager);
atlas = AndroidTextureAtlas.loadFromAssets("spineboy.atlas", assetManager); AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas);
AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas); SkeletonBinary binary = new SkeletonBinary(attachmentLoader);
SkeletonBinary binary = new SkeletonBinary(attachmentLoader); try (InputStream in = new BufferedInputStream(assetManager.open(skel))) {
try (InputStream in = new BufferedInputStream(assetManager.open("spineboy-pro.skel"))) { data = binary.readSkeletonData(in);
data = binary.readSkeletonData(in); } catch (IOException e) {
} catch (IOException e) { throw new RuntimeException(e);
throw new RuntimeException(e); }
} }
}
static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0}; private void init () {
private final FloatArray vertices = new FloatArray(32); textPaint = new Paint();
private final FloatArray texCoords = new FloatArray(32); textPaint.setColor(Color.WHITE); // Set the color of the paint
private final IntArray colors = new IntArray(32); textPaint.setTextSize(48);
private final SkeletonClipping clipper = new SkeletonClipping(); Choreographer.getInstance().postFrameCallback(this);
public void render (Canvas canvas, Skeleton skeleton, float x, float y) { loadSkeleton();
canvas.save();
canvas.translate(x, y);
canvas.scale(1, -1);
BlendMode blendMode = null;
int verticesLength = 0;
short[] triangles = null;
com.badlogic.gdx.graphics.Color color = null, skeletonColor = skeleton.getColor();
float r = skeletonColor.r, g = skeletonColor.g, b = skeletonColor.b, a = skeletonColor.a;
Object[] drawOrder = skeleton.getDrawOrder().items;
for (int i = 0, n = skeleton.getDrawOrder().size; i < n; i++) {
Slot slot = (Slot)drawOrder[i];
if (!slot.getBone().isActive()) {
clipper.clipEnd(slot);
continue;
}
AndroidTexture texture = null;
int vertexSize = 2;
Attachment attachment = slot.getAttachment();
if (attachment instanceof RegionAttachment) {
RegionAttachment region = (RegionAttachment)attachment;
verticesLength = vertexSize << 2;
region.computeWorldVertices(slot, vertices.items, 0, vertexSize);
triangles = quadTriangles;
texture = (AndroidTexture)region.getRegion().getTexture();
texCoords.clear();
texCoords.addAll(region.getUVs());
color = region.getColor();
} else if (attachment instanceof MeshAttachment) { for (int i = 0; i < instances; i++) {
MeshAttachment mesh = (MeshAttachment)attachment; Skeleton skeleton = new Skeleton(data);
int count = mesh.getWorldVerticesLength(); skeleton.setScaleY(-1);
verticesLength = (count >> 1) * vertexSize; skeleton.setToSetupPose();
this.vertices.setSize(verticesLength); skeletons.add(skeleton);
mesh.computeWorldVertices(slot, 0, count, vertices.items, 0, vertexSize);
triangles = mesh.getTriangles();
texture = (AndroidTexture)mesh.getRegion().getTexture();
texCoords.clear();;
texCoords.addAll(mesh.getUVs());
color = mesh.getColor();
} else if (attachment instanceof ClippingAttachment) { AnimationStateData stateData = new AnimationStateData(data);
ClippingAttachment clip = (ClippingAttachment)attachment; stateData.setDefaultMix(0.2f);
clipper.clipStart(slot, clip); AnimationState state = new AnimationState(stateData);
continue; state.setAnimation(0, "hoverboard", true);
states.add(state);
} else { if (i == 0) {
continue; coords[i] = new Vector2(500, 1000);
} } else {
coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(3000));
}
}
}
if (texture != null) { @Override
com.badlogic.gdx.graphics.Color slotColor = slot.getColor(); public void onDraw (Canvas canvas) {
float alpha = a * slotColor.a * color.a * 255; super.onDraw(canvas);
float multiplier = 255;
BlendMode slotBlendMode = slot.getData().getBlendMode(); for (int i = 0; i < instances; i++) {
if (slotBlendMode != blendMode) { AnimationState state = states.get(i);
if (slotBlendMode == BlendMode.additive) { Skeleton skeleton = skeletons.get(i);
slotBlendMode = BlendMode.normal; state.update(delta);
alpha = 0; state.apply(skeleton);
} skeleton.update(delta);
blendMode = slotBlendMode; skeleton.updateWorldTransform(Skeleton.Physics.update);
// FIXME renderer.render(canvas, skeleton, coords[i].x, coords[i].y);
// blendMode.apply(batch, pmaBlendModes); }
}
int c = (int)alpha << 24 // canvas.drawText(delta * 1000 + " ms", 100, 100, textPaint);
| (int)(b * slotColor.b * color.b * multiplier) << 16 // canvas.drawText(instances + " instances", 100, 150, textPaint);
| (int)(g * slotColor.g * color.g * multiplier) << 8 // }
| (int)(r * slotColor.r * color.r * multiplier);
if (clipper.isClipping()) { @Override
// FIXME public void doFrame (long frameTimeNanos) {
throw new RuntimeException("Not implemented, need to split positions, uvs, colors"); if (lastTime != 0) delta = (frameTimeNanos - lastTime) / 1e9f;
// clipper.clipTriangles(vertices, verticesLength, triangles, triangles.length, uvs, c, 0, false); lastTime = frameTimeNanos;
// FloatArray clippedVertices = clipper.getClippedVertices(); invalidate();
// ShortArray clippedTriangles = clipper.getClippedTriangles(); Choreographer.getInstance().postFrameCallback(this);
// batch.draw(texture, clippedVertices.items, 0, clippedVertices.size, clippedTriangles.items, 0, }
// clippedTriangles.size);
} else {
float[] uvsArray = texCoords.items;
for (int ii = 0, w = texture.getWidth(), h = texture.getHeight(); ii < verticesLength; ii += 2) {
uvsArray[ii] = uvsArray[ii] * w;
uvsArray[ii + 1] = uvsArray[ii + 1] * h;
}
colors.setSize(verticesLength >> 1);
int[] colorsArray = colors.items;
for (int ii = 0, nn = verticesLength >> 1; ii < nn; ii++) {
colorsArray[ii] = c;
}
canvas.drawVertices(Canvas.VertexMode.TRIANGLES, verticesLength, vertices.items, 0, uvsArray, 0, colorsArray, 0, triangles, 0, triangles.length, texture.getPaint());
}
}
clipper.clipEnd(slot);
}
clipper.clipEnd();
canvas.restore();
}
private void init() {
textPaint = new Paint();
textPaint.setColor(Color.WHITE); // Set the color of the paint
textPaint.setTextSize(48);
Choreographer.getInstance().postFrameCallback(this);
loadSkeleton();
for (int i = 0; i < instances; i++) {
Skeleton skeleton = new Skeleton(data);
skeleton.setToSetupPose();
skeletons.add(skeleton);
AnimationStateData stateData = new AnimationStateData(data);
stateData.setDefaultMix(0.2f);
AnimationState state = new AnimationState(stateData);
state.setAnimation(0, "walk", true);
states.add(state);
coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(2000));
}
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
float deltaF = delta / 1e9f;
for (int i = 0; i < instances; i++) {
AnimationState state = states.get(i);
Skeleton skeleton = skeletons.get(i);
state.update(deltaF);
state.apply(skeleton);
skeleton.update(deltaF);
skeleton.updateWorldTransform(Skeleton.Physics.update);
render(canvas, skeleton, coords[i].x, coords[i].y);
}
// canvas.drawVertices(Canvas.VertexMode.TRIANGLES, vertices.size, vertices.items, 0, uvs.items, 0, null, 0, indices.items, 0, 3 * 75, paint);
canvas.drawText(delta / 1e6 + " ms", 100, 100, textPaint);
canvas.drawText(instances + " instances", 100, 150, textPaint);
}
@Override
public void doFrame(long frameTimeNanos) {
if (lastTime != 0) delta = frameTimeNanos - lastTime;
lastTime = frameTimeNanos;
// Invalidate this view, causing onDraw to be called at the next animation frame
invalidate();
Choreographer.getInstance().postFrameCallback(this);
}
} }