Add DressUp sample and drawable to bitmap rendering

This commit is contained in:
Denis Andrasec 2024-07-05 11:20:00 +02:00
parent 5ed26e6ef5
commit 63681d86af
8 changed files with 605 additions and 2 deletions

View File

@ -0,0 +1,359 @@
mix-and-match-pma.png
size: 1024, 512
filter: Linear, Linear
pma: true
scale: 0.5
base-head
bounds: 118, 70, 95, 73
boy/arm-front
bounds: 831, 311, 36, 115
rotate: 90
boy/backpack
bounds: 249, 357, 119, 153
boy/backpack-pocket
bounds: 628, 193, 34, 62
rotate: 90
boy/backpack-strap-front
bounds: 330, 263, 38, 88
rotate: 90
boy/backpack-up
bounds: 482, 171, 21, 70
boy/body
bounds: 845, 413, 97, 132
rotate: 90
boy/boot-ribbon-front
bounds: 234, 304, 9, 11
boy/collar
bounds: 471, 243, 73, 29
rotate: 90
boy/ear
bounds: 991, 352, 19, 23
rotate: 90
boy/eye-back-low-eyelid
bounds: 66, 72, 17, 6
boy/eye-back-pupil
bounds: 694, 279, 8, 9
rotate: 90
boy/eye-back-up-eyelid
bounds: 460, 101, 23, 5
rotate: 90
boy/eye-back-up-eyelid-back
bounds: 979, 414, 19, 10
rotate: 90
boy/eye-front-low-eyelid
bounds: 1015, 203, 22, 7
rotate: 90
boy/eye-front-pupil
bounds: 309, 50, 9, 9
boy/eye-front-up-eyelid
bounds: 991, 373, 31, 6
boy/eye-front-up-eyelid-back
bounds: 107, 76, 26, 9
rotate: 90
boy/eye-iris-back
bounds: 810, 260, 17, 17
boy/eye-iris-front
bounds: 902, 230, 18, 18
boy/eye-white-back
bounds: 599, 179, 20, 12
boy/eye-white-front
bounds: 544, 183, 27, 13
boy/eyebrow-back
bounds: 1002, 225, 20, 11
rotate: 90
boy/eyebrow-front
bounds: 975, 234, 25, 11
boy/hair-back
bounds: 629, 289, 122, 81
rotate: 90
boy/hair-bangs
bounds: 505, 180, 70, 37
rotate: 90
boy/hair-side
bounds: 979, 435, 25, 43
rotate: 90
boy/hand-backfingers
bounds: 858, 183, 19, 21
boy/hand-front-fingers
bounds: 879, 183, 19, 21
boy/hat
bounds: 218, 121, 93, 56
boy/leg-front
bounds: 85, 104, 31, 158
boy/mouth-close
bounds: 467, 100, 21, 5
girl-blue-cape/mouth-close
bounds: 467, 100, 21, 5
girl-spring-dress/mouth-close
bounds: 467, 100, 21, 5
girl/mouth-close
bounds: 467, 100, 21, 5
boy/mouth-smile
bounds: 1015, 258, 29, 7
rotate: 90
boy/nose
bounds: 323, 79, 17, 10
boy/pompom
bounds: 979, 462, 48, 43
rotate: 90
boy/zip
bounds: 922, 231, 14, 23
rotate: 90
girl-blue-cape/back-eyebrow
bounds: 527, 106, 18, 12
rotate: 90
girl-blue-cape/body-dress
bounds: 2, 264, 109, 246
girl-blue-cape/body-ribbon
bounds: 576, 193, 50, 38
girl-blue-cape/cape-back
bounds: 113, 317, 134, 193
girl-blue-cape/cape-back-up
bounds: 504, 305, 123, 106
girl-blue-cape/cape-ribbon
bounds: 396, 118, 50, 18
rotate: 90
girl-blue-cape/cape-shoulder-back
bounds: 420, 243, 49, 59
girl-blue-cape/cape-shoulder-front
bounds: 2, 2, 62, 76
girl-blue-cape/cape-up-front
bounds: 118, 145, 98, 117
girl-blue-cape/ear
bounds: 837, 181, 19, 23
girl-spring-dress/ear
bounds: 837, 181, 19, 23
girl/ear
bounds: 837, 181, 19, 23
girl-blue-cape/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl-spring-dress/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl/eye-back-low-eyelid
bounds: 810, 252, 17, 6
girl-blue-cape/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl-spring-dress/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl/eye-back-pupil
bounds: 309, 40, 8, 9
rotate: 90
girl-blue-cape/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl-spring-dress/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl/eye-back-up-eyelid
bounds: 573, 179, 24, 12
girl-blue-cape/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl-spring-dress/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl/eye-back-up-eyelid-back
bounds: 380, 105, 17, 11
rotate: 90
girl-blue-cape/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl-spring-dress/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl/eye-front-low-eyelid
bounds: 1016, 353, 18, 6
rotate: 90
girl-blue-cape/eye-front-pupil
bounds: 363, 94, 9, 9
girl-spring-dress/eye-front-pupil
bounds: 363, 94, 9, 9
girl/eye-front-pupil
bounds: 363, 94, 9, 9
girl-blue-cape/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl-spring-dress/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl/eye-front-up-eyelid
bounds: 679, 413, 30, 14
rotate: 90
girl-blue-cape/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl-spring-dress/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl/eye-front-up-eyelid-back
bounds: 947, 234, 26, 11
girl-blue-cape/eye-iris-back
bounds: 323, 105, 17, 17
girl-blue-cape/eye-iris-front
bounds: 467, 107, 18, 18
girl-blue-cape/eye-white-back
bounds: 621, 175, 20, 16
girl-spring-dress/eye-white-back
bounds: 621, 175, 20, 16
girl-blue-cape/eye-white-front
bounds: 643, 175, 20, 16
girl-spring-dress/eye-white-front
bounds: 643, 175, 20, 16
girl/eye-white-front
bounds: 643, 175, 20, 16
girl-blue-cape/front-eyebrow
bounds: 309, 101, 18, 12
rotate: 90
girl-blue-cape/hair-back
bounds: 712, 317, 117, 98
girl-blue-cape/hair-bangs
bounds: 313, 170, 91, 40
rotate: 90
girl-blue-cape/hair-head-side-back
bounds: 544, 198, 30, 52
girl-blue-cape/hair-head-side-front
bounds: 466, 127, 41, 42
girl-blue-cape/hair-side
bounds: 175, 2, 36, 71
rotate: 90
girl-blue-cape/hand-front-fingers
bounds: 902, 207, 19, 21
girl-spring-dress/hand-front-fingers
bounds: 902, 207, 19, 21
girl-blue-cape/leg-front
bounds: 519, 413, 30, 158
rotate: 90
girl-blue-cape/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl-spring-dress/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl/mouth-smile
bounds: 1015, 227, 29, 7
rotate: 90
girl-blue-cape/nose
bounds: 342, 82, 11, 7
girl-spring-dress/nose
bounds: 342, 82, 11, 7
girl/nose
bounds: 342, 82, 11, 7
girl-blue-cape/sleeve-back
bounds: 416, 95, 42, 29
girl-blue-cape/sleeve-front
bounds: 249, 303, 52, 119
rotate: 90
girl-spring-dress/arm-front
bounds: 829, 292, 17, 111
rotate: 90
girl-spring-dress/back-eyebrow
bounds: 309, 81, 18, 12
rotate: 90
girl-spring-dress/body-up
bounds: 66, 2, 64, 66
girl-spring-dress/cloak-down
bounds: 758, 227, 50, 50
girl-spring-dress/cloak-up
bounds: 628, 229, 64, 58
girl-spring-dress/eye-iris-back
bounds: 342, 105, 17, 17
girl-spring-dress/eye-iris-front
bounds: 487, 107, 18, 18
girl-spring-dress/front-eyebrow
bounds: 323, 91, 18, 12
girl-spring-dress/hair-back
bounds: 370, 417, 147, 93
girl-spring-dress/hair-bangs
bounds: 829, 250, 91, 40
girl-spring-dress/hair-head-side-back
bounds: 509, 126, 30, 52
girl-spring-dress/hair-head-side-front
bounds: 816, 206, 41, 42
girl-spring-dress/hair-side
bounds: 248, 2, 36, 71
rotate: 90
girl-spring-dress/leg-front
bounds: 831, 381, 30, 158
rotate: 90
girl-spring-dress/neck
bounds: 85, 70, 20, 32
girl-spring-dress/shoulder-ribbon
bounds: 175, 44, 36, 24
girl-spring-dress/skirt
bounds: 2, 80, 182, 81
rotate: 90
girl-spring-dress/underskirt
bounds: 519, 445, 175, 65
girl/arm-front
bounds: 712, 279, 36, 115
rotate: 90
girl/back-eyebrow
bounds: 309, 61, 18, 12
rotate: 90
girl/bag-base
bounds: 694, 219, 62, 58
girl/bag-strap-front
bounds: 370, 304, 12, 96
rotate: 90
girl/bag-top
bounds: 765, 175, 49, 50
girl/body
bounds: 370, 318, 97, 132
rotate: 90
girl/boot-ribbon-front
bounds: 323, 64, 13, 13
girl/eye-iris-back
bounds: 361, 105, 17, 17
girl/eye-iris-front
bounds: 507, 106, 18, 18
girl/eye-white-back
bounds: 665, 175, 20, 16
girl/front-eyebrow
bounds: 343, 91, 18, 12
girl/hair-back
bounds: 696, 417, 147, 93
girl/hair-bangs
bounds: 922, 247, 91, 40
girl/hair-flap-down-front
bounds: 415, 171, 70, 65
rotate: 90
girl/hair-head-side-back
bounds: 991, 381, 30, 52
girl/hair-head-side-front
bounds: 859, 206, 41, 42
girl/hair-patch
bounds: 132, 2, 66, 41
rotate: 90
girl/hair-side
bounds: 692, 181, 36, 71
rotate: 90
girl/hair-strand-back-1
bounds: 948, 289, 58, 74
rotate: 90
girl/hair-strand-back-2
bounds: 355, 170, 91, 58
rotate: 90
girl/hair-strand-back-3
bounds: 215, 40, 92, 79
girl/hair-strand-front-1
bounds: 234, 263, 38, 94
rotate: 90
girl/hair-strand-front-2
bounds: 576, 233, 70, 50
rotate: 90
girl/hair-strand-front-3
bounds: 313, 124, 44, 81
rotate: 90
girl/hand-front-fingers
bounds: 923, 208, 19, 21
girl/hat
bounds: 218, 179, 93, 82
girl/leg-front
bounds: 831, 349, 30, 158
rotate: 90
girl/pompom
bounds: 416, 126, 48, 43
girl/scarf
bounds: 113, 264, 119, 51
girl/scarf-back
bounds: 502, 252, 72, 51
girl/zip
bounds: 816, 179, 19, 25

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

