diff --git a/orx-jvm/orx-dnk3/build.gradle.kts b/orx-jvm/orx-dnk3/build.gradle.kts index d5d0d22a..ce1d6e49 100644 --- a/orx-jvm/orx-dnk3/build.gradle.kts +++ b/orx-jvm/orx-dnk3/build.gradle.kts @@ -1,9 +1,12 @@ plugins { id("org.openrndr.extra.convention.kotlin-jvm") + alias(libs.plugins.kotlin.serialization) + } dependencies { - implementation(libs.gson) + implementation(sharedLibs.kotlin.serialization.core) + implementation(sharedLibs.kotlin.serialization.json) implementation(project(":orx-fx")) implementation(project(":orx-jvm:orx-keyframer")) implementation(project(":orx-easing")) diff --git a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Glb.kt b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Glb.kt index 87301f4c..599ca7ef 100644 --- a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Glb.kt +++ b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Glb.kt @@ -1,6 +1,6 @@ package org.openrndr.extra.dnk3.gltf -import com.google.gson.Gson +import kotlinx.serialization.json.Json import java.io.File import java.io.RandomAccessFile import java.nio.ByteBuffer @@ -37,11 +37,11 @@ fun loadGltfFromGlbFile(file: File): GltfFile { val jsonByteArray = ByteArray(jsonBuffer.capacity()) jsonBuffer.get(jsonByteArray) val json = String(jsonByteArray) - val gson = Gson() val bufferBuffer = if (channel.position() < length) readChunk() else null - return gson.fromJson(json, GltfFile::class.java).apply { - this.file = file - this.bufferBuffer = bufferBuffer - } + val gltFile = Json { ignoreUnknownKeys = true }.decodeFromString(json) + gltFile.file = file + gltFile.bufferBuffer = bufferBuffer + + return gltFile } \ No newline at end of file diff --git a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Gltf.kt b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Gltf.kt index 61e464ff..bf1812ea 100644 --- a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Gltf.kt +++ b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/Gltf.kt @@ -2,7 +2,11 @@ package org.openrndr.extra.dnk3.gltf -import com.google.gson.Gson +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonBuilder +import kotlinx.serialization.json.JsonIgnoreUnknownKeys import org.openrndr.draw.* import java.io.File import java.io.RandomAccessFile @@ -24,33 +28,45 @@ const val GLTF_BYTE = 5120 const val GLTF_ARRAY_BUFFER = 34962 const val GLTF_ELEMENT_ARRAY_BUFFER = 34963 -data class GltfAsset(val generator: String?, val version: String?) +@Serializable +data class GltfAsset(val generator: String? = null, val version: String? = null) @JvmRecord -data class GltfScene(val nodes: IntArray) +@Serializable +data class GltfScene(val nodes: IntArray, val name: String? = null) @JvmRecord -data class GltfNode(val name: String?, - val children: IntArray?, - val matrix: DoubleArray?, - val scale: DoubleArray?, - val rotation: DoubleArray?, - val translation: DoubleArray?, - val mesh: Int?, - val skin: Int?, - val camera: Int?, - val extensions: GltfNodeExtensions?) +@Serializable +data class GltfNode( + val name: String? = null, + val children: IntArray? = null, + val matrix: DoubleArray? = null, + val scale: DoubleArray? = null, + val rotation: DoubleArray? = null, + val translation: DoubleArray? = null, + val mesh: Int? = null, + val skin: Int? = null, + val camera: Int? = null, + val extensions: GltfNodeExtensions? = null +) @JvmRecord +@Serializable data class KHRLightsPunctualIndex(val light: Int) @JvmRecord +@Serializable data class GltfNodeExtensions(val KHR_lights_punctual: KHRLightsPunctualIndex?) { } - -data class GltfPrimitive(val attributes: LinkedHashMap, val indices: Int?, val mode: Int?, val material: Int) { +@Serializable +data class GltfPrimitive( + val attributes: LinkedHashMap, + val indices: Int? = null, + val mode: Int? = null, + val material: Int? = null +) { fun createDrawCommand(gltfFile: GltfFile): GltfDrawCommand { val indexBuffer = indices?.let { indices -> @@ -65,8 +81,10 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices val contents = buffer.contents(gltfFile) (contents as Buffer).limit(contents.capacity()) (contents as Buffer).position((bufferView.byteOffset ?: 0) + (accessor.byteOffset)) - (contents as Buffer).limit((bufferView.byteOffset ?: 0) + (accessor.byteOffset) - + accessor.count * indexType.sizeInBytes) + (contents as Buffer).limit( + (bufferView.byteOffset ?: 0) + (accessor.byteOffset) + + accessor.count * indexType.sizeInBytes + ) val ib = indexBuffer(accessor.count, indexType) ib.write(contents) ib @@ -74,7 +92,46 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices var maxCount = 0 + abstract class Convertor { + abstract fun convert(buffer: ByteBuffer, offset: Int, size: Int, writer: BufferWriter) + } + + class CopyConvertor : Convertor() { + override fun convert(buffer: ByteBuffer, offset: Int, size: Int, writer: BufferWriter) { + writer.copyBuffer(buffer, offset, size) + } + } + + class Uint8ToUint32Convertor : Convertor() { + override fun convert(buffer: ByteBuffer, offset: Int, size: Int, writer: BufferWriter) { + for (i in 0 until 4) { + val ui = buffer.get(offset).toInt() + writer.write(ui) + } + } + } + + class Uint16ToUint32Convertor : Convertor() { + override fun convert(buffer: ByteBuffer, offset: Int, size: Int, writer: BufferWriter) { + for (i in 0 until 4) { + val ui = buffer.getShort(offset).toInt() + writer.write(ui) + } + } + } + + class CopyPadConvertor(val padFloats: Int) : Convertor() { + override fun convert(buffer: ByteBuffer, offset: Int, size: Int, writer: BufferWriter) { + writer.copyBuffer(buffer, offset, size) + for (i in 0 until padFloats) { + writer.write(0.0f) + } + } + } + + val accessors = mutableListOf() + val convertors = mutableListOf() val format = vertexFormat { for ((name, index) in attributes.toSortedMap()) { val accessor = gltfFile.accessors[index] @@ -82,16 +139,24 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices when (name) { "NORMAL" -> { normal(3) + paddingFloat(1) accessors.add(accessor) + convertors.add(CopyPadConvertor(1)) } + "POSITION" -> { position(3) + paddingFloat(1) accessors.add(accessor) + convertors.add(CopyPadConvertor(1)) } + "TANGENT" -> { attribute("tangent", VertexElementType.VECTOR4_FLOAT32) accessors.add(accessor) + convertors.add(CopyConvertor()) } + "TEXCOORD_0" -> { val dimensions = when (accessor.type) { "SCALAR" -> 1 @@ -99,18 +164,24 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices "VEC3" -> 3 else -> error("unsupported texture coordinate type ${accessor.type}") } - textureCoordinate(dimensions, 0) + textureCoordinate(4, 0) + //paddingFloat(4 - dimensions) accessors.add(accessor) + convertors.add(CopyPadConvertor(4 - dimensions)) } + "JOINTS_0" -> { - val type = when (Pair(accessor.type, accessor.componentType)) { - Pair("VEC4", GLTF_UNSIGNED_BYTE) -> VertexElementType.VECTOR4_UINT8 - Pair("VEC4", GLTF_UNSIGNED_SHORT) -> VertexElementType.VECTOR4_UINT16 - else -> error("not supported ${accessor.type} / ${accessor.componentType}") - } - attribute("joints", type) + attribute("joints", VertexElementType.VECTOR4_UINT32) accessors.add(accessor) + convertors.add( + when (Pair(accessor.type, accessor.componentType)) { + Pair("VEC4", GLTF_UNSIGNED_BYTE) -> Uint8ToUint32Convertor() + Pair("VEC4", GLTF_UNSIGNED_SHORT) -> Uint16ToUint32Convertor() + else -> error("not supported ${accessor.type} / ${accessor.componentType}") + } + ) } + "WEIGHTS_0" -> { val type = when (Pair(accessor.type, accessor.componentType)) { Pair("VEC4", GLTF_FLOAT) -> VertexElementType.VECTOR4_FLOAT32 @@ -118,23 +189,26 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices } attribute("weights", type) accessors.add(accessor) + convertors.add(CopyConvertor()) } } } } val buffers = - accessors.map { it.bufferView } - .distinct() - .associate { - Pair(gltfFile.bufferViews[it].buffer, - gltfFile.buffers[gltfFile.bufferViews[it].buffer].contents(gltfFile)) - } + accessors.map { it.bufferView } + .distinct() + .associate { + Pair( + gltfFile.bufferViews[it].buffer, + gltfFile.buffers[gltfFile.bufferViews[it].buffer].contents(gltfFile) + ) + } val vb = vertexBuffer(format, maxCount) vb.put { for (i in 0 until maxCount) { - for (a in accessors) { + for ((a, conv) in accessors zip convertors) { val bufferView = gltfFile.bufferViews[a.bufferView] val buffer = buffers[bufferView.buffer] ?: error("no buffer ${bufferView.buffer}") val componentSize = when (a.componentType) { @@ -155,7 +229,8 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices } val size = componentCount * componentSize val offset = (bufferView.byteOffset ?: 0) + a.byteOffset + i * (bufferView.byteStride ?: size) - copyBuffer(buffer, offset, size) + conv.convert(buffer, offset, size, this) + //copyBuffer(buffer, offset, size) } } } @@ -167,51 +242,66 @@ data class GltfPrimitive(val attributes: LinkedHashMap, val indices return GltfDrawCommand(vb, indexBuffer, drawPrimitive, indexBuffer?.indexCount ?: maxCount) } } - +@Serializable data class GltfMesh(val primitives: List, val name: String) { fun createDrawCommands(gltfFile: GltfFile): List { return primitives.map { it.createDrawCommand(gltfFile) } } } -data class GltfPbrMetallicRoughness(val baseColorFactor: DoubleArray?, - val baseColorTexture: GltfMaterialTexture?, - var metallicRoughnessTexture: GltfMaterialTexture?, - val roughnessFactor: Double?, - val metallicFactor: Double?) +@Serializable +data class GltfPbrMetallicRoughness( + val baseColorFactor: DoubleArray? = null, + val baseColorTexture: GltfMaterialTexture? = null, + var metallicRoughnessTexture: GltfMaterialTexture? = null, + val roughnessFactor: Double? = null, + val metallicFactor: Double? = null +) -data class GltfMaterialTexture(val index: Int, val scale: Double?, val texCoord: Int?) +@Serializable +data class GltfMaterialTexture(val index: Int, val scale: Double? = null, val texCoord: Int? = null) -data class GltfImage(val uri: String?, val bufferView: Int?) +@Serializable +data class GltfImage(val uri: String? = null, val bufferView: Int? = null) -data class GltfSampler(val magFilter: Int, val minFilter: Int, val wrapS: Int, val wrapT: Int) +@Serializable +data class GltfSampler(val magFilter: Int? = null, val minFilter: Int? = null, val wrapS: Int? = null, val wrapT: Int? = null) +@Serializable data class GltfTexture(val sampler: Int, val source: Int) -data class GltfMaterial(val name: String, - val alphaMode: String?, - val doubleSided: Boolean?, - val normalTexture: GltfMaterialTexture?, - val occlusionTexture: GltfMaterialTexture?, - val emissiveTexture: GltfMaterialTexture?, - val emissiveFactor: DoubleArray?, - val pbrMetallicRoughness: GltfPbrMetallicRoughness?, - val extensions: GltfMaterialExtensions? +@Serializable +data class GltfMaterial( + val name: String, + val alphaMode: String? = null, + val doubleSided: Boolean? = null, + val normalTexture: GltfMaterialTexture? = null, + val occlusionTexture: GltfMaterialTexture? = null, + val emissiveTexture: GltfMaterialTexture? = null, + val emissiveFactor: DoubleArray? = null, + val pbrMetallicRoughness: GltfPbrMetallicRoughness? = null, + val extensions: GltfMaterialExtensions? = null ) +@Serializable data class GltfMaterialExtensions( - val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness? + val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness? ) +@Serializable class KhrMaterialsPbrSpecularGlossiness(val diffuseFactor: DoubleArray?, val diffuseTexture: GltfMaterialTexture?) -data class GltfBufferView(val buffer: Int, - val byteOffset: Int?, - val byteLength: Int, - val byteStride: Int?, - val target: Int) +@Serializable +data class GltfBufferView( + val buffer: Int, + val byteOffset: Int? = null, + val byteLength: Int, + val byteStride: Int? = null, + val target: Int? = null +) -data class GltfBuffer(val byteLength: Int, val uri: String?) { +@Serializable +data class GltfBuffer(val byteLength: Int, val uri: String? = null) { fun contents(gltfFile: GltfFile): ByteBuffer = if (uri != null) { if (uri.startsWith("data:")) { val base64 = uri.substring(uri.indexOf(",") + 1) @@ -235,58 +325,93 @@ data class GltfBuffer(val byteLength: Int, val uri: String?) { } } -data class GltfDrawCommand(val vertexBuffer: VertexBuffer, val indexBuffer: IndexBuffer?, val primitive: DrawPrimitive, var vertexCount: Int) - -data class GltfAccessor( - val bufferView: Int, - val byteOffset: Int, - val componentType: Int, - val count: Int, - val max: DoubleArray, - val min: DoubleArray, - val type: String +data class GltfDrawCommand( + val vertexBuffer: VertexBuffer, + val indexBuffer: IndexBuffer?, + val primitive: DrawPrimitive, + var vertexCount: Int ) -data class GltfAnimation(val name: String?, val channels: List, val samplers: List) -data class GltfAnimationSampler(val input: Int, val interpolation: String, val output: Int) +@Serializable +data class GltfAccessor( + val bufferView: Int, + val byteOffset: Int = 0, + val componentType: Int, + val count: Int, + val max: DoubleArray? = null, + val min: DoubleArray? = null, + val type: String +) +@Serializable +data class GltfAnimation(val name: String? = null, val channels: List, val samplers: List) + +@Serializable +data class GltfAnimationSampler(val input: Int, val interpolation: String? = null, val output: Int) + +@Serializable data class GltfChannelTarget(val node: Int?, val path: String?) +@Serializable data class GltfChannel(val sampler: Int, val target: GltfChannelTarget) - +@Serializable data class GltfSkin(val inverseBindMatrices: Int, val joints: IntArray, val skeleton: Int) +@Serializable +data class KHRLightsPunctualLight( + val color: DoubleArray?, + val type: String, + val name: String, + val intensity: Double?, + val range: Double? = null, + val spot: KHRLightsPunctualLightSpot? = null +) -data class KHRLightsPunctualLight(val color: DoubleArray?, val type: String, val intensity: Double?, val range: Double, val spot: KHRLightsPunctualLightSpot?) +@Serializable data class KHRLightsPunctualLightSpot(val innerConeAngle: Double?, val outerConeAngle: Double?) +@Serializable data class KHRLightsPunctual(val lights: List) -data class GltfExtensions(val KHR_lights_punctual: KHRLightsPunctual?) +@Serializable +@JsonIgnoreUnknownKeys +data class GltfExtensions(val KHR_lights_punctual: KHRLightsPunctual? = null) -data class GltfCameraPerspective(val aspectRatio: Double?, val yfov: Double, val zfar: Double?, val znear: Double) +@Serializable +data class GltfCameraPerspective(val aspectRatio: Double? = null, val yfov: Double, val zfar: Double?, val znear: Double) + +@Serializable data class GltfCameraOrthographic(val xmag: Double, val ymag: Double, val zfar: Double, val znear: Double) -data class GltfCamera(val name: String?, val type: String, val perspective: GltfCameraPerspective?, val orthographic: GltfCameraOrthographic?) +@Serializable +data class GltfCamera( + val name: String? = null, + val type: String, + val perspective: GltfCameraPerspective? = null, + val orthographic: GltfCameraOrthographic? = null +) +@Serializable class GltfFile( - val asset: GltfAsset?, - val scene: Int?, - val scenes: List, - val nodes: List, - val meshes: List, - val accessors: List, - val materials: List, - val bufferViews: List, - val buffers: List, - val images: List?, - val textures: List?, - val samplers: List?, - val animations: List?, - val skins: List?, - val extensions: GltfExtensions?, - val cameras: List? + val asset: GltfAsset?, + val scene: Int? = null, + val scenes: List, + val nodes: List, + val meshes: List, + val accessors: List, + val materials: List, + val bufferViews: List, + val buffers: List, + val images: List? = null, + val textures: List? = null, + val samplers: List? = null, + val animations: List? = null, + val skins: List? = null, + val extensions: GltfExtensions? = null, + val extensionsUsed: List? = null, + val extensionsRequired: List? = null, + val cameras: List? = null ) { @Transient lateinit var file: File @@ -297,15 +422,17 @@ class GltfFile( fun loadGltfFromFile(file: File): GltfFile = when (file.extension) { "gltf" -> { - val gson = Gson() - val json = file.readText() - gson.fromJson(json, GltfFile::class.java).apply { - this.file = file - } + val gltfFile = Json{ + ignoreUnknownKeys = true + }.decodeFromString(file.readText()) + gltfFile.file = file + gltfFile } + "glb" -> { loadGltfFromGlbFile(file) } + else -> error("extension ${file.extension} not supported in ${file}") } diff --git a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt index 0cc27414..f16998dd 100644 --- a/orx-jvm/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt +++ b/orx-jvm/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt @@ -238,7 +238,7 @@ fun GltfFile.buildSceneNodes(): GltfSceneData { 0, drawCommand.vertexCount ) - val material = materials.getOrNull(material)?.createSceneMaterial() ?: PBRMaterial() + val material = materials.getOrNull(material ?: -1 )?.createSceneMaterial() ?: PBRMaterial() return MeshPrimitive(geometry, material) } @@ -278,16 +278,9 @@ fun GltfFile.buildSceneNodes(): GltfSceneData { ibmData.order(ByteOrder.nativeOrder()) (ibmData as Buffer).position(ibmAccessor.byteOffset + (ibmBufferView.byteOffset ?: 0)) - require(ibmAccessor.type == "MAT4") { - "Unsupported inverse bind matrix type: ${ibmAccessor.type}" - } - require(ibmAccessor.componentType == GLTF_FLOAT) { - "Unsupported inverse bind matrix component type: ${ibmAccessor.componentType}" - } - require(ibmAccessor.count == joints.size) { - "Mismatch between inverse bind matrix count (${ibmAccessor.count}) and joints size (${joints.size})" - - } + require(ibmAccessor.type == "MAT4") + require(ibmAccessor.componentType == GLTF_FLOAT) + require(ibmAccessor.count == joints.size) val ibms = (0 until ibmAccessor.count).map { val array = DoubleArray(16) for (i in 0 until 16) {