Add gltf animation support to orx-dnk3

This commit is contained in:
Edwin Jakobs
2020-05-23 13:35:47 +02:00
parent acbfcabf60
commit 4f00cddaec
16 changed files with 416 additions and 75 deletions

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ abstract class ColorBufferFacetCombiner(facets: Set<FacetType>,
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;
"""
}

View File

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

View File

@@ -5,9 +5,16 @@ import org.openrndr.draw.DepthFormat
import org.openrndr.draw.RenderTarget
import org.openrndr.draw.renderTarget
class RenderPass(val combiners: List<FacetCombiner>, val renderOpaque: Boolean = true, val renderTransparent: Boolean = false)
class RenderPass(val combiners: List<FacetCombiner>,
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()))

View File

@@ -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<Entity> = mutableListOf()) {
open class SceneNode() {
var entities: MutableList<Entity> = mutableListOf()
var parent: SceneNode? = null
var transform = Matrix44.IDENTITY
open var transform = Matrix44.IDENTITY
var worldTransform = Matrix44.IDENTITY
val children = mutableListOf<SceneNode>()
var disposed = false

View File

@@ -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<NodeContent<Mesh>>,
instancedMeshes: List<NodeContent<InstancedMesh>>) {
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
}
}

View File

@@ -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<GltfChannel>, val samplers: List<GltfAnimationSampler>)
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<GltfBuffer>,
val images: List<GltfImage>?,
val textures: List<GltfTexture>?,
val samplers: List<GltfSampler>?
val samplers: List<GltfSampler>?,
val animations: List<GltfAnimation>?) {
@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
}
}
"glb" -> {
loadGltfFromGlbFile(file)
}
else -> error("extension ${file.extension} not supported in ${file}")
}

View File

@@ -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<AnimationChannel>) {
fun applyToTargets(input: Double) {
for (channel in channels) {
channel.applyToTarget(input)
}
}
}
sealed class AnimationChannel {
abstract fun applyToTarget(input: Double)
}
class QuaternionChannel(val target: KMutableProperty0<Quaternion>,
val keyframer: KeyframerChannelQuaternion) : AnimationChannel() {
override fun applyToTarget(input: Double) {
target.set(keyframer.value(input) ?: Quaternion.IDENTITY)
}
}
class Vector3Channel(val target: KMutableProperty0<Vector3>,
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<List<SceneNode>>, val animations: List<SceneAnimation>)
/** Tools to convert GltfFile into a DNK3 scene */
fun GltfFile.buildSceneNodes(): List<List<SceneNode>> {
fun GltfFile.buildSceneNodes(): GltfSceneData {
val sceneImages = mutableMapOf<GltfImage, ColorBuffer>()
fun GltfImage.createSceneImage(): ColorBuffer {
return sceneImages.getOrPut(this) {
@@ -43,6 +88,7 @@ fun GltfFile.buildSceneNodes(): List<List<SceneNode>> {
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<List<SceneNode>> {
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<List<SceneNode>> {
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<List<SceneNode>> {
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<List<SceneNode>> {
})
}
fun GltfNode.createSceneNode(): SceneNode {
val node = SceneNode()
val sceneNodes = mutableMapOf<GltfNode, SceneNode>()
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)

View File

@@ -66,3 +66,4 @@ class KeyframerChannel {
}
}
}

View File

@@ -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<KeyQuaternion>()
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
}
}
}

View File

@@ -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<KeyVector3>()
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)
}
}
}

View File

@@ -23,8 +23,6 @@ enum class KeyframerFormat {
FULL
}
open class Keyframer {
private var currentTime = 0.0
operator fun invoke(time: Double) {