여러 기하 도형 인스턴스를 효율적으로 그리기(Direct3D 9)

동일한 기하 도형을 사용하는 많은 개체가 포함된 장면의 경우 렌더러에 제공해야 하는 데이터의 양을 줄여 다양한 방향, 크기, 색 등에서 해당 기하 도형의 많은 인스턴스를 훨씬 더 나은 성능으로 그릴 수 있습니다.

이 작업은 인덱싱된 기하 도형을 그리는 첫 번째 기술과 인덱싱되지 않은 기하 도형의 경우 두 번째 기술을 사용하여 수행할 수 있습니다. 두 기술 모두 두 개의 꼭짓점 버퍼를 사용합니다. 하나는 기하 도형 데이터를 제공하고 다른 하나는 개체별 instance 데이터를 제공합니다. instance 데이터는 변환, 색 데이터 또는 조명 데이터와 같은 다양한 정보일 수 있습니다. 기본적으로 꼭짓점 선언에서 설명할 수 있는 모든 정보입니다. 이러한 기술을 사용하여 기하 도형의 많은 인스턴스를 그리면 렌더러로 전송되는 데이터의 양을 크게 줄일 수 있습니다.

인덱싱된 기하 도형 그리기

꼭짓점 버퍼에는 꼭짓점 선언으로 정의된 꼭짓점별 데이터가 포함됩니다. 각 꼭짓점의 일부에 기하 도형 데이터가 포함되어 있고 각 꼭짓점의 일부가 다음 다이어그램과 같이 개체별 instance 데이터를 포함하고 있다고 가정합니다.

인덱싱된 기하 도형에 대한 꼭짓점 버퍼 다이어그램

이 기술을 사용하려면 3_0 꼭짓점 셰이더 모델을 지원하는 디바이스가 필요합니다. 이 기술은 프로그래밍 가능한 셰이더에서 작동하지만 고정 함수 파이프라인에서는 작동하지 않습니다.

위에 표시된 꼭짓점 버퍼의 경우 해당 꼭짓점 버퍼 선언은 다음과 같습니다.

const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] =
{
{0,  0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,   0},
{0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT,  0},
{0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0},
{0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};

const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] =
{
{1, 0,  D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1},
{1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2},
{1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3},
{1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4},
{1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR,    0},
D3DDECL_END()
};

이러한 선언은 두 개의 꼭짓점 버퍼를 정의합니다. 첫 번째 선언(스트림 0의 경우 열 1의 0으로 표시됨)은 위치, 일반, 탄젠트, 이진 및 텍스처 좌표 데이터로 구성된 기하 도형 데이터를 정의합니다.

두 번째 선언(1열에 있는 것으로 표시된 스트림 1의 경우)은 개체별 instance 데이터를 정의합니다. 각 instance 네 개의 4개 구성 요소 부동 소수점 숫자와 4개 구성 요소 색으로 정의됩니다. 처음 네 개의 값을 사용하여 4x4 행렬을 초기화할 수 있습니다. 즉, 이 데이터는 기하 도형의 각 instance 고유하게 크기, 위치 및 회전합니다. 처음 4개 구성 요소는 텍스처 좌표 의미 체계를 사용합니다. 이 경우 "일반적인 4개 구성 요소 번호"를 의미합니다. 꼭짓점 선언에서 임의의 데이터를 사용하는 경우 텍스처 좌표 의미 체계를 사용하여 표시합니다. 스트림의 마지막 요소는 색 데이터에 사용됩니다. 이는 꼭짓점 셰이더에 적용되어 각 instance 고유한 색을 제공할 수 있습니다.

렌더링하기 전에 SetStreamSourceFreq 를 호출하여 꼭짓점 버퍼 스트림을 디바이스에 바인딩해야 합니다. 다음은 두 꼭짓점 버퍼를 모두 바인딩하는 예제입니다.

// Set up the geometry data stream
pd3dDevice->SetStreamSourceFreq(0,
    (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw));
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
    D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1,
    (D3DSTREAMSOURCE_INSTANCEDATA | 1));
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
    D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

SetStreamSourceFreqD3DSTREAMSOURCE_INDEXEDDATA 사용하여 인덱싱된 기하 도형 데이터를 식별합니다. 이 경우 스트림 0에는 개체 기하 도형을 설명하는 인덱싱된 데이터가 포함됩니다. 이 값은 그릴 기하 도형의 인스턴스 수와 논리적으로 결합됩니다.

D3DSTREAMSOURCE_INDEXEDDATA 및 그릴 인스턴스 수는 항상 스트림 0으로 설정해야 합니다.

