First, please allow me to explain what I've got and then I'll go over what I'm trying to figure out next.
What I've got
I've got a textured custom mesh with some edges that exactly align with integer world coordinates in Unity. To the mesh I've added my own crude yet effective custom surface shader that looks like this:
Shader "Custom/GridHighlightShader"
{
Properties
{
[HideInInspector]_SelectionColor("SelectionColor", Color) = (0.1,0.1,0.1,1)
[HideInInspector]_MovementColor("MovementColor", Color) = (0,0.205,1,1)
[HideInInspector]_AttackColor("AttackColor", Color) = (1,0,0,1)
[HideInInspector]_GlowInterval("_GlowInterval", float) = 1
_MainTex("Albedo (RGB)", 2D) = "white" {}
_Glossiness("Smoothness", Range(0,1)) = 0.5
_Metallic("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
struct Input
{
float2 uv_MainTex;
float3 worldNormal;
float3 worldPos;
};
sampler2D _MainTex;
half _Glossiness;
half _Metallic;
fixed4 _SelectionColor;
fixed4 _MovementColor;
fixed4 _AttackColor;
half _GlowInterval;
half _ColorizationArrayLength = 0;
float4 _ColorizationArray[600];
half _isPixelInColorizationArray = 0;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf(Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
// Update only the normals facing up and down
if (abs(IN.worldNormal.x) <= 0.5 && (abs(IN.worldNormal.z) <= 0.5))
{
// If no colors were passed in, reset all of the colors
if (_ColorizationArray[0].w == 0)
{
_isPixelInColorizationArray = 0;
}
else
{
for (int i = 0; i < _ColorizationArrayLength; i++)
{
if (abs(IN.worldPos.x) >= _ColorizationArray[i].x && abs(IN.worldPos.x) < _ColorizationArray[i].x + 1
&& abs(IN.worldPos.z) >= _ColorizationArray[i].z && abs(IN.worldPos.z) < _ColorizationArray[i].z + 1
)
{
_isPixelInColorizationArray = _ColorizationArray[i].w;
}
}
}
if (_isPixelInColorizationArray > 0)
{
if (_isPixelInColorizationArray == 1)
{
c = tex2D(_MainTex, IN.uv_MainTex) + (_SelectionColor * _GlowInterval) - 1;
}
else if (_isPixelInColorizationArray == 2)
{
c = tex2D(_MainTex, IN.uv_MainTex) + (_MovementColor * _GlowInterval);
}
else if (_isPixelInColorizationArray == 3)
{
c = tex2D(_MainTex, IN.uv_MainTex) + (_AttackColor * _GlowInterval);
}
}
}
o.Albedo = c.rgb;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
Into the shader I feed a float that simply oscilates between 2 and 3 over time using some maths, this is done from a simple update function in Unity:
private void Update()
{
var t = (2 + ((Mathf.Sin(Time.time))));
meshRenderer.material.SetFloat("_GlowInterval", t);
}
I also feed the shader an array of Vector4 called _ColorizationArray that stores 0 to 600 coordinates, each representing a tile to be colored at runtime. These tiles may or may not be highlighted depending on their selectionMode value at runtime. Here's the method I'm using to do that:
public void SetColorizationCollectionForShader()
{
var coloredTilesArray = Battlemap.Instance.tiles.Where(x => x.selectionMode != TileSelectionMode.None).ToArray();
// https://docs.unity3d.com/ScriptReference/Material.SetVectorArray.html
// Set the tile count in the shader's own integer variable
meshRenderer.material.SetInt("_ColorizationArrayLength", coloredTilesArray.Length);
// Loop through the tiles to be colored only and grab their world coordinates
for(int i = 0; i < coloredTilesArray.Length; i++)
{
// Also grab the selection mode as the w value of a float4
colorizationArray[i] = new Vector4(coloredTilesArray[i].x - Battlemap.HALF_TILE_SIZE, coloredTilesArray[i].y, coloredTilesArray[i].z - Battlemap.HALF_TILE_SIZE, (float)coloredTilesArray[i].selectionMode);
}
// Feed the overwritten array into the shader
meshRenderer.material.SetVectorArray("_ColorizationArray", colorizationArray);
}
And the result is this blue glowy set of tiles that are set and changed dynamically at runtime:
My goal with all of this is to highlight squares (or tiles, if you will) on a mesh as part of a grid-based tactical game where units can move to any tile within the highlighted area. After each unit moves it can then undergo an attack where tiles are highlighted red, and then the next unit takes it's turn and so on. Since I expect AI, and movement calculations, and particle effects to take up the majority of processing time I need to highlight the tiles both dynamically and very efficiently at runtime.
What I'd like to do next
Whew, ok. Now, if you know anything about shaders (which I certainly don't, I only started looking at cg code yesterday) you're probably thinking "Oh dear god what an inefficient mess. What are you doing?! If statements?! In a shader?" And I wouldn't blame you.
What I'd really like to do is pretty much the same thing, only much more efficiently. Using specific tile indices I'd like to tell the shader "color the surface blue inside of these tiles, and these tiles only" and do it in a way that is efficient for both the GPU and the CPU.
How can I achieve this? I'm already computing tile world coordinates in C# code and providing the coordinates to the shader, but beyond that I'm at a loss. I realize I should maybe switch to a vertex/frag shader, but I'd also like to avoid losing any of the default dynamic lighting on the mesh if possible.
Also, is there a type of variable that would allow the shader to color the mesh blue using the local mesh coordinates rather than world coordinates? Would be nice to be able to move the mesh around without having to worry about shader code.
Edit: In the 2 weeks since posting this question I've edited the shader by passing in an array of Vector4s and a half to represent how much of the array to actually process, _ColorizationArrayLength
, it works well, but is hardly more efficient - this is producing GPU spikes that take about 17ms to process on a fairly modern graphics card. I've updated the shader code above as well as portions of the original question.
tex2d
and get rid of the branching. But as an alternative to iterating in shader at all, I've submitted an answer that uses texture sampling instead of iterative indexing. That should be much faster in shader. – Pandean