From 4f00cddaec1e10ae8d7b61107447b3ee7f0f637e Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sat, 23 May 2020 13:35:47 +0200 Subject: [PATCH] Add gltf animation support to orx-dnk3 --- .../gltf-models/box-animated/BoxAnimated.glb | Bin 0 -> 11944 bytes orx-dnk3/build.gradle | 1 + orx-dnk3/src/demo/kotlin/DemoAnimations01.kt | 61 ++++++ orx-dnk3/src/demo/kotlin/DemoScene01.kt | 2 +- orx-dnk3/src/demo/kotlin/DemoScene02.kt | 15 +- orx-dnk3/src/main/kotlin/Facet.kt | 8 +- orx-dnk3/src/main/kotlin/PBRMaterial.kt | 19 +- orx-dnk3/src/main/kotlin/RenderPass.kt | 9 +- orx-dnk3/src/main/kotlin/Scene.kt | 9 +- orx-dnk3/src/main/kotlin/SceneRenderer.kt | 16 +- orx-dnk3/src/main/kotlin/gltf/Gltf.kt | 40 ++-- orx-dnk3/src/main/kotlin/gltf/GltfScene.kt | 179 +++++++++++++++--- orx-keyframer/src/main/kotlin/Key.kt | 3 +- .../src/main/kotlin/KeyQuaternion.kt | 64 +++++++ orx-keyframer/src/main/kotlin/KeyVector3.kt | 63 ++++++ orx-keyframer/src/main/kotlin/Keyframer.kt | 2 - 16 files changed, 416 insertions(+), 75 deletions(-) create mode 100644 demo-data/gltf-models/box-animated/BoxAnimated.glb create mode 100644 orx-dnk3/src/demo/kotlin/DemoAnimations01.kt create mode 100644 orx-keyframer/src/main/kotlin/KeyQuaternion.kt create mode 100644 orx-keyframer/src/main/kotlin/KeyVector3.kt diff --git a/demo-data/gltf-models/box-animated/BoxAnimated.glb b/demo-data/gltf-models/box-animated/BoxAnimated.glb new file mode 100644 index 0000000000000000000000000000000000000000..69481ec3caeb57428c806fdac08bfbe40d19ff55 GIT binary patch literal 11944 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 pHHPa9R~T+E++?@~#, targetOutput: String, val format: ColorFormat, val type: ColorType, - val blendMode: BlendMode = BlendMode.REPLACE) : FacetCombiner(facets, targetOutput) + val blendMode: BlendMode = BlendMode.BLEND) : FacetCombiner(facets, targetOutput) class MomentsFacet : ColorBufferFacetCombiner(setOf(FacetType.WORLD_POSITION), "moments", ColorFormat.RG, ColorType.FLOAT16) { override fun generateShader(): String { @@ -114,7 +114,9 @@ class ClipPositionFacet : ColorBufferFacetCombiner(setOf(FacetType.CLIP_POSITION class LDRColorFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR), "color", ColorFormat.RGBa, ColorType.UINT8) { override fun generateShader() = """ vec3 oofinalColor = (f_diffuse.rgb + f_specular.rgb + f_emission.rgb) * (1.0 - f_fog.a) + f_fog.rgb * f_fog.a; - o_$targetOutput.rgba = pow(vec4(oofinalColor, 1.0), vec4(1.0/2.2)); - o_$targetOutput.a = f_alpha; + o_$targetOutput = pow(vec4(oofinalColor.rgb, 1.0), vec4(1.0/2.2)); + o_$targetOutput *= m_color.a; + + """ } \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/PBRMaterial.kt b/orx-dnk3/src/main/kotlin/PBRMaterial.kt index fdf26472..835d2503 100644 --- a/orx-dnk3/src/main/kotlin/PBRMaterial.kt +++ b/orx-dnk3/src/main/kotlin/PBRMaterial.kt @@ -57,12 +57,12 @@ private fun DirectionalLight.fs(index: Int) = """ //| vec3 L = normalize(Lr); | ${shadows.fs(index)} | -| f_diffuse += NoL * attenuation * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb ; +| f_diffuse += NoL * attenuation * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb * m_ambientOcclusion;; | float Dg = D_GGX(m_roughness * m_roughness, NoH, H); | float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL); -| vec3 F = F_Schlick(m_color * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); +| vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); | vec3 Fr = (Dg * Vs) * F; -| f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb; +| f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb * m_ambientOcclusion;; |} """.trimMargin() @@ -70,7 +70,7 @@ private fun HemisphereLight.fs(index: Int): String = """ |{ | float f = dot(N, p_lightDirection$index) * 0.5 + 0.5; | vec3 irr = ${irradianceMap?.let { "texture(p_lightIrradianceMap$index, N).rgb" } ?: "vec3(1.0)"}; -| f_diffuse += mix(p_lightDownColor$index.rgb, p_lightUpColor$index.rgb, f) * irr * ((1.0 - m_metalness) * m_color.rgb);// * m_ambientOcclusion; +| f_diffuse += mix(p_lightDownColor$index.rgb, p_lightUpColor$index.rgb, f) * irr * ((1.0 - m_metalness) * m_color.rgb) * m_ambientOcclusion; |} """.trimMargin() @@ -98,7 +98,7 @@ private fun SpotLight.fs(index: Int): String { | f_diffuse += NoL * (0.1+0.9*attenuation) * Fd_Burley(m_roughness * m_roughness, NoV, NoL, LoH) * p_lightColor$index.rgb * m_color.rgb ; | float Dg = D_GGX(m_roughness * m_roughness, NoH, H); | float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL); -| vec3 F = F_Schlick(m_color * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); +| vec3 F = F_Schlick(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); | vec3 Fr = (Dg * Vs) * F; | f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb; | } @@ -252,7 +252,6 @@ class PBRMaterial : Material { var color = ColorRGBa.WHITE var metalness = 0.5 var roughness = 1.0 - var opacity = 1.0 var emission = ColorRGBa.BLACK var vertexPreamble: String? = null @@ -281,15 +280,13 @@ class PBRMaterial : Material { val cached = shadeStyles.getOrPut(context) { val needLight = needLight(context) val preambleFS = """ - vec3 m_color = p_color.rgb; + vec4 m_color = p_color; float m_f0 = 0.5; float m_roughness = p_roughness; float m_metalness = p_metalness; - float m_opacity = p_opacity; float m_ambientOcclusion = 1.0; vec3 m_emission = p_emission.rgb; vec3 m_normal = vec3(0.0, 0.0, 1.0); - float f_alpha = m_opacity; vec4 f_fog = vec4(0.0, 0.0, 0.0, 0.0); vec3 f_worldNormal = v_worldNormal; """.trimIndent() @@ -306,7 +303,7 @@ class PBRMaterial : Material { } + textures.mapIndexed { index, texture -> when (texture.target) { TextureTarget.NONE -> "" - TextureTarget.COLOR -> "m_color.rgb *= pow(tex$index.rgb, vec3(2.2));" + TextureTarget.COLOR -> "m_color.rgb *= pow(tex$index.rgb, vec3(2.2)); m_color.a *= tex$index.a;" TextureTarget.METALNESS -> "m_metalness = tex$index.r;" TextureTarget.ROUGHNESS -> "m_roughness = tex$index.r;" TextureTarget.METALNESS_ROUGHNESS -> "m_metalness = tex$index.r; m_roughness = tex$index.g;" @@ -420,7 +417,6 @@ class PBRMaterial : Material { shadeStyle.parameter("color", color) shadeStyle.parameter("metalness", metalness) shadeStyle.parameter("roughness", roughness) - shadeStyle.parameter("opacity", opacity) parameters.forEach { (k, v) -> when (v) { @@ -464,7 +460,6 @@ class PBRMaterial : Material { when (light) { is AmbientLight -> { } - is PointLight -> { shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz) shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation) diff --git a/orx-dnk3/src/main/kotlin/RenderPass.kt b/orx-dnk3/src/main/kotlin/RenderPass.kt index 36f36950..67716204 100644 --- a/orx-dnk3/src/main/kotlin/RenderPass.kt +++ b/orx-dnk3/src/main/kotlin/RenderPass.kt @@ -5,9 +5,16 @@ import org.openrndr.draw.DepthFormat import org.openrndr.draw.RenderTarget import org.openrndr.draw.renderTarget -class RenderPass(val combiners: List, val renderOpaque: Boolean = true, val renderTransparent: Boolean = false) +class RenderPass(val combiners: List, + val renderOpaque: Boolean = true, + val renderTransparent: Boolean = false, + val depthWrite: Boolean = true + +) val DefaultPass = RenderPass(listOf(LDRColorFacet())) +val DefaultOpaquePass = RenderPass(listOf(LDRColorFacet()), renderOpaque = true, renderTransparent = false) +val DefaultTransparentPass = RenderPass(listOf(LDRColorFacet()), renderOpaque = false, renderTransparent = true, depthWrite = false) val LightPass = RenderPass(emptyList()) val VSMLightPass = RenderPass(listOf(MomentsFacet())) diff --git a/orx-dnk3/src/main/kotlin/Scene.kt b/orx-dnk3/src/main/kotlin/Scene.kt index 8a91c2c9..0fcbe275 100644 --- a/orx-dnk3/src/main/kotlin/Scene.kt +++ b/orx-dnk3/src/main/kotlin/Scene.kt @@ -1,14 +1,15 @@ package org.openrndr.extra.dnk3 +import org.openrndr.Dispatcher import org.openrndr.math.Matrix44 -class Scene(val root: SceneNode = SceneNode(), - val updateFunctions: MutableList<() -> Unit> = mutableListOf()) +class Scene(val root: SceneNode = SceneNode(), val dispatcher: Dispatcher = Dispatcher()) -open class SceneNode(var entities: MutableList = mutableListOf()) { +open class SceneNode() { + var entities: MutableList = mutableListOf() var parent: SceneNode? = null - var transform = Matrix44.IDENTITY + open var transform = Matrix44.IDENTITY var worldTransform = Matrix44.IDENTITY val children = mutableListOf() var disposed = false diff --git a/orx-dnk3/src/main/kotlin/SceneRenderer.kt b/orx-dnk3/src/main/kotlin/SceneRenderer.kt index 6c39f45c..870b401f 100644 --- a/orx-dnk3/src/main/kotlin/SceneRenderer.kt +++ b/orx-dnk3/src/main/kotlin/SceneRenderer.kt @@ -21,7 +21,7 @@ class SceneRenderer { var cubemapDepthBuffer = depthBuffer(256, 256, DepthFormat.DEPTH16, BufferMultisample.Disabled) - var outputPasses = mutableListOf(DefaultPass) + var outputPasses = mutableListOf(DefaultOpaquePass, DefaultTransparentPass) var outputPassTarget: RenderTarget? = null var outputPassTargetMS: RenderTarget? = null @@ -36,9 +36,9 @@ class SceneRenderer { drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL drawer.cullTestPass = CullTestPass.FRONT - scene.updateFunctions.forEach { - it() - } + + scene.dispatcher.execute() + // update all the transforms scene.root.scan(Matrix44.IDENTITY) { p -> @@ -99,7 +99,10 @@ class SceneRenderer { for (pass in outputPasses) { val materialContext = MaterialContext(pass, lights, fogs, shadowLightTargets, meshCubemaps) - if ((pass != DefaultPass || postSteps.isNotEmpty()) && outputPassTarget == null) { + + val defaultPasses = setOf(DefaultTransparentPass, DefaultOpaquePass) + + if ((pass !in defaultPasses || postSteps.isNotEmpty()) && outputPassTarget == null) { outputPassTarget = pass.createPassTarget(RenderTarget.active.width, RenderTarget.active.height) } @@ -165,6 +168,8 @@ class SceneRenderer { private fun drawPass(drawer: Drawer, pass: RenderPass, materialContext: MaterialContext, meshes: List>, instancedMeshes: List>) { + + drawer.depthWrite = pass.depthWrite val primitives = meshes.flatMap { mesh -> mesh.content.primitives.map { primitive -> NodeContent(mesh.node, primitive) @@ -230,6 +235,7 @@ class SceneRenderer { primitive.primitive.geometry.vertexCount) } } + drawer.depthWrite = true } } diff --git a/orx-dnk3/src/main/kotlin/gltf/Gltf.kt b/orx-dnk3/src/main/kotlin/gltf/Gltf.kt index 5d4bcb7d..78f6cc21 100644 --- a/orx-dnk3/src/main/kotlin/gltf/Gltf.kt +++ b/orx-dnk3/src/main/kotlin/gltf/Gltf.kt @@ -147,6 +147,7 @@ class GltfPbrMetallicRoughness(val baseColorFactor: DoubleArray?, var metallicRoughnessTexture: GltfMaterialTexture?, val roughnessFactor: Double?, val metallicFactor: Double?) + class GltfMaterialTexture(val index: Int, val scale: Double?, val texCoord: Int?) class GltfImage(val uri: String?, val bufferView: Int?) @@ -156,6 +157,7 @@ class GltfSampler(val magFilter: Int, val minFilter: Int, val wrapS: Int, val wr class GltfTexture(val sampler: Int, val source: Int) class GltfMaterial(val name: String, + val alphaMode: String?, val doubleSided: Boolean?, val normalTexture: GltfMaterialTexture?, val occlusionTexture: GltfMaterialTexture?, @@ -163,7 +165,7 @@ class GltfMaterial(val name: String, val emissiveFactor: DoubleArray?, val pbrMetallicRoughness: GltfPbrMetallicRoughness?, val extensions: GltfMaterialExtensions? - ) +) class GltfMaterialExtensions( val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness? @@ -214,6 +216,13 @@ class GltfAccessor( val type: String ) +class GltfAnimation(val name: String?, val channels: List, val samplers: List) +class GltfAnimationSampler(val input: Int, val interpolation: String, val output: Int) + +class GltfChannelTarget(val node: Int?, val path: String?) + +class GltfChannel(val sampler: Int, val target: GltfChannelTarget) + class GltfFile( val asset: GltfAsset?, val scene: Int?, @@ -226,17 +235,26 @@ class GltfFile( val buffers: List, val images: List?, val textures: List?, - val samplers: List? + val samplers: List?, + val animations: List?) { + @Transient + lateinit var file: File -) { - @Transient lateinit var file: File - @Transient var bufferBuffer : ByteBuffer? = null + @Transient + var bufferBuffer: ByteBuffer? = null } -fun loadGltfFromFile(file: File): GltfFile { - val gson = Gson() - val json = file.readText() - return gson.fromJson(json, GltfFile::class.java).apply { - this.file = file +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 + } } -} \ No newline at end of file + "glb" -> { + loadGltfFromGlbFile(file) + } + else -> error("extension ${file.extension} not supported in ${file}") +} + diff --git a/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt b/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt index 8c21a5eb..432381f4 100644 --- a/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt +++ b/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt @@ -3,15 +3,60 @@ package org.openrndr.extra.dnk3.gltf import org.openrndr.color.ColorRGBa import org.openrndr.draw.* import org.openrndr.extra.dnk3.* +import org.openrndr.extra.keyframer.KeyframerChannelQuaternion +import org.openrndr.extra.keyframer.KeyframerChannelVector3 import org.openrndr.math.Matrix44 import org.openrndr.math.Quaternion +import org.openrndr.math.Vector3 import org.openrndr.math.transforms.transform import java.io.File +import java.nio.ByteOrder +import kotlin.reflect.KMutableProperty0 + +class SceneAnimation(var channels: List) { + fun applyToTargets(input: Double) { + for (channel in channels) { + channel.applyToTarget(input) + } + } +} + +sealed class AnimationChannel { + abstract fun applyToTarget(input: Double) +} + +class QuaternionChannel(val target: KMutableProperty0, + val keyframer: KeyframerChannelQuaternion) : AnimationChannel() { + override fun applyToTarget(input: Double) { + target.set(keyframer.value(input) ?: Quaternion.IDENTITY) + } +} + +class Vector3Channel(val target: KMutableProperty0, + val keyframer: KeyframerChannelVector3, val default: Vector3) : AnimationChannel() { + override fun applyToTarget(input: Double) { + target.set(keyframer.value(input) ?: default) + } +} + +class GltfSceneNode : SceneNode() { + var translation = Vector3.ZERO + var scale = Vector3.ONE + var rotation = Quaternion.IDENTITY + + override var transform: Matrix44 = Matrix44.IDENTITY + get() = transform { + translate(translation) + multiply(rotation.matrix.matrix44) + scale(scale) + } * field +} + +class GltfSceneData(val scenes: List>, val animations: List) + /** Tools to convert GltfFile into a DNK3 scene */ - -fun GltfFile.buildSceneNodes(): List> { - +fun GltfFile.buildSceneNodes(): GltfSceneData { val sceneImages = mutableMapOf() fun GltfImage.createSceneImage(): ColorBuffer { return sceneImages.getOrPut(this) { @@ -43,6 +88,7 @@ fun GltfFile.buildSceneNodes(): List> { val material = PBRMaterial() material.doubleSided = this.doubleSided ?: false + material.transparent = this.alphaMode != null pbrMetallicRoughness?.let { pbr -> material.roughness = pbr.roughnessFactor ?: 1.0 @@ -63,8 +109,8 @@ fun GltfFile.buildSceneNodes(): List> { val sceneTexture = Texture(ModelCoordinates(texture = cb, pre = "x_texCoord.y = 1.0-x_texCoord.y;"), TextureTarget.METALNESS_ROUGHNESS) material.textures.add(sceneTexture) } - } + occlusionTexture?.let { texture -> val cb = images!![textures!![texture.index].source].createSceneImage() cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR) @@ -94,9 +140,7 @@ fun GltfFile.buildSceneNodes(): List> { material.textures.add(sceneTexture) } - extensions?.let { ext -> - ext.KHR_materials_pbrSpecularGlossiness?.let { sg -> sg.diffuseFactor?.let { material.color = ColorRGBa(it[0], it[1], it[2], it[3]) @@ -109,13 +153,17 @@ fun GltfFile.buildSceneNodes(): List> { val sceneTexture = Texture(ModelCoordinates(texture = cb, pre = "x_texCoord.y = 1.0-x_texCoord.y;"), TextureTarget.COLOR) material.textures.add(sceneTexture) } - + occlusionTexture?.let { texture -> + val cb = images!![textures!![texture.index].source].createSceneImage() + cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR) + cb.wrapU = WrapMode.REPEAT + cb.wrapV = WrapMode.REPEAT + val sceneTexture = Texture(ModelCoordinates(texture = cb, pre = "x_texCoord.y = 1.0-x_texCoord.y;"), TextureTarget.AMBIENT_OCCLUSION) + material.textures.add(sceneTexture) + } } - - } - emissiveFactor?.let { material.emission = ColorRGBa(it[0], it[1], it[2], 1.0) } @@ -140,41 +188,114 @@ fun GltfFile.buildSceneNodes(): List> { }) } - fun GltfNode.createSceneNode(): SceneNode { - val node = SceneNode() + val sceneNodes = mutableMapOf() + fun GltfNode.createSceneNode(): SceneNode = sceneNodes.getOrPut(this) { + val node = GltfSceneNode() mesh?.let { node.entities.add(meshes[it].createSceneMesh()) } +// val localTransform = transform { +// translation?.let { +// translate(it[0], it[1], it[2]) +// } +// rotation?.let { +// val q = Quaternion(it[0], it[1], it[2], it[3]) +// multiply(q.matrix.matrix44) +// } +// scale?.let { +// scale(it[0], it[1], it[2]) +// } +// } - val localTransform = transform { + node.translation = translation?.let { Vector3(it[0], it[1], it[2]) } ?: Vector3.ZERO + node.scale = scale?.let { Vector3(it[0], it[1], it[2]) } ?: Vector3.ONE + node.rotation = rotation?.let { Quaternion(it[0], it[1], it[2], it[3]) } ?: Quaternion.IDENTITY - translation?.let { - translate(it[0], it[1], it[2]) - } - rotation?.let { - val q = Quaternion(it[0], it[1], it[2], it[3]) - multiply(q.matrix.matrix44) - } - - scale?.let { - scale(it[0], it[1], it[2]) - } + matrix?.let { + node.transform = Matrix44.fromDoubleArray(it).transposed } - node.transform = this.matrix?.let { - Matrix44.fromDoubleArray(it).transposed - } ?: localTransform + +// node.transform = this.matrix?.let { +// Matrix44.fromDoubleArray(it).transposed +// } ?: localTransform for (child in children.orEmpty) { node.children.add(nodes[child].createSceneNode()) } - return node + node } - return scenes.map { scene -> + val scenes = scenes.map { scene -> scene.nodes.map { node -> nodes[node].createSceneNode() } } + + val sceneAnimations = animations?.map { animation -> + val animationChannels = animation.channels.mapNotNull { channel -> + val candidate = channel.target.node?.let { nodes[it] }?.createSceneNode() as? GltfSceneNode + candidate?.let { sceneNode -> + val sampler = animation.samplers[channel.sampler] + + val inputAccessor = accessors[sampler.input] + val inputBufferView = bufferViews[inputAccessor.bufferView] + val inputData = buffers[inputBufferView.buffer].contents(this) + + val outputAccessor = accessors[sampler.output] + val outputBufferView = bufferViews[outputAccessor.bufferView] + val outputData = buffers[outputBufferView.buffer].contents(this) + + inputData.order(ByteOrder.nativeOrder()) + outputData.order(ByteOrder.nativeOrder()) + + require(inputAccessor.count == outputAccessor.count) + when (channel.target.path) { + "scale", "translation" -> { + require(inputAccessor.type == "SCALAR") + require(outputAccessor.type == "VEC3") + val keyframer = KeyframerChannelVector3() + val inputOffset = (inputBufferView.byteOffset ?: 0) + (inputAccessor.byteOffset ?: 0) + val outputOffset = (outputBufferView.byteOffset ?: 0) + (outputAccessor.byteOffset ?: 0) + val inputStride = (inputBufferView.byteStride ?: 4) + val outputStride = (outputBufferView.byteStride ?: 12) + for (i in 0 until outputAccessor.count) { + val input = inputData.getFloat(inputOffset + i * inputStride).toDouble() + val outputX = outputData.getFloat(outputOffset + i * outputStride).toDouble() + val outputY = outputData.getFloat(outputOffset + i * outputStride + 4).toDouble() + val outputZ = outputData.getFloat(outputOffset + i * outputStride + 8).toDouble() + keyframer.add(input, Vector3(outputX, outputY, outputZ)) + } + val target = if (channel.target.path == "translation") sceneNode::translation else sceneNode::scale + val default = if (channel.target.path == "translation") Vector3.ZERO else Vector3.ONE + Vector3Channel(target, keyframer, default) + } + "rotation" -> { + require(inputAccessor.type == "SCALAR") + require(outputAccessor.type == "VEC4") { + "${outputAccessor.type}" + } + val keyframer = KeyframerChannelQuaternion() + val inputOffset = (inputBufferView.byteOffset ?: 0) + (inputAccessor.byteOffset ?: 0) + val outputOffset = (outputBufferView.byteOffset ?: 0) + (inputAccessor.byteOffset ?: 0) + val inputStride = (inputBufferView.byteStride ?: 4) + val outputStride = (outputBufferView.byteStride ?: 16) + for (i in 0 until outputAccessor.count) { + val input = inputData.getFloat(inputOffset + i * inputStride).toDouble() + val outputX = outputData.getFloat(outputOffset + i * outputStride).toDouble() + val outputY = outputData.getFloat(outputOffset + i * outputStride + 4).toDouble() + val outputZ = outputData.getFloat(outputOffset + i * outputStride + 8).toDouble() + val outputW = outputData.getFloat(outputOffset + i * outputStride + 12).toDouble() + keyframer.add(input, Quaternion(outputX, outputY, outputZ, outputW)) + } + QuaternionChannel(sceneNode::rotation, keyframer) + } + else -> error("unsupported path ${channel.target.path}") + } + } + } + SceneAnimation(animationChannels) + } + return GltfSceneData(scenes, sceneAnimations.orEmpty()) } private val IntArray?.orEmpty: IntArray get() = this ?: IntArray(0) \ No newline at end of file diff --git a/orx-keyframer/src/main/kotlin/Key.kt b/orx-keyframer/src/main/kotlin/Key.kt index 6569c40a..6da2a271 100644 --- a/orx-keyframer/src/main/kotlin/Key.kt +++ b/orx-keyframer/src/main/kotlin/Key.kt @@ -65,4 +65,5 @@ class KeyframerChannel { leftKey.value * (1.0 - e0) + rightKey.value * (e0) } } -} \ No newline at end of file +} + diff --git a/orx-keyframer/src/main/kotlin/KeyQuaternion.kt b/orx-keyframer/src/main/kotlin/KeyQuaternion.kt new file mode 100644 index 00000000..8821e437 --- /dev/null +++ b/orx-keyframer/src/main/kotlin/KeyQuaternion.kt @@ -0,0 +1,64 @@ +package org.openrndr.extra.keyframer + +import org.openrndr.extras.easing.Easing +import org.openrndr.extras.easing.EasingFunction +import org.openrndr.math.Quaternion +import org.openrndr.math.slerp + +class KeyQuaternion(val time: Double, val value: Quaternion, val easing: EasingFunction) + +class KeyframerChannelQuaternion { + val keys = mutableListOf() + + operator fun invoke() : Double { + return 0.0 + } + + fun add(time: Double, value: Quaternion?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) { + if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) { + lastValue()?.let { + keys.add(KeyQuaternion(time, it, Easing.Linear.function)) + } + } + value?.let { + keys.add(KeyQuaternion(time, it, easing)) + } + } + + fun lastValue(): Quaternion? { + return keys.lastOrNull()?.value + } + + fun duration(): Double { + return keys.last().time + } + + fun value(time: Double): Quaternion? { + if (keys.size == 0) { + return null + } + if (keys.size == 1) { + return if (time < keys.first().time) { + keys[0].value.normalized + } else { + keys[0].value.normalized + } + } + + if (time < keys.first().time) { + return null + } + + val rightIndex = keys.indexOfFirst { it.time > time } + return if (rightIndex == -1) { + keys.last().value.normalized + } else { + val leftIndex = (rightIndex - 1).coerceAtLeast(0) + val rightKey = keys[rightIndex] + val leftKey = keys[leftIndex] + val t0 = (time - leftKey.time) / (rightKey.time - leftKey.time) + val e0 = rightKey.easing(t0, 0.0, 1.0, 1.0) + slerp(leftKey.value, rightKey.value, e0).normalized + } + } +} \ No newline at end of file diff --git a/orx-keyframer/src/main/kotlin/KeyVector3.kt b/orx-keyframer/src/main/kotlin/KeyVector3.kt new file mode 100644 index 00000000..af9350c9 --- /dev/null +++ b/orx-keyframer/src/main/kotlin/KeyVector3.kt @@ -0,0 +1,63 @@ +package org.openrndr.extra.keyframer + +import org.openrndr.extras.easing.Easing +import org.openrndr.extras.easing.EasingFunction +import org.openrndr.math.Vector3 + +class KeyVector3(val time: Double, val value: Vector3, val easing: EasingFunction) + +class KeyframerChannelVector3 { + val keys = mutableListOf() + + operator fun invoke() : Double { + return 0.0 + } + + fun add(time: Double, value: Vector3?, easing: EasingFunction = Easing.Linear.function, jump: Hold = Hold.HoldNone) { + if (jump == Hold.HoldAll || (jump == Hold.HoldSet && value != null)) { + lastValue()?.let { + keys.add(KeyVector3(time, it, Easing.Linear.function)) + } + } + value?.let { + keys.add(KeyVector3(time, it, easing)) + } + } + + fun lastValue(): Vector3? { + return keys.lastOrNull()?.value + } + + fun duration(): Double { + return keys.last().time + } + + fun value(time: Double): Vector3? { + if (keys.size == 0) { + return null + } + if (keys.size == 1) { + return if (time < keys.first().time) { + null + } else { + keys[0].value + } + } + + if (time < keys.first().time) { + return null + } + + val rightIndex = keys.indexOfFirst { it.time > time } + return if (rightIndex == -1) { + keys.last().value + } else { + val leftIndex = (rightIndex - 1).coerceAtLeast(0) + val rightKey = keys[rightIndex] + val leftKey = keys[leftIndex] + val t0 = (time - leftKey.time) / (rightKey.time - leftKey.time) + val e0 = rightKey.easing(t0, 0.0, 1.0, 1.0) + leftKey.value * (1.0 - e0) + rightKey.value * (e0) + } + } +} \ No newline at end of file diff --git a/orx-keyframer/src/main/kotlin/Keyframer.kt b/orx-keyframer/src/main/kotlin/Keyframer.kt index a11161e9..fd803af4 100644 --- a/orx-keyframer/src/main/kotlin/Keyframer.kt +++ b/orx-keyframer/src/main/kotlin/Keyframer.kt @@ -23,8 +23,6 @@ enum class KeyframerFormat { FULL } - - open class Keyframer { private var currentTime = 0.0 operator fun invoke(time: Double) {