- Home /
Need to improve 2D circle shader performance
Hi, I'm working on a mobile game. I need resizeable rings (not stroke size but radius). I've written a shader works great but shows poor performance on mobile phones.
I set shader's float array(RadiusArray) from outside once in a while to add more circles. However whenever I add new circle, it gets slower and slower until I remove all circles from the array. Array has two information about a circle: Radius and Opacity.
Is there any way that I don't have to use for-loop like matrix multiplication etc.? Or any tips for optimization?
Thank you.
Here is how it looks;
Shader "Custom/CircleShader" {
Properties{
_Color("Color", Color) = (1,1,0,0)
_Thickness("Thickness", Range(0.0,0.5)) = 0.05
_Radius("Radius", Range(0.0, 0.5)) = 0.4
}
SubShader{
//Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" }
Pass{
Blend SrcAlpha OneMinusSrcAlpha // Alpha blending
//Cull Off
//Lighting Off
//ZWrite Off //Make On for background
//Fog{ Mode Off }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
fixed4 _Color; // low precision type is usually enough for colors
fixed _Thickness;
fixed _Radius;
fixed RadiusArray[80]; //Coming from outside
struct appdata_t
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct fragmentInput {
fixed4 pos : POSITION;
fixed2 uv : TEXCOORD0;
};
fragmentInput vert(appdata_t v)
{
fragmentInput o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy - fixed2(0.5,0.5);
return o;
}
// r = radius
// d = distance
// t = thickness
fixed antialias(fixed d, fixed t) {
fixed innerRadius = 0;
fixed outerRadius = 0;
fixed totalPixelOpacity = 0;
fixed circleOpacity = 0.0;
fixed currentcircleOpacity = 0.0;
fixed radius;
int circleCount = (int)RadiusArray[0];
for (int ri = circleCount * 2; ri > 0; ri -= 2)
{
radius = RadiusArray[ri - 1];
if (radius < 0 || totalPixelOpacity >= 1) //Last item
break;
innerRadius = radius - 0.5*t;
outerRadius = radius + 0.5*t;
circleOpacity = RadiusArray[ri];
currentcircleOpacity =
1 - (1 - (smoothstep(innerRadius - 0.003,
innerRadius,
d))
+ (smoothstep(outerRadius,
outerRadius + 0.003,
d)));
currentcircleOpacity *= circleOpacity;
totalPixelOpacity += currentcircleOpacity;
}
return totalPixelOpacity > 1 ? 1 : totalPixelOpacity;
}
fixed4 frag(fragmentInput i) : SV_Target{
fixed distance = 0;
distance = length(fixed2(i.uv.x, i.uv.y));
return fixed4(_Color.r, _Color.g, _Color.b, _Color.a*antialias(distance, _Thickness));
}
ENDCG
}
}
}
Answer by Glurth · Mar 04, 2017 at 05:49 PM
Rather than specify the circles, and compute the colors , inside the shader, I would recommend you pass a texture, 1 pixel wide, to the shader instead.
You can compute and generate (and assign to the shader) this texture ONCE, only when you CHANGE the number of circles, their radius', or thicknesses, using a regular c# class. In this class you would loop though all the possible "distance" values, and assign the resultant color to the 1 pixel wide texture. The color would be stored at the uv.y coordinate equal to the distance.
Then, your fragment shader will only need to lookup the pixel color (via UV) of this texture based upon your fragment-shader-computed "distance" value.
So, if your texture was say 1 pixel wide (in uv.X direction), you could use this to get the pixel color in your fragment shader:
distance = length(fixed2(i.uv.x, i.uv.y));
fixed4 color = tex2D(_MainTex, float2(0,distance));
Alternatively, you can use this method (compute texture outside of shader, only on changes) to generate a FULL texture (not one pixel), and just assign it to a standard shader.
Such an interesting solution. I'm having hard times to understand it since I'm a beginner on Unity and shaders. Let me tell you what I understood so far; you see circles as one dimension objects which is true from symmetry perspective. And you put all possible colors(alphas in my case) in one dimension array starting from the center of the circle. And in the fragment shader you pull the color only in the given distance value.
So, if I have a circle with 4 pixel radius, the one dimension array(just showing alpha values) would look like this; {0,0,0,1,0,0, ...}
Please let me know if I got it wrong.
After I implement this and see it working, I'll let you know and accept the answer. Thank you very much.
Such a performance improvement. Brilliant idea. Thank you very much.
So the basic code looks like;
public void SetTexture(float[] radiusList)
{
Texture2D texture = new Texture2D(1, 500);
// Reset all pixels color to transparent
Color32 resetColor = new Color32(0, 0, 0, 0);
Color32[] resetColorArray = texture.GetPixels32();
for (int i = 0; i < resetColorArray.Length; i++)
{
resetColorArray[i] = resetColor;
}
texture.SetPixels32(resetColorArray);
int circleCount = (int)radiusList[0];
for (int ri = circleCount * 2; ri > 0; ri -= 2)
{
float radius = radiusList[ri - 1] / 0.5f * 250;
texture.SetPixel(0, (int)$$anonymous$$ath.Floor(radius), new Color32(0, 0, 0, 255));
}
texture.Apply();
Renderer rend = gameObject.GetComponent<Renderer>();
rend.material.SetTexture("_$$anonymous$$ainTex", texture);
}
Don't use "SetPixel". It's slow and inefficient. Since you already have an array of colors, change the color in that array and use SetPixels or SetPixels32. Also you might want to cache your texture and the color array and reuse it. Currrently you recreate a new texture and array everytime. Also note that Textures have to be destroyed when you no longer use them. Otherwise they stay in memory until you call Resources.UnloadUnusedAssets.
I'm just mentioning those points as your main question was how to improve performance ^^.
So just create the texture and the Color32 array in Start. Something like that:
Texture circleTex;
Color32[] circleColors;
static Color32 blank = new Color32(0,0,0,0);
static Color32 black = new Color32(0,0,0,255);
void Start()
{
circleTex = new Texture2D(1, 500);
circleColors = new Color32[500];
Renderer rend = gameObject.GetComponent<Renderer>();
rend.material.SetTexture("_$$anonymous$$ainTex", circleTex);
}
public void SetTexture(float[] radiusList)
{
for (int i = 0; i < circleColors.Length; i++)
{
circleColors[i] = blank;
}
int circleCount = (int)radiusList[0];
for (int ri = circleCount * 2; ri > 0; ri -= 2)
{
float radius = radiusList[ri - 1] / 0.5f * 250;
circleColors[ (int)$$anonymous$$ath.Floor(radius) ] = black;
}
texture.SetPixels32(circleColors);
texture.Apply();
}
Btw: Your use of the "radiusList" is certainly "creative" but not a particular good approach. You may simply use a List ins$$anonymous$$d. It internally uses an array. You can simply add and remove elements. If the capacity of the List is large enough it won't allocate any new memory when you add / remove items. A List has a "Count" property so no need for storing the count as float ^^.
The array thing was designed for SetFloatArray and apparently it's time to change it. I had made all changes except Destroy (I recreate texture only when resolution changes). Very good tips indeed. Thank you :)
When I put 1000 px height texture, I can only use its half (500 px). Is there any way to say shader that its width is 1000 pixel and set its texture only 500 px?
Hope this makes it more clear;
Your answer
Follow this Question
Related Questions
weird shader problem (correct in editor, weird ingame) 2 Answers
LineRenderer shows up behind transparent shaders 0 Answers
How to prevent shader overlap in LWRP Shadergraph. 0 Answers
Skybox color pls help! 0 Answers
LightingLambert': cannot implicitly convert from 'half3' to 'struct UnityGI' 0 Answers