From d0579d4dd3302399d9b68f646c0546522ac38b76 Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sat, 1 Feb 2020 10:55:06 +0100 Subject: [PATCH] Improve orx-shader-phrases --- .../src/main/kotlin/phrases/NoisePhrases.kt | 4 -- orx-shader-phrases/src/README.md | 47 ++++++++++++++++ .../src/main/kotlin/ShaderPreprocessor.kt | 55 ++++++++++++++----- .../main/kotlin/annotations/ShaderPhrase.kt | 11 +--- .../src/main/kotlin/phrases/Depth.kt | 33 +++++++++++ .../src/main/kotlin/phrases/Dummy.kt | 5 +- .../src/test/kotlin/TestPreprocessShader.kt | 52 +++++++++++++++--- .../src/test/resources/from-url-test.frag | 2 + 8 files changed, 168 insertions(+), 41 deletions(-) create mode 100644 orx-shader-phrases/src/README.md create mode 100644 orx-shader-phrases/src/main/kotlin/phrases/Depth.kt create mode 100644 orx-shader-phrases/src/test/resources/from-url-test.frag diff --git a/orx-noise/src/main/kotlin/phrases/NoisePhrases.kt b/orx-noise/src/main/kotlin/phrases/NoisePhrases.kt index d92129c6..006e5444 100644 --- a/orx-noise/src/main/kotlin/phrases/NoisePhrases.kt +++ b/orx-noise/src/main/kotlin/phrases/NoisePhrases.kt @@ -1,20 +1,16 @@ @file:ShaderPhrases(exports = ["hash22","hash21","valueNoise21"]) package org.openrndr.extra.noise.phrases -import org.openrndr.extra.shaderphrases.annotations.ShaderPhrase import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases -@ShaderPhrase(exports = ["hash22"]) val phraseHash22 = """vec2 hash22(vec2 p) { float n = sin(dot(p, vec2(41, 289))); return fract(vec2(262144, 32768)*n); } """ -@ShaderPhrase(exports = ["hash21"]) val phraseHash21 = "float hash21(vec2 p) { return fract(1e4 * sin(17.0 * p.x + p.y * 0.1) * (0.1 + abs(sin(p.y * 13.0 + p.x)))); }" -@ShaderPhrase(exports = ["valueNoise21"], imports = ["hash21"]) val phraseValueNoise21 = """ float noise(vec2 x) { diff --git a/orx-shader-phrases/src/README.md b/orx-shader-phrases/src/README.md new file mode 100644 index 00000000..34fdc356 --- /dev/null +++ b/orx-shader-phrases/src/README.md @@ -0,0 +1,47 @@ +# orx-shader-phrases + +A library that provides a `#pragma import` statement for shaders by using the JVM class loader. + +## Usage + +Given a shader source: + +````glsl +#version 330 +// -- this imports all phrases in Dummy +#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.* + +void main() { + float a = dummy(); +} +```` + +We can use the `preprocessShader()` function to resolve `#pragma import` statements. + +```kotlin + val preprocessedSource = preprocessShader(originalSource) +``` + +Alternatively loading and preprocessing can be combined in a single function call. + +```kotlin + val preprocessedSource = preprocessShaderFromUrl(resourceUrl("/some-shader.frag")) +``` + +To create importable shader phrases one creates a Kotlin class and adds the `ShaderPhrases` annotation. +For example the `dummy` phrase in our example is made available as follows: + +```kotlin +// -- force the class name to be Dummy on the JVM +@file:JvmName("Dummy") +@file:ShaderPhrases +package org.openrndr.extra.shaderphrases.phrases +import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases + +// -- the shader phrase +const val dummy = """ +float dummy() { + return 0.0; +} +""" +``` \ No newline at end of file diff --git a/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt b/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt index 447067ff..21ac77b8 100644 --- a/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt +++ b/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt @@ -1,33 +1,58 @@ package org.openrndr.extra.shaderphrases +import org.openrndr.draw.codeFromURL import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases -fun preprocessShader(shader: String): String { - val lines = shader.split("\n") - val processed = lines.map { - if (it.startsWith("import ")) { +/** + * Preprocess shader source. + * Looks for "#pragma import" statements and injects found phrases. + * @param source GLSL source code encoded as string + * @return GLSL source code with injected shader phrases + */ +fun preprocessShader(source: String): String { + val lines = source.split("\n") + val processed = lines.mapIndexed { index, it -> + if (it.startsWith("#pragma import")) { val tokens = it.split(" ") - val full = tokens[1] + val full = tokens[2] val fullTokens = full.split(".") - val fieldName = fullTokens.last().replace(";","").trim() + val fieldName = fullTokens.last().replace(";", "").trim() val packageClassTokens = fullTokens.dropLast(1) val packageClass = packageClassTokens.joinToString(".") - val c = Class.forName(packageClass) - if (c.annotations.any { it.annotationClass == ShaderPhrases::class }) { - if (fieldName == "*") { - c.declaredFields.filter { it.type.name =="java.lang.String" }.map { - it.get(null) - }.joinToString("\n") + try { + val c = Class.forName(packageClass) + if (c.annotations.any { it.annotationClass == ShaderPhrases::class }) { + if (fieldName == "*") { + c.declaredFields.filter { it.type.name == "java.lang.String" }.map { + "/* imported from $packageClass.$it */\n ${it.get(null)}" + }.joinToString("\n") + } else { + try { + c.getDeclaredField(fieldName).get(null) + } catch (e: NoSuchFieldException) { + error("field \"$fieldName\" not found in \"#pragma import $packageClass.$fieldName\" on line ${index + 1}") + } + } } else { - c.getDeclaredField(fieldName).get(null) + throw IllegalArgumentException("class $packageClass has no ShaderPhrases annotation") } - } else { - throw IllegalArgumentException("class $packageClass has no ShaderPhrases annotation") + } catch (e: ClassNotFoundException) { + error("class \"$packageClass\" not found in \"#pragma import $packageClass\" on line ${index + 1}") } } else { it } } return processed.joinToString("\n") +} + +/** + * Preprocess shader source from url + * Looks for "#pragma import" statements and injects found phrases. + * @param url url pointing to GLSL shader source + * @return GLSL source code with injected shader phrases + */ +fun preprocessShaderFromUrl(url: String): String { + return preprocessShader(codeFromURL(url)) } \ No newline at end of file diff --git a/orx-shader-phrases/src/main/kotlin/annotations/ShaderPhrase.kt b/orx-shader-phrases/src/main/kotlin/annotations/ShaderPhrase.kt index 369e2a69..72c9f035 100644 --- a/orx-shader-phrases/src/main/kotlin/annotations/ShaderPhrase.kt +++ b/orx-shader-phrases/src/main/kotlin/annotations/ShaderPhrase.kt @@ -1,15 +1,8 @@ package org.openrndr.extra.shaderphrases.annotations enum class ShaderPhraseLanguage { - GLSL + GLSL_330 } -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.FILE, AnnotationTarget.CLASS, AnnotationTarget.FIELD) -annotation class ShaderPhrase(val exports: Array, - val imports: Array = emptyArray(), - val language: ShaderPhraseLanguage = ShaderPhraseLanguage.GLSL) - - @Target(AnnotationTarget.FILE) -annotation class ShaderPhrases(val exports: Array) \ No newline at end of file +annotation class ShaderPhrases(val exports: Array = emptyArray()) \ No newline at end of file diff --git a/orx-shader-phrases/src/main/kotlin/phrases/Depth.kt b/orx-shader-phrases/src/main/kotlin/phrases/Depth.kt new file mode 100644 index 00000000..f5628375 --- /dev/null +++ b/orx-shader-phrases/src/main/kotlin/phrases/Depth.kt @@ -0,0 +1,33 @@ +@file:JvmName("Depth") +@file:ShaderPhrases([]) +package org.openrndr.extra.shaderphrases.phrases + +import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases + +/** + * phrase for conversion from view to projection depth + * @param viewDepth depth in view space ([0.0 .. -far]) + * @param projection projection matrix + * @return depth in projection space ([0.0 .. 1.0]] + */ +const val viewToProjectionDepth = """ +float viewToProjectionDepth(float viewDepth, mat4 projection) { + float z = viewDepth * projection[2].z + projection[3].z; + float w = viewDepth * projection[2].w + projection[3].w; + return z / w; +} +""" + +/** + * phrase for conversion from view to projection depth + * @param projectionDepth depth in projection space ([0.0 .. 1.0]) + * @param projectionInversed inverse of the projection matrix + * @return depth in view space ([0.0 .. -far]] + */ +const val projectionToViewDepth = """ +float projectionToViewDepth(float projectionDepth, mat4 projectionInverse) { + float z = projectionDepth * projectionInverse[2].z + projectionInverse[3].z; + float w = projectionDepth * projectionInverse[2].w + projectionInverse[3].w; + return z / w; +} +""" diff --git a/orx-shader-phrases/src/main/kotlin/phrases/Dummy.kt b/orx-shader-phrases/src/main/kotlin/phrases/Dummy.kt index 41f694ac..d703ec23 100644 --- a/orx-shader-phrases/src/main/kotlin/phrases/Dummy.kt +++ b/orx-shader-phrases/src/main/kotlin/phrases/Dummy.kt @@ -1,19 +1,16 @@ @file:JvmName("Dummy") -@file:ShaderPhrases(["dummy"]) +@file:ShaderPhrases package org.openrndr.extra.shaderphrases.phrases -import org.openrndr.extra.shaderphrases.annotations.ShaderPhrase import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases import org.openrndr.extra.shaderphrases.preprocessShader -@ShaderPhrase(["dummy"]) const val phraseDummy = """ float dummy() { return 0.0; } """ - fun main() { val c = Class.forName("org.openrndr.extra.shaderphrases.phrases.Dummy") diff --git a/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt b/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt index c6cc5f0f..24a0c2d8 100644 --- a/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt +++ b/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt @@ -1,19 +1,53 @@ +import org.amshove.kluent.`should contain` +import org.amshove.kluent.`should throw` +import org.amshove.kluent.`with message` +import org.amshove.kluent.invoking import org.openrndr.extra.shaderphrases.preprocessShader +import org.openrndr.extra.shaderphrases.preprocessShaderFromUrl +import org.openrndr.resourceUrl import org.spekframework.spek2.Spek import org.spekframework.spek2.style.specification.describe -object TestPreprocessShader:Spek({ +object TestPreprocessShader : Spek({ + describe("An url pointing to a shader resource") { + val url = resourceUrl("/from-url-test.frag") + describe("results in injected dummy phrase when preprocessed") { + val processed = preprocessShaderFromUrl(url) + processed `should contain` "float dummy" + } + } describe("A shader with import statements") { - val shader = """ -#version 330 -import org.openrndr.extra.shaderphrases.phrases.Dummy.* - - -""".trimIndent() - describe("when preprocessed") { + val shader = """#version 330 +#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.* +""" + describe("injects dummy phrase when preprocessed") { val processed = preprocessShader(shader) - println(processed) + processed `should contain` "float dummy" + } + } + + describe("A shader with non-resolvable class statements") { + val shader = """#version 330 +#pragma import invalid.Class.* +""" + describe("throws exception when preprocessed") { + invoking { + preprocessShader(shader) + } `should throw` RuntimeException::class `with message` + ("class \"invalid.Class\" not found in \"#pragma import invalid.Class\" on line 2") + } + } + + describe("A shader with non-resolvable property statements") { + val shader = """#version 330 +#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.invalid +""" + describe("throws exception when preprocessed") { + invoking { + preprocessShader(shader) + } `should throw` RuntimeException::class `with message` + ("field \"invalid\" not found in \"#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.invalid\" on line 2") } } }) \ No newline at end of file diff --git a/orx-shader-phrases/src/test/resources/from-url-test.frag b/orx-shader-phrases/src/test/resources/from-url-test.frag new file mode 100644 index 00000000..71e96ed5 --- /dev/null +++ b/orx-shader-phrases/src/test/resources/from-url-test.frag @@ -0,0 +1,2 @@ +#version 330 +#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.* \ No newline at end of file