Skip to content

Commit f7d2b97

Browse files
committed
make motion blur preview better
1 parent 88c01aa commit f7d2b97

2 files changed

Lines changed: 115 additions & 71 deletions

File tree

src/main/kotlin/org/polyfrost/polyplus/client/gui/PolyPlusOnboardingScreen.kt

Lines changed: 21 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,10 @@ import androidx.compose.ui.Modifier
3737
import androidx.compose.ui.draw.clip
3838
import androidx.compose.ui.draw.shadow
3939
import androidx.compose.ui.graphics.Color
40-
import androidx.compose.ui.graphics.ImageBitmap
41-
import androidx.compose.ui.graphics.BlendMode
4240
import androidx.compose.ui.graphics.TransformOrigin
4341
import androidx.compose.ui.graphics.graphicsLayer
44-
import androidx.compose.ui.graphics.toComposeImageBitmap
45-
import androidx.compose.ui.geometry.Offset
42+
import androidx.compose.ui.graphics.skiaCanvas
43+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
4644
import androidx.compose.ui.input.pointer.pointerInput
4745
import androidx.compose.ui.layout.ContentScale
4846
import androidx.compose.ui.layout.onSizeChanged
@@ -51,6 +49,7 @@ import androidx.compose.ui.text.font.FontWeight
5149
import androidx.compose.ui.text.style.TextAlign
5250
import androidx.compose.ui.unit.dp
5351
import androidx.compose.ui.unit.sp
52+
import org.jetbrains.skia.Matrix33
5453
import org.jetbrains.skia.Image as SkiaImage
5554
import org.polyfrost.oneconfig.internal.ui.components.Icon
5655
import org.polyfrost.oneconfig.internal.ui.components.LocalUiOversample
@@ -60,9 +59,7 @@ import org.polyfrost.oneconfig.internal.ui.themes.Theme
6059
import org.polyfrost.polyplus.client.PolyPlusConfig
6160
import org.polyfrost.polyplus.client.PolyPlusClient
6261
import org.polyfrost.polyplus.client.features.OnboardingFeatures
63-
import kotlinx.coroutines.delay
64-
import kotlinx.coroutines.launch
65-
import kotlinx.coroutines.coroutineScope
62+
import org.polyfrost.polyplus.client.gui.preview.UnityMotionBlur
6663
import kotlin.math.roundToInt
6764

