[orx-dnk3] Switch from gson to kotlinx.serialization

This commit is contained in:
Edwin Jakobs
2025-11-08 19:56:07 +01:00
parent e21683640d
commit c0832197cd
4 changed files with 236 additions and 113 deletions

View File

@@ -1,9 +1,12 @@
plugins { plugins {
id("org.openrndr.extra.convention.kotlin-jvm") id("org.openrndr.extra.convention.kotlin-jvm")
alias(libs.plugins.kotlin.serialization)
} }
dependencies { dependencies {
implementation(libs.gson) implementation(sharedLibs.kotlin.serialization.core)
implementation(sharedLibs.kotlin.serialization.json)
implementation(project(":orx-fx")) implementation(project(":orx-fx"))
implementation(project(":orx-jvm:orx-keyframer")) implementation(project(":orx-jvm:orx-keyframer"))
implementation(project(":orx-easing")) implementation(project(":orx-easing"))

View File

@@ -1,6 +1,6 @@
package org.openrndr.extra.dnk3.gltf package org.openrndr.extra.dnk3.gltf
import com.google.gson.Gson import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -37,11 +37,11 @@ fun loadGltfFromGlbFile(file: File): GltfFile {
val jsonByteArray = ByteArray(jsonBuffer.capacity()) val jsonByteArray = ByteArray(jsonBuffer.capacity())
jsonBuffer.get(jsonByteArray) jsonBuffer.get(jsonByteArray)
val json = String(jsonByteArray) val json = String(jsonByteArray)
val gson = Gson()
val bufferBuffer = if (channel.position() < length) readChunk() else null val bufferBuffer = if (channel.position() < length) readChunk() else null
return gson.fromJson(json, GltfFile::class.java).apply { val gltFile = Json { ignoreUnknownKeys = true }.decodeFromString<GltfFile>(json)
this.file = file gltFile.file = file
this.bufferBuffer = bufferBuffer gltFile.bufferBuffer = bufferBuffer
}
return gltFile
} }

View File

@@ -2,7 +2,11 @@
package org.openrndr.extra.dnk3.gltf 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 org.openrndr.draw.*
import java.io.File import java.io.File
import java.io.RandomAccessFile import java.io.RandomAccessFile
@@ -24,33 +28,45 @@ const val GLTF_BYTE = 5120
const val GLTF_ARRAY_BUFFER = 34962 const val GLTF_ARRAY_BUFFER = 34962
const val GLTF_ELEMENT_ARRAY_BUFFER = 34963 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 @JvmRecord
data class GltfScene(val nodes: IntArray) @Serializable
data class GltfScene(val nodes: IntArray, val name: String? = null)
@JvmRecord @JvmRecord
data class GltfNode(val name: String?, @Serializable
val children: IntArray?, data class GltfNode(
val matrix: DoubleArray?, val name: String? = null,
val scale: DoubleArray?, val children: IntArray? = null,
val rotation: DoubleArray?, val matrix: DoubleArray? = null,
val translation: DoubleArray?, val scale: DoubleArray? = null,
val mesh: Int?, val rotation: DoubleArray? = null,
val skin: Int?, val translation: DoubleArray? = null,
val camera: Int?, val mesh: Int? = null,
val extensions: GltfNodeExtensions?) val skin: Int? = null,
val camera: Int? = null,
val extensions: GltfNodeExtensions? = null
)
@JvmRecord @JvmRecord
@Serializable
data class KHRLightsPunctualIndex(val light: Int) data class KHRLightsPunctualIndex(val light: Int)
@JvmRecord @JvmRecord
@Serializable
data class GltfNodeExtensions(val KHR_lights_punctual: KHRLightsPunctualIndex?) { data class GltfNodeExtensions(val KHR_lights_punctual: KHRLightsPunctualIndex?) {
} }
@Serializable
data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices: Int?, val mode: Int?, val material: Int) { data class GltfPrimitive(
val attributes: LinkedHashMap<String, Int>,
val indices: Int? = null,
val mode: Int? = null,
val material: Int? = null
) {
fun createDrawCommand(gltfFile: GltfFile): GltfDrawCommand { fun createDrawCommand(gltfFile: GltfFile): GltfDrawCommand {
val indexBuffer = indices?.let { indices -> val indexBuffer = indices?.let { indices ->
@@ -65,8 +81,10 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
val contents = buffer.contents(gltfFile) val contents = buffer.contents(gltfFile)
(contents as Buffer).limit(contents.capacity()) (contents as Buffer).limit(contents.capacity())
(contents as Buffer).position((bufferView.byteOffset ?: 0) + (accessor.byteOffset)) (contents as Buffer).position((bufferView.byteOffset ?: 0) + (accessor.byteOffset))
(contents as Buffer).limit((bufferView.byteOffset ?: 0) + (accessor.byteOffset) (contents as Buffer).limit(
+ accessor.count * indexType.sizeInBytes) (bufferView.byteOffset ?: 0) + (accessor.byteOffset)
+ accessor.count * indexType.sizeInBytes
)
val ib = indexBuffer(accessor.count, indexType) val ib = indexBuffer(accessor.count, indexType)
ib.write(contents) ib.write(contents)
ib ib
@@ -74,7 +92,46 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
var maxCount = 0 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<GltfAccessor>() val accessors = mutableListOf<GltfAccessor>()
val convertors = mutableListOf<Convertor>()
val format = vertexFormat { val format = vertexFormat {
for ((name, index) in attributes.toSortedMap()) { for ((name, index) in attributes.toSortedMap()) {
val accessor = gltfFile.accessors[index] val accessor = gltfFile.accessors[index]
@@ -82,16 +139,24 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
when (name) { when (name) {
"NORMAL" -> { "NORMAL" -> {
normal(3) normal(3)
paddingFloat(1)
accessors.add(accessor) accessors.add(accessor)
convertors.add(CopyPadConvertor(1))
} }
"POSITION" -> { "POSITION" -> {
position(3) position(3)
paddingFloat(1)
accessors.add(accessor) accessors.add(accessor)
convertors.add(CopyPadConvertor(1))
} }
"TANGENT" -> { "TANGENT" -> {
attribute("tangent", VertexElementType.VECTOR4_FLOAT32) attribute("tangent", VertexElementType.VECTOR4_FLOAT32)
accessors.add(accessor) accessors.add(accessor)
convertors.add(CopyConvertor())
} }
"TEXCOORD_0" -> { "TEXCOORD_0" -> {
val dimensions = when (accessor.type) { val dimensions = when (accessor.type) {
"SCALAR" -> 1 "SCALAR" -> 1
@@ -99,18 +164,24 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
"VEC3" -> 3 "VEC3" -> 3
else -> error("unsupported texture coordinate type ${accessor.type}") else -> error("unsupported texture coordinate type ${accessor.type}")
} }
textureCoordinate(dimensions, 0) textureCoordinate(4, 0)
//paddingFloat(4 - dimensions)
accessors.add(accessor) accessors.add(accessor)
convertors.add(CopyPadConvertor(4 - dimensions))
} }
"JOINTS_0" -> { "JOINTS_0" -> {
val type = when (Pair(accessor.type, accessor.componentType)) { attribute("joints", VertexElementType.VECTOR4_UINT32)
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)
accessors.add(accessor) 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" -> { "WEIGHTS_0" -> {
val type = when (Pair(accessor.type, accessor.componentType)) { val type = when (Pair(accessor.type, accessor.componentType)) {
Pair("VEC4", GLTF_FLOAT) -> VertexElementType.VECTOR4_FLOAT32 Pair("VEC4", GLTF_FLOAT) -> VertexElementType.VECTOR4_FLOAT32
@@ -118,23 +189,26 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
} }
attribute("weights", type) attribute("weights", type)
accessors.add(accessor) accessors.add(accessor)
convertors.add(CopyConvertor())
} }
} }
} }
} }
val buffers = val buffers =
accessors.map { it.bufferView } accessors.map { it.bufferView }
.distinct() .distinct()
.associate { .associate {
Pair(gltfFile.bufferViews[it].buffer, Pair(
gltfFile.buffers[gltfFile.bufferViews[it].buffer].contents(gltfFile)) gltfFile.bufferViews[it].buffer,
} gltfFile.buffers[gltfFile.bufferViews[it].buffer].contents(gltfFile)
)
}
val vb = vertexBuffer(format, maxCount) val vb = vertexBuffer(format, maxCount)
vb.put { vb.put {
for (i in 0 until maxCount) { for (i in 0 until maxCount) {
for (a in accessors) { for ((a, conv) in accessors zip convertors) {
val bufferView = gltfFile.bufferViews[a.bufferView] val bufferView = gltfFile.bufferViews[a.bufferView]
val buffer = buffers[bufferView.buffer] ?: error("no buffer ${bufferView.buffer}") val buffer = buffers[bufferView.buffer] ?: error("no buffer ${bufferView.buffer}")
val componentSize = when (a.componentType) { val componentSize = when (a.componentType) {
@@ -155,7 +229,8 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices
} }
val size = componentCount * componentSize val size = componentCount * componentSize
val offset = (bufferView.byteOffset ?: 0) + a.byteOffset + i * (bufferView.byteStride ?: size) 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<String, Int>, val indices
return GltfDrawCommand(vb, indexBuffer, drawPrimitive, indexBuffer?.indexCount ?: maxCount) return GltfDrawCommand(vb, indexBuffer, drawPrimitive, indexBuffer?.indexCount ?: maxCount)
} }
} }
@Serializable
data class GltfMesh(val primitives: List<GltfPrimitive>, val name: String) { data class GltfMesh(val primitives: List<GltfPrimitive>, val name: String) {
fun createDrawCommands(gltfFile: GltfFile): List<GltfDrawCommand> { fun createDrawCommands(gltfFile: GltfFile): List<GltfDrawCommand> {
return primitives.map { it.createDrawCommand(gltfFile) } return primitives.map { it.createDrawCommand(gltfFile) }
} }
} }
data class GltfPbrMetallicRoughness(val baseColorFactor: DoubleArray?, @Serializable
val baseColorTexture: GltfMaterialTexture?, data class GltfPbrMetallicRoughness(
var metallicRoughnessTexture: GltfMaterialTexture?, val baseColorFactor: DoubleArray? = null,
val roughnessFactor: Double?, val baseColorTexture: GltfMaterialTexture? = null,
val metallicFactor: Double?) 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 GltfTexture(val sampler: Int, val source: Int)
data class GltfMaterial(val name: String, @Serializable
val alphaMode: String?, data class GltfMaterial(
val doubleSided: Boolean?, val name: String,
val normalTexture: GltfMaterialTexture?, val alphaMode: String? = null,
val occlusionTexture: GltfMaterialTexture?, val doubleSided: Boolean? = null,
val emissiveTexture: GltfMaterialTexture?, val normalTexture: GltfMaterialTexture? = null,
val emissiveFactor: DoubleArray?, val occlusionTexture: GltfMaterialTexture? = null,
val pbrMetallicRoughness: GltfPbrMetallicRoughness?, val emissiveTexture: GltfMaterialTexture? = null,
val extensions: GltfMaterialExtensions? val emissiveFactor: DoubleArray? = null,
val pbrMetallicRoughness: GltfPbrMetallicRoughness? = null,
val extensions: GltfMaterialExtensions? = null
) )
@Serializable
data class GltfMaterialExtensions( data class GltfMaterialExtensions(
val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness? val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness?
) )
@Serializable
class KhrMaterialsPbrSpecularGlossiness(val diffuseFactor: DoubleArray?, val diffuseTexture: GltfMaterialTexture?) class KhrMaterialsPbrSpecularGlossiness(val diffuseFactor: DoubleArray?, val diffuseTexture: GltfMaterialTexture?)
data class GltfBufferView(val buffer: Int, @Serializable
val byteOffset: Int?, data class GltfBufferView(
val byteLength: Int, val buffer: Int,
val byteStride: Int?, val byteOffset: Int? = null,
val target: Int) 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) { fun contents(gltfFile: GltfFile): ByteBuffer = if (uri != null) {
if (uri.startsWith("data:")) { if (uri.startsWith("data:")) {
val base64 = uri.substring(uri.indexOf(",") + 1) 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 GltfDrawCommand(
val vertexBuffer: VertexBuffer,
data class GltfAccessor( val indexBuffer: IndexBuffer?,
val bufferView: Int, val primitive: DrawPrimitive,
val byteOffset: Int, var vertexCount: Int
val componentType: Int,
val count: Int,
val max: DoubleArray,
val min: DoubleArray,
val type: String
) )
data class GltfAnimation(val name: String?, val channels: List<GltfChannel>, val samplers: List<GltfAnimationSampler>) @Serializable
data class GltfAnimationSampler(val input: Int, val interpolation: String, val output: Int) 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<GltfChannel>, val samplers: List<GltfAnimationSampler>)
@Serializable
data class GltfAnimationSampler(val input: Int, val interpolation: String? = null, val output: Int)
@Serializable
data class GltfChannelTarget(val node: Int?, val path: String?) data class GltfChannelTarget(val node: Int?, val path: String?)
@Serializable
data class GltfChannel(val sampler: Int, val target: GltfChannelTarget) data class GltfChannel(val sampler: Int, val target: GltfChannelTarget)
@Serializable
data class GltfSkin(val inverseBindMatrices: Int, val joints: IntArray, val skeleton: Int) 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?) data class KHRLightsPunctualLightSpot(val innerConeAngle: Double?, val outerConeAngle: Double?)
@Serializable
data class KHRLightsPunctual(val lights: List<KHRLightsPunctualLight>) data class KHRLightsPunctual(val lights: List<KHRLightsPunctualLight>)
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 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( class GltfFile(
val asset: GltfAsset?, val asset: GltfAsset?,
val scene: Int?, val scene: Int? = null,
val scenes: List<GltfScene>, val scenes: List<GltfScene>,
val nodes: List<GltfNode>, val nodes: List<GltfNode>,
val meshes: List<GltfMesh>, val meshes: List<GltfMesh>,
val accessors: List<GltfAccessor>, val accessors: List<GltfAccessor>,
val materials: List<GltfMaterial>, val materials: List<GltfMaterial>,
val bufferViews: List<GltfBufferView>, val bufferViews: List<GltfBufferView>,
val buffers: List<GltfBuffer>, val buffers: List<GltfBuffer>,
val images: List<GltfImage>?, val images: List<GltfImage>? = null,
val textures: List<GltfTexture>?, val textures: List<GltfTexture>? = null,
val samplers: List<GltfSampler>?, val samplers: List<GltfSampler>? = null,
val animations: List<GltfAnimation>?, val animations: List<GltfAnimation>? = null,
val skins: List<GltfSkin>?, val skins: List<GltfSkin>? = null,
val extensions: GltfExtensions?, val extensions: GltfExtensions? = null,
val cameras: List<GltfCamera>? val extensionsUsed: List<String>? = null,
val extensionsRequired: List<String>? = null,
val cameras: List<GltfCamera>? = null
) { ) {
@Transient @Transient
lateinit var file: File lateinit var file: File
@@ -297,15 +422,17 @@ class GltfFile(
fun loadGltfFromFile(file: File): GltfFile = when (file.extension) { fun loadGltfFromFile(file: File): GltfFile = when (file.extension) {
"gltf" -> { "gltf" -> {
val gson = Gson() val gltfFile = Json{
val json = file.readText() ignoreUnknownKeys = true
gson.fromJson(json, GltfFile::class.java).apply { }.decodeFromString<GltfFile>(file.readText())
this.file = file gltfFile.file = file
} gltfFile
} }
"glb" -> { "glb" -> {
loadGltfFromGlbFile(file) loadGltfFromGlbFile(file)
} }
else -> error("extension ${file.extension} not supported in ${file}") else -> error("extension ${file.extension} not supported in ${file}")
} }

View File

@@ -238,7 +238,7 @@ fun GltfFile.buildSceneNodes(): GltfSceneData {
0, 0,
drawCommand.vertexCount drawCommand.vertexCount
) )
val material = materials.getOrNull(material)?.createSceneMaterial() ?: PBRMaterial() val material = materials.getOrNull(material ?: -1 )?.createSceneMaterial() ?: PBRMaterial()
return MeshPrimitive(geometry, material) return MeshPrimitive(geometry, material)
} }
@@ -278,16 +278,9 @@ fun GltfFile.buildSceneNodes(): GltfSceneData {
ibmData.order(ByteOrder.nativeOrder()) ibmData.order(ByteOrder.nativeOrder())
(ibmData as Buffer).position(ibmAccessor.byteOffset + (ibmBufferView.byteOffset ?: 0)) (ibmData as Buffer).position(ibmAccessor.byteOffset + (ibmBufferView.byteOffset ?: 0))
require(ibmAccessor.type == "MAT4") { require(ibmAccessor.type == "MAT4")
"Unsupported inverse bind matrix type: ${ibmAccessor.type}" require(ibmAccessor.componentType == GLTF_FLOAT)
} require(ibmAccessor.count == joints.size)
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})"
}
val ibms = (0 until ibmAccessor.count).map { val ibms = (0 until ibmAccessor.count).map {
val array = DoubleArray(16) val array = DoubleArray(16)
for (i in 0 until 16) { for (i in 0 until 16) {