From 80fd0d3c61f315b83c1eeda40daef31af01ad798 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Thu, 10 Sep 2026 00:12:15 +0530 Subject: Ray-march in play_shader --- build.gradle.kts | 2 +- src/main/kotlin/org/openrndr/Preload.kt | 2 +- src/main/kotlin/phenax/utils/program.kt | 63 ++++++++++++++++++++++++-- src/main/kotlin/play/PlayProgram.kt | 47 +++++++++++++++++++ src/main/kotlin/play/PlayShowProgram.kt | 62 ------------------------- src/main/resources/shaders/play_shader.cs | 75 +++++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 67 deletions(-) create mode 100644 src/main/kotlin/play/PlayProgram.kt delete mode 100644 src/main/kotlin/play/PlayShowProgram.kt create mode 100644 src/main/resources/shaders/play_shader.cs diff --git a/build.gradle.kts b/build.gradle.kts index 1b5c024..694f23f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,7 +27,7 @@ val orxFeatures = setOf( // "orx-expression-evaluator", // "orx-fcurve", // "orx-fft", -// "orx-file-watcher", + "orx-file-watcher", "orx-fx", // "orx-git-archiver", // "orx-gradient-descent", diff --git a/src/main/kotlin/org/openrndr/Preload.kt b/src/main/kotlin/org/openrndr/Preload.kt index 375294a..1bbd673 100644 --- a/src/main/kotlin/org/openrndr/Preload.kt +++ b/src/main/kotlin/org/openrndr/Preload.kt @@ -7,7 +7,7 @@ import phenax.utils.recordingEnabled class Preload : ApplicationPreload() { override fun onProgramSetup(program: Program) { println("BACKEND(${program.name}): ${program.application::class.qualifiedName}") - if (recordingEnabled()) { + if (recordingEnabled) { program.extend(HeadlessRecorder()) } } diff --git a/src/main/kotlin/phenax/utils/program.kt b/src/main/kotlin/phenax/utils/program.kt index 2d3e04b..6ff5379 100644 --- a/src/main/kotlin/phenax/utils/program.kt +++ b/src/main/kotlin/phenax/utils/program.kt @@ -1,9 +1,66 @@ package phenax.utils -fun recordDuration(durationSeconds: Int) { - System.setProperty("phenax.record.frameCount", (60 * durationSeconds).toString()) +import java.io.File +import kotlin.properties.ReadOnlyProperty +import org.openrndr.ApplicationBuilder +import org.openrndr.Program +import org.openrndr.draw.* +import org.openrndr.extra.filewatcher.watchingFile +import org.openrndr.extra.olive.oliveProgram as oliveProgramOriginal + +fun Int.seconds(): Int = this * 1000 + +fun Int.minutes(): Int = this.seconds() * 60 + +fun recordDuration(millis: Int) { + System.setProperty("phenax.record.frameCount", (60 * millis / 1000).toInt().toString()) } fun recordDuration(): Int? = System.getProperty("phenax.record.frameCount")?.toIntOrNull() -fun recordingEnabled(): Boolean = System.getenv("RECORD") != null +val recordingEnabled: Boolean by lazy { System.getenv("RECORD") != null } + +// fun classDir(): String = +// "src/main/kotlin/${stackRootClassName(sanitize = +// true).split(".").dropLast(1).joinToString("/")}"; + +// fun localComputeShader(name: String): ComputeShader = +// ComputeShader.fromCode(File("src/main/resources/shaders/${name}.cs").readText(), name) + +// Have to keep the same name (oliveProgram) to get update on save to work +fun ApplicationBuilder.oliveProgram(init: Program.() -> Unit) { + if (recordingEnabled) { + program(init) + } else { + // TODO: Make watching shader files work + oliveProgramOriginal(init = init) + } +} + +fun Program.liveComputeShader(name: String): ReadOnlyProperty { + val shaderCode by + watchingFile(File("src/main/resources/shaders/${name}.cs")) { file -> + println("change") + file.readText() + } + + var cs: ComputeShader = ComputeShader.fromCode(shaderCode, name) + + if (!recordingEnabled) { + var lastCompiledCode: String = shaderCode + + program.extend { + if (shaderCode != lastCompiledCode) { + cs.destroy() + try { + cs = ComputeShader.fromCode(shaderCode, name) + } catch (e: Exception) { + println("Shader compile failed: ${e.message}") + } + lastCompiledCode = shaderCode + } + } + } + + return ReadOnlyProperty { _, _ -> cs } +} diff --git a/src/main/kotlin/play/PlayProgram.kt b/src/main/kotlin/play/PlayProgram.kt new file mode 100644 index 0000000..0ec0133 --- /dev/null +++ b/src/main/kotlin/play/PlayProgram.kt @@ -0,0 +1,47 @@ +package play + +import java.io.File +import org.openrndr.Program +import org.openrndr.application +import org.openrndr.draw.* +import phenax.utils.* + +fun main() = application { + recordDuration(10.seconds()) + + configure { + width = 640 + height = 480 + windowResizable = false + } + + oliveProgram { + val cs by liveComputeShader("play_shader") + // val image = loadImage("data/images/pm5544.png") + // val font = loadFont("data/fonts/default.otf", 64.0) + + // val inputBuffer = colorBuffer(width, height, type = ColorType.UINT8) + val outputBuffer = colorBuffer(width, height, type = ColorType.UINT8) + + extend { + // drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2)) + // drawer.image(image) + + // drawer.fill = ColorRGBa.PINK + // drawer.circle( + // cos(seconds) * width / 2.0 + width / 2.0, + // sin(0.5 * seconds) * height / 2.0 + height / 2.0, + // 140.0 + // ) + + // drawer.fontMap = font + // drawer.fill = ColorRGBa.WHITE + // drawer.text("OPENRNDR", width / 2.0, height / 2.0) + + cs.uniform("seconds", seconds) + cs.image("outputImg", 1, outputBuffer.imageBinding(0, ImageAccess.WRITE)) + cs.execute(outputBuffer.width, outputBuffer.height, 1) + drawer.image(outputBuffer) + } + } +} diff --git a/src/main/kotlin/play/PlayShowProgram.kt b/src/main/kotlin/play/PlayShowProgram.kt deleted file mode 100644 index 4107cd7..0000000 --- a/src/main/kotlin/play/PlayShowProgram.kt +++ /dev/null @@ -1,62 +0,0 @@ -package play - -import org.openrndr.application -import org.openrndr.draw.* -import org.openrndr.extra.olive.oliveProgram -import phenax.utils.* - -fun main() = application { - recordDuration(10) - - configure { - width = 640 - height = 480 - windowResizable = false - } - - oliveProgram { - // val image = loadImage("data/images/pm5544.png") - // val font = loadFont("data/fonts/default.otf", 64.0) - - val cs = computeStyle { - computeTransform = - """ - ivec2 coords = ivec2(gl_GlobalInvocationID.xy); - ivec2 resolution = imageSize(p_outputImg).xy; - float aspect_ratio = resolution.y*1.0 / resolution.x; - - vec2 uv = vec2(1.0, aspect_ratio) * (coords - 0.5*resolution)/resolution; - float dist = length(uv) - 0.1; - float step = step(0.0, dist); - - vec4 color = vec4(step, 0.0, 0.0, 1.0); - imageStore(p_outputImg, coords, color); - """.trimIndent() - } - - // val inputBuffer = colorBuffer(width, height, type = ColorType.UINT8) - - val outputBuffer = colorBuffer(width, height, type = ColorType.UINT8) - - extend { - // drawer.drawStyle.colorMatrix = tint(ColorRGBa.WHITE.shade(0.2)) - // drawer.image(image) - - // drawer.fill = ColorRGBa.PINK - // drawer.circle( - // cos(seconds) * width / 2.0 + width / 2.0, - // sin(0.5 * seconds) * height / 2.0 + height / 2.0, - // 140.0 - // ) - - // drawer.fontMap = font - // drawer.fill = ColorRGBa.WHITE - // drawer.text("OPENRNDR", width / 2.0, height / 2.0) - - cs.parameter("seconds", seconds) - cs.image("outputImg", outputBuffer.imageBinding(0, ImageAccess.WRITE)) - cs.execute(outputBuffer.width, outputBuffer.height, 1) - drawer.image(outputBuffer) - } - } -} diff --git a/src/main/resources/shaders/play_shader.cs b/src/main/resources/shaders/play_shader.cs new file mode 100644 index 0000000..d3cd4f5 --- /dev/null +++ b/src/main/resources/shaders/play_shader.cs @@ -0,0 +1,75 @@ +#version 430 +layout(local_size_x = 1, local_size_y = 1) in; + +uniform float seconds = 0; +layout(rgba8) uniform writeonly image2D outputImg; + +#define MAX_ITERATIONS 500 +#define MAX_DISTANCE 100.0 +#define SURF_DIST 0.01 + +float sdfCircle(in vec3 p, in vec3 origin, in float radius) { + return length(p - origin) - radius; +} + +float smin(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); + return mix(b, a, h) - k * h * (1.0 - h); +} + +float scene(in vec3 p) { + float c1 = sdfCircle(p, vec3(0, 1, 2.4), 0.2); + float c2 = sdfCircle(p, vec3(-0.6, 1, 3), 0.3); + return min(c1, c2); +} + +float ray_march_scene(in vec3 ray_origin, in vec3 ray_direction) { + float dist = 0.; + for (int i = 0; i < MAX_ITERATIONS && dist < MAX_DISTANCE; i++) { + vec3 p = ray_origin + dist * ray_direction; + float gap = scene(p); + dist += gap; + if (gap <= SURF_DIST) + return dist; + } + return dist; +} + +vec3 surface_normal_scene(in vec3 p) { + vec2 inc = vec2(0.01, 0); + return normalize(vec3(scene(p + inc.xyy) - scene(p - inc.xyy), + scene(p + inc.yxy) - scene(p - inc.yxy), + scene(p + inc.yyx) - scene(p - inc.yyx))); +} + +void main() { + vec2 resolution = vec2(imageSize(outputImg).xy); + vec3 uv = vec3((gl_GlobalInvocationID.xy - resolution / 2.0) / resolution, 0); + float aspect_ratio = resolution.y / resolution.x; + uv /= vec3(aspect_ratio, 1, 1); + + vec3 camera_pos = vec3(0, 1, 0); + vec3 camera_direction = normalize(vec3(uv.x, uv.y, 1.)); + + vec3 light_pos = vec3(1, 2. * sin(seconds), 1); + + float depth = ray_march_scene(camera_pos, camera_direction); + vec3 p = camera_pos + camera_direction * depth; + vec3 normal = surface_normal_scene(p); + vec3 light_vec = normalize(light_pos - p); + + float contrast = dot(normal, light_vec); + contrast = clamp(contrast, 0.1, 1.); + + vec3 col = vec3(0.15); + if (depth < MAX_DISTANCE) { + float light_ray_dist = + ray_march_scene(p + 10. * normal * SURF_DIST, light_vec); + if (light_ray_dist < length(light_pos - p)) + contrast *= 0.3; + col = vec3(0, 1. - (depth / MAX_DISTANCE), 0.4); + col *= contrast; + } + + imageStore(outputImg, ivec2(gl_GlobalInvocationID.xy), vec4(col, 1.)); +} -- cgit v1.3.1