[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm

This commit is contained in:
Edwin Jakobs
2021-06-27 21:32:24 +02:00
parent 5814acef8f
commit 874d49779f
159 changed files with 22 additions and 21 deletions

View File

@@ -0,0 +1,76 @@
# orx-dnk3
A scene graph based 3d renderer with support for Gltf based assets
Status: in development
Supported Gltf features
- [x] Scene hierarchy
- [x] Loading mesh data
- [x] Glb
- [ ] Materials
- [x] Basic materials
- [x] Normal maps
- [x] Metallic/roughness maps
- [x] Skinning
- [x] Double-sided materials
- [ ] Transparency
- [x] Animations
- [ ] Cameras
- [ ] Lights
<!-- __demos__ -->
## Demos
### DemoAnimations01
[source code](src/demo/kotlin/DemoAnimations01.kt)
![DemoAnimations01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoAnimations01Kt.png)
### DemoCamera01
[source code](src/demo/kotlin/DemoCamera01.kt)
![DemoCamera01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoCamera01Kt.png)
### DemoDSL01
[source code](src/demo/kotlin/DemoDSL01.kt)
![DemoDSL01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoDSL01Kt.png)
### DemoDSL02
[source code](src/demo/kotlin/DemoDSL02.kt)
![DemoDSL02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoDSL02Kt.png)
### DemoLights01
[source code](src/demo/kotlin/DemoLights01.kt)
![DemoLights01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoLights01Kt.png)
### DemoLights02
[source code](src/demo/kotlin/DemoLights02.kt)
![DemoLights02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoLights02Kt.png)
### DemoLights03
[source code](src/demo/kotlin/DemoLights03.kt)
![DemoLights03Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoLights03Kt.png)
### DemoObject01
[source code](src/demo/kotlin/DemoObject01.kt)
![DemoObject01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoObject01Kt.png)
### DemoScene01
[source code](src/demo/kotlin/DemoScene01.kt)
![DemoScene01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoScene01Kt.png)
### DemoScene02
[source code](src/demo/kotlin/DemoScene02.kt)
![DemoScene02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoScene02Kt.png)
### DemoScene03
[source code](src/demo/kotlin/DemoScene03.kt)
![DemoScene03Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-dnk3/images/DemoScene03Kt.png)

View File

@@ -0,0 +1,30 @@
sourceSets {
demo {
java {
srcDirs = ["src/demo/kotlin"]
compileClasspath += main.getCompileClasspath()
runtimeClasspath += main.getRuntimeClasspath()
}
}
}
dependencies {
implementation "com.google.code.gson:gson:$gsonVersion"
implementation(project(":orx-fx"))
implementation(project(":orx-jvm:orx-keyframer"))
implementation(project(":orx-easing"))
implementation(project(":orx-shader-phrases"))
implementation(project(":orx-mesh-generators"))
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-mesh-generators"))
demoImplementation(project(":orx-noise"))
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-ffmpeg:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-filter:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
demoImplementation(sourceSets.getByName("main").output)
}

View File

@@ -0,0 +1,59 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import org.openrndr.math.mod_
import org.openrndr.math.transforms.transform
import java.io.File
suspend 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_(sceneData.animations[0].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,49 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.math.*
import java.io.File
suspend 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/camera/Scene.glb"))
val scene = Scene(SceneNode())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa(0.1, 0.1, 0.4)
downColor = ColorRGBa(0.1, 0.0, 0.0)
})
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = dryRenderer()
val cameras = scene.root.findContent { this as? PerspectiveCamera }
extend {
sceneData.animations[0].applyToTargets(seconds.mod_(sceneData.animations[0].duration))
drawer.view = cameras[0].content.viewMatrix
drawer.projection = cameras[0].content.projectionMatrix
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,86 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.dsl.*
import org.openrndr.extra.dnk3.renderers.dryRenderer
import org.openrndr.extra.dnk3.tools.addSkybox
import org.openrndr.extras.camera.Orbital
import org.openrndr.extras.meshgenerators.boxMesh
import org.openrndr.extras.meshgenerators.groundPlaneMesh
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
suspend fun main() = application {
configure {
width = 1280
height = 720
}
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) {
eye = Vector3(4.0, 4.0, 4.0)
}
val renderer = dryRenderer()
val scene = scene {
addSkybox("file:demo-data/cubemaps/garage_iem.dds")
root.hemisphereLight {
upColor = ColorRGBa.WHITE.shade(0.1)
downColor = ColorRGBa.BLACK
}
root.node {
transform = transform {
translate(0.0, 2.0, 0.0)
}
pointLight {
constantAttenuation = 0.0
quadraticAttenuation = 1.0
}
}
root.node {
simpleMesh {
vertexBuffer = groundPlaneMesh(100.0, 100.0)
material = pbrMaterial {
color = ColorRGBa.GREEN
}
}
}
for (j in -3..3) {
for (i in -3..3) {
root.node {
transform = transform {
translate(i * 2.0, 1.0, j * 2.0)
}
update {
transform = transform {
translate(i * 2.0, 1.0, j * 2.0)
rotate(Vector3.UNIT_Z, seconds* 45.0 + i * 20.0 + j * 50.0)
}
}
simpleMesh {
vertexBuffer = boxMesh()
material = pbrMaterial {
color = ColorRGBa.WHITE
}
}
}
}
}
}
extend {
drawer.clear(ColorRGBa.BLACK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,87 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.dsl.*
import org.openrndr.extra.dnk3.renderers.dryRenderer
import org.openrndr.extra.dnk3.tools.addSkybox
import org.openrndr.extra.noise.simplex
import org.openrndr.extras.camera.Orbital
import org.openrndr.extras.meshgenerators.groundPlaneMesh
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import org.openrndr.shape.path3D
suspend fun main() = application {
configure {
width = 1280
height = 720
}
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) {
eye = Vector3(4.0, 4.0, 4.0)
}
val renderer = dryRenderer()
val scene = scene {
addSkybox("file:demo-data/cubemaps/garage_iem.dds")
root.hemisphereLight {
upColor = ColorRGBa.WHITE.shade(0.1)
downColor = ColorRGBa.BLACK
}
root.node {
transform = transform {
translate(0.0, 2.0, 0.0)
}
pointLight {
constantAttenuation = 0.0
quadraticAttenuation = 1.0
}
}
root.node {
simpleMesh {
vertexBuffer = groundPlaneMesh(100.0, 100.0)
material = pbrMaterial {
color = ColorRGBa.GREEN
}
}
}
root.node {
pathMesh {
weight = 10.0
material = pbrMaterial {
color = ColorRGBa.PINK
}
update {
paths = mutableListOf(
path3D {
val t = seconds * 0.1
moveTo(Vector3.ZERO)
val control = Vector3.simplex(3032, t).let { it.copy(y = it.y * 0.5 + 0.5) } * 4.0
val target = Vector3.simplex(5077, t).let { it.copy(y = it.y * 0.5 + 0.5) } * 4.0
val end = Vector3.simplex(9041, t).let { it.copy(y = it.y * 0.5 + 0.5) } * 4.0
curveTo(control, target)
continueTo(end)
}
)
}
}
}
}
extend {
drawer.clear(ColorRGBa.BLACK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,115 @@
import kotlinx.coroutines.yield
import org.openrndr.*
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.BufferMultisample
import org.openrndr.draw.ColorFormat
import org.openrndr.draw.ColorType
import org.openrndr.draw.DrawPrimitive
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.features.IrradianceSH
import org.openrndr.extra.dnk3.features.addIrradianceSH
import org.openrndr.extra.dnk3.gltf.buildSceneNodes
import org.openrndr.extra.dnk3.gltf.loadGltfFromFile
import org.openrndr.extra.dnk3.post.ScreenspaceReflections
import org.openrndr.extra.dnk3.post.VolumetricIrradiance
import org.openrndr.extra.dnk3.renderers.postRenderer
import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases
import org.openrndr.extras.camera.Orbital
import org.openrndr.extras.meshgenerators.sphereMesh
import org.openrndr.ffmpeg.ScreenRecorder
import org.openrndr.filter.color.Delinearize
import org.openrndr.math.Matrix44
import org.openrndr.math.Spherical
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.scale
import org.openrndr.math.transforms.transform
import org.openrndr.math.transforms.translate
import java.io.File
import kotlin.math.cos
import kotlin.math.sin
suspend 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/irradiance-probes/model.glb"))
val scene = Scene(SceneNode())
val probeBox = sphereMesh(16, 16, 0.1)
val probeGeometry = Geometry(listOf(probeBox), null, DrawPrimitive.TRIANGLES, 0, probeBox.vertexCount)
val c = 5
scene.addIrradianceSH(c, c, c, 3.0 / c, cubemapSize = 32, offset = Vector3(0.0, 0.0, 0.0))
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = postRenderer()
// renderer.postSteps.add(
// FilterPostStep(1.0, ScreenspaceReflections(), listOf("color", "clipDepth", "viewNormal"), "reflections", ColorFormat.RGB, ColorType.FLOAT16) {
// val p = Matrix44.scale(drawer.width / 2.0, drawer.height / 2.0, 1.0) * Matrix44.translate(Vector3(1.0, 1.0, 0.0)) * drawer.projection
// this.projection = p
// this.projectionMatrixInverse = drawer.projection.inversed
// }
// )
// renderer.postSteps.add(
// FilterPostStep(1.0, VolumetricIrradiance(), listOf("color", "clipDepth"), "volumetric-irradiance", ColorFormat.RGB, ColorType.FLOAT16) {
// this.irradianceSH = scene.features[0] as IrradianceSH
// this.projectionMatrixInverse = drawer.projection.inversed
// this.viewMatrixInverse = drawer.view.inversed
// }
// )
renderer.postSteps.add(
FilterPostStep(1.0, Delinearize(), listOf("color"), "ldr", ColorFormat.RGB, ColorType.FLOAT16)
)
val orb = extend(Orbital()) {
this.fov = 20.0
camera.setView(Vector3(-0.49, -0.24, 0.20), Spherical(26.56, 90.0, 6.533), 40.0)
}
renderer.draw(drawer, scene)
val dynNode = SceneNode()
val dynMaterial = PBRMaterial()
val dynPrimitive = MeshPrimitive(probeGeometry, dynMaterial)
val dynMesh = Mesh(listOf(dynPrimitive))
dynNode.entities.add(dynMesh)
scene.root.children.add(dynNode)
scene.dispatcher.launch {
while (true) {
dynNode.transform = transform {
translate(cos(seconds) * 0.5, 0.5, sin(seconds) * 0.5)
scale(2.0)
}
yield()
}
}
extend {
drawer.clear(ColorRGBa.BLACK)
renderer.draw(drawer, scene)
drawer.defaults()
}
}
}

View File

@@ -0,0 +1,49 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.*
import java.io.File
suspend 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/point-light/Scene.glb"))
val scene = Scene(SceneNode())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa(0.1, 0.1, 0.4)
downColor = ColorRGBa(0.1, 0.0, 0.0)
})
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = dryRenderer()
val orb = extend(Orbital()) {
far = 50.0
camera.setView(Vector3.ZERO, Spherical(30.50, 26.0, 5.6), 40.0)
}
extend {
sceneData.animations[0].applyToTargets(seconds.mod_(sceneData.animations[0].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,50 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.*
import java.io.File
suspend 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/spot-light/Scene.glb"))
val scene = Scene(SceneNode())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa(0.1, 0.1, 0.4)
downColor = ColorRGBa(0.1, 0.0, 0.0)
})
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = dryRenderer()
val orb = extend(Orbital()) {
far = 50.0
camera.setView(Vector3(-0.514, -0.936, -1.122), Spherical(454.346, 25.0, 8.444), 40.0)
}
extend {
sceneData.animations[0].applyToTargets(seconds.mod_(sceneData.animations[0].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,49 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.*
import java.io.File
suspend 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/directional-light/Scene.glb"))
val scene = Scene(SceneNode())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa(0.1, 0.1, 0.4)
downColor = ColorRGBa(0.1, 0.0, 0.0)
})
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = dryRenderer()
val orb = extend(Orbital()) {
camera.setView(Vector3(-0.49, -0.24, 0.20), Spherical(26.56, 90.0, 6.533), 40.0)
}
extend {
sceneData.animations[0].applyToTargets(seconds.mod_(sceneData.animations[0].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,47 @@
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.gltf.loadGltfFromFile
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import java.io.File
suspend fun main() = application {
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gltf = loadGltfFromFile(File("demo-data/gltf-models/duck/Duck.gltf"))
val meshes = gltf.meshes.map {
it.createDrawCommands(gltf)
}
extend(Orbital()) {
far = 400.0
lookAt = Vector3(0.0, 50.0, 0.0)
eye = Vector3(100.0, 200.0, 150.0)
fov = 45.0
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb = vec3(v_viewNormal.z);
""".trimIndent()
}
for (mesh in meshes) {
for (primitive in mesh) {
if (primitive.indexBuffer == null) {
drawer.vertexBuffer(primitive.vertexBuffer, DrawPrimitive.TRIANGLES)
} else {
drawer.vertexBuffer(primitive.indexBuffer!!, listOf(primitive.vertexBuffer), DrawPrimitive.TRIANGLES)
}
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import java.io.File
suspend 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/suzanne/Suzanne.gltf"))
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)
scene.root.children.addAll(gltf.buildSceneNodes().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 {
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,58 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import java.io.File
suspend fun main() = application {
configure {
width = 1280
height = 720
}
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gltf = loadGltfFromFile(File("demo-data/gltf-models/duck/Duck.gltf"))
val scene = Scene(SceneNode())
// -- add some lights
val lightNode = SceneNode()
lightNode.transform = transform {
translate(0.0, 10.0, 0.0)
rotate(Vector3.UNIT_X, -90.0)
}
lightNode.entities.add(DirectionalLight())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa.WHITE.shade(1.0)
downColor = ColorRGBa.WHITE.shade(0.1)
})
scene.root.children.add(lightNode)
scene.root.children.addAll(gltf.buildSceneNodes().scenes.first())
// -- create a renderer
val renderer = dryRenderer()
extend(Orbital()) {
far = 500.0
lookAt = Vector3(0.0, 0.8, 0.0)
eye = Vector3(3.0, 0.8, -2.0)
fov = 30.0
}
extend {
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,60 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.extras.meshgenerators.sphereMesh
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
suspend 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 root = SceneNode()
val scene = Scene(root)
val lightNode = SceneNode()
lightNode.transform = transform {
translate(0.0, 10.0, 0.0)
}
lightNode.entities.add(PointLight())
lightNode.entities.add(HemisphereLight(upColor = ColorRGBa.PINK, downColor = ColorRGBa(0.1,0.1,0.1)))
scene.root.children.add(lightNode)
val meshNode = SceneNode()
val box = sphereMesh(32, 32)
val geometry = Geometry(listOf(box), null, DrawPrimitive.TRIANGLES, 0, box.vertexCount)
val material = PBRMaterial()
val primitive = MeshPrimitive(geometry, material)
val mesh = Mesh(listOf(primitive))
meshNode.entities.add(mesh)
root.children.add(meshNode)
// -- create a renderer
val renderer = dryRenderer()
extend(Orbital()) {
far = 500.0
lookAt = Vector3(0.0, 0.0, 0.0)
eye = Vector3(3.0, 2.0, -3.0)
fov = 30.0
}
extend {
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,49 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.BufferMultisample
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.extra.dnk3.renderers.segmentContourRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import org.openrndr.math.mod_
import java.io.File
suspend 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/fox/Fox.glb"))
val scene = Scene(SceneNode())
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer, try it with BufferMultisample.SampleCount(8) for better results
val renderer = segmentContourRenderer(BufferMultisample.Disabled)
extend(Orbital()) {
far = 500.0
lookAt = Vector3(0.0, 40.0, 0.0)
eye = Vector3(150.0, 40.0, 200.0)
fov = 40.0
}
extend {
sceneData.animations[2].applyToTargets(seconds.mod_(sceneData.animations[2].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,53 @@
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.extra.dnk3.renderers.dryRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.math.Vector3
import org.openrndr.math.mod_
import java.io.File
suspend 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/fox/Fox.glb"))
val scene = Scene(SceneNode())
scene.root.entities.add(HemisphereLight().apply {
upColor = ColorRGBa.WHITE.shade(0.4)
downColor = ColorRGBa.GRAY.shade(0.1)
})
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = dryRenderer()
extend(Orbital()) {
far = 500.0
lookAt = Vector3(0.0, 40.0, 0.0)
eye = Vector3(150.0, 40.0, 200.0)
fov = 40.0
}
extend {
sceneData.animations[2].applyToTargets(seconds.mod_(sceneData.animations[2].duration))
drawer.clear(ColorRGBa.PINK)
renderer.draw(drawer, scene)
}
}
}

View File

@@ -0,0 +1,114 @@
import kotlinx.coroutines.yield
import org.openrndr.*
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.features.addVoxelConeTracing
import org.openrndr.extra.dnk3.gltf.buildSceneNodes
import org.openrndr.extra.dnk3.gltf.loadGltfFromFile
import org.openrndr.extra.dnk3.renderers.postRenderer
import org.openrndr.extras.camera.Orbital
import org.openrndr.extras.meshgenerators.sphereMesh
import org.openrndr.filter.color.Delinearize
import org.openrndr.math.Spherical
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import java.io.File
import kotlin.math.cos
import kotlin.math.sin
suspend 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/irradiance-probes/model.glb"))
val scene = Scene(SceneNode())
val probeBox = sphereMesh(16, 16, 0.1)
val probeGeometry = Geometry(listOf(probeBox), null, DrawPrimitive.TRIANGLES, 0, probeBox.vertexCount)
val c = 5
// scene.addIrradianceSH(c, c, c, 3.0 / c, cubemapSize = 32, offset = Vector3(0.0, 0.0, 0.0))
val vctFeature = scene.addVoxelConeTracing(64,64,64, 0.1)
val sceneData = gltf.buildSceneNodes()
scene.root.children.addAll(sceneData.scenes.first())
// -- create a renderer
val renderer = postRenderer()
// renderer.postSteps.add(
// FilterPostStep(1.0, ScreenspaceReflections(), listOf("color", "clipDepth", "viewNormal"), "reflections", ColorFormat.RGB, ColorType.FLOAT16) {
// val p = Matrix44.scale(drawer.width / 2.0, drawer.height / 2.0, 1.0) * Matrix44.translate(Vector3(1.0, 1.0, 0.0)) * drawer.projection
// this.projection = p
// this.projectionMatrixInverse = drawer.projection.inversed
// }
// )
// renderer.postSteps.add(
// FilterPostStep(1.0, VolumetricIrradiance(), listOf("color", "clipDepth"), "volumetric-irradiance", ColorFormat.RGB, ColorType.FLOAT16) {
// this.irradianceSH = scene.features[0] as IrradianceSH
// this.projectionMatrixInverse = drawer.projection.inversed
// this.viewMatrixInverse = drawer.view.inversed
// }
// )
renderer.postSteps.add(
FilterPostStep(1.0, Delinearize(), listOf("color"), "ldr", ColorFormat.RGB, ColorType.FLOAT16)
)
val orb = extend(Orbital()) {
this.fov = 20.0
camera.setView(Vector3(-0.49, -0.24, 0.20), Spherical(26.56, 90.0, 6.533), 40.0)
}
renderer.draw(drawer, scene)
val dynNode = SceneNode()
val dynMaterial = PBRMaterial()
val dynPrimitive = MeshPrimitive(probeGeometry, dynMaterial)
val dynMesh = Mesh(listOf(dynPrimitive))
dynNode.entities.add(dynMesh)
scene.root.children.add(dynNode)
scene.dispatcher.launch {
while (true) {
dynNode.transform = transform {
translate(cos(seconds) * 0.5, 0.5, sin(seconds) * 0.5)
scale(2.0)
}
yield()
}
}
val viz = colorBuffer(64,64)
extend {
drawer.clear(ColorRGBa.BLACK)
renderer.draw(drawer, scene)
drawer.defaults()
for (i in 0 until 128) {
vctFeature.voxelMap?.let {
it.copyTo(viz, i)
}
drawer.image(viz, (i * 128) % width + 0.0, ((i * 128)/width * 128 + 0.0 ))
}
drawer.image(vctFeature.voxelRenderTarget!!.colorBuffer(0))
}
}
}

View File

@@ -0,0 +1,47 @@
package org.openrndr.extra.dnk3
import org.openrndr.math.Matrix44
import org.openrndr.math.transforms.ortho
import org.openrndr.math.transforms.perspective
class PerspectiveCamera(var node: SceneNode) : Camera() {
override val projectionMatrix: Matrix44
get() = perspective(fov, aspectRatio, near, far)
override val viewMatrix: Matrix44
get() = node.worldTransform.inversed
var aspectRatio: Double = 16.0 / 9.0
var fov = 45.0
var far = 100.0
var near = 0.1
override fun hashCode(): Int {
var result = aspectRatio.hashCode()
result = 31 * result + fov.hashCode()
result = 31 * result + far.hashCode()
result = 31 * result + near.hashCode()
return result
}
}
class OrthographicCamera(var node: SceneNode) : Camera() {
override val projectionMatrix: Matrix44
get() = ortho(xMag, yMag, near, far)
override val viewMatrix: Matrix44
get() = node.worldTransform.inversed
var xMag = 1.0
var yMag = 1.0
var near = 0.1
var far = 100.0
override fun hashCode(): Int {
var result = xMag.hashCode()
result = 31 * result + yMag.hashCode()
result = 31 * result + near.hashCode()
result = 31 * result + far.hashCode()
return result
}
}

View File

@@ -0,0 +1,103 @@
package org.openrndr.extra.dnk3
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.IndexBuffer
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Matrix44
import org.openrndr.math.transforms.perspective
import org.openrndr.shape.Path3D
class Geometry(val vertexBuffers: List<VertexBuffer>,
val indexBuffer: IndexBuffer?,
val primitive: DrawPrimitive,
val offset: Int,
val vertexCount: Int) {
override fun toString(): String {
return "Geometry(vertexBuffers: $vertexBuffers, indexBuffers: $indexBuffer, primitive: $primitive, offset: $offset, vertexCount: $vertexCount)"
}
override fun hashCode(): Int {
var result = 0
result = 31 * result + primitive.ordinal.hashCode()
result = 31 * result + offset.hashCode()
result = 31 * result + vertexCount.hashCode()
return result
}
}
val DummyGeometry = Geometry(emptyList(), null, DrawPrimitive.TRIANGLES, 0, 0)
sealed class Entity
class MeshPrimitive(var geometry: Geometry, var material: Material) {
override fun toString(): String {
return "MeshPrimitive(geometry: $geometry, material: $material)"
}
override fun hashCode(): Int {
var result = geometry.hashCode()
result = 31 * result + material.hashCode()
return result
}
}
class MeshPrimitiveInstance(val primitive: MeshPrimitive, val instances: Int, val attributes: List<VertexBuffer>)
class PathMesh(var paths: MutableList<Path3D>, var material: Material, var weight: Double) : Entity() {
override fun toString(): String {
return "PathMesh(paths=$paths)"
}
override fun hashCode(): Int {
return paths.hashCode()
}
}
abstract class MeshBase(var primitives: List<MeshPrimitive>) : Entity()
class Mesh(primitives: List<MeshPrimitive>) : MeshBase(primitives) {
override fun toString(): String {
return "Mesh(primitives: $primitives)"
}
override fun hashCode(): Int {
return primitives.hashCode()
}
}
class SkinnedMesh(primitives: List<MeshPrimitive>,
val joints: List<SceneNode>,
val skeleton: SceneNode,
val inverseBindMatrices: List<Matrix44>
) : MeshBase(primitives)
class InstancedMesh(primitives: List<MeshPrimitive>,
var instances: Int,
var attributes: List<VertexBuffer>) : MeshBase(primitives)
data class Fog(var color: ColorRGBa = ColorRGBa.WHITE, var end: Double = 100.0) : Entity()
abstract class Light : Entity() {
var color: ColorRGBa = ColorRGBa.WHITE
}
abstract class Camera : Entity() {
abstract val projectionMatrix: Matrix44
abstract val viewMatrix: Matrix44
}
abstract class CubemapProbe : Entity() {
open val projectionMatrix: Matrix44
get() {
return perspective(90.0, 1.0, 0.1, 150.0)
}
var dirty = true
}
class IrradianceProbe : CubemapProbe() {
override fun hashCode(): Int {
return true.hashCode()
}
}

View File

@@ -0,0 +1,158 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.BlendMode
import org.openrndr.draw.ColorFormat
import org.openrndr.draw.ColorType
enum class FacetType(val shaderFacet: String) {
WORLD_POSITION("f_worldPosition"),
VIEW_POSITION("f_viewPosition"),
CLIP_POSITION("f_clipPosition"),
WORLD_NORMAL("f_worldNormal"),
VIEW_NORMAL("f_viewNormal"),
SPECULAR("f_specular"),
DIFFUSE("f_diffuse"),
EMISSIVE("f_emission"),
AMBIENT("f_ambient"),
OCCLUSION("f_occlusion"),
FRAGMENT_ID("f_fragmentID"),
COLOR("m_color"),
}
abstract class FacetCombiner(val facets: Set<FacetType>, val targetOutput: String) {
abstract fun generateShader(): String
override fun toString(): String {
return "FacetCombiner(facets=$facets, targetOutput='$targetOutput')"
}
}
abstract class ColorBufferFacetCombiner(facets: Set<FacetType>,
targetOutput: String,
val format: ColorFormat,
val type: ColorType,
val blendMode: BlendMode = BlendMode.BLEND) : FacetCombiner(facets, targetOutput) {
}
class MomentsFacet : ColorBufferFacetCombiner(setOf(FacetType.WORLD_POSITION), "moments", ColorFormat.RG, ColorType.FLOAT16) {
override fun generateShader(): String {
return """
float depth = length(v_viewPosition);
float dx = dFdx(depth);
float dy = dFdy(depth);
o_$targetOutput = vec4(depth, depth*depth + 0.25 * dx*dx+dy*dy, 0.0, 1.0);
"""
}
}
class DiffuseSpecularFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR),
"diffuseSpecular", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4( max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0), f_specular.rgb), 1.0);"
}
class DiffuseSpecularAlphaFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR),
"diffuseSpecular", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4( (max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0), f_specular.rgb)) * f_alpha, f_alpha);"
}
class AmbientOcclusionFacet : ColorBufferFacetCombiner(setOf(FacetType.AMBIENT, FacetType.OCCLUSION),
"ambientOcclusion", ColorFormat.RGBa, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4(f_ambient, f_occlusion);"
}
class MaterialFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE),
"material", ColorFormat.RGBa, ColorType.UINT8) {
override fun generateShader(): String =
"o_$targetOutput = vec4(m_metalness, m_roughness, 0.0, 1.0);"
}
class BaseColorFacet : ColorBufferFacetCombiner(setOf(FacetType.COLOR),
"baseColor", ColorFormat.RGB, ColorType.UINT8) {
override fun generateShader(): String = "o_$targetOutput = vec4(m_color.rgb, 1.0);"
}
class DiffuseFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE),
"diffuse", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4( max(vec3(0.0), f_diffuse.rgb), 1.0 );"
}
class SpecularFacet : ColorBufferFacetCombiner(setOf(FacetType.SPECULAR),
"diffuseSpecular", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4( max(vec3(0.0), f_specular.rgb), 1.0);"
}
class EmissiveFacet: ColorBufferFacetCombiner(setOf(FacetType.EMISSIVE),
"emissive", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String =
"o_$targetOutput = vec4(f_emission, 1.0);"
}
class EmissiveAlphaFacet: ColorBufferFacetCombiner(setOf(FacetType.EMISSIVE),
"emissive", ColorFormat.RGB, ColorType.FLOAT16, BlendMode.OVER) {
override fun generateShader(): String =
"o_$targetOutput = vec4(f_emission, f_alpha);"
}
class PositionFacet : ColorBufferFacetCombiner(setOf(FacetType.WORLD_POSITION), "position", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String = "o_$targetOutput = vec4(v_worldPosition.rgb, 1.0);"
}
class NormalFacet : ColorBufferFacetCombiner(setOf(FacetType.WORLD_NORMAL), "normal", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String = "o_$targetOutput = vec4(v_worldNormal.rgb, 1.0);"
}
class ViewDepthFacet : ColorBufferFacetCombiner(setOf(FacetType.VIEW_POSITION), "viewDepth", ColorFormat.R, ColorType.FLOAT16) {
override fun generateShader(): String = "o_$targetOutput.r = v_viewPosition.z;"
}
class ClipDepthFacet : ColorBufferFacetCombiner(setOf(FacetType.CLIP_POSITION), "clipDepth", ColorFormat.R, ColorType.FLOAT32) {
override fun generateShader(): String = "o_$targetOutput.r = gl_FragCoord.z;"
}
class ViewPositionFacet : ColorBufferFacetCombiner(setOf(FacetType.VIEW_POSITION), "viewPosition", ColorFormat.RGB, ColorType.FLOAT32) {
override fun generateShader(): String = "o_$targetOutput.rgb = v_viewPosition.rgb;"
}
class ViewNormalFacet : ColorBufferFacetCombiner(setOf(FacetType.VIEW_NORMAL), "viewNormal", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader(): String = "o_$targetOutput.rgb = normalize( (u_viewNormalMatrix * vec4(f_worldNormal,0.0)).xyz );"
}
class ClipPositionFacet : ColorBufferFacetCombiner(setOf(FacetType.CLIP_POSITION), "position", ColorFormat.RGB, ColorType.FLOAT16) {
override fun generateShader() = "o_$targetOutput.rgb = gl_FragCoord.xyz;"
}
class FragmentIDFacet: ColorBufferFacetCombiner(setOf(FacetType.FRAGMENT_ID), "fragmentID", ColorFormat.R, ColorType.UINT16_INT) {
override fun generateShader(): String {
return "o_$targetOutput = f_fragmentID;"
}
}
class LDRColorFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR, FacetType.EMISSIVE), "color", ColorFormat.RGBa, ColorType.UINT8) {
override fun generateShader() = """
vec3 finalColor = (max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0),f_specular.rgb) + max(vec3(0.0), f_emission.rgb) + max(vec3(0.0), f_ambient.rgb)) * (1.0 - f_fog.a) + f_fog.rgb * f_fog.a;
o_$targetOutput = pow(vec4(finalColor.rgb, 1.0), vec4(1.0/2.2));
o_$targetOutput *= m_color.a;
"""
}
class HDRColorFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR, FacetType.EMISSIVE), "color", ColorFormat.RGBa, ColorType.FLOAT16) {
override fun generateShader() = """
vec3 finalColor = (max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0),f_specular.rgb) + max(vec3(0.0), f_emission.rgb) + max(vec3(0.0), f_ambient.rgb)) * (1.0 - f_fog.a) + f_fog.rgb * f_fog.a;
o_$targetOutput = vec4(finalColor.rgb, 1.0);
o_$targetOutput *= m_color.a;
"""
}
class DiffuseIrradianceFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR), "color", ColorFormat.RGBa, ColorType.UINT8) {
override fun generateShader() = """
vec3 finalColor = (max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0), f_emission.rgb));
o_$targetOutput = vec4(finalColor.rgb, 1.0);
"""
}

View File

@@ -0,0 +1,13 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.Drawer
interface Feature {
fun <T : Feature> update(
drawer: Drawer,
sceneRenderer: SceneRenderer,
scene: Scene,
feature: T,
context: RenderContext
)
}

View File

@@ -0,0 +1,82 @@
package org.openrndr.extra.dnk3
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.Cubemap
import org.openrndr.draw.RenderTarget
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.ortho
import org.openrndr.math.transforms.perspective
data class LightContext(val lights: List<NodeContent<Light>>,
val shadowMaps: Map<ShadowLight, RenderTarget>)
interface AttenuatedLight {
var constantAttenuation: Double
var linearAttenuation: Double
var quadraticAttenuation: Double
}
class DirectionalLight(var direction: Vector3 = -Vector3.UNIT_Z, override var shadows: Shadows = Shadows.None) : Light(), ShadowLight {
var projectionSize = 50.0
override fun projection(renderTarget: RenderTarget): Matrix44 {
return ortho(-projectionSize / 2.0, projectionSize / 2.0, -projectionSize / 2.0, projectionSize / 2.0, 1.0, 150.0)
}
override fun hashCode(): Int {
return color.hashCode()
}
}
class SpotLight(var direction: Vector3 = -Vector3.UNIT_Z, var innerAngle: Double = 45.0, var outerAngle: Double = 90.0) : Light(), ShadowLight, AttenuatedLight {
override var constantAttenuation = 1.0
override var linearAttenuation = 0.0
override var quadraticAttenuation = 0.0
override var shadows: Shadows = Shadows.None
override fun projection(renderTarget: RenderTarget): Matrix44 {
return perspective(outerAngle * 2.0, renderTarget.width * 1.0 / renderTarget.height, 1.0, 150.0)
}
override fun hashCode(): Int {
var result = direction.hashCode()
result = 31 * result + innerAngle.hashCode()
result = 31 * result + outerAngle.hashCode()
result = 31 * result + constantAttenuation.hashCode()
result = 31 * result + linearAttenuation.hashCode()
result = 31 * result + quadraticAttenuation.hashCode()
return result
}
}
class HemisphereLight(var direction: Vector3 = Vector3.UNIT_Y,
var upColor: ColorRGBa = ColorRGBa.WHITE,
var downColor: ColorRGBa = ColorRGBa.BLACK) : Light() {
var irradianceMap: Cubemap? = null
override fun hashCode(): Int {
var result = direction.hashCode()
result = 31 * result + upColor.hashCode()
result = 31 * result + downColor.hashCode()
return result
}
}
class PointLight(var constantAttenuation: Double = 1.0,
var linearAttenuation: Double = 0.0,
var quadraticAttenuation: Double = 1.0) : Light() {
override fun hashCode(): Int {
var result = constantAttenuation.hashCode()
result = 31 * result + linearAttenuation.hashCode()
result = 31 * result + quadraticAttenuation.hashCode()
result = 31 * result + color.hashCode()
return result
}
}
class AmbientLight : Light() {
override fun hashCode(): Int {
return color.hashCode()
}
}

View File

@@ -0,0 +1,59 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.features.IrradianceSH
import org.openrndr.math.Vector3
interface Material {
val name: String?
var doubleSided: Boolean
var transparent: Boolean
val fragmentID: Int
fun generateShadeStyle(context: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle
fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle)
}
class DummyMaterial : Material {
override var name: String? = null
override var doubleSided: Boolean = true
override var transparent: Boolean = false
override var fragmentID = 0
override fun generateShadeStyle(context: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle {
return shadeStyle {
fragmentPreamble = """
int f_fragmentID = p_fragmentID;
""".trimIndent()
fragmentTransform = """
x_fill.rgb = vec3(normalize(v_viewNormal).z);
""".trimIndent()
parameter("fragmentID", fragmentID)
}
}
override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) {
}
}
data class MaterialContext(val pass: RenderPass,
val lights: List<NodeContent<Light>>,
val fogs: List<NodeContent<Fog>>,
val shadowMaps: Map<ShadowLight, RenderTarget>,
val meshCubemaps: Map<Mesh, Cubemap>,
val irradianceProbeCount: Int
) {
var irradianceSH: IrradianceSH? = null
}
data class PrimitiveContext(val hasNormalAttribute: Boolean, val hasSkinning: Boolean)
data class ContextKey(val materialContext: MaterialContext, val primitiveContext: PrimitiveContext)

View File

@@ -0,0 +1,727 @@
package org.openrndr.extra.dnk3
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.cubemap.glslEvaluateSH
import org.openrndr.extra.dnk3.cubemap.glslFetchSH
import org.openrndr.extra.dnk3.cubemap.genGlslGatherSH
import org.openrndr.extra.shaderphrases.phrases.phraseTbnMatrix
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.normalMatrix
import java.nio.ByteBuffer
import kotlin.math.cos
private val noise128 by lazy {
val cb = colorBuffer(128, 128)
val items = cb.width * cb.height * cb.format.componentCount
val buffer = ByteBuffer.allocateDirect(items)
for (y in 0 until cb.height) {
for (x in 0 until cb.width) {
for (i in 0 until 4)
buffer.put((Math.random() * 255).toInt().toByte())
}
}
buffer.rewind()
cb.write(buffer)
cb.generateMipmaps()
cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR)
cb.wrapU = WrapMode.REPEAT
cb.wrapV = WrapMode.REPEAT
cb
}
private fun PointLight.fs(index: Int, hasNormalAttribute: Boolean): String = """
|{
| vec3 Lr = p_lightPosition$index - v_worldPosition;
| float distance = length(Lr);
| float attenuation = 1.0 / (p_lightConstantAttenuation$index +
| p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance);
| vec3 L = normalize(Lr);
|
| float side = ${if (hasNormalAttribute) "dot(L, N)" else "3.1415"};
| f_diffuse += attenuation * max(0, side / 3.1415) * p_lightColor$index.rgb * m_color.rgb;
| f_specular += attenuation * ggx(N, V, L, m_roughness, m_f0) * p_lightColor$index.rgb * m_color.rgb;
}
""".trimMargin()
private fun AmbientLight.fs(index: Int): String = "f_ambient += p_lightColor$index.rgb * ((1.0 - m_metalness) * m_color.rgb);"
private fun DirectionalLight.fs(index: Int, hasNormalAttribute: Boolean) = """
|{
| vec3 L = normalize(-p_lightDirection$index);
| float attenuation = 1.0;
| vec3 H = normalize(V + L);
| float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"};
| float LoH = clamp(dot(L, H), 0.0, 1.0);
| float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else "1"};
| vec3 Lr = (p_lightPosition$index - v_worldPosition);
//| 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 * 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.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH);
| vec3 Fr = (Dg * Vs) * F;
| f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb * m_ambientOcclusion;;
|}
""".trimMargin()
private fun HemisphereLight.fs(index: Int, hasNormalAttribute: Boolean): String = """
|{
| float f = ${if (hasNormalAttribute) "dot(N, p_lightDirection$index) * 0.5 + 0.5" else "1"};
| 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;
|}
""".trimMargin()
private fun SpotLight.fs(index: Int, hasNormalAttribute: Boolean): String {
val shadows = shadows
return """
|{
| vec3 Lr = p_lightPosition$index - v_worldPosition;
| float distance = length(Lr);
| float attenuation = 1.0 / (p_lightConstantAttenuation$index +
| p_lightLinearAttenuation$index * distance + p_lightQuadraticAttenuation$index * distance * distance);
| attenuation = 1.0;
| vec3 L = normalize(Lr);
| float NoL = ${if (hasNormalAttribute) "clamp(dot(N, L), 0.0, 1.0)" else "1"};
| float side = dot(L, N);
| float hit = max(dot(-L, p_lightDirection$index), 0.0);
| float falloff = clamp((hit - p_lightOuterCos$index) / (p_lightInnerCos$index - p_lightOuterCos$index), 0.0, 1.0);
| attenuation *= falloff;
| ${shadows.fs(index)}
| {
| vec3 H = normalize(V + L);
| float LoH = clamp(dot(L, H), 0.0, 1.0);
| float NoH = ${if (hasNormalAttribute) "clamp(dot(N, H), 0.0, 1.0)" else 1.0};
| 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.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), LoH);
| vec3 Fr = (Dg * Vs) * F;
| f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb;
| }
}
""".trimMargin()
}
private fun Fog.fs(index: Int): String = """
|{
| float dz = min(1.0, -v_viewPosition.z/p_fogEnd$index);
| f_fog = vec4(p_fogColor$index.rgb, dz);
|}
""".trimMargin()
sealed class TextureSource
object DummySource : TextureSource() {
override fun toString(): String {
return "DummySource()"
}
}
abstract class TextureFromColorBuffer(var texture: ColorBuffer, var textureFunction: TextureFunction) : TextureSource()
class TextureFromCode(val code: String) : TextureSource() {
override fun hashCode(): Int {
return code.hashCode()
}
}
private fun TextureFromCode.fs(index: Int, target: TextureTarget) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0);
|{
|vec4 texOut;
|$code;
|tex$index = texOut;
|}
"""
enum class TextureFunction(val function: (String, String) -> String) {
TILING({ texture, uv -> "texture($texture, $uv)" }),
NOT_TILING({ texture, uv -> "textureNoTile(p_textureNoise, $texture, x_noTileOffset, $uv)" })
;
}
/**
* @param texture the texture to sample from
* @param input input coordinates, default is "va_texCoord0.xy"
* @param textureFunction the texture function to use, default is TextureFunction.TILING
* @param pre the pre-fetch shader code to inject, can only adjust "x_texCoord"
* @param post the post-fetch shader code to inject, can only adjust "x_texture"
*/
class ModelCoordinates(texture: ColorBuffer,
var input: String = "va_texCoord0.xy",
var tangentInput: String? = null,
textureFunction: TextureFunction = TextureFunction.TILING,
var pre: String? = null,
var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) {
override fun toString(): String {
return "ModelCoordinates(texture: $texture, input: $input, $tangentInput: $tangentInput, textureFunction: $textureFunction, pre: $pre, post: $post)"
}
override fun hashCode(): Int {
var result = input.hashCode()
result = 31 * result + (tangentInput?.hashCode() ?: 0)
result = 31 * result + (pre?.hashCode() ?: 0)
result = 31 * result + (post?.hashCode() ?: 0)
return result
}
}
class Triplanar(texture: ColorBuffer,
var scale: Double = 1.0,
var offset: Vector3 = Vector3.ZERO,
var sharpness: Double = 2.0,
textureFunction: TextureFunction = TextureFunction.TILING,
var pre: String? = null,
var post: String? = null) : TextureFromColorBuffer(texture, textureFunction) {
init {
texture.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR)
texture.wrapU = WrapMode.REPEAT
texture.wrapV = WrapMode.REPEAT
}
override fun hashCode(): Int {
var result = scale.hashCode()
result = 31 * result + offset.hashCode()
result = 31 * result + sharpness.hashCode()
result = 31 * result + (pre?.hashCode() ?: 0)
result = 31 * result + (post?.hashCode() ?: 0)
return result
}
}
private fun ModelCoordinates.fs(index: Int) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0);
|{
| vec2 x_texCoord = $input;
| vec2 x_noTileOffset = vec2(0.0);
| vec4 x_texture;
| ${if (pre != null) "{ $pre } " else ""}
| x_texture = ${textureFunction.function("p_texture$index", "x_texCoord")};
| ${if (post != null) "{ $post } " else ""}
| ${if (tangentInput != null) {
"""
| vec3 normal = normalize(va_normal.xyz);
| vec3 tangent = normalize(${tangentInput}.xyz);
| vec3 bitangent = cross(normal, tangent) * ${tangentInput}.w;
| mat3 tbn = mat3(tangent, bitangent, normal);
| x_texture.rgb = tbn * normalize( (x_texture.rgb - vec3(0.5, 0.5, 0.0))*vec3(2.0, 2.0, 1.0)) ;
""".trimMargin()
} else ""}
| tex$index = x_texture;
|}
""".trimMargin()
private fun Triplanar.fs(index: Int, target: TextureTarget) = """
|vec4 tex$index = vec4(0.0, 0.0, 0.0, 1.0);
|{
| vec3 x_normal = va_normal;
| vec3 x_position = va_position;
| float x_scale = p_textureTriplanarScale$index;
| vec3 x_offset = p_textureTriplanarOffset$index;
| vec2 x_noTileOffset = vec2(0.0);
| ${if (pre != null) "{ $pre } " else ""}
| vec3 n = normalize(x_normal);
| vec3 an = abs(n);
| vec2 uvY = x_position.xz * x_scale + x_offset.x;
| vec2 uvX = x_position.zy * x_scale + x_offset.y;
| vec2 uvZ = x_position.xy * x_scale + x_offset.z;
| vec4 tY = ${textureFunction.function("p_texture$index", "uvY")};
| vec4 tX = ${textureFunction.function("p_texture$index", "uvX")};
| vec4 tZ = ${textureFunction.function("p_texture$index", "uvZ")};
| vec3 weights = pow(an, vec3(p_textureTriplanarSharpness$index));
| weights = weights / (weights.x + weights.y + weights.z);
| tex$index = tX * weights.x + tY * weights.y + weights.z * tZ;
| ${if (target == TextureTarget.NORMAL) """
| vec3 tnX = normalize( tX.xyz - vec3(0.5, 0.5, 0.0));
| vec3 tnY = normalize( tY.xyz - vec3(0.5, 0.5, 0.0)) * vec3(1.0, -1.0, 1.0);
| vec3 tnZ = normalize( tZ.xyz - vec3(0.5, 0.5, 0.0));
| vec3 nX = vec3(0.0, tnX.yx);
| vec3 nY = vec3(tnY.x, 0.0, tnY.y);
| vec3 nZ = vec3(tnZ.xy, 0.0);
| vec3 normal = normalize(nX * weights.x + nY * weights.y + nZ * weights.z + n);
| tex$index = vec4(normal, 0.0);
""".trimMargin() else ""}
|}
${if (post != null) """
vec4 x_texture = tex$index;
{
$post
}
tex$index = x_texture;
""".trimIndent() else ""}
""".trimMargin()
sealed class TextureTarget(val name: String) {
object NONE : TextureTarget("NONE")
object COLOR : TextureTarget("COLOR")
object ROUGHNESS : TextureTarget("ROUGHNESS")
object METALNESS : TextureTarget("METALNESS")
object METALNESS_ROUGHNESS : TextureTarget("METALNESS_ROUGHNESS")
object EMISSION : TextureTarget("EMISSION")
object NORMAL : TextureTarget("NORMAL")
object AMBIENT_OCCLUSION : TextureTarget("AMBIENT_OCCLUSION")
class Height(var scale: Double = 1.0) : TextureTarget("Height")
override fun toString(): String {
return "TextureTarget(name: $name)"
}
override fun hashCode(): Int {
return name.hashCode()
}
}
class Texture(var source: TextureSource,
var target: TextureTarget) {
fun copy(): Texture {
val copied = Texture(source, target)
return copied
}
override fun toString(): String {
return "Texture(source: $source, target: $target)"
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + target.hashCode()
return result
}
}
private var fragmentIDCounter = 1
data class SubsurfaceScatter(var enabled: Boolean) {
var color: ColorRGBa = ColorRGBa.WHITE
var shape = 1.0
fun fs(): String {
return if (enabled) """
f_diffuse.rgb += pow(smoothstep(1.0, 0.0, abs(dot(normalize(N),normalize(V)))), p_sssShape) * clamp(evaluateSH(-V, sh), vec3(0.0), vec3(1.0)) * p_sssColor.rgb;
""" else ""
}
fun applyToShadeStyle(shadeStyle: ShadeStyle) {
if (enabled) {
shadeStyle.parameter("sssColor", color)
shadeStyle.parameter("sssShape", shape)
}
}
}
data class CubemapReflection(var cubemap: Cubemap? = null) {
var color: ColorRGBa = ColorRGBa.WHITE
fun fs(): String {
return if (cubemap != null) {
"""
vec2 dfg = PrefilteredDFG_Karis(m_roughness, NoV);
vec3 sc = m_metalness * m_color.rgb + (1.0-m_metalness) * vec3(0.04);
f_specular.rgb += sc * (texture(p_radianceMap, reflect(-V, normalize(f_worldNormal)), m_roughness*7.0 ).rgb * dfg.x + dfg.y) * p_radianceColor.rgb;
"""
} else { "" }
}
fun applyToShadeStyle(shadeStyle: ShadeStyle) {
if (cubemap != null) {
shadeStyle.parameter("radianceMap", cubemap!!)
shadeStyle.parameter("radianceColor", color)
}
}
}
class PBRMaterial : Material {
override var name: String? = null
override fun toString(): String {
return "PBRMaterial(name: $name, fragmentID: $fragmentID, doubleSided: $doubleSided, textures: $textures, color: $color, metalness: $metalness, roughness: $roughness, emissive: $emission))"
}
override var fragmentID = fragmentIDCounter.apply {
fragmentIDCounter++
}
override var doubleSided: Boolean = false
override var transparent: Boolean = false
var environmentMap = false
var color = ColorRGBa.WHITE
var metalness = 0.5
var roughness = 1.0
var emission = ColorRGBa.BLACK
var subsurfaceScatter = SubsurfaceScatter(false)
var cubemapReflection = CubemapReflection(null)
var fragmentPreamble: String? = null
var vertexPreamble: String? = null
var vertexTransform: String? = null
var parameters = mutableMapOf<String, Any>()
var textures = mutableListOf<Texture>()
val shadeStyles = mutableMapOf<ContextKey, ShadeStyle>()
override fun generateShadeStyle(materialContext: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle {
val cached = shadeStyles.getOrPut(ContextKey(materialContext, primitiveContext)) {
val needLight = needLight(materialContext)
val preambleFS = """
vec4 m_color = p_color;
uint f_fragmentID = uint(p_fragmentID);
float m_f0 = 0.5;
float m_roughness = p_roughness;
float m_metalness = p_metalness;
float m_ambientOcclusion = 1.0;
vec3 m_emission = p_emission.rgb;
vec3 m_normal = vec3(0.0, 0.0, 1.0);
vec4 f_fog = vec4(0.0, 0.0, 0.0, 0.0);
vec3 f_worldNormal = v_worldNormal;
vec3 f_emission = m_emission;
""".trimIndent()
val textureFs = if (needLight) {
(textures.mapIndexed { index, it ->
when (val source = it.source) {
DummySource -> "vec4 tex$index = vec4(1.0);"
is ModelCoordinates -> source.fs(index)
is Triplanar -> source.fs(index, it.target)
is TextureFromCode -> source.fs(index, it.target)
else -> TODO()
}
} + textures.mapIndexed { index, texture ->
when (texture.target) {
TextureTarget.NONE -> ""
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;"
TextureTarget.EMISSION -> "m_emission *= tex$index.rgb;"
TextureTarget.NORMAL -> "f_worldNormal = normalize((v_modelNormalMatrix * vec4(tex$index.xyz,0.0)).xyz);"
TextureTarget.AMBIENT_OCCLUSION -> "m_ambientOcclusion *= tex$index.r;"
is TextureTarget.Height -> ""
}
}).joinToString("\n")
} else ""
val displacers = textures.filter { it.target is TextureTarget.Height }
val skinVS = if (primitiveContext.hasSkinning) """
uvec4 j = a_joints;
mat4 skinTransform = p_jointTransforms[j.x] * a_weights.x
+ p_jointTransforms[j.y] * a_weights.y
+ p_jointTransforms[j.z] * a_weights.z
+ p_jointTransforms[j.w] * a_weights.w;
${if (primitiveContext.hasNormalAttribute) """
x_normal = normalize(mat3(skinTransform) * x_normal);
""".trimIndent() else ""}
x_position = (skinTransform * vec4(x_position,1)).xyz;
""".trimIndent() else ""
val textureVS = if (displacers.isNotEmpty()) textures.mapIndexed { index, it ->
if (it.target is TextureTarget.Height) {
when (val source = it.source) {
DummySource -> "vec4 tex$index = vec4(1.0);"
is ModelCoordinates -> source.fs(index)
is Triplanar -> source.fs(index, it.target)
is TextureFromCode -> source.fs(index, it.target)
else -> TODO()
} + """
x_position += x_normal * tex$index.r * p_textureHeightScale$index;
""".trimIndent()
} else ""
}.joinToString("\n") else ""
val lights = materialContext.lights
val doubleSidedFS = if (doubleSided) {
"""
if (dot(V, N) <0) {
N *= -1.0;
}
""".trimIndent()
} else ""
val lightFS = if (needLight) """
vec3 f_diffuse = vec3(0.0);
vec3 f_specular = vec3(0.0);
vec3 f_ambient = vec3(0.0);
float f_occlusion = 1.0;
vec3 N = normalize(f_worldNormal);
vec3 ep = (p_viewMatrixInverse * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vec3 Vr = ep - v_worldPosition;
vec3 V = normalize(Vr);
float NoV = ${if (primitiveContext.hasNormalAttribute) "abs(dot(N, V)) + 1e-5" else "1"};
${if (environmentMap && materialContext.meshCubemaps.isNotEmpty() && primitiveContext.hasNormalAttribute) """
{
vec2 dfg = PrefilteredDFG_Karis(m_roughness, NoV);
vec3 sc = m_metalness * m_color.rgb + (1.0-m_metalness) * vec3(0.04);
f_specular.rgb += sc * (texture(p_environmentMap, reflect(-V, normalize(f_worldNormal))).rgb * dfg.x + dfg.y) * m_ambientOcclusion;
}
""".trimIndent() else ""}
${lights.mapIndexed { index, (node, light) ->
when (light) {
is AmbientLight -> light.fs(index)
is PointLight -> light.fs(index, primitiveContext.hasNormalAttribute)
is SpotLight -> light.fs(index, primitiveContext.hasNormalAttribute)
is DirectionalLight -> light.fs(index, primitiveContext.hasNormalAttribute)
is HemisphereLight -> light.fs(index, primitiveContext.hasNormalAttribute)
else -> TODO()
}
}.joinToString("\n")}
${if (materialContext.irradianceSH?.shMap != null) """
vec3[9] sh;
gatherSH(p_shMap, v_worldPosition, sh);
vec3 irradiance = clamp(evaluateSH(normalize(N), sh), vec3(0.0), vec3(1.0)) * m_color.rgb;
vec3 ks = F_SchlickRoughness(m_color.rgb * (m_metalness) + 0.04 * (1.0-m_metalness), m_roughness+0.1, min(NoV, 1.0-1.0e-6));
f_diffuse.rgb = irradiance * ks;
f_ambient.rgb = (1.0-ks) * irradiance;
${subsurfaceScatter.fs()}
${cubemapReflection.fs()}
""".trimIndent() else ""
}
${materialContext.fogs.mapIndexed { index, (node, fog) ->
fog.fs(index)
}.joinToString("\n")}
""".trimIndent() else ""
val rt = RenderTarget.active
val combinerFS = materialContext.pass.combiners.map {
it.generateShader()
}.joinToString("\n")
val fs = preambleFS + textureFs + lightFS + combinerFS
val vs = (this@PBRMaterial.vertexTransform ?: "") + textureVS + skinVS
shadeStyle {
fragmentPreamble = this@PBRMaterial.fragmentPreamble ?: ""
vertexPreamble = """
$shaderNoRepetitionVert
${(this@PBRMaterial.vertexPreamble) ?: ""}
""".trimIndent()
fragmentPreamble += """
${if (materialContext.irradianceSH?.shMap != null) {
"""
$glslEvaluateSH
$glslFetchSH
${genGlslGatherSH(materialContext.irradianceSH!!.xCount, materialContext.irradianceSH!!.yCount,
materialContext.irradianceSH!!.zCount, materialContext.irradianceSH!!.spacing, materialContext.irradianceSH!!.offset)}
"""
} else {
""
}
}
|$shaderLinePlaneIntersect
|$shaderProjectOnPlane
|$shaderSideOfPlane
|$shaderGGX
|$shaderVSM
|$shaderNoRepetition
|$phraseTbnMatrix
""".trimMargin()
this.suppressDefaultOutput = true
this.vertexTransform = vs
fragmentTransform = fs
materialContext.pass.combiners.map {
if (rt is ProgramRenderTarget || materialContext.pass === DefaultPass || materialContext.pass === DefaultOpaquePass || materialContext.pass == DefaultTransparentPass || materialContext.pass == IrradianceProbePass || materialContext.pass.skipTarget ) {
this.output(it.targetOutput, ShadeStyleOutput(0))
} else {
val index = rt.colorAttachmentIndexByName(it.targetOutput)?:error("attachment ${it.targetOutput} not found")
val type = rt.colorBuffer(index).type
val format = rt.colorBuffer(index).format
this.output(it.targetOutput, ShadeStyleOutput(index, format, type))
}
}
}
}
return cached
}
private fun needLight(context: MaterialContext): Boolean {
val needSpecular = context.pass.combiners.any { FacetType.SPECULAR in it.facets }
val needDiffuse = context.pass.combiners.any { FacetType.DIFFUSE in it.facets }
val needLight = needSpecular || needDiffuse
return needLight
}
override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) {
shadeStyle.parameter("emission", emission)
shadeStyle.parameter("color", color)
shadeStyle.parameter("metalness", metalness)
shadeStyle.parameter("roughness", roughness)
shadeStyle.parameter("fragmentID", fragmentID)
if (context.irradianceProbeCount > 0) {
shadeStyle.parameter("shMap", context.irradianceSH?.shMap!!)
}
parameters.forEach { (k, v) ->
when (v) {
is Double -> shadeStyle.parameter(k, v)
is Int -> shadeStyle.parameter(k, v)
is Vector2 -> shadeStyle.parameter(k, v)
is Vector3 -> shadeStyle.parameter(k, v)
is Vector4 -> shadeStyle.parameter(k, v)
is BufferTexture -> shadeStyle.parameter(k, v)
is ColorBuffer -> shadeStyle.parameter(k, v)
else -> TODO("support ${v::class.java}")
}
}
if (needLight(context)) {
subsurfaceScatter.applyToShadeStyle(shadeStyle)
cubemapReflection.applyToShadeStyle(shadeStyle)
textures.forEachIndexed { index, texture ->
when (val source = texture.source) {
is TextureFromColorBuffer -> {
shadeStyle.parameter("texture$index", source.texture)
if (source.textureFunction == TextureFunction.NOT_TILING) {
shadeStyle.parameter("textureNoise", noise128)
}
}
}
when (val source = texture.source) {
is Triplanar -> {
shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness)
shadeStyle.parameter("textureTriplanarScale$index", source.scale)
shadeStyle.parameter("textureTriplanarOffset$index", source.offset)
}
}
if (texture.target is TextureTarget.Height) {
val target = texture.target as TextureTarget.Height
shadeStyle.parameter("textureHeightScale$index", target.scale)
}
}
val lights = context.lights
lights.forEachIndexed { index, (node, light) ->
shadeStyle.parameter("lightColor$index", light.color)
when (light) {
is AmbientLight -> {
}
is PointLight -> {
shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation)
shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation)
shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation)
}
is SpotLight -> {
shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
shadeStyle.parameter("lightConstantAttenuation$index", light.constantAttenuation)
shadeStyle.parameter("lightLinearAttenuation$index", light.linearAttenuation)
shadeStyle.parameter("lightQuadraticAttenuation$index", light.quadraticAttenuation)
shadeStyle.parameter("lightInnerCos$index", cos(Math.toRadians(light.innerAngle)))
shadeStyle.parameter("lightOuterCos$index", cos(Math.toRadians(light.outerAngle)))
if (light.shadows is Shadows.MappedShadows) {
context.shadowMaps[light]?.let {
val look = light.view(node)
shadeStyle.parameter("lightTransform$index",
light.projection(it) * look)
if (light.shadows is Shadows.DepthMappedShadows) {
shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO())
}
if (light.shadows is Shadows.ColorMappedShadows) {
shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0))
}
}
}
}
is DirectionalLight -> {
shadeStyle.parameter("lightPosition$index", (node.worldTransform * Vector4.UNIT_W).xyz)
shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
if (light.shadows is Shadows.MappedShadows) {
context.shadowMaps[light]?.let {
val look = light.view(node)
shadeStyle.parameter("lightTransform$index",
light.projection(it) * look)
if (light.shadows is Shadows.DepthMappedShadows) {
shadeStyle.parameter("lightShadowMap$index", it.depthBuffer ?: TODO())
}
if (light.shadows is Shadows.ColorMappedShadows) {
shadeStyle.parameter("lightShadowMap$index", it.colorBuffer(0))
}
}
}
}
is HemisphereLight -> {
shadeStyle.parameter("lightDirection$index", ((normalMatrix(node.worldTransform)) * light.direction.xyz0).normalized.xyz)
shadeStyle.parameter("lightUpColor$index", light.upColor)
shadeStyle.parameter("lightDownColor$index", light.downColor)
light.irradianceMap?.let {
shadeStyle.parameter("lightIrradianceMap$index", it)
}
}
}
}
context.fogs.forEachIndexed { index, (node, fog) ->
shadeStyle.parameter("fogColor$index", fog.color)
shadeStyle.parameter("fogEnd$index", fog.end)
}
} else {
textures.forEachIndexed { index, texture ->
if (texture.target is TextureTarget.Height) {
when (val source = texture.source) {
is TextureFromColorBuffer -> shadeStyle.parameter("texture$index", source.texture)
}
when (val source = texture.source) {
is Triplanar -> {
shadeStyle.parameter("textureTriplanarSharpness$index", source.sharpness)
shadeStyle.parameter("textureTriplanarScale$index", source.scale)
shadeStyle.parameter("textureTriplanarOffset$index", source.offset)
}
}
val target = texture.target as TextureTarget.Height
shadeStyle.parameter("textureHeightScale$index", target.scale)
}
}
}
}
override fun hashCode(): Int {
var result = fragmentID.hashCode()
result = 31 * doubleSided.hashCode()
result = 31 * result + transparent.hashCode()
// result = 31 * result + environmentMap.hashCode()
result = 31 * result + color.hashCode()
result = 31 * result + metalness.hashCode()
result = 31 * result + roughness.hashCode()
result = 31 * result + emission.hashCode()
result = 31 * result + (fragmentPreamble?.hashCode() ?: 0)
result = 31 * result + (vertexPreamble?.hashCode() ?: 0)
result = 31 * result + (vertexTransform?.hashCode() ?: 0)
// result = 31 * result + parameters.hashCode()
// result = 31 * result + textures.hashCode()
// result = 31 * result + shadeStyles.hashCode()
return result
}
}

View File

@@ -0,0 +1,61 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.*
import org.openrndr.math.Matrix44
data class PostContext(val lightContext: LightContext, val inverseViewMatrix: Matrix44)
interface PostStep {
fun apply(buffers: MutableMap<String, ColorBuffer>, postContext: PostContext)
}
class FilterPostStep<T:Filter>(val outputScale: Double,
val filter: T,
val inputs: List<String>,
val output: String,
val outputFormat: ColorFormat,
val outputType: ColorType,
val update: (T.(PostContext) -> Unit)? = null) : PostStep {
override fun apply(buffers: MutableMap<String, ColorBuffer>, postContext: PostContext) {
val inputBuffers = inputs.map { buffers[it]?: error("buffer not found: $it") }
val outputBuffer = buffers.getOrPut(output) {
colorBuffer((inputBuffers[0].width * outputScale).toInt(),
(inputBuffers[0].height * outputScale).toInt(),
format = outputFormat,
type = outputType)
}
update?.invoke(filter, postContext)
filter.apply(inputBuffers.toTypedArray(), outputBuffer)
}
}
class FunctionPostStep(val function:(MutableMap<String, ColorBuffer>)->Unit) : PostStep {
override fun apply(buffers: MutableMap<String, ColorBuffer>, postContext: PostContext) {
function(buffers)
}
}
class FilterPostStepBuilder<T : Filter>(val filter: T) {
var outputScale = 1.0
val inputs = mutableListOf<String>()
var output = "untitled"
var outputFormat = ColorFormat.RGBa
var outputType = ColorType.UINT8
var update: (T.(PostContext) -> Unit)? = null
internal fun build(): PostStep {
@Suppress("UNCHECKED_CAST", "PackageDirectoryMismatch")
return FilterPostStep(outputScale, filter, inputs, output, outputFormat, outputType, update as (Filter.(PostContext) -> Unit)?)
}
}
fun <T : Filter> postStep(filter: T, configure: FilterPostStepBuilder<T>.() -> Unit) : PostStep {
val psb = FilterPostStepBuilder(filter)
psb.configure()
return psb.build()
}
fun postStep(function: (MutableMap<String, ColorBuffer>)->Unit) : PostStep {
return FunctionPostStep(function)
}

View File

@@ -0,0 +1,35 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.BufferMultisample
import org.openrndr.draw.DepthFormat
import org.openrndr.draw.RenderTarget
import org.openrndr.draw.renderTarget
data class RenderPass(val combiners: List<FacetCombiner>,
val renderOpaque: Boolean = true,
val renderTransparent: Boolean = false,
val depthWrite: Boolean = true,
val multisample: BufferMultisample = BufferMultisample.Disabled,
val skipTarget: Boolean = false
)
val DefaultPass = RenderPass(listOf(LDRColorFacet()))
val IrradianceProbePass = RenderPass(listOf(DiffuseIrradianceFacet()))
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()))
fun RenderPass.createPassTarget(width: Int, height: Int, depthFormat: DepthFormat = DepthFormat.DEPTH24, multisample: BufferMultisample = this.multisample): RenderTarget {
return renderTarget(width, height, multisample = multisample) {
for (combiner in combiners) {
when (combiner) {
is ColorBufferFacetCombiner ->
colorBuffer(combiner.targetOutput, combiner.format, combiner.type)
}
}
depthBuffer(depthFormat)
}
}

View File

@@ -0,0 +1,95 @@
package org.openrndr.extra.dnk3
import org.openrndr.Dispatcher
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import java.util.*
class Scene(val root: SceneNode = SceneNode(), val dispatcher: Dispatcher = Dispatcher()) {
val features = mutableListOf<Feature>()
override fun hashCode(): Int {
var result = root.hashCode()
result = result * 31 + features.hashCode()
return result
}
fun hash(): String = Base64.getEncoder().encodeToString(hashCode().toString().toByteArray())
}
open class SceneNode {
var name: String = ""
var entities: MutableList<Entity> = mutableListOf()
var parent: SceneNode? = null
open var transform = Matrix44.IDENTITY
var worldTransform = Matrix44.IDENTITY
val children = mutableListOf<SceneNode>()
var disposed = false
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + entities.hashCode()
// result = 31 * result + (parent?.hashCode() ?: 0)
result = 31 * result + transform.hashCode()
result = 31 * result + worldTransform.hashCode()
result = 31 * result + children.hashCode()
result = 31 * result + disposed.hashCode()
return result
}
}
val SceneNode.worldPosition: Vector3
get() {
return (worldTransform * Vector4.UNIT_W).xyz
}
class NodeContent<T>(val node: SceneNode, val content: T) {
operator fun component1() = node
operator fun component2() = content
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NodeContent<*>
if (node != other.node) return false
if (content != other.content) return false
return true
}
override fun hashCode(): Int {
var result = node.hashCode()
result = 31 * result + content.hashCode()
return result
}
}
fun SceneNode.visit(visitor: SceneNode.() -> Unit) {
visitor()
children.forEach { it.visit(visitor) }
}
fun <P> SceneNode.scan(initial: P, scanner: SceneNode.(P) -> P) {
val p = scanner(initial)
children.forEach { it.scan(p, scanner) }
}
fun SceneNode.findNodes(selector: SceneNode.() -> Boolean): List<SceneNode> {
val result = mutableListOf<SceneNode>()
visit {
if (selector()) result.add(this)
}
return result
}
fun <P : Entity> SceneNode.findContent(selector: Entity.() -> P?): List<NodeContent<P>> {
val result = mutableListOf<NodeContent<P>>()
visit {
entities.forEach {
val s = it.selector()
if (s != null) {
result.add(NodeContent(this, s))
}
}
}
return result
}

View File

@@ -0,0 +1,342 @@
package org.openrndr.extra.dnk3
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.features.IrradianceSH
import org.openrndr.extra.fx.blur.ApproximateGaussianBlur
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
import java.nio.ByteBuffer
class RenderContext(
val lights: List<NodeContent<Light>>,
val meshes: List<NodeContent<Mesh>>,
val skinnedMeshes: List<NodeContent<SkinnedMesh>>,
val instancedMeshes: List<NodeContent<InstancedMesh>>,
val pathMeshes: List<NodeContent<PathMesh>>,
val fogs: List<NodeContent<Fog>>
)
class SceneRenderer {
class Configuration {
var multisampleLines = false
}
val configuration = Configuration()
val blur = ApproximateGaussianBlur()
var shadowLightTargets = mutableMapOf<ShadowLight, RenderTarget>()
var meshCubemaps = mutableMapOf<Mesh, Cubemap>()
var outputPasses = mutableListOf(DefaultOpaquePass, DefaultTransparentPass)
var outputPassTarget: RenderTarget? = null
var outputPassTargetMS: RenderTarget? = null
val postSteps = mutableListOf<PostStep>()
val buffers = mutableMapOf<String, ColorBuffer>()
var drawFinalBuffer = true
var first = true
fun draw(drawer: Drawer, scene: Scene) {
drawer.pushStyle()
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
drawer.cullTestPass = CullTestPass.FRONT
scene.dispatcher.execute()
// update all the transforms
scene.root.scan(Matrix44.IDENTITY) { p ->
if (p !== Matrix44.IDENTITY) {
worldTransform = p * transform
} else {
worldTransform = transform
}
worldTransform
}
val context = RenderContext(
lights = scene.root.findContent { this as? Light },
meshes = scene.root.findContent { this as? Mesh },
skinnedMeshes = scene.root.findContent { this as? SkinnedMesh },
fogs = scene.root.findContent { this as? Fog },
instancedMeshes = scene.root.findContent { this as? InstancedMesh },
pathMeshes = scene.root.findContent { this as? PathMesh}
)
// shadow passes
run {
context.lights.filter { it.content is ShadowLight && (it.content as ShadowLight).shadows is Shadows.MappedShadows }.forEach {
val shadowLight = it.content as ShadowLight
val pass: RenderPass
pass = when (shadowLight.shadows) {
is Shadows.PCF, is Shadows.Simple -> {
LightPass
}
is Shadows.VSM -> {
VSMLightPass
}
else -> TODO()
}
val target = shadowLightTargets.getOrPut(shadowLight) {
val mapSize = (shadowLight.shadows as Shadows.MappedShadows).mapSize
pass.createPassTarget(mapSize, mapSize, DepthFormat.DEPTH16)
}
target.clearDepth(depth = 1.0)
val look = shadowLight.view(it.node)
val materialContext = MaterialContext(pass, context.lights, context.fogs, shadowLightTargets, emptyMap(), 0)
drawer.isolatedWithTarget(target) {
drawer.projection = shadowLight.projection(target)
drawer.view = look
drawer.model = Matrix44.IDENTITY
drawer.clear(ColorRGBa.BLACK)
drawer.cullTestPass = CullTestPass.FRONT
drawPass(drawer, pass, materialContext, context)
}
when (shadowLight.shadows) {
is Shadows.VSM -> {
blur.gain = 1.0
blur.sigma = 3.0
blur.window = 9
blur.spread = 1.0
blur.apply(target.colorBuffer(0), target.colorBuffer(0))
}
}
}
}
// -- feature passes
for (feature in scene.features) {
feature.update(drawer, this, scene, feature, context)
}
// -- output passes
run {
val irradianceSH = scene.features.find { it is IrradianceSH } as? IrradianceSH
for (pass in outputPasses) {
val materialContext = MaterialContext(pass, context.lights, context.fogs, shadowLightTargets, meshCubemaps, irradianceSH?.probeCount
?: 0)
materialContext.irradianceSH = irradianceSH
val defaultPasses = setOf(DefaultTransparentPass, DefaultOpaquePass)
if ((pass !in defaultPasses || postSteps.isNotEmpty()) && outputPassTarget == null) {
outputPassTarget = pass.createPassTarget(RenderTarget.active.width, RenderTarget.active.height)
}
if (pass == outputPasses[0]) {
outputPassTarget?.let {
drawer.withTarget(it) {
clear(ColorRGBa.TRANSPARENT)
}
}
}
outputPassTarget?.let { target ->
pass.combiners.forEach {
if (it is ColorBufferFacetCombiner) {
val index = target.colorAttachmentIndexByName(it.targetOutput)
?: error("attachment not found ${it.targetOutput}")
target.blendMode(index, it.blendMode)
}
}
}
outputPassTarget?.bind()
drawPass(drawer, pass, materialContext, context)
outputPassTarget?.unbind()
outputPassTarget?.let { output ->
for (combiner in pass.combiners) {
buffers[combiner.targetOutput] = (output.colorAttachmentByName(combiner.targetOutput) as? ColorBufferAttachment)?.colorBuffer
?: error("attachment not found ${combiner.targetOutput}")
}
}
}
val lightContext = LightContext(context.lights, shadowLightTargets)
val postContext = PostContext(lightContext, drawer.view.inversed)
for (postStep in postSteps) {
postStep.apply(buffers, postContext)
}
}
drawer.popStyle()
if (drawFinalBuffer) {
outputPassTarget?.let { output ->
drawer.isolated {
drawer.defaults()
drawer.ortho()
val outputName = (postSteps.lastOrNull() as? FilterPostStep<*>)?.output ?: "color"
val outputBuffer = buffers[outputName]
?: throw IllegalArgumentException("can't find $outputName buffer")
drawer.image(outputBuffer)
}
}
}
}
internal fun drawPass(drawer: Drawer, pass: RenderPass, materialContext: MaterialContext,
context: RenderContext, shadeStyleTransformer: ((ShadeStyle)->Unit)? = null
) {
drawer.depthWrite = pass.depthWrite
val primitives = context.meshes.flatMap { mesh ->
mesh.content.primitives.map { primitive ->
NodeContent(mesh.node, primitive)
}
}
// -- draw all meshes
primitives
.filter { (it.content.material.transparent && pass.renderTransparent) || (!it.content.material.transparent && pass.renderOpaque) }
.forEach {
val primitive = it.content
drawer.isolated {
if (primitive.material.doubleSided) {
drawer.drawStyle.cullTestPass = CullTestPass.ALWAYS
}
val hasNormalAttribute = primitive.geometry.vertexBuffers.any { it.vertexFormat.hasAttribute("normal") }
val primitiveContext = PrimitiveContext(hasNormalAttribute, false)
val shadeStyle = primitive.material.generateShadeStyle(materialContext, primitiveContext)
shadeStyle.parameter("viewMatrixInverse", drawer.view.inversed)
primitive.material.applyToShadeStyle(materialContext, shadeStyle)
shadeStyleTransformer?.invoke(shadeStyle)
drawer.shadeStyle = shadeStyle
drawer.model = it.node.worldTransform
if (primitive.geometry.indexBuffer == null) {
drawer.vertexBuffer(primitive.geometry.vertexBuffers,
primitive.geometry.primitive,
primitive.geometry.offset,
primitive.geometry.vertexCount)
} else {
drawer.vertexBuffer(primitive.geometry.indexBuffer!!,
primitive.geometry.vertexBuffers,
primitive.geometry.primitive,
primitive.geometry.offset,
primitive.geometry.vertexCount)
}
}
}
val skinnedPrimitives = context.skinnedMeshes.flatMap { mesh ->
mesh.content.primitives.map { primitive ->
NodeContent(mesh.node, Pair(primitive, mesh))
}
}
skinnedPrimitives
.filter {
(it.content.first.material.transparent && pass.renderTransparent) ||
(!it.content.first.material.transparent && pass.renderOpaque)
}
.forEach {
val primitive = it.content.first
val skinnedMesh = it.content.second.content
drawer.isolated {
if (primitive.material.doubleSided) {
drawer.drawStyle.cullTestPass = CullTestPass.ALWAYS
}
val hasNormalAttribute = primitive.geometry.vertexBuffers.any { it.vertexFormat.hasAttribute("normal") }
val primitiveContext = PrimitiveContext(hasNormalAttribute, true)
val nodeInverse = it.node.worldTransform.inversed
val jointTransforms = (skinnedMesh.joints zip skinnedMesh.inverseBindMatrices)
.map { (nodeInverse * it.first.worldTransform * it.second) }
val shadeStyle = primitive.material.generateShadeStyle(materialContext, primitiveContext)
shadeStyle.parameter("jointTransforms", jointTransforms.toTypedArray())
shadeStyle.parameter("viewMatrixInverse", drawer.view.inversed)
primitive.material.applyToShadeStyle(materialContext, shadeStyle)
drawer.shadeStyle = shadeStyle
drawer.model = it.node.worldTransform
if (primitive.geometry.indexBuffer == null) {
drawer.vertexBuffer(primitive.geometry.vertexBuffers,
primitive.geometry.primitive,
primitive.geometry.offset,
primitive.geometry.vertexCount)
} else {
drawer.vertexBuffer(primitive.geometry.indexBuffer!!,
primitive.geometry.vertexBuffers,
primitive.geometry.primitive,
primitive.geometry.offset,
primitive.geometry.vertexCount)
}
}
}
val instancedPrimitives = context.instancedMeshes.flatMap { mesh ->
mesh.content.primitives.map { primitive ->
NodeContent(mesh.node, MeshPrimitiveInstance(primitive, mesh.content.instances, mesh.content.attributes))
}
}
// -- draw all instanced meshes
instancedPrimitives
.filter { (it.content.primitive.material.transparent && pass.renderTransparent) || (!it.content.primitive.material.transparent && pass.renderOpaque) }
.forEach {
val primitive = it.content
drawer.isolated {
val primitiveContext = PrimitiveContext(true, false)
val shadeStyle = primitive.primitive.material.generateShadeStyle(materialContext, primitiveContext)
shadeStyle.parameter("viewMatrixInverse", drawer.view.inversed)
primitive.primitive.material.applyToShadeStyle(materialContext, shadeStyle)
if (primitive.primitive.material.doubleSided) {
drawer.drawStyle.cullTestPass = CullTestPass.ALWAYS
}
drawer.shadeStyle = shadeStyle
drawer.model = it.node.worldTransform
drawer.vertexBufferInstances(primitive.primitive.geometry.vertexBuffers,
primitive.attributes,
DrawPrimitive.TRIANGLES,
primitive.instances,
primitive.primitive.geometry.offset,
primitive.primitive.geometry.vertexCount)
}
}
context.pathMeshes.filter { (it.content.material.transparent && pass.renderTransparent) || (!it.content.material.transparent && pass.renderOpaque) }
.forEach {
drawer.isolated {
val primitiveContext = PrimitiveContext(true, false)
val shadeStyle = it.content.material.generateShadeStyle(materialContext, primitiveContext)
shadeStyle.parameter("viewMatrixInverse", drawer.view.inversed)
it.content.material.applyToShadeStyle(materialContext, shadeStyle)
drawer.drawStyle.cullTestPass = CullTestPass.ALWAYS
drawer.shadeStyle = shadeStyle
drawer.model = it.node.worldTransform
drawer.strokeWeight = it.content.weight
for (path in it.content.paths) {
drawer.path(path.sampleLinear(0.0005))
}
}
}
drawer.depthWrite = true
}
}
fun sceneRenderer(builder: SceneRenderer.() -> Unit): SceneRenderer {
val sceneRenderer = SceneRenderer()
sceneRenderer.builder()
return sceneRenderer
}
internal fun ByteBuffer.putVector3(v: Vector3) {
putFloat(v.x.toFloat())
putFloat(v.y.toFloat())
putFloat(v.z.toFloat())
}

View File

@@ -0,0 +1,237 @@
package org.openrndr.extra.dnk3
val shaderNoRepetition = """
// -- shaderNoRepetition
float sum( vec3 v ) { return v.x+v.y+v.z; }
// based on https://www.shadertoy.com/view/Xtl3zf
vec4 textureNoTile(in sampler2D noiseTex, in sampler2D tex, in vec2 noiseOffset, in vec2 x)
{
float v = 1.0;
float k = texture(noiseTex, noiseOffset + x*0.01 ).x; // cheap (cache friendly) lookup
vec2 duvdx = dFdx( x );
vec2 duvdy = dFdx( x );
float l = k*8.0;
float f = fract(l);
#if 0
float ia = floor(l); // my method
float ib = ia + 1.0;
#else
float ia = floor(l+0.5); // suslik's method (see comments)
float ib = floor(l);
f = min(f, 1.0-f)*2.0;
#endif
vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
vec3 cola = textureGrad( tex, x + v*offa, duvdx, duvdy ).xyz;
vec3 colb = textureGrad( tex, x + v*offb, duvdx, duvdy ).xyz;
return vec4(mix( cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola-colb)) ), 1.0);
}
"""
val shaderNoRepetitionVert = """
// -- shaderNoRepetitionVert
float sum( vec3 v ) { return v.x+v.y+v.z; }
// based on https://www.shadertoy.com/view/Xtl3zf
vec4 textureNoTile(in sampler2D tex, in vec2 noiseOffset, in vec2 x)
{
float v = 1.0;
float k = texture(tex, noiseOffset + 0.005*x ).x; // cheap (cache friendly) lookup
float l = k*8.0;
float f = fract(l);
#if 0
float ia = floor(l); // my method
float ib = ia + 1.0;
#else
float ia = floor(l+0.5); // suslik's method (see comments)
float ib = floor(l);
f = min(f, 1.0-f)*2.0;
#endif
vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
vec3 cola = texture( tex, x + v*offa).xyz;
vec3 colb = texture( tex, x + v*offb).xyz;
return vec4(mix( cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola-colb)) ), 1.0);
}
"""
val shaderProjectOnPlane = """
// -- shaderProjectOnPlane
vec3 projectOnPlane(vec3 p, vec3 pc, vec3 pn) {
float distance = dot(pn, p-pc);
return p - distance * pn;
}
""".trimIndent()
val shaderSideOfPlane = """
// -- shaderSideOfPlane
int sideOfPlane(in vec3 p, in vec3 pc, in vec3 pn){
if (dot(p-pc,pn) >= 0.0) return 1; else return 0;
}
""".trimIndent()
val shaderLinePlaneIntersect = """
// -- shaderLinePlaneIntersect
vec3 linePlaneIntersect(in vec3 lp, in vec3 lv, in vec3 pc, in vec3 pn){
return lp+lv*(dot(pn,pc-lp)/dot(pn,lv));
}
""".trimIndent()
val shaderVSM = """
|// -- shaderVSM
|float linstep(float min, float max, float v)
|{
| return clamp((v - min) / (max - min), 0, 1);
|}
|// https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch08.html
|float chebyshevUpperBound(vec2 moments, float t, float minVariance) {
| // One-tailed inequality valid if t > Moments.x
| float p = (t <= moments.x) ? 1.0 : 0.0;
| // Compute variance.
| float variance = moments.y - (moments.x * moments.x);
| variance = max(variance, minVariance);
| // Compute probabilistic upper bound.
| float d = t - moments.x;
| float p_max = variance / (variance + d*d);
| p_max = smoothstep(0.6, 1.0, p_max);
| return max(p, p_max);
}
""".trimIndent()
/*
N - world space normal
V - eye - world vertex position
L - world light pos - world vertex position
*/
val shaderGGX = """
// -- shaderGGX
#define bias 0.125
#define HASHSCALE 443.8975
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * HASHSCALE);
p3 += dot(p3, p3.yzx+19.19);
return fract(vec2((p3.x + p3.y)*p3.z, (p3.x+p3.z)*p3.y));
}
#define PI 3.1415926535
float pow5(float x) {
float x2 = x * x;
return x2 * x2 * x;
}
float D_GGX(float linearRoughness, float NoH, const vec3 h) {
// Walter et al. 2007, "Microfacet Models for Refraction through Rough Surfaces"
float oneMinusNoHSquared = 1.0 - NoH * NoH;
float a = NoH * linearRoughness;
float k = linearRoughness / (oneMinusNoHSquared + a * a);
float d = k * k * (1.0 / PI);
return d;
}
float D_GGXm(float linearRoughness, float NoH, const vec3 h, const vec3 n) {
vec3 NxH = cross(n, h);
float oneMinusNoHSquared = dot(NxH, NxH);
// Walter et al. 2007, "Microfacet Models for Refraction through Rough Surfaces"
//float oneMinusNoHSquared = 1.0 - NoH * NoH;
float a = NoH * linearRoughness;
float k = linearRoughness / (oneMinusNoHSquared + a * a);
float d = k * k * (1.0 / PI);
return d;
}
float V_SmithGGXCorrelated(float linearRoughness, float NoV, float NoL) {
// Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"
float a2 = linearRoughness * linearRoughness;
float GGXV = NoL * sqrt((NoV - a2 * NoV) * NoV + a2);
float GGXL = NoV * sqrt((NoL - a2 * NoL) * NoL + a2);
return 0.5 / (GGXV + GGXL);
}
vec3 F_Schlick(const vec3 f0, float VoH) {
// Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"
return f0 + (vec3(1.0) - f0) * pow5(1.0 - VoH);
}
vec3 F_SchlickRoughness(vec3 F0, float roughness, float VoH)
{
return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - VoH, 5.0);
}
float F_Schlick(float f0, float f90, float VoH) {
return f0 + (f90 - f0) * pow5(1.0 - VoH);
}
float Fd_Burley(float linearRoughness, float NoV, float NoL, float LoH) {
// Burley 2012, "Physically-Based Shading at Disney"
float f90 = 0.5 + 2.0 * linearRoughness * LoH * LoH;
float lightScatter = F_Schlick(1.0, f90, NoL);
float viewScatter = F_Schlick(1.0, f90, NoV);
return lightScatter * viewScatter * (1.0 / PI);
}
vec2 PrefilteredDFG_Karis(float roughness, float NoV) {
//https://www.shadertoy.com/view/XlKSDR
// Karis 2014, "Physically Based Material on Mobile"
const vec4 c0 = vec4(-1.0, -0.0275, -0.572, 0.022);
const vec4 c1 = vec4( 1.0, 0.0425, 1.040, -0.040);
vec4 r = roughness * c0 + c1;
float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
return vec2(-1.04, 1.04) * a004 + r.zw;
}
float saturate(float x) {
return clamp(x, 0.0, 1.0);
}
float G1V(float dotNV, float k)
{
return 1.0f/(dotNV*(1.0f-k)+k);
}
float ggx(vec3 N, vec3 V, vec3 L, float roughness, float F0)
{
float alpha = roughness*roughness;
vec3 H = normalize(V+L);
float dotNL = saturate(dot(N,L));
float dotNV = saturate(dot(N,V));
float dotNH = saturate(dot(N,H));
float dotLH = saturate(dot(L,H));
float F, D, vis;
// D
float alphaSqr = alpha*alpha;
float pi = 3.14159f;
float denom = dotNH * dotNH *(alphaSqr-1.0) + 1.0f;
D = alphaSqr/(pi * denom * denom);
// F
float dotLH5 = pow(1.0f-dotLH,5);
F = F0 + (1.0-F0)*(dotLH5);
// V
float k = alpha/2.0f;
vis = G1V(dotNL,k)*G1V(dotNV,k);
float specular = dotNL * D * F * vis;
return specular;
}
""".trimIndent()

View File

@@ -0,0 +1,99 @@
package org.openrndr.extra.dnk3
import org.openrndr.draw.RenderTarget
import org.openrndr.math.Matrix44
sealed class Shadows {
object None : Shadows()
abstract class MappedShadows(val mapSize: Int) : Shadows()
abstract class DepthMappedShadows(mapSize: Int) : MappedShadows(mapSize)
abstract class ColorMappedShadows(mapSize: Int) : MappedShadows(mapSize)
class Simple(mapSize: Int = 1024) : DepthMappedShadows(mapSize)
class PCF(mapSize: Int = 1024, val sampleCount: Int = 12) : DepthMappedShadows(mapSize)
class VSM(mapSize: Int = 1024) : ColorMappedShadows(mapSize)
}
interface ShadowLight {
var shadows: Shadows
fun projection(renderTarget: RenderTarget): Matrix44
fun view(node: SceneNode): Matrix44 {
return node.worldTransform.inversed
}
}
// shaders
fun Shadows.VSM.fs(index: Int) : String = """
|{
| vec4 smc = (p_lightTransform$index * vec4(v_worldPosition,1.0));
| vec3 lightProj = (smc.xyz/smc.w) * 0.5 + 0.5;
| if (lightProj.x > 0.0 && lightProj.x < 1.0 && lightProj.y > 0 && lightProj.y < 1) {
| vec2 moments = texture(p_lightShadowMap$index, lightProj.xy).xy;
| attenuation *= (chebyshevUpperBound(moments, length(Lr), 50.0));
| }
|}
""".trimMargin()
fun Shadows.Simple.fs(index: Int): String = """
|{
| vec4 smc = (p_lightTransform$index * vec4(v_worldPosition,1.0));
| vec3 lightProj = (smc.xyz/smc.w) * 0.5 + 0.5;
| if (lightProj.x > 0.0 && lightProj.x < 1.0 && lightProj.y > 0 && lightProj.y < 1) {
| vec3 smz = texture(p_lightShadowMap$index, lightProj.xy).rgb;
| vec2 step = 1.0 / textureSize(p_lightShadowMap$index,0);
| float result = 0.0;
| float compToZ = (lightProj.z- 0.0020 * tan(acos(NoL))) - 0.0003;
| float currentDepth = lightProj.z;
| float closestDepth = smz.x;
| float shadow = (currentDepth - 0.0020 * tan(acos(NoL))) - 0.0003 >= closestDepth ? 0.0 : 1.0;
| attenuation *= shadow;
| }
|}
""".trimMargin()
fun Shadows.PCF.fs(index: Int): String = """
|{
| float lrl = length(Lr)/100.0;
| vec2 fTaps_Poisson[12];
| fTaps_Poisson[0] = vec2(-.326,-.406);
| fTaps_Poisson[1] = vec2(-.840,-.074);
| fTaps_Poisson[2] = vec2(-.696, .457);
| fTaps_Poisson[3] = vec2(-.203, .621);
| fTaps_Poisson[4] = vec2( .962,-.195);
| fTaps_Poisson[5] = vec2( .473,-.480);
| fTaps_Poisson[6] = vec2( .519, .767);
| fTaps_Poisson[7] = vec2( .185,-.893);
| fTaps_Poisson[8] = vec2( .507, .064);
| fTaps_Poisson[9] = vec2( .896, .412);
| fTaps_Poisson[10] = vec2(-.322,-.933);
| fTaps_Poisson[11] = vec2(-.792,-.598);
| vec4 smc = (p_lightTransform$index * vec4(v_worldPosition,1.0));
| vec3 lightProj = (smc.xyz/smc.w) * 0.5 + 0.5;
| if (lightProj.x > 0.0 && lightProj.x < 1.0 && lightProj.y > 0 && lightProj.y < 1) {
| vec3 smz = texture(p_lightShadowMap$index, lightProj.xy).rgb;
| vec2 stepSize = 1.0 / textureSize(p_lightShadowMap$index,0);
| float result = 0.0;
| float compToZ = (lightProj.z- 0.0020 * tan(acos(NoL))) - 0.0003;
| float noise = hash22(lightProj.xy*10.0).x;
| float r = noise * 3.1415926535 * 2.0;
| mat2 rot = mat2( vec2(cos(r), -sin(r)), vec2(sin(r),cos(r)));
| for (int i = 0; i < 12; ++i) {
| float depth = texture(p_lightShadowMap$index, lightProj.xy + rot*fTaps_Poisson[i]*i*lrl*stepSize ).r;
| result += step(compToZ, depth);
| }
| result /= 12;
| float currentDepth = lightProj.z;
| float closestDepth = smz.x;
| float shadow = result;// (currentDepth - 0.0020 * tan(acos(NoL))) - 0.0003 >= closestDepth ? 0.0 : 1.0;
| attenuation *= shadow;
| }
|}
""".trimMargin()
fun Shadows.fs(index: Int): String = when (this) {
is Shadows.PCF -> this.fs(index)
is Shadows.Simple -> this.fs(index)
is Shadows.VSM -> this.fs(index)
is Shadows.None -> ""
else -> TODO()
}

View File

@@ -0,0 +1,187 @@
package org.openrndr.extra.dnk3.cubemap
import org.openrndr.draw.*
import org.openrndr.color.ColorRGBa
import org.openrndr.internal.Driver
import org.openrndr.math.*
import org.openrndr.math.transforms.ortho
private val filterDrawStyle = DrawStyle().apply {
blendMode = BlendMode.REPLACE
depthWrite = false
depthTestPass = DepthTestPass.ALWAYS
stencil.stencilTest = StencilTest.DISABLED
}
private var filterQuad: VertexBuffer? = null
private var filterQuadFormat = vertexFormat {
position(2)
textureCoordinate(2)
}
/**
* Filter base class. Renders "full-screen" quads.
*/
open class CubemapFilter(private val shader: Shader? = null, private val watcher: ShaderWatcher? = null) {
/**
* parameter map
*/
val parameters = mutableMapOf<String, Any>()
var padding = 0
var depthBufferOut: DepthBuffer? = null
companion object {
val filterVertexCode: String get() = Driver.instance.internalShaderResource("filter.vert")
}
open fun apply(source: Array<Cubemap>, target: Array<Cubemap>) {
if (target.isEmpty()) {
return
}
for (side in CubemapSide.values()) {
val renderTarget = renderTarget(target[0].width, target[0].width, 1.0) {}
shader?.begin()
shader?.uniform("sideNormal", side.forward)
shader?.uniform("sideUp", side.up)
shader?.uniform("sideRight", (side.forward cross side.up))
shader?.end()
target.forEach {
renderTarget.attach(it, side, 0)
}
for (i in 1 until target.size) {
renderTarget.blendMode(i, BlendMode.REPLACE)
}
apply(source, renderTarget)
depthBufferOut?.let {
renderTarget.attach(it)
}
if (depthBufferOut != null) {
renderTarget.detachDepthBuffer()
}
renderTarget.detachColorAttachments()
renderTarget.destroy()
}
}
fun apply(source: Array<Cubemap>, target: RenderTarget) {
val shader = if (this.watcher != null) watcher.shader!! else this.shader!!
target.bind()
if (filterQuad == null) {
val fq = VertexBuffer.createDynamic(filterQuadFormat, 6, Session.root)
fq.shadow.writer().apply {
write(Vector2(0.0, 1.0)); write(Vector2(0.0, 0.0))
write(Vector2(0.0, 0.0)); write(Vector2(0.0, 1.0))
write(Vector2(1.0, 0.0)); write(Vector2(1.0, 1.0))
write(Vector2(0.0, 1.0)); write(Vector2(0.0, 0.0))
write(Vector2(1.0, 1.0)); write(Vector2(1.0, 0.0))
write(Vector2(1.0, 0.0)); write(Vector2(1.0, 1.0))
}
fq.shadow.upload()
fq.shadow.destroy()
filterQuad = fq
}
shader.begin()
source.forEachIndexed { index, cubemap ->
cubemap.bind(index)
cubemap.filter(MinifyingFilter.LINEAR, MagnifyingFilter.LINEAR)
shader.uniform("tex$index", index)
}
Driver.instance.setState(filterDrawStyle)
shader.uniform("projectionMatrix", ortho(0.0, target.width.toDouble(), target.height.toDouble(), 0.0, -1.0, 1.0))
shader.uniform("targetSize", Vector2(target.width.toDouble(), target.height.toDouble()))
shader.uniform("padding", Vector2(padding.toDouble(), padding.toDouble()))
var textureIndex = source.size + 0
parameters.forEach { (uniform, value) ->
@Suppress("UNCHECKED_CAST")
when (value) {
is Boolean -> shader.uniform(uniform, value)
is Float -> shader.uniform(uniform, value)
is Double -> shader.uniform(uniform, value.toFloat())
is Matrix44 -> shader.uniform(uniform, value)
is Vector2 -> shader.uniform(uniform, value)
is Vector3 -> shader.uniform(uniform, value)
is Vector4 -> shader.uniform(uniform, value)
is ColorRGBa -> shader.uniform(uniform, value)
is Int -> shader.uniform(uniform, value)
is Matrix55 -> shader.uniform(uniform, value.floatArray)
is FloatArray -> shader.uniform(uniform, value)
// EJ: this is not so nice but I have no other ideas for this
is Array<*> -> if (value.size > 0) when (value[0]) {
is Vector2 -> shader.uniform(uniform, value as Array<Vector2>)
is Vector3 -> shader.uniform(uniform, value as Array<Vector3>)
is Vector4 -> shader.uniform(uniform, value as Array<Vector4>)
else -> throw IllegalArgumentException("unsupported array value: ${value[0]!!::class.java}")
//is ColorRGBa -> shader.uniform(uniform, value as Array<ColorRGBa>)
}
is DepthBuffer -> {
shader.uniform("$uniform", textureIndex)
value.bind(textureIndex)
textureIndex++
}
is ColorBuffer -> {
shader.uniform("$uniform", textureIndex)
value.bind(textureIndex)
textureIndex++
}
is Cubemap -> {
shader.uniform("$uniform", textureIndex)
value.bind(textureIndex)
textureIndex++
}
is ArrayTexture -> {
shader.uniform("$uniform", textureIndex)
value.bind(textureIndex)
textureIndex++
}
is BufferTexture -> {
shader.uniform("$uniform", textureIndex)
value.bind(textureIndex)
textureIndex++
}
}
}
Driver.instance.drawVertexBuffer(shader, listOf(filterQuad!!), DrawPrimitive.TRIANGLES, 0, 6)
shader.end()
target.unbind()
}
fun apply(source: Cubemap, target: Cubemap) = apply(arrayOf(source), arrayOf(target))
fun apply(source: Cubemap, target: Array<Cubemap>) = apply(arrayOf(source), target)
fun apply(source: Array<Cubemap>, target: Cubemap) = apply(source, arrayOf(target))
fun untrack() {
shader?.let { Session.active.untrack(shader) }
}
protected val format get() = filterQuadFormat
}

View File

@@ -0,0 +1,6 @@
package org.openrndr.extra.dnk3.cubemap
import org.openrndr.draw.filterShaderFromUrl
import org.openrndr.resourceUrl
class CubemapPassthrough : CubemapFilter(filterShaderFromUrl(resourceUrl("/shaders/cubemap-filters/cubemap-passthrough.frag")))

View File

@@ -0,0 +1,6 @@
package org.openrndr.extra.dnk3.cubemap
import org.openrndr.draw.filterShaderFromUrl
import org.openrndr.resourceUrl
class IrradianceConvolution : CubemapFilter(filterShaderFromUrl(resourceUrl("/shaders/cubemap-filters/irradiance-convolution.frag")))

View File

@@ -0,0 +1,187 @@
@file:ShaderPhrases([])
package org.openrndr.extra.dnk3.cubemap
import org.openrndr.draw.*
import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases
import org.openrndr.extra.shaderphrases.phraseResource
import org.openrndr.math.Vector3
import org.openrndr.math.max
import org.openrndr.resourceUrl
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.sqrt
class SphericalHarmonics : Filter(filterShaderFromUrl(resourceUrl("/shaders/cubemap-filters/spherical-harmonics.frag"))) {
var input: Cubemap by parameters
}
/** based on https://andrew-pham.blog/2019/08/26/spherical-harmonics/ */
fun Cubemap.irradianceCoefficients(): Array<Vector3> {
val cubemap = this
require(cubemap.format == ColorFormat.RGB)
require(cubemap.type == ColorType.FLOAT32)
val result = Array(9) { Vector3.ZERO }
var buffer = ByteBuffer.allocateDirect(cubemap.width * cubemap.width * cubemap.format.componentCount * cubemap.type.componentSize)
buffer.order(ByteOrder.nativeOrder())
var weightSum = 0.0
for (side in CubemapSide.values()) {
//cubemap.side(side).read(buffer)
buffer.rewind()
cubemap.read(side, buffer)
buffer.rewind()
for (y in 0 until cubemap.width) {
for (x in 0 until cubemap.width) {
val rf = buffer.float.toDouble()
val gf = buffer.float.toDouble()
val bf = buffer.float.toDouble()
val L = Vector3(rf, gf, bf)
var u = (x + 0.5) / cubemap.width;
var v = (y + 0.5) / cubemap.width;
u = u * 2.0 - 1.0
v = v * 2.0 - 1.0
val temp = 1.0 + u * u + v * v
val weight = 4.0 / (sqrt(temp) * temp)
val N = cubemap.mapUVSToN(u, v, side)
val coefficients = genLightingCoefficientsForNormal(N, L)
for (i in 0 until 9) {
result[i] += coefficients[i] * weight
}
weightSum += weight
}
}
}
for (i in 0 until 9) {
result[i] = result[i] * (4.0 * Math.PI) / weightSum
}
return result;
}
fun genSHCoefficients(N: Vector3): DoubleArray {
val result = DoubleArray(9)
// Band 0
result[0] = 0.282095;
// Band 1
result[1] = 0.488603 * N.y
result[2] = 0.488603 * N.z
result[3] = 0.488603 * N.x
// Band 2
result[4] = 1.092548 * N.x * N.y
result[5] = 1.092548 * N.y * N.z
result[6] = 0.315392 * (3.0 * N.z * N.z - 1.0)
result[7] = 1.092548 * N.x * N.z
result[8] = 0.546274 * (N.x * N.x - N.y * N.y)
return result;
}
fun genLightingCoefficientsForNormal(N: Vector3, L: Vector3): Array<Vector3> {
val coefficients = genSHCoefficients(N)
val result = Array(9) { Vector3.ZERO }
for (i in 0 until 9) {
result[i] = L * coefficients[i]
}
return result
}
fun Cubemap.mapUVSToN(u: Double, v: Double, side: CubemapSide): Vector3 {
return (side.right * u + side.up * v + side.forward).normalized
}
// Evaluates the irradiance perceived in the provided direction
// Analytic method from http://www1.cs.columbia.edu/~ravir/papers/envmap/envmap.pdf eq. 13
//
fun evaluateSHIrradiance(direction: Vector3, _SH: Array<Vector3>): Vector3 {
val c1 = 0.42904276540489171563379376569857; // 4 * Â2.Y22 = 1/4 * sqrt(15.PI)
val c2 = 0.51166335397324424423977581244463; // 0.5 * Â1.Y10 = 1/2 * sqrt(PI/3)
val c3 = 0.24770795610037568833406429782001; // Â2.Y20 = 1/16 * sqrt(5.PI)
val c4 = 0.88622692545275801364908374167057; // Â0.Y00 = 1/2 * sqrt(PI)
val x = direction.x;
val y = direction.y;
val z = direction.z;
return max(Vector3.ZERO,
_SH[8] * (c1 * (x * x - y * y)) // c1.L22.(x²-y²)
+ _SH[6] * (c3 * (3.0 * z * z - 1)) // c3.L20.(3.z² - 1)
+ _SH[0] * c4 // c4.L00
+ (_SH[4] * x * y + _SH[7] * x * z + _SH[5] * y * z) * 2.0 * c1 // 2.c1.(L2-2.xy + L21.xz + L2-1.yz)
+ (_SH[3] * x + _SH[1] * y + _SH[2] * z) * c2 * 2.0); // 2.c2.(L11.x + L1-1.y + L10.z)
}
val glslEvaluateSH: String by phraseResource("/phrases/irradiance-sh/evaluate-sh.frag")
val glslFetchSH: String by phraseResource("/phrases/irradiance-sh/fetch-sh.frag")
val glslFetchSH0: String by phraseResource("/phrases/irradiance-sh/fetch-sh0.frag")
fun genGlslGatherSH(xProbes: Int, yProbes: Int, zProbes: Int, spacing: Double = 1.0, offset: Vector3) = """
ivec3 gridCoordinates(vec3 p, out vec3 f) {
float x = (p.x - ${offset.x}) / $spacing;
float y = (p.y - ${offset.y})/ $spacing;
float z = (p.z - ${offset.z}) / $spacing;
int ix = int(floor(x)) + $xProbes / 2;
int iy = int(floor(y)) + $yProbes / 2;
int iz = int(floor(z)) + $zProbes / 2;
f.x = fract((x));
f.y = fract((y));
f.z = fract((z));
return ivec3(ix, iy, iz);
}
int gridIndex(ivec3 p) {
ivec3 c = clamp(p, ivec3(0), ivec3(${xProbes - 1}, ${yProbes - 1}, ${zProbes - 1}));
return c.x + c.y * $xProbes + c.z * ${xProbes * yProbes};
}
void gatherSH(samplerBuffer btex, vec3 p, out vec3[9] blend) {
vec3[9] c000;
vec3[9] c001;
vec3[9] c010;
vec3[9] c011;
vec3[9] c100;
vec3[9] c101;
vec3[9] c110;
vec3[9] c111;
vec3 f;
ivec3 io = gridCoordinates(p, f);
fetchSH(btex, gridIndex(io + ivec3(0,0,0)), c000);
fetchSH(btex, gridIndex(io + ivec3(0,0,1)), c001);
fetchSH(btex, gridIndex(io + ivec3(0,1,0)), c010);
fetchSH(btex, gridIndex(io + ivec3(0,1,1)), c011);
fetchSH(btex, gridIndex(io + ivec3(1,0,0)), c100);
fetchSH(btex, gridIndex(io + ivec3(1,0,1)), c101);
fetchSH(btex, gridIndex(io + ivec3(1,1,0)), c110);
fetchSH(btex, gridIndex(io + ivec3(1,1,1)), c111);
for (int i = 0; i < 9; ++i) {
blend[i] = mix( mix( mix(c000[i], c001[i], f.z), mix(c010[i], c011[i], f.z), f.y), mix( mix(c100[i], c101[i], f.z), mix(c110[i], c111[i], f.z), f.y), f.x);
}
}
""".trimIndent()
val glslGridCoordinates: String by phraseResource("/phrases/irradiance-sh/grid-coordinates.frag")
val glslGridIndex: String by phraseResource("/phrases/irradiance-sh/grid-index.frag")
val glslGatherSH: String by phraseResource("/phrases/irradiance-sh/gather-sh.frag")
val glslGatherSH0: String by phraseResource("/phrases/irradiance-sh/gather-sh0.frag")

View File

@@ -0,0 +1,12 @@
package org.openrndr.extra.dnk3.dsl
import org.openrndr.extra.dnk3.PBRMaterial
fun pbrMaterial(builder: PBRMaterial.() -> Unit): PBRMaterial {
return PBRMaterial().apply { builder() }
}
fun test() {
pbrMaterial {
}
}

View File

@@ -0,0 +1,88 @@
package org.openrndr.extra.dnk3.dsl
import kotlinx.coroutines.yield
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.VertexBuffer
import org.openrndr.extra.dnk3.*
import org.openrndr.launch
fun scene(builder: Scene.() -> Unit): Scene {
val scene = Scene()
scene.builder()
return scene
}
fun SceneNode.node(builder: SceneNode.() -> Unit): SceneNode {
val node = SceneNode()
node.builder()
children.add(node)
return node
}
fun SceneNode.hemisphereLight(builder: HemisphereLight.() -> Unit): HemisphereLight {
val hemisphereLight = HemisphereLight()
hemisphereLight.builder()
entities.add(hemisphereLight)
return hemisphereLight
}
fun SceneNode.directionalLight(buider: DirectionalLight.() -> Unit): DirectionalLight {
val directionalLight = DirectionalLight()
directionalLight.buider()
this.entities.add(directionalLight)
return directionalLight
}
fun SceneNode.pointLight(builder: PointLight.() -> Unit): PointLight {
val pointLight = PointLight()
pointLight.builder()
this.entities.add(pointLight)
return pointLight
}
fun SceneNode.spotLight(builder: SpotLight.() -> Unit): SpotLight {
val spotLight = SpotLight()
spotLight.builder()
this.entities.add(spotLight)
return spotLight
}
class SimpleMeshBuilder {
var vertexBuffer: VertexBuffer? = null
var primitive = DrawPrimitive.TRIANGLES
var material: Material? = null
fun build(): Mesh {
val geometry = Geometry(
listOf(vertexBuffer ?: error("no vertex buffer")),
null,
primitive,
0,
vertexBuffer?.vertexCount ?: error("no vertex buffer")
)
val primitive = MeshPrimitive(geometry, material ?: error("no material"))
return Mesh(listOf(primitive))
}
}
fun SceneNode.simpleMesh(builder: SimpleMeshBuilder.() -> Unit): Mesh {
val mesh = SimpleMeshBuilder().apply { builder() }.build()
entities.add(mesh)
return mesh
}
fun SceneNode.pathMesh(builder: PathMesh.() -> Unit): PathMesh {
val pathMesh = PathMesh(mutableListOf(), DummyMaterial(), 1.0)
pathMesh.builder()
entities.add(pathMesh)
return pathMesh
}
fun Scene.update(function: () -> Unit) {
dispatcher.launch {
while (true) {
function()
yield()
}
}
}

View File

@@ -0,0 +1,109 @@
package org.openrndr.extra.dnk3.features
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.cubemap.irradianceCoefficients
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class IrradianceSH(val xCount: Int, val yCount: Int, val zCount: Int, val spacing: Double, val offset: Vector3, val cubemapSize: Int) : Feature {
override fun <T : Feature> update(drawer: Drawer, sceneRenderer: SceneRenderer, scene: Scene, feature: T, context: RenderContext) {
sceneRenderer.processIrradiance(drawer, scene, feature as IrradianceSH, context)
}
var shMap: BufferTexture? = null
val probeCount
get() = xCount * yCount * zCount
}
fun Scene.addIrradianceSH(xCount: Int,
yCount: Int,
zCount: Int,
spacing: Double,
offset: Vector3 = Vector3.ZERO,
cubemapSize: Int = 256
) {
features.add(IrradianceSH(xCount * 2 + 1, yCount * 2 + 1, zCount * 2 + 1, spacing, offset, cubemapSize))
var probeID = 0
for (k in -zCount..zCount) {
for (j in -yCount..yCount) {
for (i in -xCount..xCount) {
val probeNode = SceneNode()
probeNode.transform = transform {
translate(offset)
translate(i * spacing, j * spacing, k * spacing)
}
probeNode.entities.add(IrradianceProbe())
probeID++
root.children.add(probeNode)
}
}
}
}
private fun SceneRenderer.processIrradiance(drawer: Drawer, scene: Scene, feature: IrradianceSH, context: RenderContext) {
val irradianceProbes = scene.root.findContent { this as? IrradianceProbe }
val irradianceProbePositions = irradianceProbes.map { it.node.worldPosition }
if (feature.shMap == null && irradianceProbes.isNotEmpty()) {
val hash = scene.hash()
val cached = File("data/scene-cache/sh-$hash.orb")
if (cached.exists()) {
feature.shMap = loadBufferTexture(cached)
} else {
var probeID = 0
val tempCubemap = cubemap(feature.cubemapSize, format = ColorFormat.RGB, type = ColorType.FLOAT32)
var cubemapDepthBuffer = depthBuffer(feature.cubemapSize, feature.cubemapSize, DepthFormat.DEPTH16, BufferMultisample.Disabled)
feature.shMap = bufferTexture(irradianceProbes.size * 9, format = ColorFormat.RGB, type = ColorType.FLOAT32)
val buffer = ByteBuffer.allocateDirect(irradianceProbePositions.size * 9 * 3 * 4)
buffer.order(ByteOrder.nativeOrder())
for ((node, probe) in irradianceProbes) {
if (probe.dirty) {
val pass = IrradianceProbePass
val materialContext = MaterialContext(pass, context.lights, emptyList(), shadowLightTargets, emptyMap(), 0)
val position = node.worldPosition
for (side in CubemapSide.values()) {
val target = renderTarget(feature.cubemapSize, feature.cubemapSize) {
//this.colorBuffer(tempCubemap.side(side))
this.cubemap(tempCubemap, side)
this.depthBuffer(cubemapDepthBuffer)
}
drawer.isolatedWithTarget(target) {
drawer.clear(ColorRGBa.BLACK)
drawer.projection = probe.projectionMatrix
drawer.view = Matrix44.IDENTITY
drawer.model = Matrix44.IDENTITY
drawer.lookAt(position, position + side.forward, side.up)
drawPass(drawer, pass, materialContext, context)
}
target.detachDepthBuffer()
target.detachColorAttachments()
target.destroy()
}
val coefficients = tempCubemap.irradianceCoefficients()
for (coef in coefficients) {
buffer.putVector3((coef))
}
probeID++
println("$probeID / ${irradianceProbePositions.size}")
probe.dirty = false
}
}
feature.shMap?.let {
buffer.rewind()
it.write(buffer)
it.saveToFile(File("data/scene-cache/sh-$hash.orb"))
}
}
}
}

View File

@@ -0,0 +1,71 @@
package org.openrndr.extra.dnk3.features
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.*
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
data class VoxelConeTracing(val xCount: Int, val yCount: Int, val zCount: Int, val spacing: Double, val offset: Vector3) : Feature {
var voxelMap: VolumeTexture? = null
var voxelRenderTarget = null as? RenderTarget?
override fun <T : Feature> update(drawer: Drawer, sceneRenderer: SceneRenderer, scene: Scene, feature: T, context: RenderContext) {
sceneRenderer.processVoxelConeTracing(drawer, scene, this, context)
}
var initialized = false
val voxelPass = RenderPass(listOf(VoxelFacet(this)), renderOpaque = true, renderTransparent = false, depthWrite = false, skipTarget = true)
}
fun Scene.addVoxelConeTracing(xCount: Int, yCount: Int, zCount: Int, spacing: Double, offset: Vector3 = Vector3.ZERO) : VoxelConeTracing {
val feature = VoxelConeTracing(xCount, yCount, zCount, spacing, offset)
features.add(feature)
return feature
}
class VoxelFacet(val voxelConeTracing: VoxelConeTracing) : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR, FacetType.EMISSIVE), "color", ColorFormat.RGBa, ColorType.FLOAT16) {
override fun generateShader() = """
vec3 finalColor = (max(vec3(0.0), f_diffuse.rgb) + max(vec3(0.0), f_emission.rgb) + max(vec3(0.0), f_ambient.rgb));
vec3 p = v_worldPosition;
{
float x = (p.x - ${voxelConeTracing.offset.x}) / ${voxelConeTracing.spacing};
float y = (p.y - ${voxelConeTracing.offset.y}) / ${voxelConeTracing.spacing};
float z = (p.z - ${voxelConeTracing.offset.z}) / ${voxelConeTracing.spacing};
int ix = int(floor(x+0.5)) + ${voxelConeTracing.xCount} / 2;
int iy = int(floor(y+0.5)) + ${voxelConeTracing.yCount} / 2;
int iz = int(floor(z+0.5)) + ${voxelConeTracing.zCount} / 2;
imageStore(p_voxelMap, ivec3(ix, iy, iz), vec4(finalColor, 1.0));
}
"""
}
private fun SceneRenderer.processVoxelConeTracing(drawer: Drawer, scene: Scene, feature: VoxelConeTracing, context: RenderContext) {
if (feature.voxelMap == null) {
feature.voxelMap = volumeTexture(feature.xCount * 2 + 1, feature.yCount * 2 + 1, feature.zCount * 2 + 1, format = ColorFormat.RGBa, type = ColorType.FLOAT16)
}
if (feature.voxelRenderTarget == null) {
feature.voxelRenderTarget = renderTarget(2048, 2048, 1.0, BufferMultisample.SampleCount(8)) {
colorBuffer()
}
}
if (!feature.initialized) {
println("drawing voxelmap")
for (side in CubemapSide.values()) {
drawer.isolatedWithTarget(feature.voxelRenderTarget ?: error("no render target")) {
val pass = feature.voxelPass
val materialContext = MaterialContext(pass, context.lights, emptyList(), shadowLightTargets, emptyMap(), 0)
drawer.clear(ColorRGBa.BLACK)
drawer.ortho(-10.0, 10.0, -10.0, 10.0, -40.0, 40.0)
drawer.view = Matrix44.IDENTITY
drawer.model = Matrix44.IDENTITY
val position = Vector3.ZERO
drawer.lookAt(position + side.forward*40.0, position , side.up)
drawPass(drawer, pass, materialContext, context) {
it.parameter("voxelMap", feature.voxelMap!!.imageBinding(0, ImageAccess.WRITE))
}
}
}
feature.initialized = true
}
}

View File

@@ -0,0 +1,47 @@
package org.openrndr.extra.dnk3.gltf
import com.google.gson.Gson
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.ByteOrder
fun loadGltfFromGlbFile(file: File): GltfFile {
val channel = RandomAccessFile(file, "r").channel
val headerBuffer = ByteBuffer.allocate(12).order(ByteOrder.nativeOrder())
headerBuffer.rewind()
channel.read(headerBuffer)
headerBuffer.rewind()
val magic = headerBuffer.int
val version = headerBuffer.int
val length = headerBuffer.int
fun readChunk(): ByteBuffer {
val chunkHeader = ByteBuffer.allocate(8).order(ByteOrder.nativeOrder())
channel.read(chunkHeader)
chunkHeader.rewind()
val chunkLength = chunkHeader.int
val chunkType = chunkHeader.int
val chunkBuffer =
if (chunkType == 0x004E4942) ByteBuffer.allocateDirect(chunkLength) else ByteBuffer.allocate(chunkLength)
(chunkBuffer as ByteBuffer)
channel.read(chunkBuffer)
chunkBuffer.order(ByteOrder.nativeOrder())
return chunkBuffer
}
val jsonBuffer = readChunk()
jsonBuffer.rewind()
val jsonByteArray = ByteArray(jsonBuffer.capacity())
jsonBuffer.get(jsonByteArray)
val json = String(jsonByteArray)
val gson = Gson()
val bufferBuffer = if (channel.position() < length) readChunk() else null
return gson.fromJson(json, GltfFile::class.java).apply {
this.file = file
this.bufferBuffer = bufferBuffer
}
}

View File

@@ -0,0 +1,307 @@
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package org.openrndr.extra.dnk3.gltf
import com.google.gson.Gson
import org.openrndr.draw.*
import java.io.File
import java.io.RandomAccessFile
import java.nio.Buffer
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*
import kotlin.collections.LinkedHashMap
import kotlin.math.max
const val GLTF_FLOAT = 5126
const val GLTF_UNSIGNED_INT = 5125
const val GLTF_INT = 5124
const val GLTF_UNSIGNED_SHORT = 5123
const val GLTF_SHORT = 5122
const val GLTF_UNSIGNED_BYTE = 5121
const val GLTF_BYTE = 5120
const val GLTF_ARRAY_BUFFER = 34962
const val GLTF_ELEMENT_ARRAY_BUFFER = 34963
data class GltfAsset(val generator: String?, val version: String?)
data class GltfScene(val nodes: IntArray)
data class GltfNode(val name: String?,
val children: IntArray?,
val matrix: DoubleArray?,
val scale: DoubleArray?,
val rotation: DoubleArray?,
val translation: DoubleArray?,
val mesh: Int?,
val skin: Int?,
val camera: Int?,
val extensions: GltfNodeExtensions?)
data class KHRLightsPunctualIndex(val light: Int)
data class GltfNodeExtensions(val KHR_lights_punctual: KHRLightsPunctualIndex?) {
}
data class GltfPrimitive(val attributes: LinkedHashMap<String, Int>, val indices: Int?, val mode: Int?, val material: Int) {
fun createDrawCommand(gltfFile: GltfFile): GltfDrawCommand {
val indexBuffer = indices?.let { indices ->
val accessor = gltfFile.accessors[indices]
val indexType = when (accessor.componentType) {
GLTF_UNSIGNED_SHORT -> IndexType.INT16
GLTF_UNSIGNED_INT -> IndexType.INT32
else -> error("unsupported index type: ${accessor.componentType}")
}
val bufferView = gltfFile.bufferViews[accessor.bufferView]
val buffer = gltfFile.buffers[bufferView.buffer]
val contents = buffer.contents(gltfFile)
(contents as Buffer).limit(contents.capacity())
(contents as Buffer).position((bufferView.byteOffset ?: 0) + (accessor.byteOffset))
(contents as Buffer).limit((bufferView.byteOffset ?: 0) + (accessor.byteOffset)
+ accessor.count * indexType.sizeInBytes)
val ib = indexBuffer(accessor.count, indexType)
ib.write(contents)
ib
}
var maxCount = 0
val accessors = mutableListOf<GltfAccessor>()
val format = vertexFormat {
for ((name, index) in attributes.toSortedMap()) {
val accessor = gltfFile.accessors[index]
maxCount = max(accessor.count, maxCount)
when (name) {
"NORMAL" -> {
normal(3)
accessors.add(accessor)
}
"POSITION" -> {
position(3)
accessors.add(accessor)
}
"TANGENT" -> {
attribute("tangent", VertexElementType.VECTOR4_FLOAT32)
accessors.add(accessor)
}
"TEXCOORD_0" -> {
val dimensions = when (accessor.type) {
"SCALAR" -> 1
"VEC2" -> 2
"VEC3" -> 3
else -> error("unsupported texture coordinate type ${accessor.type}")
}
textureCoordinate(dimensions, 0)
accessors.add(accessor)
}
"JOINTS_0" -> {
val type = when (Pair(accessor.type, accessor.componentType)) {
Pair("VEC4", GLTF_UNSIGNED_BYTE) -> VertexElementType.VECTOR4_UINT8
Pair("VEC4", GLTF_UNSIGNED_SHORT) -> VertexElementType.VECTOR4_UINT16
else -> error("not supported ${accessor.type} / ${accessor.componentType}")
}
attribute("joints", type)
accessors.add(accessor)
}
"WEIGHTS_0" -> {
val type = when (Pair(accessor.type, accessor.componentType)) {
Pair("VEC4", GLTF_FLOAT) -> VertexElementType.VECTOR4_FLOAT32
else -> error("not supported ${accessor.type} / ${accessor.componentType}")
}
attribute("weights", type)
accessors.add(accessor)
}
}
}
}
val buffers =
accessors.map { it.bufferView }
.distinct()
.associate {
Pair(gltfFile.bufferViews[it].buffer,
gltfFile.buffers[gltfFile.bufferViews[it].buffer].contents(gltfFile))
}
val vb = vertexBuffer(format, maxCount)
vb.put {
for (i in 0 until maxCount) {
for (a in accessors) {
val bufferView = gltfFile.bufferViews[a.bufferView]
val buffer = buffers[bufferView.buffer] ?: error("no buffer ${bufferView.buffer}")
val componentSize = when (a.componentType) {
GLTF_BYTE, GLTF_UNSIGNED_BYTE -> 1
GLTF_SHORT, GLTF_UNSIGNED_SHORT -> 2
GLTF_FLOAT, GLTF_UNSIGNED_INT, GLTF_INT -> 4
else -> error("unsupported type")
}
val componentCount = when (a.type) {
"SCALAR" -> 1
"VEC2" -> 2
"VEC3" -> 3
"VEC4" -> 4
"MAT2" -> 4
"MAT3" -> 9
"MAT4" -> 16
else -> error("unsupported type")
}
val size = componentCount * componentSize
val offset = (bufferView.byteOffset ?: 0) + a.byteOffset + i * (bufferView.byteStride ?: size)
copyBuffer(buffer, offset, size)
}
}
}
val drawPrimitive = when (mode) {
null, 4 -> DrawPrimitive.TRIANGLES
5 -> DrawPrimitive.TRIANGLE_STRIP
else -> error("unsupported mode $mode")
}
return GltfDrawCommand(vb, indexBuffer, drawPrimitive, indexBuffer?.indexCount ?: maxCount)
}
}
data class GltfMesh(val primitives: List<GltfPrimitive>, val name: String) {
fun createDrawCommands(gltfFile: GltfFile): List<GltfDrawCommand> {
return primitives.map { it.createDrawCommand(gltfFile) }
}
}
data class GltfPbrMetallicRoughness(val baseColorFactor: DoubleArray?,
val baseColorTexture: GltfMaterialTexture?,
var metallicRoughnessTexture: GltfMaterialTexture?,
val roughnessFactor: Double?,
val metallicFactor: Double?)
data class GltfMaterialTexture(val index: Int, val scale: Double?, val texCoord: Int?)
data class GltfImage(val uri: String?, val bufferView: Int?)
data class GltfSampler(val magFilter: Int, val minFilter: Int, val wrapS: Int, val wrapT: Int)
data class GltfTexture(val sampler: Int, val source: Int)
data class GltfMaterial(val name: String,
val alphaMode: String?,
val doubleSided: Boolean?,
val normalTexture: GltfMaterialTexture?,
val occlusionTexture: GltfMaterialTexture?,
val emissiveTexture: GltfMaterialTexture?,
val emissiveFactor: DoubleArray?,
val pbrMetallicRoughness: GltfPbrMetallicRoughness?,
val extensions: GltfMaterialExtensions?
)
data class GltfMaterialExtensions(
val KHR_materials_pbrSpecularGlossiness: KhrMaterialsPbrSpecularGlossiness?
)
class KhrMaterialsPbrSpecularGlossiness(val diffuseFactor: DoubleArray?, val diffuseTexture: GltfMaterialTexture?)
data class GltfBufferView(val buffer: Int,
val byteOffset: Int?,
val byteLength: Int,
val byteStride: Int?,
val target: Int)
data class GltfBuffer(val byteLength: Int, val uri: String?) {
fun contents(gltfFile: GltfFile): ByteBuffer = if (uri != null) {
if (uri.startsWith("data:")) {
val base64 = uri.substring(uri.indexOf(",") + 1)
val decoded = Base64.getDecoder().decode(base64)
val buffer = ByteBuffer.allocateDirect(decoded.size)
buffer.order(ByteOrder.nativeOrder())
buffer.put(decoded)
buffer.rewind()
buffer
} else {
val raf = RandomAccessFile(File(gltfFile.file.parentFile, uri), "r")
val buffer = ByteBuffer.allocateDirect(byteLength)
buffer.order(ByteOrder.nativeOrder())
buffer.rewind()
raf.channel.read(buffer)
buffer.rewind()
buffer
}
} else {
gltfFile.bufferBuffer ?: error("no embedded buffer from glb")
}
}
data class GltfDrawCommand(val vertexBuffer: VertexBuffer, val indexBuffer: IndexBuffer?, val primitive: DrawPrimitive, var vertexCount: Int)
data class GltfAccessor(
val bufferView: Int,
val byteOffset: Int,
val componentType: Int,
val count: Int,
val max: DoubleArray,
val min: DoubleArray,
val type: String
)
data class GltfAnimation(val name: String?, val channels: List<GltfChannel>, val samplers: List<GltfAnimationSampler>)
data class GltfAnimationSampler(val input: Int, val interpolation: String, val output: Int)
data class GltfChannelTarget(val node: Int?, val path: String?)
data class GltfChannel(val sampler: Int, val target: GltfChannelTarget)
data class GltfSkin(val inverseBindMatrices: Int, val joints: IntArray, val skeleton: Int)
data class KHRLightsPunctualLight(val color: DoubleArray?, val type: String, val intensity: Double?, val range: Double, val spot: KHRLightsPunctualLightSpot?)
data class KHRLightsPunctualLightSpot(val innerConeAngle: Double?, val outerConeAngle: Double?)
data class KHRLightsPunctual(val lights: List<KHRLightsPunctualLight>)
data class GltfExtensions(val KHR_lights_punctual: KHRLightsPunctual?)
data class GltfCameraPerspective(val aspectRatio: Double?, val yfov: Double, val zfar: Double?, val znear: Double)
data class GltfCameraOrthographic(val xmag: Double, val ymag: Double, val zfar: Double, val znear: Double)
data class GltfCamera(val name: String?, val type: String, val perspective: GltfCameraPerspective?, val orthographic: GltfCameraOrthographic?)
class GltfFile(
val asset: GltfAsset?,
val scene: Int?,
val scenes: List<GltfScene>,
val nodes: List<GltfNode>,
val meshes: List<GltfMesh>,
val accessors: List<GltfAccessor>,
val materials: List<GltfMaterial>,
val bufferViews: List<GltfBufferView>,
val buffers: List<GltfBuffer>,
val images: List<GltfImage>?,
val textures: List<GltfTexture>?,
val samplers: List<GltfSampler>?,
val animations: List<GltfAnimation>?,
val skins: List<GltfSkin>?,
val extensions: GltfExtensions?,
val cameras: List<GltfCamera>?
) {
@Transient
lateinit var file: File
@Transient
var bufferBuffer: ByteBuffer? = null
}
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

@@ -0,0 +1,439 @@
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.Buffer
import java.nio.ByteOrder
import kotlin.reflect.KMutableProperty0
class SceneAnimation(var channels: List<AnimationChannel>) {
val duration: Double
get() {
return channels.maxByOrNull { it.duration }?.duration ?: 0.0
}
fun applyToTargets(input: Double) {
for (channel in channels) {
channel.applyToTarget(input)
}
}
}
sealed class AnimationChannel {
abstract val duration: Double
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)
}
override val duration: Double
get() = keyframer.duration()
}
class Vector3Channel(
val target: KMutableProperty0<Vector3>,
val keyframer: KeyframerChannelVector3, val default: Vector3
) : AnimationChannel() {
override fun applyToTarget(input: Double) {
target.set(keyframer.value(input) ?: default)
}
override val duration: Double
get() = keyframer.duration()
}
class GltfSceneNode : SceneNode() {
var translation = Vector3.ZERO
var scale = Vector3.ONE
var rotation = Quaternion.IDENTITY
override fun toString(): String {
return "translation: $translation, scale: $scale, rotation: $rotation, children: ${children.size}, entities: ${entities} "
}
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(): GltfSceneData {
val sceneImages = mutableMapOf<GltfImage, ColorBuffer>()
fun GltfImage.createSceneImage(): ColorBuffer {
return sceneImages.getOrPut(this) {
if (uri == null) {
bufferView?.let { bv ->
val localBufferView = bufferViews[bv]
val localBuffer = buffers[localBufferView.buffer].contents(this@buildSceneNodes)
require(localBufferView.byteOffset != null)
require(localBufferView.byteLength != null)
localBuffer.position(localBufferView.byteOffset)
localBuffer.limit(localBufferView.byteOffset + localBufferView.byteLength)
val cb = ColorBuffer.fromBuffer(localBuffer, null)
cb.generateMipmaps()
cb.filter(MinifyingFilter.LINEAR_MIPMAP_LINEAR, MagnifyingFilter.LINEAR)
cb.anisotropy = 100.0
localBuffer.limit(localBuffer.capacity())
cb
} ?: error("no uri and no bufferview")
} else {
if (uri.startsWith("data:")) {
loadImage(uri)
} else {
loadImage(File(file.parent, uri))
}
}
}
}
val sceneMaterials = mutableMapOf<GltfMaterial, Material>()
fun GltfMaterial.createSceneMaterial(): Material = sceneMaterials.getOrPut(this) {
val material = PBRMaterial()
material.name = this.name
material.doubleSided = this.doubleSided ?: false
material.transparent = this.alphaMode != null
pbrMetallicRoughness?.let { pbr ->
material.roughness = pbr.roughnessFactor ?: 1.0
material.metalness = pbr.metallicFactor ?: 1.0
material.color = ColorRGBa.WHITE
pbr.baseColorFactor?.let {
material.color = ColorRGBa(it[0], it[1], it[2], it[3])
}
pbr.baseColorTexture?.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.COLOR
)
material.textures.add(sceneTexture)
}
pbr.metallicRoughnessTexture?.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.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)
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)
}
normalTexture?.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,
tangentInput = "va_tangent",
pre = "x_texCoord.y = 1.0-x_texCoord.y;"
), TextureTarget.NORMAL
)
material.textures.add(sceneTexture)
}
emissiveFactor?.let {
material.emission = ColorRGBa(it[0], it[1], it[2])
}
emissiveTexture?.let {
val cb = images!![textures!![it.index].source].createSceneImage()
val sceneTexture = Texture(
ModelCoordinates(texture = cb, pre = "x_texCoord.y = 1.0-x_texCoord.y;"),
TextureTarget.EMISSION
)
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])
}
sg.diffuseTexture?.let {
val cb = images!![textures!![it.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.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)
}
material
}
fun GltfPrimitive.createScenePrimitive(): MeshPrimitive {
val drawCommand = createDrawCommand(this@buildSceneNodes)
val geometry = Geometry(
listOf(drawCommand.vertexBuffer),
drawCommand.indexBuffer,
drawCommand.primitive,
0,
drawCommand.vertexCount
)
val material = materials.getOrNull(material)?.createSceneMaterial() ?: PBRMaterial()
return MeshPrimitive(geometry, material)
}
val sceneNodes = mutableMapOf<GltfNode, SceneNode>()
fun GltfNode.createSceneNode(): SceneNode = sceneNodes.getOrPut(this) {
val node = GltfSceneNode()
node.name = name ?: "no name"
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
matrix?.let {
node.transform = Matrix44.fromDoubleArray(it).transposed
}
for (child in children.orEmpty) {
val childNode = nodes.getOrNull(child) ?: error("child node not found: $child")
node.children.add(childNode.createSceneNode())
}
node
}
val sceneMeshes = mutableMapOf<GltfMesh, MeshBase>()
fun GltfMesh.createSceneMesh(skin: GltfSkin?): MeshBase = sceneMeshes.getOrPut(this) {
if (skin == null) {
Mesh(primitives.map {
it.createScenePrimitive()
})
} else {
val joints = skin.joints.map { nodes[it].createSceneNode() }
val skeleton = nodes[skin.skeleton].createSceneNode()
val ibmAccessor = accessors[skin.inverseBindMatrices]
val ibmBufferView = bufferViews[ibmAccessor.bufferView]
val ibmBuffer = buffers[ibmBufferView.buffer]
val ibmData = ibmBuffer.contents(this@buildSceneNodes)
ibmData.order(ByteOrder.nativeOrder())
(ibmData as Buffer).position(ibmAccessor.byteOffset + (ibmBufferView.byteOffset ?: 0))
require(ibmAccessor.type == "MAT4")
require(ibmAccessor.componentType == GLTF_FLOAT)
require(ibmAccessor.count == joints.size)
val ibms = (0 until ibmAccessor.count).map {
val array = DoubleArray(16)
for (i in 0 until 16) {
array[i] = ibmData.float.toDouble()
}
Matrix44.fromDoubleArray(array).transposed
}
SkinnedMesh(primitives.map {
it.createScenePrimitive()
}, joints, skeleton, ibms)
}
}
fun GltfCamera.createSceneCamera(sceneNode: SceneNode): Camera {
return when (type) {
"perspective" -> {
PerspectiveCamera(sceneNode).apply {
aspectRatio = perspective?.aspectRatio ?: aspectRatio
far = perspective?.zfar ?: far
near = perspective?.znear ?: near
fov = perspective?.yfov?.let { Math.toDegrees(it) } ?: fov
}
}
"orthographic" -> {
OrthographicCamera(sceneNode).apply {
xMag = orthographic?.xmag ?: xMag
yMag = orthographic?.ymag ?: yMag
near = orthographic?.znear ?: near
far = orthographic?.zfar ?: far
}
}
else -> error("unsupported camera type: $type")
}
}
val scenes = scenes.map { scene ->
scene.nodes.map { node ->
val gltfNode = nodes.getOrNull(node) ?: error("node not found: $node")
require(gltfNode != null)
val sceneNode = gltfNode.createSceneNode()
sceneNode
}
}
for ((gltfNode, sceneNode) in sceneNodes) {
gltfNode.mesh?.let {
val skin = gltfNode.skin?.let { (skins!!)[it] }
sceneNode.entities.add(meshes[it].createSceneMesh(skin))
}
gltfNode.camera?.let {
sceneNode.entities.add(cameras!![it].createSceneCamera(sceneNode))
}
gltfNode.extensions?.let { exts ->
exts.KHR_lights_punctual?.let { lightIndex ->
extensions?.KHR_lights_punctual?.lights?.get(lightIndex.light)?.let { light ->
val sceneLight = when (light.type) {
"point" -> {
PointLight()
}
"directional" -> {
DirectionalLight().apply {
shadows = Shadows.PCF()
}
}
"spot" -> {
SpotLight().apply {
innerAngle = Math.toDegrees(light.spot!!.innerConeAngle ?: 0.0)
outerAngle = Math.toDegrees(light.spot.outerConeAngle ?: Math.PI / 4.0)
shadows = Shadows.PCF()
}
}
else -> error("unsupported light type ${light.type}")
}
sceneLight.apply {
val lightColor = (light.color ?: doubleArrayOf(1.0, 1.0, 1.0))
color = ColorRGBa(lightColor[0], lightColor[1], lightColor[2])
}
sceneNode.entities.add(sceneLight)
}
}
}
}
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)
inputData.limit(inputData.capacity())
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) + (outputAccessor.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

@@ -0,0 +1,71 @@
package org.openrndr.extra.dnk3.materials
import org.openrndr.draw.ShadeStyle
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.dnk3.Material
import org.openrndr.extra.dnk3.MaterialContext
import org.openrndr.extra.dnk3.PrimitiveContext
import org.openrndr.extra.dnk3.cubemap.glslEvaluateSH
import org.openrndr.extra.dnk3.cubemap.glslFetchSH
import org.openrndr.extra.dnk3.cubemap.genGlslGatherSH
class IrradianceDebugMaterial : Material {
override val name: String? = null
override var doubleSided: Boolean = false
override var transparent: Boolean = false
override val fragmentID: Int = 0
override fun generateShadeStyle(context: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle {
return shadeStyle {
fragmentPreamble = """
$glslEvaluateSH
$glslFetchSH
${genGlslGatherSH(context.irradianceSH!!.xCount, context.irradianceSH!!.yCount, context.irradianceSH!!.zCount, context.irradianceSH!!.spacing, context.irradianceSH!!.offset)}
vec3 f_emission = vec3(0.0);
"""
if (context.irradianceSH != null) {
fragmentTransform = """
vec3[9] sh;
gatherSH(p_shMap, v_worldPosition, sh);
x_fill.rgb = evaluateSH(normalize(v_worldNormal), sh);
""".trimIndent()
} else {
fragmentTransform = """
discard;
"""
}
}
}
override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) {
context.irradianceSH?.shMap?.let {
shadeStyle.parameter("shMap", it)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is IrradianceDebugMaterial) return false
if (name != other.name) return false
if (doubleSided != other.doubleSided) return false
if (transparent != other.transparent) return false
if (fragmentID != other.fragmentID) return false
return true
}
override fun hashCode(): Int {
var result = name?.hashCode() ?: 0
result = 31 * result + doubleSided.hashCode()
result = 31 * result + transparent.hashCode()
result = 31 * result + fragmentID
return result
}
}

View File

@@ -0,0 +1,38 @@
package org.openrndr.extra.dnk3.post
import org.openrndr.draw.Filter
import org.openrndr.draw.Shader
import org.openrndr.draw.filterShaderFromUrl
import org.openrndr.math.Matrix44
import org.openrndr.resourceUrl
class ScreenspaceReflections : Filter(preprocessedFilterShaderFromUrl(resourceUrl("/shaders/screenspace-reflections.frag"))) {
var projection: Matrix44 by parameters
var projectionMatrixInverse: Matrix44 by parameters
var colors: Int by parameters
var projDepth: Int by parameters
var normals: Int by parameters
var jitterOriginGain: Double by parameters
var iterationLimit: Int by parameters
var distanceLimit: Double by parameters
var gain: Double by parameters
var borderWidth: Double by parameters
init {
colors = 0
projDepth = 1
normals = 2
projection = Matrix44.IDENTITY
projectionMatrixInverse = Matrix44.IDENTITY
distanceLimit = 100.0
iterationLimit = 128
jitterOriginGain = 0.0
gain = 1.0
borderWidth = 130.0
}
}

View File

@@ -0,0 +1,8 @@
package org.openrndr.extra.dnk3.post
import org.openrndr.draw.Filter
import org.openrndr.draw.filterShaderFromUrl
import org.openrndr.resourceUrl
class SegmentContoursMSAA8 : Filter(filterShaderFromUrl(resourceUrl("/shaders/segment-contours-msaa-8.frag")))
class SegmentContours : Filter(filterShaderFromUrl(resourceUrl("/shaders/segment-contours.frag")))

View File

@@ -0,0 +1,45 @@
package org.openrndr.extra.dnk3.post
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.features.IrradianceSH
import org.openrndr.extra.shaderphrases.preprocessShader
import org.openrndr.math.IntVector3
import org.openrndr.math.Matrix44
import org.openrndr.resourceUrl
import java.net.URL
fun preprocessedFilterShaderFromUrl(url: String): Shader {
return filterShaderFromCode( preprocessShader(URL(url).readText()), "filter-shader: $url")
}
fun preprocessedFilterShaderFromCode(fragmentShaderCode: String, name: String): Shader {
return Shader.createFromCode(vsCode = Filter.filterVertexCode, fsCode = fragmentShaderCode, name = name)
}
class VolumetricIrradiance : Filter(preprocessedFilterShaderFromUrl(resourceUrl("/shaders/volumetric-irradiance.frag"))) {
var stepLength: Double by parameters
var irradianceSH: IrradianceSH? = null
var viewMatrixInverse: Matrix44 by parameters
var projectionMatrixInverse: Matrix44 by parameters
init {
stepLength = 0.1
viewMatrixInverse = Matrix44.IDENTITY
projectionMatrixInverse = Matrix44.IDENTITY
}
override fun apply(source: Array<ColorBuffer>, target: Array<ColorBuffer>) {
irradianceSH?.shMap?.let {
parameters["shMap"] = it
}
irradianceSH?.let {
parameters["shMapDimensions"] = IntVector3(it.xCount, it.yCount, it.zCount)
parameters["shMapOffset"] = it.offset
parameters["shMapSpacing"] = it.spacing
}
super.apply(source, target)
}
}

View File

@@ -0,0 +1,56 @@
package org.openrndr.extra.dnk3.query
import org.openrndr.extra.dnk3.Material
import org.openrndr.extra.dnk3.Mesh
import org.openrndr.extra.dnk3.Scene
import org.openrndr.extra.dnk3.SceneNode
fun Scene.findNodeByName(name: String): SceneNode? {
return root.findNodeByName(name)
}
fun SceneNode.findNodeByName(name: String): SceneNode? {
if (this.name == name) {
return this
} else {
for (child in children) {
val candidate = child.findNodeByName(name)
if (candidate != null) {
return candidate
}
}
}
return null
}
fun SceneNode.findMaterialByName(name: String): Material? {
return allMaterials().find { it.name == name }
}
fun Scene.allMaterials(): Set<Material> {
return root.allMaterials()
}
fun SceneNode.allMaterials(): Set<Material> {
val materials = mutableSetOf<Material>()
fun processNode(node: SceneNode) {
for (entity in node.entities) {
when (entity) {
is Mesh -> {
materials.addAll(entity.primitives.map { it.material })
}
else -> {
}
}
}
for (child in node.children) {
processNode(child)
}
}
processNode(this)
return materials
}

View File

@@ -0,0 +1,8 @@
package org.openrndr.extra.dnk3.renderers
import org.openrndr.extra.dnk3.SceneRenderer
fun dryRenderer() : SceneRenderer {
val sr = SceneRenderer()
return sr
}

View File

@@ -0,0 +1,22 @@
package org.openrndr.extra.dnk3.renderers
import org.openrndr.draw.BufferMultisample
import org.openrndr.draw.ColorFormat
import org.openrndr.draw.ColorType
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.post.SegmentContours
import org.openrndr.extra.dnk3.post.SegmentContoursMSAA8
fun postRenderer(multisample: BufferMultisample = BufferMultisample.Disabled): SceneRenderer {
val sr = SceneRenderer()
sr.outputPasses.clear()
sr.outputPasses.add(
RenderPass(
listOf(HDRColorFacet(),FragmentIDFacet(), ClipDepthFacet(), ViewNormalFacet()),
multisample = multisample
)
)
sr.drawFinalBuffer = true
return sr
}

View File

@@ -0,0 +1,34 @@
package org.openrndr.extra.dnk3.renderers
import org.openrndr.draw.BufferMultisample
import org.openrndr.draw.ColorFormat
import org.openrndr.draw.ColorType
import org.openrndr.extra.dnk3.*
import org.openrndr.extra.dnk3.post.SegmentContours
import org.openrndr.extra.dnk3.post.SegmentContoursMSAA8
fun segmentContourRenderer(multisample: BufferMultisample = BufferMultisample.Disabled): SceneRenderer {
val sr = SceneRenderer()
sr.outputPasses.clear()
sr.outputPasses.add(
RenderPass(
listOf(LDRColorFacet(),FragmentIDFacet()),
multisample = multisample
)
)
sr.postSteps.add(
FilterPostStep(1.0,
when (multisample) {
BufferMultisample.Disabled -> SegmentContours()
BufferMultisample.SampleCount(8) -> SegmentContoursMSAA8()
else -> error("unsupported multisampling mode $multisample")
},
listOf("fragmentID"),
"segments",
ColorFormat.RGB,
ColorType.UINT8
)
)
sr.drawFinalBuffer = true
return sr
}

View File

@@ -0,0 +1,98 @@
package org.openrndr.extra.dnk3.tools
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.Geometry
import org.openrndr.extra.dnk3.Mesh
import org.openrndr.extra.dnk3.MeshPrimitive
import org.openrndr.extra.dnk3.PBRMaterial
import java.nio.ByteBuffer
import java.nio.ByteOrder
private data class CollapseItem(val vertexFormats: List<VertexFormat>,
val drawPrimitive: DrawPrimitive,
val hasIndexBuffer: Boolean)
fun Mesh.collapse() {
val grouped = primitives.groupBy {
CollapseItem(it.geometry.vertexBuffers.map { it.vertexFormat }, it.geometry.primitive, it.geometry.indexBuffer != null)
}
grouped.map {
val vertexCount = it.value.sumBy { primitive ->
primitive.geometry.vertexCount
}
val indexCount = if (it.key.hasIndexBuffer)
it.value.sumBy { primitive ->
primitive.geometry.indexBuffer?.indexCount ?: 0
}
else 0
val collapsedVertices = it.key.vertexFormats.map {
vertexBuffer(it, vertexCount)
} + vertexBuffer(vertexFormat { attribute("fragmentID", VertexElementType.INT16) }, vertexCount)
val fragmentBuffer = ByteBuffer.allocateDirect(vertexCount * 2)
fragmentBuffer.order(ByteOrder.nativeOrder())
for (i in 0 until collapsedVertices.size) {
var offset = 0
for (fromPrimitive in it.value) {
val fromBuffer = fromPrimitive.geometry.vertexBuffers[i]
val copy = ByteBuffer.allocateDirect(fromBuffer.vertexCount * fromBuffer.vertexFormat.size)
copy.order(ByteOrder.nativeOrder())
fromBuffer.read(copy)
copy.rewind()
collapsedVertices[i].write(copy, offset)
offset += copy.capacity()
for (v in 0 until fromBuffer.vertexCount) {
fragmentBuffer.putShort(fromPrimitive.material.fragmentID.toShort())
}
}
}
val collapsedIndices = if (it.key.hasIndexBuffer) indexBuffer(indexCount, IndexType.INT32) else null
if (it.key.hasIndexBuffer) {
var offset = 0
val result = ByteBuffer.allocateDirect(4 * indexCount)
result.order(ByteOrder.nativeOrder())
for (fromPrimitive in it.value) {
val fromBuffer = fromPrimitive.geometry.indexBuffer!!
when (fromBuffer.type) {
IndexType.INT16 -> {
val copy = ByteBuffer.allocateDirect(fromBuffer.indexCount * 2)
fromBuffer.read(copy)
copy.rewind()
for (i in 0 until fromBuffer.indexCount) {
val index = (copy.getShort().toInt() and 0xffff) + offset
result.putInt(index)
}
}
IndexType.INT32 -> {
val copy = ByteBuffer.allocateDirect(fromBuffer.indexCount * 4)
fromBuffer.read(copy)
copy.rewind()
for (i in 0 until fromBuffer.indexCount) {
val index = copy.getInt() + offset
result.putInt(index)
}
}
}
offset += fromPrimitive.geometry.vertexCount
}
}
val collapsedGeometry = Geometry(collapsedVertices, collapsedIndices, it.key.drawPrimitive, 0, if (collapsedIndices == null)
vertexCount else indexCount
)
MeshPrimitive(collapsedGeometry, PBRMaterial())
}
}

View File

@@ -0,0 +1,84 @@
package org.openrndr.extra.dnk3.tools
import org.openrndr.draw.*
import org.openrndr.extra.dnk3.*
import org.openrndr.extras.meshgenerators.boxMesh
data class SkyboxMaterial(val cubemap: Cubemap, val intensity: Double = 0.0) : Material {
override val name: String = "skybox"
override var doubleSided: Boolean = false
override var transparent: Boolean = false
override val fragmentID: Int = 0
override fun generateShadeStyle(materialContext: MaterialContext, primitiveContext: PrimitiveContext): ShadeStyle {
return shadeStyle {
vertexTransform = """
vec2 i = vec2(1.0, 0.0);
x_viewMatrix = x_viewNormalMatrix;
""".trimIndent()
val combinerFS = materialContext.pass.combiners.map {
it.generateShader()
}.joinToString("\n")
fragmentPreamble = """
vec4 f_diffuse = vec4(0.0, 0.0, 0.0, 1.0);
vec3 f_specular = vec3(0.0);
vec3 f_ambient = vec3(0.0);
vec3 f_emission = vec3(0.0);
int f_fragmentID = 0;
vec4 m_color = vec4(1.0);
vec4 f_fog = vec4(0.0);
""".trimIndent()
fragmentTransform = """
f_diffuse = texture(p_skybox, va_position);
f_diffuse.rgb *= p_intensity;
""" + combinerFS
suppressDefaultOutput = true
val rt = RenderTarget.active
materialContext.pass.combiners.map {
if (rt is ProgramRenderTarget || materialContext.pass === DefaultPass || materialContext.pass === DefaultOpaquePass || materialContext.pass == DefaultTransparentPass || materialContext.pass == IrradianceProbePass) {
this.output(it.targetOutput, ShadeStyleOutput(0))
} else {
val index = rt.colorAttachmentIndexByName(it.targetOutput)
?: error("attachment ${it.targetOutput} not found")
val type = rt.colorBuffer(index).type
val format = rt.colorBuffer(index).format
this.output(it.targetOutput, ShadeStyleOutput(index, format, type))
}
}
}
}
override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) {
shadeStyle.parameter("skybox", cubemap)
shadeStyle.parameter("intensity", intensity)
}
override fun hashCode(): Int {
var result = intensity.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + doubleSided.hashCode()
result = 31 * result + transparent.hashCode()
result = 31 * result + fragmentID
return result
}
}
fun Scene.addSkybox(cubemapUrl: String, size: Double = 100.0, intensity: Double = 1.0) {
val cubemap = Cubemap.fromUrl(cubemapUrl, null, Session.active).apply { generateMipmaps() }
val box = boxMesh(size, size, size, 1, 1, 1, true)
val node = SceneNode()
val material = SkyboxMaterial(cubemap, intensity)
val geometry = Geometry(listOf(box), null, DrawPrimitive.TRIANGLES, 0, box.vertexCount)
val primitive = MeshPrimitive(geometry, material)
val mesh = Mesh(listOf(primitive))
node.entities.add(mesh)
root.children.add(node)
}

View File

@@ -0,0 +1,17 @@
vec3 evaluateSH(vec3 direction, vec3[9] _SH) {
const float c1 = 0.42904276540489171563379376569857; // 4 * Â2.Y22 = 1/4 * sqrt(15.PI)
const float c2 = 0.51166335397324424423977581244463; // 0.5 * Â1.Y10 = 1/2 * sqrt(PI/3)
const float c3 = 0.24770795610037568833406429782001; // Â2.Y20 = 1/16 * sqrt(5.PI)
const float c4 = 0.88622692545275801364908374167057; // Â0.Y00 = 1/2 * sqrt(PI)
float x = direction.x;
float y = direction.y;
float z = direction.z;
return max(vec3(0.0),
_SH[8] * (c1 * (x * x - y * y)) // c1.L22.(x²-y²)
+ _SH[6] * (c3 * (3.0 * z * z - 1)) // c3.L20.(3.z² - 1)
+ _SH[0] * c4 // c4.L00
+ (_SH[4] * x * y + _SH[7] * x * z + _SH[5] * y * z) * 2.0 * c1 // 2.c1.(L2-2.xy + L21.xz + L2-1.yz)
+ (_SH[3] * x + _SH[1] * y + _SH[2] * z) * c2 * 2.0); // 2.c2.(L11.x + L1-1.y + L10.z)
}

View File

@@ -0,0 +1,12 @@
void fetchSH(samplerBuffer btex, int probeID, out vec3[9] _SH) {
int offset = probeID * 9;
_SH[0] = texelFetch(btex, offset).rgb;
_SH[1] = texelFetch(btex, offset+1).rgb;
_SH[2] = texelFetch(btex, offset+2).rgb;
_SH[3] = texelFetch(btex, offset+3).rgb;
_SH[4] = texelFetch(btex, offset+4).rgb;
_SH[5] = texelFetch(btex, offset+5).rgb;
_SH[6] = texelFetch(btex, offset+6).rgb;
_SH[7] = texelFetch(btex, offset+7).rgb;
_SH[8] = texelFetch(btex, offset+8).rgb;
}

View File

@@ -0,0 +1,4 @@
void fetchSH0(samplerBuffer btex, int probeID, out vec3 _SH) {
int offset = probeID * 9;
_SH = texelFetch(btex, offset).rgb;
}

View File

@@ -0,0 +1,26 @@
void gatherSH(samplerBuffer btex, vec3 p, ivec3 probeCounts, vec3 offset, float spacing, out vec3[9] blend) {
vec3[9] c000;
vec3[9] c001;
vec3[9] c010;
vec3[9] c011;
vec3[9] c100;
vec3[9] c101;
vec3[9] c110;
vec3[9] c111;
vec3 f;
ivec3 io = gridCoordinates(p, f, probeCounts, offset, spacing);
fetchSH(btex, gridIndex(io + ivec3(0,0,0), probeCounts), c000);
fetchSH(btex, gridIndex(io + ivec3(0,0,1), probeCounts), c001);
fetchSH(btex, gridIndex(io + ivec3(0,1,0), probeCounts), c010);
fetchSH(btex, gridIndex(io + ivec3(0,1,1), probeCounts), c011);
fetchSH(btex, gridIndex(io + ivec3(1,0,0), probeCounts), c100);
fetchSH(btex, gridIndex(io + ivec3(1,0,1), probeCounts), c101);
fetchSH(btex, gridIndex(io + ivec3(1,1,0), probeCounts), c110);
fetchSH(btex, gridIndex(io + ivec3(1,1,1), probeCounts), c111);
for (int i = 0; i < 9; ++i) {
blend[i] = mix( mix( mix(c000[i], c001[i], f.z), mix(c010[i], c011[i], f.z), f.y), mix( mix(c100[i], c101[i], f.z), mix(c110[i], c111[i], f.z), f.y), f.x);
}
}

View File

@@ -0,0 +1,25 @@
void gatherSH0(samplerBuffer btex, vec3 p, ivec3 probeCounts, vec3 offset, float spacing, out vec3 blend) {
vec3 c000;
vec3 c001;
vec3 c010;
vec3 c011;
vec3 c100;
vec3 c101;
vec3 c110;
vec3 c111;
vec3 f;
ivec3 io = gridCoordinates(p, f, probeCounts, offset, spacing);
fetchSH0(btex, gridIndex(io + ivec3(0,0,0), probeCounts), c000);
fetchSH0(btex, gridIndex(io + ivec3(0,0,1), probeCounts), c001);
fetchSH0(btex, gridIndex(io + ivec3(0,1,0), probeCounts), c010);
fetchSH0(btex, gridIndex(io + ivec3(0,1,1), probeCounts), c011);
fetchSH0(btex, gridIndex(io + ivec3(1,0,0), probeCounts), c100);
fetchSH0(btex, gridIndex(io + ivec3(1,0,1), probeCounts), c101);
fetchSH0(btex, gridIndex(io + ivec3(1,1,0), probeCounts), c110);
fetchSH0(btex, gridIndex(io + ivec3(1,1,1), probeCounts), c111);
blend = mix( mix( mix(c000, c001, f.z), mix(c010, c011, f.z), f.y), mix( mix(c100, c101, f.z), mix(c110, c111, f.z), f.y), f.x);
}

View File

@@ -0,0 +1,15 @@
ivec3 gridCoordinates(vec3 p, out vec3 f, ivec3 probeCounts, vec3 offset, float spacing) {
float x = (p.x - offset.x) / spacing;
float y = (p.y - offset.y)/ spacing;
float z = (p.z - offset.z) / spacing;
int ix = int(floor(x)) + probeCounts.x / 2;
int iy = int(floor(y)) + probeCounts.y / 2;
int iz = int(floor(z)) + probeCounts.z / 2;
f.x = fract((x));
f.y = fract((y));
f.z = fract((z));
return ivec3(ix, iy, iz);
}

View File

@@ -0,0 +1,4 @@
int gridIndex(ivec3 p, ivec3 probeCounts) {
ivec3 c = clamp(p, ivec3(0), probeCounts - ivec3(1));
return c.x + c.y * probeCounts.x + c.z * probeCounts.x * probeCounts.y;
}

View File

@@ -0,0 +1,22 @@
#version 330
uniform samplerCube tex0;
uniform vec3 sideUp;
uniform vec3 sideRight;
uniform vec3 sideNormal;
in vec2 v_texCoord0;
out vec4 o_output;
#define PI 3.1415926536
void main() {
vec3 irradiance = vec3(0.0);
vec2 uv = (v_texCoord0 - vec2(0.5))*2.0;
vec3 normal = normalize(uv.x * sideRight + uv.y * sideUp + sideNormal);
o_output.rgb = texture(tex0, normal).rgb;
o_output.a = 1.0;
}

View File

@@ -0,0 +1,40 @@
#version 330
uniform samplerCube tex0;
uniform vec3 sideUp;
uniform vec3 sideRight;
uniform vec3 sideNormal;
in vec2 v_texCoord0;
out vec4 o_output;
#define PI 3.1415926536
void main() {
vec3 irradiance = vec3(0.0);
vec2 uv = (v_texCoord0 - vec2(0.5))*2.0;
vec3 normal = normalize(uv.x * sideRight + uv.y * sideUp + sideNormal);
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up = cross(normal, right);
float sampleDelta = 0.025;
int nrSamples = 0;
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) {
for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) {
// spherical to cartesian (in tangent space)
vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
// tangent space to world
vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * normal;
irradiance += texture(tex0, sampleVec).rgb * cos(theta) * sin(theta);
nrSamples++;
}
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));
o_output.rgb = irradiance.rgb;
o_output.a = 1.0;
}

View File

@@ -0,0 +1,11 @@
uniform samplerCube tex0;
in v_texCoord0;
uniform vec2 targetSize;
out o_term0;
out o_term1;
out o_term2;
void main() {
}

View File

@@ -0,0 +1,349 @@
#version 330
// --- varyings ---
in vec2 v_texCoord0;
// --- G buffer ---
uniform sampler2D colors;
uniform sampler2D projDepth;
uniform sampler2D normals;
// --- transforms ---
uniform mat4 projection;
uniform mat4 projectionMatrixInverse;
// --- output ---
layout(location = 0) out vec4 o_color;
// --- parameters ---
uniform float jitterOriginGain;
uniform int iterationLimit;
uniform float distanceLimit;
uniform float gain;
uniform float borderWidth;
float distanceSquared(vec2 a, vec2 b) {
vec2 d = b-a;
return dot(d,d);
}
#pragma import org.openrndr.extra.shaderphrases.phrases.Depth.projectionToViewCoordinate;
#pragma import org.openrndr.extra.shaderphrases.phrases.Depth.projectionToViewDepth;
#pragma import org.openrndr.extra.noise.phrases.NoisePhrasesKt.phraseHash22;
// this is from http://casual-effects.blogspot.nl/2014/08/screen-space-ray-tracing.html
void swap(inout float a, inout float b) {
float temp = a;
a = b;
b = temp;
}
bool traceScreenSpaceRay1
(vec3 csOrigin,
vec3 csDirection,
mat4x4 projectToPixelMatrix,
sampler2D csZBuffer,
vec2 csZBufferSize,
float csZThickness,
float nearPlaneZ,
float stride,
float jitterFraction,
float maxSteps,
in float maxRayTraceDistance,
out vec2 hitPixel,
out vec3 csHitPoint,
out vec3 csHitNormal
// ,out vec3 debugColor
) {
vec3 debugColor = vec3(0);
// Clip ray to a near plane in 3D (doesn't have to be *the* near plane, although that would be a good idea)
float rayLength = ((csOrigin.z + csDirection.z * maxRayTraceDistance) > nearPlaneZ) ?
(nearPlaneZ - csOrigin.z) / csDirection.z :
maxRayTraceDistance;
vec3 csEndPoint = csDirection * rayLength + csOrigin;
// Project into screen space
vec4 H0 = projectToPixelMatrix * vec4(csOrigin, 1.0);
vec4 H1 = projectToPixelMatrix * vec4(csEndPoint, 1.0);
// There are a lot of divisions by w that can be turned into multiplications
// at some minor precision loss...and we need to interpolate these 1/w values
// anyway.
//
// Because the caller was required to clip to the near plane,
// this homogeneous division (projecting from 4D to 2D) is guaranteed
// to succeed.
float k0 = 1.0 / H0.w;
float k1 = 1.0 / H1.w;
// Switch the original points to values that interpolate linearly in 2D
vec3 Q0 = csOrigin * k0;
vec3 Q1 = csEndPoint * k1;
// Screen-space endpoints
vec2 P0 = H0.xy * k0;
vec2 P1 = H1.xy * k1;
// [Optional clipping to frustum sides here]
// Initialize to off screen
hitPixel = vec2(-1.0, -1.0);
// If the line is degenerate, make it cover at least one pixel
// to avoid handling zero-pixel extent as a special case later
P1 += vec2((distanceSquared(P0, P1) < 0.0001) ? 0.01 : 0.0);
vec2 delta = P1 - P0;
// Permute so that the primary iteration is in x to reduce
// large branches later
bool permute = (abs(delta.x) < abs(delta.y));
if (permute) {
// More-vertical line. Create a permutation that swaps x and y in the output
// by directly swizzling the inputs.
delta = delta.yx;
P1 = P1.yx;
P0 = P0.yx;
}
// From now on, "x" is the primary iteration direction and "y" is the secondary one
float stepDirection = sign(delta.x);
float invdx = stepDirection / delta.x;
vec2 dP = vec2(stepDirection, invdx * delta.y);
// Track the derivatives of Q and k
vec3 dQ = (Q1 - Q0) * invdx;
float dk = (k1 - k0) * invdx;
// Because we test 1/2 a texel forward along the ray, on the very last iteration
// the interpolation can go past the end of the ray. Use these bounds to clamp it.
float zMin = min(csEndPoint.z, csOrigin.z);
float zMax = max(csEndPoint.z, csOrigin.z);
// Scale derivatives by the desired pixel stride
dP *= stride; dQ *= stride; dk *= stride;
// Offset the starting values by the jitter fraction
P0 += dP * jitterFraction; Q0 += dQ * jitterFraction; k0 += dk * jitterFraction;
// Slide P from P0 to P1, (now-homogeneous) Q from Q0 to Q1, and k from k0 to k1
vec3 Q = Q0;
float k = k0;
// We track the ray depth at +/- 1/2 pixel to treat pixels as clip-space solid
// voxels. Because the depth at -1/2 for a given pixel will be the same as at
// +1/2 for the previous iteration, we actually only have to compute one value
// per iteration.
float prevZMaxEstimate = csOrigin.z;
float stepCount = 0.0;
float rayZMax = prevZMaxEstimate, rayZMin = prevZMaxEstimate;
float sceneZMax = rayZMax + 1e4;
// P1.x is never modified after this point, so pre-scale it by
// the step direction for a signed comparison
float end = P1.x * stepDirection;
// We only advance the z field of Q in the inner loop, since
// Q.xy is never used until after the loop terminates.
vec2 P;
for (P = P0;
((P.x * stepDirection) <= end) &&
(stepCount < maxSteps) &&
((rayZMax < sceneZMax - csZThickness) ||
(rayZMin > sceneZMax)) &&
(sceneZMax != 0.0);
P += dP, Q.z += dQ.z, k += dk, stepCount += 1.0) {
// The depth range that the ray covers within this loop
// iteration. Assume that the ray is moving in increasing z
// and swap if backwards. Because one end of the interval is
// shared between adjacent iterations, we track the previous
// value and then swap as needed to ensure correct ordering
rayZMin = prevZMaxEstimate;
// Compute the value at 1/2 step into the future
rayZMax = (dQ.z * 0.5 + Q.z) / (dk * 0.5 + k);
// -- this is not in the other implementation
rayZMax = clamp(rayZMax, zMin, zMax);
prevZMaxEstimate = rayZMax;
// Since we don't know if the ray is stepping forward or backward in depth,
// maybe swap. Note that we preserve our original z "max" estimate first.
if (rayZMin > rayZMax) { swap(rayZMin, rayZMax); }
// Camera-space z of the background
hitPixel = permute ? P.yx : P;
vec4 depthData = texelFetch(csZBuffer, ivec2(hitPixel), 0);
sceneZMax = projectionToViewCoordinate(v_texCoord0, depthData.x, projectionMatrixInverse).z;
} // pixel on ray
// Undo the last increment, which ran after the test variables
// were set up.
P -= dP; Q.z -= dQ.z; k -= dk; stepCount -= 1.0;
bool hit = (rayZMax >= sceneZMax - csZThickness) && (rayZMin <= sceneZMax);
// If using non-unit stride and we hit a depth surface...
if ((stride > 1) && hit) {
// Refine the hit point within the last large-stride step
// Retreat one whole stride step from the previous loop so that
// we can re-run that iteration at finer scale
P -= dP; Q.z -= dQ.z; k -= dk; stepCount -= 1.0;
// Take the derivatives back to single-pixel stride
float invStride = 1.0 / stride;
dP *= invStride; dQ.z *= invStride; dk *= invStride;
// For this test, we don't bother checking thickness or passing the end, since we KNOW there will
// be a hit point. As soon as
// the ray passes behind an object, call it a hit. Advance (stride + 1) steps to fully check this
// interval (we could skip the very first iteration, but then we'd need identical code to prime the loop)
float refinementStepCount = 0;
// This is the current sample point's z-value, taken back to camera space
prevZMaxEstimate = Q.z / k;
rayZMin = prevZMaxEstimate;
// Ensure that the FOR-loop test passes on the first iteration since we
// won't have a valid value of sceneZMax to test.
sceneZMax = rayZMin - 1e7;
for (;
(refinementStepCount <= stride*1.4) &&
(rayZMin > sceneZMax) && (sceneZMax != 0.0);
P += dP, Q.z += dQ.z, k += dk, refinementStepCount += 1.0) {
rayZMin = prevZMaxEstimate;
// Compute the ray camera-space Z value at 1/2 fine step (pixel) into the future
rayZMax = (dQ.z * 0.5 + Q.z) / (dk * 0.5 + k);
rayZMax = clamp(rayZMax, zMin, zMax);
prevZMaxEstimate = rayZMax;
rayZMin = min(rayZMax, rayZMin);
hitPixel = permute ? P.yx : P;
vec4 depthData = texelFetch(csZBuffer, ivec2(hitPixel), 0);
sceneZMax = projectionToViewCoordinate(v_texCoord0, depthData.x, projectionMatrixInverse).z;
csHitNormal = texelFetch(normals, ivec2(hitPixel), 0).xyz;
// sceneZMax = texelFetch(csZBuffer, ivec2(hitPixel), 0).r;
}
// Undo the last increment, which happened after the test variables were set up
Q.z -= dQ.z; refinementStepCount -= 1;
// Count the refinement steps as fractions of the original stride. Save a register
// by not retaining invStride until here
stepCount += refinementStepCount / stride;
// debugColor = vec3(refinementStepCount / stride);
} // refinement
Q.xy += dQ.xy * stepCount;
csHitPoint = Q * (1.0 / k);
// Support debugging. This will compile away if debugColor is unused
if ((P.x * stepDirection) > end) {
// Hit the max ray distance -> blue
debugColor = vec3(0,0,1);
} else if (stepCount >= maxSteps) {
// Ran out of steps -> red
debugColor = vec3(1,0,0);
} else if (sceneZMax == 0.0) {
// Went off screen -> yellow
debugColor = vec3(1,1,0);
} else {
// Encountered a valid hit -> green
// ((rayZMax >= sceneZMax - csZThickness) && (rayZMin <= sceneZMax))
debugColor = vec3(0,1,0);
}
// Does the last point discovered represent a valid hit?
return hit;
}
void main() {
vec2 hitPixel = vec2(0.0, 0.0);
vec3 hitPoint = vec3(0.0, 0.0, 0.0);
vec3 hitNormal = vec3(0.0, 0.0, 0.0);
vec2 jitter = abs(hash22(v_texCoord0));
vec2 ts = vec2(textureSize(projDepth, 0).xy);
vec3 viewNormal = normalize(texture(normals, v_texCoord0).xyz);// + (texture(noise, v_texCoord0*0.1).xyz - 0.5) * 0.0;
float depth = texture(projDepth, v_texCoord0).r;
vec3 viewPos = projectionToViewCoordinate(v_texCoord0, depth, projectionMatrixInverse);
vec3 reflected = normalize(reflect(normalize(viewPos), normalize(-viewNormal)));
float angle = abs(dot(reflected, viewNormal));
float frontalFade = clamp(-reflected.z,0, 1);
if ( true ) {
bool hit = traceScreenSpaceRay1(
viewPos,
reflected,
projection,
projDepth,
ts,
0.1,
0.0, // near plane z
1.0,// + projPos.z*2.0, // stride
10.0, // jitterfraction
iterationLimit*8,// + int((1.0-projPos.z)*iterationLimit),
100.0, // max distance
hitPixel,
hitPoint, hitNormal);
float distanceFade = 1.0;//max( 0.0, (distanceLimit -length(hitPoint-viewPos))/ distanceLimit);
vec4 p = projection * vec4(hitPoint, 1.0);
float k = 1.0 / p.w;
vec2 pos = vec2(p.xy*k);
vec2 ad = vec2(ts/2- abs(pos - ts/2));
float borderFade = 1.0; //smoothstep(0, borderWidth, min(ad.x, ad.y));
float l = 0.0;
int l0 = int(l);
int l1 = l0 + 1;
float lf = l - l0;
vec4 reflectedColor0 = texelFetch(colors, ivec2(p.xy*k)/(1<<l0), l0);
vec4 reflectedColor1 = texelFetch(colors, ivec2(p.xy*k)/(1<<l1), l1);
vec4 reflectedColor = reflectedColor0 * (1.0-lf) + reflectedColor1 * lf;
// vec2 uv = vec2(p.xy*k) / textureSize(colors, 0);
//reflectedColor = textureLod(colors, uv, l);
float hitFade = hit? 1.0: 0.0;
float angleFade = 1.0;/// smoothstep(0.0, 0.3, angle);;//angle < 0.5? 0.0 : 1.0;
float faceFade = 1.0; //step(0.00001, dot(-normalize(hitNormal), reflected));
o_color.rgb = (1.0 * reflectedColor.rgb * hitFade * frontalFade * distanceFade * borderFade * angleFade * faceFade) + texture(colors, v_texCoord0).rgb;
o_color.a = 1.0;
} else {
o_color = texture(colors, v_texCoord0).rgba;
o_color.a = 1.0;
}
}

View File

@@ -0,0 +1,34 @@
#version 330
uniform usampler2DMS tex0;
in vec2 v_texCoord0;
out vec4 o_output;
void main() {
ivec2 ts = textureSize(tex0);
ivec2 pixel = ivec2(v_texCoord0 * ts);
ivec2 c = pixel;
ivec2 n = c + ivec2(0, -1);
ivec2 s = c + ivec2(0, 1);
ivec2 w = c + ivec2(-1, 0);
ivec2 e = c + ivec2(1, 0);
float sf = 0.0;
for (int i = 0; i < 8; ++i) {
float f = 1.0;
uint sc = texelFetch(tex0, c, i).r;
uint sn = texelFetch(tex0, n, i).r;
uint ss = texelFetch(tex0, s, i).r;
uint se = texelFetch(tex0, e, i).r;
uint sw = texelFetch(tex0, w, i).r;
if (sc == se) f -= 0.25;
if (sc == sw) f -= 0.25;
if (sc == sn) f -= 0.25;
if (sc == ss) f -= 0.25;
sf+= f;
}
o_output = vec4(vec3(sf/4.0), 1.0);
}

View File

@@ -0,0 +1,33 @@
#version 330
uniform usampler2D tex0;
in vec2 v_texCoord0;
out vec4 o_output;
void main() {
ivec2 ts = textureSize(tex0, 0);
ivec2 pixel = ivec2(v_texCoord0 * ts);
ivec2 c = pixel;
ivec2 n = c + ivec2(0, -1);
ivec2 s = c + ivec2(0, 1);
ivec2 w = c + ivec2(-1, 0);
ivec2 e = c + ivec2(1, 0);
float sf = 0.0;
for (int i = 0; i < 1; ++i) {
float f = 1.0;
uint sc = texelFetch(tex0, c, i).r;
uint sn = texelFetch(tex0, n, i).r;
uint ss = texelFetch(tex0, s, i).r;
uint se = texelFetch(tex0, e, i).r;
uint sw = texelFetch(tex0, w, i).r;
if (sc == se) f -= 0.25;
if (sc == sw) f -= 0.25;
if (sc == sn) f -= 0.25;
if (sc == ss) f -= 0.25;
sf+= f;
}
o_output = vec4(vec3(sf/0.5), 1.0);
}

View File

@@ -0,0 +1,57 @@
#version 330 core
#pragma import org.openrndr.extra.shaderphrases.phrases.Depth.projectionToViewCoordinate;
#pragma import org.openrndr.extra.dnk3.cubemap.SphericalHarmonicsKt.glslFetchSH0;
#pragma import org.openrndr.extra.dnk3.cubemap.SphericalHarmonicsKt.glslGridCoordinates;
#pragma import org.openrndr.extra.dnk3.cubemap.SphericalHarmonicsKt.glslGridIndex;
#pragma import org.openrndr.extra.dnk3.cubemap.SphericalHarmonicsKt.glslGatherSH0;
#pragma import org.openrndr.extra.noise.phrases.NoisePhrasesKt.phraseHash22;
#pragma import org.openrndr.extra.noise.phrases.SimplexKt.phraseSimplex3;
in vec2 v_texCoord0;
uniform sampler2D tex0; // image
uniform sampler2D tex1; // projDepth
uniform samplerBuffer shMap;
uniform ivec3 shMapDimensions;
uniform vec3 shMapOffset;
uniform float shMapSpacing;
uniform mat4 projectionMatrixInverse;
uniform mat4 viewMatrixInverse;
uniform float stepLength;
out vec4 o_output;
void main() {
vec3 inputColor = texture(tex0, v_texCoord0).rgb;
float projDepth = texture(tex1, v_texCoord0).r;
vec3 viewCoordinate = projectionToViewCoordinate(v_texCoord0, projDepth, projectionMatrixInverse);
vec3 worldCoordinate = (viewMatrixInverse * vec4(viewCoordinate, 1.0)).xyz;
vec3 cameraPosition = (viewMatrixInverse * vec4(vec3(0.0), 1.0)).xyz;
// trace in world space
vec3 traverse = cameraPosition - worldCoordinate;
vec3 direction = normalize(traverse);
if (length(traverse) > 10.0) {
traverse = direction*10.0;
worldCoordinate = cameraPosition - traverse;
}
int steps = min(100, int(length(traverse) / 0.1));
vec3 step = traverse / steps;
vec3 marchPosition = worldCoordinate;
vec3 accumulated = inputColor;
float jitter = hash22(v_texCoord0).x;
marchPosition += jitter * step*0.5;
for (int stepIndex = 0; stepIndex < steps; ++stepIndex) {
float density = pow(abs(simplex31(marchPosition*0.25)), 4.0) * 0.1;
vec3 sh0;
gatherSH0(shMap, marchPosition, shMapDimensions, shMapOffset, shMapSpacing, sh0);
accumulated = accumulated * (1.0-density) + sh0 * density;
marchPosition += step;
}
o_output = vec4(accumulated, 1.0);
}

200
orx-jvm/orx-gui/README.md Normal file
View File

@@ -0,0 +1,200 @@
# orx-gui
Automatic UI (sliders, buttons, etc.) generated from annotated classes and properties. Uses `orx-panel` and `orx-parameters`.
A quick-and-dirty user interface toolkit.
`orx-gui` uses class and property annotations to generate simple interfaces. The annotations used
are provided by [`orx-parameters`](../orx-parameters/README.md) and most filters in [`orx-fx`](../orx-fx/README.md) have been annotated.
`orx-gui` is made with an [`orx-olive`](../orx-olive/README.md) workflow in mind but can be used in normal OPENRNDR programs
just as well.
## Usage
Preparation: make sure `orx-gui` is in the `orxFeatures` of your project (if you working on a template based project)
The essence of `orx-gui` lies in the provided a `GUI` extension, which can be used in your program using the `extend {}` function.
The `GUI` class has an `add()` function that allows any annotated object to be passed in.
The visibility of the side bar can be toggled by pressing the F11 key on your keyboard.
### UIs for parameter objects
A simple UI can be created by creating an annotated `object`.
```kotlin
import org.openrndr.application
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
enum class Option {
Option1,
Option2,
Option3
}
fun main() = application {
program {
// -- this @Description annotation is optional
val parameters = @Description("parameters") object {
@DoubleParameter("radius", 20.0, 200.0, precision = 2, order = 0)
var radius = 50.0
@TextParameter("A string", order = 1)
var s = "Hello"
@BooleanParameter("A bool", order = 2)
var b = true
@IntParameter("An int", 0, 127, order = 3)
var i = 64
@ColorParameter("A fill color", order = 4)
var fill = ColorRGBa.PINK
@XYParameter("Position", minX = 0.0, maxX = 640.0,
minY = 0.0, maxY = 480.0, order = 5)
var pos = Vector2.ZERO
@Vector2Parameter("A Vector2", order = 6)
var v2 = Vector2(200.0, 200.0)
@Vector3Parameter("A Vector3", order = 7)
var v3 = Vector3(200.0, 200.0, 200.0)
@Vector4Parameter("A Vector4", order = 8)
var v4 = Vector4(200.0, 200.0, 200.0, 200.0)
@DoubleListParameter("Mixer", order = 9)
var mixer = MutableList(5) { 0.5 }
@ActionParameter("Action test", order = 10)
fun clicked() {
println("GUI says hi!")
}
@OptionParameter("An option", order = 11)
var option = Option.Option1
}
extend(GUI()) {
add(parameters)
}
extend {
drawer.fill = parameters.fill
drawer.circle(parameters.pos, parameters.radius)
}
}
}
```
### UIs for filters
In a similar fashion to the previous example we can create a simple UI for most filters in `orx-fx`
```kotlin
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.isolatedWithTarget
import org.openrndr.draw.renderTarget
import org.openrndr.extra.fx.blur.BoxBlur
import org.openrndr.extra.gui.GUI
fun main() = application {
program {
val blur = BoxBlur()
val rt = renderTarget(width, height) {
colorBuffer()
}
extend(GUI()) {
add(blur)
}
extend {
drawer.isolatedWithTarget(rt) {
drawer.background(ColorRGBa.BLACK)
drawer.fill = ColorRGBa.PINK
drawer.circle(width / 2.0, height / 2.0, 200.0)
}
blur.apply(rt.colorBuffer(0), rt.colorBuffer(0))
drawer.image(rt.colorBuffer(0))
}
}
}
```
### UIs in Olive
Using `orx-gui` in Olive (`orx-olive`) is very similar to how one would use it in a normal OPENRNDR program. There is
one detail that doesn't occur in normal programs: the UI state is reset when a
script is changed and re-evaluated. This is overcome by using an annotated `Reloadable` object.
An example `live.kts` script that uses `orx-gui` and `Reloadable`:
```kotlin
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
import org.openrndr.Program
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.olive.Reloadable
import org.openrndr.extra.parameters.DoubleParameter
{ program: Program ->
program.apply {
val p = object : Reloadable() {
@DoubleParameter("x-position", 0.0, 640.0, order = 0)
var x = 0.5
@DoubleParameter("y-position", 0.0, 480.0, order = 1)
var y = 0.5
@DoubleParameter("radius", 0.0, 480.0, order = 2)
var radius = 100.0
}
p.reload()
extend(GUI()) {
add(p)
}
extend {
drawer.circle(p.x, p.y, p.radius)
}
}
}
```
## Credits
`orx-gui` is based on a proof-of-concept by [Ricardo Matias](https://github.com/ricardomatias/)
<!-- __demos__ >
# Demos
[DemoOptions01Kt](src/demo/kotlin/DemoOptions01Kt.kt
![DemoOptions01Kt](https://github.com/openrndr/orx/blob/media/orx-gui/images/DemoOptions01Kt.png
[DemoSimple01Kt](src/demo/kotlin/DemoSimple01Kt.kt
![DemoSimple01Kt](https://github.com/openrndr/orx/blob/media/orx-gui/images/DemoSimple01Kt.png
<!-- __demos__ -->
## Demos
### DemoHide01
[source code](src/demo/kotlin/DemoHide01.kt)
![DemoHide01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoHide01Kt.png)
### DemoOptions01
[source code](src/demo/kotlin/DemoOptions01.kt)
![DemoOptions01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoOptions01Kt.png)
### DemoPresets01
[source code](src/demo/kotlin/DemoPresets01.kt)
![DemoPresets01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoPresets01Kt.png)
### DemoSimple01
[source code](src/demo/kotlin/DemoSimple01.kt)
![DemoSimple01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoSimple01Kt.png)
### DemoXYParameter
[source code](src/demo/kotlin/DemoXYParameter.kt)
![DemoXYParameterKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoXYParameterKt.png)

View File

@@ -0,0 +1,22 @@
sourceSets {
demo {
java {
srcDirs = ["src/demo/kotlin"]
compileClasspath += main.getCompileClasspath()
runtimeClasspath += main.getRuntimeClasspath()
}
}
}
dependencies {
api project(":orx-parameters")
api project(":orx-jvm:orx-panel")
implementation "org.openrndr:openrndr-dialogs:$openrndrVersion"
implementation "com.google.code.gson:gson:$gsonVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
demoImplementation(sourceSets.getByName("main").output)
}

View File

@@ -0,0 +1,57 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.*
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
/**
* A simple demonstration of a GUI for drawing some circles
*/
suspend fun main() = application {
program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val settings = @Description("Settings") object {
@DoubleParameter("radius", 0.0, 100.0)
var radius = 50.0
@Vector2Parameter("position", 0.0, 1.0)
var position = Vector2(0.6, 0.5)
@ColorParameter("color")
var color = ColorRGBa.PINK
@DoubleListParameter("radii", 5.0, 30.0)
var radii = mutableListOf(5.0, 6.0, 8.0, 14.0, 20.0, 30.0)
}
gui.add(settings)
extend(gui)
// note we can only change the visibility after the extend
gui.visible = false
extend {
// determine visibility through mouse x-coordinate
gui.visible = mouse.position.x < 200.0
drawer.fill = settings.color
drawer.circle(settings.position * drawer.bounds.position(1.0, 1.0), settings.radius)
drawer.circles(
settings.radii.mapIndexed { i, radius ->
Circle(width - 50.0, 60.0 + i * 70.0, radius)
}
)
}
}
}

View File

@@ -0,0 +1,43 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.*
/**
* A simple demonstration of a GUI with a drop down menu
*/
enum class BackgroundColors {
Pink,
Black,
Yellow
}
suspend fun main() = application {
program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val settings = @Description("Settings") object {
@OptionParameter("Background color")
var option = BackgroundColors.Pink
}
gui.add(settings)
extend(gui)
extend {
when(settings.option) {
BackgroundColors.Pink -> drawer.clear(ColorRGBa.PINK)
BackgroundColors.Black -> drawer.clear(ColorRGBa.BLACK)
BackgroundColors.Yellow -> drawer.clear(ColorRGBa.YELLOW)
}
}
}
}

View File

@@ -0,0 +1,72 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.color.mix
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.*
/**
* Shows how to store and retrieve in-memory gui presets.
* Keyboard controls:
* [Left Shift] + [0]..[9] => store current gui values to a preset
* [0]..[9] => recall a preset
*/
suspend fun main() = application {
program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val presets = MutableList(10) {
gui.toObject()
}
val settings = @Description("Settings") object {
@IntParameter("a", 1, 10)
var a = 7
@IntParameter("b", 1, 10)
var b = 3
@ColorParameter("foreground")
var foreground = ColorRGBa.fromHex("654062")
@ColorParameter("background")
var background = ColorRGBa.fromHex("ff9c71")
}
gui.add(settings)
extend(gui)
extend {
drawer.clear(settings.background)
drawer.stroke = settings.background
drawer.fill = settings.foreground
// Draw a pattern based on modulo
for(i in 0 until 100) {
if(i % settings.a == 0 || i % settings.b == 0) {
val x = (i % 10) * 64.0
val y = (i / 10) * 48.0
drawer.rectangle(x, y, 64.0, 48.0)
}
}
}
keyboard.keyDown.listen {
when (it.name) {
in "0" .. "9" -> {
if(keyboard.pressedKeys.contains("left-shift")) {
// 1. Get the current gui state, store it in a list
presets[it.name.toInt()] = gui.toObject()
} else {
// 2. Set the gui state
gui.fromObject(presets[it.name.toInt()])
}
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.*
import org.openrndr.math.Vector2
import org.openrndr.shape.Circle
/**
* A simple demonstration of a GUI for drawing some circles
*/
suspend fun main() = application {
program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val settings = @Description("Settings") object {
@DoubleParameter("radius", 0.0, 100.0)
var radius = 50.0
@Vector2Parameter("position", 0.0, 1.0)
var position = Vector2(0.6, 0.5)
@ColorParameter("color")
var color = ColorRGBa.PINK
@DoubleListParameter("radii", 5.0, 30.0)
var radii = mutableListOf(5.0, 6.0, 8.0, 14.0, 20.0, 30.0)
}
gui.add(settings)
extend(gui)
extend {
drawer.fill = settings.color
drawer.circle(settings.position * drawer.bounds.position(1.0, 1.0), settings.radius)
drawer.circles(
settings.radii.mapIndexed { i, radius ->
Circle(width - 50.0, 60.0 + i * 70.0, radius)
}
)
}
}
}

View File

@@ -0,0 +1,40 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.Description
import org.openrndr.extra.parameters.XYParameter
import org.openrndr.math.Vector2
suspend fun main() = application {
configure {
width = 800
height = 800
}
program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val settings = @Description("Settings") object {
@XYParameter("Position", 0.0, 800.0, 0.0, 800.0,
precision = 2,
invertY = true,
showVector = true)
var position: Vector2 = Vector2(0.0,0.0)
}
gui.add(settings)
extend(gui)
extend {
drawer.circle(settings.position, 50.0)
}
}
}

View File

@@ -0,0 +1,900 @@
package org.openrndr.extra.gui
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.openrndr.*
import org.openrndr.color.ColorRGBa
import org.openrndr.dialogs.getDefaultPathForContext
import org.openrndr.dialogs.openFileDialog
import org.openrndr.dialogs.saveFileDialog
import org.openrndr.dialogs.setDefaultPathForContext
import org.openrndr.draw.Drawer
import org.openrndr.extra.parameters.*
import org.openrndr.internal.Driver
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.panel.ControlManager
import org.openrndr.panel.controlManager
import org.openrndr.panel.elements.*
import org.openrndr.panel.style.*
import java.io.File
import kotlin.math.roundToInt
import kotlin.reflect.KMutableProperty1
/** Dear contributor, just in case you are here looking to add a new parameter type.
There is a 6-step incantation to add a new parameter type
0) Add your parameter type to orx-parameters, follow the instructions provided there.
1) Setup a control style, very likely analogous to the styles already in place.
2) Add control creation code.
3) Add value serialization code, may need to update ParameterValue too.
4) Add value deserialization code.
5) Add value randomization code.
6) Add control update code.
You can use your editor's search functionality to jump to "1)", "2)".
*/
private data class LabeledObject(val label: String, val obj: Any)
private class CompartmentState(var collapsed: Boolean, val parameterValues: MutableMap<String, Any> = mutableMapOf())
private class SidebarState(var hidden: Boolean = false, var collapsed: Boolean = false, var scrollTop: Double = 0.0)
private class TrackedObjectBinding(
val parameters: List<Parameter>,
val parameterControls: MutableMap<Parameter, Element> = mutableMapOf()
)
private val persistentCompartmentStates = mutableMapOf<Long, MutableMap<String, CompartmentState>>()
private val persistentSidebarStates = mutableMapOf<Long, SidebarState>()
private fun sidebarState(): SidebarState = persistentSidebarStates.getOrPut(Driver.instance.contextID) {
SidebarState()
}
private fun <T : Any> getPersistedOrDefault(compartmentLabel: String, property: KMutableProperty1<Any, T>, obj: Any): T? {
val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel]
if (state == null) {
return property.get(obj)
} else {
@Suppress("UNCHECKED_CAST")
return (state.parameterValues[property.name] as? T?) ?: return property.get(obj)
}
}
private fun <T : Any> setAndPersist(compartmentLabel: String, property: KMutableProperty1<Any, T>, obj: Any, value: T) {
property.set(obj, value)
val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel]!!
state.parameterValues[property.name] = value
}
@Suppress("unused", "UNCHECKED_CAST")
class GUI : Extension {
private var onChangeListener: ((name: String, value: Any?) -> Unit)? = null
override var enabled = true
var visible = true
set(value) {
if (field != value) {
field = value
if (field) {
panel?.body?.classes?.remove(collapsed)
} else {
panel?.body?.classes?.add(collapsed)
}
sidebarState().hidden = !field
}
}
var compartmentsCollapsedByDefault = true
var doubleBind = false
private var panel: ControlManager? = null
// Randomize button
private var shiftDown = false
private var randomizeButton: Button? = null // FIXME should this be null or is there a better way?
fun onChange(listener: (name: String, value: Any?) -> Unit) {
onChangeListener = listener
}
val collapsed = ElementClass("collapsed")
override fun setup(program: Program) {
program.keyboard.keyDown.listen {
if (it.key == KEY_F11) {
println("f11 pressed")
visible = !visible
}
if (it.key == KEY_LEFT_SHIFT) {
shiftDown = true
randomizeButton!!.classes.add(ElementClass("randomize-strong"))
}
}
program.keyboard.keyUp.listen {
if (it.key == KEY_LEFT_SHIFT) {
shiftDown = false
randomizeButton!!.classes.remove(ElementClass("randomize-strong"))
}
}
panel = program.controlManager {
styleSheet(has class_ "container") {
this.display = Display.FLEX
this.flexDirection = FlexDirection.Column
this.width = 200.px
this.height = 100.percent
}
styleSheet(has class_ "collapse-border") {
this.display = Display.FLEX
this.flexDirection = FlexDirection.Column
this.height = 5.px
this.width = 100.percent
this.background = Color.RGBa(ColorRGBa.GRAY.shade(0.9))
and(has state "hover") {
this.background = Color.RGBa(ColorRGBa.GRAY.shade(1.1))
}
}
styleSheet(has class_ "toolbar") {
this.height = 42.px
this.width = 100.percent
this.display = Display.FLEX
this.flexDirection = FlexDirection.Row
this.background = Color.RGBa(ColorRGBa.GRAY.copy(a = 0.99))
}
styleSheet(has class_ "collapsed") {
this.display = Display.NONE
}
styleSheet(has class_ "compartment") {
this.paddingBottom = 20.px
}
styleSheet(has class_ "sidebar") {
this.width = 200.px
this.paddingBottom = 20.px
this.paddingTop = 10.px
this.paddingLeft = 10.px
this.paddingRight = 10.px
this.marginRight = 2.px
this.height = 100.percent
this.background = Color.RGBa(ColorRGBa.GRAY.copy(a = 0.99))
this.overflow = Overflow.Scroll
//<editor-fold desc="1) setup control style">
descendant(has type "colorpicker-button") {
this.width = 175.px
}
descendant(has type "slider") {
this.width = 175.px
}
descendant(has type "button") {
this.width = 175.px
}
descendant(has type "textfield") {
this.width = 175.px
}
descendant(has type "toggle") {
this.width = 175.px
}
descendant(has type "xy-pad") {
this.width = 175.px
this.height = 175.px
}
descendant(has type listOf(
"sequence-editor",
"sliders-vector2",
"sliders-vector3",
"sliders-vector4"
)) {
this.width = 175.px
this.height = 100.px
}
//</editor-fold>
}
styleSheet(has class_ "randomize-strong") {
color = Color.RGBa(ColorRGBa.PINK)
and(has state "hover") {
color = Color.RGBa(ColorRGBa.BLACK)
background = Color.RGBa(ColorRGBa.PINK)
}
}
styleSheet(has type "dropdown-button") {
this.width = 175.px
}
layout {
div("container") {
id = "container"
@Suppress("UNUSED_VARIABLE") val header = div("toolbar") {
randomizeButton = button {
label = "Randomize"
clicked {
randomize(strength = if (shiftDown) .75 else .05)
}
}
button {
label = "Load"
clicked {
openFileDialog(supportedExtensions = listOf("json"), contextID = "gui.parameters") {
loadParameters(it)
}
}
}
button {
label = "Save"
clicked {
val defaultPath = getDefaultPathForContext(contextID = "gui.parameters")
if (defaultPath == null) {
val local = File(".")
val data = File(local, "data")
if (data.exists() && data.isDirectory) {
val parameters = File(data, "parameters")
if (!parameters.exists()) {
if (parameters.mkdirs()) {
setDefaultPathForContext(contextID = "gui.parameters", file = parameters)
}
} else {
setDefaultPathForContext(contextID = "gui.parameters", file = parameters)
}
}
}
saveFileDialog(suggestedFilename = "parameters.json", contextID = "gui.parameters", supportedExtensions = listOf("json")) {
saveParameters(it)
}
}
}
}
val collapseBorder = div("collapse-border") {
}
val collapsibles = mutableSetOf<Div>()
val sidebar = div("sidebar") {
id = "sidebar"
scrollTop = sidebarState().scrollTop
for ((labeledObject, binding) in trackedObjects) {
val (label, _) = labeledObject
val h3Header = h3 { label }
val collapsible = div("compartment") {
for (parameter in binding.parameters) {
val element = addControl(labeledObject, parameter)
binding.parameterControls[parameter] = element
}
}
collapsibles.add(collapsible)
val collapseClass = ElementClass("collapsed")
/* this is guaranteed to be in the dictionary after insertion through add() */
val collapseState = persistentCompartmentStates[Driver.instance.contextID]!![label]!!
if (collapseState.collapsed) {
collapsible.classes.add(collapseClass)
}
h3Header.mouse.pressed.listen {
it.cancelPropagation()
}
h3Header.mouse.clicked.listen { me ->
if (KeyModifier.CTRL in me.modifiers) {
collapsible.classes.remove(collapseClass)
persistentCompartmentStates[Driver.instance.contextID]!!.forEach {
it.value.collapsed = true
}
collapseState.collapsed = false
(collapsibles - collapsible).forEach {
it.classes.add(collapseClass)
}
} else {
if (collapseClass in collapsible.classes) {
collapsible.classes.remove(collapseClass)
collapseState.collapsed = false
} else {
collapsible.classes.add(collapseClass)
collapseState.collapsed = true
}
}
}
}
}
collapseBorder.mouse.pressed.listen {
it.cancelPropagation()
}
collapseBorder.mouse.clicked.listen {
val collapsed = ElementClass("collapsed")
if (collapsed in sidebar.classes) {
sidebar.classes.remove(collapsed)
sidebarState().collapsed = false
} else {
sidebar.classes.add(collapsed)
sidebarState().collapsed = true
}
it.cancelPropagation()
}
sidebar.mouse.scrolled.listen {
sidebarState().scrollTop = sidebar.scrollTop
}
if (sidebarState().collapsed) {
sidebar.classes.add(ElementClass("collapsed"))
}
sidebar.scrollTop = sidebarState().scrollTop
}
}
}
visible = !sidebarState().hidden
program.extend(panel ?: error("no panel"))
}
/* 2) control creation. create control, set label, set range, setup event-handler, load values */
//<editor-fold desc="2) Control creation">
private fun Div.addControl(compartment: LabeledObject, parameter: Parameter): Element {
val obj = compartment.obj
return when (parameter.parameterType) {
ParameterType.Int -> {
slider {
label = parameter.label
range = Range(parameter.intRange!!.first.toDouble(), parameter.intRange!!.last.toDouble())
precision = 0
events.valueChanged.listen {
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj, it.newValue.toInt())
(parameter.property as KMutableProperty1<Any, Int>).set(obj, value.toInt())
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj)?.let {
value = it.toDouble()
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj, it)
}
}
}
ParameterType.Double -> {
slider {
label = parameter.label
range = Range(parameter.doubleRange!!.start, parameter.doubleRange!!.endInclusive)
precision = parameter.precision!!
events.valueChanged.listen {
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj, it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj)?.let {
value = it
/* this is generally not needed, but when the persisted value is equal to the slider default
it will not emit the newly set value */
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj, it)
}
}
}
ParameterType.Action -> {
button {
label = parameter.label
events.clicked.listen {
/* the `obj` we pass in here is the receiver */
parameter.function!!.call(obj)
onChangeListener?.invoke(parameter.function!!.name, null)
}
}
}
ParameterType.Boolean -> {
toggle {
label = parameter.label
events.valueChanged.listen {
value = it.newValue
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj, it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj)?.let {
value = it
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj, it)
}
}
}
ParameterType.Text -> {
textfield {
label = parameter.label
events.valueChanged.listen {
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, String>, obj, it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, String>, obj)?.let {
value = it
}
}
}
ParameterType.Color -> {
colorpickerButton {
label = parameter.label
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, ColorRGBa>,
obj,
it.color
)
onChangeListener?.invoke(parameter.property!!.name, it.color)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, ColorRGBa>,
obj
)?.let {
color = it
}
}
}
ParameterType.XY -> {
xyPad {
minX = parameter.vectorRange!!.first.x
minY = parameter.vectorRange!!.first.y
maxX = parameter.vectorRange!!.second.x
maxY = parameter.vectorRange!!.second.y
precision = parameter.precision!!
showVector = parameter.showVector!!
invertY = parameter.invertY!!
label = parameter.label
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector2>,
obj,
it.newValue
)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
}
}
ParameterType.DoubleList -> {
sequenceEditor {
range = parameter.doubleRange!!
label = parameter.label
minimumSequenceLength = parameter.sizeRange!!.start
maximumSequenceLength = parameter.sizeRange!!.endInclusive
precision = parameter.precision!!
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, MutableList<Double>>,
obj,
it.newValue.toMutableList()
)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, MutableList<Double>>,
obj
)?.let {
value = it
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, MutableList<Double>>, obj, it)
}
}
}
ParameterType.Vector2 -> {
slidersVector2 {
range = parameter.doubleRange!!
label = parameter.label
precision = parameter.precision!!
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector2>,
obj,
it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector2>,
obj
)?.let {
value = it
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector2>, obj, it)
}
}
}
ParameterType.Vector3 -> {
slidersVector3 {
range = parameter.doubleRange!!
label = parameter.label
precision = parameter.precision!!
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector3>,
obj,
it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector3>,
obj
)?.let {
value = it
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector3>, obj, it)
}
}
}
ParameterType.Vector4 -> {
slidersVector4 {
range = parameter.doubleRange!!
label = parameter.label
precision = parameter.precision!!
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector4>,
obj,
it.newValue)
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, Vector4>,
obj
)?.let {
value = it
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector4>, obj, it)
}
}
}
ParameterType.Option -> {
dropdownButton {
val enumProperty = parameter.property as KMutableProperty1<Any, Enum<*>>
val value = enumProperty.get(obj)
label = parameter.label
// -- this is dirty, but it is the only way to get the constants for arbitrary enums
// -- (that I know of, at least)
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") val jEnum = value as java.lang.Enum<*>
// -- we don't use the property syntax here because that leads to compilation errors
@Suppress("UsePropertyAccessSyntax") val constants = jEnum.getDeclaringClass().getEnumConstants()
constants.forEach {
item {
label = it.name
data = it
}
}
events.valueChanged.listen {
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, Enum<*>>,
obj,
it.value.data as? Enum<*> ?: error("no data")
)
}
getPersistedOrDefault(
compartment.label,
parameter.property as KMutableProperty1<Any, Enum<*>>,
obj
)?.let { enum ->
(this@dropdownButton).value = items().find { item -> item.data == enum }
?: error("no matching item found")
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Enum<*>>, obj, enum)
}
}
}
}
}
//</editor-fold>
private val trackedObjects = mutableMapOf<LabeledObject, TrackedObjectBinding>()
private fun updateControls() {
for ((labeledObject, binding) in trackedObjects) {
for ((parameter, control) in binding.parameterControls) {
updateControl(labeledObject, parameter, control)
}
}
}
class ParameterValue(var doubleValue: Double? = null,
var intValue: Int? = null,
var booleanValue: Boolean? = null,
var colorValue: ColorRGBa? = null,
var vector2Value: Vector2? = null,
var vector3Value: Vector3? = null,
var vector4Value: Vector4? = null,
var doubleListValue: MutableList<Double>? = null,
var textValue: String? = null,
var optionValue: String? = null
)
/**
* Can be called by the user to obtain an object to be serialized
* externally. This allows the user to combine custom data with gui
* state and save it all to one file. Complements `.fromObject()`.
*/
fun toObject(): Map<String, Map<String, ParameterValue>> {
fun <T> KMutableProperty1<out Any, Any?>?.qget(obj: Any): T {
return (this as KMutableProperty1<Any, T>).get(obj)
}
return trackedObjects.entries.associate { (lo, b) ->
Pair(lo.label, b.parameterControls.keys.associate { k ->
Pair(k.property?.name ?: k.function?.name
?: error("no name"), when (k.parameterType) {
/* 3) setup serializers */
ParameterType.Double -> ParameterValue(doubleValue = k.property.qget(lo.obj) as Double)
ParameterType.Int -> ParameterValue(intValue = k.property.qget(lo.obj) as Int)
ParameterType.Action -> ParameterValue()
ParameterType.Color -> ParameterValue(colorValue = k.property.qget(lo.obj) as ColorRGBa)
ParameterType.Text -> ParameterValue(textValue = k.property.qget(lo.obj) as String)
ParameterType.Boolean -> ParameterValue(booleanValue = k.property.qget(lo.obj) as Boolean)
ParameterType.XY -> ParameterValue(vector2Value = k.property.qget(lo.obj) as Vector2)
ParameterType.DoubleList -> ParameterValue(doubleListValue = k.property.qget(lo.obj) as MutableList<Double>)
ParameterType.Vector2 -> ParameterValue(vector2Value = k.property.qget(lo.obj) as Vector2)
ParameterType.Vector3 -> ParameterValue(vector3Value = k.property.qget(lo.obj) as Vector3)
ParameterType.Vector4 -> ParameterValue(vector4Value = k.property.qget(lo.obj) as Vector4)
ParameterType.Option -> ParameterValue(optionValue = (k.property.qget(lo.obj) as Enum<*>).name)
})
})
}
}
fun saveParameters(file: File) {
file.writeText(Gson().toJson(toObject()))
}
/**
* Can be called by the user to update the gui using an object
* deserialized externally. Allows the user to load a larger json object,
* deserialize it, and use part of it to update the GUI.
* Complements `.toObject()`.
*/
fun fromObject(labeledValues: Map<String, Map<String, ParameterValue>>) {
fun <T> KMutableProperty1<out Any, Any?>?.qset(obj: Any, value: T) =
(this as KMutableProperty1<Any, T>).set(obj, value)
fun KMutableProperty1<out Any, Any?>?.enumSet(obj: Any, value: String) {
val v = (this as KMutableProperty1<Any, Enum<*>>).get(obj)
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UsePropertyAccessSyntax")
val enumValue = (v as java.lang.Enum<*>).getDeclaringClass().getEnumConstants().find { it.name == value }
?: error("cannot map value $value to enum")
(this as KMutableProperty1<Any, Enum<*>>).set(obj, enumValue)
}
labeledValues.forEach { (label, ps) ->
trackedObjects.keys.find { it.label == label }?.let { lo ->
val binding = trackedObjects[lo]!!
ps.forEach { (parameterName, parameterValue) ->
binding.parameters.find { it.property?.name == parameterName }?.let { parameter ->
when (parameter.parameterType) {
/* 4) Set up deserializers */
ParameterType.Double -> parameterValue.doubleValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Int -> parameterValue.intValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Text -> parameterValue.textValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Color -> parameterValue.colorValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.XY -> parameterValue.vector2Value?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.DoubleList -> parameterValue.doubleListValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Boolean -> parameterValue.booleanValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Vector2 -> parameterValue.vector2Value?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Vector3 -> parameterValue.vector3Value?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Vector4 -> parameterValue.vector4Value?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Option -> parameterValue.optionValue?.let {
parameter.property.enumSet(lo.obj, it)
}
ParameterType.Action -> {
// intentionally do nothing
}
}
}
}
}
}
updateControls()
}
fun loadParameters(file: File) {
val json = file.readText()
val typeToken = object : TypeToken<Map<String, Map<String, ParameterValue>>>() {}
val labeledValues: Map<String, Map<String, ParameterValue>> = Gson().fromJson(json, typeToken.type)
fromObject(labeledValues)
}
private fun updateControl(labeledObject: LabeledObject, parameter: Parameter, control: Element) {
when (parameter.parameterType) {
/* 5) Update control from property value */
ParameterType.Double -> {
(control as Slider).value = (parameter.property as KMutableProperty1<Any, Double>).get(labeledObject.obj)
}
ParameterType.Int -> {
(control as Slider).value = (parameter.property as KMutableProperty1<Any, Int>).get(labeledObject.obj).toDouble()
}
ParameterType.Text -> {
(control as Textfield).value = (parameter.property as KMutableProperty1<Any, String>).get(labeledObject.obj)
}
ParameterType.Color -> {
(control as ColorpickerButton).color = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(labeledObject.obj)
}
ParameterType.XY -> {
(control as XYPad).value = (parameter.property as KMutableProperty1<Any, Vector2>).get(labeledObject.obj)
}
ParameterType.DoubleList -> {
(control as SequenceEditor).value = (parameter.property as KMutableProperty1<Any, MutableList<Double>>).get(labeledObject.obj)
}
ParameterType.Boolean -> {
(control as Toggle).value = (parameter.property as KMutableProperty1<Any, Boolean>).get(labeledObject.obj)
}
ParameterType.Vector2 -> {
(control as SlidersVector2).value = (parameter.property as KMutableProperty1<Any, Vector2>).get(labeledObject.obj)
}
ParameterType.Vector3 -> {
(control as SlidersVector3).value = (parameter.property as KMutableProperty1<Any, Vector3>).get(labeledObject.obj)
}
ParameterType.Vector4 -> {
(control as SlidersVector4).value = (parameter.property as KMutableProperty1<Any, Vector4>).get(labeledObject.obj)
}
ParameterType.Option -> {
val ddb = control as DropdownButton
ddb.value = ddb.items().find { item -> item.data == (parameter.property as KMutableProperty1<Any, Enum<*>>).get(labeledObject.obj) } ?: error("could not find item")
}
ParameterType.Action -> {
// intentionally do nothing
}
}
}
fun randomize(strength: Double = 0.05) {
for ((labeledObject, binding) in trackedObjects) {
// -- only randomize visible parameters
for (parameter in binding.parameterControls.keys) {
when (parameter.parameterType) {
/* 6) Set up value randomizers */
ParameterType.Double -> {
val min = parameter.doubleRange!!.start
val max = parameter.doubleRange!!.endInclusive
val currentValue = (parameter.property as KMutableProperty1<Any, Double>).get(labeledObject.obj)
val randomValue = Math.random() * (max - min) + min
val newValue = (1.0 - strength) * currentValue + randomValue * strength
(parameter.property as KMutableProperty1<Any, Double>).set(labeledObject.obj, newValue)
}
ParameterType.Int -> {
val min = parameter.intRange!!.first
val max = parameter.intRange!!.last
val currentValue = (parameter.property as KMutableProperty1<Any, Int>).get(labeledObject.obj)
val randomValue = Math.random() * (max - min) + min
val newValue = ((1.0 - strength) * currentValue + randomValue * strength).roundToInt()
(parameter.property as KMutableProperty1<Any, Int>).set(labeledObject.obj, newValue)
}
ParameterType.Boolean -> {
//I am not sure about randomizing boolean values here
//(parameter.property as KMutableProperty1<Any, Boolean>).set(labeledObject.obj, (Math.random() < 0.5))
}
ParameterType.Color -> {
val currentValue = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(labeledObject.obj)
val randomValue = ColorRGBa(Math.random(), Math.random(), Math.random(), currentValue.a)
val newValue = ColorRGBa((1.0 - strength) * currentValue.r + randomValue.r * strength,
(1.0 - strength) * currentValue.g + randomValue.g * strength,
(1.0 - strength) * currentValue.b + randomValue.b * strength)
(parameter.property as KMutableProperty1<Any, ColorRGBa>).set(labeledObject.obj, newValue)
}
else -> {
// intentionally do nothing
}
}
}
}
updateControls()
}
/**
* Recursively find a unique label
* @param label to find an alternate for in case it already exist
*/
private fun resolveUniqueLabel(label: String): String {
return trackedObjects.keys.find { it.label == label }?.let { lo ->
resolveUniqueLabel(Regex("(.*) / ([0-9]+)").matchEntire(lo.label)?.let {
"${it.groupValues[1]} / ${1 + it.groupValues[2].toInt()}"
} ?: "$label / 2")
} ?: label
}
/**
* Add an object to the GUI
* @param objectWithParameters an object of a class that annotated parameters
* @param label an optional label that overrides the label supplied in a [Description] annotation
* @return pass-through of [objectWithParameters]
*/
fun <T : Any> add(objectWithParameters: T, label: String? = objectWithParameters.title()): T {
val parameters = objectWithParameters.listParameters()
val uniqueLabel = resolveUniqueLabel(label ?: "No name")
if (parameters.isNotEmpty()) {
val collapseStates = persistentCompartmentStates.getOrPut(Driver.instance.contextID) {
mutableMapOf()
}
collapseStates.getOrPut(uniqueLabel) {
CompartmentState(compartmentsCollapsedByDefault)
}
trackedObjects[LabeledObject(uniqueLabel, objectWithParameters)] = TrackedObjectBinding(parameters)
}
return objectWithParameters
}
/**
* Add an object to the GUI using a builder.
* @param label an optional label that overrides the label supplied in a [Description] annotation
* @return the built object
*/
fun <T : Any> add(label: String? = null, builder: () -> T): T {
val t = builder()
return add(t, label ?: t.title())
}
override fun afterDraw(drawer: Drawer, program: Program) {
if (doubleBind) {
updateControls()
}
}
}
@JvmName("addToGui")
fun <T : Any> T.addTo(gui: GUI, label: String? = this.title()): T {
gui.add(this, label)
return this
}

View File

@@ -0,0 +1,220 @@
# orx-keyframer
Create animated timelines by specifying properties and times in keyframes, then play it back at any speed (even
backwards) automatically interpolating properties. Save, load, use mathematical expressions and callbacks. Powerful and
highly reusable.
What this allows you to do:
1. Create a keyframed animation in a json file.
```json
[
{
"time": 0.0,
"easing": "cubic-in-out",
"x": 3.0,
"y": 4.0,
"z": 9.0,
"r": 0.1,
"g": 0.5,
"b": 0.2,
"radius": 50
},
{
"time": 2.0,
"easing": "cubic-in-out",
"r": 0.6,
"g": 0.5,
"b": 0.1
},
{
"time": 4.0,
"easing": "cubic-in-out",
"x": 10.0,
"y": 4.0,
"radius": 400
},
{
"time": 5.0,
"easing": "cubic-in-out",
"x": 100.0,
"y": 320.0,
"radius": 400
},
{
"time": 5.3,
"easing": "cubic-in-out",
"x": 100.0,
"y": 320.0,
"radius": {
"value": 50.0,
"easing": "linear"
}
}
]
```
2. Map the animation data to Kotlin types:
```kotlin
class Animation : Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(File("data/keyframes/animation.json"))
```
3. Animate! (from an OPENRNDR program)
```kotlin
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
```
## Easing
All the easing functions of orx-easing are available
- linear
- back-in
- back-out
- back-in-out
- bounce-in
- bounce-out
- bounce-in-out
- circ-in
- circ-out
- circ-in-out
- cubic-in
- cubic-out
- cubic-in-out
- elastic-in
- elastic-out
- elastic-in-out
- expo-in
- expo-out
- expo-in-out
- quad-in
- quad-out
- quad-in-out
- quart-in
- quart-out
- quart-in-out
- quint-in
- quint-out
- quint-in-out
- sine-in
- sine-out
- sine-in-out
- one
- zero
## More expressive interface
orx-keyframer has two ways of programming key frames. The first is the `"x": <number>` style we have seen before. The
second way uses a dictionary instead of a number value.
For example:
```json
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0
},
{
"time": 10.0,
"easing": "cubic-out",
"x": {
"easing": "cubic-in-out",
"value": 0.0
},
"y": {
"duration": -5.0,
"easing": "cubic-in",
"value": 0.0
}
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"easing": "cubic-in-out"
}
]
```
Inside the value dictionary one can set `value`, `easing`, `duration` and `envelope`.
* `value` the target value, required value
* `easing` easing method that overrides the key's easing method, optional value
* `duration` an optional duration for the animation, set to `0` to jump from the previous
value to the new value, a negative value will start the interpolation before `time`. A positive value
wil start the interpolation at `time` and end at `time + duration`
* `envelope` optional 2-point envelope that modifies the playback of the animation. The default envelope is
`[0.0, 1.0]`. Reverse playback is achieved by supplying `[1.0, 0.0]`. To start the animation later try `[0.1, 1.0]`,
to end the animation earlier try `[0.0, 0.9]`
## Advanced features
orx-keyframer uses two file formats. A `SIMPLE` format and a `FULL` format. For reference check
the [example full format .json](src/demo/resources/demo-full-01.json) and
the [example program](src/demo/kotlin/DemoFull01.kt). The full format adds a `parameters` block and a `prototypes`
block.
[Expressions](src/demo/resources/demo-simple-expressions-01.json), expression mechanism. Currently uses values `r` to
indicate repeat index and `t` the last used key time, `v` the last used value (for the animated attribute).
Supported functions in expressions:
- `min(x, y)`, `max(x, y)`
- `cos(x)`, `sin(x)`, `acos(x)`, `asin(x)`, `tan(x)`, `atan(x)`, `atan2(y, x)`
- `abs(x)`, `saturate(x)`
- `degrees(x)`, `radians(x)`
- `pow(x, y)`, `sqrt(x)`, `exp(x)`
- `mix(left, right, x)`
- `smoothstep(t0, t1, x)`
- `map(leftBefore, rightBefore, leftAfter, rightAfter, x)`
- `random()`, `random(min, max)`
[Parameters and prototypes](src/demo/resources/demo-full-01.json)
<!-- __demos__ -->
## Demos
### DemoEvelope01
[source code](src/demo/kotlin/DemoEvelope01.kt)
![DemoEvelope01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoEvelope01Kt.png)
### DemoFull01
[source code](src/demo/kotlin/DemoFull01.kt)
![DemoFull01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoFull01Kt.png)
### DemoScrub01
[source code](src/demo/kotlin/DemoScrub01.kt)
![DemoScrub01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoScrub01Kt.png)
### DemoSimple01
[source code](src/demo/kotlin/DemoSimple01.kt)
![DemoSimple01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoSimple01Kt.png)
### DemoSimple02
[source code](src/demo/kotlin/DemoSimple02.kt)
![DemoSimple02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoSimple02Kt.png)
### DemoSimpleExpressions01
[source code](src/demo/kotlin/DemoSimpleExpressions01.kt)
![DemoSimpleExpressions01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-keyframer/images/DemoSimpleExpressions01Kt.png)

View File

@@ -0,0 +1,51 @@
//plugins {
// id 'antlr'
//}
apply plugin: 'antlr'
sourceSets {
demo {
java {
srcDirs = ["src/demo/kotlin"]
compileClasspath += main.getCompileClasspath()
runtimeClasspath += main.getRuntimeClasspath()
}
}
main {
java {
srcDir("src/main/java")
srcDir("src/main/kotlin")
srcDir("build/generated-src/antlr")
}
}
}
generateGrammarSource {
maxHeapSize = "64m"
arguments += ["-visitor", "-long-messages"]
outputDirectory = file("${project.buildDir}/generated-src/antlr/org/openrndr/extra/keyframer/antlr".toString())
}
dependencies {
antlr("org.antlr:antlr4:$antlrVersion")
implementation("org.antlr:antlr4-runtime:$antlrVersion")
implementation(project(":orx-noise"))
implementation(project(":orx-easing"))
implementation "com.google.code.gson:gson:$gsonVersion"
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-jvm:orx-panel"))
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
demoImplementation(sourceSets.getByName("main").output)
}
tasks.getByName("compileKotlin").dependsOn("generateGrammarSource")

View File

@@ -0,0 +1,24 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-envelope-01.json")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,28 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-full-01.json")), format = KeyframerFormat.FULL)
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,52 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.panel.controlManager
import org.openrndr.panel.elements.Range
import org.openrndr.panel.elements.Slider
import org.openrndr.panel.elements.slider
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
// -- replace the default clock with an offset clock
var clockOffset = 0.0
val oldClock = clock
clock = { oldClock() - clockOffset }
var clockSlider: Slider? = null
// -- setup a simple UI
val cm = controlManager {
layout {
clockSlider = slider {
range = Range(0.0, 30.0)
events.valueChanged.listen {
if (it.interactive) {
clockOffset = oldClock() - it.newValue
}
}
}
}
}
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(cm)
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-01.json")))
extend {
// -- update the slider
clockSlider?.value = seconds
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,24 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-01.json")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, 100.0)
}
}
}

View File

@@ -0,0 +1,27 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation: Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("radius")
val color by RGBChannel(arrayOf("r", "g", "b"))
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-02.json")))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.fill = animation.color
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,27 @@
import org.openrndr.application
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.resourceUrl
import java.net.URL
suspend fun main() = application {
program {
class Animation : Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
val radius by DoubleChannel("x")
}
val animation = Animation()
animation.loadFromJson(URL(resourceUrl("/demo-simple-expressions-01.json")),
parameters = mapOf("cycleDuration" to 2.0))
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend {
animation(seconds)
drawer.circle(animation.position, animation.radius)
}
}
}

View File

@@ -0,0 +1,25 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0
},
{
"time": 10.0,
"easing": "cubic-in-out",
"x": {
"envelope": [0.5, 1.0],
"value": 0.0
},
"y": {
"envelope": [0.4, 1.0],
"value": 0.0
}
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"easing": "cubic-in-out"
}
]

View File

@@ -0,0 +1,74 @@
{
// this is breaking with proper json but.. gson accepts comments and they are invaluable
// in the parameters block you can add custom values, which can be used in expressions
"parameters": {
"smallRadius": 5.0,
"repetitionCount": 10,
"width": 640.0,
"height": 480.0,
// you can have expressions inside parameters too, they are evaluated once, on load
"resolvedOnLoad" : "width * 2.0"
},
// in the prototypes you can set up key prototypes
"prototypes": {
"red": {
"r": 1.0,
"g": 0.0,
"b": 0.0
},
"blue": {
"r": 0.0,
"g": 0.0,
"b": 1.0
},
"center": {
// prototypes can have expressions too, they are evaluated as late as possible
// thus, they are evaluated more than once
"x": "width / 2",
"y": "height / 2"
},
"small": {
"radius": "smallRadius"
},
"large": {
"radius": "smallRadius * 10.0"
}
},
"keys": [
{
"time": 0.0,
"easing": "cubic-in-out",
"x": 3.0,
"y": 4.0,
"z": 9.0,
"r": 0.0,
"g": 1.0,
"b": 0.0,
"radius": 50,
"foo" : 0.0
},
{
"time": 2.0,
"easing": "cubic-in-out",
// here we apply the prototypes in cascading fashion from left to right
"prototypes": "red center small"
},
{
"time": 3.0,
"repeat": {
"count": "repetitionCount",
"keys": [
{
"time": "(rep * 2.0) + 3.0",
"prototypes": "blue large",
"easing": "cubic-in-out"
},
{
"time": "t + 1.0",
"prototypes": "red small"
}
]
}
}
]
}

View File

@@ -0,0 +1,20 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0
},
{
"time": 10.0,
"x": 0.0,
"y": 0.0,
"easing": "cubic-in-out"
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"easing": "cubic-in-out"
}
]

View File

@@ -0,0 +1,32 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0,
"radius": 0.0,
"r": 1.0,
"g": 1.0,
"b": 1.0
},
{
"time": 5.0,
"radius": 200.0,
"r": 0.0
},
{
"time": 10.0,
"g": 0.0,
"x": 0.0,
"y": 0.0,
"easing": "cubic-in-out"
},
{
"time": 20.0,
"x": 640.0,
"y": 480.0,
"radius": 50.0,
"easing": "cubic-in-out",
"g": 1.0,
"b": 0.0
}
]

View File

@@ -0,0 +1,30 @@
[
{
"time": 0.0,
"x": 320.0,
"y": 240.0,
"radius": 0.0
},
{
"time": 3.0,
"repeat": {
"count": 5,
"keys": [
{
"duration": "cycleDuration * 0.5",
"easing": "cubic-in-out",
"x": 10.0,
"y": 4.0,
"radius": 400
},
{
"duration": "cycleDuration * 0.5",
"easing": "cubic-in-out",
"x": 630.0,
"y": 470.0,
"radius": 40
}
]
}
}
]

View File

@@ -0,0 +1,90 @@
lexer grammar KeyLangLexer;
@header {
package org.openrndr.extra.keyframer.antlr;
}
channels { WHITESPACE }
// Whitespace
NEWLINE : '\r\n' | '\r' | '\n' ;
WS : [\t ]+ -> channel(WHITESPACE) ;
// Keywords
INPUT : 'input' ;
VAR : 'var' ;
PRINT : 'print';
AS : 'as';
INT : 'Int';
DECIMAL : 'Decimal';
STRING : 'String';
// Identifiers
ID : [$_]*[a-zA-Z][A-Za-z0-9_]* ;
FUNCTION_ID : [$_]*[a-z][A-Za-z0-9_]* ;
// Literals
DECLIT : [0-9][0-9]* '.' [0-9]+ ;
INTLIT : '0'|[0-9][0-9]* ;
// Operators
PLUS : '+' ;
PERCENTAGE : '%' ;
MINUS : '-' ;
ASTERISK : '*' ;
DIVISION : '/' ;
ASSIGN : '=' ;
LPAREN : '(' ;
RPAREN : ')' ;
COMMA : ',' ;
STRING_OPEN : '"' -> pushMode(MODE_IN_STRING);
UNMATCHED : . ;
mode MODE_IN_STRING;
ESCAPE_STRING_DELIMITER : '\\"' ;
ESCAPE_SLASH : '\\\\' ;
ESCAPE_NEWLINE : '\\n' ;
ESCAPE_SHARP : '\\#' ;
STRING_CLOSE : '"' -> popMode ;
INTERPOLATION_OPEN : '#{' -> pushMode(MODE_IN_INTERPOLATION) ;
STRING_CONTENT : ~["\n\r\t\\#]+ ;
STR_UNMATCHED : . -> type(UNMATCHED) ;
mode MODE_IN_INTERPOLATION;
INTERPOLATION_CLOSE : '}' -> popMode ;
INTERP_WS : [\t ]+ -> channel(WHITESPACE), type(WS) ;
// Keywords
INTERP_AS : 'as'-> type(AS) ;
INTERP_INT : 'Int'-> type(INT) ;
INTERP_DECIMAL : 'Decimal'-> type(DECIMAL) ;
INTERP_STRING : 'String'-> type(STRING) ;
// Literals
INTERP_INTLIT : ('0'|[1-9][0-9]*) -> type(INTLIT) ;
INTERP_DECLIT : ('0'|[1-9][0-9]*) '.' [0-9]+ -> type(DECLIT) ;
// Operators
INTERP_PLUS : '+' -> type(PLUS) ;
INTERP_MINUS : '-' -> type(MINUS) ;
INTERP_ASTERISK : '*' -> type(ASTERISK) ;
INTERP_DIVISION : '/' -> type(DIVISION) ;
INTERP_PERCENTAGE : '%' -> type(PERCENTAGE) ;
INTERP_ASSIGN : '=' -> type(ASSIGN) ;
INTERP_LPAREN : '(' -> type(LPAREN) ;
INTERP_RPAREN : ')' -> type(RPAREN) ;
// Identifiers
INTERP_ID : [_]*[a-z][A-Za-z0-9_]* -> type(ID);
INTERP_STRING_OPEN : '"' -> type(STRING_OPEN), pushMode(MODE_IN_STRING);
INTERP_UNMATCHED : . -> type(UNMATCHED) ;

View File

@@ -0,0 +1,46 @@
parser grammar KeyLangParser;
@header {
package org.openrndr.extra.keyframer.antlr;
}
options { tokenVocab=KeyLangLexer; }
miniCalcFile : lines=line+ ;
line : statement (NEWLINE | EOF) ;
statement : inputDeclaration # inputDeclarationStatement
| varDeclaration # varDeclarationStatement
| assignment # assignmentStatement
| print # printStatement
| expression # expressionStatement ;
print : PRINT LPAREN expression RPAREN ;
inputDeclaration : INPUT type name=ID ;
varDeclaration : VAR assignment ;
assignment : ID ASSIGN expression ;
expression : INTLIT # intLiteral
| DECLIT # decimalLiteral
| ID LPAREN RPAREN # functionCall0Expression
| ID LPAREN expression RPAREN # functionCall1Expression
| ID LPAREN expression COMMA expression RPAREN # functionCall2Expression
| ID LPAREN expression COMMA expression COMMA expression RPAREN # functionCall3Expression
| ID LPAREN expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall4Expression
| ID LPAREN expression COMMA expression COMMA expression COMMA expression COMMA expression RPAREN # functionCall5Expression
| ID # valueReference
| LPAREN expression RPAREN # parenExpression
| MINUS expression # minusExpression
| expression operator=(DIVISION|ASTERISK|PERCENTAGE) expression # binaryOperation1
| expression operator=(PLUS|MINUS) expression # binaryOperation2;
type : DECIMAL # decimal
| INT # integer
| STRING # string ;

View File

@@ -0,0 +1,386 @@
package org.openrndr.extra.keyframer
import org.antlr.v4.runtime.*
import org.antlr.v4.runtime.tree.ParseTreeWalker
import org.antlr.v4.runtime.tree.TerminalNode
import org.openrndr.extra.keyframer.antlr.KeyLangLexer
import org.openrndr.extra.keyframer.antlr.KeyLangParser
import org.openrndr.extra.keyframer.antlr.KeyLangParserBaseListener
import org.openrndr.extra.noise.uniform
import org.openrndr.math.*
import java.util.*
import kotlin.math.*
typealias Function0 = () -> Double
typealias Function1 = (Double) -> Double
typealias Function2 = (Double, Double) -> Double
typealias Function3 = (Double, Double, Double) -> Double
typealias Function4 = (Double, Double, Double, Double) -> Double
typealias Function5 = (Double, Double, Double, Double, Double) -> Double
class FunctionExtensions(
val functions0: Map<String, Function0> = emptyMap(),
val functions1: Map<String, Function1> = emptyMap(),
val functions2: Map<String, Function2> = emptyMap(),
val functions3: Map<String, Function3> = emptyMap(),
val functions4: Map<String, Function4> = emptyMap(),
val functions5: Map<String, Function5> = emptyMap()
) {
companion object {
val EMPTY = FunctionExtensions()
}
}
internal enum class IDType {
VARIABLE,
FUNCTION0,
FUNCTION1,
FUNCTION2,
FUNCTION3,
FUNCTION4,
FUNCTION5
}
internal class ExpressionListener(val functions: FunctionExtensions = FunctionExtensions.EMPTY) :
KeyLangParserBaseListener() {
val doubleStack = Stack<Double>()
val functionStack = Stack<(DoubleArray) -> Double>()
val variables = mutableMapOf<String, Double>()
val idTypeStack = Stack<IDType>()
var lastExpressionResult: Double? = null
val exceptionStack = Stack<ExpressionException>()
override fun exitExpressionStatement(ctx: KeyLangParser.ExpressionStatementContext) {
ifError {
throw ExpressionException("error in evaluation of '${ctx.text}': ${it.message ?: ""}")
}
val result = doubleStack.pop()
lastExpressionResult = result
}
override fun exitAssignment(ctx: KeyLangParser.AssignmentContext) {
val value = doubleStack.pop()
variables[ctx.ID()?.text ?: error("buh")] = value
}
override fun exitMinusExpression(ctx: KeyLangParser.MinusExpressionContext) {
val op = doubleStack.pop()
doubleStack.push(-op)
}
override fun exitBinaryOperation1(ctx: KeyLangParser.BinaryOperation1Context) {
ifError {
pushError(it.message ?: "")
return
}
val right = doubleStack.pop()
val left = doubleStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangParser.PLUS -> left + right
KeyLangParser.MINUS -> left - right
KeyLangParser.ASTERISK -> left * right
KeyLangParser.DIVISION -> left / right
KeyLangParser.PERCENTAGE -> mod(left, right)
else -> error("operator '$operator' not implemented")
}
doubleStack.push(result)
}
override fun exitBinaryOperation2(ctx: KeyLangParser.BinaryOperation2Context) {
ifError {
pushError(it.message ?: "")
return
}
val left = doubleStack.pop()
val right = doubleStack.pop()
val result = when (val operator = ctx.operator?.type) {
KeyLangParser.PLUS -> left + right
KeyLangParser.MINUS -> right - left
KeyLangParser.ASTERISK -> left * right
KeyLangParser.DIVISION -> left / right
else -> error("operator '$operator' not implemented")
}
doubleStack.push(result)
}
override fun enterValueReference(ctx: KeyLangParser.ValueReferenceContext) {
idTypeStack.push(IDType.VARIABLE)
}
override fun enterFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
idTypeStack.push(IDType.FUNCTION0)
}
override fun exitFunctionCall0Expression(ctx: KeyLangParser.FunctionCall0ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val result = function.invoke(doubleArrayOf())
doubleStack.push(result)
}
override fun enterFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
idTypeStack.push(IDType.FUNCTION1)
}
override fun exitFunctionCall1Expression(ctx: KeyLangParser.FunctionCall1ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument))
doubleStack.push(result)
}
override fun enterFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
idTypeStack.push(IDType.FUNCTION2)
}
override fun exitFunctionCall2Expression(ctx: KeyLangParser.FunctionCall2ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1))
doubleStack.push(result)
}
override fun enterFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
idTypeStack.push(IDType.FUNCTION3)
}
override fun exitFunctionCall3Expression(ctx: KeyLangParser.FunctionCall3ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2))
doubleStack.push(result)
}
override fun enterFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
idTypeStack.push(IDType.FUNCTION4)
}
override fun exitFunctionCall4Expression(ctx: KeyLangParser.FunctionCall4ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument3 = doubleStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2, argument3))
doubleStack.push(result)
}
override fun enterFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
idTypeStack.push(IDType.FUNCTION5)
}
override fun exitFunctionCall5Expression(ctx: KeyLangParser.FunctionCall5ExpressionContext) {
ifError {
pushError(it.message ?: "")
return
}
val function = functionStack.pop()
val argument4 = doubleStack.pop()
val argument3 = doubleStack.pop()
val argument2 = doubleStack.pop()
val argument1 = doubleStack.pop()
val argument0 = doubleStack.pop()
val result = function.invoke(doubleArrayOf(argument0, argument1, argument2, argument3, argument4))
doubleStack.push(result)
}
private fun <T> errorValue(message: String, value: T): T {
pushError(message)
return value
}
private fun pushError(message: String) {
exceptionStack.push(ExpressionException(message))
}
private inline fun ifError(f: (e: Throwable) -> Unit) {
if (exceptionStack.isNotEmpty()) {
val e = exceptionStack.pop()
f(e)
}
}
override fun visitTerminal(node: TerminalNode) {
val type = node.symbol?.type
if (type == KeyLangParser.INTLIT) {
doubleStack.push(node.text.toDouble())
}
if (type == KeyLangParser.DECLIT) {
doubleStack.push(node.text.toDouble())
}
if (type == KeyLangParser.ID) {
@Suppress("DIVISION_BY_ZERO")
when (val idType = idTypeStack.pop()) {
IDType.VARIABLE -> doubleStack.push(
when (val name = node.text) {
"PI" -> PI
else -> variables[name] ?: errorValue("unresolved variable: '${name}'", 0.0 / 0.0)
}
)
IDType.FUNCTION0 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"random" -> { _ -> Double.uniform(0.0, 1.0) }
else -> functions.functions0[candidate]?.let { { _: DoubleArray -> it.invoke() } }
?: errorValue(
"unresolved function: '${candidate}()'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION1 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"sqrt" -> { x -> sqrt(x[0]) }
"radians" -> { x -> Math.toRadians(x[0]) }
"degrees" -> { x -> Math.toDegrees(x[0]) }
"cos" -> { x -> cos(x[0]) }
"sin" -> { x -> sin(x[0]) }
"tan" -> { x -> tan(x[0]) }
"atan" -> { x -> atan(x[0]) }
"acos" -> { x -> acos(x[0]) }
"asin" -> { x -> asin(x[0]) }
"exp" -> { x -> exp(x[0]) }
"abs" -> { x -> abs(x[0]) }
"floor" -> { x -> floor(x[0]) }
"ceil" -> { x -> ceil(x[0]) }
"saturate" -> { x -> x[0].coerceIn(0.0, 1.0) }
else -> functions.functions1[candidate]?.let { { x: DoubleArray -> it.invoke(x[0]) } }
?: errorValue(
"unresolved function: '${candidate}(x0)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION2 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"max" -> { x -> max(x[0], x[1]) }
"min" -> { x -> min(x[0], x[1]) }
"pow" -> { x -> x[0].pow(x[1]) }
"atan2" -> { x -> atan2(x[0], x[1]) }
"random" -> { x -> Double.uniform(x[0], x[1]) }
"length" -> { x -> Vector2(x[0], x[1]).length }
else -> functions.functions2[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION3 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"mix" -> { x -> mix(x[0], x[1], x[2]) }
"smoothstep" -> { x -> smoothstep(x[0], x[1], x[2]) }
"length" -> { x -> Vector3(x[0], x[1], x[2]).length }
else -> functions.functions3[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION4 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
else -> functions.functions4[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2, x3)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
IDType.FUNCTION5 -> {
val function: (DoubleArray) -> Double =
when (val candidate = node.text) {
"map" -> { x -> map(x[0], x[1], x[2], x[3], x[4]) }
else -> functions.functions5[candidate]?.let { { x: DoubleArray -> it.invoke(x[0], x[1], x[2], x[3], x[4]) } }
?: errorValue(
"unresolved function: '${candidate}(x0, x1, x2, x3, x4)'"
) { _ -> error("this is the error function") }
}
functionStack.push(function)
}
else -> error("unsupported id-type $idType")
}
}
}
}
class ExpressionException(message: String) : RuntimeException(message)
fun evaluateExpression(
input: String,
variables: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
): Double? {
val lexer = KeyLangLexer(CharStreams.fromString(input))
val parser = KeyLangParser(CommonTokenStream(lexer))
parser.removeErrorListeners()
parser.addErrorListener(object : BaseErrorListener() {
override fun syntaxError(
recognizer: Recognizer<*, *>?,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String?,
e: RecognitionException?
) {
throw ExpressionException("parser error in expression: '$input'; [line: $line, character: $charPositionInLine ${offendingSymbol?.let { ", near: $it" } ?: ""} ]")
}
})
val root = parser.miniCalcFile()
val listener = ExpressionListener(functions)
listener.variables.putAll(variables)
try {
ParseTreeWalker.DEFAULT.walk(listener, root)
} catch (e: ExpressionException) {
throw ExpressionException(e.message ?: "")
}
return listener.lastExpressionResult
}

View File

@@ -0,0 +1,75 @@
package org.openrndr.extra.keyframer
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.map
internal val defaultEnvelope = doubleArrayOf(0.0, 1.0)
class Key(val time: Double, val value: Double, val easing: EasingFunction, val envelope: DoubleArray = defaultEnvelope)
class KeyframerChannel {
val keys = mutableListOf<Key>()
operator fun invoke() : Double {
return 0.0
}
fun add(
time: Double,
value: Double?,
easing: EasingFunction = Easing.Linear.function,
envelope: DoubleArray = defaultEnvelope
) {
require(envelope.size >= 2) {
"envelope should contain at least 2 entries"
}
value?.let {
keys.add(Key(time, it, easing, envelope))
}
}
fun lastValue(): Double? {
return keys.lastOrNull()?.value
}
fun lastTime(): Double? {
return keys.lastOrNull()?.time
}
fun duration(): Double {
return keys.last().time
}
fun value(time: Double): Double? {
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 te = t0.map(rightKey.envelope[0], rightKey.envelope[1], 0.0, 1.0, clamp = true)
val e0 = rightKey.easing(te, 0.0, 1.0, 1.0)
leftKey.value * (1.0 - e0) + rightKey.value * (e0)
}
}
}

View File

@@ -0,0 +1,60 @@
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) {
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,58 @@
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) {
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

@@ -0,0 +1,454 @@
package org.openrndr.extra.keyframer
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import org.openrndr.color.ColorRGBa
import org.openrndr.extras.easing.Easing
import org.openrndr.extras.easing.EasingFunction
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import java.io.File
import java.lang.IllegalStateException
import java.lang.NullPointerException
import java.net.URL
import kotlin.math.max
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible
enum class KeyframerFormat {
SIMPLE,
FULL
}
open class Keyframer {
private var currentTime = 0.0
operator fun invoke(time: Double) {
currentTime = time
}
open inner class CompoundChannel(val keys: Array<String>, private val defaultValues: Array<Double>) {
private var channelTimes: Array<Double> = Array(keys.size) { Double.NEGATIVE_INFINITY }
private var compoundChannels: Array<KeyframerChannel?> = Array(keys.size) { null }
private var cachedValues: Array<Double?> = Array(keys.size) { null }
open fun reset() {
for (i in channelTimes.indices) {
channelTimes[i] = Double.NEGATIVE_INFINITY
}
}
fun getValue(compound: Int): Double {
if (compoundChannels[compound] == null) {
compoundChannels[compound] = channels[keys[compound]]
}
return if (compoundChannels[compound] != null) {
if (channelTimes[compound] == currentTime && cachedValues[compound] != null) {
cachedValues[compound] ?: defaultValues[compound]
} else {
val value = compoundChannels[compound]?.value(currentTime) ?: defaultValues[compound]
cachedValues[compound] = value
value
}
} else {
defaultValues[compound]
}
}
}
val duration: Double
get() = channels.values.maxByOrNull { it.duration() }?.duration() ?: 0.0
inner class DoubleChannel(key: String, defaultValue: Double = 0.0) :
CompoundChannel(arrayOf(key), arrayOf(defaultValue)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Double = getValue(0)
}
inner class Vector2Channel(keys: Array<String>, defaultValue: Vector2 = Vector2.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector2 = Vector2(getValue(0), getValue(1))
}
inner class Vector3Channel(keys: Array<String>, defaultValue: Vector3 = Vector3.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector3 =
Vector3(getValue(0), getValue(1), getValue(2))
}
inner class Vector4Channel(keys: Array<String>, defaultValue: Vector4 = Vector4.ZERO) :
CompoundChannel(keys, arrayOf(defaultValue.x, defaultValue.y, defaultValue.z, defaultValue.w)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): Vector4 =
Vector4(getValue(0), getValue(1), getValue(2), getValue(3))
}
inner class RGBaChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b, defaultValue.a)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2), getValue(3))
}
inner class RGBChannel(keys: Array<String>, defaultValue: ColorRGBa = ColorRGBa.WHITE) :
CompoundChannel(keys, arrayOf(defaultValue.r, defaultValue.g, defaultValue.b)) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): ColorRGBa =
ColorRGBa(getValue(0), getValue(1), getValue(2))
}
inner class DoubleArrayChannel(keys: Array<String>, defaultValue: DoubleArray = DoubleArray(keys.size)) :
CompoundChannel(keys, defaultValue.toTypedArray()) {
operator fun getValue(keyframer: Keyframer, property: KProperty<*>): DoubleArray {
val result = DoubleArray(keys.size)
for (i in keys.indices) {
result[i] = getValue(i)
}
return result
}
}
val channels = mutableMapOf<String, KeyframerChannel>()
fun loadFromJson(
file: File,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
require(file.exists()) {
"failed to load keyframer from json: '${file.absolutePath}' does not exist."
}
try {
loadFromJsonString(file.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading from '${file.path}': ${e.message ?: ""}")
}
}
fun loadFromJson(
url: URL,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
try {
loadFromJsonString(url.readText(), format, parameters, functions)
} catch (e: ExpressionException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
} catch (e: IllegalStateException) {
throw ExpressionException("Error loading $format from '${url}': ${e.message ?: ""}")
}
}
fun loadFromJsonString(
json: String,
format: KeyframerFormat = KeyframerFormat.SIMPLE,
parameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
when (format) {
KeyframerFormat.SIMPLE -> {
try {
val type = object : TypeToken<List<Map<String, Any>>>() {}.type
val keys: List<MutableMap<String, Any>> = Gson().fromJson(json, type)
loadFromKeyObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
} catch (e: NullPointerException) {
error("Error parsing simple Keyframer data: ${e.cause?.message}")
}
}
KeyframerFormat.FULL -> {
try {
val type = object : TypeToken<Map<String, Any>>() {}.type
val keys: Map<String, Any> = Gson().fromJson(json, type)
loadFromObjects(keys, parameters, functions)
} catch (e: JsonSyntaxException) {
error("Error parsing full Keyframer data: ${e.cause?.message}")
}
}
}
}
private val parameters = mutableMapOf<String, Double>()
private val prototypes = mutableMapOf<String, Map<String, Any>>()
fun loadFromObjects(
dict: Map<String, Any>,
externalParameters: Map<String, Double> = emptyMap(),
functions: FunctionExtensions = FunctionExtensions.EMPTY
) {
this.parameters.clear()
this.parameters.putAll(externalParameters)
prototypes.clear()
@Suppress("UNCHECKED_CAST")
(dict["parameters"] as? Map<String, Any>)?.let { lp ->
for (entry in lp) {
this.parameters[entry.key] = try {
when (val candidate = entry.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, parameters, functions)
?: error("could not evaluate expression: '$candidate'")
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown type for parameter '${entry.key}'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in 'parameters': ${e.message ?: ""} ")
}
}
}
this.parameters.putAll(externalParameters)
@Suppress("UNCHECKED_CAST")
(dict["prototypes"] as? Map<String, Map<String, Any>>)?.let {
prototypes.putAll(it)
}
@Suppress("UNCHECKED_CAST")
(dict["keys"] as? List<Map<String, Any>>)?.let { keys ->
loadFromKeyObjects(keys, parameters, functions)
}
}
private fun resolvePrototype(prototypeNames: String): Map<String, Any> {
val prototypeTokens = prototypeNames.split(" ").map { it.trim() }.filter { it.isNotBlank() }
val prototypeRefs = prototypeTokens.mapNotNull { prototypes[it] }
val computed = mutableMapOf<String, Any>()
for (ref in prototypeRefs) {
computed.putAll(ref)
}
return computed
}
fun loadFromKeyObjects(
keys: List<Map<String, Any>>,
externalParameters: Map<String, Double>,
functions: FunctionExtensions
) {
if (externalParameters !== parameters) {
parameters.clear()
parameters.putAll(externalParameters)
}
var lastTime = 0.0
val channelDelegates = this::class.memberProperties
.mapNotNull {
@Suppress("UNCHECKED_CAST")
it as? KProperty1<Keyframer, Any>
}
.filter { it.isAccessible = true; it.getDelegate(this) is CompoundChannel }
.associate { Pair(it.name, it.getDelegate(this) as CompoundChannel) }
val channelKeys = channelDelegates.values.flatMap { channel ->
channel.keys.map { it }
}.toSet()
for (delegate in channelDelegates.values) {
delegate.reset()
}
val expressionContext = mutableMapOf<String, Double>()
expressionContext.putAll(parameters)
expressionContext["t"] = 0.0
fun easingFunctionFromName(easingCandidate: String): EasingFunction {
return when (easingCandidate) {
"linear" -> Easing.Linear.function
"back-in" -> Easing.BackIn.function
"back-out" -> Easing.BackOut.function
"back-in-out" -> Easing.BackInOut.function
"bounce-in" -> Easing.BounceIn.function
"bounce-out" -> Easing.BounceOut.function
"bounce-in-out" -> Easing.BackInOut.function
"circ-in" -> Easing.CircIn.function
"circ-out" -> Easing.CircOut.function
"circ-in-out" -> Easing.CircInOut.function
"cubic-in" -> Easing.CubicIn.function
"cubic-out" -> Easing.CubicOut.function
"cubic-in-out" -> Easing.CubicInOut.function
"elastic-in" -> Easing.ElasticIn.function
"elastic-out" -> Easing.ElasticInOut.function
"elastic-in-out" -> Easing.ElasticOut.function
"expo-in" -> Easing.ExpoIn.function
"expo-out" -> Easing.ExpoOut.function
"expo-in-out" -> Easing.ExpoInOut.function
"quad-in" -> Easing.QuadIn.function
"quad-out" -> Easing.QuadOut.function
"quad-in-out" -> Easing.QuadInOut.function
"quart-in" -> Easing.QuartIn.function
"quart-out" -> Easing.QuartOut.function
"quart-in-out" -> Easing.QuartInOut.function
"quint-in" -> Easing.QuintIn.function
"quint-out" -> Easing.QuintOut.function
"quint-in-out" -> Easing.QuintInOut.function
"sine-in" -> Easing.SineIn.function
"sine-out" -> Easing.SineOut.function
"sine-in-out" -> Easing.SineInOut.function
"one" -> Easing.One.function
"zero" -> Easing.Zero.function
else -> error("unknown easing name '$easingCandidate'")
}
}
fun handleKey(key: Map<String, Any>, path: String) {
val prototype = (key["prototypes"] as? String)?.let {
resolvePrototype(it)
} ?: emptyMap()
val computed = mutableMapOf<String, Any>()
computed.putAll(prototype)
computed.putAll(key)
val time = try {
when (val candidate = computed["time"]) {
null -> lastTime
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Double -> candidate
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
else -> error("unknown time format for '$candidate'")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'time': ${e.message ?: ""}")
}
val duration = try {
when (val candidate = computed["duration"]) {
null -> 0.0
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error { "unknown value format for time : $candidate" }
is Int -> candidate.toDouble()
is Float -> candidate.toDouble()
is Double -> candidate
else -> error("unknown duration type for '$candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'duration': ${e.message ?: ""}")
}
val easing = try {
when (val easingCandidate = computed["easing"]) {
null -> Easing.Linear.function
is String -> easingFunctionFromName(easingCandidate)
else -> error("unknown easing for '$easingCandidate'")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'easing': ${e.message ?: ""}")
}
val envelope = try {
when (val candidate = computed["envelope"]) {
null -> defaultEnvelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
} catch (e: IllegalStateException) {
throw ExpressionException("error in $path.'envelope': ${e.message ?: ""}")
}
val reservedKeys = setOf("time", "easing", "envelope")
for (channelCandidate in computed.filter { it.key !in reservedKeys }) {
if (channelCandidate.key in channelKeys) {
val channel = channels.getOrPut(channelCandidate.key) {
KeyframerChannel()
}
val lastValue = channel.lastValue() ?: 0.0
expressionContext["v"] = lastValue
val lastTime = (channel.lastTime()) ?: 0.0
expressionContext["d"] = time - lastTime
if (channelCandidate.value is Map<*, *>) {
@Suppress("UNCHECKED_CAST")
val valueMap = channelCandidate.value as Map<String, Any>
val value = try {
when (val candidate = valueMap["value"]) {
null -> error("no value for '${channelCandidate.key}'")
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
val dictEasing = when (val candidate = valueMap["easing"]) {
null -> easing
is String -> easingFunctionFromName(candidate)
else -> error("unknown easing for '$candidate'")
}
val dictEnvelope = when (val candidate = valueMap["envelope"]) {
null -> envelope
is DoubleArray -> candidate
is List<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
is Array<*> -> candidate.map { it.toString().toDouble() }.toDoubleArray()
else -> error("unknown envelope for '$candidate")
}
val dictDuration = try {
when (val candidate = valueMap["duration"]) {
null -> null
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
if (dictDuration != null) {
if (dictDuration <= 0.0) {
channel.add(max(lastTime, time + dictDuration), lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time, value, dictEasing, dictEnvelope)
} else {
channel.add(time, lastValue, Easing.Linear.function, defaultEnvelope)
channel.add(time + dictDuration, value, dictEasing, dictEnvelope)
}
} else {
channel.add(time, value, dictEasing, dictEnvelope)
}
} else {
val value = try {
when (val candidate = channelCandidate.value) {
is Double -> candidate
is String -> evaluateExpression(candidate, expressionContext, functions)
?: error("unknown value format for key '${channelCandidate.key}' : $candidate")
is Int -> candidate.toDouble()
else -> error("unknown value type for key '${channelCandidate.key}' : $candidate")
}
} catch (e: ExpressionException) {
throw ExpressionException("error in $path.'${channelCandidate.key}': ${e.message ?: ""}")
}
channel.add(time, value, easing, envelope)
}
}
}
lastTime = time + duration
expressionContext["t"] = lastTime
}
for ((index, key) in keys.withIndex()) {
handleKey(key, "keys[$index]")
}
}
}

View File

@@ -0,0 +1,57 @@
import org.amshove.kluent.`should throw`
import org.amshove.kluent.`with message`
import org.amshove.kluent.invoking
import org.openrndr.extra.keyframer.ExpressionException
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.lang.IllegalStateException
object TestExpressionErrors : Spek({
describe("an expression with non-sensible writing") {
val expression = ")("
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
}
describe("an expression with equality instead of assign") {
val expression = "a == 5"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: 'a == 5'; [line: 1, character: 3 , near: [@3,3:3='=',<19>,1:3] ]"
}
}
describe("an expression trying to reassign a number") {
val expression = "3 = 5"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "parser error in expression: '3 = 5'; [line: 1, character: 2 , near: [@2,2:2='=',<19>,1:2] ]"
}
}
describe("an expression that uses non-existing functions") {
val expression = "notExisting(5)"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting(5)': unresolved function: 'notExisting(x0)'"
}
}
describe("an expression that uses non-existing variables") {
val expression = "notExisting + 4"
it("should cause an exception to be thrown when evaluated") {
invoking {
evaluateExpression(expression)
} `should throw` ExpressionException::class `with message` "error in evaluation of 'notExisting+4': unresolved variable: 'notExisting'"
}
}
})

View File

@@ -0,0 +1,92 @@
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.FunctionExtensions
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestFunctionCall : Spek({
describe("a function call") {
val expression = "sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
describe("two function calls") {
val expression = "sqrt(4.0) * sqrt(4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("two argument function call") {
val expression = "max(0.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("two argument function call") {
val expression = "min(8.0, 4.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("three argument function call") {
val expression = "mix(8.0, 4.0, 0.5)"
val result = evaluateExpression(expression)
result?.shouldBeNear(6.0, 10E-6)
}
describe("five argument function call") {
val expression = "map(0.0, 1.0, 0.0, 8.0, 0.5)"
val result = evaluateExpression(expression)
result?.shouldBeNear(4.0, 10E-6)
}
describe("two argument function call, where argument order matters") {
val expression = "pow(2.0, 3.0)"
val result = evaluateExpression(expression)
result?.shouldBeNear(8.0, 10E-6)
}
describe("nested function call") {
val expression = "sqrt(min(8.0, 4.0))"
val result = evaluateExpression(expression)
result?.shouldBeNear(2.0, 10E-6)
}
describe("extension function0 call") {
val expression = "extension()"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions0 = mapOf("extension" to { 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
describe("extension function1 call") {
val expression = "extension(1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions1 = mapOf("extension" to { x -> x * 2.0 })))
result?.shouldBeNear(2.0, 10E-6)
}
describe("extension function2 call") {
val expression = "extension(1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions2 = mapOf("extension" to { x, y -> x + y })))
result?.shouldBeNear(2.0, 10E-6)
}
describe("extension function3 call") {
val expression = "extension(1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions3 = mapOf("extension" to { x, y, z -> x + y + z})))
result?.shouldBeNear(3.0, 10E-6)
}
describe("extension function4 call") {
val expression = "extension(1.0, 1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions4 = mapOf("extension" to { x, y, z, w -> x + y + z + w})))
result?.shouldBeNear(4.0, 10E-6)
}
describe("extension function5 call") {
val expression = "extension(1.0, 1.0, 1.0, 1.0, 1.0)"
val result = evaluateExpression(expression, functions = FunctionExtensions(functions5 = mapOf("extension" to { x, y, z, w, u -> x + y + z + w + u})))
result?.shouldBeNear(5.0, 10E-6)
}
})

View File

@@ -0,0 +1,35 @@
import org.amshove.kluent.`should be`
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.KeyframerChannel
import org.openrndr.extras.easing.Easing
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestKeyframerChannel : Spek({
describe("a keyframer channel without keys") {
val kfc = KeyframerChannel()
it ("should return null when asking for value before first key time") {
kfc.value(0.0) `should be` null
}
}
describe("a keyframer channel with a single key") {
val kfc = KeyframerChannel()
kfc.add(0.0, 1.0, Easing.Linear.function)
kfc.value(0.0)?.shouldBeNear(1.0, 10E-6)
it ("should return null when asking for value before first key time") {
kfc.value(-1.0) `should be` null
}
}
describe("a keyframer channel with two keys") {
val kfc = KeyframerChannel()
kfc.add(0.0, 1.0, Easing.Linear.function)
kfc.add(1.0, 2.0, Easing.Linear.function)
kfc.value(0.0)?.shouldBeNear(1.0, 10E-6)
it ("should return null when asking for value before first key time") {
kfc.value(-1.0) `should be` null
}
}
})

View File

@@ -0,0 +1,110 @@
import org.amshove.kluent.`should throw`
import org.amshove.kluent.`with message`
import org.amshove.kluent.invoking
import org.openrndr.extra.keyframer.ExpressionException
import org.openrndr.extra.keyframer.Keyframer
import org.openrndr.extra.keyframer.KeyframerFormat
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import java.io.File
import kotlin.IllegalStateException
private fun testFile(path: String) : File {
val test = File(".")
return if (test.absolutePath.endsWith("orx-keyframer/.")) {
File(path)
} else {
File("orx-keyframer/$path")
}
}
private fun testName(path: String) : String {
val test = File(".")
return (if (test.absolutePath.endsWith("orx-keyframer/.")) {
path
} else {
"orx-keyframer/$path"
}).replace("/", File.separator)
}
object TestKeyframerErrors : Spek({
class Animation : Keyframer() {
val position by Vector2Channel(arrayOf("x", "y"))
}
describe("loading a faulty json") {
val animation = Animation()
val json = """
"""
it("should throw an exception") {
invoking { animation.loadFromJsonString(json) } `should throw` (IllegalStateException::class)
}
}
describe("loading a non existing json") {
val animation = Animation()
it("should throw an exception") {
invoking { animation.loadFromJson(testFile("this-does-not-exist")) } `should throw` (IllegalArgumentException::class)
}
}
describe("loading a json with a faulty time expression (1)") {
File(".").apply {
println(this.absolutePath)
}
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/time-01.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/time-01.json")}': error in keys[0].'time': parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]"
}
}
// Paths.sep
//
//Expected <Error loading from 'orx-keyframer/src\test\resources\error-reporting\time-01.json': error in keys[0].'time': parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]>,
// actual <Error loading from 'orx-keyframer\src\test\resources\error-reporting\time-01.json': error in keys[0].'time': parser error in expression: ')('; [line: 1, character: 0 , near: [@0,0:0=')',<21>,1:0] ]>.
describe("loading a json with a faulty time expression (2) ") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/time-02.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/time-02.json")}': error in keys[0].'time': error in evaluation of 'doesNotExist': unresolved variable: 'doesNotExist'"
}
}
describe("loading a json with a non-existing easing") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/easing.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/easing.json")}': error in keys[0].'easing': unknown easing name 'garble'"
}
}
describe("loading a json with a faulty value (1)") {
val animation = Animation()
it("should throw an exception") {
invoking {
animation.loadFromJson(
testFile("src/test/resources/error-reporting/value-01.json"),
format = KeyframerFormat.SIMPLE
)
} `should throw` ExpressionException::class `with message` "Error loading from '${testName("src/test/resources/error-reporting/value-01.json")}': error in keys[0].'x': error in evaluation of 'garble': unresolved variable: 'garble'"
}
}
})

View File

@@ -0,0 +1,39 @@
import org.amshove.kluent.shouldBeNear
import org.openrndr.extra.keyframer.evaluateExpression
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
object TestOperators : Spek({
describe("an addition operation") {
val result = evaluateExpression("1 + 2")
result?.shouldBeNear(3.0, 10E-6)
}
describe("a subtraction operation") {
val result = evaluateExpression("1 - 2")
result?.shouldBeNear(-1.0, 10E-6)
}
describe("a modulus operation") {
val result = evaluateExpression("4 % 2")
result?.shouldBeNear(0.0, 10E-6)
}
describe("a multiplication operation") {
val result = evaluateExpression("4 * 2")
result?.shouldBeNear(8.0, 10E-6)
}
describe("a division operation") {
val result = evaluateExpression("4 / 2")
result?.shouldBeNear(2.0, 10E-6)
}
describe("a multiplication/addition operation") {
val result = evaluateExpression("4 * 2 + 1")
result?.shouldBeNear(9.0, 10E-6)
}
describe("an addition/multiplication") {
val result = evaluateExpression("4 + 2 * 3")
result?.shouldBeNear(10.0, 10E-6)
}
describe("unary minus") {
val result = evaluateExpression("-4.0")
result?.shouldBeNear(-4.0, 10E-6)
}
})

View File

@@ -0,0 +1,5 @@
[
{
"easing": "garble"
}
]

View File

@@ -0,0 +1,5 @@
[
{
"time": ")("
}
]

Some files were not shown because too many files have changed in this diff Show More