parent
01d37d110a
commit
5fcc8816f4
|
@ -3,6 +3,7 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
* **Added** switch data file exporter for Moho (formerly Anime Studio) and OpenToonz ([issue #69](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/69))
|
* **Added** switch data file exporter for Moho (formerly Anime Studio) and OpenToonz ([issue #69](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/69))
|
||||||
|
* **Added** support for Spine 3.8 beta ([issue #74](https://github.com/DanielSWolf/rhubarb-lip-sync/issues/74))
|
||||||
* **Improved** animation rule for OW sound: animating it as E-F rather than F.
|
* **Improved** animation rule for OW sound: animating it as E-F rather than F.
|
||||||
|
|
||||||
## Version 1.9.1
|
## Version 1.9.1
|
||||||
|
|
|
@ -22,8 +22,13 @@ class SpineJson(private val filePath: Path) {
|
||||||
throw EndUserException("Wrong file format. This is not a valid JSON file.")
|
throw EndUserException("Wrong file format. This is not a valid JSON file.")
|
||||||
}
|
}
|
||||||
skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.")
|
skeleton = json.obj("skeleton") ?: throw EndUserException("JSON file is corrupted.")
|
||||||
val skins = json.obj("skins") ?: throw EndUserException("JSON file doesn't contain skins.")
|
val skins = json["skins"] ?: throw EndUserException("JSON file doesn't contain skins.")
|
||||||
defaultSkin = skins.obj("default") ?: throw EndUserException("JSON file doesn't have a default skin.")
|
defaultSkin = when (skins) {
|
||||||
|
is JsonObject -> skins.obj("default")
|
||||||
|
is JsonArray<*> -> (skins as JsonArray<JsonObject>).find { it.string("name") == "default" }
|
||||||
|
else -> null
|
||||||
|
} ?: throw EndUserException("JSON file doesn't have a default skin.")
|
||||||
|
|
||||||
validateProperties()
|
validateProperties()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +96,9 @@ class SpineJson(private val filePath: Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSlotAttachmentNames(slotName: String): List<String> {
|
fun getSlotAttachmentNames(slotName: String): List<String> {
|
||||||
val attachments = defaultSkin.obj(slotName) ?: JsonObject()
|
val attachments = defaultSkin.obj(slotName)
|
||||||
|
?: defaultSkin.obj("attachments")?.obj(slotName)
|
||||||
|
?: JsonObject()
|
||||||
return attachments.map { it.key }
|
return attachments.map { it.key }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +149,11 @@ class SpineJson(private val filePath: Path) {
|
||||||
animationNames.add(animationName)
|
animationNames.add(animationName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return json.toJsonString(prettyPrint = true)
|
||||||
|
}
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
var string = json.toJsonString(prettyPrint = true)
|
Files.write(filePath, listOf(toString()), StandardCharsets.UTF_8)
|
||||||
Files.write(filePath, listOf(string), StandardCharsets.UTF_8)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"skeleton": { "hash": "rSEJPpMBeapC2jv56cUew+IkQd0", "spine": "3.8.42-beta", "x": -394.13, "y": -0.43, "width": 795, "height": 1249.62 },
|
||||||
|
"bones": [
|
||||||
|
{ "name": "root" },
|
||||||
|
{ "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 },
|
||||||
|
{ "name": "head", "parent": "torso", "length": 515.83, "x": 390 },
|
||||||
|
{ "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 }
|
||||||
|
],
|
||||||
|
"slots": [
|
||||||
|
{ "name": "legs", "bone": "legs", "attachment": "legs" },
|
||||||
|
{ "name": "torso", "bone": "torso", "attachment": "torso" },
|
||||||
|
{ "name": "head", "bone": "head", "attachment": "head" },
|
||||||
|
{ "name": "mouth", "bone": "head", "attachment": "mouth_c" }
|
||||||
|
],
|
||||||
|
"skins": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"attachments": {
|
||||||
|
"mouth": {
|
||||||
|
"mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 },
|
||||||
|
"mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 },
|
||||||
|
"mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 },
|
||||||
|
"mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 },
|
||||||
|
"mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 },
|
||||||
|
"mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 },
|
||||||
|
"mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 },
|
||||||
|
"mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 }
|
||||||
|
},
|
||||||
|
"head": {
|
||||||
|
"head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 }
|
||||||
|
},
|
||||||
|
"legs": {
|
||||||
|
"legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 }
|
||||||
|
},
|
||||||
|
"torso": {
|
||||||
|
"torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": {
|
||||||
|
"1-have-you-heard": { "audio": "1-have-you-heard.wav" },
|
||||||
|
"2-it's-a-tool": { "audio": "2-it's-a-tool.wav" },
|
||||||
|
"3-and-now-you-can": { "audio": "3-and-now-you-can.wav" }
|
||||||
|
},
|
||||||
|
"animations": {
|
||||||
|
"shake_head": {
|
||||||
|
"bones": {
|
||||||
|
"head": {
|
||||||
|
"rotate": [
|
||||||
|
{ "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.1667, "angle": 10.02, "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.5, "angle": -9.37, "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.8333, "angle": 10.39, "curve": 0.574, "c3": 0.666 },
|
||||||
|
{ "time": 1.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"walk": {
|
||||||
|
"bones": {
|
||||||
|
"torso": {
|
||||||
|
"translate": [
|
||||||
|
{ "curve": 0, "c2": 0.5, "c3": 0.75 },
|
||||||
|
{ "time": 0.1333, "y": 30, "curve": 0.25, "c4": 0.49 },
|
||||||
|
{ "time": 0.2667 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"skeleton": {
|
||||||
|
"hash": "sH1atSHvppLIr/A6E6H7PXWiU4s",
|
||||||
|
"spine": "3.8.42-beta",
|
||||||
|
"x": -394.13,
|
||||||
|
"y": -0.43,
|
||||||
|
"width": 795,
|
||||||
|
"height": 1249.62,
|
||||||
|
"images": "./images/",
|
||||||
|
"audio": "./audio/"
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{ "name": "root" },
|
||||||
|
{ "name": "torso", "parent": "root", "length": 394.49, "rotation": 90, "y": 100 },
|
||||||
|
{ "name": "head", "parent": "torso", "length": 515.83, "x": 390 },
|
||||||
|
{ "name": "legs", "parent": "torso", "length": 79.85, "rotation": 180, "x": -6 }
|
||||||
|
],
|
||||||
|
"slots": [
|
||||||
|
{ "name": "legs", "bone": "legs", "attachment": "legs" },
|
||||||
|
{ "name": "torso", "bone": "torso", "attachment": "torso" },
|
||||||
|
{ "name": "head", "bone": "head", "attachment": "head" },
|
||||||
|
{ "name": "mouth", "bone": "head", "attachment": "mouth_c" }
|
||||||
|
],
|
||||||
|
"skins": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"attachments": {
|
||||||
|
"mouth": {
|
||||||
|
"mouth_a": { "x": -53.21, "y": -2.6, "rotation": -90, "width": 118, "height": 27 },
|
||||||
|
"mouth_b": { "x": -38.68, "y": -0.88, "rotation": -90, "width": 170, "height": 59 },
|
||||||
|
"mouth_c": { "x": -45.57, "y": -2.21, "rotation": -90, "width": 145, "height": 71 },
|
||||||
|
"mouth_d": { "x": -50.58, "y": -16.55, "rotation": -90, "width": 122, "height": 91 },
|
||||||
|
"mouth_e": { "x": -47.51, "y": 1.69, "rotation": -90, "width": 105, "height": 73 },
|
||||||
|
"mouth_f": { "x": -42.7, "y": -1.9, "rotation": -90, "width": 55, "height": 54 },
|
||||||
|
"mouth_g": { "x": -42.77, "y": 2.56, "rotation": -90, "width": 141, "height": 37 },
|
||||||
|
"mouth_h": { "x": -44.53, "y": 1.07, "rotation": -90, "width": 141, "height": 71 }
|
||||||
|
},
|
||||||
|
"head": {
|
||||||
|
"head": { "x": 305.19, "y": -3.37, "rotation": -90, "width": 795, "height": 908 }
|
||||||
|
},
|
||||||
|
"legs": {
|
||||||
|
"legs": { "x": 20.93, "y": 9.45, "rotation": 90, "width": 602, "height": 147 }
|
||||||
|
},
|
||||||
|
"torso": {
|
||||||
|
"torso": { "x": 185.9, "y": 0.39, "rotation": -90, "width": 741, "height": 449 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"events": {
|
||||||
|
"1-have-you-heard": { "audio": "1-have-you-heard.wav" },
|
||||||
|
"2-it's-a-tool": { "audio": "2-it's-a-tool.wav" },
|
||||||
|
"3-and-now-you-can": { "audio": "3-and-now-you-can.wav" }
|
||||||
|
},
|
||||||
|
"animations": {
|
||||||
|
"shake_head": {
|
||||||
|
"bones": {
|
||||||
|
"head": {
|
||||||
|
"rotate": [
|
||||||
|
{ "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.1667, "angle": 10.02, "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.5, "angle": -9.37, "curve": 0.25, "c3": 0.75 },
|
||||||
|
{ "time": 0.8333, "angle": 10.39, "curve": 0.574, "c3": 0.666 },
|
||||||
|
{ "time": 1.5 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"walk": {
|
||||||
|
"bones": {
|
||||||
|
"torso": {
|
||||||
|
"translate": [
|
||||||
|
{ "curve": 0, "c2": 0.5, "c3": 0.75 },
|
||||||
|
{ "time": 0.1333, "y": 30, "curve": 0.25, "c4": 0.49 },
|
||||||
|
{ "time": 0.2667 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,4 +36,34 @@ class SpineJsonTest {
|
||||||
.hasMessage("JSON file is incomplete: Images path is missing. Make sure to check 'Nonessential data' when exporting.")
|
.hasMessage("JSON file is incomplete: Images path is missing. Make sure to check 'Nonessential data' when exporting.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
inner class `file format 3_8` {
|
||||||
|
@Test
|
||||||
|
fun `correctly reads valid file`() {
|
||||||
|
val path = Paths.get("src/test/data/jsonFiles/matt-3.8.json").toAbsolutePath()
|
||||||
|
val spine = SpineJson(path)
|
||||||
|
|
||||||
|
assertThat(spine.audioDirectoryPath)
|
||||||
|
.isEqualTo(Paths.get("src/test/data/jsonFiles/audio").toAbsolutePath())
|
||||||
|
assertThat(spine.frameRate).isEqualTo(30.0)
|
||||||
|
assertThat(spine.slots).containsExactly("legs", "torso", "head", "mouth")
|
||||||
|
assertThat(spine.guessMouthSlot()).isEqualTo("mouth")
|
||||||
|
assertThat(spine.audioEvents).containsExactly(
|
||||||
|
SpineJson.AudioEvent("1-have-you-heard", "1-have-you-heard.wav", null),
|
||||||
|
SpineJson.AudioEvent("2-it's-a-tool", "2-it's-a-tool.wav", null),
|
||||||
|
SpineJson.AudioEvent("3-and-now-you-can", "3-and-now-you-can.wav", null)
|
||||||
|
)
|
||||||
|
assertThat(spine.getSlotAttachmentNames("mouth")).isEqualTo(('a'..'h').map{ "mouth_$it" })
|
||||||
|
assertThat(spine.animationNames).containsExactly("shake_head", "walk")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `throws on file without nonessential data`() {
|
||||||
|
val path = Paths.get("src/test/data/jsonFiles/matt-3.8-essential.json").toAbsolutePath()
|
||||||
|
val throwable = catchThrowable { SpineJson(path) }
|
||||||
|
assertThat(throwable)
|
||||||
|
.hasMessage("JSON file is incomplete: Images path is missing. Make sure to check 'Nonessential data' when exporting.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue