How can I apply BlendMode to composable? I know ho...
# compose
p
How can I apply BlendMode to composable? I know how to add it using canvas, but I want to apply it on composable. I am playing around with shaders and I do not want to add BlendMode
color_dodge
to blend this shader: https://www.shadertoy.com/view/MdjfRK into transparent background version. I want the blaks to disappear, but do not loose the details of color noise.
r
You can just output transparent pixels instead of using a blend mode
i.e. where you output black, output
vec4(0.0)
instead (fully transparent)
t
Maybe also interesting for you to know: https://skia.org/docs/user/sksl/#premultiplied-alpha But you need to change the shader code a little bit for that. But it is possible:
For your shader maybe something like that:
Copy code
float grad = pow((r.y + r.y) * max(.0, uv.y) + .1, 4.0);
    color = ramp(grad);
    color /= (1.50 + max(vec3(0), color));
    float alpha = 1.0-grad;              // <-- you could use the grad value for alpha
    return vec4(color * alpha, alpha);   // <-- premultiply the color. It does not really make a big difference i think.
image.png
p
cool. It works. It is really better now. However it still do not give the same effect that you can get from simple blendMode in figma. Bottom image represent the shader without blendMode Top image has blendMode.Screen in figma. It blends nicely into the screen (view). I want to achive the same effect in android.
t
Can you show how it looks looks in Android please? When you are using the graphics layer you can get the background as an other shader. And than you can do what ever you want to blend things on each other.
Copy code
fun Modifier.rainEffect() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this.composed {
    val shader = remember { RuntimeShader(SHADER_RAIN) }
    val time by produceState(0f) {
        while (true) {
            withInfiniteAnimationFrameMillis {
                value = it / 1000f
            }
        }
    }
    this.graphicsLayer {
        // Pass the color to support color space automatically
        shader.setFloatUniform("iResolution", density * 400, density * 400)
        shader.setFloatUniform("iTime", time)

        renderEffect = RenderEffect.createRuntimeShaderEffect(shader, "background")
            .asComposeRenderEffect()
        //renderEffect = RenderEffect.createBlurEffect(5f, 5f, Shader.TileMode.CLAMP).asComposeRenderEffect()
    }
} else Modifier
In shader code you get the
uniform shader background;
. Than you do not need to use alpha color at all. And can do blending yourself.
If you are using this you lose the ability to use your shader on earlier android version than 13. Because you could also use opengl to show shaders as background but you never get the background as a shader in opengl. At least as far as i know.
Also why not using canvas calls to draw the shader? As you already mentioned there you can configure the Blendmode.
p
the code you posted is almost identical to mine. It didnt work. Finally I found the parameter of transparency. In shader, the result color was:
Copy code
return vec4(color, 1.0);
and I have change that to:
Copy code
return vec4(color, 0.0);
which returns transparent for all places where there is no noise function.
t
try this what i posted:
Copy code
float alpha = 1.0-grad;              // <-- you could use the grad value for alpha
 return vec4(color * alpha, alpha);   // <-- premultiply the color. It does not really make a big difference i think.
p
The alpha thing adds this red mask. weird 😕
t
Not sure. I did not really looked to deep into the shader code. In my tests i did not had this red thing. Maybe the grad value is not what i thought.
Maybe can you show your Compose code? Also still i think you could also do it with canvas drawRect and use your shader. There you can specify the blend mode.
p
here is the code:
Copy code
package com.piotrprus.flameshader

import android.graphics.RenderEffect
import android.graphics.RuntimeShader
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asComposeRenderEffect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.piotrprus.flameshader.ui.theme.FlameShaderTheme

