Как получить параллельный рендеринг пикселей на GPU? Для воксельной трассировки лучей

Я сделал воксельный формирователь лучей в Unity, используя вычислительный шейдер и текстуру. Но при разрешении 1080p расстояние обзора ограничено всего 100 при 30 кадрах в секунду. Пока нет никаких отскоков света или чего-то подобного, я очень разочарован этой производительностью.

Я пробовал изучить Vulkan, и лучшие уроки основаны на растеризации, и я думаю, все, что мне действительно нужно, - это параллельное вычисление пикселей на графическом процессоре. Я знаком с CUDA и читал, что иногда используется для рендеринга? Или есть простой способ просто параллельно вычислять пиксели в Vulcan? У меня уже есть шаблон проекта Vulkan, который открывает пустое окно. Мне не нужно возвращать какие-либо данные с графического процессора, просто визуализируйте прямо на экране после передачи данных.

И с приведенным ниже кодом будет ли он значительно быстрее в Vulkan по сравнению с вычислительным шейдером Unity? В нем ОЧЕНЬ много операторов if / else, которые я прочитал, плохи для графических процессоров, но я не могу придумать другого способа их написания.

РЕДАКТИРОВАТЬ: Я оптимизировал его, насколько мог, но он все еще довольно медленный, например, 30 кадров в секунду при 1080p.

Вот вычислительный шейдер:

#pragma kernel CSMain

RWTexture2D<float4> Result; // the actual array of pixels the player sees
const float width; // in pixels
const float height;

const StructuredBuffer<int> voxelMaterials; // for now just getting a flat voxel array
const int voxelBufferRowSize;
const int voxelBufferPlaneSize;
const int voxelBufferSize;
const StructuredBuffer<float3> rayDirections; // I'm now actually using it as points instead of directions
const float maxRayDistance;

