diff --git a/build.gradle b/build.gradle index aa49c59e..f98fc092 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { apply plugin: 'org.jetbrains.dokka' project.ext { - openrndrVersion = "0.3.42" + openrndrVersion = "0.3.43-rc.1" kotlinVersion = "1.3.72" spekVersion = "2.0.10" libfreenectVersion = "0.5.7-1.5.3" diff --git a/demo-data/gltf-models/box/Box.gltf b/demo-data/gltf-models/box/Box.gltf new file mode 100644 index 00000000..f434ca23 --- /dev/null +++ b/demo-data/gltf-models/box/Box.gltf @@ -0,0 +1,142 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2 + }, + "indices": 0, + "mode": 5, + "material": 0 + } + ], + "name": "Mesh" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 288, + "componentType": 5126, + "count": 24, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.800000011920929, + 0.0, + 0.0, + 1.0 + ], + "metallicFactor": 0.0 + }, + "name": "Red" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 576, + "byteStride": 12, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 648, + "uri": "Box0.bin" + } + ] +} diff --git a/demo-data/gltf-models/box/Box0.bin b/demo-data/gltf-models/box/Box0.bin new file mode 100644 index 00000000..d7798abb Binary files /dev/null and b/demo-data/gltf-models/box/Box0.bin differ diff --git a/demo-data/gltf-models/duck/Duck.gltf b/demo-data/gltf-models/duck/Duck.gltf new file mode 100644 index 00000000..b80c842c --- /dev/null +++ b/demo-data/gltf-models/duck/Duck.gltf @@ -0,0 +1,219 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 2, + 1 + ], + "matrix": [ + 0.009999999776482582, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009999999776482582, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009999999776482582, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "matrix": [ + -0.7289686799049377, + 0.0, + -0.6845470666885376, + 0.0, + -0.4252049028873444, + 0.7836934328079224, + 0.4527972936630249, + 0.0, + 0.5364750623703003, + 0.6211478114128113, + -0.571287989616394, + 0.0, + 400.1130065917969, + 463.2640075683594, + -431.0780334472656, + 1.0 + ], + "camera": 0 + }, + { + "mesh": 0 + } + ], + "cameras": [ + { + "perspective": { + "aspectRatio": 1.5, + "yfov": 0.6605925559997559, + "zfar": 10000.0, + "znear": 1.0 + }, + "type": "perspective" + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2, + "TEXCOORD_0": 3 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ], + "name": "LOD3spShape" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 12636, + "max": [ + 2398 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 2399, + "max": [ + 0.9995989799499512, + 0.999580979347229, + 0.9984359741210938 + ], + "min": [ + -0.9990839958190918, + -1.0, + -0.9998319745063782 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 28788, + "componentType": 5126, + "count": 2399, + "max": [ + 96.17990112304688, + 163.97000122070313, + 53.92519760131836 + ], + "min": [ + -69.29850006103516, + 9.929369926452637, + -61.32819747924805 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 2399, + "max": [ + 0.9833459854125976, + 0.9800369739532472 + ], + "min": [ + 0.026409000158309938, + 0.01996302604675293 + ], + "type": "VEC2" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.0 + }, + "emissiveFactor": [ + 0.0, + 0.0, + 0.0 + ], + "name": "blinn3-fx" + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ], + "images": [ + { + "uri": "DuckCM.png" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9986, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 76768, + "byteLength": 25272, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 57576, + "byteStride": 12, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 57576, + "byteLength": 19192, + "byteStride": 8, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 102040, + "uri": "Duck0.bin" + } + ] +} diff --git a/demo-data/gltf-models/duck/Duck0.bin b/demo-data/gltf-models/duck/Duck0.bin new file mode 100644 index 00000000..5f01f88a Binary files /dev/null and b/demo-data/gltf-models/duck/Duck0.bin differ diff --git a/demo-data/gltf-models/duck/DuckCM.png b/demo-data/gltf-models/duck/DuckCM.png new file mode 100644 index 00000000..9fa2dd4c Binary files /dev/null and b/demo-data/gltf-models/duck/DuckCM.png differ diff --git a/demo-data/gltf-models/suzanne/Suzanne.bin b/demo-data/gltf-models/suzanne/Suzanne.bin new file mode 100644 index 00000000..60f54db7 Binary files /dev/null and b/demo-data/gltf-models/suzanne/Suzanne.bin differ diff --git a/demo-data/gltf-models/suzanne/Suzanne.gltf b/demo-data/gltf-models/suzanne/Suzanne.gltf new file mode 100644 index 00000000..56607849 --- /dev/null +++ b/demo-data/gltf-models/suzanne/Suzanne.gltf @@ -0,0 +1,193 @@ +{ + "accessors" : [ + { + "bufferView" : 0, + "byteOffset" : 0, + "componentType" : 5123, + "count" : 11808, + "max" : [ + 11807 + ], + "min" : [ + 0 + ], + "type" : "SCALAR" + }, + { + "bufferView" : 1, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 11808, + "max" : [ + 1.336914, + 0.950195, + 0.825684 + ], + "min" : [ + -1.336914, + -0.974609, + -0.800781 + ], + "type" : "VEC3" + }, + { + "bufferView" : 2, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 11808, + "max" : [ + 0.996339, + 0.999958, + 0.999929 + ], + "min" : [ + -0.996339, + -0.985940, + -0.999994 + ], + "type" : "VEC3" + }, + { + "bufferView" : 3, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 11808, + "max" : [ + 0.998570, + 0.999996, + 0.999487, + 1.000000 + ], + "min" : [ + -0.999233, + -0.999453, + -0.999812, + 1.000000 + ], + "type" : "VEC4" + }, + { + "bufferView" : 4, + "byteOffset" : 0, + "componentType" : 5126, + "count" : 11808, + "max" : [ + 0.999884, + 0.884359 + ], + "min" : [ + 0.000116, + 0.000116 + ], + "type" : "VEC2" + } + ], + "asset" : { + "generator" : "VKTS glTF 2.0 exporter", + "version" : "2.0" + }, + "bufferViews" : [ + { + "buffer" : 0, + "byteLength" : 23616, + "byteOffset" : 0, + "target" : 34963 + }, + { + "buffer" : 0, + "byteLength" : 141696, + "byteOffset" : 23616, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 141696, + "byteOffset" : 165312, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 188928, + "byteOffset" : 307008, + "target" : 34962 + }, + { + "buffer" : 0, + "byteLength" : 94464, + "byteOffset" : 495936, + "target" : 34962 + } + ], + "buffers" : [ + { + "byteLength" : 590400, + "uri" : "Suzanne.bin" + } + ], + "images" : [ + { + "uri" : "Suzanne_BaseColor.png" + }, + { + "uri" : "Suzanne_MetallicRoughness.png" + } + ], + "materials" : [ + { + "name" : "Suzanne", + "pbrMetallicRoughness" : { + "baseColorTexture" : { + "index" : 0 + }, + "metallicRoughnessTexture" : { + "index" : 1 + } + } + } + ], + "meshes" : [ + { + "name" : "Suzanne", + "primitives" : [ + { + "attributes" : { + "NORMAL" : 2, + "POSITION" : 1, + "TANGENT" : 3, + "TEXCOORD_0" : 4 + }, + "indices" : 0, + "material" : 0, + "mode" : 4 + } + ] + } + ], + "nodes" : [ + { + "mesh" : 0, + "name" : "Suzanne" + } + ], + "samplers" : [ + {} + ], + "scene" : 0, + "scenes" : [ + { + "nodes" : [ + 0 + ] + } + ], + "textures" : [ + { + "sampler" : 0, + "source" : 0 + }, + { + "sampler" : 0, + "source" : 1 + } + ] +} diff --git a/demo-data/gltf-models/suzanne/Suzanne_BaseColor.png b/demo-data/gltf-models/suzanne/Suzanne_BaseColor.png new file mode 100644 index 00000000..35469abf Binary files /dev/null and b/demo-data/gltf-models/suzanne/Suzanne_BaseColor.png differ diff --git a/demo-data/gltf-models/suzanne/Suzanne_MetallicRoughness.png b/demo-data/gltf-models/suzanne/Suzanne_MetallicRoughness.png new file mode 100644 index 00000000..e4ff1fde Binary files /dev/null and b/demo-data/gltf-models/suzanne/Suzanne_MetallicRoughness.png differ diff --git a/orx-dnk3/README.md b/orx-dnk3/README.md new file mode 100644 index 00000000..c0275497 --- /dev/null +++ b/orx-dnk3/README.md @@ -0,0 +1,20 @@ +# 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 + - [ ] Skinning + - [x] Double-sided materials + - [ ] Transparency +- [ ] Animations +- [ ] Cameras +- [ ] Lights diff --git a/orx-dnk3/build.gradle b/orx-dnk3/build.gradle new file mode 100644 index 00000000..e87d229e --- /dev/null +++ b/orx-dnk3/build.gradle @@ -0,0 +1,20 @@ +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")) + demoImplementation(project(":orx-camera")) + demoImplementation("org.openrndr:openrndr-core:$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) +} diff --git a/orx-dnk3/src/demo/kotlin/DemoObject01.kt b/orx-dnk3/src/demo/kotlin/DemoObject01.kt new file mode 100644 index 00000000..a757e4fe --- /dev/null +++ b/orx-dnk3/src/demo/kotlin/DemoObject01.kt @@ -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 + +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) + } + } + } + } + } +} \ No newline at end of file diff --git a/orx-dnk3/src/demo/kotlin/DemoScene01.kt b/orx-dnk3/src/demo/kotlin/DemoScene01.kt new file mode 100644 index 00000000..83599853 --- /dev/null +++ b/orx-dnk3/src/demo/kotlin/DemoScene01.kt @@ -0,0 +1,55 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.dnk3.* +import org.openrndr.extra.dnk3.gltf.buildSceneNodes +import org.openrndr.extra.dnk3.gltf.loadGltfFromFile +import org.openrndr.extras.camera.Orbital +import org.openrndr.math.Vector3 +import org.openrndr.math.transforms.transform +import java.io.File + +fun main() = application { + configure { + width = 1280 + height = 720 + //multisample = WindowMultisample.SampleCount(8) + } + + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val gltf = loadGltfFromFile(File("demo-data/gltf-models/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().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) + } + } +} \ No newline at end of file diff --git a/orx-dnk3/src/demo/kotlin/DemoScene02.kt b/orx-dnk3/src/demo/kotlin/DemoScene02.kt new file mode 100644 index 00000000..d322da48 --- /dev/null +++ b/orx-dnk3/src/demo/kotlin/DemoScene02.kt @@ -0,0 +1,57 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.dnk3.* + +import org.openrndr.extra.dnk3.gltf.buildSceneNodes +import org.openrndr.extra.dnk3.gltf.loadGltfFromFile +import org.openrndr.extras.camera.Orbital +import org.openrndr.math.Vector3 +import org.openrndr.math.transforms.transform +import java.io.File + +fun main() = application { + configure { + width = 1280 + height = 720 + //multisample = WindowMultisample.SampleCount(8) + } + + program { + if (System.getProperty("takeScreenshot") == "true") { + extend(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") + } + } + + val gltf = loadGltfFromFile(File("demo-data/gltf-models/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, -65.0) + } + lightNode.entities.add(DirectionalLight()) + scene.root.entities.add(HemisphereLight().apply { + upColor = ColorRGBa.WHITE.shade(0.4) + downColor = ColorRGBa.WHITE.shade(0.1) + }) + scene.root.children.add(lightNode) + scene.root.children.addAll(gltf.buildSceneNodes().first()) + + // -- create a renderer + val renderer = dryRenderer() + extend(Orbital()) { + far = 50.0 + lookAt = Vector3(0.0, 0.7, 0.0) + eye = Vector3(3.0, 0.7, -2.0) + fov = 40.0 + } + extend { + drawer.clear(ColorRGBa.PINK) + renderer.draw(drawer, scene) + } + } +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/DryRenderer.kt b/orx-dnk3/src/main/kotlin/DryRenderer.kt new file mode 100644 index 00000000..5d8183f3 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/DryRenderer.kt @@ -0,0 +1,6 @@ +package org.openrndr.extra.dnk3 + +fun dryRenderer() : SceneRenderer { + val sr = SceneRenderer() + return sr +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/Entity.kt b/orx-dnk3/src/main/kotlin/Entity.kt new file mode 100644 index 00000000..f8a4d1cf --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Entity.kt @@ -0,0 +1,37 @@ +package org.openrndr.extra.dnk3 + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* + + +class Geometry(val vertexBuffers: List, + val indexBuffer: IndexBuffer?, + val primitive: DrawPrimitive, + val offset: Int, + val vertexCount: Int) + +val DummyGeometry = Geometry(emptyList(), null, DrawPrimitive.TRIANGLES, 0, 0) + +sealed class Entity + +class MeshPrimitive(var geometry: Geometry, var material: Material) + +class MeshPrimitiveInstance(val primitive: MeshPrimitive, val instances: Int, val attributes: List) + +abstract class MeshBase(var primitives: List) : Entity() +class Mesh(primitives: List) : MeshBase(primitives) + +class InstancedMesh(primitives: List, + var instances: Int, + var attributes: List) : MeshBase(primitives) + + +class Fog : Entity() { + var color: ColorRGBa = ColorRGBa.WHITE + var end: Double = 100.0 +} + +abstract class Light : Entity() { + var color: ColorRGBa = ColorRGBa.WHITE +} + diff --git a/orx-dnk3/src/main/kotlin/Facet.kt b/orx-dnk3/src/main/kotlin/Facet.kt new file mode 100644 index 00000000..b0d8d00a --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Facet.kt @@ -0,0 +1,120 @@ +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"), + COLOR("m_color"), +} + +abstract class FacetCombiner(val facets: Set, val targetOutput: String) { + abstract fun generateShader(): String +} + +abstract class ColorBufferFacetCombiner(facets: Set, + targetOutput: String, + val format: ColorFormat, + val type: ColorType, + val blendMode: BlendMode = BlendMode.REPLACE) : 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 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 LDRColorFacet : ColorBufferFacetCombiner(setOf(FacetType.DIFFUSE, FacetType.SPECULAR), "color", ColorFormat.RGBa, ColorType.UINT8) { + override fun generateShader() = """ + vec3 oofinalColor = (f_diffuse.rgb + f_specular.rgb + f_emission.rgb) * (1.0 - f_fog.a) + f_fog.rgb * f_fog.a; + o_$targetOutput.rgba = pow(vec4(oofinalColor, 1.0), vec4(1.0/2.2)); + o_$targetOutput.a = f_alpha; + """ +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/Light.kt b/orx-dnk3/src/main/kotlin/Light.kt new file mode 100644 index 00000000..2ba20b31 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Light.kt @@ -0,0 +1,48 @@ +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>, + val shadowMaps: Map) + +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 = 10.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) + } +} + +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) + } +} + +class HemisphereLight(var direction: Vector3 = Vector3.UNIT_Y, + var upColor: ColorRGBa = ColorRGBa.WHITE, + var downColor: ColorRGBa = ColorRGBa.BLACK) : Light() { + var irradianceMap: Cubemap? = null +} + +class PointLight(var constantAttenuation: Double = 1.0, + var linearAttenuation: Double = 0.0, + var quadraticAttenuation: Double = 0.0) : Light() + +class AmbientLight : Light() diff --git a/orx-dnk3/src/main/kotlin/Material.kt b/orx-dnk3/src/main/kotlin/Material.kt new file mode 100644 index 00000000..9e488538 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Material.kt @@ -0,0 +1,40 @@ +package org.openrndr.extra.dnk3 + +import org.openrndr.draw.Cubemap +import org.openrndr.draw.RenderTarget +import org.openrndr.draw.ShadeStyle +import org.openrndr.draw.shadeStyle + +interface Material { + var doubleSided: Boolean + var transparent: Boolean + fun generateShadeStyle(context: MaterialContext): ShadeStyle + fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) +} + +class DummyMaterial : Material { + override var doubleSided: Boolean = true + override var transparent: Boolean = false + + + override fun generateShadeStyle(context: MaterialContext): ShadeStyle { + return shadeStyle { + fragmentTransform = """ + x_fill.rgb = vec3(normalize(v_viewNormal).z); + """.trimIndent() + } + } + + override fun applyToShadeStyle(context: MaterialContext, shadeStyle: ShadeStyle) { + + } + +} + +data class MaterialContext(val pass: RenderPass, + val lights: List>, + val fogs: List>, + val shadowMaps: Map, + val meshCubemaps: Map +) + diff --git a/orx-dnk3/src/main/kotlin/PBRMaterial.kt b/orx-dnk3/src/main/kotlin/PBRMaterial.kt new file mode 100644 index 00000000..9dcf258a --- /dev/null +++ b/orx-dnk3/src/main/kotlin/PBRMaterial.kt @@ -0,0 +1,555 @@ +package org.openrndr.extra.dnk3 + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +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).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): 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 = dot(L, N) ; +| 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) = """ +|{ +| vec3 L = normalize(-p_lightDirection$index); +| float attenuation = 1.0; +| vec3 H = normalize(V + L); +| float NoL = clamp(dot(N, L), 0.0, 1.0); +| float LoH = clamp(dot(L, H), 0.0, 1.0); +| float NoH = clamp(dot(N, H), 0.0, 1.0); +| 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 ; +| float Dg = D_GGX(m_roughness * m_roughness, NoH, H); +| float Vs = V_SmithGGXCorrelated(m_roughness * m_roughness, NoV, NoL); +| vec3 F = F_Schlick(m_color * (m_metalness) + 0.04 * (1.0-m_metalness), LoH); +| vec3 Fr = (Dg * Vs) * F; +| f_specular += NoL * attenuation * Fr * p_lightColor$index.rgb; +|} +""".trimMargin() + +private fun HemisphereLight.fs(index: Int): String = """ +|{ +| float f = dot(N, p_lightDirection$index) * 0.5 + 0.5; +| vec3 irr = ${irradianceMap?.let { "texture(p_lightIrradianceMap$index, N).rgb" } ?: "vec3(1.0)"}; +| f_diffuse += mix(p_lightDownColor$index.rgb, p_lightUpColor$index.rgb, f) * irr * ((1.0 - m_metalness) * m_color.rgb);// * m_ambientOcclusion; +|} +""".trimMargin() + +private fun SpotLight.fs(index: Int): 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 = clamp(dot(N, L), 0.0, 1.0); +| 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 = clamp(dot(N, H), 0.0, 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 * (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() +abstract class TextureFromColorBuffer(var texture: ColorBuffer, var textureFunction: TextureFunction) : TextureSource() + +class TextureFromCode(val code: String) : TextureSource() + +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) + +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 + } +} + +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.)) ; +| +""".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 { + object NONE : TextureTarget() + object COLOR : TextureTarget() + object ROUGHNESS : TextureTarget() + object METALNESS : TextureTarget() + object METALNESS_ROUGHNESS : TextureTarget() + object EMISSION : TextureTarget() + object NORMAL : TextureTarget() + object AMBIENT_OCCLUSION : TextureTarget() + class Height(var scale: Double = 1.0) : TextureTarget() +} + +class Texture(var source: TextureSource, + var target: TextureTarget) { + fun copy(): Texture { + val copied = Texture(source, target) + return copied + } +} + +class PBRMaterial : Material { + 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 opacity = 1.0 + var emission = ColorRGBa.BLACK + + var vertexPreamble: String? = null + var vertexTransform: String? = null + var parameters = mutableMapOf() + var textures = mutableListOf() + + val shadeStyles = mutableMapOf() + +// fun copy(): PBRMaterial { +// val copied = PBRMaterial() +// copied.environmentMap = environmentMap +// copied.color = color +// copied.opacity = opacity +// copied.metalness = metalness +// copied.roughness = roughness +// copied.emission = emission +// copied.vertexPreamble = vertexPreamble +// copied.vertexTransform = vertexTransform +// copied.parameters.putAll(parameters) +// copied.textures.addAll(textures.map { it.copy() }) +// return copied +// } + + override fun generateShadeStyle(context: MaterialContext): ShadeStyle { + val cached = shadeStyles.getOrPut(context) { + val needLight = needLight(context) + val preambleFS = """ + vec3 m_color = p_color.rgb; + float m_f0 = 0.5; + float m_roughness = p_roughness; + float m_metalness = p_metalness; + float m_opacity = p_opacity; + float m_ambientOcclusion = 1.0; + vec3 m_emission = p_emission.rgb; + vec3 m_normal = vec3(0.0, 0.0, 1.0); + float f_alpha = m_opacity; + vec4 f_fog = vec4(0.0, 0.0, 0.0, 0.0); + vec3 f_worldNormal = v_worldNormal; + """.trimIndent() + + 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));" + 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 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 = context.lights + val lightFS = if (needLight) """ + vec3 f_diffuse = vec3(0.0); + vec3 f_specular = vec3(0.0); + vec3 f_emission = m_emission; + 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 = abs(dot(N, V)) + 1e-5; + + ${if (environmentMap && context.meshCubemaps.isNotEmpty()) """ + { + 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) + is SpotLight -> light.fs(index) + is DirectionalLight -> light.fs(index) + is HemisphereLight -> light.fs(index) + else -> TODO() + } + }.joinToString("\n")} + + ${context.fogs.mapIndexed { index, (node, fog) -> + fog.fs(index) + }.joinToString("\n")} + + """.trimIndent() else "" + val rt = RenderTarget.active + + val combinerFS = context.pass.combiners.map { + it.generateShader() + }.joinToString("\n") + + val fs = preambleFS + textureFs + lightFS + combinerFS + val vs = (this@PBRMaterial.vertexTransform ?: "") + textureVS + + shadeStyle { + vertexPreamble = """ + $shaderNoRepetitionVert + ${(this@PBRMaterial.vertexPreamble)?:""} + """.trimIndent() + fragmentPreamble = """ + |$shaderLinePlaneIntersect + |$shaderProjectOnPlane + |$shaderSideOfPlane + |$shaderGGX + |$shaderVSM + |$shaderNoRepetition + """.trimMargin() + this.suppressDefaultOutput = true + this.vertexTransform = vs + fragmentTransform = fs + context.pass.combiners.map { + if (rt.colorBuffers.size <= 1) { + this.output(it.targetOutput, 0) + } else + this.output(it.targetOutput, rt.colorBufferIndex(it.targetOutput)) + } + } + } + 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("opacity", opacity) + + 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)) { + 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).normalized) + 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).normalized) + 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).normalized) + 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) + } + } + } + } +} + diff --git a/orx-dnk3/src/main/kotlin/Post.kt b/orx-dnk3/src/main/kotlin/Post.kt new file mode 100644 index 00000000..23127493 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Post.kt @@ -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, postContext: PostContext) +} + +class FilterPostStep(val outputScale: Double, + val filter: Filter, + val inputs: List, + val output: String, + val outputFormat: ColorFormat, + val outputType: ColorType, + val update: (Filter.(PostContext) -> Unit)? = null) : PostStep { + + override fun apply(buffers: MutableMap, postContext: PostContext) { + val inputBuffers = inputs.map { buffers[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)->Unit) : PostStep { + override fun apply(buffers: MutableMap, postContext: PostContext) { + function(buffers) + } +} + +class FilterPostStepBuilder(val filter: T) { + var outputScale = 1.0 + val inputs = mutableListOf() + 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 postStep(filter: T, configure: FilterPostStepBuilder.() -> Unit) : PostStep { + val psb = FilterPostStepBuilder(filter) + psb.configure() + return psb.build() +} + +fun postStep(function: (MutableMap)->Unit) : PostStep { + return FunctionPostStep(function) +} diff --git a/orx-dnk3/src/main/kotlin/RenderPass.kt b/orx-dnk3/src/main/kotlin/RenderPass.kt new file mode 100644 index 00000000..36f36950 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/RenderPass.kt @@ -0,0 +1,24 @@ +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 + +class RenderPass(val combiners: List, val renderOpaque: Boolean = true, val renderTransparent: Boolean = false) + +val DefaultPass = RenderPass(listOf(LDRColorFacet())) +val LightPass = RenderPass(emptyList()) +val VSMLightPass = RenderPass(listOf(MomentsFacet())) + +fun RenderPass.createPassTarget(width: Int, height: Int, depthFormat: DepthFormat = DepthFormat.DEPTH24, multisample: BufferMultisample = BufferMultisample.Disabled): RenderTarget { + return renderTarget(width, height, multisample = multisample) { + for (combiner in combiners) { + when (combiner) { + is ColorBufferFacetCombiner -> + colorBuffer(combiner.targetOutput, combiner.format, combiner.type) + } + } + depthBuffer(depthFormat) + } +} diff --git a/orx-dnk3/src/main/kotlin/Scene.kt b/orx-dnk3/src/main/kotlin/Scene.kt new file mode 100644 index 00000000..8a91c2c9 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Scene.kt @@ -0,0 +1,67 @@ +package org.openrndr.extra.dnk3 + +import org.openrndr.math.Matrix44 + +class Scene(val root: SceneNode = SceneNode(), + val updateFunctions: MutableList<() -> Unit> = mutableListOf()) + + +open class SceneNode(var entities: MutableList = mutableListOf()) { + var parent: SceneNode? = null + var transform = Matrix44.IDENTITY + var worldTransform = Matrix44.IDENTITY + val children = mutableListOf() + var disposed = false +} + +class NodeContent(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

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 { + val result = mutableListOf() + visit { + if (selector()) result.add(this) + } + return result +} + +fun

SceneNode.findContent(selector: Entity.() -> P?): List> { + val result = mutableListOf>() + + visit { + entities.forEach { + val s = it.selector() + if (s != null) { + result.add(NodeContent(this, s)) + } + } + } + return result +} + diff --git a/orx-dnk3/src/main/kotlin/SceneRenderer.kt b/orx-dnk3/src/main/kotlin/SceneRenderer.kt new file mode 100644 index 00000000..6c39f45c --- /dev/null +++ b/orx-dnk3/src/main/kotlin/SceneRenderer.kt @@ -0,0 +1,240 @@ +package org.openrndr.extra.dnk3 + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.draw.depthBuffer +import org.openrndr.extra.fx.blur.ApproximateGaussianBlur +import org.openrndr.math.Matrix44 + +class SceneRenderer { + + class Configuration { + var multisampleLines = false + } + + val configuration = Configuration() + + val blur = ApproximateGaussianBlur() + + var shadowLightTargets = mutableMapOf() + var meshCubemaps = mutableMapOf() + + var cubemapDepthBuffer = depthBuffer(256, 256, DepthFormat.DEPTH16, BufferMultisample.Disabled) + + var outputPasses = mutableListOf(DefaultPass) + var outputPassTarget: RenderTarget? = null + var outputPassTargetMS: RenderTarget? = null + + val postSteps = mutableListOf() + val buffers = mutableMapOf() + + var drawFinalBuffer = true + + fun draw(drawer: Drawer, scene: Scene) { + drawer.pushStyle() + drawer.depthWrite = true + drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL + + drawer.cullTestPass = CullTestPass.FRONT + scene.updateFunctions.forEach { + it() + } + + // update all the transforms + scene.root.scan(Matrix44.IDENTITY) { p -> + worldTransform = p * transform + worldTransform + } + + val lights = scene.root.findContent { this as? Light } + val meshes = scene.root.findContent { this as? Mesh } + val fogs = scene.root.findContent { this as? Fog } + val instancedMeshes = scene.root.findContent { this as? InstancedMesh } + + + run { + 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, lights, fogs, shadowLightTargets, emptyMap()) + 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, meshes, instancedMeshes) + } + 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)) + } + } + } + } + + run { + //val pass = outputPasses + for (pass in outputPasses) { + val materialContext = MaterialContext(pass, lights, fogs, shadowLightTargets, meshCubemaps) + + if ((pass != DefaultPass || postSteps.isNotEmpty()) && outputPassTarget == null) { + outputPassTarget = pass.createPassTarget(RenderTarget.active.width, RenderTarget.active.height) + } + + if (pass == outputPasses[0]) { + outputPassTarget?.let { + drawer.withTarget(it) { + background(ColorRGBa.PINK) + } + } + } + outputPassTarget?.let { target -> + pass.combiners.forEach { + if (it is ColorBufferFacetCombiner) { + val index = target.colorBufferIndex(it.targetOutput) + target.blendMode(index, it.blendMode) + } + } + } + outputPassTarget?.bind() + drawPass(drawer, pass, materialContext, meshes, instancedMeshes) + outputPassTarget?.unbind() + + outputPassTarget?.let { output -> + for (combiner in pass.combiners) { + buffers[combiner.targetOutput] = output.colorBuffer(combiner.targetOutput) + } + } + } + val lightContext = LightContext(lights, shadowLightTargets) + val postContext = PostContext(lightContext, drawer.view.inversed) + + for (postStep in postSteps) { +// if (postStep is FilterPostStep) { +// if (postStep.filter is Ssao) { +// postStep.filter.projection = drawer.projection +// } +// if (postStep.filter is Sslr) { +// 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 +// postStep.filter.projection = p +// } +// } + postStep.apply(buffers, postContext) + } + + } + + drawer.popStyle() + if (drawFinalBuffer) { + outputPassTarget?.let { output -> + drawer.isolated { + drawer.ortho() + drawer.view = Matrix44.IDENTITY + drawer.model = Matrix44.IDENTITY + val outputName = (postSteps.last() as FilterPostStep).output + val outputBuffer = buffers[outputName] + ?: throw IllegalArgumentException("can't find $outputName buffer") + drawer.image(outputBuffer) + } + } + } + } + + private fun drawPass(drawer: Drawer, pass: RenderPass, materialContext: MaterialContext, + meshes: List>, + instancedMeshes: List>) { + val primitives = 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 shadeStyle = primitive.material.generateShadeStyle(materialContext) + 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 = 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 shadeStyle = primitive.primitive.material.generateShadeStyle(materialContext) + 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) + } + } + } +} + +fun sceneRenderer(builder: SceneRenderer.() -> Unit): SceneRenderer { + val sceneRenderer = SceneRenderer() + sceneRenderer.builder() + return sceneRenderer +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/ShaderUtilities.kt b/orx-dnk3/src/main/kotlin/ShaderUtilities.kt new file mode 100644 index 00000000..d9d77df7 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/ShaderUtilities.kt @@ -0,0 +1,228 @@ +package org.openrndr.extra.dnk3 + +val 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 = """ +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 = """ +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 = """ +|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, 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 = """ +#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); +} + +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() \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/Shadows.kt b/orx-dnk3/src/main/kotlin/Shadows.kt new file mode 100644 index 00000000..9c2178e8 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/Shadows.kt @@ -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() +} diff --git a/orx-dnk3/src/main/kotlin/gltf/Glb.kt b/orx-dnk3/src/main/kotlin/gltf/Glb.kt new file mode 100644 index 00000000..bcc1cde3 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/gltf/Glb.kt @@ -0,0 +1,46 @@ +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) + 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 + } +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/gltf/Gltf.kt b/orx-dnk3/src/main/kotlin/gltf/Gltf.kt new file mode 100644 index 00000000..6e3e7cdb --- /dev/null +++ b/orx-dnk3/src/main/kotlin/gltf/Gltf.kt @@ -0,0 +1,220 @@ +@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 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 + +class GltfAsset(val generator: String?, val version: String?) + +class GltfScene(val nodes: IntArray) + +class GltfNode(val children: IntArray?, + val matrix: DoubleArray?, + val scale: DoubleArray?, + val rotation: DoubleArray?, + val translation: DoubleArray?, + val mesh: Int?) + +class GltfPrimitive(val attributes: LinkedHashMap, 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).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() + 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) + } + } + } + } + + 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) + } +} + +class GltfMesh(val primitives: List, val name: String) { + fun createDrawCommands(gltfFile: GltfFile): List { + return primitives.map { it.createDrawCommand(gltfFile) } + } +} + +class GltfPbrMetallicRoughness(val baseColorFactor: DoubleArray?, + val baseColorTexture: GltfMaterialTexture?, + var metallicRoughnessTexture: GltfMaterialTexture?, + val roughnessFactor: Double?, + val metallicFactor: Double?) +class GltfMaterialTexture(val index: Int, val scale: Double?, val texCoord: Int?) + +class GltfImage(val uri: String) + +class GltfSampler(val magFilter: Int, val minFilter: Int, val wrapS: Int, val wrapT: Int) + +class GltfTexture(val sampler: Int, val source: Int) + +class GltfMaterial(val name: String, + val doubleSided: Boolean?, + val normalTexture: GltfMaterialTexture?, + val occlusionTexture: GltfMaterialTexture?, + val emissiveTexture: GltfMaterialTexture?, + val emissiveFactor: DoubleArray?, + val pbrMetallicRoughness: GltfPbrMetallicRoughness?) + +class GltfBufferView(val buffer: Int, + val byteOffset: Int?, + val byteLength: Int, + val byteStride: Int?, + val target: Int) + +class GltfBuffer(val byteLength: Int, val uri: String?) { + fun contents(gltfFile: GltfFile): ByteBuffer = if (uri != null) { + 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") + } +} + +class GltfDrawCommand(val vertexBuffer: VertexBuffer, val indexBuffer: IndexBuffer?, val primitive: DrawPrimitive, var vertexCount: Int) + +class GltfAccessor( + val bufferView: Int, + val byteOffset: Int, + val componentType: Int, + val count: Int, + val max: DoubleArray, + val min: DoubleArray, + val type: String +) + +class GltfFile( + val asset: GltfAsset?, + val scene: Int?, + val scenes: List, + val nodes: List, + val meshes: List, + val accessors: List, + val materials: List, + val bufferViews: List, + val buffers: List, + val images: List?, + val textures: List?, + val samplers: List? + +) { + @Transient lateinit var file: File + @Transient var bufferBuffer : ByteBuffer? = null +} + +fun loadGltfFromFile(file: File): GltfFile { + val gson = Gson() + val json = file.readText() + return gson.fromJson(json, GltfFile::class.java).apply { + this.file = file + } +} \ No newline at end of file diff --git a/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt b/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt new file mode 100644 index 00000000..3122c724 --- /dev/null +++ b/orx-dnk3/src/main/kotlin/gltf/GltfScene.kt @@ -0,0 +1,126 @@ +package org.openrndr.extra.dnk3.gltf + +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.extra.dnk3.* +import org.openrndr.math.Matrix44 +import org.openrndr.math.Quaternion +import org.openrndr.math.transforms.transform +import java.io.File + +/** Tools to convert GltfFile into a DNK3 scene */ + +fun GltfFile.buildSceneNodes(): List> { + + val sceneImages = mutableMapOf() + fun GltfImage.createSceneImage(): ColorBuffer { + return sceneImages.getOrPut(this) { loadImage(File(file.parent, uri)) } + } + + val sceneMaterials = mutableMapOf() + fun GltfMaterial.createSceneMaterial(): Material = sceneMaterials.getOrPut(this) { + val material = PBRMaterial() + + material.doubleSided = this.doubleSided ?: false + + pbrMetallicRoughness?.let { pbr -> + material.roughness = pbr.roughnessFactor ?: 1.0 + material.metalness = pbr.metallicFactor ?: 1.0 + 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], 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[material].createSceneMaterial() + return MeshPrimitive(geometry, material) + } + + val sceneMeshes = mutableMapOf() + fun GltfMesh.createSceneMesh(): Mesh = sceneMeshes.getOrPut(this) { + Mesh(primitives.map { + it.createScenePrimitive() + }) + } + + fun GltfNode.createSceneNode(): SceneNode { + val node = SceneNode() + mesh?.let { + node.entities.add(meshes[it].createSceneMesh()) + } + + val localTransform = transform { + + translation?.let { + translate(it[0], it[1], it[2]) + } + rotation?.let { + val q = Quaternion(it[0], it[1], it[2], it[3]) + multiply(q.matrix.matrix44) + } + + scale?.let { + scale(it[0], it[1], it[2]) + } + } + + node.transform = this.matrix?.let { + Matrix44.fromDoubleArray(it).transposed + } ?: localTransform + for (child in children.orEmpty) { + node.children.add(nodes[child].createSceneNode()) + } + return node + } + + return scenes.map { scene -> + scene.nodes.map { node -> + nodes[node].createSceneNode() + } + } +} + +private val IntArray?.orEmpty: IntArray get() = this ?: IntArray(0) \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 141bb5ac..331a1c78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ rootProject.name = 'orx' include 'orx-boofcv', 'orx-camera', 'orx-compositor', + 'orx-dnk3', 'orx-easing', 'orx-file-watcher', 'orx-parameters',