ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [DirectX11] DrawIndexedInstanced로 여러개의 인스턴스 그리기
    DirectX11 2023. 12. 7. 22:29

     

    여러개의 동일한 모델을 그려야 하는 경우 DrawIndexed를 여러번 호출하는 대신 DrawIndexedInstanced를 사용하면 한번의 드로우 콜으로 여러개의 모델을 그릴 수 있다.

    각 인스턴스들의 정보를 배열의 형태로 버퍼를 통해 전달하면 DrawIndexedInstanced에 지정한 횟수만큼 배열의 인덱스를 늘리면서 각 인스턴스의 정보에 맞게 그린다.

     

     

     


     

     

     

    필요한 것은 다음과 같다.

    • 인스턴스의 정보를 담을 구조체
    • 인스턴스의 정보를 GPU로 보내줄 버퍼
    • 인스턴스용 버텍스 쉐이더 또는 픽셀 쉐이더

     

    인스턴스의 정보는 간단하게 월드 변환 매트릭스만 전달하겠다.

    struct InstanceInfo
    {
        XMMATRIX mInstanceToWorld;
    };
    
    // ...
    
    int g_InstanceNum = 2;
    InstanceInfo g_Instances[] =
    {
        { XMMatrixTranspose(XMMatrixIdentity()) },
        { XMMatrixTranspose(XMMatrixTranslation(4.f, 0.f, 0.f)) }
    };

    글로벌 변수로 각 인스턴스의 정보를 저장해두기로 했다.

    하나는 원점에 그대로 두고, 다른 하나는 x방향으로 4만큼 이동시킨다.

     

    그 다음에는 해당 정보를 담을 버퍼를 생성한다.

    ID3D11Buffer* g_pInstanceBuffer = NULL;
    
    // ...
    HRESULT InitDevice()
    {
        // ...
        
        // Create Instance buffer
        D3D11_BUFFER_DESC instanceBufferDesc;
        ZeroMemory(&instanceBufferDesc, sizeof(D3D11_BUFFER_DESC));
        instanceBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
        instanceBufferDesc.ByteWidth = sizeof(InstanceInfo) * g_InstanceNum;
        instanceBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
        instanceBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
        instanceBufferDesc.MiscFlags = 0;
        instanceBufferDesc.StructureByteStride = 0;
    
        D3D11_SUBRESOURCE_DATA instanceData;
        ZeroMemory(&instanceData, sizeof(D3D11_SUBRESOURCE_DATA));
        instanceData.pSysMem = g_Instances;
        hr = g_pd3dDevice->CreateBuffer(&instanceBufferDesc, &instanceData, &g_pInstanceBuffer);
        if (FAILED(hr))
            return hr;
            
        g_pImmediateContext->VSSetConstantBuffers(4, 1, &g_pInstanceBuffer);
        
        // ...
    }

    인스턴스의 정보를 계속 업데이트 하기 위해서 버퍼 디스크립션에서 UsageD3D11_USAGE_DYNAMIC으로, CPUAccessFlagsD3D11_CPU_ACCESS_WRITE로 지정했다.

    초기 데이터는 위의 배열을 지정했다.

    상수 버퍼의 다른 슬롯이 이미 사용중이여서 4번 슬롯에 지정했다.

     


     

    다음으로 버텍스 쉐이더를 인스턴스용으로 새로 만든다.

    // HLSL
    
    cbuffer cbInstanceInfo : register(b4)
    {
        matrix mInstanceToWorld[2];
    }
    
    // ...
    
    VS_OUTPUT VS_Instance(VS_INPUT input, uint instanceID : SV_InstanceID)
    {
        VS_OUTPUT output = (VS_OUTPUT) 0;
        input.Pos.w = 1.f;
        output.Pos = mul(input.Pos, mInstanceToWorld[instanceID]);
        output.Pos = mul(output.Pos, mView);
        output.Pos = mul(output.Pos, mViewProjection);
        
        // 나머지는 기존의 버텍스 쉐이더가 있다면 그것과 동일하게 해도 됨.
        
        return output;
    }

    4번 슬롯의 상수 버퍼에 들어있는 데이터를 배열로 받는다.

    지금은 인스턴스가 2개뿐이어서 배열의 크기도 2로 했다.

    그리고 버텍스 쉐이더의 파라미터에는 uint instanceID : SV_InstanceID 가 추가된다.

    instanceID라는 입력 값에 SV_InstanceID 시맨틱을 지정해둔 것으로, 접두사 SV가 붙으면 System Value라는 뜻이다.

    시스템에서 사용하는 값으로 시스템에서 해당 값에 인스턴스의 인덱스를 넣어준다고 생각하면 될듯 하다.

    이 값을 이용해서 인스턴스의 정보가 담긴 배열의 값에 접근하면 된다.

    지금은 월드 변환 매트릭스만 넣어놨으니 입력으로 들어온 정점의 좌표를 월드 좌표로 변환하는 부분만 바꾸면되고 나머지는 동일하다.

     


     

    마지막으로 인스턴스용 버텍스 쉐이더를 Context에 지정해둔 다음에 DrawIndexedInstanced를 호출하면 된다.

    물론 그 전에 버텍스 버퍼와 인덱스 버퍼에는 해당하는 정보가 지정되어있어야 한다.

    // Render
    g_pImmediateContext->VSSetShader(g_pInstanceVertexShader, NULL, 0);
    g_pImmediateContext->DrawIndexedInstanced(Mesh->GetIndexNum(), g_InstanceNum, 0, 0, 0);

     

    렌더 결과

    정육면체 두개를 x방향으로 4만큼 띄워서 렌더했다.

     


     

    만약 인스턴스의 정보를 업데이트하고싶다면 버퍼를 매핑하는 방법을 쓰면 된다.

    // Update Instance
    g_Instances[1].mInstanceToWorld = XMMatrixTranspose(XMMatrixRotationY(CurrentTime * XM_PI / 2) * XMMatrixTranslation(4.f, 0.f, 0.f));
    
    // 인스턴스 버퍼 매핑
    D3D11_MAPPED_SUBRESOURCE mappedData;
    g_pImmediateContext->Map(g_pInstanceBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
    // 매핑된 메모리에 업데이트된 인스턴스 데이터 복사
    memcpy(mappedData.pData, g_Instances, sizeof(InstanceInfo) * g_InstanceNum);
    // 인스턴스 버퍼 매핑 해제
    g_pImmediateContext->Unmap(g_pInstanceBuffer, 0);

    g_Instances는 각 인스턴스의 정보를 가지고있는 배열이다.

    두번째 인스턴스의 정보를 변경해서 y축 기준으로 회전시킨 다음에 원래의 위치인 x방향으로 4만큼 이동시켰다.

    이렇게 하면 오프셋은 유지한 채로 제자리에서 회전하는것처럼 보인다.

     

    그 다음 인스턴스 정보가 들어있는 버퍼를 mappedData으로 매핑해와서 업데이트된 g_Instances 배열의 값을 넣어준다.

    버퍼가 매핑되어있는 동안에는 GPU에서 해당 버퍼를 접근할 수 없으므로 마지막으로 매핑을 해제해야 한다.

     

    참고로 memcpy 말고 아래와 같은 방법으로 값을 설정할 수도 있다.

    InstanceInfo* data = (InstanceInfo*)mappedData.pData;
    data[0] = g_Instances[0];
    data[1] = g_Instances[1];

     

    Map 함수를 사용할 때 세번째 파라미터로 매핑 방식을 지정할 수 있고 5가지 방법이 있다.

    방식에 따라서 기존의 값을 그대로 가져올 수도 있고, 비워져있는 상태로 가져올 수도 있다.

    기존의 값을 유지한 채로 일부만 변경하려면 D3D11_MAP_WRITE_NO_OVERWRITE을 쓰면 된다.

    자세한건 https://learn.microsoft.com/ko-kr/windows/win32/api/d3d11/ne-d3d11-d3d11_map

     

    D3D11_MAP(d3d11.h) - Win32 apps

    CPU에서 읽고 쓰기 위해 액세스할 리소스를 식별합니다. 애플리케이션은 이러한 플래그 중 하나 이상을 결합할 수 있습니다. (D3D11_MAP)

    learn.microsoft.com

     

    매 틱마다 인스턴스의 정보를 업데이트한 결과는 아래와 같다.

     

     

     


     

     

     

    마지막으로 굳이 하면 이렇게도 할 수 있다.

     

    월드 변환 매트릭스에 색상 정보도 추가했다.

     

    이렇게 해보고 알게된 건데 새로운 정보를 추가하려면 같은 버퍼에 넣으면 안되고 다른 버퍼에 따로 넣어야하는듯 하다.

    아무래도 GPU에서는 버퍼의 값을 배열로 읽다보니 하나의 구조체에 두개의 정보를 넣으면 각각의 정보를 두개의 인스턴스가 나눠 가지게 되는 듯 하다.

    이 예시처럼 매트릭스와 벡터를 전달하게 되면 첫번째 인스턴스는 매트릭스만 받고, 두번째 인스턴스는 벡터만 받아서 두번째 인스턴스는 벡터를 이용해서 월드 변환 매트릭스에 사용하는듯 했다.

    더 따지고보면 매트릭스는 4x4고 벡터는 1x4이라서 에러는 발생하지 않지만, 두번째 인스턴스의 경우에는 컬러 벡터와 그 다음 구조체의 월드 변환 매트릭스중 3번째 행까지 이어붙혀진 4x4 매트릭스가 월드 변환 매트릭스로 사용될 듯 하다.

     

    그림으로 표현하면 이런 느낌이지 않을까 싶다.

     

    초록색이 매트릭스고 노란색이 벡터다.

     

    개인적인 추측일 뿐이고 확인된 것은 아니다.

     

     

Designed by Tistory.