const val GlowSource = """
   uniform shader glowComposable;
   uniform float2 iResolution;          // viewport resolution (in pixels)
   uniform float iTime;                 // shader playback time (in seconds)
   
float rand(vec2 n) {
    return fract(sin(dot(n, vec2(12.9898,12.1414))) * 83758.5453);
}

float noise(vec2 n) {
    const vec2 d = vec2(0.0, 1.0);
    vec2 b = floor(n);
    vec2 f = mix(vec2(0.0), vec2(1.0), fract(n));
    return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);
}

vec3 ramp(float t) {
    return t <= .5 ? vec3( 1. - t * 1.4, .2, 1.05 ) / t : vec3( .3 * (1. - t) * 2., .2, 1.05 ) / t;
}

float fire(vec2 n) {
    return noise(n) + noise(n * 2.1) * .6 + noise(n * 5.4) * .42;
}

vec4 main(vec2 fragCoord) {
    
    float t = iTime;
    vec2 uv = fragCoord / iResolution.y;
    
    uv.x += uv.y < .5 ? 23.0 + t * .35 : -11.0 + t * .3;
    uv.y = abs(uv.y - 0.5);
    uv *= 2.5;
    
    float q = fire(uv - t * .013) / 2.0;
    vec2 r = vec2(fire(uv + q / 2.0 + t - uv.x - uv.y), fire(uv + q - t));
    vec3 color = vec3(1.0 / (pow(vec3(0.5, 0.0, .1) + 1.61, vec3(4.0))));
    
    float grad = pow((r.y + r.y) * max(.0, uv.y) + .1, 4.0);
    color = ramp(grad);
    color /= (1.50 + max(vec3(0), color));
    
    float alpha = 1.0-grad;              // <-- you could use the grad value for alpha
    return vec4(color * alpha, alpha);   // <-- premultiply the color. It does not really make a big difference i think.
//    return vec4(color, 0.0);
}
"""

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FlameShaderTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainScreen()
                }
            }
        }
    }
}

@Composable
fun MainScreen() {
    Box(
        modifier = Modifier
            .fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Image(
            painter = painterResource(id = R.drawable.vert_image),
            contentDescription = null,
            modifier = Modifier.fillMaxSize()
        )
        Box(modifier = Modifier.width(200.dp)) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(35.dp)
                    .flameEffect()
                    .background(Color.Black)
            )
        }
    }

}

fun Modifier.flameEffect() =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this.composed {
        var size by remember { mutableStateOf(IntSize.Zero) }
        val shader = remember { RuntimeShader(GlowSource) }
        val time by produceState(0f) {
            while (true) {
                withInfiniteAnimationFrameMillis {
                    value = it / 1000f
                }
            }
        }
        this
            .onPlaced { layoutCoordinates ->
                size = layoutCoordinates.size
            }
            .graphicsLayer {
                if (size != IntSize.Zero) {
                    // Pass the color to support color space automatically
                    shader.setFloatUniform(
                        "iResolution",
                        size.width.toFloat(),
                        size.height.toFloat()
                    )
                    shader.setFloatUniform("iTime", time)
                    clip = true
                    renderEffect = RenderEffect
                        .createRuntimeShaderEffect(shader, "glowComposable")
                        .asComposeRenderEffect()
                }
            }
    } else Modifier
I really appreciate your help 😉 thank you.
t
You are welcome. I am currently working on exploring the possibilities of AGSL shaders in my Android and Desktop apps. Btw. i also did wrote a small wrapper to make it possible to use this shaders in older Android version with some limitations.
p
if you could share this wrapper that would be cool. And, also the canvas method that you were talking about. I would like to see if it works for my usecase
t
Copy code
@RequiresApi(33)
fun Modifier.backgroundShader(shaderSrc: String): Modifier = this.drawWithCache {
    val shader = RuntimeShader(shaderSrc)
    val brush = ShaderBrush(shader)
    onDrawBehind {
        drawRect(brush, blendMode = BlendMode.Screen)
    }
}
This opengl wrapper is still under havy development. If you want you can follow this project: https://github.com/timo-drick/compose_libraries/tree/main/opengl_pixel_shader
I rewrote you flameEffect modifier. For me it works now. But you need to remove the background in your MainScreen where you use the .background:
Copy code
fun Modifier.flameEffect2() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) this.composed {
    val shader = remember { RuntimeShader(GlowSource) }
    val time by produceState(0f) {
        while (true) {
            withInfiniteAnimationFrameMillis {
                value = it / 1000f
            }
        }
    }
    this.drawWithCache {
        val brush = ShaderBrush(shader)
        shader.setFloatUniform("iResolution", size.width, size.height)
        shader.setFloatUniform("iTime", time)
        onDrawBehind {
            drawRect(brush)
        }
    }
} else this.background(Color.Black)
image.png
You could also change the line after onDrawBehind to
drawRect(brush, blendMode = BlendMode.Screen)
Than you do not need to change the shader code. So you can always set the color to `return vec4(color, 1.0)`;
Than it looks like this:
p
Thanks a lot! Another learning. I had no idea I can put shader in the
brush
🙂 😂
799 Views