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 00000000..69481ec3 Binary files /dev/null and b/demo-data/gltf-models/box-animated/BoxAnimated.glb differ diff --git a/orx-dnk3/build.gradle b/orx-dnk3/build.gradle index e87d229e..ba8a852a 100644 --- a/orx-dnk3/build.gradle +++ b/orx-dnk3/build.gradle @@ -11,6 +11,7 @@ sourceSets { dependencies { implementation "com.google.code.gson:gson:$gsonVersion" implementation(project(":orx-fx")) + implementation(project(":orx-keyframer")) demoImplementation(project(":orx-camera")) demoImplementation("org.openrndr:openrndr-core:$openrndrVersion") demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion") diff --git a/orx-dnk3/src/demo/kotlin/DemoAnimations01.kt b/orx-dnk3/src/demo/kotlin/DemoAnimations01.kt new file mode 100644 index 00000000..8c2cca6b --- /dev/null +++ b/orx-dnk3/src/demo/kotlin/DemoAnimations01.kt @@ -0,0 +1,61 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.dnk3.* +import org.openrndr.extra.dnk3.gltf.buildSceneNodes +import org.openrndr.extra.dnk3.gltf.loadGltfFromFile +import org.openrndr.extras.camera.Orbital +import org.openrndr.math.Matrix33 +import org.openrndr.math.Quaternion +import org.openrndr.math.Vector3 +import org.openrndr.math.mod_ +import org.openrndr.math.transforms.transform +import java.io.File + +fun main() = application { + configure { + width = 1280 + height = 720 + //multisample = WindowMultisample.SampleCount(8) + } + + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val gltf = loadGltfFromFile(File("demo-data/gltf-models/box-animated/BoxAnimated.glb")) + val scene = Scene(SceneNode()) + + + // -- add some lights + val lightNode = SceneNode() + lightNode.transform = transform { + translate(0.0, 10.0, 0.0) + rotate(Vector3.UNIT_X, -65.0) + } + lightNode.entities.add(DirectionalLight()) + scene.root.entities.add(HemisphereLight().apply { + upColor = ColorRGBa.BLUE.shade(0.4) + downColor = ColorRGBa.GRAY.shade(0.1) + }) + scene.root.children.add(lightNode) + val sceneData = gltf.buildSceneNodes() + scene.root.children.addAll(sceneData.scenes.first()) + + // -- create a renderer + val renderer = dryRenderer() + extend(Orbital()) { + far = 50.0 + eye = Vector3(1.5, 0.0, 3.0) + fov = 40.0 + } + extend { + sceneData.animations[0].applyToTargets(seconds.mod_(4.0)) + drawer.clear(ColorRGBa.PINK) + renderer.draw(drawer, scene) + } + } +} \ No newline at end of file diff --git a/orx-dnk3/src/demo/kotlin/DemoScene01.kt b/orx-dnk3/src/demo/kotlin/DemoScene01.kt index 83599853..0aec8020 100644 --- a/orx-dnk3/src/demo/kotlin/DemoScene01.kt +++ b/orx-dnk3/src/demo/kotlin/DemoScene01.kt @@ -38,7 +38,7 @@ fun main() = application { downColor = ColorRGBa.GRAY.shade(0.1) }) scene.root.children.add(lightNode) - scene.root.children.addAll(gltf.buildSceneNodes().first()) + scene.root.children.addAll(gltf.buildSceneNodes().scenes.first()) // -- create a renderer val renderer = dryRenderer() diff --git a/orx-dnk3/src/demo/kotlin/DemoScene02.kt b/orx-dnk3/src/demo/kotlin/DemoScene02.kt index d322da48..a589b042 100644 --- a/orx-dnk3/src/demo/kotlin/DemoScene02.kt +++ b/orx-dnk3/src/demo/kotlin/DemoScene02.kt @@ -5,6 +5,7 @@ import org.openrndr.extra.dnk3.* import org.openrndr.extra.dnk3.gltf.buildSceneNodes import org.openrndr.extra.dnk3.gltf.loadGltfFromFile +import org.openrndr.extra.dnk3.gltf.loadGltfFromGlbFile import org.openrndr.extras.camera.Orbital import org.openrndr.math.Vector3 import org.openrndr.math.transforms.transform @@ -24,30 +25,32 @@ fun main() = application { } } - val gltf = loadGltfFromFile(File("demo-data/gltf-models/duck/Duck.gltf")) + val gltf = loadGltfFromFile(File("demo-data/gltf-models/complex02/scene.gltf")) +// val gltf = loadGltfFromGlbFile(File("demo-data/gltf-models/splash-sss.glb")) val scene = Scene(SceneNode()) // -- add some lights val lightNode = SceneNode() lightNode.transform = transform { translate(0.0, 10.0, 0.0) - rotate(Vector3.UNIT_X, -65.0) + rotate(Vector3.UNIT_X, -90.0) } lightNode.entities.add(DirectionalLight()) scene.root.entities.add(HemisphereLight().apply { - upColor = ColorRGBa.WHITE.shade(0.4) + upColor = ColorRGBa.WHITE.shade(1.0) downColor = ColorRGBa.WHITE.shade(0.1) }) scene.root.children.add(lightNode) - scene.root.children.addAll(gltf.buildSceneNodes().first()) + scene.root.children.addAll(gltf.buildSceneNodes().scenes.first()) + // -- create a renderer val renderer = dryRenderer() extend(Orbital()) { - far = 50.0 + far = 500.0 lookAt = Vector3(0.0, 0.7, 0.0) eye = Vector3(3.0, 0.7, -2.0) - fov = 40.0 + fov = 30.0 } extend { drawer.clear(ColorRGBa.PINK) diff --git a/orx-dnk3/src/main/kotlin/Facet.kt b/orx-dnk3/src/main/kotlin/Facet.kt index b0d8d00a..f4519eab 100644 --- a/orx-dnk3/src/main/kotlin/Facet.kt +++ b/orx-dnk3/src/main/kotlin/Facet.kt @@ -26,7 +26,7 @@ abstract class ColorBufferFacetCombiner(facets: Set, 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) {