- Home /
RenderTexture Painting - color blending issues
I am trying to implement brush painting, very similar to what photoshop has. I have been "mostly" successful, except that I am having some issues with blending and I'm not really sure how this needs to be addressed.
Painting technique (C#)
RenderTexture.active = paintRenderTexture;
GL.PushMatrix();
GL.LoadPixelMatrix(0, paintRenderTexture.width, 0, paintRenderTexture.height);
Graphics.DrawTexture(rect, brushTexture, paintMaterial);
GL.PopMatrix();
RenderTexture.active = null;
Paint Shader - Shader used to render the brush in the render texture
Shader "Unlit/TexturePainter/Blit"
{
Properties
{
_MainTex("MainTex", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType" = "Opaque" }
Pass
{
Blend One OneMinusSrcAlpha
Lighting Off
Cull Off
ZTest Always
ZWrite Off
...
fixed4 frag(v2f i) : SV_Target
{
fixed4 brush = tex2D(_MainTex, i.texcoord0) * _Color;
return fixed4(_Color.rgb * brush.a, brush.a);
}
ENDCG
}
}
}
Preview shader - This shader is used to render the final image on the quad.
Shader "Unlit/TexturePainter/Preview"
{
Properties
{
_MainTex( "Texture", 2D ) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Transparent" }
Pass
{
Blend One OneMinusSrcAlpha
ZWrite Off
...
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return fixed4(col.rgb, col.a);
}
ENDCG
}
}
}
Blending issue GIF - If you focus on the area where I am overpainting on one spot, you can see the artifacts start forming around the edges of the brush texture (areas where alpha is not 100%)
Conclusion
've also tried using separate blending modes for RGB and for A channels, but I was mostly unsuccessful. While I got rid of the issue I mentioned above, I had a different issue where alpha values weren't blended correctly. Is it even possible to achieve what I want using this technique, or is there another way this needs to be aproached?
Any help on this issue will be greatly appreciated :)
Answer by Eno-Khaon · Dec 20, 2020 at 08:23 PM
Back before Unity 5 (and, therefore, before the "free" version supported many features like RenderTextures), I self-taught myself color blending by making a texture-painting script.
Why the preface?
It meant I was blending all the colors without using shaders.
The problem you're running into is that you're pre-multiplying the alpha into the color you're drawing onto your surface, so the edges (as you mentioned) of the brush wind up the wrong color. Pre-multiplying color is only important in the context of having no alpha channel support. Rather, pre-multiplying is how you get the resulting color to display on screen in certain context, but it's not universally necessary and definitely isn't necessary in Unity, where textures are (generally speaking) 32-bit, containing alpha data.
In regard to my aforementioned "old project", the main function I used for complex color blending was:
// Transcribed from Unityscript, so apologies ahead of time for any typos
public static Color Blend(Color current, Color new)
{
Color output;
// Blended alpha values with a maximum of 1
// Example: 0.5 background + 0.5 brush = 0.75 output alpha
output.a = current.a + (new.a * (1.0f - current.a));
if(output.a == 0.0f)
{
return new Color(current.r, current.g, current.b, 0f);
}
else
{
// Example: White to Black, 0.5 alpha on background (current) and brush (new)
// ((0.0 * 0.5) / 0.75) + ((1.0 * 0.5 * (1-0.75)) / 0.75)
// ((0) + ((0.5 * 0.25) / 0.75)
// ~0.1666 final pixel channel color
output.r = ((new.r * new.a) / output.a) + ((current.r * current.a * (1 - new.a)) / output.a);
output.g = ((new.g * new.a) / output.a) + ((current.g * current.a * (1 - new.a)) / output.a);
output.b = ((new.b * new.a) / output.a) + ((current.b * current.a * (1 - new.a)) / output.a);
}
return output;
}
This is made on the basis of blending a non-zero opacity new pixel color with any existing pixel, where the opacity of the existing pixel influences how much priority the color of the new pixel will have. This is also weighted with an additional step over a shader's Blend SrcAlpha OneMinusSrcAlpha
approach to transparency blending by dividing by the output color to determine the new resulting color with high accuracy, but again, this is just part of the process of doing all the blending manually.
Now, having said all that, when you're drawing a texture on top of another one through a shader, you would generally be able to just rely on that blending. At a glance, and by my own personal impression, it looks like you have your role backwards for the brush, in that it's your opaque shader. Furthermore, you're also double-multiplying its values, by combining both tex2D(_MainTex, i.texcoord0) * _Color
and fixed4(_Color.rgb * brush.a
(factoring in the _Color value in both scenarios).
If you make your first shader Unlit/TexturePainter/Blit
transparent ("RenderType" and "Queue", most importantly), blend using Blend SrcAlpha OneMinusSrcAlpha
and remove the rgb pre-multiplication (i.e. _Color), I imagine that would solve most of the problems in this situation.
Hey, first of all, thank you for the answer, but I'm afraid simply changing blending to standard alpha blending doesn't solve the issue. In fact this is what I started off with in the first place. Sorry I didn't mention this, I actually tried a lot of stuff before making this post. The technique I explained in the post was just the one that gave the best results.
I would also like to note that having RenderType
set to Opaque
doesn't have any actual effect in my case, since this is just used for replacement shaders. Same goes with Queue
not being set to Transparent
.
I would also like to note that what I am doing with premultiplied blending is correct (and by correct I mean how a premultiplied alpha shader should look like). In my case, I'm actually using the texture only for the alpha value, therefore the final formula looks the way it does: fixed4 out = fixed4(_Color.rgb * brush.a, brush.a);
Here is how it looks if I change it to SrcAlpha One$$anonymous$$inusSrcAlpha
blending.
Unfortunately, your image doesn't seem like it uploaded properly in your reply (on that note, I'm not really sure why they're so finicky, but it's nothing new).
I threw together a few simple testing scripts to try and recreate the problem/effect you're experiencing on the "brush edges" with no luck yet. Perhaps it means there's more to this than meets the eye.
That said, here is the approach I used:
// Attached to a camera
using UnityEngine;
public class BrushTest : $$anonymous$$onoBehaviour
{
public Texture2D brushImage;
public $$anonymous$$aterial brush$$anonymous$$at;
Vector2 drawOffset;
Texture2D screenTex;
RenderTexture rt;
bool drawing = false;
void Start()
{
drawOffset = new Vector2(brushImage.width, brushImage.height) * 0.5f;
screenTex = new Texture2D(Screen.width, Screen.height);
rt = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
}
private void Update()
{
if(Input.Get$$anonymous$$ouseButtonDown(0))
{
drawing = true;
}
else if(Input.Get$$anonymous$$ouseButtonUp(0))
{
drawing = false;
}
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
RenderTexture activeRT = RenderTexture.active;
RenderTexture.active = rt;
if(drawing)
{
GL.Push$$anonymous$$atrix();
GL.LoadPixel$$anonymous$$atrix(0, Screen.width, 0, Screen.height);
Graphics.DrawTexture(new Rect(Input.mousePosition.x - drawOffset.x, Input.mousePosition.y - drawOffset.y, brushImage.width, brushImage.height), brushImage, brush$$anonymous$$at);
GL.Pop$$anonymous$$atrix();
}
Graphics.Blit(rt, destination);
RenderTexture.active = activeRT;
}
}
Shader "Unlit/BrushBlend"
{
Properties
{
_$$anonymous$$ainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Blend SrcAlpha One$$anonymous$$inusSrcAlpha
Cull Off
ZWrite Off
CGPROGRA$$anonymous$$
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _$$anonymous$$ainTex;
float4 _$$anonymous$$ainTex_ST;
float4 _Color;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFOR$$anonymous$$_TEX(v.uv, _$$anonymous$$ainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_$$anonymous$$ainTex, i.uv);
return fixed4(_Color.rgb, col.a);
}
ENDCG
}
}
}
(Ran out of characters in the previous comment)
For my test, I attached the BrushTest script to my main camera, created a material using the shader and set that on the script, then used "Default-Particle" as my "brushImage" Texture2D value.
Of note, using Blend One One$$anonymous$$inusSrcAlpha
with pre-multiplied alpha on the colors did have a similar-or-same visual output as Blend SrcAlpha One$$anonymous$$inusSrcAlpha
without factoring in alpha, since the point of pre-multiplying *IS* related to influencing the final resulting value (I didn't exactly mean to sound quite so vehemently against the idea). I mainly draw attention to it because it could easily become misleading in how it relies on such a specific blending mode as well.
In the shader, I also set the color to have ~50% opacity.
That said, I wasn't able to reproduce the problem you're running into. Are there any other details you can provide that might make it stand out?
Edit: typo
I put the code you posted in my Unity project and the result is actually the same as in the previously failed uploaded video of the result.
Here is the result using SrcAlpha One$$anonymous$$inusSrcAlpha