На Хабре уже есть много статей про использование вычислительных шейдеров с Unity, однако статью о использовании вычислительного шейдера на "чистом" Win32 API + DirectX 11 найти затруднительно. Однако эта задача ненамного сложнее, подробнее — под катом.


Для этого будем использовать:


  • Windows 10
  • Visual Studio 2017 Community Edition с модулем "Разработка классических приложений на C++"
    После создания проекта укажем компоновщику использовать библиотеку `d3d11.lib`.


Заголовочные файлы

Для подсчета количества кадров в секунду будем использовать стандартную библиотеку


#include <time.h>

Выводить количество кадров в секунду будем через заголовок окна, для чего нам понадобится формировать соответствующую строку


#include <stdio.h>

Не будем подробно рассматривать обработку ошибок, в нашем случае достаточно, чтобы приложение упало в отладочной версии и указало на момент падения:


#include <assert.h>

Заголовочные файлы для WinAPI:


#define WIN32_LEAN_AND_MEAN
#include <tchar.h>
#include <Windows.h>

Заголовочные файлы для Direct3D 11:


#include <dxgi.h>
#include <d3d11.h>

Идентификаторы ресурсов для загрузки шейдера. Можно вместо этого загружать в память объектный файл шейдера, генерируемый компилятором HLSL. Создание файла ресурсов описано позже.


#include "resource.h"

Константы, общие для шейдера и вызывающей части, объявим в отдельном заголовочном файле.


#include "SharedConst.h"

Объявим функцию обработки Windows-событий, которая будет определена позже:


LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

Напишем функции для создания и уничтожения окна


int windowWidth, windowHeight;
HINSTANCE hInstance;
HWND hWnd;

void InitWindows()
{
    // Получаем информацию о локальном модуле
    hInstance = GetModuleHandle(NULL);
    windowWidth = 800;
    windowHeight = 800;

    WNDCLASS wc;
    // Без дополнительных параметров
    wc.style = 0;
    // Задаем обработчик событий
    wc.lpfnWndProc = &WndProc;
    // Нам не нужно дополнительное выделение памяти к структуре окна и структуре класса
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    // Модуль (программа), которому принадлежит обработчик событий
    wc.hInstance = hInstance;
    // Загружаем стандартный курсор и стандартную иконку
    wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
    wc.hCursor = LoadCursor(hInstance, IDC_ARROW);
    // Цвет фона не важен, поэтому не задаем "кисть"
    wc.hbrBackground = NULL;
    // Окно без меню
    wc.lpszMenuName = NULL;
    // Задаем название класса окна
    wc.lpszClassName = _T("WindowClass1");

    // Регистрируем класс окна
    ATOM result = RegisterClass(&wc);
    // Проверяем, что класс успешно зарегистрирован
    assert(result);

    // Стандартное окно -- имеет заголовок, можно изменить размер и т.д.
    DWORD dwStyle = WS_OVERLAPPEDWINDOW;
    RECT rect;
    // Клиентская область (внутри рамки) по центру экрана и заданного размера
    rect.left = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2;
    rect.top = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2;
    rect.right = rect.left + windowWidth;
    rect.bottom = rect.top + windowHeight;
    // Вычислим область окна с рамкой. Последний параметр -- наличие меню
    AdjustWindowRect(&rect, dwStyle, FALSE);

    hWnd = CreateWindow(
        _T("WindowClass1"),
        _T("WindowName1"),
        dwStyle,
        // Левый верхний угол окна
        rect.left, rect.top,
        // Размер окна
        rect.right - rect.left,
        rect.bottom - rect.top,
        // Родительское окно
        // HWND_DESKTOP раскрывается в NULL
        HWND_DESKTOP,
        // Меню
        NULL,
        // Модуль (программа), которой принадлежит окно
        hInstance,
        // Дополнительные свойства
        NULL);
    // Проверяем, что окно успешно создано
    assert(hWnd);
}

void DisposeWindows()
{
    // Удаляем окно
    DestroyWindow(hWnd);
    // Удаляем класс
    UnregisterClass(_T("WindowClass1"), hInstance);
}

Далее — инициализация интерфейса обращения к видеокарте (Device и DeviceContext) и цепочки буферов вывода (SwapChain):


IDXGISwapChain *swapChain;
ID3D11Device *device;
ID3D11DeviceContext *deviceContext;

