From 6df5c2cb29f1b6e6b7fb39404abdf7607002beb7 Mon Sep 17 00:00:00 2001 From: dragbone Date: Tue, 20 Jan 2026 16:30:25 +0100 Subject: [PATCH 1/6] Add ModelTemplate --- .../de/fabmax/kool/modules/gltf/GltfFile.kt | 554 +-------------- .../kotlin/de/fabmax/kool/scene/Model.kt | 8 - .../de/fabmax/kool/scene/ModelTemplate.kt | 641 ++++++++++++++++++ 3 files changed, 668 insertions(+), 535 deletions(-) create mode 100644 kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt index 9ed604c79..6f5cb41cf 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt @@ -4,18 +4,24 @@ import de.fabmax.kool.AssetLoader import de.fabmax.kool.Assets import de.fabmax.kool.math.* import de.fabmax.kool.modules.ksl.KslPbrShader +import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.pipeline.BlendMode import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader import de.fabmax.kool.pipeline.shading.AlphaMode import de.fabmax.kool.pipeline.shading.DepthShader import de.fabmax.kool.scene.* import de.fabmax.kool.scene.animation.* +import de.fabmax.kool.scene.geometry.IndexedVertexList import de.fabmax.kool.util.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlin.math.min -suspend fun GltfFile(data: Uint8Buffer, filePath: String, assetLoader: AssetLoader = Assets.defaultLoader): Result { +suspend fun GltfFile( + data: Uint8Buffer, + filePath: String, + assetLoader: AssetLoader = Assets.defaultLoader +): Result { return try { val gltfData = if (filePath.lowercase().endsWith(".gz")) data.inflate() else data val gltfFile = when (val type = filePath.lowercase().removeSuffix(".gz").substringAfterLast('.')) { @@ -28,12 +34,20 @@ suspend fun GltfFile(data: Uint8Buffer, filePath: String, assetLoader: AssetLoad gltfFile.let { m -> m.buffers.filter { it.uri != null }.forEach { val uri = it.uri!! - val bufferUri = if (uri.startsWith("data:", true)) { uri } else { "$modelBasePath/$uri" } + val bufferUri = if (uri.startsWith("data:", true)) { + uri + } else { + "$modelBasePath/$uri" + } it.data = assetLoader.loadBlob(bufferUri).getOrThrow() } m.images.filter { it.uri != null }.forEach { val uri = it.uri!! - val imageUri = if (uri.startsWith("data:", true)) { uri } else { "$modelBasePath/$uri" } + val imageUri = if (uri.startsWith("data:", true)) { + uri + } else { + "$modelBasePath/$uri" + } it.uri = imageUri } m.updateReferences() @@ -74,7 +88,7 @@ private fun loadGlb(data: Uint8Buffer): GltfFile { chunkLen = str.readUInt() chunkType = str.readUInt() if (chunkType == GltfFile.GLB_CHUNK_MAGIC_BIN) { - model.buffers[iChunk-1].data = str.readData(chunkLen) + model.buffers[iChunk - 1].data = str.readData(chunkLen) } else { logW("loadGlb") { "Unexpected chunk type for chunk $iChunk: $chunkType (should be ${GltfFile.GLB_CHUNK_MAGIC_BIN} / ' BIN')" } @@ -126,8 +140,16 @@ data class GltfFile( val textures: List = emptyList() ) { + @Deprecated( + "Use template method instead", + replaceWith = ReplaceWith("makeModelTemplate(scene).makeModel(modelCfg)") + ) fun makeModel(modelCfg: GltfLoadConfig = GltfLoadConfig(), scene: Int = this.scene): Model { - return ModelGenerator(modelCfg).makeModel(scenes[scene]) + return makeModelTemplate(scene).makeModel(modelCfg) + } + + fun makeModelTemplate(scene: Int = this.scene): ModelTemplate { + return ModelTemplate(scenes[scene], this) } internal fun updateReferences() { @@ -183,528 +205,6 @@ data class GltfFile( } } - private inner class ModelGenerator(val cfg: GltfLoadConfig) { - val modelAnimations = mutableListOf() - val modelNodes = mutableMapOf() - val meshesByMaterial = mutableMapOf>>() - val meshMaterials = mutableMapOf, GltfMaterial?>() - - fun makeModel(scene: GltfScene): Model { - val model = Model(scene.name ?: "model_scene") - scene.nodeRefs.forEach { nd -> model += nd.makeNode(model, cfg) } - - if (cfg.loadAnimations) makeTrsAnimations() - if (cfg.loadAnimations) makeSkins(model) - modelNodes.forEach { (node, grp) -> node.createMeshes(model, grp, cfg) } - if (cfg.loadAnimations) makeMorphAnimations() - modelAnimations.filter { it.channels.isNotEmpty() }.forEach { modelAnim -> - modelAnim.prepareAnimation() - model.animations += modelAnim - } - model.disableAllAnimations() - - if (cfg.applyTransforms && model.animations.isEmpty()) applyTransforms(model) - if (cfg.mergeMeshesByMaterial) mergeMeshesByMaterial(model) - if (cfg.sortNodesByAlpha) model.sortNodesByAlpha() - if (cfg.removeEmptyNodes) model.removeEmpty() - - return model - } - - private fun Node.removeEmpty() { - children.filter { it !is Mesh<*> }.forEach { - it.removeEmpty() - if (it.children.isEmpty()) { - removeNode(it) - } - } - } - - private fun makeTrsAnimations() { - animations.forEach { anim -> - val modelAnim = Animation(anim.name) - modelAnimations += modelAnim - - val animNodes = mutableMapOf() - anim.channels.forEach { channel -> - val nodeGrp = modelNodes[channel.target.nodeRef] - if (nodeGrp != null) { - val animationNd = animNodes.getOrPut(nodeGrp) { AnimatedTransformGroup(nodeGrp) } - when (channel.target.path) { - GltfAnimation.Target.PATH_TRANSLATION -> makeTranslationAnimation(channel, animationNd, modelAnim) - GltfAnimation.Target.PATH_ROTATION -> makeRotationAnimation(channel, animationNd, modelAnim) - GltfAnimation.Target.PATH_SCALE -> makeScaleAnimation(channel, animationNd, modelAnim) - } - } - } - } - } - - private fun makeTranslationAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { - val inputAcc = animCh.samplerRef.inputAccessorRef - val outputAcc = animCh.samplerRef.outputAccessorRef - - if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported translation animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } - return - } - if (outputAcc.type != GltfAccessor.TYPE_VEC3 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported translation animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC3 and 5126 (float)" } - return - } - - val transChannel = TranslationAnimationChannel("${modelAnim.name}_translation", animNd) - val interpolation = when (animCh.samplerRef.interpolation) { - GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP - GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE - else -> AnimationKey.Interpolation.LINEAR - } - modelAnim.channels += transChannel - - val bindTranslation = animNd.initTranslation - val inTime = FloatAccessor(inputAcc) - val outTranslation = Vec3fAccessor(outputAcc) - for (i in 0 until min(inputAcc.count, outputAcc.count)) { - val t = inTime.next() - val transKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { - val startTan = outTranslation.next() - val point = outTranslation.next() - val endTan = outTranslation.next() - CubicTranslationKey( - t, - point - bindTranslation, - startTan - bindTranslation, - endTan - bindTranslation - ) - } else { - TranslationKey(t, outTranslation.next() - bindTranslation) - } - transKey.interpolation = interpolation - transChannel.keys[t] = transKey - } - } - - private fun makeRotationAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { - val inputAcc = animCh.samplerRef.inputAccessorRef - val outputAcc = animCh.samplerRef.outputAccessorRef - - if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported rotation animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } - return - } - if (outputAcc.type != GltfAccessor.TYPE_VEC4 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported rotation animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC4 and 5126 (float)" } - return - } - - val rotChannel = RotationAnimationChannel("${modelAnim.name}_rotation", animNd) - val interpolation = when (animCh.samplerRef.interpolation) { - GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP - GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE - else -> AnimationKey.Interpolation.LINEAR - } - modelAnim.channels += rotChannel - - val bindRotation = animNd.initRotation - val inTime = FloatAccessor(inputAcc) - val outRotation = Vec4fAccessor(outputAcc) - for (i in 0 until min(inputAcc.count, outputAcc.count)) { - val t = inTime.next() - val rotKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { - val startTan = outRotation.next().toQuatF() - val point = outRotation.next().toQuatF() - val endTan = outRotation.next().toQuatF() - CubicRotationKey( - t, - bindRotation.inverted().mul(point), - bindRotation.inverted().mul(startTan), - bindRotation.inverted().mul(endTan) - ) - } else { - RotationKey(t, bindRotation.inverted().mul(outRotation.next().toQuatF())) - } - rotKey.interpolation = interpolation - rotChannel.keys[t] = rotKey - } - } - - private fun makeScaleAnimation(animCh: GltfAnimation.Channel, animNd: AnimatedTransformGroup, modelAnim: Animation) { - val inputAcc = animCh.samplerRef.inputAccessorRef - val outputAcc = animCh.samplerRef.outputAccessorRef - - if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported scale animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } - return - } - if (outputAcc.type != GltfAccessor.TYPE_VEC3 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported scale animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC3 and 5126 (float)" } - return - } - - val scaleChannel = ScaleAnimationChannel("${modelAnim.name}_scale", animNd) - val interpolation = when (animCh.samplerRef.interpolation) { - GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP - GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE - else -> AnimationKey.Interpolation.LINEAR - } - modelAnim.channels += scaleChannel - - val bindScale = animNd.initScale - val inTime = FloatAccessor(inputAcc) - val outScale = Vec3fAccessor(outputAcc) - for (i in 0 until min(inputAcc.count, outputAcc.count)) { - val t = inTime.next() - val scaleKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { - val startTan = outScale.next() - val point = outScale.next() - val endTan = outScale.next() - CubicScaleKey(t, bindScale / point, bindScale / startTan, bindScale / endTan) - } else { - ScaleKey(t, bindScale / outScale.next()) - } - scaleKey.interpolation = interpolation - scaleChannel.keys[t] = scaleKey - } - } - - private fun makeMorphAnimations() { - animations.forEachIndexed { iAnim, anim -> - anim.channels.forEach { channel -> - if (channel.target.path == GltfAnimation.Target.PATH_WEIGHTS) { - val modelAnim = modelAnimations[iAnim] - val gltfMesh = channel.target.nodeRef?.meshRef - val nodeGrp = modelNodes[channel.target.nodeRef] - nodeGrp?.children?.filterIsInstance>()?.forEach { - makeWeightAnimation(gltfMesh!!, channel, MorphAnimatedMesh(it), modelAnim) - } - } - } - } - } - - private fun makeWeightAnimation(gltfMesh: GltfMesh, animCh: GltfAnimation.Channel, animNd: MorphAnimatedMesh, modelAnim: Animation) { - val inputAcc = animCh.samplerRef.inputAccessorRef - val outputAcc = animCh.samplerRef.outputAccessorRef - - if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported weight animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } - return - } - if (outputAcc.type != GltfAccessor.TYPE_SCALAR || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { - logW { "Unsupported weight animation output accessor: type = ${outputAcc.type}, component type = ${inputAcc.componentType}, should be VEC3 and 5126 (float)" } - return - } - - val weightChannel = WeightAnimationChannel("${modelAnim.name}_weight", animNd) - val interpolation = when (animCh.samplerRef.interpolation) { - GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP - GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE - else -> AnimationKey.Interpolation.LINEAR - } - modelAnim.channels += weightChannel - - val morphTargets = gltfMesh.primitives[0].targets - val nAttribs = gltfMesh.primitives[0].targets.sumOf { it.size } - val inTimes = FloatAccessor(inputAcc) - val outWeight = FloatAccessor(outputAcc) - for (i in 0 until min(inputAcc.count, outputAcc.count)) { - val t = inTimes.next() - val weightKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { - val startTan = FloatArray(nAttribs) - val point = FloatArray(nAttribs) - val endTan = FloatArray(nAttribs) - - var iAttrib = 0 - for (m in morphTargets.indices) { - val w = outWeight.next() - for (j in 0 until morphTargets[m].size) { - startTan[iAttrib++] = w - } - } - iAttrib = 0 - for (m in morphTargets.indices) { - val w = outWeight.next() - for (j in 0 until morphTargets[m].size) { - point[iAttrib++] = w - } - } - iAttrib = 0 - for (m in morphTargets.indices) { - val w = outWeight.next() - for (j in 0 until morphTargets[m].size) { - endTan[iAttrib++] = w - } - } - CubicWeightKey(t, point, startTan, endTan) - - } else { - val attribWeights = FloatArray(nAttribs) - var iAttrib = 0 - for (m in morphTargets.indices) { - val w = outWeight.next() - for (j in 0 until morphTargets[m].size) { - attribWeights[iAttrib++] = w - } - } - WeightKey(t, attribWeights) - } - - weightKey.interpolation = interpolation - weightChannel.keys[t] = weightKey - } - } - - private fun makeSkins(model: Model) { - skins.forEach { skin -> - val modelSkin = Skin() - val invBinMats = skin.inverseBindMatrixAccessorRef?.let { Mat4fAccessor(it) } - if (invBinMats != null) { - // 1st pass: make SkinNodes for specified nodes / TransformGroups - val skinNodes = mutableMapOf() - skin.jointRefs.forEach { joint -> - val jointGrp = modelNodes[joint]!! - val invBindMat = invBinMats.next() - val skinNode = Skin.SkinNode(jointGrp, invBindMat) - modelSkin.nodes += skinNode - skinNodes[joint] = skinNode - } - // 2nd pass: build SkinNode hierarchy - skin.jointRefs.forEach { joint -> - val skinNode = skinNodes[joint] - if (skinNode != null) { - joint.childRefs.forEach { child -> - val childNode = skinNodes[child] - childNode?.let { skinNode.addChild(it) } - } - } - } - model.skins += modelSkin - } - } - } - - private fun Node.sortNodesByAlpha(): Float { - val childAlphas = mutableMapOf() - var avgAlpha = 0f - for (child in children) { - val a = if (child is Mesh<*> && !child.isOpaque) { - 0f - } else { - child.sortNodesByAlpha() - } - childAlphas[child] = a - avgAlpha += a - } - sortChildrenBy { -(childAlphas[it] ?: 1f) } - if (children.isNotEmpty()) { - avgAlpha /= children.size - } - return avgAlpha - } - - private fun mergeMeshesByMaterial(model: Model) { - model.mergeMeshesByMaterial() - } - - private fun Node.mergeMeshesByMaterial() { - children.filter{ it.children.isNotEmpty() }.forEach { it.mergeMeshesByMaterial() } - - meshesByMaterial.values.forEach { sameMatMeshes -> - val mergeMeshes = children.filter { it in sameMatMeshes }.map { it as Mesh<*> } - if (mergeMeshes.size > 1) { - val r = mergeMeshes[0] - for (i in 1 until mergeMeshes.size) { - val m = mergeMeshes[i] - if (m.geometry.layout == r.geometry.layout) { - r.geometry.addGeometry(m.geometry) - removeNode(m) - } - } - } - } - } - - private fun applyTransforms(model: Model) { - val transform = Mat4fStack() - transform.setIdentity() - model.applyTransforms(transform, model) - } - - private fun Node.applyTransforms(transform: Mat4fStack, rootGroup: Node) { - transform.push() - transform.mul(this.transform.matrixF) - - children.filterIsInstance>().forEach { - it.geometry.forEach { v -> - transform.transform(v.position, 1f) - transform.transform(v.normal, 0f) - val tan3 = MutableVec3f(v.tangent.xyz) - transform.transform(tan3, 0f) - v.tangent.set(tan3, v.tangent.w) - } - it.updateGeometryBounds() - if (rootGroup != this) { - rootGroup += it - } - } - - children.filter { it.children.isNotEmpty() }.forEach { - it.applyTransforms(transform, rootGroup) - removeNode(it) - } - - transform.pop() - } - - private fun GltfNode.makeNode(model: Model, cfg: GltfLoadConfig): Node { - val modelNdName = name ?: "node_${model.nodes.size}" - val nodeGrp = Node(modelNdName) - modelNodes[this] = nodeGrp - model.nodes[modelNdName] = nodeGrp - - if (matrix != null) { - val t = MatrixTransformF() - t.matrixF.set(matrix.toFloatArray()) - nodeGrp.transform = t - } else { - val t = translation?.let { Vec3f(it[0], it[1], it[2]) } ?: Vec3f.ZERO - val r = rotation?.let { QuatF(it[0], it[1], it[2], it[3]) } ?: QuatF.IDENTITY - val s = scale?.let { Vec3f(it[0], it[1], it[2]) } ?: Vec3f.ONES - nodeGrp.transform.setCompositionOf(t, r, s) - } - - childRefs.forEach { - nodeGrp += it.makeNode(model, cfg) - } - - return nodeGrp - } - - private fun GltfNode.createMeshes(model: Model, nodeGrp: Node, cfg: GltfLoadConfig) { - meshRef?.primitives?.forEachIndexed { index, prim -> - val name = "${meshRef?.name ?: "${nodeGrp.name}.mesh"}_$index" - val geometry = prim.toGeometry(cfg, accessors) - if (!geometry.isEmpty()) { - var isFrustumChecked = true - var meshSkin: Skin? = null - var morphWeights: FloatArray? = null - - if (cfg.loadAnimations && cfg.applySkins && skin >= 0) { - meshSkin = model.skins[skin] - isFrustumChecked = false - } - if (cfg.loadAnimations && cfg.applyMorphTargets && prim.targets.isNotEmpty()) { - morphWeights = FloatArray(prim.targets.sumOf { it.size }) - isFrustumChecked = false - } - - val instances = cfg.instanceLayout?.let { MeshInstanceList(it) } - val mesh = Mesh(geometry, instances = instances, morphWeights = morphWeights, skin = meshSkin, name = name) - mesh.isFrustumChecked = isFrustumChecked - - nodeGrp += mesh - if (meshSkin != null) { - val skeletonRoot = skins[skin].skeleton - if (skeletonRoot >= 0) { - nodeGrp -= mesh - modelNodes[nodes[skeletonRoot]]!! += mesh - } - } - - meshesByMaterial.getOrPut(prim.material) { mutableSetOf() } += mesh - meshMaterials[mesh] = prim.materialRef - - if (cfg.applyMaterials) { - makeKslMaterial(prim, mesh, cfg, model) - } - model.meshes[name] = mesh - } - } - } - - private fun makeKslMaterial(prim: GltfMesh.Primitive, mesh: Mesh<*>, cfg: GltfLoadConfig, model: Model) { - var isDeferred = cfg.materialConfig.isDeferredShading - val useVertexColor = prim.attributes.containsKey(GltfMesh.Primitive.ATTRIBUTE_COLOR_0) - - val pbrConfig = DeferredKslPbrShader.Config.Builder().apply { - val material = prim.materialRef - if (material != null) { - material.applyTo(this, useVertexColor, this@GltfFile, cfg.assetLoader ?: Assets.defaultLoader) - } else { - color { - uniformColor(Color.GRAY.toLinear()) - } - } - - vertices { - modelMatrixComposition = cfg.materialConfig.modelMatrixComposition - if (mesh.instances != null) { - instancedModelMatrix() - } - if (mesh.skin != null) { - if (cfg.materialConfig.fixedNumberOfJoints > 0) { - enableArmatureFixedNumberOfBones(cfg.materialConfig.fixedNumberOfJoints) - } else { - enableArmature(mesh.skin.nodes.size) - } - if (cfg.materialConfig.fixedNumberOfJoints > 0 && cfg.materialConfig.fixedNumberOfJoints < mesh.skin.nodes.size) { - logW("GltfFile") { "\"${model.name}\": Number of joints exceeds the material config's fixedNumberOfJoints (mesh has ${mesh.skin.nodes.size}, materialConfig.fixedNumberOfJoints is ${cfg.materialConfig.fixedNumberOfJoints})" } - } - } - if (mesh.morphWeights != null) { - morphAttributes += mesh.geometry.getMorphAttributes() - } - } - - cfg.materialConfig.let { matCfg -> - lighting { - maxNumberOfLights = matCfg.maxNumberOfLights - addShadowMaps(matCfg.shadowMaps) - - matCfg.environmentMap?.let { ibl -> - imageBasedAmbientLight(ibl.irradianceMap) - reflectionMap = ibl.reflectionMap - } - matCfg.scrSpcAmbientOcclusionMap?.let { - enableSsao(it) - } - } - } - cfg.pbrBlock?.invoke(this, prim) - - if (alphaMode is AlphaMode.Blend) { - mesh.isOpaque = false - // transparent / blended meshes must be rendered in forward pass - isDeferred = false - } - - colorCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - emissionCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - normalMapCfg.defaultNormalMap?.let { model.textures[it.name] = it } - roughnessCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - metallicCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - aoCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - vertexCfg.displacementCfg.primaryTexture?.defaultTexture?.let { model.textures[it.name] = it } - } - - mesh.shader = if (isDeferred) { - pbrConfig.pipelineCfg.blendMode = BlendMode.DISABLED - DeferredKslPbrShader(pbrConfig.build()) - } else { - KslPbrShader(pbrConfig.build()) - } - - if (pbrConfig.alphaMode is AlphaMode.Mask) { - mesh.depthShaderConfig = DepthShader.Config.forMesh( - mesh, - pbrConfig.pipelineCfg.cullMethod, - pbrConfig.alphaMode, - pbrConfig.colorCfg.primaryTexture?.defaultTexture - ) - } - } - } - companion object { const val GLB_FILE_MAGIC = 0x46546c67 const val GLB_CHUNK_MAGIC_JSON = 0x4e4f534a diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt index 2f6bde13d..0bc9ee192 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt @@ -5,11 +5,8 @@ import de.fabmax.kool.scene.animation.Animation import de.fabmax.kool.scene.animation.Skin class Model(name: String? = null) : Node(name) { - val nodes = mutableMapOf() val meshes = mutableMapOf>() - val textures = mutableMapOf() - val animations = mutableListOf() val skins = mutableListOf() @@ -60,9 +57,4 @@ class Model(name: String? = null) : Node(name) { } } } - - override fun doRelease() { - super.doRelease() - textures.values.forEach { it.release() } - } } \ No newline at end of file diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt new file mode 100644 index 000000000..9bc10000d --- /dev/null +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -0,0 +1,641 @@ +package de.fabmax.kool.scene + +import de.fabmax.kool.Assets +import de.fabmax.kool.math.Mat4fStack +import de.fabmax.kool.math.MutableVec3f +import de.fabmax.kool.math.QuatF +import de.fabmax.kool.math.Vec3f +import de.fabmax.kool.math.toQuatF +import de.fabmax.kool.modules.gltf.FloatAccessor +import de.fabmax.kool.modules.gltf.GltfAccessor +import de.fabmax.kool.modules.gltf.GltfAnimation +import de.fabmax.kool.modules.gltf.GltfFile +import de.fabmax.kool.modules.gltf.GltfLoadConfig +import de.fabmax.kool.modules.gltf.GltfMaterial +import de.fabmax.kool.modules.gltf.GltfMesh +import de.fabmax.kool.modules.gltf.GltfNode +import de.fabmax.kool.modules.gltf.GltfScene +import de.fabmax.kool.modules.gltf.Mat4fAccessor +import de.fabmax.kool.modules.gltf.Vec3fAccessor +import de.fabmax.kool.modules.gltf.Vec4fAccessor +import de.fabmax.kool.modules.ksl.KslPbrShader +import de.fabmax.kool.modules.ksl.KslShader +import de.fabmax.kool.pipeline.BlendMode +import de.fabmax.kool.pipeline.Texture2d +import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader +import de.fabmax.kool.pipeline.shading.AlphaMode +import de.fabmax.kool.pipeline.shading.DepthShader +import de.fabmax.kool.scene.animation.AnimatedTransformGroup +import de.fabmax.kool.scene.animation.Animation +import de.fabmax.kool.scene.animation.AnimationKey +import de.fabmax.kool.scene.animation.CubicRotationKey +import de.fabmax.kool.scene.animation.CubicScaleKey +import de.fabmax.kool.scene.animation.CubicTranslationKey +import de.fabmax.kool.scene.animation.CubicWeightKey +import de.fabmax.kool.scene.animation.MorphAnimatedMesh +import de.fabmax.kool.scene.animation.RotationAnimationChannel +import de.fabmax.kool.scene.animation.RotationKey +import de.fabmax.kool.scene.animation.ScaleAnimationChannel +import de.fabmax.kool.scene.animation.ScaleKey +import de.fabmax.kool.scene.animation.Skin +import de.fabmax.kool.scene.animation.TranslationAnimationChannel +import de.fabmax.kool.scene.animation.TranslationKey +import de.fabmax.kool.scene.animation.WeightAnimationChannel +import de.fabmax.kool.scene.animation.WeightKey +import de.fabmax.kool.scene.geometry.IndexedVertexList +import de.fabmax.kool.util.BaseReleasable +import de.fabmax.kool.util.Color +import de.fabmax.kool.util.logW +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.get +import kotlin.collections.plusAssign +import kotlin.collections.set +import kotlin.math.min + +class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasable() { + val name = scene.name ?: "model_scene" + val textures = mutableMapOf() + + override fun doRelease() { + textures.values.forEach { it.release() } + geometryCache.values.forEach { it.release() } + } + + val geometryCache: MutableMap> = mutableMapOf() + fun getMeshGeometry(name: String, create: () -> IndexedVertexList<*>): IndexedVertexList<*> = + geometryCache.getOrPut(name, create) + + val shaderCache: MutableMap> = mutableMapOf() + fun getShaders( + name: String, + create: () -> Pair + ): Pair = + shaderCache.getOrPut(name, create) + + fun makeModel(cfg: GltfLoadConfig = GltfLoadConfig()): Model { + return ModelGenerator().makeModel(cfg) + } + + private inner class ModelGenerator { + val modelAnimations = mutableListOf() + val modelNodes = mutableMapOf() + val meshesByMaterial = mutableMapOf>>() + val meshMaterials = mutableMapOf, GltfMaterial?>() + + val animations get() = gltfFile.animations + val skins get() = gltfFile.skins + val accessors get() = gltfFile.accessors + val nodes get() = gltfFile.nodes + + fun makeModel(cfg: GltfLoadConfig): Model { + val model = Model(scene.name ?: "model_scene") + scene.nodeRefs.forEach { nd -> model += nd.makeNode(model, cfg) } + + if (cfg.loadAnimations) makeTrsAnimations() + if (cfg.loadAnimations) makeSkins(model) + modelNodes.forEach { (node, grp) -> node.createMeshes(model, grp, cfg) } + if (cfg.loadAnimations) makeMorphAnimations() + modelAnimations.filter { it.channels.isNotEmpty() }.forEach { modelAnim -> + modelAnim.prepareAnimation() + model.animations += modelAnim + } + model.disableAllAnimations() + + if (cfg.applyTransforms && model.animations.isEmpty()) applyTransforms(model) + if (cfg.mergeMeshesByMaterial) mergeMeshesByMaterial(model) + if (cfg.sortNodesByAlpha) model.sortNodesByAlpha() + if (cfg.removeEmptyNodes) model.removeEmpty() + + return model + } + + private fun Node.removeEmpty() { + children.filter { it !is Mesh<*> }.forEach { + it.removeEmpty() + if (it.children.isEmpty()) { + removeNode(it) + } + } + } + + private fun makeTrsAnimations() { + animations.forEach { anim -> + val modelAnim = Animation(anim.name) + modelAnimations += modelAnim + + val animNodes = mutableMapOf() + anim.channels.forEach { channel -> + val nodeGrp = modelNodes[channel.target.nodeRef] + if (nodeGrp != null) { + val animationNd = animNodes.getOrPut(nodeGrp) { AnimatedTransformGroup(nodeGrp) } + when (channel.target.path) { + GltfAnimation.Target.PATH_TRANSLATION -> makeTranslationAnimation( + channel, + animationNd, + modelAnim + ) + + GltfAnimation.Target.PATH_ROTATION -> makeRotationAnimation(channel, animationNd, modelAnim) + GltfAnimation.Target.PATH_SCALE -> makeScaleAnimation(channel, animationNd, modelAnim) + } + } + } + } + } + + private fun makeTranslationAnimation( + animCh: GltfAnimation.Channel, + animNd: AnimatedTransformGroup, + modelAnim: Animation + ) { + val inputAcc = animCh.samplerRef.inputAccessorRef + val outputAcc = animCh.samplerRef.outputAccessorRef + + if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported translation animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } + return + } + if (outputAcc.type != GltfAccessor.TYPE_VEC3 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported translation animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC3 and 5126 (float)" } + return + } + + val transChannel = TranslationAnimationChannel("${modelAnim.name}_translation", animNd) + val interpolation = when (animCh.samplerRef.interpolation) { + GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP + GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE + else -> AnimationKey.Interpolation.LINEAR + } + modelAnim.channels += transChannel + + val bindTranslation = animNd.initTranslation + val inTime = FloatAccessor(inputAcc) + val outTranslation = Vec3fAccessor(outputAcc) + for (i in 0 until min(inputAcc.count, outputAcc.count)) { + val t = inTime.next() + val transKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { + val startTan = outTranslation.next() + val point = outTranslation.next() + val endTan = outTranslation.next() + CubicTranslationKey( + t, + point - bindTranslation, + startTan - bindTranslation, + endTan - bindTranslation + ) + } else { + TranslationKey(t, outTranslation.next() - bindTranslation) + } + transKey.interpolation = interpolation + transChannel.keys[t] = transKey + } + } + + private fun makeRotationAnimation( + animCh: GltfAnimation.Channel, + animNd: AnimatedTransformGroup, + modelAnim: Animation + ) { + val inputAcc = animCh.samplerRef.inputAccessorRef + val outputAcc = animCh.samplerRef.outputAccessorRef + + if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported rotation animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } + return + } + if (outputAcc.type != GltfAccessor.TYPE_VEC4 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported rotation animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC4 and 5126 (float)" } + return + } + + val rotChannel = RotationAnimationChannel("${modelAnim.name}_rotation", animNd) + val interpolation = when (animCh.samplerRef.interpolation) { + GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP + GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE + else -> AnimationKey.Interpolation.LINEAR + } + modelAnim.channels += rotChannel + + val bindRotation = animNd.initRotation + val inTime = FloatAccessor(inputAcc) + val outRotation = Vec4fAccessor(outputAcc) + for (i in 0 until min(inputAcc.count, outputAcc.count)) { + val t = inTime.next() + val rotKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { + val startTan = outRotation.next().toQuatF() + val point = outRotation.next().toQuatF() + val endTan = outRotation.next().toQuatF() + CubicRotationKey( + t, + bindRotation.inverted().mul(point), + bindRotation.inverted().mul(startTan), + bindRotation.inverted().mul(endTan) + ) + } else { + RotationKey(t, bindRotation.inverted().mul(outRotation.next().toQuatF())) + } + rotKey.interpolation = interpolation + rotChannel.keys[t] = rotKey + } + } + + private fun makeScaleAnimation( + animCh: GltfAnimation.Channel, + animNd: AnimatedTransformGroup, + modelAnim: Animation + ) { + val inputAcc = animCh.samplerRef.inputAccessorRef + val outputAcc = animCh.samplerRef.outputAccessorRef + + if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported scale animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } + return + } + if (outputAcc.type != GltfAccessor.TYPE_VEC3 || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported scale animation output accessor: type = ${outputAcc.type}, component type = ${outputAcc.componentType}, should be VEC3 and 5126 (float)" } + return + } + + val scaleChannel = ScaleAnimationChannel("${modelAnim.name}_scale", animNd) + val interpolation = when (animCh.samplerRef.interpolation) { + GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP + GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE + else -> AnimationKey.Interpolation.LINEAR + } + modelAnim.channels += scaleChannel + + val bindScale = animNd.initScale + val inTime = FloatAccessor(inputAcc) + val outScale = Vec3fAccessor(outputAcc) + for (i in 0 until min(inputAcc.count, outputAcc.count)) { + val t = inTime.next() + val scaleKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { + val startTan = outScale.next() + val point = outScale.next() + val endTan = outScale.next() + CubicScaleKey(t, bindScale / point, bindScale / startTan, bindScale / endTan) + } else { + ScaleKey(t, bindScale / outScale.next()) + } + scaleKey.interpolation = interpolation + scaleChannel.keys[t] = scaleKey + } + } + + private fun makeMorphAnimations() { + animations.forEachIndexed { iAnim, anim -> + anim.channels.forEach { channel -> + if (channel.target.path == GltfAnimation.Target.PATH_WEIGHTS) { + val modelAnim = modelAnimations[iAnim] + val gltfMesh = channel.target.nodeRef?.meshRef + val nodeGrp = modelNodes[channel.target.nodeRef] + nodeGrp?.children?.filterIsInstance>()?.forEach { + makeWeightAnimation(gltfMesh!!, channel, MorphAnimatedMesh(it), modelAnim) + } + } + } + } + } + + private fun makeWeightAnimation( + gltfMesh: GltfMesh, + animCh: GltfAnimation.Channel, + animNd: MorphAnimatedMesh, + modelAnim: Animation + ) { + val inputAcc = animCh.samplerRef.inputAccessorRef + val outputAcc = animCh.samplerRef.outputAccessorRef + + if (inputAcc.type != GltfAccessor.TYPE_SCALAR || inputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported weight animation input accessor: type = ${inputAcc.type}, component type = ${inputAcc.componentType}, should be SCALAR and 5126 (float)" } + return + } + if (outputAcc.type != GltfAccessor.TYPE_SCALAR || outputAcc.componentType != GltfAccessor.COMP_TYPE_FLOAT) { + logW { "Unsupported weight animation output accessor: type = ${outputAcc.type}, component type = ${inputAcc.componentType}, should be VEC3 and 5126 (float)" } + return + } + + val weightChannel = WeightAnimationChannel("${modelAnim.name}_weight", animNd) + val interpolation = when (animCh.samplerRef.interpolation) { + GltfAnimation.Sampler.INTERPOLATION_STEP -> AnimationKey.Interpolation.STEP + GltfAnimation.Sampler.INTERPOLATION_CUBICSPLINE -> AnimationKey.Interpolation.CUBICSPLINE + else -> AnimationKey.Interpolation.LINEAR + } + modelAnim.channels += weightChannel + + val morphTargets = gltfMesh.primitives[0].targets + val nAttribs = gltfMesh.primitives[0].targets.sumOf { it.size } + val inTimes = FloatAccessor(inputAcc) + val outWeight = FloatAccessor(outputAcc) + for (i in 0 until min(inputAcc.count, outputAcc.count)) { + val t = inTimes.next() + val weightKey = if (interpolation == AnimationKey.Interpolation.CUBICSPLINE) { + val startTan = FloatArray(nAttribs) + val point = FloatArray(nAttribs) + val endTan = FloatArray(nAttribs) + + var iAttrib = 0 + for (m in morphTargets.indices) { + val w = outWeight.next() + for (j in 0 until morphTargets[m].size) { + startTan[iAttrib++] = w + } + } + iAttrib = 0 + for (m in morphTargets.indices) { + val w = outWeight.next() + for (j in 0 until morphTargets[m].size) { + point[iAttrib++] = w + } + } + iAttrib = 0 + for (m in morphTargets.indices) { + val w = outWeight.next() + for (j in 0 until morphTargets[m].size) { + endTan[iAttrib++] = w + } + } + CubicWeightKey(t, point, startTan, endTan) + + } else { + val attribWeights = FloatArray(nAttribs) + var iAttrib = 0 + for (m in morphTargets.indices) { + val w = outWeight.next() + for (j in 0 until morphTargets[m].size) { + attribWeights[iAttrib++] = w + } + } + WeightKey(t, attribWeights) + } + + weightKey.interpolation = interpolation + weightChannel.keys[t] = weightKey + } + } + + private fun makeSkins(model: Model) { + skins.forEach { skin -> + val modelSkin = Skin() + val invBinMats = skin.inverseBindMatrixAccessorRef?.let { Mat4fAccessor(it) } + if (invBinMats != null) { + // 1st pass: make SkinNodes for specified nodes / TransformGroups + val skinNodes = mutableMapOf() + skin.jointRefs.forEach { joint -> + val jointGrp = modelNodes[joint]!! + val invBindMat = invBinMats.next() + val skinNode = Skin.SkinNode(jointGrp, invBindMat) + modelSkin.nodes += skinNode + skinNodes[joint] = skinNode + } + // 2nd pass: build SkinNode hierarchy + skin.jointRefs.forEach { joint -> + val skinNode = skinNodes[joint] + if (skinNode != null) { + joint.childRefs.forEach { child -> + val childNode = skinNodes[child] + childNode?.let { skinNode.addChild(it) } + } + } + } + model.skins += modelSkin + } + } + } + + private fun Node.sortNodesByAlpha(): Float { + val childAlphas = mutableMapOf() + var avgAlpha = 0f + for (child in children) { + val a = if (child is Mesh<*> && !child.isOpaque) { + 0f + } else { + child.sortNodesByAlpha() + } + childAlphas[child] = a + avgAlpha += a + } + sortChildrenBy { -(childAlphas[it] ?: 1f) } + if (children.isNotEmpty()) { + avgAlpha /= children.size + } + return avgAlpha + } + + private fun mergeMeshesByMaterial(model: Model) { + model.mergeMeshesByMaterial() + } + + private fun Node.mergeMeshesByMaterial() { + children.filter { it.children.isNotEmpty() }.forEach { it.mergeMeshesByMaterial() } + + meshesByMaterial.values.forEach { sameMatMeshes -> + val mergeMeshes = children.filter { it in sameMatMeshes }.map { it as Mesh<*> } + if (mergeMeshes.size > 1) { + val r = mergeMeshes[0] + for (i in 1 until mergeMeshes.size) { + val m = mergeMeshes[i] + if (m.geometry.layout == r.geometry.layout) { + r.geometry.addGeometry(m.geometry) + removeNode(m) + } + } + } + } + } + + private fun applyTransforms(model: Model) { + val transform = Mat4fStack() + transform.setIdentity() + model.applyTransforms(transform, model) + } + + private fun Node.applyTransforms(transform: Mat4fStack, rootGroup: Node) { + transform.push() + transform.mul(this.transform.matrixF) + + children.filterIsInstance>().forEach { + it.geometry.forEach { v -> + transform.transform(v.position, 1f) + transform.transform(v.normal, 0f) + val tan3 = MutableVec3f(v.tangent.xyz) + transform.transform(tan3, 0f) + v.tangent.set(tan3, v.tangent.w) + } + it.updateGeometryBounds() + if (rootGroup != this) { + rootGroup += it + } + } + + children.filter { it.children.isNotEmpty() }.forEach { + it.applyTransforms(transform, rootGroup) + removeNode(it) + } + + transform.pop() + } + + private fun GltfNode.makeNode(model: Model, cfg: GltfLoadConfig): Node { + val modelNdName = name ?: "node_${model.nodes.size}" + val nodeGrp = Node(modelNdName) + modelNodes[this] = nodeGrp + model.nodes[modelNdName] = nodeGrp + + if (matrix != null) { + val t = MatrixTransformF() + t.matrixF.set(matrix.toFloatArray()) + nodeGrp.transform = t + } else { + val t = translation?.let { Vec3f(it[0], it[1], it[2]) } ?: Vec3f.ZERO + val r = rotation?.let { QuatF(it[0], it[1], it[2], it[3]) } ?: QuatF.IDENTITY + val s = scale?.let { Vec3f(it[0], it[1], it[2]) } ?: Vec3f.ONES + nodeGrp.transform.setCompositionOf(t, r, s) + } + + childRefs.forEach { + nodeGrp += it.makeNode(model, cfg) + } + + return nodeGrp + } + + private fun GltfNode.createMeshes( + model: Model, + nodeGrp: Node, + cfg: GltfLoadConfig + ) { + meshRef?.primitives?.forEachIndexed { index, prim -> + val name = "${meshRef?.name ?: "${nodeGrp.name}.mesh"}_$index" + val geometry = getMeshGeometry(name) { prim.toGeometry(cfg, accessors) } + if (!geometry.isEmpty()) { + var isFrustumChecked = true + var meshSkin: Skin? = null + var morphWeights: FloatArray? = null + + if (cfg.loadAnimations && cfg.applySkins && skin >= 0) { + meshSkin = model.skins[skin] + isFrustumChecked = false + } + if (cfg.loadAnimations && cfg.applyMorphTargets && prim.targets.isNotEmpty()) { + morphWeights = FloatArray(prim.targets.sumOf { it.size }) + isFrustumChecked = false + } + + val instances = cfg.instanceLayout?.let { MeshInstanceList(it) } + val mesh = + Mesh(geometry, instances = instances, morphWeights = morphWeights, skin = meshSkin, name = name) + mesh.isFrustumChecked = isFrustumChecked + + nodeGrp += mesh + if (meshSkin != null) { + val skeletonRoot = skins[skin].skeleton + if (skeletonRoot >= 0) { + nodeGrp -= mesh + modelNodes[nodes[skeletonRoot]]!! += mesh + } + } + + meshesByMaterial.getOrPut(prim.material) { mutableSetOf() } += mesh + meshMaterials[mesh] = prim.materialRef + + if (cfg.applyMaterials) { + val (shader, depthShader) = getShaders(name) { + makeKslMaterial(prim, mesh, cfg) + } + mesh.shader = shader + mesh.depthShaderConfig = depthShader + } + model.meshes[name] = mesh + } + } + } + + private fun makeKslMaterial( + prim: GltfMesh.Primitive, + mesh: Mesh<*>, + cfg: GltfLoadConfig, + ): Pair { + var isDeferred = cfg.materialConfig.isDeferredShading + val useVertexColor = prim.attributes.containsKey(GltfMesh.Primitive.ATTRIBUTE_COLOR_0) + + val pbrConfig = DeferredKslPbrShader.Config.Builder().apply { + val material = prim.materialRef + if (material != null) { + material.applyTo(this, useVertexColor, gltfFile, cfg.assetLoader ?: Assets.defaultLoader) + } else { + color { + uniformColor(Color.GRAY.toLinear()) + } + } + + vertices { + modelMatrixComposition = cfg.materialConfig.modelMatrixComposition + if (mesh.instances != null) { + instancedModelMatrix() + } + if (mesh.skin != null) { + if (cfg.materialConfig.fixedNumberOfJoints > 0) { + enableArmatureFixedNumberOfBones(cfg.materialConfig.fixedNumberOfJoints) + } else { + enableArmature(mesh.skin.nodes.size) + } + if (cfg.materialConfig.fixedNumberOfJoints > 0 && cfg.materialConfig.fixedNumberOfJoints < mesh.skin.nodes.size) { + logW("GltfFile") { "\"${this@ModelTemplate.name}\": Number of joints exceeds the material config's fixedNumberOfJoints (mesh has ${mesh.skin.nodes.size}, materialConfig.fixedNumberOfJoints is ${cfg.materialConfig.fixedNumberOfJoints})" } + } + } + if (mesh.morphWeights != null) { + morphAttributes += mesh.geometry.getMorphAttributes() + } + } + + cfg.materialConfig.let { matCfg -> + lighting { + maxNumberOfLights = matCfg.maxNumberOfLights + addShadowMaps(matCfg.shadowMaps) + + matCfg.environmentMap?.let { ibl -> + imageBasedAmbientLight(ibl.irradianceMap) + reflectionMap = ibl.reflectionMap + } + matCfg.scrSpcAmbientOcclusionMap?.let { + enableSsao(it) + } + } + } + cfg.pbrBlock?.invoke(this, prim) + + if (alphaMode is AlphaMode.Blend) { + mesh.isOpaque = false + // transparent / blended meshes must be rendered in forward pass + isDeferred = false + } + + colorCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + emissionCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + normalMapCfg.defaultNormalMap?.let { textures[it.name] = it } + roughnessCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + metallicCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + aoCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + vertexCfg.displacementCfg.primaryTexture?.defaultTexture?.let { textures[it.name] = it } + } + + val shader = if (isDeferred) { + pbrConfig.pipelineCfg.blendMode = BlendMode.DISABLED + DeferredKslPbrShader(pbrConfig.build()) + } else { + KslPbrShader(pbrConfig.build()) + } + val depthShader = if (pbrConfig.alphaMode is AlphaMode.Mask) { + DepthShader.Config.forMesh( + mesh, + pbrConfig.pipelineCfg.cullMethod, + pbrConfig.alphaMode, + pbrConfig.colorCfg.primaryTexture?.defaultTexture + ) + } else null + return Pair(shader, depthShader) + } + } +} \ No newline at end of file From b78da02be422cd1123f275f6b7e88d8fb28b510d Mon Sep 17 00:00:00 2001 From: dragbone Date: Fri, 23 Jan 2026 07:59:48 +0100 Subject: [PATCH 2/6] Add release checks --- kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt index aa49e10f3..5f620a323 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt @@ -277,7 +277,7 @@ open class Mesh( */ override fun doRelease() { super.doRelease() - geometry.release() + // geometry.release() drawGeometry.release() instances?.release() drawInstances?.release() @@ -303,6 +303,7 @@ open class Mesh( } // update bounds and ray test if geometry has changed + geometry.checkIsNotReleased() if (geometry.modCount.isDirty(geometryUpdateModCount)) { geometryUpdateModCount = geometry.modCount.count rayTest.onMeshDataChanged(this) @@ -323,6 +324,7 @@ open class Mesh( geom = geometry insts = instances } + geom.checkIsNotReleased() cmd.setup(this, geom, insts, pipeline, drawGroupId) } @@ -330,6 +332,7 @@ open class Mesh( meshPipelineData.captureBuffer() if (isAsyncRendering) { if (drawGeometry.modCount.isDirty(geometry.modCount)) { + geometry.checkIsNotReleased() drawGeometry.set(geometry) } if (instances != null && instances.modCount.isDirty(drawInstances!!.modCount)) { From 0d72c0602a91339ea6cadfebb84a6d59f0e72514 Mon Sep 17 00:00:00 2001 From: dragbone Date: Fri, 23 Jan 2026 10:05:09 +0100 Subject: [PATCH 3/6] Disable releasing of geometry by Meshes created by a ModelTemplate --- .../src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt | 3 ++- .../kotlin/de/fabmax/kool/scene/ModelTemplate.kt | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt index 5f620a323..673bde0bf 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Mesh.kt @@ -138,6 +138,7 @@ open class Mesh( ) : Node(name), DoubleBuffered { var isOpaque = true + var releaseGeometry = true val meshPipelineData = MultiPipelineBindGroupData(BindGroupScope.MESH) @@ -277,7 +278,7 @@ open class Mesh( */ override fun doRelease() { super.doRelease() - // geometry.release() + if (releaseGeometry) geometry.release() drawGeometry.release() instances?.release() drawInstances?.release() diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index 9bc10000d..9bf166b14 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -524,8 +524,13 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab } val instances = cfg.instanceLayout?.let { MeshInstanceList(it) } - val mesh = - Mesh(geometry, instances = instances, morphWeights = morphWeights, skin = meshSkin, name = name) + val mesh = Mesh( + geometry = geometry, + instances = instances, + morphWeights = morphWeights, + skin = meshSkin, + name = name + ).apply { releaseGeometry = false } mesh.isFrustumChecked = isFrustumChecked nodeGrp += mesh From d0dfa31196696095416fd4950aa1562deac46b6f Mon Sep 17 00:00:00 2001 From: dragbone Date: Fri, 23 Jan 2026 11:14:40 +0100 Subject: [PATCH 4/6] Improve documentation --- .../de/fabmax/kool/modules/gltf/GltfFile.kt | 21 +++++++++++++--- .../de/fabmax/kool/scene/ModelTemplate.kt | 25 ++++++++----------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt index 6f5cb41cf..b0238773e 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt @@ -141,17 +141,32 @@ data class GltfFile( ) { @Deprecated( - "Use template method instead", - replaceWith = ReplaceWith("makeModelTemplate(scene).makeModel(modelCfg)") + message = "Use makeModelTemplate if creating multiple Models or makeSingleModel if creating a single Model from this GltfFile", + ReplaceWith("makeSingleModel(scene, modelCfg)") ) fun makeModel(modelCfg: GltfLoadConfig = GltfLoadConfig(), scene: Int = this.scene): Model { - return makeModelTemplate(scene).makeModel(modelCfg) + return makeSingleModel(scene, modelCfg) } + /** + * Create a model template from this GltfFile. If you only need a single instances use [makeModel] instead. + * @see GltfFile.makeModel + */ fun makeModelTemplate(scene: Int = this.scene): ModelTemplate { return ModelTemplate(scenes[scene], this) } + /** + * Create a single model from this GltfFile. If you want to create multiple instances use [makeModelTemplate] instead. + * @see GltfFile.makeModelTemplate + */ + fun makeSingleModel(scene: Int = this.scene, modelCfg: GltfLoadConfig = GltfLoadConfig()): Model { + val template = makeModelTemplate(scene) + return template.makeModel(modelCfg).apply { + addDependingReleasable(template) + } + } + internal fun updateReferences() { accessors.forEach { if (it.bufferView >= 0) { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index 9bf166b14..2035a9337 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -53,26 +53,21 @@ import kotlin.collections.plusAssign import kotlin.collections.set import kotlin.math.min +/** + * Template that enables creation of multiple [Models][Model] with shared geometry and shader data. + * Releasing this template releases the shared data, breaking any Models still in use. + */ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasable() { val name = scene.name ?: "model_scene" - val textures = mutableMapOf() + private val textures = mutableMapOf() + private val geometryCache: MutableMap> = mutableMapOf() + private val shaderCache: MutableMap> = mutableMapOf() override fun doRelease() { textures.values.forEach { it.release() } geometryCache.values.forEach { it.release() } } - val geometryCache: MutableMap> = mutableMapOf() - fun getMeshGeometry(name: String, create: () -> IndexedVertexList<*>): IndexedVertexList<*> = - geometryCache.getOrPut(name, create) - - val shaderCache: MutableMap> = mutableMapOf() - fun getShaders( - name: String, - create: () -> Pair - ): Pair = - shaderCache.getOrPut(name, create) - fun makeModel(cfg: GltfLoadConfig = GltfLoadConfig()): Model { return ModelGenerator().makeModel(cfg) } @@ -89,7 +84,7 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab val nodes get() = gltfFile.nodes fun makeModel(cfg: GltfLoadConfig): Model { - val model = Model(scene.name ?: "model_scene") + val model = Model(name) scene.nodeRefs.forEach { nd -> model += nd.makeNode(model, cfg) } if (cfg.loadAnimations) makeTrsAnimations() @@ -508,7 +503,7 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab ) { meshRef?.primitives?.forEachIndexed { index, prim -> val name = "${meshRef?.name ?: "${nodeGrp.name}.mesh"}_$index" - val geometry = getMeshGeometry(name) { prim.toGeometry(cfg, accessors) } + val geometry = geometryCache.getOrPut(name) { prim.toGeometry(cfg, accessors) } if (!geometry.isEmpty()) { var isFrustumChecked = true var meshSkin: Skin? = null @@ -546,7 +541,7 @@ class ModelTemplate(val scene: GltfScene, val gltfFile: GltfFile) : BaseReleasab meshMaterials[mesh] = prim.materialRef if (cfg.applyMaterials) { - val (shader, depthShader) = getShaders(name) { + val (shader, depthShader) = shaderCache.getOrPut(name) { makeKslMaterial(prim, mesh, cfg) } mesh.shader = shader From 95a92524de4ac05ed2444aba8b3161627eef1253 Mon Sep 17 00:00:00 2001 From: dragbone Date: Fri, 23 Jan 2026 11:30:56 +0100 Subject: [PATCH 5/6] Revert formatting changes --- .../de/fabmax/kool/modules/gltf/GltfFile.kt | 20 ++++--------------- .../kotlin/de/fabmax/kool/scene/Model.kt | 3 ++- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt index b0238773e..beaf3e51e 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt @@ -17,11 +17,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlin.math.min -suspend fun GltfFile( - data: Uint8Buffer, - filePath: String, - assetLoader: AssetLoader = Assets.defaultLoader -): Result { +suspend fun GltfFile(data: Uint8Buffer, filePath: String, assetLoader: AssetLoader = Assets.defaultLoader): Result { return try { val gltfData = if (filePath.lowercase().endsWith(".gz")) data.inflate() else data val gltfFile = when (val type = filePath.lowercase().removeSuffix(".gz").substringAfterLast('.')) { @@ -34,20 +30,12 @@ suspend fun GltfFile( gltfFile.let { m -> m.buffers.filter { it.uri != null }.forEach { val uri = it.uri!! - val bufferUri = if (uri.startsWith("data:", true)) { - uri - } else { - "$modelBasePath/$uri" - } + val bufferUri = if (uri.startsWith("data:", true)) { uri } else { "$modelBasePath/$uri" } it.data = assetLoader.loadBlob(bufferUri).getOrThrow() } m.images.filter { it.uri != null }.forEach { val uri = it.uri!! - val imageUri = if (uri.startsWith("data:", true)) { - uri - } else { - "$modelBasePath/$uri" - } + val imageUri = if (uri.startsWith("data:", true)) { uri } else { "$modelBasePath/$uri" } it.uri = imageUri } m.updateReferences() @@ -88,7 +76,7 @@ private fun loadGlb(data: Uint8Buffer): GltfFile { chunkLen = str.readUInt() chunkType = str.readUInt() if (chunkType == GltfFile.GLB_CHUNK_MAGIC_BIN) { - model.buffers[iChunk - 1].data = str.readData(chunkLen) + model.buffers[iChunk-1].data = str.readData(chunkLen) } else { logW("loadGlb") { "Unexpected chunk type for chunk $iChunk: $chunkType (should be ${GltfFile.GLB_CHUNK_MAGIC_BIN} / ' BIN')" } diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt index 0bc9ee192..e15d29e5b 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/Model.kt @@ -1,12 +1,13 @@ package de.fabmax.kool.scene -import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.scene.animation.Animation import de.fabmax.kool.scene.animation.Skin class Model(name: String? = null) : Node(name) { + val nodes = mutableMapOf() val meshes = mutableMapOf>() + val animations = mutableListOf() val skins = mutableListOf() From 002d662f837a0601a4b48b6aa0d3722793e5293b Mon Sep 17 00:00:00 2001 From: dragbone Date: Fri, 23 Jan 2026 11:32:45 +0100 Subject: [PATCH 6/6] Optimize imports --- .../de/fabmax/kool/modules/gltf/GltfFile.kt | 13 +----- .../de/fabmax/kool/scene/ModelTemplate.kt | 42 ++----------------- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt index beaf3e51e..b1add6ee1 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/gltf/GltfFile.kt @@ -2,20 +2,11 @@ package de.fabmax.kool.modules.gltf import de.fabmax.kool.AssetLoader import de.fabmax.kool.Assets -import de.fabmax.kool.math.* -import de.fabmax.kool.modules.ksl.KslPbrShader -import de.fabmax.kool.modules.ksl.KslShader -import de.fabmax.kool.pipeline.BlendMode -import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader -import de.fabmax.kool.pipeline.shading.AlphaMode -import de.fabmax.kool.pipeline.shading.DepthShader -import de.fabmax.kool.scene.* -import de.fabmax.kool.scene.animation.* -import de.fabmax.kool.scene.geometry.IndexedVertexList +import de.fabmax.kool.scene.Model +import de.fabmax.kool.scene.ModelTemplate import de.fabmax.kool.util.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import kotlin.math.min suspend fun GltfFile(data: Uint8Buffer, filePath: String, assetLoader: AssetLoader = Assets.defaultLoader): Result { return try { diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt index 2035a9337..efa8e4ce7 100644 --- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt +++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/scene/ModelTemplate.kt @@ -1,23 +1,8 @@ package de.fabmax.kool.scene import de.fabmax.kool.Assets -import de.fabmax.kool.math.Mat4fStack -import de.fabmax.kool.math.MutableVec3f -import de.fabmax.kool.math.QuatF -import de.fabmax.kool.math.Vec3f -import de.fabmax.kool.math.toQuatF -import de.fabmax.kool.modules.gltf.FloatAccessor -import de.fabmax.kool.modules.gltf.GltfAccessor -import de.fabmax.kool.modules.gltf.GltfAnimation -import de.fabmax.kool.modules.gltf.GltfFile -import de.fabmax.kool.modules.gltf.GltfLoadConfig -import de.fabmax.kool.modules.gltf.GltfMaterial -import de.fabmax.kool.modules.gltf.GltfMesh -import de.fabmax.kool.modules.gltf.GltfNode -import de.fabmax.kool.modules.gltf.GltfScene -import de.fabmax.kool.modules.gltf.Mat4fAccessor -import de.fabmax.kool.modules.gltf.Vec3fAccessor -import de.fabmax.kool.modules.gltf.Vec4fAccessor +import de.fabmax.kool.math.* +import de.fabmax.kool.modules.gltf.* import de.fabmax.kool.modules.ksl.KslPbrShader import de.fabmax.kool.modules.ksl.KslShader import de.fabmax.kool.pipeline.BlendMode @@ -25,32 +10,11 @@ import de.fabmax.kool.pipeline.Texture2d import de.fabmax.kool.pipeline.deferred.DeferredKslPbrShader import de.fabmax.kool.pipeline.shading.AlphaMode import de.fabmax.kool.pipeline.shading.DepthShader -import de.fabmax.kool.scene.animation.AnimatedTransformGroup -import de.fabmax.kool.scene.animation.Animation -import de.fabmax.kool.scene.animation.AnimationKey -import de.fabmax.kool.scene.animation.CubicRotationKey -import de.fabmax.kool.scene.animation.CubicScaleKey -import de.fabmax.kool.scene.animation.CubicTranslationKey -import de.fabmax.kool.scene.animation.CubicWeightKey -import de.fabmax.kool.scene.animation.MorphAnimatedMesh -import de.fabmax.kool.scene.animation.RotationAnimationChannel -import de.fabmax.kool.scene.animation.RotationKey -import de.fabmax.kool.scene.animation.ScaleAnimationChannel -import de.fabmax.kool.scene.animation.ScaleKey -import de.fabmax.kool.scene.animation.Skin -import de.fabmax.kool.scene.animation.TranslationAnimationChannel -import de.fabmax.kool.scene.animation.TranslationKey -import de.fabmax.kool.scene.animation.WeightAnimationChannel -import de.fabmax.kool.scene.animation.WeightKey +import de.fabmax.kool.scene.animation.* import de.fabmax.kool.scene.geometry.IndexedVertexList import de.fabmax.kool.util.BaseReleasable import de.fabmax.kool.util.Color import de.fabmax.kool.util.logW -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.get -import kotlin.collections.plusAssign -import kotlin.collections.set import kotlin.math.min /**