From 94722ba1f3f0635f2ed6aea6c4838f51cf8ccb9b Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Fri, 19 Mar 2021 20:01:52 +0100 Subject: [PATCH] [orx-shader-phrases] Add ShaderPhraseRegistry --- orx-shader-phrases/build.gradle | 3 + .../src/main/kotlin/ShaderPreprocessor.kt | 147 ++++++++++++++---- .../src/test/kotlin/TestPreprocessShader.kt | 6 +- .../src/test/kotlin/TestShaderPhrase.kt | 20 +++ .../src/test/kotlin/TestShaderPhraseBook.kt | 23 +++ 5 files changed, 163 insertions(+), 36 deletions(-) create mode 100644 orx-shader-phrases/build.gradle create mode 100644 orx-shader-phrases/src/test/kotlin/TestShaderPhrase.kt create mode 100644 orx-shader-phrases/src/test/kotlin/TestShaderPhraseBook.kt diff --git a/orx-shader-phrases/build.gradle b/orx-shader-phrases/build.gradle new file mode 100644 index 00000000..dec2d403 --- /dev/null +++ b/orx-shader-phrases/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" +} \ 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 7ad01958..42fa31c1 100644 --- a/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt +++ b/orx-shader-phrases/src/main/kotlin/ShaderPreprocessor.kt @@ -1,9 +1,81 @@ package org.openrndr.extra.shaderphrases +import mu.KotlinLogging import org.openrndr.draw.Shader import org.openrndr.draw.codeFromURL import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases -import org.openrndr.internal.Driver +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties + +private val logger = KotlinLogging.logger {} + +/** + * A single shader phrase. + */ +class ShaderPhrase(val phrase: String) { + /** + * Register this shader phrase in the [ShaderPhraseRegistry] + * This will likely be called by [ShaderPhraseBook] + */ + fun register(bookId: String? = null) { + val functionRex = + Regex("(float|int|[bi]?vec2|[bi]?vec3|[bi]?vec4|mat3|mat4)[ ]+([a-zA-Z0-9_]+)[ ]*\\(.*\\).*") + val defs = phrase.split("\n").filter { + functionRex.matches(it) + }.take(1).mapNotNull { + val m = functionRex.find(it) + m?.groupValues?.getOrNull(2) + } + val id = defs.firstOrNull() ?: error("no function body found in phrase") + ShaderPhraseRegistry.registerPhrase("${bookId?.let { "$it." } ?: ""}$id", this) + } +} +/** + * A book of shader phrases. + */ +open class ShaderPhraseBook(val bookId: String) { + private var registered = false + + /** + * Registers all known shader phrases + */ + fun register() { + if (!registered) { + this::class.declaredMemberProperties.filter { + it.returnType.toString() == "org.openrndr.extra.shaderphrases.ShaderPhrase" + }.map { + @Suppress("UNCHECKED_CAST") + val m = it as? KProperty1 + m?.get(this)?.register(bookId) + } + registered = true + } + } +} + +/** + * The global, application-wide, shader phrase registry + */ +object ShaderPhraseRegistry { + private val phrases = mutableMapOf() + /** + * Registers a [phrase] with [id] + */ + fun registerPhrase(id: String, phrase: ShaderPhrase) { + phrases[id] = phrase + } + + /** + * Finds a phrase for [id], returns null when no phrase found + */ + fun findPhrase(id: String): ShaderPhrase? { + val phrase = phrases[id] + if (phrase == null) { + logger.warn { "no phrase found for id: \"$id\"" } + } + return phrase + } +} /** * Preprocess shader source. @@ -27,38 +99,46 @@ fun preprocessShader(source: String, symbols: Set = emptySet()): String if (symbol !in newSymbols) { newSymbols.add(symbol) - try { - /* Note that JVM-style reflection is used here because of short-comings in the Kotlin reflection - library (as of 1.3.61), most notably reflection support for file facades is missing. */ - val c = Class.forName(packageClass) - if (c.annotations.any { it.annotationClass == ShaderPhrases::class }) { - if (fieldName == "*") { - c.declaredMethods.filter { it.returnType.name == "java.lang.String" }.map { - "/* imported from $packageClass.$it */\n${it.invoke(null)}\n" - }.joinToString("\n") + - c.declaredFields.filter { it.type.name == "java.lang.String" }.map { - "/* imported from $packageClass.$it */\n${it.get(null)}\n" - }.joinToString("\n") - } else { - var result: String? - try { - val methodName = "get${fieldName.take(1).toUpperCase() + fieldName.drop(1)}" - result = preprocessShader(c.getMethod(methodName).invoke(null) as String, newSymbols) - } catch (e: NoSuchMethodException) { + val registryPhrase = ShaderPhraseRegistry.findPhrase(symbol) + + registryPhrase?.let { preprocessShader(it.phrase, newSymbols) } + ?: try { + /* Note that JVM-style reflection is used here because of short-comings in the Kotlin reflection + library (as of 1.3.61), most notably reflection support for file facades is missing. */ + val c = Class.forName(packageClass) + if (c.annotations.any { it.annotationClass == ShaderPhrases::class }) { + if (fieldName == "*") { + c.declaredMethods.filter { it.returnType.name == "java.lang.String" }.map { + "/* imported from $packageClass.$it */\n${it.invoke(null)}\n" + }.joinToString("\n") + + c.declaredFields.filter { it.type.name == "java.lang.String" }.map { + "/* imported from $packageClass.$it */\n${it.get(null)}\n" + }.joinToString("\n") + } else { + var result: String? try { - result = preprocessShader(c.getDeclaredField(fieldName).get(null) as String, newSymbols) - } catch (e: NoSuchFieldException) { - error("field \"$fieldName\" not found in \"#pragma import $packageClass.$fieldName\" on line ${index + 1}") + val methodName = "get${fieldName.take(1).toUpperCase() + fieldName.drop(1)}" + result = + preprocessShader(c.getMethod(methodName).invoke(null) as String, newSymbols) + } catch (e: NoSuchMethodException) { + try { + result = + preprocessShader( + c.getDeclaredField(fieldName).get(null) as String, + newSymbols + ) + } catch (e: NoSuchFieldException) { + error("field \"$fieldName\" not found in \"#pragma import $packageClass.$fieldName\" on line ${index + 1}") + } } + result } - result + } else { + 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}") } - } catch (e: ClassNotFoundException) { - error("class \"$packageClass\" not found in \"#pragma import $packageClass\" on line ${index + 1}") - } } else { "" } @@ -83,11 +163,12 @@ fun preprocessShaderFromUrl(url: String, symbols: Set = emptySet()): Str } fun Shader.Companion.preprocessedFromUrls( - vsUrl: String, - tcsUrl: String? = null, - tesUrl: String? = null, - gsUrl: String? = null, - fsUrl: String): Shader { + vsUrl: String, + tcsUrl: String? = null, + tesUrl: String? = null, + gsUrl: String? = null, + fsUrl: String +): Shader { val vsCode = codeFromURL(vsUrl).preprocess() val tcsCode = tcsUrl?.let { codeFromURL(it) }?.preprocess() diff --git a/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt b/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt index 24a0c2d8..6bc5d5a9 100644 --- a/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt +++ b/orx-shader-phrases/src/test/kotlin/TestPreprocessShader.kt @@ -18,9 +18,9 @@ object TestPreprocessShader : Spek({ } describe("A shader with import statements") { - val shader = """#version 330 -#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.* -""" + val shader = """ + |#version 330 + |#pragma import org.openrndr.extra.shaderphrases.phrases.Dummy.*""".trimMargin() describe("injects dummy phrase when preprocessed") { val processed = preprocessShader(shader) processed `should contain` "float dummy" diff --git a/orx-shader-phrases/src/test/kotlin/TestShaderPhrase.kt b/orx-shader-phrases/src/test/kotlin/TestShaderPhrase.kt new file mode 100644 index 00000000..f45c3aad --- /dev/null +++ b/orx-shader-phrases/src/test/kotlin/TestShaderPhrase.kt @@ -0,0 +1,20 @@ +import org.amshove.kluent.`should be` +import org.openrndr.extra.shaderphrases.ShaderPhrase +import org.openrndr.extra.shaderphrases.ShaderPhraseRegistry +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +object TestShaderPhrase : Spek({ + describe("A shader phrase") { + val phrase = ShaderPhrase(""" + |vec4 test_phrase() { + |} + """.trimMargin() ) + it("can be registered") { + phrase.register() + } + it("can be found") { + ShaderPhraseRegistry.findPhrase("test_phrase") `should be` phrase + } + } +}) diff --git a/orx-shader-phrases/src/test/kotlin/TestShaderPhraseBook.kt b/orx-shader-phrases/src/test/kotlin/TestShaderPhraseBook.kt new file mode 100644 index 00000000..695d7625 --- /dev/null +++ b/orx-shader-phrases/src/test/kotlin/TestShaderPhraseBook.kt @@ -0,0 +1,23 @@ +import org.amshove.kluent.`should be` +import org.openrndr.extra.shaderphrases.ShaderPhrase +import org.openrndr.extra.shaderphrases.ShaderPhraseBook +import org.openrndr.extra.shaderphrases.ShaderPhraseRegistry +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +class TestShaderPhraseBookobject : Spek({ + describe("A shader phrase book") { + val book = object:ShaderPhraseBook("testBook") { + val phrase = ShaderPhrase(""" + |vec4 test_phrase() { + |} + """.trimMargin() ) + } + it("can be registered") { + book.register() + } + it("can be found") { + ShaderPhraseRegistry.findPhrase("testBook.test_phrase") `should be` book.phrase + } + } +}) \ No newline at end of file