void InitSwapChain()
{
    HRESULT result;

    DXGI_SWAP_CHAIN_DESC swapChainDesc;
    // Разрмер совпадает с размером клиентской части окна
    swapChainDesc.BufferDesc.Width = windowWidth;
    swapChainDesc.BufferDesc.Height = windowHeight;
    // Ограничение количества кадров в секунду задается в виде рационального числа
    // Т.к. нам нужна максимальная частота кадров, отключаем
    swapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
    swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
    // Формат вывода -- 32-битный RGBA
    swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    // Не задаем масштабирования при выводе
    swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
    swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    // Не используем сглаживание
    swapChainDesc.SampleDesc.Count = 1;
    swapChainDesc.SampleDesc.Quality = 0;
    // Используем SwapChain для вывода
    swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    // Один "задний" (не отображаемый) буфер
    swapChainDesc.BufferCount = 1;
    // Задаем окно для вывода
    swapChainDesc.OutputWindow = hWnd;
    // Оконный режим
    swapChainDesc.Windowed = TRUE;
    // Отбрасываем старую информацию из буфера при выводе на экран
    swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
    swapChainDesc.Flags = 0;

    // Используем DirectX 11.0, т.к. его нам достаточно
    D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_0;

    // В Debug-версии создаем поток отладки DirectX
#ifndef NDEBUG
    UINT flags = D3D11_CREATE_DEVICE_DEBUG;
#else
    UINT flags = 0;
#endif

    result = D3D11CreateDeviceAndSwapChain(
        // Используем видеоадаптер по-умолчанию
        NULL,
        // Используем аппаратную реализацию
        D3D_DRIVER_TYPE_HARDWARE, NULL,
        // См. выше
        flags,
        // Используем одну версию DirectX
        &featureLevel, 1,
        // Версия SDK
        D3D11_SDK_VERSION,
        // Передаем созданное ранее описание
        &swapChainDesc,
        // Указатели, куда записать результат
        &swapChain, &device, NULL, &deviceContext);
    // Проверяем, что операция прошла успешно
    assert(SUCCEEDED(result));
}

void DisposeSwapChain()
{
    deviceContext->Release();
    device->Release();
    swapChain->Release();
}

Инициализация доступа из шейдеров к буферу, в который будет производиться отрисовка:


ID3D11RenderTargetView *renderTargetView;

void InitRenderTargetView()
{
    HRESULT result;
    ID3D11Texture2D *backBuffer;

    // Берем "задний" буфер из SwapChain
    result = swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void **)&backBuffer);
    assert(SUCCEEDED(result));

    // Инициализируем доступ к буферу для записи и для отрисовки
    result = device->CreateRenderTargetView(backBuffer, NULL, &renderTargetView);
    assert(SUCCEEDED(result));

    // Указатель на буфер больше нам не нужен
    // Стоит отметить, что сам буфер при этом не удаляется,
    // т.к. на него всё ещё указывает SwapChain,
    // Release() лишь освобождает указатель
    backBuffer->Release();

    // Используем созданный View для отрисовки
    deviceContext->OMSetRenderTargets(1, &renderTargetView, NULL);

    // Задаем область отрисовки
    D3D11_VIEWPORT viewport;
    viewport.TopLeftX = 0;
    viewport.TopLeftY = 0;
    viewport.Width = (FLOAT)windowWidth;
    viewport.Height = (FLOAT)windowHeight;
    viewport.MinDepth = 0;
    viewport.MaxDepth = 1;
    deviceContext->RSSetViewports(1, &viewport);
}

void DisposeRenderTargetView()
{
    renderTargetView->Release();
}

До инициализации шейдеров нужно их создать. Visual Studio умеет распознавать расширение файла, поэтому мы можем просто создать исходник с расширением .hlsl, или же напрямую создавать шейдер через меню. Я выбрал первый способ, т.к. все равно через свойства придется задавать использование Shader Model 5.




Аналогично создаем вершинный и пиксельный шейдеры.


В вершинном шейдере будем просто преобразовывать координаты из двумерного вектора (т.к. позиции точек у нас именно двухмерные) в четырехмерный (принимаемый видеокартой):


float4 main(float2 input: POSITION): SV_POSITION
{
    return float4(input, 0, 1);
}

