SpinningDiffusionDemo.kt
src/jvmMain/kotlin/afield/media/video/diffusion/SpinningDiffusionDemo.kt · 168 lines
package afield.media.video.diffusion
import afield.graphics.vulkan.Renderer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.render.Module
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.shader.fromViews
import afield.math.linalg.vec
import org.lwjgl.vulkan.VK10.*
import kotlin.math.*
A complete demo: SpinningDiffusionDemo draws five rotating curves into a source texture, runs them through a StreamDiffusionView, and shows source and stylized output side by side.
It also supplies a renderer-derived motion provider for its ribbons, so it exercises display
reprojection and history blending. Set angle to turn the curves.
class SpinningDiffusionDemo
Native Field geometry for the video study's five curves. Left: live source; right: causal output.
class SpinningDiffusionDemo(
/** The renderer that draws the curves and carries the comparison to the screen. */
val renderer: Renderer,
config: StreamDiffusionConfig,
/** Draw light curves on a dark ground instead of dark ones on pale paper, which is what the
* white-on-black prompts in [StreamDiffusionPresets] expect to be given. */
val whiteOnBlack: Boolean = false,
/** The name the demo and its view register under; two demos need two names. */
val name: String = "spinning-diffusion"
) : AutoCloseable {
private val background = if (whiteOnBlack) doubleArrayOf(8.0, 8.0, 10.0) else doubleArrayOf(244.0, 241.0, 234.0)
private fun linear(v: Double): Double = (v / 255).let { if (it <= .04045) it / 12.92 else ((it + .055) / 1.055).pow(2.4) }
val source
The 512 by 512 target the curves are drawn into, and the only thing the worker is shown.
val source = RenderToTexture(renderer) {
width = 512; height = 512; useDepth = false
clearColor = vec(linear(background[0]), linear(background[1]), linear(background[2]), 1)
}.also { it.setDefaultSampler(anisotropyEnable = false) }
private val lines = Module(parameters(renderer) {
+source; vertex = VERTEX; fragment = FRAGMENT
depthTest = false; depthWrite = false; blend = false
})
private val linePointer = lines.pointer()
private val mesh = Mesh(renderer, linePointer)
private var motion: SpinningDiffusionMotion? = null
val view: StreamDiffusionView
The view over source, with the demo's own motion provider. Change the prompt or the
strength while it runs through its reset, and read its counters for what the worker is doing.
private val comparison: Module
private var closed = false
var angle: Float
Where the curves are in their turn, in radians; it also tilts them out of the plane. Set it once a frame to animate, which is what gives the worker something to follow.
var angle: Float = 0f
set(value) { field = value; mesh.push["angle"] = value }
init
body — 68 lines, from line 58
init {
check(lines.valid)
val positions = mutableListOf<Float>()
val normals = mutableListOf<Float>()
val indices = mutableListOf<Int>()
val surfaceIds = mutableListOf<Float>()
val paths = (0..4).map { curve ->
val n = if (curve == 4) 1 else 160
(0..n).map { j ->
val t = j.toDouble() / n * 2 * PI
val u = j.toDouble() / n * 2 - 1
when (curve) {
0 -> doubleArrayOf(.85 * cos(t), .65 * sin(t), .16 * sin(2 * t))
1 -> doubleArrayOf(.46 * cos(t), .36 * sin(t), .32)
2 -> doubleArrayOf(.95 * u, .27 * sin(1.5 * PI * u), -.24 + .10 * cos(PI * u))
3 -> doubleArrayOf(.35 * sin(PI * u), .87 * u, .13 * cos(2 * PI * u))
else -> if (j == 0) doubleArrayOf(-.70, -.70, .12) else doubleArrayOf(.65, .65, .12)
}
}
}
for ((curveId, path) in paths.withIndex()) for (i in 0 until path.lastIndex) {
val a = path[i]; val b = path[i + 1]
val dx = b[0] - a[0]; val dy = b[1] - a[1]
val length = hypot(dx, dy).coerceAtLeast(1e-9)
val nx = -dy / length * .0077; val ny = dx / length * .0077
val start = positions.size / 3
for ((point, side) in listOf(a to -1, a to 1, b to -1, b to 1)) {
positions.addAll(point.map { it.toFloat() })
surfaceIds += (curveId + 1).toFloat()
normals += (side * nx).toFloat(); normals += (side * ny).toFloat()
}
indices.addAll(listOf(start, start + 1, start + 2, start + 2, start + 1, start + 3))
}
mesh.resize(positions.size / 3, indices.size / 3)
mesh.write("pos") { positions.forEach { put(it) } }
mesh.write("tc") { normals.forEach { put(it) } }
mesh.t!!.mapAndWrite_ints { indices.forEach { put(it) } }
mesh.push["angle"] = 0f; mesh.push["whitePaper"] = if (whiteOnBlack) 0 else 1
lines.meshes["curves"] = mesh
source.modules["curves"] = linePointer
try {
motion = SpinningDiffusionMotion(renderer, source, positions, normals, surfaceIds, indices) { angle }
view = StreamDiffusionView(renderer, source, config, name, motion = motion)
}
catch (e: Throwable) { motion?.close(); renderer.cleanup += Runnable { lines.cleanup(); cleanupSource() }; throw e }
comparison = Module(parameters(renderer) {
vertex = COMPARE_VERTEX; fragment = COMPARE_FRAGMENT
depthTest = false; depthWrite = false; blend = false
textureResolver = { t -> when (t.name) {
"sourceImage" -> fromViews(source, t.name, source.defaultSampler!!)
"diffusedImage" -> fromViews(view.output, t.name, view.output.defaultSampler!!)
else -> null
} }
})
if (!comparison.valid) {
view.close(); motion?.close()
renderer.cleanup += Runnable { comparison.cleanup(); lines.cleanup(); cleanupSource() }
error("Diffusion comparison shader failed to create its pipeline")
}
val pointer = comparison.pointer()
val triangle = Mesh(renderer, pointer).resize(3, 0)
comparison.meshes["comparison"] = triangle
renderer.renderList.modules[name] = pointer
}
private fun cleanupSource() {
source.imageViewArray.forEach { it.imageView.cleanup() }
source.colorImageRefs.forEach { it?.cleanup() }; source.defaultSampler?.cleanup()
}
override fun close()
Takes the comparison off the screen, stops the worker, and releases the demo's targets.
override fun close() {
if (closed) return
closed = true
renderer.renderList.modules.remove(name)
view.close(); motion?.close()
renderer.cleanup += Runnable { comparison.cleanup(); lines.cleanup(); cleanupSource() }
}
companion object
companion object {
private const val VERTEX = """#version 460
layout(location=0) in vec3 pos;
layout(location=1) in vec2 tc;
layout(push_constant) uniform P { float angle; int whitePaper; } p;
void main() {
float tilt=.32*sin(p.angle);
vec3 q=pos+vec3(tc,0);
q=vec3(cos(tilt)*q.x+sin(tilt)*q.z,q.y,-sin(tilt)*q.x+cos(tilt)*q.z);
mat2 turn=mat2(cos(p.angle),sin(p.angle),-sin(p.angle),cos(p.angle));
gl_Position=vec4(turn*q.xy*(3.5/(q.z+3.5))*.76,0,1);
}
"""
private const val FRAGMENT = """#version 460
layout(push_constant) uniform P { float angle; int whitePaper; } p;
layout(location=0) out vec4 color;
void main() {
vec3 c=p.whitePaper==1?vec3(38,36,33)/255.:vec3(244,244,240)/255.;
color=vec4(mix(c/12.92,pow((c+.055)/1.055,vec3(2.4)),greaterThan(c,vec3(.04045))),1);
}
"""
private const val COMPARE_VERTEX = """#version 460
layout(location=0) out vec2 uv;
void main(){vec2 p=vec2((gl_VertexIndex<<1)&2,gl_VertexIndex&2);uv=vec2(p.x,1-p.y);gl_Position=vec4(p*2-1,0,1);}
"""
private const val COMPARE_FRAGMENT = """#version 460
layout(location=0) in vec2 uv;
layout(set=0,binding=0) uniform sampler2D sourceImage;
layout(set=0,binding=1) uniform sampler2D diffusedImage;
layout(location=0) out vec4 color;
void main(){vec2 q=vec2(fract(uv.x*2),uv.y);color=uv.x<.5?texture(sourceImage,q):texture(diffusedImage,q);}
"""
}
}