[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 {
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"))

View File

@@ -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<GltfFile>(json)
gltFile.file = file
gltFile.bufferBuffer = bufferBuffer
return gltFile
}

View File

@@ -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<String, Int>, val indices: Int?, val mode: Int?, val material: Int) {
@Serializable
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 {
val indexBuffer = indices?.let { indices ->
@@ -65,8 +81,10 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, 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<String, Int>, 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<GltfAccessor>()
val convertors = mutableListOf<Convertor>()
val format = vertexFormat {
for ((name, index) in attributes.toSortedMap()) {
val accessor = gltfFile.accessors[index]
@@ -82,16 +139,24 @@ data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, 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<String, Int>, 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<String, Int>, 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<String, Int>, 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<String, Int>, val indices
return GltfDrawCommand(vb, indexBuffer, drawPrimitive, indexBuffer?.indexCount ?: maxCount)
}
}
@Serializable
data class GltfMesh(val primitives: List<GltfPrimitive>, val name: String) {
fun createDrawCommands(gltfFile: GltfFile): List<GltfDrawCommand> {
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<GltfChannel>, val samplers: List<GltfAnimationSampler>)
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<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?)
@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<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 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<GltfScene>,
val nodes: List<GltfNode>,
val meshes: List<GltfMesh>,
val accessors: List<GltfAccessor>,
val materials: List<GltfMaterial>,
val bufferViews: List<GltfBufferView>,
val buffers: List<GltfBuffer>,
val images: List<GltfImage>?,
val textures: List<GltfTexture>?,
val samplers: List<GltfSampler>?,
val animations: List<GltfAnimation>?,
val skins: List<GltfSkin>?,
val extensions: GltfExtensions?,
val cameras: List<GltfCamera>?
val asset: GltfAsset?,
val scene: Int? = null,
val scenes: List<GltfScene>,
val nodes: List<GltfNode>,
val meshes: List<GltfMesh>,
val accessors: List<GltfAccessor>,
val materials: List<GltfMaterial>,
val bufferViews: List<GltfBufferView>,
val buffers: List<GltfBuffer>,
val images: List<GltfImage>? = null,
val textures: List<GltfTexture>? = null,
val samplers: List<GltfSampler>? = null,
val animations: List<GltfAnimation>? = null,
val skins: List<GltfSkin>? = null,
val extensions: GltfExtensions? = null,
val extensionsUsed: List<String>? = null,
val extensionsRequired: List<String>? = null,
val cameras: List<GltfCamera>? = 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<GltfFile>(file.readText())
gltfFile.file = file
gltfFile
}
"glb" -> {
loadGltfFromGlbFile(file)
}
else -> error("extension ${file.extension} not supported in ${file}")
}

View File

@@ -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) {