В пиксельном шейдере будем возвращать белый цвет:


float4 main(float4 input: SV_POSITION): SV_TARGET
{
    return float4(1, 1, 1, 1);
}

Теперь вычислительный шейдер. Зададим такую формулу для взаимодействий точек:


$\vec{F_{ij}} = 10^{-9} \vec{r_{ij}} \frac{\lvert \vec{r_{ij}} \rvert - 0.25}{\lvert \vec{r_{ij}} \rvert}$


При массе, принятой 1


Так будет выглядеть реализация этого на HLSL:


#include "SharedConst.h"

// Буфер позиций, UAV в слоте 0
RWBuffer<float2> position: register(u0);
// Буфер скоростей, UAV в слоте 1
RWBuffer<float2> velocity: register(u1);

// Количество потоков выполнения
[numthreads(NUMTHREADS, 1, 1)]
void main(uint3 id: SV_DispatchThreadID)
{
    float2 acc = float2(0, 0);
    for (uint i = 0; i < PARTICLE_COUNT; i++)
    {
        // Вектор от одной точки до другой
        float2 diff = position[i] - position[id.x];
        // Берем минимальное значение модуля вектора, чтобы не рассматривать случай 0-вектора
        float len = max(1e-10, length(diff));
        float k = 1e-9 * (len - 0.25) / len;
        acc += k * diff;
    }

    position[id.x] += velocity[id.x] + 0.5 * acc;
    velocity[id.x] += acc;
}

Можно заметить, что в шейдер включается файл SharedConst.h. Это тот заголовочный файл с константами, который включается в main.cpp. Вот содержание этого файла:


#ifndef PARTICLE_COUNT
#define PARTICLE_COUNT (1 << 15)
#endif

#ifndef NUMTHREADS
#define NUMTHREADS 64
#endif

Просто объявление количества частиц и количества потоков в одной группе. Мы выделим по одному потоку каждой частице, поэтому количество групп зададим как PARTICLE_COUNT / NUMTHREADS. Это число должно быть целым, поэтому нужно, чтобы число частиц делилось на число потоков в группе.


Загрузку скомпилированного байткода шейдеров будем производить при помощи механизма ресурсов Windows. Для этого создадим следующие файлы:


resource.h, где будут содержаться ID соответствующего ресурса:


#pragma once

#define IDR_BYTECODE_COMPUTE 101
#define IDR_BYTECODE_VERTEX 102
#define IDR_BYTECODE_PIXEL 103

И resource.rc, файл для генерации соответствующего ресурса следующего содержания:


#include "resource.h"

IDR_BYTECODE_COMPUTE ShaderObject "compute.cso"
IDR_BYTECODE_VERTEX ShaderObject "vertex.cso"
IDR_BYTECODE_PIXEL ShaderObject "pixel.cso"

Где ShaderObject — тип ресурса, а compute.cso, vertex.cso и pixel.cso — соответствующие названия файлов Compiled Shader Object в выходной директории.


Чтобы файлы были найдены, следует в свойствах resource.rc прописать путь до выходной директории проекта:



Visual Studio автоматически распознала файл как описание ресурсов и добавила его в сборку, вручную это делать не нужно


Теперь можно написать код инициализации шейдеров:


ID3D11ComputeShader *computeShader;
ID3D11VertexShader *vertexShader;
ID3D11PixelShader *pixelShader;

ID3D11InputLayout *inputLayout;