두 번째 호출에서 SetStreamSourceFreqD3DSTREAMSOURCE_INSTANCEDATA 사용하여 instance 데이터가 포함된 스트림을 식별합니다. 각 꼭짓점은 하나의 instance 데이터 집합을 포함하므로 이 값은 논리적으로 1과 결합됩니다.

SetStreamSource에 대한 마지막 두 호출은 꼭짓점 버퍼 포인터를 디바이스에 바인딩합니다.

instance 데이터 렌더링을 마치면 꼭짓점 스트림 빈도를 기본 상태로 다시 설정해야 합니다(인스턴싱을 사용하지 않음). 이 예제에서는 두 개의 스트림을 사용했기 때문에 아래와 같이 두 스트림을 모두 설정합니다.

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

인덱싱된 기하 도형 성능 비교

이 기술이 모든 애플리케이션에서 렌더링 시간을 줄일 수 있는 양에 대해 단 하나의 결론을 내릴 수는 없지만 런타임으로 스트리밍되는 데이터의 양과 인스턴싱 기술을 사용하는 경우 감소될 상태 변경 횟수의 차이를 고려합니다. 이 렌더링 시퀀스는 동일한 기하 도형의 여러 인스턴스를 그리는 데 활용합니다.

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    // Set up the geometry data stream
    pd3dDevice->SetStreamSourceFreq(0,
                (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw));
    pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

    // Set up the instance data stream
    pd3dDevice->SetStreamSourceFreq(1,
                (D3DSTREAMSOURCE_INSTANCEDATA | 1));
    pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
                D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

    pd3dDevice->SetVertexDeclaration( ... );
    pd3dDevice->SetVertexShader( ... );
    pd3dDevice->SetIndices( ... );

    pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    
    pd3dDevice->EndScene();
}

렌더링 루프가 한 번 호출되고, 기하 도형 데이터가 한 번 스트리밍되고, n개의 인스턴스가 한 번 스트리밍됩니다. 다음 렌더링 시퀀스는 기능에서 동일하지만 인스턴싱을 활용하지는 않습니다.

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    for(int i=0; i < g_numObjects; i++)
    {
        pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));


        pd3dDevice->SetVertexDeclaration( ... );
        pd3dDevice->SetVertexShader( ... );
        pd3dDevice->SetIndices( ... );

        pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    }                             
    
    pd3dDevice->EndScene();
}

전체 렌더링 루프는 두 번째 루프로 래핑되어 각 개체를 그립니다. 이제 기하 도형 데이터가 렌더러에 한 번이 아닌 n번 스트리밍되고 모든 파이프라인 상태가 그려진 각 개체에 대해 중복으로 설정될 수도 있습니다. 이 렌더링 시퀀스는 상당히 느릴 가능성이 높습니다. 또한 DrawIndexedPrimitive에 대한 매개 변수는 두 렌더링 루프 간에 변경되지 않았습니다.

인덱싱되지 않은 기하 도형 그리기

인덱싱된 기하 도형 그리기에서 꼭짓점 버퍼는 효율성이 더 높은 인덱싱된 기하 도형의 여러 인스턴스를 그리도록 구성되었습니다. SetStreamSourceFreq를 사용하여 인덱싱되지 않은 기하 도형을 그릴 수도 있습니다. 이렇게 하려면 다른 꼭짓점 버퍼 레이아웃이 필요하며 제약 조건이 다릅니다. 인덱싱되지 않은 기하 도형을 그리려면 다음 다이어그램과 같이 꼭짓점 버퍼를 준비합니다.

인덱싱되지 않은 기하 도형에 대한 꼭짓점 버퍼 다이어그램

이 기술은 디바이스의 하드웨어 가속에서 지원되지 않습니다. 소프트웨어 꼭짓점 처리에서만 지원되며 vs_3_0 셰이더에서만 작동합니다.

이 기술은 인덱싱되지 않은 기하 도형에서 작동하므로 인덱스 버퍼가 없습니다. 다이어그램에서와 같이 기하 도형이 포함된 꼭짓점 버퍼에는 기하 도형 데이터의 n개 복사본이 포함됩니다. 그려지는 각 instance 대해 기하 도형 데이터는 첫 번째 꼭짓점 버퍼에서 읽혀지고 instance 데이터는 두 번째 꼭짓점 버퍼에서 읽습니다.

해당 꼭짓점 버퍼 선언은 다음과 같습니다.

