Unity: Compute Shader для вычисления ближайшей точки к каждой вершине

У меня есть сетка и массив точек. Я хочу вычислить для каждой вершины индекс ближайшей точки в массиве. У меня есть рутина, которая работает:

        for (int i=0;i<vertexPositions.Length;i++)
    {
        float minDist = 100000.0f;
        int index=0;
        float dist;
        for (int a=0;a<pointPositions.Length;a++)
        {
            dist = (vertexPositions[i] - pointPositions[a]).sqrMagnitude;
            if (dist<minDist)
            {
                minDist = dist;
                index = a;
            }
        }
        vertexParameter[i] = index;
    }

Массив vertexParameter содержит желаемый результат. Эта процедура очень медленная, если есть много вершин, поэтому я хотел создать вычислительный шейдер, который делает то же самое. Но я новичок в вычислительных шейдерах…

Это мой код вычислительного шейдера:

#pragma kernel ClosestPoint

struct vertexData
{
    float3 position;
    int parameter;
};
struct pointData
{
    float3 position;
    float parameter;
};

RWStructuredBuffer<vertexData> vertex;
StructuredBuffer<pointData> point;


[numthreads(32, 1, 1)]
void ClosestPoint(uint3 id : SV_DispatchThreadID)
{
    int index;
    float dist;
    float minDist = 1000.0f;
    for (uint i = 0; i < point.Length; i++)
    {
        dist = distance(point[i].position, vertex[id.x].position);
        if (dist < minDist)
        {
            minDist = dist;
            index =  i;
        }

    }
    vertex[id.x].parameter =  index;
}

Не знаю почему, но этот код дает ошибочные результаты. Результаты меняются, если я изменяю ThreadGroups в вызове Dispatch, поэтому я полагаю, что это может быть связано с некоторыми проблемами синхронизации…?

В случае необходимости, это код скрипта, который вызывает шейдер:

        vertex = new ComputeBuffer(vertices.Length, System.Runtime.InteropServices.Marshal.SizeOf(typeof(vertexData)));
    vertex.SetData(vertices);


    point= new ComputeBuffer(points.Length, System.Runtime.InteropServices.Marshal.SizeOf(typeof(pointData)));
    point.SetData(points);

    shader.SetBuffer(kernelHandle, "vertex", vertex);
    shader.SetBuffer(kernelHandle, "point", point);
    shader.Dispatch(kernelHandle, 1, 1, 1);
    vertex.GetData(vertices);
    for (int i = 0; i < vertexParameter.Length; i++)
    {
        vertexParameter[i] = vertices[i].parameter;
    }
    vertex.Release();
    point.Release();

person kefren    schedule 17.06.2019    source источник
comment
Что такое переменная dxl в коде hlsl? Кажется, это не определено/объявлено там.   -  person Ruzihm    schedule 18.06.2019
comment
несоответствие имен предыдущей версии, теперь исправлено. Извини.   -  person kefren    schedule 18.06.2019
comment
Я удивлен, что point.Length компилируется в hlsl. Возможно, он случайно имел в виду что-то другое. Попробуйте добавить переменную int count, передающую длину буфера точек, используя SetInt. См. здесь для примера того, о чем я говорю.   -  person Ruzihm    schedule 18.06.2019
comment
Добавление переменной count ничего не изменило. Но я как-то частично нашел решение. Кажется, проблема в том, что я не отправляю нужное количество потоков. Если я устанавливаю `[numthreads(32, 1, 1)]` в шейдере и shader.Dispatch(kernelHandle, vertices.Length, 1, 1); , процедура дает правильный результат. Но иметь так много групп потоков, все с одним потоком, кажется очень плохой оптимизацией...   -  person kefren    schedule 18.06.2019


Ответы (1)


Я считаю, что вы ошибаетесь в связи между threadGroups в вашем вызове Dispatch() и [numthreads()] в спецификации вашего ядра.

Результатом shader.Dispatch(kernelHandle, vertices.Length, 1, 1); в сочетании с [numthreads(32,1,1)] является не «много групп потоков с одним потоком», а vertices.Length групп потоков, все с 32 потоками.

Таким образом, ваше ядро ​​будет вызываться 32*vertices.Length раз, при этом id.x будет расти соразмерно... вы получите правильный результат с кодом из вашего комментария, потому что что бы ни случилось, когда вы попытаетесь прочитать и записать vertex[id.x] после того, как id.x вышел за пределы, это не произойдет. изменить тот факт, что к тому времени вы уже вычислили все правильные результаты и сохранили их в соответствующем месте.

Что вам нужно сделать, чтобы не тратить время впустую, установить threadGroupsX в вашем Dispatch() на ceil(vertices.Length/32) (псевдокод).

Вы также можете добавить что-то вроде

if (id.x >= vertexLength) return;

в вашем ядре (потому что, если у вас не будет кратного 32 вершины, некоторые потоки будут за пределами границ)... но на самом деле это, вероятно, ничем не поможет с точки зрения производительности или безопасности; чтение и запись за пределами vertices.Length будет по существу без операций, в то время как дополнительное ветвление в ядре может повлечь за собой затраты. Я предполагаю, что в данном случае это, вероятно, не имеет значения в любом случае, и, возможно, подобное утверждение может сделать логику более ясной для читателей-людей... но это означает дополнительный шаблон прохождения в дополнительной униформе.

Кстати, вы также можете использовать ASyncGPUReadbackRequest, чтобы избежать остановки вашего кода на vertex.GetData(vertices);, если это имеет смысл в вашем приложении. Вы могли бы написать это так в вопросе для краткости (что, как вы можете заметить, не всегда является моей сильной стороной).

person PeterT    schedule 10.07.2019
comment
Вы правы, и спасибо. Я далек от того, чтобы быть экспертом в вычислительных шейдерах (и шейдерах в целом), и, конечно же, это не та область, где вы легко найдете учебники или документацию... кстати, в этой книге описано поведение за пределами границ, и, кажется, я только нужно побеспокоиться о производительности: ссылка - person kefren; 10.07.2019
comment
Отлично, спасибо @kefren. Возможно, то же самое относится и к моему вопросу о добавлении. Google говорит, что либо я достиг лимита просмотра, либо эта часть книги недоступна. Я только что нашел это, которое довольно плотное, но, вероятно, содержит информацию, которая мне нужна, по крайней мере, для DirectX. - person PeterT; 10.07.2019
comment
На самом деле, во всяком случае, if (id.x >= vertexLength) return; ничего не сделает, кроме добавления дополнительной ветки, которая может плохо сказаться на производительности, поэтому, вероятно, лучше без нее. Я также могу отредактировать свой ответ. - person PeterT; 14.07.2019