diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/Scene.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/Scene.kt index e1ae342..6e23da0 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/Scene.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/Scene.kt @@ -5,9 +5,10 @@ import com.dwursteisen.minigdx.scene.api.armature.Armature import com.dwursteisen.minigdx.scene.api.camera.Camera import com.dwursteisen.minigdx.scene.api.camera.OrthographicCamera import com.dwursteisen.minigdx.scene.api.camera.PerspectiveCamera +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.light.PointLight import com.dwursteisen.minigdx.scene.api.material.Material -import com.dwursteisen.minigdx.scene.api.model.Boxe +import com.dwursteisen.minigdx.scene.api.model.Box import com.dwursteisen.minigdx.scene.api.model.Model import com.dwursteisen.minigdx.scene.api.relation.Node import kotlinx.serialization.Serializable @@ -20,21 +21,21 @@ import kotlinx.serialization.protobuf.ProtoId @Serializable data class Scene( @ProtoId(0) - val perspectiveCameras: Map = emptyMap(), + val perspectiveCameras: Map = emptyMap(), @ProtoId(1) - val orthographicCameras: Map = emptyMap(), + val orthographicCameras: Map = emptyMap(), @ProtoId(2) - val models: Map = emptyMap(), + val models: Map = emptyMap(), @ProtoId(3) - val materials: Map = emptyMap(), + val materials: Map = emptyMap(), @ProtoId(4) - val pointLights: Map = emptyMap(), + val pointLights: Map = emptyMap(), @ProtoId(5) - val armatures: Map = emptyMap(), + val armatures: Map = emptyMap(), @ProtoId(6) - val animations: Map> = emptyMap(), + val animations: Map> = emptyMap(), @ProtoId(7) - val boxes: Map = emptyMap(), + val boxes: Map = emptyMap(), @ProtoId(8) val children: List = emptyList() ) { diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Animation.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Animation.kt index f20ee07..1750e20 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Animation.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Animation.kt @@ -1,5 +1,6 @@ package com.dwursteisen.minigdx.scene.api.armature +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.common.Transformation import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoId @@ -15,9 +16,9 @@ class Frame( @Serializable class Animation( @ProtoId(0) - val id: Int, + val id: Id, @ProtoId(1) - val armatureId: Int, + val armatureId: Id, @ProtoId(2) val name: String, @ProtoId(3) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Armature.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Armature.kt index 7b3137e..6911fa2 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Armature.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/armature/Armature.kt @@ -1,5 +1,6 @@ package com.dwursteisen.minigdx.scene.api.armature +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.common.Transformation import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoId @@ -15,7 +16,7 @@ class Joint( @Serializable class Armature( @ProtoId(0) - val id: Int, + val id: Id, @ProtoId(1) val name: String, @ProtoId(2) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/camera/Camera.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/camera/Camera.kt index 40d4c43..dca14ad 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/camera/Camera.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/camera/Camera.kt @@ -1,38 +1,39 @@ package com.dwursteisen.minigdx.scene.api.camera +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.common.Transformation import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoId interface Camera { + val id: Id val name: String - val transformation: Transformation } @Serializable data class PerspectiveCamera( @ProtoId(0) - override val name: String, + override val id: Id, @ProtoId(1) - val far: Float, + override val name: String, @ProtoId(2) - val near: Float, + val far: Float, @ProtoId(3) - val fov: Float, + val near: Float, @ProtoId(4) - override val transformation: Transformation + val fov: Float ) : Camera @Serializable data class OrthographicCamera( @ProtoId(0) - override val name: String, + override val id: Id, @ProtoId(1) - val far: Float, + override val name: String, @ProtoId(2) - val near: Float, + val far: Float, @ProtoId(3) - val scale: Float, + val near: Float, @ProtoId(4) - override val transformation: Transformation + val scale: Float ) : Camera diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Id.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Id.kt index 5d0fd6a..e4a258a 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Id.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Id.kt @@ -1,3 +1,28 @@ package com.dwursteisen.minigdx.scene.api.common -typealias Id = Int +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoId +import kotlin.random.Random + +@Serializable +data class Id( + @ProtoId(0) + val value: String = generate() +) { + + companion object { + private fun generate(): String { + val randomValues = ByteArray(ID_SIZE) + Random.nextBytes(randomValues) + return randomValues.map { it.toInt() and 0x0F } + .joinToString("") { CONVERT[it] } + + } + + private const val ID_SIZE = 8 + + private val CONVERT = arrayOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F") + + val None = Id("NONE") + } +} diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Transformation.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Transformation.kt index 789e75c..353faf5 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Transformation.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/common/Transformation.kt @@ -5,6 +5,6 @@ import kotlinx.serialization.protobuf.ProtoId @Serializable class Transformation( - @ProtoId(1) + @ProtoId(0) val matrix: FloatArray ) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/light/PointLight.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/light/PointLight.kt index 40f9906..92eb613 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/light/PointLight.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/light/PointLight.kt @@ -1,18 +1,24 @@ package com.dwursteisen.minigdx.scene.api.light +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.model.Color -import com.dwursteisen.minigdx.scene.api.model.Position import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoId + +interface Light { + val id: Id + val name: String +} + @Serializable data class PointLight( @ProtoId(0) - val name: String, + override val id: Id, @ProtoId(1) - val position: Position, + override val name: String, @ProtoId(2) val color: Color, @ProtoId(3) val intensity: Int -) +) : Light diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/material/Material.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/material/Material.kt index 2e6557a..882486b 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/material/Material.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/material/Material.kt @@ -1,18 +1,21 @@ package com.dwursteisen.minigdx.scene.api.material +import com.dwursteisen.minigdx.scene.api.common.Id import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoId @Serializable class Material( @ProtoId(0) - val name: String, + val id: Id, @ProtoId(1) - val id: Int, + val name: String, @ProtoId(2) val width: Int, @ProtoId(3) val height: Int, @ProtoId(4) - val data: ByteArray + val data: ByteArray, + @ProtoId(5) + val hasAlpha: Boolean = false ) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/model/Model.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/model/Model.kt index b51fd9a..387667f 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/model/Model.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/model/Model.kt @@ -81,21 +81,21 @@ data class Vertex( @Serializable class Primitive( @ProtoId(0) - val vertices: List = emptyList(), + val id: Id = Id(), @ProtoId(1) - val verticesOrder: IntArray = intArrayOf(), + val vertices: List = emptyList(), @ProtoId(2) - val materialId: Int = -1 + val verticesOrder: IntArray = intArrayOf(), + @ProtoId(3) + val materialId: Id = Id.None ) @Serializable -data class Boxe( +data class Box( @ProtoId(0) - val id: Id = -1, + val id: Id, @ProtoId(1) - val name: String, - @ProtoId(2) - val transformation: Transformation + val name: String ) @Serializable @@ -107,17 +107,9 @@ data class Mesh( @Serializable data class Model( @ProtoId(0) - val id: Id = -1, + val id: Id, @ProtoId(1) val name: String, @ProtoId(2) - @Deprecated("Prefer check the transformation from the scene graph") - val transformation: Transformation, - @ProtoId(3) - val mesh: Mesh, - @ProtoId(4) - val armatureId: Int = -1, - @Deprecated("Prefer check the box from the scene graph") - @ProtoId(5) - val boxes: List = emptyList() + val mesh: Mesh ) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/Node.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/Node.kt index c1b3cfc..8bf6335 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/Node.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/Node.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.protobuf.ProtoId @Serializable data class Node( @ProtoId(0) - val reference: Id = -1, + val reference: Id, @ProtoId(1) val name: String, @ProtoId(2) diff --git a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/ObjectType.kt b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/ObjectType.kt index 905c0d8..099a722 100644 --- a/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/ObjectType.kt +++ b/gltf-api/src/commonMain/kotlin/com.dwursteisen.minigdx.scene.api/relation/ObjectType.kt @@ -1,8 +1,9 @@ package com.dwursteisen.minigdx.scene.api.relation enum class ObjectType { - MODEL, + ARMATURE, BOX, CAMERA, - LIGHT + LIGHT, + MODEL } diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParser.kt index 08a857d..48aa49e 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParser.kt @@ -1,20 +1,22 @@ package com.dwursteisen.gltf.parser.armature -import com.dwursteisen.minigdx.scene.api.common.Transformation import com.adrienben.tools.gltf.models.* import com.curiouscreature.kotlin.math.* +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.toFloatArray import com.dwursteisen.minigdx.scene.api.armature.Animation import com.dwursteisen.minigdx.scene.api.armature.Armature import com.dwursteisen.minigdx.scene.api.armature.Frame import com.dwursteisen.minigdx.scene.api.armature.Joint +import com.dwursteisen.minigdx.scene.api.common.Id +import com.dwursteisen.minigdx.scene.api.common.Transformation typealias KeyFrame = Pair typealias GltfIndex = Int -class ArmatureParser(private val gltf: GltfAsset) { +class ArmatureParser(private val gltf: GltfAsset, private val ids: Dictionary) { - private fun GltfSkin.toArmature(index: Int): Armature { + private fun GltfSkin.toArmature(): Armature { val matrices = inverseBindMatrices.toFloatArray() .toList() .chunked(16) @@ -29,23 +31,22 @@ class ArmatureParser(private val gltf: GltfAsset) { } return Armature( - id = index, + id = ids.get(this), name = name ?: "", joints = joints.toTypedArray() ) } - fun armatures(): Map { + fun armatures(): Map { val skins = gltf.skin ?: emptyList() - return skins.mapIndexed { index, skin -> skin.toArmature(index) } + return skins.map { skin -> skin.toArmature() } .map { it.id to it } .toMap() } - fun animations(): Map> { - return gltf.animations.mapIndexed { index, it -> - it.toAnimation(index) - }.flatten() + fun animations(): Map> { + return gltf.animations.map { it.toAnimation() } + .flatten() .groupBy { it.armatureId } } @@ -115,14 +116,8 @@ class ArmatureParser(private val gltf: GltfAsset) { } } - private class AnimationDescription( - val name: String, - val skinId: Int, - val channels: List - ) - - private fun GltfAnimation.toAnimation(animationIndex: Int): List { - return gltf.skin?.mapIndexed { index, skin -> + private fun GltfAnimation.toAnimation(): List { + return gltf.skin?.map { skin -> val channels = this.channels.filter { skin.joints.contains(it.target.node) } val transformations: Map> = channels.groupBy { it.target.node!!.index } .mapValues { it.value.toKeyframes() } @@ -135,8 +130,8 @@ class ArmatureParser(private val gltf: GltfAsset) { skin.toFrames(localTransforms) } Animation( - id = animationIndex, - armatureId = index, + id = ids.get(this), + armatureId = ids.get(skin), name = name ?: "", duration = animation.keys.max() ?: 0f, frames = animation.map { (time, globalTransformations) -> @@ -160,6 +155,7 @@ class ArmatureParser(private val gltf: GltfAsset) { globals[index] = global children?.forEach { it.traverse(global) } } + val filterIndex = joints.flatMap { it.children ?: emptyList() } .map { it.index } diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/camera/CameraParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/camera/CameraParser.kt index 1e18fe1..0ceffb6 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/camera/CameraParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/camera/CameraParser.kt @@ -1,23 +1,19 @@ package com.dwursteisen.gltf.parser.camera -import com.dwursteisen.minigdx.scene.api.common.Transformation import com.adrienben.tools.gltf.models.GltfAsset import com.adrienben.tools.gltf.models.GltfCamera import com.adrienben.tools.gltf.models.GltfCameraType -import com.curiouscreature.kotlin.math.Float3 -import com.curiouscreature.kotlin.math.Mat4 -import com.curiouscreature.kotlin.math.inverse -import com.curiouscreature.kotlin.math.rotation -import com.dwursteisen.gltf.parser.support.transformation +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.minigdx.scene.api.camera.Camera import com.dwursteisen.minigdx.scene.api.camera.OrthographicCamera import com.dwursteisen.minigdx.scene.api.camera.PerspectiveCamera +import com.dwursteisen.minigdx.scene.api.common.Id -class CameraParser(private val source: GltfAsset) { +class CameraParser(private val source: GltfAsset, private val ids: Dictionary) { private fun GltfAsset.convertToCameras( type: GltfCameraType, - factory: (name: String, camera: GltfCamera, transformation: Mat4) -> T + factory: (name: String, camera: GltfCamera) -> T ): List { val cameras = this.nodes.filter { node -> val children = node.children ?: emptyList() @@ -27,54 +23,41 @@ class CameraParser(private val source: GltfAsset) { return cameras .map { node -> val cam = node.children!!.first { it.camera != null } - // Default camera orientation in blender is rotated by 90 on x and y. - val transformation = node.transformation * - rotation( - Float3( - 1f, - 0f, - 0f - ), -90f - ) - factory(node.name ?: "", cam.camera!!, inverse(transformation)) + factory(node.name ?: "", cam.camera!!) } } - fun orthographicCameras(): Map { - val factory = { name: String, camera: GltfCamera, transformation: Mat4 -> + fun orthographicCameras(): Map { + val factory = { name: String, camera: GltfCamera -> OrthographicCamera( + id = ids.get(camera), name = name, far = camera.orthographic?.zFar ?: 0f, near = camera.orthographic?.zNear ?: 0f, - scale = camera.orthographic?.xMag ?: 0f, - transformation = Transformation( - transformation.asGLArray().toFloatArray() - ) + scale = camera.orthographic?.xMag ?: 0f ) } return source.convertToCameras( GltfCameraType.ORTHOGRAPHIC, factory - ).map { it.name to it } + ).map { it.id to it } .toMap() } - fun perspectiveCameras(): Map { - val factory = { name: String, camera: GltfCamera, transformation: Mat4 -> + fun perspectiveCameras(): Map { + val factory = { name: String, camera: GltfCamera -> PerspectiveCamera( + id = ids.get(camera), name = name, far = camera.perspective?.zFar ?: 0f, near = camera.perspective?.zNear ?: 0f, - fov = camera.perspective?.yFov?.let { it * 100f } ?: 90f, - transformation = Transformation( - transformation.asGLArray().toFloatArray() - ) + fov = camera.perspective?.yFov?.let { it * 100f } ?: 90f ) } return source.convertToCameras( GltfCameraType.PERSPECTIVE, factory - ).map { it.name to it } + ).map { it.id to it } .toMap() } } diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/ligts/LightParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/lights/LightParser.kt similarity index 72% rename from gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/ligts/LightParser.kt rename to gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/lights/LightParser.kt index bde78a4..ac3faa0 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/ligts/LightParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/lights/LightParser.kt @@ -1,13 +1,15 @@ -package com.dwursteisen.gltf.parser.ligts +package com.dwursteisen.gltf.parser.lights import com.adrienben.tools.gltf.models.GltfAsset import com.adrienben.tools.gltf.models.GltfNode import com.beust.klaxon.JsonObject +import com.dwursteisen.gltf.parser.support.Dictionary +import com.dwursteisen.minigdx.scene.api.common.Id +import com.dwursteisen.minigdx.scene.api.light.Light import com.dwursteisen.minigdx.scene.api.light.PointLight import com.dwursteisen.minigdx.scene.api.model.Color -import com.dwursteisen.minigdx.scene.api.model.Position -class LightParser(private val gltfAsset: GltfAsset) { +class LightParser(private val gltfAsset: GltfAsset, private val ids: Dictionary) { private fun toPointLight(node: GltfNode, obj: JsonObject): PointLight { val colors = obj.array("color")?.value ?: mutableListOf(0, 0, 0) @@ -15,6 +17,7 @@ class LightParser(private val gltfAsset: GltfAsset) { val name = obj.string("name") ?: "" return PointLight( + id = ids.get(node), color = Color( colors[0] / 255f, colors[1] / 255f, @@ -22,20 +25,15 @@ class LightParser(private val gltfAsset: GltfAsset) { 1f ), name = name, - intensity = intensity, - position = Position( - node.translation.x, - node.translation.y, - node.translation.z - ) + intensity = intensity ) } - fun pointLights(): Map { + fun pointLights(): Map { return extractLight("point", ::toPointLight) } - private fun extractLight(type: String, mapper: (GltfNode, JsonObject) -> T): Map { + private fun extractLight(type: String, mapper: (GltfNode, JsonObject) -> T): Map { val lightsExtension = gltfAsset.extensions["KHR_lights_punctual"] ?: return emptyMap() val lights = (lightsExtension as? JsonObject)?.array("lights") ?: return emptyMap() val lightsStructure = lights.mapChildrenObjectsOnly { it }.value @@ -50,7 +48,8 @@ class LightParser(private val gltfAsset: GltfAsset) { return gltfAsset.nodes .flatMap { p -> p.children?.map { p to it } ?: emptyList() } .filter { n.contains(it.second) } - .map { (it.first.name ?: "") to mapper(it.first, nodesWithLight.getValue(it.second)) } + .map { mapper(it.first, nodesWithLight.getValue(it.second)) } + .map { it.id to it } .toMap() } } diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/material/MaterialParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/material/MaterialParser.kt index c99b51f..3bc7d72 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/material/MaterialParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/material/MaterialParser.kt @@ -1,20 +1,24 @@ package com.dwursteisen.gltf.parser.material import com.adrienben.tools.gltf.models.GltfAsset +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.isEmissiveTexture +import com.dwursteisen.gltf.parser.support.isSupportedTexture +import com.dwursteisen.gltf.parser.support.source +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.material.Material import de.matthiasmann.twl.utils.PNGDecoder import java.io.ByteArrayInputStream import java.nio.ByteBuffer -class MaterialParser(private val gltfAsset: GltfAsset) { +class MaterialParser(private val gltfAsset: GltfAsset, private val ids: Dictionary) { - fun materials(): Map { + fun materials(): Map { return gltfAsset.materials // keep only materials using texture - .filter { m -> m.isEmissiveTexture() } + .filter { m -> m.isSupportedTexture() } .map { m -> - val buffer = m.emissiveTexture?.texture?.source?.bufferView!! + val buffer = m.source!!.bufferView!! val data = buffer.buffer.data.copyOfRange(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) val decoder = PNGDecoder(ByteArrayInputStream(data)) @@ -33,13 +37,14 @@ class MaterialParser(private val gltfAsset: GltfAsset) { Material( name = m.name ?: "", - id = m.index, + id = ids.get(m), data = result, width = decoder.width, - height = decoder.height + height = decoder.height, + hasAlpha = decoder.hasAlpha() ) }.map { - it.name to it + it.id to it }.toMap() } } diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/model/ModelParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/model/ModelParser.kt index 92b028f..9451b84 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/model/ModelParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/model/ModelParser.kt @@ -1,57 +1,41 @@ package com.dwursteisen.gltf.parser.model -import com.dwursteisen.minigdx.scene.api.common.Transformation import com.adrienben.tools.gltf.models.GltfAsset import com.adrienben.tools.gltf.models.GltfMesh import com.adrienben.tools.gltf.models.GltfNode import com.dwursteisen.gltf.parser.support.* +import com.dwursteisen.minigdx.scene.api.common.Id +import com.dwursteisen.minigdx.scene.api.common.Transformation import com.dwursteisen.minigdx.scene.api.model.* -class ModelParser(private val gltfAsset: GltfAsset) { +class ModelParser(private val gltfAsset: GltfAsset, private val ids: Dictionary) { - fun objects(): Map { + fun objects(): Map { val nodes = gltfAsset.nodes.filter { it.mesh != null } - return nodes.mapIndexed { index, it -> it.toObject().copy(id = index) } - .map { it.name to it } + return nodes.map { it.toObject() } + .map { it.id to it } .toMap() } - fun boxes(): Map { + fun boxes(): Map { return gltfAsset.nodes.filter { it.isBox } - .mapIndexed { index, it -> - Boxe( - id = index, - name = it.name ?: "", - transformation = Transformation(it.transformation.asGLArray().toFloatArray()) + .map { it -> + Box( + id = ids.get(it), + name = it.name ?: "" ) - }.map { it.name to it } + }.map { it.id to it } .toMap() } private fun GltfNode.toObject(): Model { - val armatureId = gltfAsset.skin?.indexOf(skin) ?: -1 return Model( + id = ids.get(this.mesh!!), name = name!!, - transformation = Transformation( - transformation.asGLArray().toFloatArray() - ), - mesh = this.mesh!!.toMesh(), - armatureId = armatureId, - boxes = this.children.toBoxes() + mesh = this.mesh!!.toMesh() ) } - private fun List?.toBoxes(): List { - val boxes = this?.filter { it.isBox } ?: emptyList() - - return boxes.map { - Boxe( - name = it.name ?: "", - transformation = Transformation(it.transformation.asGLArray().toFloatArray()) - ) - } - } - private fun GltfMesh.toMesh(): Mesh { val primitives = primitives.map { primitive -> val positions = primitive.attributes["POSITION"].toFloatArray() @@ -89,7 +73,7 @@ class ModelParser(private val gltfAsset: GltfAsset) { } val material = gltfAsset.materials.getOrNull(primitive.material.index) - val uvs = if (!material.isEmissiveTexture()) { + val uvs = if (!material.isSupportedTexture()) { emptyList() } else { primitive.attributes["TEXCOORD_0"].toFloatArray() @@ -111,10 +95,10 @@ class ModelParser(private val gltfAsset: GltfAsset) { ) } - val materialId = if (primitive.material.isEmissiveTexture()) { - primitive.material.index + val materialId = if (primitive.material.isSupportedTexture()) { + ids.get(primitive.material) } else { - -1 + Id.None } Primitive( vertices = vertices, diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/scene/SceneParser.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/scene/SceneParser.kt index 43683ed..e081c4d 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/scene/SceneParser.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/scene/SceneParser.kt @@ -2,11 +2,15 @@ package com.dwursteisen.gltf.parser.scene import com.adrienben.tools.gltf.models.GltfAsset import com.adrienben.tools.gltf.models.GltfNode +import com.curiouscreature.kotlin.math.Float3 +import com.curiouscreature.kotlin.math.inverse +import com.curiouscreature.kotlin.math.rotation import com.dwursteisen.gltf.parser.armature.ArmatureParser import com.dwursteisen.gltf.parser.camera.CameraParser -import com.dwursteisen.gltf.parser.ligts.LightParser +import com.dwursteisen.gltf.parser.lights.LightParser import com.dwursteisen.gltf.parser.material.MaterialParser import com.dwursteisen.gltf.parser.model.ModelParser +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.isBox import com.dwursteisen.gltf.parser.support.transformation import com.dwursteisen.minigdx.scene.api.Scene @@ -17,15 +21,17 @@ import com.dwursteisen.minigdx.scene.api.relation.ObjectType class SceneParser(private val gltfAsset: GltfAsset) { - private val cameras = CameraParser(gltfAsset) + private val ids: Dictionary = Dictionary() - private val models = ModelParser(gltfAsset) + private val cameras = CameraParser(gltfAsset, ids) - private val materials = MaterialParser(gltfAsset) + private val models = ModelParser(gltfAsset, ids) - private val lights = LightParser(gltfAsset) + private val materials = MaterialParser(gltfAsset, ids) - private val armatures = ArmatureParser(gltfAsset) + private val lights = LightParser(gltfAsset, ids) + + private val armatures = ArmatureParser(gltfAsset, ids) fun parse(): Scene { return Scene( @@ -37,50 +43,75 @@ class SceneParser(private val gltfAsset: GltfAsset) { armatures = armatures.armatures(), animations = armatures.animations(), boxes = models.boxes(), - children = gltfAsset.scene?.nodes?.flatMap { gltfNode -> gltfNode.toNode() } ?: emptyList() + children = gltfAsset.scene?.nodes?.flatMap { gltfNode -> gltfNode.toNode(ids) } ?: emptyList() ) } - private fun GltfNode.toNode(): List { + private fun GltfNode.toNode(ids: Dictionary): List { return when { // Model - mesh != null -> listOf(createModelNode(this)) + mesh != null -> listOf(createModelNode(ids, this)) // Camera - camera != null -> emptyList() + children?.any { it.camera != null } == true -> listOf(createCamera(ids, this)) // Light extensions?.containsKey("KHR_lights_punctual") == true -> emptyList() - isBox -> listOf(createBoxNode(this)) + // Armature + children?.any { it.skin != null} == true -> listOf(createArmature(ids, this)) + // Box + isBox -> listOf(createBoxNode(ids, this)) else -> emptyList() } } - private fun createBoxNode(node: GltfNode): Node { - val id: Id = gltfAsset.nodes.filter { it.isBox } - .mapIndexed { index, gltfNode -> index to gltfNode } - .first { it.second == node } - .first + private fun createArmature(ids: Dictionary, node: GltfNode): Node { + val skin = node.children!!.first { it.skin != null } + return Node( + reference = ids.get(skin.skin!!), + name = node.name ?: "", + type = ObjectType.ARMATURE, + transformation = Transformation(node.transformation.asGLArray().toFloatArray()), + children = node.children?.flatMap { gltfNode -> gltfNode.toNode(this.ids) } ?: emptyList() + ) + } + private fun createCamera(ids: Dictionary, node: GltfNode): Node { + val camera = node.children!!.first { it.camera != null } + val id: Id = ids.get(camera.camera!!) + val transformation = node.transformation * + rotation( + Float3( + 1f, + 0f, + 0f + ), -90f + ) + return Node( + reference = id, + name = node.name ?: "", + type = ObjectType.CAMERA, + transformation = Transformation(inverse(transformation).asGLArray().toFloatArray()), + children = node.children?.flatMap { gltfNode -> gltfNode.toNode(ids) } ?: emptyList() + ) + } + private fun createBoxNode(ids: Dictionary, node: GltfNode): Node { + val id: Id = ids.get(node) return Node( reference = id, name = node.name ?: "", type = ObjectType.BOX, transformation = Transformation(node.transformation.asGLArray().toFloatArray()), - children = node.children?.flatMap { gltfNode -> gltfNode.toNode() } ?: emptyList() + children = node.children?.flatMap { gltfNode -> gltfNode.toNode(ids) } ?: emptyList() ) } - private fun createModelNode(node: GltfNode): Node { - val id: Id = gltfAsset.nodes.filter { it.mesh != null } - .mapIndexed { index, gltfNode -> index to gltfNode } - .first { it.second.mesh == node.mesh } - .first - + private fun createModelNode(ids: Dictionary, node: GltfNode): Node { return Node( - reference = id, + reference = ids.get(node.mesh!!), name = node.name ?: "", type = ObjectType.MODEL, transformation = Transformation(node.transformation.asGLArray().toFloatArray()), - children = node.children?.flatMap { gltfNode -> gltfNode.toNode() } ?: emptyList() + children = node.children?.flatMap { gltfNode -> gltfNode.toNode(this.ids) } ?: emptyList() ) } } + diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/Dictionary.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/Dictionary.kt new file mode 100644 index 0000000..0ddf97e --- /dev/null +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/Dictionary.kt @@ -0,0 +1,12 @@ +package com.dwursteisen.gltf.parser.support + +import com.dwursteisen.minigdx.scene.api.common.Id + +class Dictionary { + + private val map: MutableMap = mutableMapOf() + + fun get(any: Any): Id { + return map.computeIfAbsent(any) { Id() } + } +} diff --git a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/GltfMaterialExts.kt b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/GltfMaterialExts.kt index aa7e80c..832cb52 100644 --- a/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/GltfMaterialExts.kt +++ b/gltf-parser/src/main/kotlin/com/dwursteisen/gltf/parser/support/GltfMaterialExts.kt @@ -1,7 +1,28 @@ package com.dwursteisen.gltf.parser.support +import com.adrienben.tools.gltf.models.GltfImage import com.adrienben.tools.gltf.models.GltfMaterial fun GltfMaterial?.isEmissiveTexture(): Boolean { return this != null && this.emissiveTexture?.texture?.source != null } + +fun GltfMaterial?.isBSDFTexture(): Boolean { + return this != null && this.pbrMetallicRoughness.baseColorTexture?.texture?.source != null +} + +fun GltfMaterial?.isSupportedTexture(): Boolean { + return this.isEmissiveTexture() || this.isBSDFTexture() +} + +val GltfMaterial?.source: GltfImage? + get() { + if (this == null) return null + return if(isEmissiveTexture()) { + this.emissiveTexture?.texture?.source + } else if(isBSDFTexture()) { + this.pbrMetallicRoughness.baseColorTexture?.texture?.source + } else { + null + } + } diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParserTest.kt index fb06f48..4254cc9 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/armature/ArmatureParserTest.kt @@ -1,6 +1,7 @@ package com.dwursteisen.gltf.parser.armature import com.curiouscreature.kotlin.math.Mat4 +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.gltf import com.dwursteisen.minigdx.scene.api.Scene import org.junit.jupiter.api.Assertions.assertTrue @@ -14,23 +15,25 @@ class ArmatureParserTest { private val cubeAnimated by gltf("/joints/cube_joints_animated.gltf") private val noJoint by gltf("/mesh/cube_translated.gltf") + private val ids = Dictionary() + @Test fun `parse | it parse armatures`() { - val parser = ArmatureParser(cube) + val parser = ArmatureParser(cube, ids) assertEquals(1, parser.armatures().size) assertTrue(parser.animations().isEmpty()) } @Test fun `parse | when there is no armature, it parse nothing`() { - val parser = ArmatureParser(noJoint) + val parser = ArmatureParser(noJoint, ids) assertTrue(parser.armatures().isEmpty()) assertTrue(parser.animations().isEmpty()) } @Test fun `parse | it parse animations`() { - val parser = ArmatureParser(cubeAnimated) + val parser = ArmatureParser(cubeAnimated, ids) val animations = parser.animations() assertEquals(1, parser.armatures().size) assertEquals(1, animations.size) @@ -41,7 +44,7 @@ class ArmatureParserTest { @Test fun `animations | it returns computed animations`() { - val parser = ArmatureParser(cubeAnimated) + val parser = ArmatureParser(cubeAnimated, ids) val scene = Scene( armatures = parser.armatures(), animations = parser.animations() @@ -52,7 +55,7 @@ class ArmatureParserTest { @Test fun `animations | it returns computed correct animations`() { - val parser = ArmatureParser(simpleAnimation) + val parser = ArmatureParser(simpleAnimation, ids) val scene = Scene( armatures = parser.armatures(), animations = parser.animations() diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/camera/CameraParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/camera/CameraParserTest.kt index a49dc35..1a609d2 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/camera/CameraParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/camera/CameraParserTest.kt @@ -1,6 +1,7 @@ package com.dwursteisen.gltf.parser.camera import com.curiouscreature.kotlin.math.Mat4 +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.assertMat4Equals import com.dwursteisen.gltf.parser.support.gltf import org.junit.jupiter.api.Assertions.assertEquals @@ -10,24 +11,24 @@ class CameraParserTest { private val gltf by gltf("/camera/camera_default.gltf") + private val ids = Dictionary() + @Test fun `parse | it parses orthographic camera`() { - val cameras = CameraParser(gltf).orthographicCameras() + val cameras = CameraParser(gltf, ids).orthographicCameras() assertEquals(1, cameras.size) val camera = cameras.values.first() assertEquals("Orthographic", camera.name) - assertMat4Equals(Mat4.identity(), Mat4.fromColumnMajor(*camera.transformation.matrix)) } @Test fun `parse | it parses perspective camera`() { - val cameras = CameraParser(gltf).perspectiveCameras() + val cameras = CameraParser(gltf, ids).perspectiveCameras() assertEquals(1, cameras.size) val camera = cameras.values.first() assertEquals("Perspective", camera.name) - assertMat4Equals(Mat4.identity(), Mat4.fromColumnMajor(*camera.transformation.matrix)) assertEquals(100f, camera.far) assertEquals(0.1f, camera.near) diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/light/LightParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/light/LightParserTest.kt index 48814b9..a69b249 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/light/LightParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/light/LightParserTest.kt @@ -1,6 +1,7 @@ package com.dwursteisen.gltf.parser.light -import com.dwursteisen.gltf.parser.ligts.LightParser +import com.dwursteisen.gltf.parser.lights.LightParser +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.gltf import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -9,14 +10,11 @@ class LightParserTest { private val lights by gltf("/lights/lights.gltf") + private val ids = Dictionary() + @Test fun `pointLights | it returns point lights`() { - val lights = LightParser(lights).pointLights() + val lights = LightParser(lights, ids).pointLights() assertEquals(2, lights.values.size) - - val (_, point) = lights.values.toList() - assertEquals(1f, point.position.x) - assertEquals(3f, point.position.y) - assertEquals(-2f, point.position.z) } } diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/material/MaterialParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/material/MaterialParserTest.kt index cc06c35..0121a22 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/material/MaterialParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/material/MaterialParserTest.kt @@ -1,5 +1,6 @@ package com.dwursteisen.gltf.parser.material +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.gltf import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -10,17 +11,38 @@ class MaterialParserTest { private val cube by gltf("/mesh/cube_translated.gltf") + private val alpha by gltf("/uv/alpha.gltf") + + private val bsdfTexture by gltf("/uv/bsdf_texture.gltf") + + private val ids = Dictionary() + @Test fun `parse | it returns materials`() { - val result = MaterialParser(uv).materials() + val result = MaterialParser(uv, ids).materials() assertEquals(2, result.size) assertEquals(4, result.values.first().width) assertEquals(1, result.values.first().height) + assertEquals(false, result.values.first().hasAlpha) } @Test fun `parse | it returns no material`() { - val result = MaterialParser(cube).materials() + val result = MaterialParser(cube, ids).materials() assertEquals(0, result.size) } + + @Test + fun `parse | it parses material with alpha`() { + val result = MaterialParser(alpha, ids).materials() + assertEquals(1, result.size) + assertEquals(true, result.values.first().hasAlpha) + } + + @Test + fun `parse | it parses bsdf texture`() { + val result = MaterialParser(bsdfTexture, ids).materials() + assertEquals(1, result.size) + assertEquals(false, result.values.first().hasAlpha) + } } diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/mesh/ModelParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/mesh/ModelParserTest.kt index f8325d0..dfa4a47 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/mesh/ModelParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/mesh/ModelParserTest.kt @@ -1,12 +1,10 @@ package com.dwursteisen.gltf.parser.mesh -import com.curiouscreature.kotlin.math.Float3 -import com.curiouscreature.kotlin.math.Mat4 -import com.curiouscreature.kotlin.math.translation import com.dwursteisen.gltf.parser.model.ModelParser -import com.dwursteisen.gltf.parser.support.assertMat4Equals +import com.dwursteisen.gltf.parser.support.Dictionary import com.dwursteisen.gltf.parser.support.assertPositionEquals import com.dwursteisen.gltf.parser.support.gltf +import com.dwursteisen.minigdx.scene.api.common.Id import com.dwursteisen.minigdx.scene.api.model.Position import com.dwursteisen.minigdx.scene.api.model.UV import org.junit.jupiter.api.Assertions.* @@ -22,23 +20,20 @@ class ModelParserTest { private val cubeWithBoxes by gltf("/empty/cube_with_empty.gltf") + private val ids = Dictionary() + @Test fun `parse | it parses a translated cube`() { - val objects = ModelParser(cube).objects() + val objects = ModelParser(cube, ids).objects() assertEquals(1, objects.size) - - val cube = objects.getValue("Cube") - val transformation = translation(Float3(1f, 3f, -2f)) - - assertMat4Equals(transformation, Mat4.fromColumnMajor(*cube.transformation.matrix)) } @Test fun `parse | it parses primitives of a cube`() { - val objects = ModelParser(cube).objects() + val objects = ModelParser(cube, ids).objects() - val cube = objects.getValue("Cube").mesh + val cube = objects.values.first { it.name == "Cube" }.mesh assertEquals(1, cube.primitives.size) // TODO: should try to be close to 8 instead assertEquals(24, cube.primitives.first().vertices.size) @@ -46,9 +41,9 @@ class ModelParserTest { @Test fun `parse | it parses a plane with correct coordinates`() { - val objects = ModelParser(plane).objects() + val objects = ModelParser(plane, ids).objects() - val cube = objects.getValue("Plane").mesh + val cube = objects.values.first { it.name == "Plane" }.mesh assertEquals(1, cube.primitives.size) assertEquals(4, cube.primitives.first().vertices.size) @@ -62,7 +57,7 @@ class ModelParserTest { @Test fun `parse | it parses a mesh with no material`() { - val objects = ModelParser(cube).objects() + val objects = ModelParser(cube, ids).objects() val uvs = objects.flatMap { it.value.mesh.primitives } .flatMap { it.vertices } .map { it.uv } @@ -70,12 +65,12 @@ class ModelParserTest { assertTrue(uvs.contains(UV.INVALID)) assertEquals(1, uvs.size) - assertEquals(-1, objects.getValue("Cube").mesh.primitives.first().materialId) + assertEquals(Id.None, objects.values.first { it.name == "Cube" }.mesh.primitives.first().materialId) } @Test fun `parse | it parses a mesh with one material`() { - val objects = ModelParser(simpleUv).objects() + val objects = ModelParser(simpleUv, ids).objects() val uvs = objects.flatMap { it.value.mesh.primitives } .flatMap { it.vertices } .map { it.uv } @@ -86,7 +81,7 @@ class ModelParserTest { @Test fun `parse | it parses a mesh with more than one material`() { - val objects = ModelParser(multipleUv).objects() + val objects = ModelParser(multipleUv, ids).objects() val materials = objects.flatMap { it.value.mesh.primitives } .map { it.materialId } @@ -95,20 +90,11 @@ class ModelParserTest { @Test fun `parse | it parses a mesh with influence`() { - val objects = ModelParser(cubeWithJoints).objects() + val objects = ModelParser(cubeWithJoints, ids).objects() val influences = objects.flatMap { it.value.mesh.primitives } .flatMap { it.vertices } .flatMap { it.influences } assertTrue(influences.isNotEmpty()) - - assertEquals(0, objects.values.first().armatureId) - } - - @Test - fun `parse | it parses boxes`() { - val objects = ModelParser(cubeWithBoxes).objects() - val boxes = objects.values.first().boxes - assertEquals(4, boxes.size) } } diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/scene/SceneParserTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/scene/SceneParserTest.kt index 6d9cccb..9de4384 100644 --- a/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/scene/SceneParserTest.kt +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/gltf/parser/scene/SceneParserTest.kt @@ -27,6 +27,10 @@ class SceneParserTest { private val emptyWithCube by gltf("/scene/empty_parent_of_cube.gltf") + private val camera by gltf("/camera/camera_default.gltf") + + private val animation by gltf("/joints/cube_joints_animated.gltf") + @Test fun `parse | it parses all file tests`() { sources.flatMap { @@ -53,11 +57,8 @@ class SceneParserTest { val camera = scene.perspectiveCameras.values.first() val cube = scene.models.values.first() - val cameraTransformation = Mat4.fromColumnMajor(*camera.transformation.matrix) - assertMat4Equals(Mat4.identity(), cameraTransformation) - - val cubeTransformation = Mat4.fromColumnMajor(*cube.transformation.matrix) - assertMat4Equals(translation(Float3(0f, 0f, -5f)), cubeTransformation) + val cubeTransformation = scene.children.first { it.reference == cube.id }.transformation + assertMat4Equals(translation(Float3(0f, 0f, -5f)), Mat4.fromColumnMajor(*cubeTransformation.matrix)) } @Test @@ -70,7 +71,7 @@ class SceneParserTest { assertEquals(3, models.size) val modelReferences = models.map { model -> model.reference }.toSet() assertEquals(1, modelReferences.size) - assertTrue(modelReferences.contains(0)) + assertTrue(modelReferences.contains(scene.models.values.first().id)) } @Test @@ -94,4 +95,21 @@ class SceneParserTest { assertMat4Equals(translation(Float3(2f, 0f, 0f)), positionEmpty) assertMat4Equals(translation(Float3(4f, 0f, 0f)), positionCube) } + + @Test + fun `parse | it parses cameras`() { + val scene = SceneParser(camera).parse() + val (perspective, ortho) = scene.children.filter { it.type == ObjectType.CAMERA } + assertEquals("Perspective", perspective.name) + assertMat4Equals(Mat4.identity(), Mat4.fromColumnMajor(*perspective.transformation.matrix)) + assertEquals("Orthographic", ortho.name) + assertMat4Equals(Mat4.identity(), Mat4.fromColumnMajor(*ortho.transformation.matrix)) + } + + @Test + fun `parse | it parses armature`() { + val scene = SceneParser(animation).parse() + val armature = scene.children.first() { it.type == ObjectType.ARMATURE } + assertMat4Equals(translation(Float3(0f, 0f, 1f)), Mat4.fromColumnMajor(*armature.transformation.matrix)) + } } diff --git a/gltf-parser/src/test/kotlin/com/dwursteisen/minigdx/scene/api/common/IdTest.kt b/gltf-parser/src/test/kotlin/com/dwursteisen/minigdx/scene/api/common/IdTest.kt new file mode 100644 index 0000000..6a06ecd --- /dev/null +++ b/gltf-parser/src/test/kotlin/com/dwursteisen/minigdx/scene/api/common/IdTest.kt @@ -0,0 +1,14 @@ +package com.dwursteisen.minigdx.scene.api.common + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class IdTest { + + @Test + fun generate_new_random_id() { + val id = Id() + assertNotEquals(id.value, Id().value) + assertEquals(8, id.value.length) + } +} diff --git a/gltf-parser/src/test/resources/mesh/cube_animation.blend b/gltf-parser/src/test/resources/mesh/cube_animation.blend new file mode 100644 index 0000000..13e184e Binary files /dev/null and b/gltf-parser/src/test/resources/mesh/cube_animation.blend differ diff --git a/gltf-parser/src/test/resources/mesh/cube_animation.gltf b/gltf-parser/src/test/resources/mesh/cube_animation.gltf new file mode 100644 index 0000000..79c06f1 --- /dev/null +++ b/gltf-parser/src/test/resources/mesh/cube_animation.gltf @@ -0,0 +1,294 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.3.48", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0, + 1, + 3 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube", + "translation" : [ + 3, + 0, + 0 + ] + }, + { + "name" : "Light", + "rotation" : [ + 0.16907575726509094, + 0.7558803558349609, + -0.27217137813568115, + 0.570947527885437 + ], + "translation" : [ + 4.076245307922363, + 5.903861999511719, + -1.0054539442062378 + ] + }, + { + "camera" : 0, + "name" : "Camera_Orientation", + "rotation" : [ + -0.7071067690849304, + 0, + 0, + 0.7071067690849304 + ] + }, + { + "children" : [ + 2 + ], + "name" : "Camera", + "rotation" : [ + 0.483536034822464, + 0.33687159419059753, + -0.20870360732078552, + 0.7804827094078064 + ], + "translation" : [ + 7.358891487121582, + 4.958309173583984, + 6.925790786743164 + ] + } + ], + "cameras" : [ + { + "name" : "Camera", + "perspective" : { + "aspectRatio" : 1.7777777777777777, + "yfov" : 0.39959648408210363, + "zfar" : 100, + "znear" : 0.10000000149011612 + }, + "type" : "perspective" + } + ], + "animations" : [ + { + "channels" : [ + { + "sampler" : 0, + "target" : { + "node" : 0, + "path" : "translation" + } + }, + { + "sampler" : 1, + "target" : { + "node" : 0, + "path" : "rotation" + } + }, + { + "sampler" : 2, + "target" : { + "node" : 0, + "path" : "scale" + } + } + ], + "name" : "CubeAction", + "samplers" : [ + { + "input" : 4, + "interpolation" : "LINEAR", + "output" : 5 + }, + { + "input" : 6, + "interpolation" : "STEP", + "output" : 7 + }, + { + "input" : 6, + "interpolation" : "STEP", + "output" : 8 + } + ] + } + ], + "materials" : [ + { + "doubleSided" : true, + "emissiveFactor" : [ + 0, + 0, + 0 + ], + "name" : "Material", + "pbrMetallicRoughness" : { + "baseColorFactor" : [ + 0.800000011920929, + 0.800000011920929, + 0.800000011920929, + 1 + ], + "metallicFactor" : 0, + "roughnessFactor" : 0.4000000059604645 + } + } + ], + "meshes" : [ + { + "name" : "Cube", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3, + "material" : 0 + } + ] + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + }, + { + "bufferView" : 4, + "componentType" : 5126, + "count" : 20, + "max" : [ + 0.8333333333333334 + ], + "min" : [ + 0.041666666666666664 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 5, + "componentType" : 5126, + "count" : 20, + "type" : "VEC3" + }, + { + "bufferView" : 6, + "componentType" : 5126, + "count" : 5, + "max" : [ + 0.20833333333333334 + ], + "min" : [ + 0.041666666666666664 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 7, + "componentType" : 5126, + "count" : 5, + "type" : "VEC4" + }, + { + "bufferView" : 8, + "componentType" : 5126, + "count" : 5, + "type" : "VEC3" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + }, + { + "buffer" : 0, + "byteLength" : 80, + "byteOffset" : 840 + }, + { + "buffer" : 0, + "byteLength" : 240, + "byteOffset" : 920 + }, + { + "buffer" : 0, + "byteLength" : 20, + "byteOffset" : 1160 + }, + { + "buffer" : 0, + "byteLength" : 80, + "byteOffset" : 1180 + }, + { + "buffer" : 0, + "byteLength" : 60, + "byteOffset" : 1260 + } + ], + "buffers" : [ + { + "byteLength" : 1320, + "uri" : "data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAgPwAAAD8AAGA/AAAAPwAAYD8AAIA+AAAgPwAAgD4AAMA+AACAPgAAID8AAIA+AAAgPwAAAAAAAMA+AAAAAAAAwD4AAIA/AAAgPwAAgD8AACA/AABAPwAAwD4AAEA/AAAAPgAAAD8AAMA+AAAAPwAAwD4AAIA+AAAAPgAAgD4AAMA+AAAAPwAAID8AAAA/AAAgPwAAgD4AAMA+AACAPgAAwD4AAEA/AAAgPwAAQD8AACA/AAAAPwAAwD4AAAA/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAq6oqPauqqj0AAAA+q6oqPlVVVT4AAIA+VVWVPquqqj4AAMA+VVXVPquq6j4AAAA/q6oKP1VVFT8AACA/q6oqP1VVNT8AAEA/q6pKP1VVVT8AAEBAAAAAAAAAAIAAAEBAAAAAAAAAAIAAAEBAAAAAAAAAAIAAAEBAAAAAAAAAAIAAAAAAAAAAAAAAAIAAAAAAAAAAAJqZGb8AAAAAAAAAAJqZmb8AAAAAAAAAAGZm5r8AAAAAAAAAAJqZGcAAAAAAAAAAAAAAQMAAAAAAAAAAABxwQMAAAAAAAAAAAFRQQcAAAAAAAAAAAMQQQ8AAAAAAAAAAAKSRRsAAAAAAAAAAAGWTTcAAAAAAAAAAAOaWW8AAAAAAAAAAAOidd8AAAAAAAAAAAPbVl8AAAAAAAAAAAPjjz8AAAAAAAAAAAAAAIMGrqio9q6qqPQAAAD6rqio+VVVVPgAAAAAAAAAAAAAAgAAAgD8AAAAAAAAAAAAAAIAAAIA/AAAAAAAAAAAAAACAAACAPwAAAAAAAAAAAAAAgAAAgD8AAAAAAAAAAAAAAIAAAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgD8AAIA/" + } + ] +} diff --git a/gltf-parser/src/test/resources/uv/alpha.aseprite b/gltf-parser/src/test/resources/uv/alpha.aseprite new file mode 100644 index 0000000..788b769 Binary files /dev/null and b/gltf-parser/src/test/resources/uv/alpha.aseprite differ diff --git a/gltf-parser/src/test/resources/uv/alpha.blend b/gltf-parser/src/test/resources/uv/alpha.blend new file mode 100644 index 0000000..96f79a2 Binary files /dev/null and b/gltf-parser/src/test/resources/uv/alpha.blend differ diff --git a/gltf-parser/src/test/resources/uv/alpha.gltf b/gltf-parser/src/test/resources/uv/alpha.gltf new file mode 100644 index 0000000..987431d --- /dev/null +++ b/gltf-parser/src/test/resources/uv/alpha.gltf @@ -0,0 +1,171 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.3.48", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0, + 1, + 2 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube" + }, + { + "name" : "Light", + "rotation" : [ + 0.16907575726509094, + 0.7558803558349609, + -0.27217137813568115, + 0.570947527885437 + ], + "translation" : [ + 4.076245307922363, + 5.903861999511719, + -1.0054539442062378 + ] + }, + { + "name" : "Camera", + "rotation" : [ + 0.483536034822464, + 0.33687159419059753, + -0.20870360732078552, + 0.7804827094078064 + ], + "translation" : [ + 7.358891487121582, + 4.958309173583984, + 6.925790786743164 + ] + } + ], + "materials" : [ + { + "doubleSided" : true, + "emissiveFactor" : [ + 1, + 1, + 1 + ], + "emissiveTexture" : { + "index" : 0, + "texCoord" : 0 + }, + "name" : "Material", + "pbrMetallicRoughness" : {} + } + ], + "meshes" : [ + { + "name" : "Cube", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3, + "material" : 0 + } + ] + } + ], + "textures" : [ + { + "sampler" : 0, + "source" : 0 + } + ], + "images" : [ + { + "bufferView" : 4, + "mimeType" : "image/png", + "name" : "alpha" + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + }, + { + "buffer" : 0, + "byteLength" : 138, + "byteOffset" : 840 + } + ], + "samplers" : [ + { + "magFilter" : 9728, + "minFilter" : 9984 + } + ], + "buffers" : [ + { + "byteLength" : 980, + "uri" : "data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/WqzROG35fz9t+X8/c/l/P2/5fz8A4NE4VuzROACg0TharNE4ACDSOFfc0Thz+X8/cfl/P3D5fz9x+X8/AKDROHH5fz9z+X8/c/l/PwAg0jhXDNI4AKDROFys0Thx+X8/WqzROHH5fz9x+X8/c/l/P3H5fz8AoNE4WMzROACg0TharNE4ACDSOFfc0Thz+X8/cfl/P3D5fz9x+X8/AKDROHH5fz9z+X8/c/l/PwAg0jhXDNI4AKDROFys0Thx+X8/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAERJREFUOI1jYBgUQEnB5D8Mw/iE5GA0E0zhvQdnGHFZgE+OCZcEsWA4GYAc8ugAXQ6ZT3QsIMsjswdRGAw+A/DFClUBAAO9Ikx9WAjqAAAAAElFTkSuQmCCAAA=" + } + ] +} diff --git a/gltf-parser/src/test/resources/uv/alpha.png b/gltf-parser/src/test/resources/uv/alpha.png new file mode 100644 index 0000000..1c92731 Binary files /dev/null and b/gltf-parser/src/test/resources/uv/alpha.png differ diff --git a/gltf-parser/src/test/resources/uv/bsdf_texture.blend b/gltf-parser/src/test/resources/uv/bsdf_texture.blend new file mode 100644 index 0000000..83dbdaf Binary files /dev/null and b/gltf-parser/src/test/resources/uv/bsdf_texture.blend differ diff --git a/gltf-parser/src/test/resources/uv/bsdf_texture.gltf b/gltf-parser/src/test/resources/uv/bsdf_texture.gltf new file mode 100644 index 0000000..67a24ec --- /dev/null +++ b/gltf-parser/src/test/resources/uv/bsdf_texture.gltf @@ -0,0 +1,167 @@ +{ + "asset" : { + "generator" : "Khronos glTF Blender I/O v1.3.48", + "version" : "2.0" + }, + "scene" : 0, + "scenes" : [ + { + "name" : "Scene", + "nodes" : [ + 0, + 1, + 2 + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Cube" + }, + { + "name" : "Light", + "rotation" : [ + 0.16907575726509094, + 0.7558803558349609, + -0.27217137813568115, + 0.570947527885437 + ], + "translation" : [ + 4.076245307922363, + 5.903861999511719, + -1.0054539442062378 + ] + }, + { + "name" : "Camera", + "rotation" : [ + 0.483536034822464, + 0.33687159419059753, + -0.20870360732078552, + 0.7804827094078064 + ], + "translation" : [ + 7.358891487121582, + 4.958309173583984, + 6.925790786743164 + ] + } + ], + "materials" : [ + { + "doubleSided" : true, + "emissiveFactor" : [ + 0, + 0, + 0 + ], + "name" : "Material", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0, + "texCoord" : 0 + }, + "metallicFactor" : 0, + "roughnessFactor" : 0.4000000059604645 + } + } + ], + "meshes" : [ + { + "name" : "Cube", + "primitives" : [ + { + "attributes" : { + "POSITION" : 0, + "NORMAL" : 1, + "TEXCOORD_0" : 2 + }, + "indices" : 3, + "material" : 0 + } + ] + } + ], + "textures" : [ + { + "source" : 0 + } + ], + "images" : [ + { + "bufferView" : 4, + "mimeType" : "image/png", + "name" : "Untitled" + } + ], + "accessors" : [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 24, + "max" : [ + 1, + 1, + 1 + ], + "min" : [ + -1, + -1, + -1 + ], + "type" : "VEC3" + }, + { + "bufferView" : 1, + "componentType" : 5126, + "count" : 24, + "type" : "VEC3" + }, + { + "bufferView" : 2, + "componentType" : 5126, + "count" : 24, + "type" : "VEC2" + }, + { + "bufferView" : 3, + "componentType" : 5123, + "count" : 36, + "type" : "SCALAR" + } + ], + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 0 + }, + { + "buffer" : 0, + "byteLength" : 288, + "byteOffset" : 288 + }, + { + "buffer" : 0, + "byteLength" : 192, + "byteOffset" : 576 + }, + { + "buffer" : 0, + "byteLength" : 72, + "byteOffset" : 768 + }, + { + "buffer" : 0, + "byteLength" : 90, + "byteOffset" : 840 + } + ], + "buffers" : [ + { + "byteLength" : 932, + "uri" : "data:application/octet-stream;base64,AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAvwAAgL8AAIA/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAgD8AAACAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAgPwAAAD8AAGA/AAAAPwAAYD8AAIA+AAAgPwAAgD4AAMA+AACAPgAAID8AAIA+AAAgPwAAAAAAAMA+AAAAAAAAwD4AAIA/AAAgPwAAgD8AACA/AABAPwAAwD4AAEA/AAAAPgAAAD8AAMA+AAAAPwAAwD4AAIA+AAAAPgAAgD4AAMA+AAAAPwAAID8AAAA/AAAgPwAAgD4AAMA+AACAPgAAwD4AAEA/AAAgPwAAQD8AACA/AAAAPwAAwD4AAAA/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcAiVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADElEQVQIHWNgGB4AAADIAAE/ZR2JAAAAAElFTkSuQmCCAAA=" + } + ] +}