View File

@ -0,0 +1,191 @@
package com.esotericsoftware.spine
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.esotericsoftware.spine.android.AndroidSkeletonDrawable
import com.esotericsoftware.spine.android.SkeletonRenderer
import com.esotericsoftware.spine.android.SpineController
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DressUp(nav: NavHostController) {
val context = LocalContext.current
val thumbnailSize = 150f
val drawable = remember {
AndroidSkeletonDrawable.fromAsset(
"mix-and-match-pma.atlas",
"mix-and-match-pro.skel",
context
)
}
val renderer = remember {
SkeletonRenderer()
}
val customSkin = remember {
mutableStateOf<Skin?>(null)
}
val skinImages = remember {
mutableStateMapOf<String, ImageBitmap>()
}
val selectedSkins = remember {
mutableStateMapOf<String, Boolean>()
}
val controller = remember {
SpineController.Builder()
.setOnInitialized {
it.animationState.setAnimation(0, "dance", true)
}
.build()
}
fun toggleSkin(skinName: String) {
selectedSkins[skinName] = !(selectedSkins[skinName] ?: false)
drawable.skeleton.setSkin("default")
customSkin.value = Skin("custom-skin");
for (skinName2 in selectedSkins.keys) {
if (selectedSkins[skinName2] == true) {
val skin = drawable.skeletonData.findSkin(skinName)
if (skin != null) customSkin.value?.addSkin(skin)
}
}
val customSkinValue = customSkin.value
if (customSkinValue != null) {
drawable.skeleton.setSkin(customSkinValue)
}
drawable.skeleton.setSlotsToSetupPose()
}
val localDensity = LocalDensity.current
LaunchedEffect(Unit) {
for (skin in drawable.skeletonData.getSkins()) {
if (skin.getName() == "default") continue
val skeleton = drawable.skeleton
skeleton.setSkin(skin)
skeleton.setToSetupPose()
skeleton.update(0f)
skeleton.updateWorldTransform(Skeleton.Physics.update)
skinImages[skin.getName()] = drawable.renderToBitmap(
renderer,
with(localDensity) { thumbnailSize.dp.toPx() },
with(localDensity) { thumbnailSize.dp.toPx() },
0xffffffff.toInt()
).asImageBitmap()
selectedSkins[skin.getName()] = false
}
toggleSkin("full-skins/girl");
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = Destination.DressUp.title) },
navigationIcon = {
IconButton({ nav.navigateUp() }) {
Icon(
Icons.Rounded.ArrowBack,
null,
)
}
}
)
}
) { paddingValues ->
Row(
modifier = Modifier
.width(thumbnailSize.dp)
.verticalScroll(rememberScrollState())
.padding(paddingValues)
) {
Column {
skinImages.keys.forEach { skinName ->
Box(modifier = Modifier
.clickable {
toggleSkin(skinName)
}
.then(
if (selectedSkins[skinName] == true) {
Modifier
} else {
Modifier.grayScale()
}
)
) {
Image(
painter = BitmapPainter(skinImages[skinName]!!),
contentDescription = null
)
}
}
}
// AndroidView(
// factory = { ctx ->
// SpineView(ctx).apply {
// loadFromDrawable(drawable, controller)
// }
// },
// modifier = Modifier.padding(paddingValues)
// )
}
}
}
fun Modifier.grayScale(): Modifier {
val saturationMatrix = ColorMatrix().apply { setToSaturation(0f) }
val saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
val paint = Paint().apply { colorFilter = saturationFilter }
return drawWithCache {
val canvasBounds = Rect(Offset.Zero, size)
onDrawWithContent {
drawIntoCanvas {
it.saveLayer(canvasBounds, paint)
drawContent()
it.restore()
}
}
}
}