void InitShaders()
{
    HRESULT result;
    HRSRC src;
    HGLOBAL res;

    // Инициализация вычислительного шейдера
    // Берем встроенные в исполняемый файл ресурсы
    // Проверка ошибок не производится, т.к. байткод расположен внутри
    // исполняемого файла и не может быть не найден
    src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_COMPUTE), _T("ShaderObject"));
    res = LoadResource(hInstance, src);
    // Инициализируем шейдер
    result = device->CreateComputeShader(
        // Байткод шейдера и его размер
        res, SizeofResource(hInstance, src),
        // Свойства для компоновщика. В нашем случае не используется, т.к. шейдер из одного объекта
        NULL,
        // Указатель на шейдер
        &computeShader);
    assert(SUCCEEDED(result));
    FreeResource(res);

    // Аналогичные операции для пиксельного шейдера
    src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_PIXEL), _T("ShaderObject"));
    res = LoadResource(hInstance, src);
    result = device->CreatePixelShader(res, SizeofResource(hInstance, src),
                                       NULL, &pixelShader);
    assert(SUCCEEDED(result));
    FreeResource(res);

    // Аналогично для вершинного шейдера
    src = FindResource(hInstance, MAKEINTRESOURCE(IDR_BYTECODE_VERTEX), _T("ShaderObject"));
    res = LoadResource(hInstance, src);
    result = device->CreateVertexShader(res, SizeofResource(hInstance, src),
                                        NULL, &vertexShader);
    assert(SUCCEEDED(result));

    // Задаем, как в вершинный шейдер будут вводиться данные
    // Описание первого (и единственного) аргумента функции
    D3D11_INPUT_ELEMENT_DESC inputDesc;
    // Семантическое имя аргумента
    inputDesc.SemanticName = "POSITION";
    // Нужно только в случае, если элементов с данным семантическим именем больше одного
    inputDesc.SemanticIndex = 0;
    // Двумерный вектор из 32-битных вещественных чисел
    inputDesc.Format = DXGI_FORMAT_R32G32_FLOAT;
    // Необязательный аргумент
    inputDesc.AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    // Для каждой вершины
    inputDesc.InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    // Первый параметр
    inputDesc.InputSlot = 0;
    // Используем вершины для отрисовки
    inputDesc.InstanceDataStepRate = 0;

    result = device->CreateInputLayout(
        // Массив описаний аргументов и его длина
        &inputDesc, 1,
        // Байткод и его длина
        res, SizeofResource(hInstance, src),
        // Структура ввода
        &inputLayout);
    assert(SUCCEEDED(result));

    FreeResource(res);
}

void DisposeShaders()
{
    inputLayout->Release();
    computeShader->Release();
    vertexShader->Release();
    pixelShader->Release();
}

Код инициализации буферов:


ID3D11Buffer *positionBuffer;
ID3D11Buffer *velocityBuffer;

void InitBuffers()
{
    HRESULT result;

    float *data = new float[2 * PARTICLE_COUNT];
    // Описание массива, который будет записан в буфер при создании
    D3D11_SUBRESOURCE_DATA subresource;
    // Указатель на массив
    subresource.pSysMem = data;
    // Имеет значение только для текстур
    subresource.SysMemPitch = 0;
    // Имеет значение только для трехмерных текстур
    subresource.SysMemSlicePitch = 0;

    // Описание буфера
    D3D11_BUFFER_DESC desc;
    // Его размер
    desc.ByteWidth = sizeof(float[2 * PARTICLE_COUNT]);
    // Доступ на чтение и запись
    desc.Usage = D3D11_USAGE_DEFAULT;
    // Буфер позиций используем и для отрисовки, и при вычислениях как массив
    desc.BindFlags = D3D11_BIND_VERTEX_BUFFER | D3D11_BIND_UNORDERED_ACCESS;
    // Доступ с процессора не нужен
    desc.CPUAccessFlags = 0;
    // Дополнительные флаги не нужны
    desc.MiscFlags = 0;
    // Размер одного элемента буфера
    desc.StructureByteStride = sizeof(float[2]);

    // Инициализируем массив позиций
    for (int i = 0; i < 2 * PARTICLE_COUNT; i++)
        data[i] = 2.0f * rand() / RAND_MAX - 1.0f;

    // Создаем буфер позиций
    result = device->CreateBuffer(&desc, &subresource, &positionBuffer);
    assert(SUCCEEDED(result));

    // Буфер скоростей используется только при вычислениях как массив
    desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;

    // Инициализируем массив скоростей
    for (int i = 0; i < 2 * PARTICLE_COUNT; i++)
        data[i] = 0.0f;

    // Создаем буфер скоростей
    result = device->CreateBuffer(&desc, &subresource, &velocityBuffer);
    assert(SUCCEEDED(result));

    // Освобождаем память, использованную для инициализации
    delete[] data;
}

void DisposeBuffers()
{
    positionBuffer->Release();
    velocityBuffer->Release();
}

И код инициализации доступа к буферам из вычислительного шейдера:


ID3D11UnorderedAccessView *positionUAV;
ID3D11UnorderedAccessView *velocityUAV;

void InitUAV()
{
    HRESULT result;

    // Описание доступа к буферу из шейдера как к массиву
    D3D11_UNORDERED_ACCESS_VIEW_DESC desc;
    // Двумерный вектор из 32-битных вещественных чисел
    desc.Format = DXGI_FORMAT_R32G32_FLOAT;
    // Доступ к буферу, есть также варианты с текстурами
    desc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
    // Доступ с первого элемента
    desc.Buffer.FirstElement = 0;
    // Количество элементов
    desc.Buffer.NumElements = PARTICLE_COUNT;
    // Без дополнительных флагов
    desc.Buffer.Flags = 0;

    // Инициализация доступа к буферу позиций
    result = device->CreateUnorderedAccessView(positionBuffer, &desc,
                                               &positionUAV);
    assert(!result);
    // Инициализация доступа к буферу скоростей
    result = device->CreateUnorderedAccessView(velocityBuffer, &desc,
                                               &velocityUAV);
    assert(!result);
}

void DisposeUAV()
{
    positionUAV->Release();
    velocityUAV->Release();
}

Далее стоит указать драйверу использовать созданные шейдеры и связки с буферами:


void InitBindings()
{
    // Устанавливаем используемые шейдеры
    // Вычислительный
    deviceContext->CSSetShader(computeShader, NULL, 0);
    // Вершинный
    deviceContext->VSSetShader(vertexShader, NULL, 0);
    // Пиксельный
    deviceContext->PSSetShader(pixelShader, NULL, 0);

    // Устанавливаем доступ к буферу скоростей у вычислительного шейдера
    deviceContext->CSSetUnorderedAccessViews(1, 1, &velocityUAV, NULL);
    // Устанавливаем способ записи аргументов вершинного шейдера
    deviceContext->IASetInputLayout(inputLayout);
    // Рисовать будем точки
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
}

Для подсчета среднего времени кадра будем использовать следующий код:


const int FRAME_TIME_COUNT = 128;
clock_t frameTime[FRAME_TIME_COUNT];
int currentFrame = 0;

float AverageFrameTime()
{
    frameTime[currentFrame] = clock();
    int nextFrame = (currentFrame + 1) % FRAME_TIME_COUNT;
    clock_t delta = frameTime[currentFrame] - frameTime[nextFrame];
    currentFrame = nextFrame;
    return (float)delta / CLOCKS_PER_SEC / FRAME_TIME_COUNT;
}

А на каждом кадре — вызывать такую функцию:


void Frame()
{
    float frameTime = AverageFrameTime();

    // Выводим фреймрейт
    char buf[256];
    sprintf_s(buf, "average framerate: %.1f", 1.0f / frameTime);
    SetWindowTextA(hWnd, buf);

    // Очищаем буфер черным цветом
    float clearColor[] = { 0.0f, 0.0f, 0.0f, 0.0f };
    deviceContext->ClearRenderTargetView(renderTargetView, clearColor);

    // Двумерные вектора из 32-битных вещественных идут подряд
    UINT stride = sizeof(float[2]);
    UINT offset = 0;

    ID3D11Buffer *nullBuffer = NULL;
    ID3D11UnorderedAccessView *nullUAV = NULL;

    // Убираем доступ вершинного шейдера к буферу позиций
    deviceContext->IASetVertexBuffers(0, 1, &nullBuffer, &stride, &offset);
    // Устанавливаем доступ вычислительного шейдера к буферу позиций
    deviceContext->CSSetUnorderedAccessViews(0, 1, &positionUAV, NULL);
    // Вызываем вычислительный шейдер
    deviceContext->Dispatch(PARTICLE_COUNT / NUMTHREADS, 1, 1);

    // Убираем доступ вычислительного шейдера к буферу позиций
    deviceContext->CSSetUnorderedAccessViews(0, 1, &nullUAV, NULL);
    // Устанавливаем доступ вершинного шейдера к буферу позиций
    deviceContext->IASetVertexBuffers(0, 1, &positionBuffer, &stride, &offset);
    // Вызываем отрисовку
    deviceContext->Draw(PARTICLE_COUNT, 0);

    // Выводим изображение на экран
    swapChain->Present(0, 0);
}

На случай, если размер окна изменился, нам нужно также изменить размер буферов отрисовки:


void ResizeSwapChain()
{
    HRESULT result;

    RECT rect;
    // Получаем актуальные размеры окна
    GetClientRect(hWnd, &rect);
    windowWidth = rect.right - rect.left;
    windowHeight = rect.bottom - rect.top;

    // Для того, чтобы изменить размер изображения, нужно
    // освободить все указатели на "задний" буфер
    DisposeRenderTargetView();
    // Изменяем размер изображения
    result = swapChain->ResizeBuffers(
        // Изменяем размер всех буферов
        0,
        // Новые размеры
        windowWidth, windowHeight,
        // Не меняем формат и флаги
        DXGI_FORMAT_UNKNOWN, 0);
    assert(SUCCEEDED(result));
    // Создаем новый доступ к "заднему" буферу
    InitRenderTargetView();
}

Наконец, можно определить функцию обработки сообщений:


LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
    switch (Msg)
    {
    case WM_CLOSE:
        PostQuitMessage(0);
        break;

    case WM_KEYDOWN:
        if (wParam == VK_ESCAPE)
            PostQuitMessage(0);
        break;

    case WM_SIZE:
        ResizeSwapChain();
        break;

    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }

    return 0;
}

И функцию main:


int main()
{
    InitWindows();
    InitSwapChain();
    InitRenderTargetView();
    InitShaders();
    InitBuffers();
    InitUAV();
    InitBindings();

    ShowWindow(hWnd, SW_SHOW);

    bool shouldExit = false;
    while (!shouldExit)
    {
        Frame();

        MSG msg;
        while (!shouldExit && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);

            if (msg.message == WM_QUIT)
                shouldExit = true;
        }
    }

    DisposeUAV();
    DisposeBuffers();
    DisposeShaders();
    DisposeRenderTargetView();
    DisposeSwapChain();
    DisposeWindows();
}

Скриншот работающей программы можно увидеть в заголовке статьи.


> Проект целиком выложен на GitHub