6865
class PolyPlusOnboardingScreen : ComposeScreen(RenderMode.CONTINUOUS) {
@@ -510,9 +507,8 @@ private fun OnboardingIcon(path: String, color: Color, modifier: Modifier) = Ico
510507

511508
@Composable
512509
private fun MotionBlurPreview(strength: Int, modifier: Modifier = Modifier) {
513-
val bitmap = remember { loadOnboardingRaster(ONBOARDING_ASSETS + "motion-test.png") }
514-
var pan by remember { mutableStateOf(Offset.Zero) }
515-
var velocity by remember { mutableStateOf(Offset.Zero) }
510+
val image = remember { loadOnboardingImage(ONBOARDING_ASSETS + "motion-test.png") }
511+
val motion = UnityMotionBlur.maxSmear(strength)
516512
val shape = RoundedCornerShape(6.dp)
517513

518514
Box(
@@ -521,85 +517,39 @@ private fun MotionBlurPreview(strength: Int, modifier: Modifier = Modifier) {
521517
.background(Color(0xFF273137))
522518
.border(1.dp, Color.White, shape),
523519
) {
524-
if (bitmap != null) {
525-
Canvas(
526-
Modifier
527-
.fillMaxSize()
528-
.pointerInput(strength) {
529-
coroutineScope {
530-
var previous: Offset? = null
531-
var generation = 0
532-
awaitPointerEventScope {
533-
while (true) {
534-
val event = awaitPointerEvent()
535-
val position = event.changes.firstOrNull()?.position ?: continue
536-
val last = previous
537-
previous = position
538-
pan = Offset(
539-
((position.x / size.width) - 0.5f) * -12f,
540-
((position.y / size.height) - 0.5f) * -8f,
541-
)
542-
if (last != null) {
543-
val scale = strength.coerceIn(0, MOTION_BLUR_MAX) / MOTION_BLUR_MAX.toFloat()
544-
velocity = Offset(
545-
(position.x - last.x).coerceIn(-32f, 32f) * scale,
546-
(position.y - last.y).coerceIn(-24f, 24f) * scale,
547-
)
548-
val current = ++generation
549-
launch {
550-
delay(70L)
551-
if (current == generation) velocity = Offset.Zero
552-
}
553-
}
554-
}
555-
}
556-
}
557-
},
558-
) {
520+
if (image != null) {
521+
Canvas(Modifier.fillMaxSize()) {
559522
val overscan = 1.12f
560523
val destinationWidth = size.width * overscan
561524
val destinationHeight = size.height * overscan
562525
val destinationRatio = destinationWidth / destinationHeight
563-
val sourceRatio = bitmap.width.toFloat() / bitmap.height.toFloat()
526+
val sourceRatio = image.width.toFloat() / image.height.toFloat()
564527
val sourceWidth: Int
565528
val sourceHeight: Int
566529
if (sourceRatio > destinationRatio) {
567-
sourceHeight = bitmap.height
530+
sourceHeight = image.height
568531
sourceWidth = (sourceHeight * destinationRatio).roundToInt()
569532
} else {
570-
sourceWidth = bitmap.width
533+
sourceWidth = image.width
571534
sourceHeight = (sourceWidth / destinationRatio).roundToInt()
572535
}
573-
val sourceOffset = androidx.compose.ui.unit.IntOffset(
574-
(bitmap.width - sourceWidth) / 2,
575-
(bitmap.height - sourceHeight) / 2,
576-
)
577-
val samples = if (strength == 0 || velocity == Offset.Zero) 1 else strength.coerceIn(2, MOTION_BLUR_MAX)
578-
repeat(samples) { index ->
579-
val samplePosition = if (samples == 1) 0f else index.toFloat() / (samples - 1) - 0.5f
580-
val sampleOffset = velocity * samplePosition
581-
drawImage(
582-
image = bitmap,
583-
srcOffset = sourceOffset,
584-
srcSize = androidx.compose.ui.unit.IntSize(sourceWidth, sourceHeight),
585-
dstOffset = androidx.compose.ui.unit.IntOffset(
586-
((size.width - destinationWidth) / 2f + pan.x + sampleOffset.x).roundToInt(),
587-
((size.height - destinationHeight) / 2f + pan.y + sampleOffset.y).roundToInt(),
588-
),
589-
dstSize = androidx.compose.ui.unit.IntSize(destinationWidth.roundToInt(), destinationHeight.roundToInt()),
590-
alpha = 1f / samples,
591-
blendMode = if (index == 0) BlendMode.SrcOver else BlendMode.Plus,
592-
)
536+
val localMatrix = Matrix33
537+
.makeTranslate((size.width - destinationWidth) / 2f, (size.height - destinationHeight) / 2f)
538+
.makeConcat(Matrix33.makeScale(destinationWidth / sourceWidth, destinationHeight / sourceHeight))
539+
.makeConcat(Matrix33.makeTranslate(-(image.width - sourceWidth) / 2f, -(image.height - sourceHeight) / 2f))
540+
drawIntoCanvas {
541+
UnityMotionBlur.draw(it.skiaCanvas, image, localMatrix, size.width, size.height, motion)
593542
}
594543
}
595544
}
596-
OnboardingText("Move your mouse here to test!", 13, Modifier.align(Alignment.Center), Color(0xBFFFFFFF), FontWeight.Light)
545+
val caption = if (strength == 0) "Motion blur off" else "Maximum blur at this strength"
546+
OnboardingText(caption, 13, Modifier.align(Alignment.Center), Color(0xBFFFFFFF), FontWeight.Light)
597547
}
598548
}
599549

600-
private fun loadOnboardingRaster(path: String): ImageBitmap? = runCatching {
550+
private fun loadOnboardingImage(path: String): SkiaImage? = runCatching {
601551
val bytes = PolyPlusOnboardingScreen::class.java.getResourceAsStream("/$path")!!.use { it.readBytes() }
602-
SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
552+
SkiaImage.makeFromEncoded(bytes)
603553
}.getOrNull()
604554

605555
private data class HudSelections(
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.polyfrost.polyplus.client.gui.preview
2+
3+
import androidx.compose.ui.geometry.Offset
4+
import org.jetbrains.skia.Canvas
5+
import org.jetbrains.skia.FilterTileMode
6+
import org.jetbrains.skia.Matrix33
7+
import org.jetbrains.skia.Paint
8+
import org.jetbrains.skia.Rect
9+
import org.jetbrains.skia.RuntimeEffect
10+
import org.jetbrains.skia.RuntimeShaderBuilder
11+
import org.jetbrains.skia.SamplingMode
12+
import org.jetbrains.skia.Image as SkiaImage
13+
14+
object UnityMotionBlur {
15+
private const val MAX_BLUR = 0.08f
16+
private const val JITTER = 1.0f
17+
private const val MIN_SAMPLES = 4f
18+
19+
private const val MAX_SAMPLES = 16f
20+
21+
private const val SAMPLE_LIMIT = 32
22+
23+
/** A still camera: no smear, PolyBlur's resting sample count. */
24+
val NONE = Motion(Offset.Zero, MIN_SAMPLES)
25+
26+
private val effect: RuntimeEffect? by lazy {
27+
runCatching { RuntimeEffect.makeForShader(SKSL) }.getOrNull()
28+
}
29+
30+
fun maxSmear(strength: Int): Motion {
31+
val intensity = (strength.coerceIn(0, 10) / 10f) * MAX_BLUR
32+
if (intensity <= 1e-6f) return NONE
33+
val samples = (MIN_SAMPLES + (intensity / MAX_BLUR) * (MAX_SAMPLES - MIN_SAMPLES))
34+
.coerceIn(MIN_SAMPLES, MAX_SAMPLES)
35+
return Motion(Offset(intensity, 0f), samples)
36+
}
37+
38+
fun draw(canvas: Canvas, image: SkiaImage, localMatrix: Matrix33, width: Float, height: Float, motion: Motion) {
39+
val source = image.makeShader(FilterTileMode.CLAMP, FilterTileMode.CLAMP, SamplingMode.LINEAR, localMatrix)
40+
source.use {
41+
val blurred = effect?.let { runtime ->
42+
RuntimeShaderBuilder(runtime).use { builder ->
43+
builder.child("DiffuseSampler", source)
44+
builder.uniform("Size", width, height)
45+
builder.uniform("Velocity", motion.velocity.x, motion.velocity.y)
46+
builder.uniform("Samples", motion.samples)
47+
builder.uniform("Jitter", JITTER)
48+
builder.makeShader()
49+
}
50+
}
51+
Paint().use { paint ->
52+
paint.shader = blurred ?: source
53+
canvas.drawRect(Rect.makeWH(width, height), paint)
54+
}
55+
blurred?.close()
56+
}
57+
}
58+
59+
data class Motion(val velocity: Offset, val samples: Float)
60+
61+
private val SKSL = """
62+
uniform shader DiffuseSampler;
63+
uniform float2 Size;
64+
uniform float2 Velocity;
65+
uniform float Samples;
66+
uniform float Jitter;
67+
68+
float gnoise(float2 p) {
69+
return fract(52.9829189 * fract(dot(p, float2(0.06711056, 0.00583715))));
70+
}
71+
72+
half4 main(float2 coord) {
73+
int n = int(Samples);
74+
if (n < 2 || dot(Velocity, Velocity) < 1e-9) {
75+
return DiffuseSampler.eval(coord);
76+
}
77+
78+
float j = (gnoise(coord) - 0.5) * Jitter;
79+
80+
float4 acc = float4(0.0);
81+
float total = 0.0;
82+
for (int i = 0; i < $SAMPLE_LIMIT; i++) {
83+
if (i < n) {
84+
float t = (float(i) + 0.5 + j) / float(n) - 0.5;
85+
float w = 1.0 - abs(t) * 2.0;
86+
acc += float4(DiffuseSampler.eval(coord + Velocity * Size * t)) * w;
87+
total += w;
88+
}
89+
}
90+
91+
return total > 0.0 ? half4(acc / total) : DiffuseSampler.eval(coord);
92+
}
93+
""".trimIndent()
94+
}

0 commit comments

Comments
 (0)