Add gltf animation support to orx-dnk3
This commit is contained in:
BIN
demo-data/gltf-models/box-animated/BoxAnimated.glb
Normal file
BIN
demo-data/gltf-models/box-animated/BoxAnimated.glb
Normal file
Binary file not shown.
@@ -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")
|
||||
|
||||
61
orx-dnk3/src/demo/kotlin/DemoAnimations01.kt
Normal file
61
orx-dnk3/src/demo/kotlin/DemoAnimations01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
"""
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?,
|
||||
@@ -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 {
|
||||
fun loadGltfFromFile(file: File): GltfFile = when (file.extension) {
|
||||
"gltf" -> {
|
||||
val gson = Gson()
|
||||
val json = file.readText()
|
||||
return gson.fromJson(json, GltfFile::class.java).apply {
|
||||
gson.fromJson(json, GltfFile::class.java).apply {
|
||||
this.file = file
|
||||
}
|
||||
}
|
||||
"glb" -> {
|
||||
loadGltfFromGlbFile(file)
|
||||
}
|
||||
else -> error("extension ${file.extension} not supported in ${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,12 +153,16 @@ 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)
|
||||
matrix?.let {
|
||||
node.transform = Matrix44.fromDoubleArray(it).transposed
|
||||
}
|
||||
|
||||
scale?.let {
|
||||
scale(it[0], it[1], it[2])
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -66,3 +66,4 @@ class KeyframerChannel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
orx-keyframer/src/main/kotlin/KeyQuaternion.kt
Normal file
64
orx-keyframer/src/main/kotlin/KeyQuaternion.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
63
orx-keyframer/src/main/kotlin/KeyVector3.kt
Normal file
63
orx-keyframer/src/main/kotlin/KeyVector3.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,6 @@ enum class KeyframerFormat {
|
||||
FULL
|
||||
}
|
||||
|
||||
|
||||
|
||||
open class Keyframer {
|
||||
private var currentTime = 0.0
|
||||
operator fun invoke(time: Double) {
|
||||
|
||||
Reference in New Issue
Block a user