Комментарии (17)


  1. HerrDirektor
    26.12.2018 15:35
    -3

    Напомнило, как в студенчестве (начало 90х) делал прогу на Си с кучей матана, которая рисовала нечто, напоминающее сосновую ветку с игрушкой (а-ля новогод). И все это крутилось под стандартной борландовской egavga.bgi, работало не сказать что сильно быстро (математик из меня так себе).
    Потащил ее на местечковый конкурс (уж дюже был горд собою, что сумел в матан). Но на конкурсе были два ботана, которые притащили две крутейших проги, целиком на ассемблере, которые рисовали какую-то симпатичную геометрию о 256 цветах и даже с некоторым подобием антиалиасинга.

    Короче, в список призеров я не попал :)


  1. maisvendoo
    26.12.2018 19:41
    -1

    DirectX? Вы серьезно?
    API используемый на одной единственной платформе в 2018 году? Хмм…


    1. asurkis Автор
      26.12.2018 19:54

      Я написал о том API, с которым у меня получилось добиться наибольшей производительности. OpenGL и Vulkan в моей реализации показали фреймрейт на 30% хуже, из чего я сделал вывод, что с ними у меня не получилось разобраться на достаточном уровне.
      К тому же, подробные «пошаговые» статьи про другие API мне также не удалось найти, возможно, их написание ещё имеет смысл (а возможно, плохо искал)


      1. maisvendoo
        26.12.2018 20:31

        OpenGL и Vulkan в моей реализации показали фреймрейт на 30% хуже, из чего я сделал вывод, что с ними у меня не получилось разобраться на достаточном уровне.

        Ещё бы, когда API пилится столько лет под одну и ту же платформу, фирмой разработчиком этой платформы. Было бы странно ожидать другой результат.

        К тому же, подробные «пошаговые» статьи про другие API мне также не удалось найти

        Первая же ссылка в гугле Learn OpenGL


        1. asurkis Автор
          26.12.2018 21:19
          +2

          Первая же ссылка в гугле Learn OpenGL

          Я неправильно выразился. Имел ввиду именно статьи, аналогичные этой, где подробно описывается простое применение вычислительного шейдера.
          К тому же LearnOpenGL описывает OpenGL 3.3, а вычислительные шейдеры официально добавлены в спецификацию в OpenGL 4.3


    1. Chaos_Optima
      27.12.2018 01:54
      +1

      DirectX? Вы серьезно?
      API используемый на одной единственной платформе в 2018 году? Хмм…

      А что не так? Использовать API под наиболее распространённую платформу вполне нормальное решение. Особенно если учесть что DX намного удобнее GL или Vulkan (имхо конечно, но на протяжении 9 лет что я работаю с графикой я по прежнему отдаю предпочтение DX)


      1. maisvendoo
        27.12.2018 09:40
        -1

        Использовать API под наиболее распространённую платформу вполне нормальное решение

        Мобильные устройства учитываете? Какой там API? Какова доля мобильных устройств в сравнении и десктопом? Каков оборот на рынке мобильного геймдева?

        Возьмем рынок игровых консолей. Да Xbox One использует directx. А PS4 и Nd swith — не используют.

        Какой смысл использовать для новых проектов API, имеющий хождение только на десктопах и одной, не самой популярной консоли? При этом совершенно не портируемый ни под что другое? В эпоху когда кроссплатформенность это маст хэв для нормального проекта. Я не вижу логики


        1. Chaos_Optima
          27.12.2018 10:24

          Мобильные устройства учитываете?

          Нет естественно, речь была только про десктоп.
          Возьмем рынок игровых консолей. Да Xbox One использует directx. А PS4 и Nd swith — не используют.

          Каждая из консолей использует своё GAPI (PS4 и ND не используют OGL) так что какая разница, DX хотя бы 2 платформы покрывает.
          В эпоху когда кроссплатформенность это маст хэв для нормального проекта.

          С чего вы взяли что это мастхэв? Большинство ААА игр выпускаются, только под винду (да на другие ос они не заглядывают) и консоли. А мобильные игры и игры под пс в любом случае имеют сильно разные подходи к использованию GAPI так что в большинстве случаев без разницы какое GAPI используешь все равно придётся всё переписать.
          Так что нету на данный момент поистине кросплатформенного GAPI, поэтому я буду выбирать то что удобнее.
          К тому же OGL больше не развивается, расширения не угадаешь где работают нормально а где бажат безбожно. Альтернатива конечно вулкан, но он ещё более геморный по сравнению с DX. Писать на OGL в сравнении с DX это всё равно что писать на С вместо С# при том что C# в данном случае быстрее. Да кросплатформенно но очень неудобно.


  1. Gromin
    26.12.2018 20:21

    Я может невнимательно смотрел, но это не C++.


    1. asurkis Автор
      26.12.2018 21:25
      -1

      __uuidof(...)
      — это расширение компилятора Visual C++, возвращает GUID заданного выражения.
      Вас это смутило?


    1. dev96
      26.12.2018 23:28

      вы к тому, что это больше на чистый С похоже?


    1. Chaos_Optima
      27.12.2018 01:50

      А то что в DirectX почти всё это классы и вызовы методов вас не смутил?


      1. dev96
        27.12.2018 02:25

        Не то, что классы, а даже интерфейсы)
        Но все же под C++ подразумевают многие не С с добавкой ООП.

        Вообще предлагаю не начинать дискуссию на эту тему. Все таки, статья посвящена написанию вычислительного шейдера и автор был намерен сосредоточиться на этом вопросе.


  1. dev96
    26.12.2018 23:30

    А вы давно на «C++» пишете?
    Советую не учится плохим вещам из MSDN (например, тот же NULL) и вообще не использовать препроцессор для создания констант.


    1. asurkis Автор
      27.12.2018 00:24

      В данном случае невозможно выразить NUMTHREADS через константу, т.к. [numthreads(...)] принимает только литералы. Чтобы эта константа была определена в одном месте (а используется она в шейдере и в вызывающем коде), использовал #define — очень редкая ситуация, в которой можно попытаться оправдать макрос. PARTICLE_COUNT задефайнен для единообразия.
      На счет NULL или nullptr здесь не уверен, вероятно, Вы правы.
      Оценку своего опыта в плюсах давать не рискну, т.к. этот опыт делится на олимпиадные задачи, мелкие проекты по типу приведенного в статье и заброшенные незавершенные проекты (в основном попытки создания игр). Если же интересует именно время — примерно с 2013.


  1. kovserg
    27.12.2018 15:22

    Вот настоящий, современный С++17.
    ps: Надо было сразу на vulkan делать что бы как-то усложнить задачу.


    1. asurkis Автор
      27.12.2018 15:45

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