View File

@ -62,6 +62,7 @@ fun AppContent() {
Destination.PlayPause,
Destination.AnimationStateEvents,
Destination.DebugRendering,
Destination.DressUp,
Destination.IKFollowing,
Destination.Physics
),
@ -94,6 +95,12 @@ fun AppContent() {
DebugRendering(navController)
}
composable(
Destination.DressUp.route
) {
DressUp(navController)
}
composable(
Destination.IKFollowing.route
) {
@ -143,6 +150,7 @@ sealed class Destination(val route: String, val title: String) {
data object PlayPause : Destination("playPause", "Play/Pause")
data object DebugRendering: Destination("debugRendering", "Debug Renderer")
data object AnimationStateEvents : Destination("animationStateEvents", "Animation State Listener")
data object DressUp : Destination("dressUp", "Dress Up")
data object IKFollowing : Destination("ikFollowing", "IK Following")
data object Physics: Destination("physics", "Physics (drag anywhere)")
}

View File

@ -1,7 +1,13 @@
package com.esotericsoftware.spine.android;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.FloatArray;
import com.esotericsoftware.spine.AnimationState;
import com.esotericsoftware.spine.AnimationStateData;
import com.esotericsoftware.spine.Skeleton;
@ -11,8 +17,6 @@ import com.esotericsoftware.spine.android.utils.SkeletonDataUtils;
import java.io.File;
import java.net.URL;
import kotlin.NotImplementedError;
public class AndroidSkeletonDrawable {
private final AndroidTextureAtlas atlas;
@ -81,4 +85,34 @@ public class AndroidSkeletonDrawable {
SkeletonData skeletonData = SkeletonDataUtils.fromHttp(atlas, skeletonUrl);
return new AndroidSkeletonDrawable(atlas, skeletonData);
}
public Bitmap renderToBitmap(SkeletonRenderer renderer, float width, float height, int bgColor) {
Vector2 offset = new Vector2(0, 0);
Vector2 size = new Vector2(0, 0);
FloatArray floatArray = new FloatArray();
getSkeleton().getBounds(offset, size, floatArray);
RectF bounds = new RectF(offset.x, offset.y, offset.x + size.x, offset.y + size.y);
float scale = (1 / (bounds.width() > bounds.height() ? bounds.width() / width : bounds.height() / height));
Bitmap bitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(bgColor);
paint.setStyle(Paint.Style.FILL);
// Draw background
canvas.drawRect(0, 0, width, height, paint);
// Transform canvas
canvas.translate(width / 2, height / 2);
canvas.scale(scale, -scale);
canvas.translate(-(bounds.left + bounds.width() / 2), -(bounds.top + bounds.height() / 2));
renderer.render(canvas, renderer.render(skeleton));
return bitmap;
}
}

View File

@ -208,6 +208,12 @@ public class SkeletonRenderer {
public void render (Canvas canvas, Array<RenderCommand> commands) {
for (int i = 0; i < commands.size; i++) {
RenderCommand command = commands.get(i);
// TODO Fix issue with dressup rendering
if (command.blendMode == null) {
continue;
}
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));
}

View File

@ -124,6 +124,11 @@ public class SpineView extends View implements Choreographer.FrameCallback {
loadFrom(() -> AndroidSkeletonDrawable.fromHttp(atlasUrl, skeletonUrl));
}
public void loadFromDrawable(AndroidSkeletonDrawable drawable, SpineController controller) {
this.controller = controller;
loadFrom(() -> drawable);
}
private void loadFrom(AndroidSkeletonDrawableLoader loader) {
Handler mainHandler = new Handler(Looper.getMainLooper());
Thread backgroundThread = new Thread(() -> {