From 4f32c5bd1bf0be178046ad7a99a3ac7a37eada50 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 26 Apr 2024 18:00:16 +0200 Subject: [PATCH] [spine-android] Clean-up, batching renderer, clipping TBD --- .../esotericsoftware/spine/MainActivity.kt | 11 - .../android/AndroidAtlasAttachmentLoader.java | 100 +++--- .../spine/android/AndroidTexture.java | 97 ++++-- .../spine/android/AndroidTextureAtlas.java | 158 ++++----- .../spine/android/SkeletonRenderer.java | 191 +++++++++++ .../spine/android/SpineView.java | 322 ++++++------------ 6 files changed, 486 insertions(+), 393 deletions(-) create mode 100644 spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java diff --git a/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt index 440b9bb2c..8f3f001a8 100644 --- a/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt +++ b/spine-android/app/src/main/java/com/esotericsoftware/spine/MainActivity.kt @@ -34,7 +34,6 @@ fun AppContent() { color = MaterialTheme.colorScheme.background ) { Box { - BackgroundImage() SpineViewComposable() } } @@ -52,13 +51,3 @@ fun SpineViewComposable(modifier: Modifier = Modifier.fillMaxSize()) { 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() - ) -} diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java index 44e29e2b7..1f9aa289b 100644 --- a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidAtlasAttachmentLoader.java @@ -29,11 +29,9 @@ 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.TextureRegion; import com.badlogic.gdx.utils.Null; - import com.esotericsoftware.spine.Skin; import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; @@ -50,61 +48,61 @@ * Spine Runtimes Guide. */ @SuppressWarnings("javadoc") public class AndroidAtlasAttachmentLoader implements AttachmentLoader { - private AndroidTextureAtlas atlas; + private AndroidTextureAtlas atlas; - public AndroidAtlasAttachmentLoader (AndroidTextureAtlas atlas) { - if (atlas == null) throw new IllegalArgumentException("atlas cannot be null."); - this.atlas = atlas; - } + public AndroidAtlasAttachmentLoader (AndroidTextureAtlas atlas) { + if (atlas == null) throw new IllegalArgumentException("atlas cannot be null."); + this.atlas = atlas; + } - private void loadSequence (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) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")"); - } - } + private void loadSequence (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) throw new RuntimeException("Region not found in atlas: " + path + " (sequence: " + name + ")"); + } + } - public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) { - RegionAttachment attachment = new RegionAttachment(name); - if (sequence != null) - loadSequence(name, path, sequence); - else { - AtlasRegion region = atlas.findRegion(path); - if (region == null) - throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")"); - attachment.setRegion(region); - } - return attachment; - } + public RegionAttachment newRegionAttachment (Skin skin, String name, String path, @Null Sequence sequence) { + RegionAttachment attachment = new RegionAttachment(name); + if (sequence != null) + loadSequence(name, path, sequence); + else { + AtlasRegion region = atlas.findRegion(path); + if (region == null) + throw new RuntimeException("Region not found in atlas: " + path + " (region attachment: " + name + ")"); + attachment.setRegion(region); + } + return attachment; + } - public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) { - MeshAttachment attachment = new MeshAttachment(name); - if (sequence != null) - loadSequence(name, path, sequence); - else { - AtlasRegion region = atlas.findRegion(path); - if (region == null) - throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); - attachment.setRegion(region); - } - return attachment; - } + public MeshAttachment newMeshAttachment (Skin skin, String name, String path, @Null Sequence sequence) { + MeshAttachment attachment = new MeshAttachment(name); + if (sequence != null) + loadSequence(name, path, sequence); + else { + AtlasRegion region = atlas.findRegion(path); + if (region == null) + throw new RuntimeException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); + attachment.setRegion(region); + } + return attachment; + } - public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) { - return new BoundingBoxAttachment(name); - } + public BoundingBoxAttachment newBoundingBoxAttachment (Skin skin, String name) { + return new BoundingBoxAttachment(name); + } - public ClippingAttachment newClippingAttachment (Skin skin, String name) { - return new ClippingAttachment(name); - } + public ClippingAttachment newClippingAttachment (Skin skin, String name) { + return new ClippingAttachment(name); + } - public PathAttachment newPathAttachment (Skin skin, String name) { - return new PathAttachment(name); - } + public PathAttachment newPathAttachment (Skin skin, String name) { + return new PathAttachment(name); + } - public PointAttachment newPointAttachment (Skin skin, String name) { - return new PointAttachment(name); - } + public PointAttachment newPointAttachment (Skin skin, String name) { + return new PointAttachment(name); + } } diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java index 42e3299a1..4bb472fe4 100644 --- a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTexture.java @@ -1,45 +1,70 @@ + 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.BitmapShader; import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Shader; -import com.badlogic.gdx.graphics.Texture; -import com.badlogic.gdx.graphics.TextureData; - public class AndroidTexture extends Texture { - private Bitmap bitmap; - private Paint paint; - - protected AndroidTexture(Bitmap bitmap) { - super(); - this.bitmap = bitmap; - this.paint = new Paint(); - BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); - paint.setShader(shader); - } - - public Bitmap getBitmap() { - return bitmap; - } - - public Paint getPaint() { - return paint; - } - - @Override - public int getWidth() { - return bitmap.getWidth(); - } - - @Override - public int getHeight() { - return bitmap.getHeight(); - } - - @Override - public void dispose() { - bitmap.recycle(); - } + private Bitmap bitmap; + private ObjectMap paints = new ObjectMap<>(); + + protected AndroidTexture (Bitmap bitmap) { + super(); + this.bitmap = bitmap; + for (BlendMode blendMode : BlendMode.values()) { + Paint paint = new Paint(); + BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + paint.setShader(shader); + + switch (blendMode) { + 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; + } + + paints.put(blendMode, paint); + } + } + + public Bitmap getBitmap () { + return bitmap; + } + + public Paint getPaint (BlendMode blendMode) { + return paints.get(blendMode); + } + + @Override + public int getWidth () { + return bitmap.getWidth(); + } + + @Override + public int getHeight () { + return bitmap.getHeight(); + } + + @Override + public void dispose () { + bitmap.recycle(); + } } diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java index 11f483b74..d82c3812b 100644 --- a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/AndroidTextureAtlas.java @@ -1,98 +1,98 @@ + package com.esotericsoftware.spine.android; -import android.content.res.AssetManager; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; +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; -import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData; 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 java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.Buffer; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; public class AndroidTextureAtlas { - private static interface BitmapLoader { - Bitmap load(String path); - } + private static interface BitmapLoader { + Bitmap load (String path); + } + + private Array textures = new Array<>(); + private Array regions = new Array<>(); - private Array textures = new Array<>(); - private Array 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); - } + 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()) { - AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, // - region.rotate ? region.height : region.width, // - region.rotate ? region.width : region.height); - 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); - } - } + for (TextureAtlasData.Region region : data.getRegions()) { + AtlasRegion atlasRegion = new AtlasRegion(region.page.texture, region.left, region.top, // + region.rotate ? region.height : region.width, // + region.rotate ? region.width : region.height); + 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 - * 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; - } + /** Returns the first region found with the specified name. This method uses string comparison to find the region, so the + * 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 getTextures() { - return textures; - } + public Array getTextures () { + return textures; + } - public Array getRegions() { - return regions; - } + public Array getRegions () { + return regions; + } - static public AndroidTextureAtlas loadFromAssets(String atlasFile, AssetManager assetManager) { - TextureAtlasData data = new TextureAtlasData(); + static public AndroidTextureAtlas loadFromAssets (String atlasFile, AssetManager assetManager) { + TextureAtlasData data = new TextureAtlasData(); - try { - FileHandle inputFile = new FileHandle() { - @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); - } + try { + FileHandle inputFile = new FileHandle() { + @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() { - @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); - } - } - }); - } + 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); + } + } + }); + } } diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java new file mode 100644 index 000000000..6bdcb7f27 --- /dev/null +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SkeletonRenderer.java @@ -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 commandPool = new Pool(10) { + @Override + protected RenderCommand newObject () { + return new RenderCommand(); + } + }; + private final Array commandList = new Array(); + + public Array 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 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(); + } +} diff --git a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java index 7a24f57e9..22b2f7487 100644 --- a/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java +++ b/spine-android/spine-android/src/main/java/com/esotericsoftware/spine/android/SpineView.java @@ -1,231 +1,121 @@ + package com.esotericsoftware.spine.android; -import android.content.Context; -import android.content.res.AssetManager; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.util.AttributeSet; -import android.view.Choreographer; -import android.view.View; +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.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; +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.Choreographer; +import android.view.View; public class SpineView extends View implements Choreographer.FrameCallback { - private long lastTime = 0; - private long delta = 0; - private Paint textPaint; - int instances = 100; - Vector2[] coords = new Vector2[instances]; - AndroidTextureAtlas atlas; - SkeletonData data; - Array skeletons = new Array<>(); - - Array states = new Array<>(); - - public SpineView(Context context) { - super(context); - init(); - } - - public SpineView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public SpineView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - private void loadSkeleton() { - AssetManager assetManager = this.getContext().getAssets(); - atlas = AndroidTextureAtlas.loadFromAssets("spineboy.atlas", assetManager); - AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas); - SkeletonBinary binary = new SkeletonBinary(attachmentLoader); - try (InputStream in = new BufferedInputStream(assetManager.open("spineboy-pro.skel"))) { - data = binary.readSkeletonData(in); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - static private final short[] quadTriangles = {0, 1, 2, 2, 3, 0}; - private final FloatArray vertices = new FloatArray(32); - private final FloatArray texCoords = new FloatArray(32); - private final IntArray colors = new IntArray(32); - private final SkeletonClipping clipper = new SkeletonClipping(); - - public void render (Canvas canvas, Skeleton skeleton, float x, float y) { - 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) { - MeshAttachment mesh = (MeshAttachment)attachment; - int count = mesh.getWorldVerticesLength(); - verticesLength = (count >> 1) * vertexSize; - this.vertices.setSize(verticesLength); - 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) { - ClippingAttachment clip = (ClippingAttachment)attachment; - clipper.clipStart(slot, clip); - continue; - - } else { - continue; - } - - if (texture != null) { - com.badlogic.gdx.graphics.Color slotColor = slot.getColor(); - float alpha = a * slotColor.a * color.a * 255; - float multiplier = 255; - - BlendMode slotBlendMode = slot.getData().getBlendMode(); - if (slotBlendMode != blendMode) { - if (slotBlendMode == BlendMode.additive) { - slotBlendMode = BlendMode.normal; - alpha = 0; - } - blendMode = slotBlendMode; - // FIXME - // blendMode.apply(batch, pmaBlendModes); - } - - int c = (int)alpha << 24 // - | (int)(b * slotColor.b * color.b * multiplier) << 16 // - | (int)(g * slotColor.g * color.g * multiplier) << 8 // - | (int)(r * slotColor.r * color.r * multiplier); - - 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 { - 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); - } + private long lastTime = 0; + private float delta = 0; + private Paint textPaint; + int instances = 1; + Vector2[] coords = new Vector2[instances]; + AndroidTextureAtlas atlas; + SkeletonData data; + Array skeletons = new Array<>(); + Array states = new Array<>(); + SkeletonRenderer renderer = new SkeletonRenderer(); + + public SpineView (Context context) { + super(context); + init(); + } + + public SpineView (Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SpineView (Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void loadSkeleton () { + String skel = "spineboy-pro.skel"; + String atlasFile = "spineboy.atlas"; + + AssetManager assetManager = this.getContext().getAssets(); + atlas = AndroidTextureAtlas.loadFromAssets(atlasFile, assetManager); + AndroidAtlasAttachmentLoader attachmentLoader = new AndroidAtlasAttachmentLoader(atlas); + SkeletonBinary binary = new SkeletonBinary(attachmentLoader); + try (InputStream in = new BufferedInputStream(assetManager.open(skel))) { + data = binary.readSkeletonData(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + 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.setScaleY(-1); + skeleton.setToSetupPose(); + skeletons.add(skeleton); + + AnimationStateData stateData = new AnimationStateData(data); + stateData.setDefaultMix(0.2f); + AnimationState state = new AnimationState(stateData); + state.setAnimation(0, "hoverboard", true); + states.add(state); + + if (i == 0) { + coords[i] = new Vector2(500, 1000); + } else { + coords[i] = new Vector2(MathUtils.random(1000), MathUtils.random(3000)); + } + } + } + + @Override + public void onDraw (Canvas canvas) { + super.onDraw(canvas); + + for (int i = 0; i < instances; i++) { + AnimationState state = states.get(i); + Skeleton skeleton = skeletons.get(i); + state.update(delta); + state.apply(skeleton); + skeleton.update(delta); + skeleton.updateWorldTransform(Skeleton.Physics.update); + renderer.render(canvas, skeleton, coords[i].x, coords[i].y); + } + + canvas.drawText(delta * 1000 + " ms", 100, 100, textPaint); + canvas.drawText(instances + " instances", 100, 150, textPaint); + } + + @Override + public void doFrame (long frameTimeNanos) { + if (lastTime != 0) delta = (frameTimeNanos - lastTime) / 1e9f; + lastTime = frameTimeNanos; + invalidate(); + Choreographer.getInstance().postFrameCallback(this); + } }