You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity Compute Shaders遍历大位图异常:EU4地图生成复刻排查

问题:Unity Compute Shader遍历EU4地图位图时输出数组计数始终为0

我正在复刻《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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 22:57:47