const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] =
{
{0,  0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,   0},
{0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT,  0},
{0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0},
{0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};

const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] =
{
{1, 0,  D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1},
{1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2},
{1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3},
{1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4},
{1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR,    0},
D3DDECL_END()
};

이러한 선언은 인덱싱된 기하 도형 예제에서 만든 선언과 동일합니다. 다시 한 번 첫 번째 선언(스트림 0의 경우)은 기하 도형 데이터를 정의하고 두 번째 선언(스트림 1의 경우)은 개체별 instance 데이터를 정의합니다. 첫 번째 꼭짓점 버퍼를 만들 때 그리려는 기하 도형 데이터의 인스턴스 수와 함께 로드해야 합니다.

렌더링하기 전에 첫 번째 꼭짓점 버퍼를 n개의 인스턴스로 나누는 방법을 런타임에 알려주는 구분자를 설정해야 합니다. 그런 다음 다음과 같이 SetStreamSourceFreq 를 사용하여 구분자를 설정합니다.

// Set the divider
pd3dDevice->SetStreamSourceFreq(0, 1);
// Bind the stream to the vertex buffer
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
        D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance);
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
        D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

SetStreamSourceFreq에 대한 첫 번째 호출은 스트림 0에 m 꼭짓점의 n 인스턴스가 포함되어 있다고 말합니다. 그런 다음 SetStreamSource 는 스트림 0을 기하 도형 꼭짓점 버퍼에 바인딩합니다.

두 번째 호출에서 SetStreamSourceFreq는 스트림 1을 instance 데이터의 원본으로 식별합니다. 두 번째 매개 변수는 각 개체(m)의 꼭짓점 수입니다. instance 데이터 스트림은 항상 두 번째 스트림으로 선언되어야 합니다. 그런 다음 SetStreamSource는 스트림 1을 instance 데이터가 포함된 꼭짓점 버퍼에 바인딩합니다.

instance 데이터 렌더링을 마치면 꼭짓점 스트림 빈도를 기본 상태로 다시 설정해야 합니다. 이 예제에서는 두 개의 스트림을 사용했기 때문에 아래와 같이 두 스트림을 모두 설정합니다.

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

인덱싱되지 않은 기하 도형 성능 비교

이 인스턴싱 스타일의 주요 장점은 인덱싱되지 않은 기하 도형에서 사용할 수 있다는 것입니다. 이 기술이 모든 애플리케이션에서 렌더링 시간을 줄일 수 있는 양에 대해 단일 결론을 내릴 수는 없지만 런타임으로 스트리밍되는 데이터의 양과 다음 렌더링 시퀀스에 대해 감소될 상태 변경 횟수의 차이를 고려합니다.

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    // Set the divider
    pd3dDevice->SetStreamSourceFreq(0, 1);
    pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

    // Set up the instance data stream
    pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance));
    pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
                D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

    pd3dDevice->SetVertexDeclaration( ... );
    pd3dDevice->SetVertexShader( ... );
    pd3dDevice->SetIndices( ... );

    pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    
    pd3dDevice->EndScene();
}

렌더링 루프가 한 번 호출됩니다. 기하 도형의 인스턴스가 n개 있지만 기하 도형 데이터는 한 번 스트리밍됩니다. instance 꼭짓점 버퍼의 데이터는 한 번 스트리밍됩니다. 다음 렌더링 시퀀스는 기능에서 동일하지만 인스턴싱을 활용하지는 않습니다.

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    for(int i=0; i < g_numObjects; i++)
    {
        pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

        pd3dDevice->SetVertexDeclaration( ... );
        pd3dDevice->SetVertexShader( ... );
        pd3dDevice->SetIndices( ... );

        pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    }
    
    pd3dDevice->EndScene();
}

인스턴싱이 없으면 렌더링 루프를 두 번째 루프로 래핑하여 각 개체를 그려야 합니다. 두 번째 렌더링 루프를 제거하면 루프 내에서 호출되는 렌더링 상태 변경이 적기 때문에 성능이 향상되어야 합니다.

전반적으로 인덱싱된 기술은 기하 도형 데이터의 복사본 하나만 스트리밍하므로 인덱싱된 기술(인덱싱된 기하 도형 그리기)이 인덱싱되지 않은 기술(인덱싱되지 않은 기하 도형 그리기)보다 더 나은 성능을 발휘할 것으로 예상하는 것이 합리적입니다. DrawIndexedPrimitive에 대한 매개 변수는 렌더링 시퀀스에 대해 변경되지 않았습니다.

고급 항목

인스턴스화 샘플