const float3 playerCameraPosition; // relative to the voxelData, ie the first voxel's bottom, back, left corner position, no negative coordinates
const float3 playerWorldForward;
const float3 playerWorldRight;
const float3 playerWorldUp;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Result[id.xy] = float4(0, 0, 0, 0); // setting the pixel to black by default
    float3 pointHolder = playerCameraPosition; // initializing the first point to the player's position
    const float3 p = rayDirections[id.x + (id.y * width)]; // vector transformation getting the world space directions of the rays relative to the player
    const float3 u1 = p.x * playerWorldRight;
    const float3 u2 = p.y * playerWorldUp;
    const float3 u3 = p.z * playerWorldForward;
    const float3 direction = u1 + u2 + u3; // the direction to that point

    float distanceTraveled = 0;
    int3 directionAxes; // 1 for positive, 0 for zero, -1 for negative
    int3 directionIfReplacements = { 0, 0, 0 }; // 1 for positive, 0 for zero, -1 for negative
    float3 axesUnit = { 1 / abs(direction.x), 1 / abs(direction.y), 1 / abs(direction.z) };
    float3 distancesXYZ = { 1000, 1000, 1000 };
    int face = 0; // 1 = x, 2 = y, 3 = z // the current face the while loop point is on

    // comparing the floats once in the beginning so the rest of the ray traversal can compare ints
    if (direction.x > 0) {
        directionAxes.x = 1;
        directionIfReplacements.x = 1;
    }
    else if (direction.x < 0) {
        directionAxes.x = -1;
    }
    else {
        distanceTraveled = maxRayDistance; // just ending the ray for now if one of it's direction axes is exactly 0. You'll see a line of black pixels if the player's rotation is zero but this never happens naturally
        directionAxes.x = 0;
    }
    if (direction.y > 0) {
        directionAxes.y = 1;
        directionIfReplacements.y = 1;
    }
    else if (direction.y < 0) {
        directionAxes.y = -1;
    }
    else {
        distanceTraveled = maxRayDistance;
        directionAxes.y = 0;
    }
    if (direction.z > 0) {
        directionAxes.z = 1;
        directionIfReplacements.z = 1;
    }
    else if (direction.z < 0) {
        directionAxes.z = -1;
    }
    else {
        distanceTraveled = maxRayDistance;
        directionAxes.z = 0;
    }

    // calculating the first point
    if (playerCameraPosition.x < voxelBufferRowSize &&
        playerCameraPosition.x >= 0 &&
        playerCameraPosition.y < voxelBufferRowSize &&
        playerCameraPosition.y >= 0 &&
        playerCameraPosition.z < voxelBufferRowSize &&
        playerCameraPosition.z >= 0)
    {
        int voxelIndex = floor(playerCameraPosition.x) + (floor(playerCameraPosition.z) * voxelBufferRowSize) + (floor(playerCameraPosition.y) * voxelBufferPlaneSize); // the voxel index in the flat array

        switch (voxelMaterials[voxelIndex]) {
        case 1:
            Result[id.xy] = float4(1, 0, 0, 0);
            distanceTraveled = maxRayDistance; // to end the while loop
            break;
        case 2:
            Result[id.xy] = float4(0, 1, 0, 0);
            distanceTraveled = maxRayDistance;
            break;
        case 3:
            Result[id.xy] = float4(0, 0, 1, 0);
            distanceTraveled = maxRayDistance;
            break;
        default:
            break;
        }
    }

    // traversing the ray beyond the first point
    while (distanceTraveled < maxRayDistance) 
    {
        switch (face) {
        case 1:
            distancesXYZ.x = axesUnit.x;
            distancesXYZ.y = (floor(pointHolder.y + directionIfReplacements.y) - pointHolder.y) / direction.y;
            distancesXYZ.z = (floor(pointHolder.z + directionIfReplacements.z) - pointHolder.z) / direction.z;
            break;
        case 2:
            distancesXYZ.y = axesUnit.y;
            distancesXYZ.x = (floor(pointHolder.x + directionIfReplacements.x) - pointHolder.x) / direction.x;
            distancesXYZ.z = (floor(pointHolder.z + directionIfReplacements.z) - pointHolder.z) / direction.z;
            break;
        case 3:
            distancesXYZ.z = axesUnit.z;
            distancesXYZ.x = (floor(pointHolder.x + directionIfReplacements.x) - pointHolder.x) / direction.x;
            distancesXYZ.y = (floor(pointHolder.y + directionIfReplacements.y) - pointHolder.y) / direction.y;
            break;
        default:
            distancesXYZ.x = (floor(pointHolder.x + directionIfReplacements.x) - pointHolder.x) / direction.x;
            distancesXYZ.y = (floor(pointHolder.y + directionIfReplacements.y) - pointHolder.y) / direction.y;
            distancesXYZ.z = (floor(pointHolder.z + directionIfReplacements.z) - pointHolder.z) / direction.z;
            break;
        }

        face = 0; // 1 = x, 2 = y, 3 = z
        float smallestDistance = 1000;
        if (distancesXYZ.x < smallestDistance) {
            smallestDistance = distancesXYZ.x;
            face = 1;
        }
        if (distancesXYZ.y < smallestDistance) {
            smallestDistance = distancesXYZ.y;
            face = 2;
        }
        if (distancesXYZ.z < smallestDistance) {
            smallestDistance = distancesXYZ.z;
            face = 3;
        }
        if (smallestDistance == 0) {
            break;
        }

        int3 facesIfReplacement = { 1, 1, 1 };
        switch (face) { // directionIfReplacements is positive if positive but I want to subtract so invert it to subtract 1 when negative subtract nothing when positive
        case 1:
            facesIfReplacement.x = 1 - directionIfReplacements.x;
            break;
        case 2:
            facesIfReplacement.y = 1 - directionIfReplacements.y;
            break;
        case 3:
            facesIfReplacement.z = 1 - directionIfReplacements.z;
            break;
        }

        pointHolder += direction * smallestDistance; // the acual ray marching
        distanceTraveled += smallestDistance;

        int3 voxelIndexXYZ = { -1,-1,-1 }; // the integer coordinates within the buffer
        voxelIndexXYZ.x = ceil(pointHolder.x - facesIfReplacement.x);
        voxelIndexXYZ.y = ceil(pointHolder.y - facesIfReplacement.y);
        voxelIndexXYZ.z = ceil(pointHolder.z - facesIfReplacement.z);

        //check if voxelIndexXYZ is within bounds of the voxel buffer before indexing the array
        if (voxelIndexXYZ.x < voxelBufferRowSize &&
            voxelIndexXYZ.x >= 0 &&
            voxelIndexXYZ.y < voxelBufferRowSize &&
            voxelIndexXYZ.y >= 0 &&
            voxelIndexXYZ.z < voxelBufferRowSize &&
            voxelIndexXYZ.z >= 0)
        {
            int voxelIndex = voxelIndexXYZ.x + (voxelIndexXYZ.z * voxelBufferRowSize) + (voxelIndexXYZ.y * voxelBufferPlaneSize); // the voxel index in the flat array
            switch (voxelMaterials[voxelIndex]) {
            case 1:
                Result[id.xy] = float4(1, 0, 0, 0) * (1 - (distanceTraveled / maxRayDistance));
                distanceTraveled = maxRayDistance; // to end the while loop
                break;
            case 2:
                Result[id.xy] = float4(0, 1, 0, 0) * (1 - (distanceTraveled / maxRayDistance));
                distanceTraveled = maxRayDistance;
                break;
            case 3:
                Result[id.xy] = float4(0, 0, 1, 0) * (1 - (distanceTraveled / maxRayDistance));
                distanceTraveled = maxRayDistance;
                break;
            }
        }
        else {
            break; // should be uncommented in actual game implementation where the player will always be inside the voxel buffer
        }
    }
}

