Создание экземпляров в DirectX 11

Я пытаюсь создавать мозаичную 2D-графику в Direct3D 11. Плиточная 2D-графика — это, по сути, просто множество текстурированных четырехугольников. Кажется естественным использовать для этого создание экземпляров вместо рисования каждого квадрата по отдельности. Рисование одного квадрата работает, но если я попытаюсь использовать создание экземпляров для рисования двух квадратов, я получу пустой экран. Если я все правильно понимаю, идея состоит в том, что вы создаете макет, который имеет макет буфера вершин в слоте 0 и макет буфера экземпляра в слоте 1. Вы создаете буфер вершин с данными вершин и буфер экземпляра с данными экземпляра. . Вы помещаете их в массив. Это массив указателей (это правда?). Индекс в этом массиве соответствует номеру слота в макете. Вы получаете данные вершин и данные индекса в вершинном шейдере и делаете с ними свои дела. А затем нарисуйте его с помощью DrawIndexedInstanced. Как обычно в DirectX, понять это концептуально — это легко, а реализовать это на самом деле — сложнее.

Вот код (я знаю, что стиль именования не совсем последовательный, часть скопирована из руководства).

Определение макета:

    D3D11_INPUT_ELEMENT_DESC inputElementDesc[] =
    {
        { "POS", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        { "TEX", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 },
        {"INSTANCEPOS", 0, DXGI_FORMAT_R32G32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1},
        {"INSTANCETEX", 0, DXGI_FORMAT_R32G32_FLOAT, 1, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_INSTANCE_DATA,1}
    };

    HRESULT hResult = m_device->CreateInputLayout(inputElementDesc, ARRAYSIZE(inputElementDesc), vs_bytecode->GetBufferPointer(), vs_bytecode->GetBufferSize(), &m_layout);
    assert(SUCCEEDED(hResult));
    vs_bytecode->Release();```

Binding of the layout:

m_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_context->IASetInputLayout(m_layout.Get());  // m_layout is a ComPtr

Vertex buffer header file:


#pragma once

#include "bindable.h"
#include "base_types.h"

#include <wrl.h>
#include <DirectXMath.h>

#include <array>

struct Vertex
{
    DirectX::XMFLOAT2 pos;
    DirectX::XMFLOAT2 tex_coords;
};

struct Instance_data
{
    DirectX::XMFLOAT2 instance_pos;
    DirectX::XMFLOAT2 instance_tex;
};


{
public:
class Vertex_buffer : public Bindable
    Vertex_buffer(ID3D11DeviceContext1* context, ID3D11Device1* device, Rect vertex_pos, Rect texture_pos);
    void bind() noexcept override;
    int add_instance(Vec2 pos, Vec2 texture_coords);
private:
    inline constexpr static size_t num_instances{ 2 };
    std::array<Instance_data, num_instances> m_instance_data;
    std::array<ID3D11Buffer*, 2> m_buffers;
    std::array<UINT, 2> m_strides{ sizeof(Vertex), sizeof(Instance_data) };
    std::array<UINT, 2> m_offsets{ 0u, 0u };
    size_t next_index{ 0 };
};    
  


Vertex buffer c++ file

#include "vertex_buffer.h"

#include <array>
#include <vector>

Vertex_buffer::Vertex_buffer(ID3D11DeviceContext1* context, ID3D11Device1* device, Rect vertex_pos, Rect texture_pos) : Bindable{ context, device }
{
    std::array<Vertex, 4> vertices;
    vertices[0]=(Vertex{ DirectX::XMFLOAT2{ vertex_pos.x,  vertex_pos.y }, DirectX::XMFLOAT2{ texture_pos.x, texture_pos.y } });
    vertices[1]=(Vertex{ DirectX::XMFLOAT2 {vertex_pos.x + vertex_pos.width, vertex_pos.y - vertex_pos.height}, DirectX::XMFLOAT2{ texture_pos.x + texture_pos.width, texture_pos.y + texture_pos.height } });
    vertices[2] = (Vertex{ DirectX::XMFLOAT2 { vertex_pos.x, vertex_pos.y - vertex_pos.height }, DirectX::XMFLOAT2{ texture_pos.x, texture_pos.y + texture_pos.height } });
    vertices[3] = (Vertex{ DirectX::XMFLOAT2 {vertex_pos.x + vertex_pos.width,  vertex_pos.y},  DirectX::XMFLOAT2{texture_pos.x + texture_pos.width, texture_pos.y} });

    UINT numVerts = vertices.size();

    D3D11_BUFFER_DESC vertexBufferDesc = {};
    vertexBufferDesc.ByteWidth = sizeof(vertices);//m_stride * numVerts;//sizeof(vertexData);
    vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;

    D3D11_SUBRESOURCE_DATA vertexSubresourceData = { vertices.data() };

    //HRESULT hResult = device->CreateBuffer(&vertexBufferDesc, &vertexSubresourceData, &m_vertex_buffer);
    HRESULT hResult = device->CreateBuffer(&vertexBufferDesc, &vertexSubresourceData, &m_buffers[0]);
    assert(SUCCEEDED(hResult));

    D3D11_BUFFER_DESC instance_buffer_desc{};
    instance_buffer_desc.Usage = D3D11_USAGE_DEFAULT;
    instance_buffer_desc.ByteWidth = sizeof(Instance_data) * num_instances;
    instance_buffer_desc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    instance_buffer_desc.CPUAccessFlags = 0;
    instance_buffer_desc.MiscFlags = 0;

    D3D11_SUBRESOURCE_DATA subres_data{};
    subres_data.pSysMem = m_instance_data.data();
  
    HRESULT hr = device->CreateBuffer(&instance_buffer_desc, &subres_data, &m_buffers[1]);
    assert(SUCCEEDED(hr));
}

void Vertex_buffer::bind() noexcept
{
    m_context->IASetVertexBuffers(0u, 2u, m_buffers.data(), m_strides.data(), m_offsets.data());
}

int Vertex_buffer::add_instance(Vec2 pos, Vec2 texture_coords)
{
    m_instance_data[next_index] = Instance_data{ {pos.x, pos.y}, {texture_coords.x, texture_coords.y} };
    return next_index++;
}

shader file

struct VS_Input {
    float2 pos : POS;
    float2 uv : TEX;
    float2 instancepos : INSTANCEPOS;
    float2 instancetex : INSTANCETEX;
};

struct VS_Output {
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD;
};

Texture2D    mytexture : register(t0);
SamplerState mysampler : register(s0);

VS_Output vs_main(VS_Input input)
{
    VS_Output output;
    output.pos = float4(input.instancepos, 0.0f, 1.0f);
    output.uv = input.instancetex;
    return output;
}

float4 ps_main(VS_Output input) : SV_Target
{
    return mytexture.Sample(mysampler, input.uv);   
}

In the main function (there is of course a lot of code that creates the window and does stuff that I know that works)


Vertex_buffer vertex_buffer {d3d11DeviceContext, d3d11Device, vertex_pos,texture_pos}; // вычисляется vertex_pos в диапазоне -1..1 и text_pos в диапазоне 0..1

// выполняем данные экземпляра

constexpr unsigned int num_instances{ 2u };
struct Instance_data   // yes, this is defined in 2 places, that should be improved
{
    DirectX::XMFLOAT2 instance_pos;
    DirectX::XMFLOAT2 instance_tex;
};
std::array<Instance_data, num_instances> instance_data;
Rect v1 = calc_vertex_pos(3.0f, 32.0f, 32.0f, screenwidth, screenheight, 4, 3); // these functions that calculate vertex and texture coordinates work correctly
Rect v2 = calc_vertex_pos(3.0f, 32.0f, 32.0f, screenwidth, screenheight, 3, 2);
Rect t1 = calc_texture_pos(32.0f, 32.0f, 10, 3, 8);
Rect t2 = calc_texture_pos(32.0f, 32.0f, 10, 3, 15);
auto inst0 = vertex_buffer.add_instance({ v1.x, v1.y }, { t1.x, t1.y });
auto inst1 = vertex_buffer.add_instance({ v2.x, v2.y }, { t1.x, t2.y });

in the main loop:


// bind all the things

// ...
layout.bind();
vertex_buffer.bind();
index_buffer.bind();

d3d11DeviceContext->DrawIndexedInstanced(index_buffer.count(), 2u, 0u, 0u, 0u);
d3d11SwapChain->Present(1, 0);

The result is a blank screen. It all compiles and runs, no assertions are hit. I suspect that for some reason the data doesn't get into the shader correctly but I am not aware of any way to debug that.

A few related questions:
1- I assume that binding stuff includes sending the associated data to the graphics hardware. Is it efficient to bind everything every frame (I assume not). Would it work if I only bind things if there are mutations?
2- The instancebuffer description takes the size of the buffer in bytes. Of course it is just a C array underneath. That means that the size is essentially fixed. But the number of objects on the screen can vary: enemies, treasure, bullets and the like can spawn and get destroyed. How do you do that with instancing? Instance only the fixed objects like walls and floors? Or recreate the instance buffer when the number of instances changes? Is recreating the buffer expensive or cheap?
3- Are these semantics names in the shader case-insensitive? I sometimes see SV_POSITION and sometimes SV_Position. Would sv_position also work?

Fiddled around with it quite a bit. Expected two textured quads, got a blank screen.

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

Theo 31.08.2024 13:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Никаких реакций вообще. Кажется, я единственный человек в Интернете, который пытается это сделать ;-). У меня пока это не работает, но у меня есть пара наблюдений, которые могут быть полезны другим, кто попытается сделать что-то подобное.

  1. Документация MSDN не очень хороша и довольно расплывчата. Например, когда вы ищете CreateBuffer(), он скажет, что эта функция создает буфер. Да, я могу сделать это из названия функции. Но что это на самом деле делает? Копирует ли он данные буфера на сторону графического процессора? Это не говорит.
  2. На самом деле он не говорит, что делает D3D11_INPUT_PER_INSTANCE_DATA. Оказывается, он просто дает одно и то же значение для всех вершин экземпляра. Итак, если у вас есть буфер экземпляра с двумя элементами, он передаст данные с индексом 0 всем вершинам экземпляра 0, а данные с индексом 1 — всем вершинам экземпляра 1.
  3. Я попытался создать буфер экземпляра с помощью D3D11_INPUT_PER_VERTEX_DATA, надеясь, что это даст данные для каждой вершины и каждого экземпляра. Оказывается, это не работает. CresteBuffer завершается сбоем с «неверным аргументом». Я не делал этого со всеми активными слоями отладки, но, похоже, ему нужен только один слот с D3D11_INPUT_PER_VERTEX_DATA.
  4. Предполагается, что создание экземпляров должно создавать несколько экземпляров с одинаковой геометрией. Что именно означает «та же геометрия»? Это не говорит. Я предположил, что, поскольку буфер вершин определяет относительные расстояния между соответствующими значениями вершин, создание экземпляров будет отражать те же самые относительные расстояния. Таким образом, будет создан новый треугольник того же размера и формы, только в другом положении. Оказывается, это не так уж и умно. Единственное, что делает «одна и та же геометрия», это то, что она знает, что все экземпляры имеют одинаковое количество вершин, один и тот же тип примитива (например, список треугольников), и использует один и тот же индексный буфер для создания примитивов из вершин. Ничего больше. В результате он просто делает то, что находится в вершинном шейдере, с одинаковыми данными для каждой вершины. Это нормально для матрицы преобразования: вы хотите, чтобы все вершины были преобразованы с помощью одной и той же матрицы, но с данными о положении или координатами текстуры. Это означает, что она рисует все вершины с одинаковой позицией и одинаковыми координатами текстуры, что приводит к пустому экрану, поскольку все вершины рисуются в одном пикселе.
  5. Оказывается, CreateBuffer делает снимки данных буфера. Шейдер увидит данные так, как будто они есть в момент вызова CreateBuffer. Более поздние изменения не будут отражены на стороне шейдера. Кажется, не существует способа изменить данные на стороне шейдера, кроме уничтожения буфера вершин/экземпляров и его повторного создания с другими данными.
  6. Следствием всего этого является то, что буфер экземпляра на самом деле подходит только для данных, одинаковых для всех вершин экземпляра, и которые довольно статичны. Для меня это звучит как довольно ограниченный вариант использования. Вероятно, они действительно думали о матрицах преобразования.
  7. Итак, мне немного надоели буферы экземпляров. Я думаю, что лучше всего использовать постоянный буфер. Буферы констант на самом деле не структурированы, но вы можете помещать туда данные для каждой вершины и каждого экземпляра. А константные буферы (несмотря на название) не такие уж и константные. Вы можете использовать функцию Map(), чтобы сопоставить ее с памятью процессора, внести изменения и зафиксировать их с помощью функции Unmap(). Конечно, MSDN на самом деле не сообщает, что происходит при Unmap(). Будет ли он копировать все данные или только дельты? Это не говорит. Учитывая мой опыт, я бы не стал делать ставку на его сверхумность и просто предположил бы, что он копирует все данные.
  8. Итак, я думаю, я бы сделал что-то вроде создания буфера констант, содержащего позиции и координаты текстур всех вершин и всех экземпляров, а в шейдере сделал бы что-то вроде:
    #define verts_per_instance 4
    #define num_entities 2
    struct Instance_data
    {
       float2 position;
       float2 texture_coords;
    };
    cbuffer cb
    {
       Instance_data instance[verts_per_instance * num_entities];
    };
    struct VS_in
    {
      uint instance_id : SV_InstanceID;
      uint vertex_id : SV_VertexID;
    };
       
    struct VS_out
    {
       float4 pos : SV_Position;
       float4 tex : TEXCOORD;
    };

    VS_out main(VS_in input)
    {
       VS_out out;
       out.pos = float4(instance[instance_id * verts_per_instance + vertex_id].position.x, instance[instance_id * verts_per_instance + vertex_id].position.y, 0.0f, 1.0f);
       // essentially the same for the texture coords;
       return out;
    }

Еще не пробовал, но думаю, что у этого способа больше шансов сработать.

Ответ принят как подходящий

Хорошо, пришла идея и все заработало! С буфером экземпляра.

Хитрость в том, что все экземпляры имеют доступ к данным буфера вершин. Таким образом, каждая вершина знает относительное положение вершины в буфере вершин. В буфере экземпляра вы сохраняете перевод всего объекта и добавляете его ко всем координатам вершин.

Итак, рецепт такой: для буфера вершин определите правильную форму, но в верхнем левом углу окна (-1, 1). для каждого экземпляра вычислите вектор перемещения (сколько вам нужно добавить к координатам x и y, чтобы попасть в новую позицию). в шейдере сделайте это дополнение. Что касается координат текстуры, то это примерно то же самое: координаты верхней левой текстуры помещаются в буфер вершин (здесь, чтобы не было единообразия, координаты изменяются от (0,0) до (1,1)). в данные экземпляра поместите вектор перевода. в шейдере добавьте вектор перевода к координатам в буфере вершин.

Теперь шейдер будет выглядеть так:

struct VS_Input 
{
    float2 pos : POS;
    float2 uv : TEX;
    float2 instancepos : INSTANCEPOS;
    float2 instancetex : INSTANCETEX;
    uint instance_id : SV_InstanceID;
};

struct VS_Output 
{
    float4 pos : SV_POSITION;
    float2 uv : TEXCOORD;
};

VS_Output vs_main(VS_Input input)
{
    VS_Output output;
    output.pos = float4(input.pos.x + input.instancepos.x, input.pos.y - input.instancepos.y, 0.0f, 1.0f);
    output.uv = float2(input.uv.x + input.instancetex.x, input.uv.y + input.instancetex.y);
    return output;
}

Другие вопросы по теме