SpinningDiffusionMotion.kt
src/jvmMain/kotlin/afield/media/video/diffusion/SpinningDiffusionMotion.kt · 252 lines
package afield.media.video.diffusion
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
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.graphics.texture.HasImageViewArray
import afield.graphics.texture.PreparesSampledImages
import org.lwjgl.vulkan.VK10.*
The motion provider behind the spinning-curves demo, which derives per-pixel correspondence from the geometry Field drew rather than from the two images.
SpinningDiffusionDemo's five curves are ribbons carrying a surface id per vertex, and the rotation that drew each frame is kept against that frame's id. To answer a request this projects every pixel's surface point back through the reference frame's rotation, which is the position that point had then; an id pass rendered at the reference angle says whether the surface visible there is the same one, so a ribbon that has turned behind another is rejected instead of carrying the wrong colour across. The correspondence ribbon is widened well past the drawn line, so the broader marks the model returns are covered too, and confidence falls off toward its edge.
The fallback pass covers what the mask rejected. Where the current frame shows bare paper it fills from paper near that pixel in the reference image, so the rejected band carries no bright impression of the reference frame's curves into empty paper, and it leaves every pixel the current source actually drew.
internal class SpinningDiffusionMotion
Renderer-derived correspondence for the demo ribbons; reference IDs reject hidden crossings.
internal class SpinningDiffusionMotion(
private val renderer: Renderer, private val source: RenderToTexture,
private val positions: List<Float>, private val normals: List<Float>, private val ids: List<Float>,
private val indices: List<Int>, private val angle: () -> Float
) : StreamDiffusionMotionProvider, AutoCloseable {
private val angles = linkedMapOf<Long, Float>()
private fun target() = RenderToTexture(renderer) {
width = source.width; height = source.height; format = VK_FORMAT_R32G32B32A32_SFLOAT
useDepth = false
}.also { it.setDefaultSampler(anisotropyEnable = false, filter = VK_FILTER_NEAREST) }
private val referenceIds = target()
private val mapping = target()
private val filledPaper = RenderToTexture(renderer) {
width = source.width; height = source.height; format = source.colorFormats[0]; useDepth = false
}.also { it.setDefaultSampler(anisotropyEnable = false) }
private var styledReference = source
private val modules = mutableListOf<Module>()
private fun geometry(target: RenderToTexture, fragment: String, name: String): Mesh {
val module = Module(parameters(renderer) {
+target; vertex = VERTEX; this.fragment = fragment
depthTest = false; depthWrite = false; blend = false
textureResolver = { t -> if (t.name == "referenceIds") fromViews(referenceIds, t.name, referenceIds.defaultSampler!!) else null }
})
modules += module; check(module.valid) { "Spinning motion shader failed" }
val pointer = module.pointer()
val mesh = Mesh(renderer, pointer).resize(positions.size / 3, indices.size / 3)
mesh.write("pos") { positions.forEach { put(it) } }
mesh.write("tc") { normals.forEach { put(it) } }
mesh.write("col") { ids.forEachIndexed { i, id -> put(id); put(if (i % 2 == 0) -1f else 1f); put(0f); put(0f) } }
mesh.t!!.mapAndWrite_ints { indices.forEach { put(it) } }
module.meshes[name] = mesh; target.modules[name] = pointer
return mesh
}
private val referenceMesh: Mesh
private val background: Module
private val mappingMesh: Mesh
private val paperFill: Module
private var closed = false
init
body — 41 lines, from line 73
init {
try {
referenceMesh = geometry(referenceIds, IDS, "reference-ids")
background = Module(parameters(renderer) {
+mapping; vertex = FULLSCREEN; fragment = BACKGROUND
depthTest = false; depthWrite = false; blend = false
textureResolver = { t -> if (t.name == "referenceIds") fromViews(referenceIds, t.name, referenceIds.defaultSampler!!) else null }
}).also { module ->
modules += module; check(module.valid)
val pointer = module.pointer()
module.meshes["background"] = Mesh(renderer, pointer).resize(3, 0)
mapping.modules["background"] = pointer
}
mappingMesh = geometry(mapping, MOTION, "curve-correspondence")
paperFill = Module(parameters(renderer) {
+filledPaper; vertex = FULLSCREEN; fragment = PAPER_FILL
depthTest = false; depthWrite = false; blend = false
textureResolver = { t ->
when (t.name) {
"sourceImage" -> fromViews(source, t.name, source.defaultSampler!!)
"referenceIds" -> fromViews(referenceIds, t.name, referenceIds.defaultSampler!!)
"motionImage" -> fromViews(mapping, t.name, mapping.defaultSampler!!)
"referenceImage" -> {
val provider = object : HasImageViewArray, PreparesSampledImages {
override val imageViewArray get() = styledReference.imageViewArray
override fun prepareSampledImagesFor(command: CommandBuffer) =
styledReference.prepareSampledImagesFor(command)
}
fromViews(provider, t.name, source.defaultSampler!!)
}
else -> null
}
}
}).also { module ->
modules += module; check(module.valid) { "Diffusion paper fill shader failed" }
val pointer = module.pointer()
module.meshes["fill-paper"] = Mesh(renderer, pointer).resize(3, 0)
filledPaper.modules["fill-paper"] = pointer
}
} catch (e: Throwable) { close(); throw e }
}
override fun onFrame(frame: StreamDiffusionFrame)
override fun onFrame(frame: StreamDiffusionFrame) {
angles[frame.id] = angle()
while (angles.size > 512) angles.remove(angles.keys.first())
}
override fun record(command: CommandBuffer, request: StreamDiffusionMotionRequest): RenderToTexture?
override fun record(command: CommandBuffer, request: StreamDiffusionMotionRequest): RenderToTexture? {
val current = angles[request.current.id] ?: return null
val previous = angles[request.reference.id] ?: return null
referenceMesh.push["angle"] = previous; referenceMesh.push["referenceAngle"] = previous
referenceIds.render(command, 0)
mappingMesh.push["angle"] = current; mappingMesh.push["referenceAngle"] = previous
mapping.render(command, 0)
return mapping
}
override fun recordFallback(command: CommandBuffer, request: StreamDiffusionMotionRequest,
override fun recordFallback(command: CommandBuffer, request: StreamDiffusionMotionRequest,
source: RenderToTexture, referenceImage: RenderToTexture): RenderToTexture {
styledReference = referenceImage
filledPaper.render(command, 0)
return filledPaper
}
override fun close()
override fun close() {
if (closed) return
closed = true
renderer.cleanup += Runnable {
modules.forEach { it.cleanup() }
for (target in listOf(filledPaper, mapping, referenceIds)) {
target.imageViewArray.forEach { it.imageView.cleanup() }
target.colorImageRefs.forEach { it?.cleanup() }; target.defaultSampler?.cleanup()
}
}
}
companion object
declaration — 59 lines, from line 144
companion object {
private const val PROJECTION = """
vec3 transformed(vec3 q,float angle) {
float tilt=.32*sin(angle);
q=vec3(cos(tilt)*q.x+sin(tilt)*q.z,q.y,-sin(tilt)*q.x+cos(tilt)*q.z);
mat2 turn=mat2(cos(angle),sin(angle),-sin(angle),cos(angle));
return vec3(turn*q.xy,q.z);
}
vec2 projected(vec3 q,float angle) {
q=transformed(q,angle); return q.xy*(3.5/(q.z+3.5))*.76;
}
"""
private val VERTEX = """#version 460
layout(location=0) in vec3 pos;
layout(location=1) in vec2 tc;
layout(location=2) in vec4 col;
layout(location=0) out vec3 surfacePoint;
layout(location=1) out float surfaceScale;
layout(location=2) flat out float surfaceId;
layout(location=3) out float ribbonSide;
layout(push_constant) uniform P { float angle; float referenceAngle; } p;
$PROJECTION
void main() {
// Extend correspondence around ribbons to carry generated stroke edges.
vec3 q=pos+vec3(tc*6,0);
surfaceScale=1./(transformed(q,p.angle).z+3.5);
surfacePoint=q*surfaceScale; surfaceId=col.x; ribbonSide=col.y;
gl_Position=vec4(projected(q,p.angle),0,1);
}
"""
private const val IDS = """#version 460
layout(location=2) flat in float surfaceId;
layout(location=0) out vec4 color;
void main() { color=vec4(surfaceId,0,0,1); }
"""
private val MOTION = """#version 460
layout(location=3) in float ribbonSide;
layout(location=0) in vec3 surfacePoint;
layout(location=1) in float surfaceScale;
layout(location=2) flat in float surfaceId;
layout(set=0,binding=0) uniform sampler2D referenceIds;
layout(push_constant) uniform P { float angle; float referenceAngle; } p;
layout(location=0) out vec4 color;
$PROJECTION
void main() {
vec2 previousUv=projected(surfacePoint/surfaceScale,p.referenceAngle)*.5+.5;
previousUv.y=1-previousUv.y; // Field uses a negative-height viewport.
bool inside=all(greaterThanEqual(previousUv,vec2(0))) && all(lessThanEqual(previousUv,vec2(1)));
float visible=inside && abs(texture(referenceIds,previousUv).r-surfaceId)<.5?1:0;
color=vec4(previousUv,visible*(1-smoothstep(.65,1,abs(ribbonSide))),0);
}
"""
private const val FULLSCREEN = """#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);
}
"""
The source paper is flat; generated paper has a different tone/texture. Fill rejected paper using nearby reference paper, preserving actual current source strokes. This prevents the visibility mask leaving a bright impression of the old curve positions.
source — 46 lines, from line 206
private const val PAPER_FILL = """#version 460
layout(location=0) in vec2 uv;
layout(set=0,binding=0) uniform sampler2D sourceImage;
layout(set=0,binding=1) uniform sampler2D referenceImage;
layout(set=0,binding=2) uniform sampler2D referenceIds;
layout(set=0,binding=3) uniform sampler2D motionImage;
layout(location=0) out vec4 color;
void main() {
vec4 fresh=texture(sourceImage,uv);
vec3 sourcePaper=texelFetch(sourceImage,ivec2(0),0).rgb;
vec4 m=texelFetch(motionImage,ivec2(gl_FragCoord.xy),0);
if (m.z*(1-m.w)>=.999 || distance(fresh.rgb,sourcePaper)>.005) {
color=fresh; return;
}
vec3 paper=texture(referenceImage,vec2(.02)).rgb;
vec2 texel=1./vec2(textureSize(referenceImage,0));
vec3 sum=vec3(0); float total=0;
for(int y=-4;y<=4;y++) for(int x=-4;x<=4;x++) {
vec2 q=uv+vec2(x,y)*6*texel;
if (any(lessThan(q,vec2(0))) || any(greaterThan(q,vec2(1))) ||
texture(referenceIds,q).r>.5) continue;
vec3 c=texture(referenceImage,q).rgb;
// Do not copy another strong mark into newly revealed paper.
if (abs(dot(c-paper,vec3(.2126,.7152,.0722)))>.25) continue;
float w=1./(1+float(x*x+y*y)); sum+=c*w; total+=w;
}
color=vec4(total>0?sum/total:paper,fresh.a);
}
"""
private const val BACKGROUND = """#version 460
layout(location=0) in vec2 uv;
layout(set=0,binding=0) uniform sampler2D referenceIds;
layout(location=0) out vec4 color;
void main() {
// Newly revealed paper, plus a conservative band for generated stroke edges.
vec2 texel=1./vec2(textureSize(referenceIds,0));
float occupied=0;
for(int y=-2;y<=2;y++) for(int x=-2;x<=2;x++) {
float hit=texture(referenceIds,uv+vec2(x,y)*4*texel).r>.5?1:0;
occupied=max(occupied,hit*(1-length(vec2(x,y))/3.));
}
color=vec4(uv,1-occupied,0);
}
"""
}
}