2018/12/14日補充:后來發現compute shader里用AppendStructuredBuffer可以解決這類問題,請看這里:https://www.cnblogs.com/hont/p/10122129.html
1.簡介
在日常開發中會遇到諸如判斷某張圖的某顏色像素百分比占多少的問題,由於gpu運算並行的原因並不能對其進行累加操作。網上一些針對此類問題
的做法是將一張大圖分成多個小塊逐步處理並逐步合並,保留關鍵像素的向下采樣:
但我在思考一種更簡便的方法,於是想到在頂點shader里做判斷檢測,在像素shader里獲取結果這樣一個形式:
用一組頂點去讀單個像素,判斷失敗的頂點坐標提交到屏幕外,而判斷成功的頂點坐標放在屏幕內。
最后在CPU中獲取是否有屏幕內頂點這樣一個結果,來進行簡單的識別操作。
而在開啟透明之后,還可以用透明度疊加來獲取更復雜的結果。
2.實踐
首先實踐結果並沒有想象的那么好,因為如果純用三角面來做頂點部分的判斷未免太費效率了。
所以我改成了傳入頂點判斷並生成面的方式,並且縮小了傳入圖片的像素大小。
Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1);
畢竟更多的運用場合是用來做刮刮卡或者擦除的識別。只需要檢測mask圖片。
上代碼:

Shader "Hidden/FooShader" { Properties { } SubShader { Blend One One tags { "Queue" = "Transparent" "RenderType" = "Transparent" } Pass { CGPROGRAM #pragma target 4.0 #pragma vertex vert #pragma geometry geom #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 color : COLOR; float4 vertex : SV_POSITION; }; sampler2D _Image; float4 _ImageSize; v2f vert(uint vid : SV_VertexID) { v2f o = (v2f)0; half y = floor(vid / _ImageSize.x); half x = (vid - y * _ImageSize.x) / _ImageSize.x; y = y / _ImageSize.y; o.vertex = 0; float4 image_col = tex2Dlod(_Image, half4(x,y,0,0)); if (all(image_col.rgb == half3(0, 0, 1))) //if (all(image_col.rgb == half3(0, 1, 1))) /*error*/ { o.color = 1; } else { o.color = 0; } return o; } [maxvertexcount(4)] void geom(point v2f vertElement[1], inout TriangleStream<v2f> triStream) { if (vertElement[0].color.r <= 0) return; float size = 10; float4 v1 = vertElement[0].vertex + float4(-size, -size, 0, 0); float4 v2 = vertElement[0].vertex + float4(-size, size, 0, 0); float4 v3 = vertElement[0].vertex + float4(size, -size, 0, 0); float4 v4 = vertElement[0].vertex + float4(size, size, 0, 0); v2f r = (v2f)0; r.vertex = mul(UNITY_MATRIX_VP, v1); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v2); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v3); r.color = vertElement[0].color; triStream.Append(r); r.vertex = mul(UNITY_MATRIX_VP, v4); r.color = vertElement[0].color; triStream.Append(r); } fixed4 frag(v2f i) : SV_Target { return i.color; } ENDCG } } }

using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace Hont { public class Foo : MonoBehaviour { void Start() { var blueTex = new Texture2D(64, 64); for (int x = 0; x < blueTex.width; x++) for (int y = 0; y < blueTex.height; y++) blueTex.SetPixel(x, y, Color.blue); blueTex.Apply(); var mat = new Material(Shader.Find("Hidden/FooShader")); mat.SetTexture("_Image", blueTex); mat.SetVector("_ImageSize", new Vector4(blueTex.width, blueTex.height)); mat.SetPass(0); var tempRT = RenderTexture.GetTemporary(16, 16, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB, 1); tempRT.filterMode = FilterMode.Point; tempRT.autoGenerateMips = false; tempRT.anisoLevel = 0; tempRT.wrapMode = TextureWrapMode.Clamp; var cacheRT = RenderTexture.active; RenderTexture.active = tempRT; Graphics.DrawProcedural(MeshTopology.Points, blueTex.width * blueTex.height, 1); var tex2D = new Texture2D(16, 16, TextureFormat.ARGB32, false, false); tex2D.wrapMode = TextureWrapMode.Clamp; tex2D.anisoLevel = 0; tex2D.filterMode = FilterMode.Point; tex2D.ReadPixels(new Rect(0, 0, 16, 16), 0, 0); var firstPixel = tex2D.GetPixel(0, 0); Debug.Log("firstPixel: " + firstPixel); RenderTexture.active = cacheRT; RenderTexture.ReleaseTemporary(tempRT); } } }
跑了一下代碼之后我發現了三個問題,也是沒解決的問題,一個是計算結果有誤差
o.color = float4(0.05, 0, 0, 0);
輸出是0.05結果卻有一些出入。
特別是當返回顏色小於0.1之后,我嘗試改變圖像格式或者RT等參數依舊沒能解決
第二個問題是開啟透明后,透明圖片的疊加是有上限的,畢竟深度有限,堆疊二十多層后,后面層會丟失。
第三個問題是傳入圖片尺寸過大直接導致帶寬爆炸,以至於unity直接假死了,512x512的圖片就是26萬多的像素要處理,也就是26萬多的頂點。
第三個問題很好解決,控制圖片尺寸+讓單個頂點采樣更多像素即可。
對於第一個問題,目前還不需要太精確所以沒解決但也能用。第二個問題可以用一些方法來緩解
比如在頂點shader中增加運算量,把返回值分散到rgba四個通道上去。
uint roll = (roll_width + roll_height) % 4; if (roll == 0) result = float4(GAIN_VALUE, 0, 0, 0); if (roll == 1) result = float4(0, GAIN_VALUE, 0, 0); if (roll == 2) result = float4(0, 0, GAIN_VALUE, 0); if (roll == 3) result = float4(0, 0, 0, GAIN_VALUE);
把更多的像素遍歷放入頂點中,這樣處理圖片的頂點數量是原大小/n:
v2f vert(uint vid : SV_VertexID) { v2f o = (v2f)0; o.vertex = 0; half2 image_size = half2(GRID_SIZE_X * LOOP_IMAGE_SIZE_X, GRID_SIZE_Y * LOOP_IMAGE_SIZE_Y); half y = floor(vid / LOOP_IMAGE_SIZE_X); half x = (vid - y * LOOP_IMAGE_SIZE_X) / LOOP_IMAGE_SIZE_X; y = y / LOOP_IMAGE_SIZE_Y; //將vid轉化為x,y坐標 for (half rx = 0; rx < GRID_SIZE_X; rx++) { for (half ry = 0; ry < GRID_SIZE_Y; ry++) { half xx = x + rx; half yy = y + ry; float4 r = Statistics_sample(_Image, _Rec_Color, half4(xx, yy, 0, 0), image_size); o.color += r; } } //一個頂點處理多個像素 return o; }
3.測試結果
最終達到了一個比較不錯的結果,我把相關函數封裝成了一個類。
我寫了一個塗抹效果demo來測試一下,它通過識別白色像素的數量來判斷是否為全部塗完:
工程文件我丟在了github上: https://github.com/hont127/Image-Rec-Base-unity-shader-
通過這個小Trick其實可以在像素里返回更多的信息,簡單的場合這么還是比較方便的,當然一些復雜的情況分塊或者配合computer shader來做其實更合適。