diff --git a/orx-glslify/README.md b/orx-glslify/README.md new file mode 100644 index 00000000..4d58ec58 --- /dev/null +++ b/orx-glslify/README.md @@ -0,0 +1,61 @@ +# orx-glslify + +Easily use glslify compatible shaders found on [npm](https://www.npmjs.com/search?q=glslify). + +### Caveats + +Some glslify shaders have their own imports. When this happens we print a message to the console, +so you can proceed to import them. These need to be imported in the shader file on top of the main import. + +There's also a mapping functionality that glslify provides that we don't support. This can be easily solved +by doing as the following example (based on `glsl-raytrace` package): + +```glsl +const int steps = 50; +vec2 map(vec3 p); + +#pragma import shaders.RayMarching.* +``` + +## Example + +Shader Phrases file: +```kotlin +@file:JvmName("Checkers") +@file:ShaderPhrases + +package shaders + +import org.openrndr.extra.glslify.glslify +import org.openrndr.extra.shaderphrases.annotations.ShaderPhrases + +val periodic by lazy { glslify("glsl-noise/classic/3d", "perlin") } +val checker by lazy { glslify("glsl-checker") } +val easings by lazy { glslify("glsl-easings/cubic-in-out", "easing")} +``` + +Shader file: + +```glsl +#version 330 + +in vec2 v_texCoord0; + +uniform sampler2D tex0; +uniform float uTime; + +out vec4 o_color; + +#pragma import shaders.Checkers.* + +void main() { + vec2 uv = v_texCoord0; + float n = perlin(vec3(uv * 2.5 + uTime * 0.01, uTime * 0.2)) * 0.5 + 0.5; + + float patt = checker(uv * easing(n), 6.0); + + vec3 col = mix(vec3(0.173, 0.216, 0.278),vec3(0.792, 0.282, 0.478), vec3(patt)) * (n + 0.1); + + o_color = vec4(col, 1.0); +} +``` diff --git a/orx-glslify/build.gradle b/orx-glslify/build.gradle new file mode 100644 index 00000000..f8c6fd27 --- /dev/null +++ b/orx-glslify/build.gradle @@ -0,0 +1,13 @@ +repositories { + maven { + url = uri("https://jitpack.io") + } +} + +dependencies { + implementation "com.google.code.gson:gson:$gsonVersion" + implementation "com.github.jkcclemens:khttp:-SNAPSHOT" + implementation "org.rauschig:jarchivelib:1.0.0" + implementation project(":orx-noise") +} + diff --git a/orx-glslify/src/main/kotlin/FileSystemUtils.kt b/orx-glslify/src/main/kotlin/FileSystemUtils.kt new file mode 100644 index 00000000..2639f869 --- /dev/null +++ b/orx-glslify/src/main/kotlin/FileSystemUtils.kt @@ -0,0 +1,42 @@ +package org.openrndr.extra.glslify + +import org.rauschig.jarchivelib.Archiver +import org.rauschig.jarchivelib.ArchiverFactory +import java.io.File +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Paths + +internal fun extractGzipStream(tarStream: InputStream, dest: File) { + val archiver: Archiver = ArchiverFactory.createArchiver("tar", "gz") + + archiver.extract(tarStream, dest) +} + +internal fun moveFilesUp(moduleDir: File) { + val modulePath = Paths.get(moduleDir.path) + val packageFolder = modulePath.resolve("package") + + val packageFiles = Files.list(packageFolder).filter { + Files.isDirectory(it) || shaderExtensions.contains(it.fileName.toString().substringAfterLast(".")) + } + + packageFiles.forEach { + // fileName also retrieves folders ¯\_(ツ)_/¯ + val dest = modulePath.resolve(modulePath.relativize(it).fileName) + Files.move(it, dest) + } +} + +internal fun parseModule(module: String): Pair { + val path = Paths.get(module) + val pathLen = path.nameCount + val moduleName = path.subpath(0, 1).toString() + var shaderPath = "index" + + if (pathLen > 1) { + shaderPath = path.subpath(1, pathLen).toString() + } + + return Pair(moduleName, shaderPath) +} diff --git a/orx-glslify/src/main/kotlin/Glslify.kt b/orx-glslify/src/main/kotlin/Glslify.kt new file mode 100644 index 00000000..7bcfd8d6 --- /dev/null +++ b/orx-glslify/src/main/kotlin/Glslify.kt @@ -0,0 +1,113 @@ +package org.openrndr.extra.glslify + +import khttp.responses.Response +import mu.KotlinLogging +import java.io.ByteArrayInputStream +import java.io.File +import java.io.InputStream + +const val BASE_URL = "https://registry.npmjs.org" +const val GLSLIFY_PATH = "src/main/resources/glslify" +const val IMPORT_PATT = """#pragma\sglslify:\s*(.*)\s=\s*require\('([\w\-]+)'\)""" +const val EXPORT_PATT = """#pragma\sglslify:\s*export\(([\w\-]+)\)""" + +internal val shaderExtensions = arrayOf("glsl", "frag") + +data class GlslifyImport( + val functionName: String, + val pkgName: String, + var exists: Boolean +) + +private val logger = KotlinLogging.logger {} + +fun glslify(module: String, renameFunctionTo: String? = null): String { + val (moduleName: String, shaderPath: String) = parseModule(module) + val moduleDir = File("$GLSLIFY_PATH/$moduleName") + + val packageUrl = getPackageUrl(moduleName) + + if (packageUrl.isNullOrEmpty()) { + throw error("[glslify] $moduleName not found") + } + + if (!moduleDir.exists()) { + try { + moduleDir.mkdirs() + + val response: Response = khttp.get(packageUrl, stream = true) + + val tarStream: InputStream = ByteArrayInputStream(response.content) + + extractGzipStream(tarStream, moduleDir) + + moveFilesUp(moduleDir) + + File("$moduleDir/package").deleteRecursively() + + logger.trace("[glslify] $moduleName downloaded") + } catch (ex: Exception) { + logger.error(ex) { "[glslify]: There was an error getting $moduleName" } + + return "" + } + } else { + logger.trace("[glslify] $moduleName already exists.. Skipping download") + } + + val shaderFile: File + + try { + shaderFile = shaderExtensions.map { + File("$GLSLIFY_PATH/$moduleName/$shaderPath.$it") + }.first { it.exists() } + } catch (ex: NoSuchElementException) { + logger.trace("[glslify] $moduleName: $shaderPath doesn't lead to any shader file") + + return "" + } + + val shader = mutableListOf() + val imports = mutableListOf() + var exportName: String? = null + + shaderFile.useLines { sequence -> + for (line in sequence.iterator()) { + if (line.contains("#pragma")) { + Regex(IMPORT_PATT).find(line)?.let { + if (it.groupValues.size > 1) { + val importExists = File("$GLSLIFY_PATH/${it.groupValues[2]}").exists() + + imports.add(GlslifyImport(it.groupValues[1], it.groupValues[2], importExists)) + } + } + + Regex(EXPORT_PATT).find(line)?.let { + if (it.groupValues.size > 1) { + exportName = it.groupValues[1] + } + } + } else { + shader.add(line) + } + } + } + + val missingImports = imports.filter { !it.exists } + + if (missingImports.isNotEmpty()) { + missingImports.forEach { + logger.info("Missing package: ${it.pkgName} - Import name: ${it.functionName}") + } + + throw error("[glslify] Please declare the missing imports") + } + + var shaderString = shader.joinToString("\n") + + if (renameFunctionTo != null && exportName != null) { + shaderString = shaderString.replace( exportName!!, renameFunctionTo) + } + + return shaderString +} diff --git a/orx-glslify/src/main/kotlin/NpmUtils.kt b/orx-glslify/src/main/kotlin/NpmUtils.kt new file mode 100644 index 00000000..cbdbc6a8 --- /dev/null +++ b/orx-glslify/src/main/kotlin/NpmUtils.kt @@ -0,0 +1,52 @@ +package org.openrndr.extra.glslify + +import com.google.gson.GsonBuilder +import com.google.gson.annotations.SerializedName +import khttp.responses.Response + + +internal data class NPMPackageDist( + val shasum: String, + val tarball: String +) + +internal data class NPMPackageVersion( + val name: String, + val version: String, + val dist: NPMPackageDist +) + +internal data class NPMResponse( + val name: String?, + val error: String?, + @SerializedName("dist-tags") + val distTags: MutableMap?, + val versions: Map? +) + +internal fun getPackageUrl(module: String): String? { + val url = "$BASE_URL/$module" + + val response : Response = khttp.get( + url = url, + headers = mapOf( + "Accept" to "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8" + ) + ) + + val gson = GsonBuilder().create() + + val npmResponse = gson.fromJson( + response.text, NPMResponse::class.java + ) + + if (npmResponse.error != null) { + return null + } + + return npmResponse.distTags?.let { + val latest = it["latest"] + + npmResponse.versions?.get(latest)?.dist?.tarball + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9fff71e7..99ffa818 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ include 'orx-camera', 'orx-parameters', 'orx-filter-extension', 'orx-fx', + 'orx-glslify', 'orx-gradient-descent', 'orx-integral-image', 'orx-interval-tree', @@ -35,4 +36,5 @@ include 'orx-camera', 'orx-kinect-v1-natives-macos', 'orx-kinect-v1-natives-windows', 'orx-kinect-v1-demo' +include 'orx-glslify'