Unity Compute Shaders遍历大位图异常:EU4地图生成复刻排查
我正在复刻《Europa Universalis IV》的地图生成系统,游戏使用5632×2048的位图标识省份(每个省份对应唯一颜色)。我计划通过Compute Shader遍历位图,将匹配目标颜色的像素坐标缓存到数组中,但调试时发现输出数组索引0的计数始终为0,导致后续C#循环无法执行。
设计逻辑:
- 使用32×32的线程组,每个线程负责遍历176×64的区域
- C#端将输出数组设为10000长度,索引0存储匹配到的像素总数
- Shader从索引1开始写入像素坐标,同步更新索引0的计数
相关代码
C#代码
struct Int2 { public int x, y; } private void ReadBitmap() { int threadIterationCount = 176 * 64; int size = sizeof(int) * 2; _shaderOutput = new Int2[10000]; ComputeBuffer buffer = new ComputeBuffer(10000, size); buffer.SetData(_shaderOutput); Color debugColor = new Color32(173, 66, 32, 255); bitmapComputeShader.SetVector("reference_color", debugColor);; bitmapComputeShader.SetTexture(0, "bitmap", bitmap); bitmapComputeShader.SetBuffer(0, "output", buffer); bitmapComputeShader.Dispatch(0, threadIterationCount, threadIterationCount, 1); buffer.GetData(_shaderOutput); buffer.Dispose(); Debug.Log(_shaderOutput[0].x); // 输出为0 // _shaderOutput索引0由Shader设置为数组长度 // 因无法确定数组实际大小 for (int m = 0; m < _shaderOutput[0].x; m++) { int x = _shaderOutput[m].x; int y = _shaderOutput[m].y; if(x==0 && y==00) continue; Color32 color = bitmap.GetPixel(x, y); _areaBuilders[color].AddPixel(x, y); } }
Compute Shader代码
#pragma kernel CSMain const uint width = 5632; const uint height = 2048; const int width_step = 176; const int height_step = 64; int last_iteration = 1; Texture2D bitmap; SamplerState smp_clamp_point; float4 reference_color; RWStructuredBuffer<int2> output; bool compare_colors(float4 col, float4 ref_col) { const int col_r = round(col.x * 255); const int col_g = round(col.y * 255); const int col_b = round(col.z * 255); const int ref_col_r = round(ref_col.x * 255); const int ref_col_g = round(ref_col.y * 255); const int ref_col_b = round(ref_col.z * 255); return (col_r == ref_col_r && col_g == ref_col_g && col_b == ref_col_b); } void thread_job(uint x_index, uint y_index) { float u = 0; float v = 0; for (int i = 0; i < width_step; i++) { for (int j = 0; j < height_step; j++) { int2 xy_coords; xy_coords.x = x_index * width_step + i; xy_coords.y = y_index * height_step + j; u = xy_coords.x / width; v = xy_coords.y / height; const float2 uv = float2(u, v); const float4 pixel = bitmap.SampleLevel(smp_clamp_point, uv, 0); if(compare_colors(pixel, reference_color)) { output[last_iteration] = xy_coords; last_iteration++; output[0].x = last_iteration; } } } } [numthreads(32,32,1)] void CSMain (uint3 id : SV_DispatchThreadID) { thread_job(id.x, id.y); }
1. Dispatch线程组数量错误
C#中Dispatch的参数是线程组的数量,不是线程总数。你使用的是[numthreads(32,32,1)]的线程组,因此需要计算对应方向的线程组数量:
- 宽度方向:5632 / 176 = 32组
- 高度方向:2048 / 64 = 32组
原代码错误传入了threadIterationCount(176*64),导致线程组数量严重超标,大部分线程处理的坐标超出位图范围。
修改C#的Dispatch代码:
bitmapComputeShader.Dispatch(0, 32, // x方向线程组数量 32, // y方向线程组数量 1);
2. 全局变量线程竞争
Shader中的last_iteration是全局变量,所有线程同时读写会引发竞态条件:多个线程同时读取同一个值,写入时互相覆盖,最终计数错误甚至为0。必须用原子操作保证计数更新的安全性。
修改HLSL代码:
// 移除全局变量last_iteration // 替换匹配颜色后的代码块: if(compare_colors(pixel, reference_color)) { // 原子递增计数,获取当前可用的索引偏移量 uint index = InterlockedAdd(output[0].x, 1); // 坐标从索引1开始存储,因此偏移量+1 output[index + 1] = xy_coords; }
3. 颜色比较精度问题
HLSL的round操作可能引入精度误差,且Unity的Color32转Vector时会自动将0-255归一化到0-1范围,直接用浮点数比较更准确。
修改HLSL颜色比较函数:
bool compare_colors(float4 col, float4 ref_col) { // 忽略alpha通道(EU4地图alpha通常为255) return abs(col.r - ref_col.r) < 0.001f && abs(col.g - ref_col.g) < 0.001f && abs(col.b - ref_col.b) < 0.001f; }
4. UV计算的整数除法问题
HLSL中xy_coords.x / width是整数除法(两者均为整数),结果会被截断为0,导致采样位置错误。需要强制转换为浮点数计算。
修改HLSL的UV计算代码:
u = (float)xy_coords.x / width; v = (float)xy_coords.y / height;
5. C#循环索引错误
原代码循环从m=0开始,但索引0是计数,实际坐标从索引1开始,循环范围应调整为1 <= m <= 计数。
修改C#循环代码:
int pixelCount = _shaderOutput[0].x; for (int m = 1; m <= pixelCount; m++) { int x = _shaderOutput[m].x; int y = _shaderOutput[m].y; Color32 color = bitmap.GetPixel(x, y); _areaBuilders[color].AddPixel(x, y); }
6. 初始化计数初始值
C#端需要将_shaderOutput[0].x初始化为0,确保Shader的原子操作从正确的基准值开始递增。
修改C#数组初始化代码:
_shaderOutput = new Int2[10000]; _shaderOutput[0].x = 0; // 初始化计数为0 ComputeBuffer buffer = new ComputeBuffer(10000, size); buffer.SetData(_shaderOutput);
内容的提问来源于stack exchange,提问作者Emir Can Akgöl