В зависимости от данных вокселей, которые вы ему предоставляете, он производит следующее:  введите описание изображения здесь

И вот шейдер после его оптимизации и удаления всех ветвящихся или расходящихся условных операторов (я думаю):

#pragma kernel CSMain

RWTexture2D<float4> Result; // the actual array of pixels the player sees
float4 resultHolder;
const float width; // in pixels
const float height;

const Buffer<int> voxelMaterials; // for now just getting a flat voxel array
const Buffer<float4> voxelColors;
const int voxelBufferRowSize;
const int voxelBufferPlaneSize;
const int voxelBufferSize;
const Buffer<float3> rayDirections; // I'm now actually using it as points instead of directions
const float maxRayDistance;

const float3 playerCameraPosition; // relative to the voxelData, ie the first voxel's bottom, back, left corner position, no negative coordinates
const float3 playerWorldForward;
const float3 playerWorldRight;
const float3 playerWorldUp;

[numthreads(16, 16, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    resultHolder = float4(0, 0, 0, 0); // setting the pixel to black by default
    float3 pointHolder = playerCameraPosition; // initializing the first point to the player's position
    const float3 p = rayDirections[id.x + (id.y * width)]; // vector transformation getting the world space directions of the rays relative to the player
    const float3 u1 = p.x * playerWorldRight;
    const float3 u2 = p.y * playerWorldUp;
    const float3 u3 = p.z * playerWorldForward;
    const float3 direction = u1 + u2 + u3; // the transformed ray direction in world space
    const bool anyDir0 = direction.x == 0 || direction.y == 0 || direction.z == 0; // preventing a division by zero
    float distanceTraveled = maxRayDistance * anyDir0;

    const float3 nonZeroDirection = { // to prevent a division by zero
        direction.x + (1 * anyDir0),
        direction.y + (1 * anyDir0),
        direction.z + (1 * anyDir0)
    };
    const float3 axesUnits = { // the distances if the axis is an integer
        1.0f / abs(nonZeroDirection.x),
        1.0f / abs(nonZeroDirection.y),
        1.0f / abs(nonZeroDirection.z)
    };
    const bool3 isDirectionPositiveOr0 = {
        direction.x >= 0,
        direction.y >= 0,
        direction.z >= 0
    };

    while (distanceTraveled < maxRayDistance)
    {
        const bool3 pointIsAnInteger = {
            (int)pointHolder.x == pointHolder.x,
            (int)pointHolder.y == pointHolder.y,
            (int)pointHolder.z == pointHolder.z
        };

        const float3 distancesXYZ = {
            ((floor(pointHolder.x + isDirectionPositiveOr0.x) - pointHolder.x) / direction.x * !pointIsAnInteger.x)  +  (axesUnits.x * pointIsAnInteger.x),
            ((floor(pointHolder.y + isDirectionPositiveOr0.y) - pointHolder.y) / direction.y * !pointIsAnInteger.y)  +  (axesUnits.y * pointIsAnInteger.y),
            ((floor(pointHolder.z + isDirectionPositiveOr0.z) - pointHolder.z) / direction.z * !pointIsAnInteger.z)  +  (axesUnits.z * pointIsAnInteger.z)
        };

        float smallestDistance = min(distancesXYZ.x, distancesXYZ.y);
        smallestDistance = min(smallestDistance, distancesXYZ.z);

        pointHolder += direction * smallestDistance;
        distanceTraveled += smallestDistance;

        const int3 voxelIndexXYZ = {
            floor(pointHolder.x) - (!isDirectionPositiveOr0.x && (int)pointHolder.x == pointHolder.x), 
            floor(pointHolder.y) - (!isDirectionPositiveOr0.y && (int)pointHolder.y == pointHolder.y),
            floor(pointHolder.z) - (!isDirectionPositiveOr0.z && (int)pointHolder.z == pointHolder.z)
        };

        const bool inBounds = (voxelIndexXYZ.x < voxelBufferRowSize && voxelIndexXYZ.x >= 0) && (voxelIndexXYZ.y < voxelBufferRowSize && voxelIndexXYZ.y >= 0) && (voxelIndexXYZ.z < voxelBufferRowSize && voxelIndexXYZ.z >= 0);

        const int voxelIndexFlat = (voxelIndexXYZ.x + (voxelIndexXYZ.z * voxelBufferRowSize) + (voxelIndexXYZ.y * voxelBufferPlaneSize)) * inBounds; // meaning the voxel on 0,0,0 will always be empty and act as a our index out of range prevention

        if (voxelMaterials[voxelIndexFlat] > 0) {
            resultHolder = voxelColors[voxelMaterials[voxelIndexFlat]] * (1 - (distanceTraveled / maxRayDistance));
            break;
        }   
        if (!inBounds) break;
    }
    Result[id.xy] = resultHolder;
}

person Tristan367    schedule 02.04.2021    source источник
comment
Я пробовал изучать Vulkan, и лучшие руководства основаны на растеризации, и я думаю, все, что мне действительно нужно, это вычислять пиксели параллельно на GPU. Растеризация - это вычисление пикселей, параллельно, на GPU. Так чем же то, что вы хотите, отличается от этого?   -  person Nicol Bolas    schedule 03.04.2021
comment
Нет, для растрирования вам нужно вычислить вершины и треугольники, а затем получить пиксели из них, используя систему, совершенно отличную от трассировки лучей. Я просто хочу сказать pixels[n] = color без лишних слов.   -  person Tristan367    schedule 03.04.2021


Ответы (1)


Вычислительный шейдер - это то, чем он является: программа, работающая на графическом процессоре, будь то на vulkan или в Unity, так что вы в любом случае делаете это параллельно. Суть vulkan, однако, в том, что он дает вам больше контроля над командами, выполняемыми на GPU - синхронизация, память и т. Д. Так что не обязательно будет быстрее в vulkan, чем в Unity. Итак, на самом деле вам нужно оптимизировать шейдеры.

Кроме того, основная проблема с if / else - это расхождение внутри групп вызовов, которые работают в режиме блокировки. шаг. Так что, если вы сможете этого избежать, влияние на производительность будет намного меньше. Они могут вам в этом помочь.


Если ты все еще хочешь делать все это на вулкане ...

Поскольку вы не собираетесь выполнять растеризацию треугольников, вам, вероятно, не потребуются проходы рендеринга или графические конвейеры, которые обычно отображаются в руководствах. Вместо этого вам понадобится конвейер вычислительных шейдеров. Это намного проще, чем графические конвейеры, требуя только одного шейдера и макета конвейера (входы и выходы связаны с помощью наборов дескрипторов).

Вам просто нужно передать образ цепочки подкачки вычислительному шейдеру как образ хранилища в дескриптор (и, конечно же, любые другие данные, которые могут понадобиться вашему шейдеру, все они передаются через дескрипторы). Для этого вам нужно указать VK_IMAGE_USAGE_STORAGE_BIT в структуре создания вашей цепочки обмена.

Затем в буфере команд вы связываете наборы дескрипторов с изображениями и другими данными, связываете конвейер вычислений и отправляете их, как вы, вероятно, делаете в Unity. Представление swapchain и отправка буферов команд не должны отличаться от того, как работает графика в руководствах.

person user369070    schedule 02.04.2021
comment
Я оптимизировал его и обновил свой опубликованный код, и теперь в фактическом обходе лучей всего 5 операторов if и 3 оператора switch, и это едва ли даже заметно быстрее. Я вижу видео людей, отслеживающих лучи полигонов в 4k с отражениями света в Vulkan, это просто кажется странным, что я с трудом могу даже использовать raycast воксели со скоростью 30 кадров в секунду с меньшим расстоянием обзора, если я должен получить аналогичную производительность. Мой алгоритм не может быть настолько плохим, правда? Спасибо. - person Tristan367; 03.04.2021
comment
@ Tristan367 Во-первых, какой у вас и у них графический процессор? Во-вторых, вы можете попытаться перевести чужие алгоритмы в свой код, чтобы увидеть более прямое сравнение ... Также рассмотрите возможность использования текстур вместо StructuredBuffer. А поплавки на GPU - это самая дешевая вещь на свете ... - person user369070; 03.04.2021
comment
У меня есть GTX 1050. Это приличный маленький графический процессор, и он отлично справляется с трассировкой лучей в блендере с большим количеством сэмплов. Я думаю, что должны быть какие-то накладные расходы на Unity или что-то в этом роде. Я должен отправлять вычислительный шейдер каждый кадр из Monobehvior (класс Unity) в цикле обновления Unity. - person Tristan367; 03.04.2021
comment
@ Tristan367 Диспетчерская - это то, как вы говорите графическому процессору начать работу, вы делаете это и в vulkan, хотя там вы бы связали его с другими командами ... Хотя не исключено, что Unity добавляет много накладных расходов, я лично сомневаюсь Это. Другие возможные накладные расходы связаны с копированием данных из процессора в графический процессор, синхронизация процессора и процессора, неэффективные структуры данных ... - person user369070; 04.04.2021
comment
Я просто удалил ВСЕ условные операторы. Остался один крошечный оператор if, который я не могу понять, но заметного улучшения скорости нет. Я отредактировал код в своем посте, проверьте его. - person Tristan367; 05.04.2021
comment
@ Tristan367 Во-первых, перерыв; и вернуться; существует. Затем вы можете заменить некоторые операции с целыми числами на побитовые, чтобы сделать их более читаемыми (&, |, ^ - и, или, xor). И что еще более важно, вы, скорее всего, связаны памятью, а не выполнением: попробуйте изменить структуры данных. И попробуйте изменить разрешение, чтобы увидеть, пропорционально ли меняется частота кадров в секунду. Оптимизация шейдеров - это не просто удаление if, а поиск и устранение узких мест. - person user369070; 05.04.2021
comment
Необязательные разрывы и возвраты требуют ветвления, верно? И я не думаю, что это память, самое большое различие - это расстояние обзора. Даже при разрешении 4K производительность будет отличной, если расстояние обзора составляет всего несколько единиц. Существенно помогает и снижение разрешения. Мне просто кажется странным, что я не могу использовать лучи для нескольких простых кубов со скоростью 30 кадров в секунду, а Blender может выполнять трассировку тысяч полигонов примерно с той же скоростью на одном компьютере. А какие структуры данных? Индексирование плоского массива - это настолько быстро, насколько это возможно для структуры данных. - person Tristan367; 05.04.2021
comment
И в этом комментарии не было места, но изменение размера буфера вокселей, которое я отправляю на графический процессор, не приводит к заметным изменениям в производительности, будь то один воксель или 5 миллионов. Это то, что вы имели в виду под памятью? - person Tristan367; 05.04.2021
comment
Во-первых, прочтите второй абзац моего ответа еще раз: ветки плохи, только если вызывают расхождение. И вы все равно вернетесь, когда выйдете из цикла. Возврат из части варпа сделает эти полосы неактивными и снизит их производительность. Под памятью я имел в виду время, необходимое графическому процессору для загрузки данных на очень кремниевый чип. Плоские массивы не так хороши для хранения трехмерных сеток (проблемы с локализацией текстур), попробуйте вместо этого использовать текстуры, так как они также могут использовать специализированную аппаратную память ... Попробуйте сделать расстояние обзора целым числом, которое содержит количество пройденных вокселей, чтобы потоки, которые никогда нажмите финиш одновременно ... - person user369070; 05.04.2021
comment
Добро пожаловать, поскольку оптимизация на низком уровне больше не помогает, пришло время полностью переосмыслить ваш подход. Я слышал, что более сложные двигатели используют деревья для ускорения марша: блоки пустого пространства собираются вместе и маршируют за один шаг. Для (возможно) более простого подхода вы можете попробовать использовать общую память или перемешивание строк, проверяя несколько вокселей на шаге на предмет пересечения в отдельных строках и добавляя прогресс для правильных потоков. Здесь будет труднее избежать расхождения, и в целом потребуется больше времени на размышления ... Кроме того, ветки не работают, и ваш компилятор попытается оптимизировать некоторые ... - person user369070; 29.04.2021
comment
Спасибо за совет, да, думаю, я попробую использовать для этого октодеревья. Я уже могу получить максимальное расстояние просмотра Minecraft, если уменьшу разрешение до 1280x720, так что это не за горами. Пересечение октодерева, вероятно, позволит ему превысить расстояния просмотра Minecraft или, по крайней мере, сопоставить их с 1080p. Однако это еще не реализовано. И да, я немного увлекся удалением ветвления, хотя на самом деле это немного быстрее. - person Tristan367; 29.04.2021
comment
Как я могу структурировать данные в буфере HLSL? HLSL не позволит мне создать структуру с членом его собственного типа, поэтому я не могу придумать другого способа хранения таких узлов данных, который был бы беспорядочным и сложным. - person Tristan367; 30.04.2021
comment
Это что-то для отдельного вопроса (и гугла). Но я бы попробовал создать массив целых чисел (узлов?), Где каждый бит указывает, существует ли узел, с некоторой постоянной максимальной глубиной. - person user369070; 30.04.2021