Fbx파일마다 기본 폴리곤 형태가 다릅니다.(삼각형 or 사각형) 제가 사용하는 DX에서는 삼각형을 통해 렌더링이 이루어지기 때문에 삼각형으로 만들어줍니다.
그리고 메시 데이터를 저장한는데 매개변수에 GetRootNode라는 매개변수가 있습니다.
Scene은 Node라는 트리 구조로 구성되어 있습니다. 즉, 이 Node 하나하나가 Scene의 각 개별 데이터를 의미합니다.(메쉬 노드, 조명 노드, 카메라 노드)
우린는 최 상단 노드인 Root Node로부터 메시 데이터를 찾아 저장할 것 입니다.
bool FbxTool::Load(const char* fileName)
{
bool ret;
// 1. 임포터 초기화
ret = m_importer->Initialize(fileName, -1, m_manager->GetIOSettings());
m_importer->Import(m_scene);
// 임포트 후 임포터는 해제하여 메모리 사용량을 줄입니다.
m_importer->Destroy();
// 2. 삼각형화할 수 있는 노드를 삼각형화 시키기
FbxGeometryConverter converter(m_manager);
converter.Triangulate(m_scene, true);
// 3. 메시 데이터 저장
ret = FindMesh(m_scene->GetRootNode());
if (ret == false) return false;
// 4. 메시의 정점 데이터(위치, 인덱스) 저장
SaveVertexData();
return true;
}
3. FindMesh
트리 구조를 메시 노드가 나올때까지 재귀형으로 탐색합니다. 노드가 가진 데이터는 FbxNodeAttribute로 알 수 있습니다.
발견했다면 m_mesh에 저장합니다 .
bool FbxTool::FindMesh(FbxNode* node)
{
// 찾은 노드가 메시타입이면 메시에 저장 후 반환합니다.
FbxNodeAttribute* attribute = node->GetNodeAttribute();
if (node->GetNodeAttribute() != nullptr)
{
if (node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eMesh)
{
m_mesh = node->GetMesh();
return true;
}
}
// 노드의 자식의 수만큼 반복
int childCnt = node->GetChildCount();
for (int i = 0; i < childCnt; ++i)
{
if (FindMesh(node->GetChild(i)) == true)
{
return true;
}
}
// 다 순회해도 메시 데이터가 없으면 false반환합니다.
return false;
}
4. SaveVertexData
찾은 메시 노드로 정점 데이터중 위치 데이터를 추출하여 저장합니다.
특별히 어려운 부분은 없으나 중간에 위치 데이터를 0, 1, 2순이 아니라 0, 2, 1순으로 된걸 볼 수 있는데, 그 이유는 backface의 방향 때문입니다.
Dx11같은 경우 정점이 시계 방향으로 그려진 면이 front, 반대 방향으로 그려진 면이 back 입니다
반면에, Fbx sdk가 사용하는 면은 그 반대입니다.
그렇기 때문에 Fbx sdk에서 Dx11으로 정점 데이터를 가져오는 순은 0, 2, 1순으로 가져와야 합니다.
이렇게 두 번째, 세 번째 만 바꿔주어도 그리는 방향을 반대로 만들수 있습니다.
bool FbxTool::SaveVertexData()
{
// 정점의 개수
int vertexCnt = m_mesh->GetControlPointsCount();
// 정점 데이터 생성
m_pos = new XMFLOAT3[vertexCnt];
// 정점배열의 포인터 얻기
FbxVector4* position = m_mesh->GetControlPoints();
// 정점 위치 구하여 저장
for (int i = 0; i < vertexCnt; ++i)
{
float x = static_cast<float>(position[i][0]);
float y = static_cast<float>(position[i][2]);
float z = static_cast<float>(position[i][1]);
m_pos[i] = XMFLOAT3(x, y, z);
}
ModelClass.h
VertexBuffer를 생성하는 클래스에 FbxTool 포인터를 생성합니다.
class ModelClass
{
// ....
private:
FbxTool* m_fbx;
}
ModelClass.cpp
Vertex Buffer를 만들때 이런식으로 사용합니다.
///////////////////////////////////////////////////////////////////////////////
// Vertex Buffer 생성
{
m_vertexCount = _msize(m_fbx->GetVertexPos()) / sizeof(XMFLOAT3);
m_vertices = new VertexColor[m_vertexCount];
// Load the vertex array and index array with data.
for (int i = 0; i < m_vertexCount; i++)
{
m_vertices[i].position = m_fbx->GetVertexPos()[i];
m_vertices[i].color = XMFLOAT4(1, 0, 0, 1);
}
Index Buffer 같은 경우는 바로 받아오기만 하면 됩니다.
///////////////////////////////////////////////////////////////////////////////
// Index Buffer 생성
{
m_indices = m_fbx->GetVertexIdx();
m_indexCount = _msize(m_indices) / sizeof(unsigned int);
DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.
들어가며
이 튜토리얼에서는 HLSL을 사용하여 DirectX 11에서 3D 모델을 렌더링하는 방법을 다룹니다.이 튜토리얼의 코드는 디퓨즈 조명 튜토리얼의 코드를 기반으로 합니다.
이전 튜토리얼에서 우리는 이미 3D 모델을 렌더링했습니다.하지만 그것들은 단일 삼각형으로 구성되어 있었고 상당히 단순했습니다.이제 기본 사항을 다루었으니, 더 복잡한 객체를 렌더링하는 방법으로 나아가겠습니다.이번에는 큐브 객체를 렌더링할 것입니다.더 복잡한 모델을 렌더링하는 방법에 대해 논의하기 전에 먼저 모델 형식에 대해 이야기하겠습니다.
사용자가 3D 모델을 만들 수 있게 해주는 많은 도구가 있습니다.Maya와 Blender가 가장 인기 있는 3D 모델링 프로그램 중 두 가지입니다.기능이 적지만 우리가 필요로 하는 기본 작업을 수행할 수 있는 다른 많은 도구들도 있습니다.
어떤 도구를 사용하든, 그들은 모두 다양한 형식으로 모델을 내보낼 것입니다. 제 제안은 여러분이 직접 모델 형식을 만들고, 내보낸 형식을 여러분의 형식으로 변환하는 파서를 작성하는 것입니다. 그 이유는 여러분이 사용하는 3D 모델링 패키지가 시간이 지남에 따라 변경될 수 있고, 그들의 모델 형식도 변경될 것이기 때문입니다.또한, 여러분은 여러 3D 모델링 패키지를 사용할 수 있어서 여러 다른 형식을 다루어야 할 수도 있습니다.그래서 여러분이 자신의 형식을 가지고, 다른 형식을 자신의 형식으로 변환한다면, 코드를 변경할 필요가 없습니다.단지 여러분의 파서 프로그램을 변경하여 그 형식을 여러분의 형식으로 변환하면 됩니다.대부분의 3D 모델링 패키지는 해당 모델링 프로그램에만 유용한 많은 불필요한 데이터를 내보내는데, 여러분의 모델 형식에는 그 모든 것이 필요하지 않습니다.
자신의 형식을 만드는 데 가장 중요한 부분은 그것이 여러분이 필요로 하는 모든 것을 포함하고, 사용하기에 간단하다는 것입니다.또한, 애니메이션 데이터가 포함된 객체와 정적인 객체 등 다양한 객체를 위해 몇 가지 다른 형식을 만드는 것도 고려할 수 있습니다.
제가 제시할 모델 형식은 매우 기본적입니다.모델 한개의 정점에 대해 한 줄이 매핑됩니다. 각 줄은 코드에서 사용되는 정점 형식에 맞게 위치 벡터(x, y, z), 텍스처 좌표(tu, tv), 그리고 법선 벡터(nx, ny, nz)를 포함할 것입니다. 형식에는 정점 수가 상단에 있어 첫 번째 줄을 읽고 데이터를 읽기 전에 필요한 메모리 구조를 빌드할 수 있습니다. 또한 형식은 세 줄마다 하나의 삼각형을 만들고, 모델 형식의 정점이 시계 방향으로 표시되어야 합니다.다음은 우리가 렌더링할 큐브의 모델 파일입니다.
보시다시피 x, y, z, tu, tv, nx, ny, nz 데이터가 총 36줄 있습니다.매 세 줄이 하나의 삼각형을 구성하여 총 12개의 삼각형이 큐브를 형성하게 됩니다.이 형식은 매우 직관적이며, 정점 버퍼로 직접 읽어 들여 수정 없이 렌더링할 수 있습니다.
이제 주의할 점은 일부 3D 모델링 프로그램이 데이터를 왼손 좌표계나 오른손 좌표계와 같은 다른 순서로 내보낼 수 있다는 것입니다.기본적으로 DirectX 11은 왼손 좌표계를 사용하므로 모델 데이터도 이에 맞춰야 합니다.이러한 차이점을 유의하고, 파싱 프로그램이 데이터를 올바른 형식이나 순서로 변환할 수 있도록 해야 합니다.
ModelClass.h
파일을 읽기 위한 fstream을 사용하고, 모델 형식을 나타내는 구조체 ModelTpye을 선언, 포인터를 생성합니다.
#include <fstream>
using namespace std;
class ModelClass
{
private:
struct ModelType
{
float x, y, z;
float tu, tv;
float nx, ny, nz;
};
private:
ModelType* m_model;
또한 이제 초기화에서 모델의 파일 이름을 받아 초기화 단계에서 모델 데이터를 로드할것입니다.
bool ModelClass::Initialize(ID3D11Device* device, ID3D11DeviceContext* deviceContext, char* modelFilename, char* textureFilename)
{
// Load in the model data
LoadModel(modelFilename);
// ....
}
2. InitializeBuffer.cpp
이제 더 이상 정점 및 인덱스 수를 수동으로 설정하지 않습니다.
인덱스 버퍼는 정점 버퍼에 들어간 정점의 순서와 똑같습니다.
bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
VertexType* vertices;
unsigned long* indices;
D3D11_BUFFER_DESC vertexBufferDesc, indexBufferDesc;
D3D11_SUBRESOURCE_DATA vertexData, indexData;
HRESULT result;
// Create the vertex array.
vertices = new VertexType[m_vertexCount];
// Create the index array.
indices = new unsigned long[m_indexCount];
// Create the vertex array.
vertices = new VertexType[m_vertexCount];
// Load the vertex array and index array with data.
for (int i = 0; i < m_vertexCount; i++)
{
vertices[i].position = XMFLOAT3(m_model[i].x, m_model[i].y, m_model[i].z);
vertices[i].texture = XMFLOAT2(m_model[i].tu, m_model[i].tv);
vertices[i].normal = XMFLOAT3(m_model[i].nx, m_model[i].ny, m_model[i].nz);
indices[i] = i;
}
3. LoadModel
간단한 파일 읽어내는 코드다.
bool ModelClass::LoadModel(char* filename)
{
ifstream fin;
char input;
int i;
// 파일 오픈.
fin.open(filename);
// 파일을 열수 없으면 종료.
if (fin.fail())
{
return false;
}
// 정점 개수가 나오기 전까지 읽어냅니다.
fin.get(input);
while (input != ':')
{
fin.get(input);
}
// 나머지 줄을 읽어 정점 개수를 m_vertexCount에 가져옵니다.
fin >> m_vertexCount;
// 인덱스의 개수는 정점 개수와 같습니다.
m_indexCount = m_vertexCount;
// 읽어온 정점 개수를 사용하여 배열을 만들어 포인터에 넣습니다.
m_model = new ModelType[m_vertexCount];
// 정점 데이터가 나오기 전까지 읽어냅니다.
fin.get(input);
while (input != ':')
{
fin.get(input);
}
// 공백문자 읽어내기
fin.get(input);
fin.get(input);
// 정점 데이터를 읽습니다
for (i = 0; i < m_vertexCount; i++)
{
fin >> m_model[i].x >> m_model[i].y >> m_model[i].z;
fin >> m_model[i].tu >> m_model[i].tv;
fin >> m_model[i].nx >> m_model[i].ny >> m_model[i].nz;
}
// 파일 닫기
fin.close();
return true;
}
ApplicationClass.cpp
1. Initialize
모델의 초기화에서 모델 파일의 이름을 사용하기에 모델이름과 함께 초기화 시켜줍니다.
bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
bool result;
char textureFileName[128];
char modelFileName[128]; // 추가
// 모델의 파일명을 설정합니다.
strcpy_s(modelFilename, "../Engine/data/cube.txt");
m_Model = new ModelClass;
// 파일명과 함께 모델을 초기화합니다.
m_Model->Initialize(m_Direct3D->GetDevice(), m_Direct3D->GetDeviceContext(), modelFilename, TextureFilename);
요약
3D모델을 로드하고 렌더링했습니다.
초기화
ModelClass 1. 3D모델 파일의 정점 데이터를 가져옵니다. 2. 가져온 데이터로 정점,인덱스 버퍼를 생성합니다.
select 함수를 중심으로 I/O가 이루어지기 때문에 Select model 이라는 이름이 붙여졌습니다.
이 모델을 정말정말 초 단순화 하면 이렇습니다.
어떻게 보면 "if문으로 "소켓에 변화가 있나요? → 변화한 소켓에 작업" 의 추가일 뿐입니다.
SOCKET listenSock;
vector<SOCKET> client;
while (true)
{
if (listenSock에 누군가 접근했나요 ? == true)
{
client.push_back(접근한 클라이언트 소켓)
}
for (SOCKET& s : client)
{
if (소켓 s에 변화가 있나요 ? == true)
{
소켓 s를 이용한 작업
}
}
}
기존 단순한 블로킹 모드의 소켓의 문제점은 함수가 성공할때까지 스레드가 무한정으로 대기한다는것이 문제였습니다.
Select모델을 사용할 경우 if문의 추가로 recv, send가 성공할때까지 대기하지 않아도 되었습니다.
기존 단순한 논블로킹 모드 소켓의 문제점은 불필요한 recv, send등 요청을 계속 하는게 문제였습니다.
Select모델을 사용할 경우 if문의 추가로 recv, send가 필요한 소켓이 나타날때 작업을 할수 있게 되었습니다.
그럼 코드로 넘어가겠습니다.
fd_set 설정
먼저 연결된 클라이언트의 소켓을 관리하기위해 clients라는 벡터를 생성했습니다.
그리고 fd_set을 정의했는데, fd_set이란 간단히 select 함수등 다양한 소켓 함수에 사용되는 소켓의 배열입니다.
fd_set은 사용되는 함수가 네 가지가 있습니다. 이를 이용해 fd_set을 이용할 것 입니다.
1. FD_ZERO(set) : fd_set인 set의 모든 비트 값을 0 으로 만듭니다. 2. FD_SET(s, &set) : 소켓 s를 fd_set인 set에 추가합니다. 3. FD_CLR(s, &set) : 소켓 s를 fd_set인 set에서 제거합니다. 4. FD_ISSET(s, set) : 소켓 s가 fd_set인set에 들어있으면 0이 아닌 값을 리턴합니다.
vector<SOCEKT> clients;
// recv가 가능한지 확인할 용도의 fd_set
fd_set receivers;
// send가 가능한지 확인할 용도의 fd_set
fd_set senders;
fd_set 초기화 작업
while (true)
{
// fd_set 0으로 초기화
FD_ZERO(&receivers);
FD_ZERO(&senders);
// 클라이언트의 접속을 얻기 위해 listening중인 소켓 등록
FD_SET(listenSock, receivers);
// 연결된 클라이언트들을 각 fd_set에 저장
for (auto& c : clients)
{
FD_SET(c.socket, &receivers);
FD_SET(c.socket, &senders);
}
Select함수 호출
Select함수는 fd_set에 있는 소켓의 변화를 체크할 수 있습니다.
변화는 총 세 가지로 체크할 수 있습니다.
1. 읽기 가능한 소켓 : 체크하고 싶은 fd_set을 두 번째 매개변수에 지정
2. 쓰기 가능한 소켓 : 체크하고 싶은 fd_set을 세 번째 매개변수에 지정
3. 예외 상태 소켓 : 체크하고 싶은 fd_set을 네 번째 매개변수에 지정
또 하나 중요한 것은 체크한 fd_set에서 변화가 없는 소켓은 fd_set에서 모두 제거 된다는게 특징입니다.
아까 만들었던 fd_set은 읽기와 쓰기용을 만들었으니 각 fd_set을 해당 매개변수에 넣습니다.
반환 값으로는 남은 소켓의 총 개수 입니다.
그래서 변화가 있는 소켓이 0개라면 다시 처음으로 돌아갑니다.
int retCnt = select(0, &receivers, &senders, nullptr, nullptr);
if (retCnt == 0)
{
continue;
}
DirectX11의 공부 목적으로 위 링크의 튜토리얼을 보고 제 생각을 포함하여 정리하였습니다.
들어가며
이번 튜토리얼에서는 Dx11을 사용하여 물체에 난반사 조명을 적용시키는 방법을 다룰 것입니다.
난반사 광이란 빛을 받아 여러 방향으로 고르게 반사되는 빛을 뜻합니다.
우리가 구현할 난반사 조명의 유형을 방향성 조명(Directional lighting)이라고 합니다.
방향성 조명은 태양이 지구를 비추는 방식과 유사합니다. 이 조명은 매우 멀리 떨어진 광원으로부터 오는 빛의 방향을 기반으로 물체에 비치는 빛의 양(밝기)을 결정할 수 있습니다. 그러나 직접 닿지 않는 표면은 조명되지 않습니다.(첫 번째로 빛이 닿는 부분만 적용되며 빛의 반사까지는 계산하지 않는다는 뜻인듯?)
난반사 조명의 구현은 VS와 PS에서 이루어집니다. 난반사 조명에는 방향과 우리가 조명하고자 하는 폴리곤의 법선 벡터가 필요합니다. 폴리곤을 구성하는 세 개의 정점을 이용하여 폴리곤의 법선을 계산할 수 있습니다. 이 튜토리얼에서는 조명 방적식에 난반사 조명의 색상을 구현할 것입니다.
프레임워크
(NEW) LightClass 장면에서 광원을 나타냅니다, 빛의 방향과 색상을 보유하는 것 외에는 다른 역할을 하지 않습니다.
(NEW) LightShaderClass
기존 TextureShaderClass를 제거하고 모델의 조명 셰이딩을 처리하는 셰이더 클래스입니다.
Light.vs
이전 셰이더와 특별하게 다른점은 법선 벡터가 float3형태로 정점데이터에 생기고, 월드 공간에서의 법선 벡터를구합니다.
// VS에 들어갈 상수버퍼 생성 ....
// 픽셀 셰이더에 들어갈 상수버퍼의 Buffer description을 설정합니다.
lightBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
// ByteWidth가 16바이트의 배수가 아니면 실패합니다.
lightBufferDesc.ByteWidth = sizeof(LightBufferType);
lightBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
lightBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
lightBufferDesc.MiscFlags = 0;
lightBufferDesc.StructureByteStride = 0;
// Create the constant buffer pointer so we can access the vertex shader constant buffer from within this class.
result = device->CreateBuffer(&lightBufferDesc, NULL, &m_lightBuffer);
return true;
}
2. SetShaderParameters.cpp
PS들어갈 상수버퍼의 값을 Map/Unmap으로 수정하고 PS단계에 설정한다.
bool LightShaderClass::SetShaderParameters(ID3D11DeviceContext* deviceContext, XMMATRIX worldMatrix, XMMATRIX viewMatrix, XMMATRIX projectionMatrix,
ID3D11ShaderResourceView* texture, XMFLOAT3 lightDirection, XMFLOAT4 diffuseColor)
{
// VS에 들어갈 상수버퍼 업데이트 후 VS에 배치 ....
deviceContext->PSSetShaderResources(0, 1, &texture);
// PS에 들어갈 상수버퍼 업데이트
result = deviceContext->Map(m_lightBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
// Get a pointer to the data in the constant buffer.
dataPtr2 = (LightBufferType*)mappedResource.pData;
// Copy the lighting variables into the constant buffer.
dataPtr2->diffuseColor = diffuseColor;
dataPtr2->lightDirection = lightDirection;
dataPtr2->padding = 0.0f;
// Unlock the constant buffer.
deviceContext->Unmap(m_lightBuffer, 0);
// Set the position of the light constant buffer in the pixel shader.
bufferNumber = 0;
// 상수버퍼 PS단계에 배치
deviceContext->PSSetConstantBuffers(bufferNumber, 1, &m_lightBuffer);
return true;
}
bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
// ....
// Light shader 개체를 생성하고 초기화 합니다.
m_LightShader = new LightShaderClass;
result = m_LightShader->Initialize(m_Direct3D->GetDevice(), hwnd);
if (!result)
{
MessageBox(hwnd, L"Could not initialize the light shader object.", L"Error", MB_OK);
return false;
}
// Light 개체를 생성하고 초기화 합니다.
m_Light = new LightClass;
m_Light->SetDiffuseColor(1.0f, 1.0f, 1.0f, 1.0f);
m_Light->SetDirection(0.0f, 0.0f, 1.0f);
return true;
}
2. Frame
각 프레임마다 계속 회전을 업데이트한 정적 변수를 추가하여 Render함수에 전달합니다.
bool ApplicationClass::Frame()
{
static float rotation = 0.0f;
bool result;
// 회전 값을 프레임마다 업데이트 합니다.
rotation -= 0.0174532925f * 0.1f;
if (rotation < 0.0f)
{
rotation += 360.0f;
}
// 그래픽 장면을 렌더링합니다.
result = Render(rotation);
return true;
}
3. Render
bool ApplicationClass::Render(float rotation)
{
// ....
// camera 와 d3d objects에서 world, view, projection 행렬을 가져옵니다.
m_Direct3D->GetWorldMatrix(worldMatrix);
m_Camera->GetViewMatrix(viewMatrix);
m_Direct3D->GetProjectionMatrix(projectionMatrix);
//(추가코드) 삼각형에 rotation값 만큼 회전을 시킵니다.
worldMatrix = XMMatrixRotationY(rotation);
// 렌더링 파이프라인에 모델의 정점, 인덱스 버퍼를 배치하여 그릴 준비를 합니다.
m_Model->Render(m_Direct3D->GetDeviceContext());
//(수정코드) 라이트 셰이더를 사용하여 모델을 렌더링합니다.
result = m_LightShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture(),m_Light->GetDirection(), m_Light->GetDiffuseColor());
// 렌더링된 장면을 화면에 표시한다.
m_Direct3D->EndScene();
return true;
}
요약
다음은 코드에 몇 가지 변경을 가하여 기본적인 난방향 조명을 구현한 것입니다. 법선 벡터가 어떻게 작동하는지, 그리고 왜 폴리곤 면에 조명을 계산하는 데 중요한지 이해하는 것이 중요합니다. 우리의 D3DClass에서 후면 컬링(back face culling)이 활성화되어 있기 때문에 회전하는 삼각형의 뒷면은 조명이 비추지 않을 것입니다.
연습
1. 조명 색상을 녹색으로 변경합니다.
DiffuseColor를 녹색으로 변경해준다.
내 생각에 중요했던 것
1. 기존 물체의 공간변환시에는 float4를 사용한 공간변환이지만, 광원의 위치가 월드로 공간변환시 사용한건 float3이다.
이유는 투영 변환에서 원근감을 표현하기위해 4차원 벡터가 필요했던것.
광원은 카메라의 위치와도 무관하고 월드 공간까지만 있어도 충분하기 때문에 flaot3로 공간변환이 되었다.
2. 상수 버퍼
상수 버퍼를 구성하는 구조체는 16바이트의 배수로 정의되어야 한다.
이유는 GPU에서 SIMD를 활용하기 위해서다.
SIMD는 "Single Instruction, Multiple Data"의 약자로 단일 명령으로 여러 데이터를 동시에 처리 한다는 뜻이다.
즉, 장치가 한 번의 명령으로 여러 데이터를 병렬로 처리할 수 있게 해주는 기술이다.
이 SIMD기술이 주로 128bit(16byte)의 단위로 데이터를 처리하기 때문이다!
예시를 들어보자면 숫자 배열의 덧셈이있다.
A = [1, 2, 3, 4]
B = [5, 6, 7, 8]
두 배열의 각 인덱스를 더해 새로운 배열 C를 만든다고 할때,
일반적인 방식으로는 밑의 과정을 거친다.
1. C[0] = 1 + 5 = 6
2. C[1] = 2 + 6 = 8
3. C[2] = 3 + 7 = 10
4. C[3] = 4 + 8 = 12
하지만 SIMD를 사용하는 경우
1. [1+5, 2+6, 3+7, 4+8] = [6, 8, 10, 12]
이렇게 한번에 C를 구할 수 있다.
용어 정리
Diffuse light : 난반사, 광원에 의해 여러 방향으로 고르게 반사되는 빛.
물체를 여러 방향에서 바라보아도 물체의 색이 크게 변하지 않는 이유다. (빛이 고르게 반사되는 난반사가 있기때문에)
Render함수는 이제 텍스쳐 리소스에 대한 포인터인 texture라는 새 매개변수를 사용합니다. 이 포인터는 SetShaderParameters 함수로 전달되어 셰이더에 텍스쳐가 설정됩니다.
bool TextureShaderClass::Render(ID3D11DeviceContext * deviceContext, int indexCount, XMMATRIX worldMatrix, XMMATRIX viewMatrix,
XMMATRIX projectionMatrix, ID3D11ShaderResourceView * texture)
{
bool result;
// Set the shader parameters that it will use for rendering.
result = SetShaderParameters(deviceContext, worldMatrix, viewMatrix, projectionMatrix, texture);
// Now render the prepared buffers with the shader.
RenderShader(deviceContext, indexCount);
return true;
}
3. InitializeShader
새로운 셰이더를 설정합니다.
// Texture sampler에 대한 설명을 담는 새로운 변수 추가
D3D11_SAMPLER_DESC samplerDesc;
/*----------------------------------------------*/
// 셰이더 파일 컴파일, 생성 부분
/*----------------------------------------------*/
이제 정점 데이터에 색상대신 텍스쳐 요소가 있으므로 Input layout이 변경되었습니다. 첫 번째 위치 요소는 동일하지만 두 번째 요소의 SemanticName과 Format은 변경되었습니다.
/*----------------------------------------------*/
// polygonLayout[0] 생성
/*----------------------------------------------*/
polygonLayout[1].SemanticName = "TEXCOORD";
polygonLayout[1].SemanticIndex = 0;
polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT; // float2(u, v좌표)
polygonLayout[1].InputSlot = 0;
polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
polygonLayout[1].InstanceDataStepRate = 0;
// Get a count of the elements in the layout.
numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);
// Create the vertex input layout.
result = device->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(),
vertexShaderBuffer->GetBufferSize(), &m_layout);
/*----------------------------------------------*/
// 상수 버퍼 생성
/*----------------------------------------------*/
이후 Sampler state의 설명이 설정됩니다. 이후에 픽셀 셰이더에 전달됩니다.
Sampler state에서 가장 중요한 요소는 Filter 입니다. Filter는 폴리곤에 텍스쳐의 최종 모양을 만들기 위해 어떤 픽셀을 사용하거나 결합할지의 방법을 정합니다.
이번 시간에서는 처리 비용이 비싸지만 최상의 시각적 결과를 제공하는 D3D11_FILTER_MIN_MAG_MIP_LINEAR을 사용해 축소, 확대 및 mip level sampling을 위해 선형 보간을 사용하도록 지시합니다.
AddredssU와 AddressV는 좌표가 0.0f와 1.0f 사이에 유지되도록 보장하는 Wrap으로 설정됩니다.
RenderShader함수는 렌더링 전에 Smaple state 설정을 픽셀 셰이더에 배치합니다.
void TextureShaderClass::RenderShader(ID3D11DeviceContext* deviceContext, int indexCount)
{
// Set the vertex input layout.
deviceContext->IASetInputLayout(m_layout);
// Set the vertex and pixel shaders that will be used to render this triangle.
deviceContext->VSSetShader(m_vertexShader, NULL, 0);
deviceContext->PSSetShader(m_pixelShader, NULL, 0);
// 픽셀 셰이더에 Texture sampler state를 배치합니다.
deviceContext->PSSetSamplers(0, 1, &m_sampleState);
// Render the triangle.
deviceContext->DrawIndexed(indexCount, 0, 0);
return;
}
TextureClass.h
TextureClass는 단일 텍스처 리소스의 로드, 언로드 및 액세스를 캡슐화합니다. 각 텍스쳐는 인스턴스화 된 이 개체가 필요합니다.
데이터를 더 쉽게 읽을 수 있도록 TargaHeader 구조체를 정의했습니다.
#pragma once
#include <d3d11.h>
#include <stdio.h>
class TextureClass
{
private:
struct TargaHeader
{
unsigned char data1[12];
unsigned short width;
unsigned short height;
unsigned char bpp;
unsigned char data2;
};
public:
TextureClass();
TextureClass(const TextureClass&) = default;
~TextureClass() = default;
bool Initialize(ID3D11Device*, ID3D11DeviceContext*, char*);
void Shutdown();
ID3D11ShaderResourceView* GetTexture();
int GetWidth();
int GetHeight();
private:
// Targa 읽기 기능
bool LoadTarga32Bit(char*);
private:
// 파일에서 직접 읽은 원시 Targa데이터
unsigned char* m_targaData;
// DirectX가 렌더링에 사용할 구조화된 텍스쳐 데이터
ID3D11Texture2D* m_texture;
// 셰이더가 그림을 그릴 때 텍스쳐 데이터에 액세스하는 리소스 장착 칸
ID3D11ShaderResourceView* m_textureView;
// 텍스쳐의 크기
int m_width, m_height;
};
TextureClass.cpp
1. Initialize
이 함수는 먼저 Targa 데이터를 배열에 로드합니다. 그런 다음 텍스쳐를 생성하고 Targa 데이터를 올바른 형식으로 로드합니다. 즉, Targa 이미지 파일을 Dx 텍스쳐로 변환하는 기능을 합니다.
다음으로 Targa 데이터를 로드할 DirectX 텍스처에 대한 설명을 설정합니다. 설명이 완료되면 CreateTexture2D를 호출하여 빈 텍스처를 만듭니다. 다음 단계는 Targa 데이터를 빈 텍스처에 복사하는 것입니다.
// Targa 이미지 데이터의 높이와 너비를 사용
textureDesc.Height = m_height;
textureDesc.Width = m_width;
textureDesc.MipLevels = 0;
textureDesc.ArraySize = 1;
// 픽셀 형식을 32비트 RGBA 텍스처로 설정
textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
// SampleDesc를 기본값으로 설정합니다
textureDesc.SampleDesc.Count = 1;
textureDesc.SampleDesc.Quality = 0;
textureDesc.Usage = D3D11_USAGE_DEFAULT;
textureDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
textureDesc.CPUAccessFlags = 0;
textureDesc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS;
// Create the empty texture.
hResult = device->CreateTexture2D(&textureDesc, NULL, &m_texture);
여기서 우리는 Targa 데이터 배열을 DirectX 텍스처로 복사하기 위해 UpdateSubresource를 사용합니다. 이전 튜토리얼에서 기억하실지 모르겠지만, 우리는 ModelClass의 행렬을 행렬 상수 버퍼로 복사하기 위해 Map과 Unmap을 사용했습니다.
사실, 여기서도 우리의 텍스처 데이터를 Map과 Unmap으로 복사할 수 있었습니다. 일반적으로 Map과 Unmap을 사용하는 것이 UpdateSubresource를 사용하는 것보다 훨씬 빠르지만, 두 가지 로딩 방법 모두 특정한 목적이 있으며 성능상의 이유로 올바르게 선택해야 합니다.
추천 사항은 매 프레임마다 또는 매우 자주 다시 로드될 데이터에는 Map과 Unmap을 사용하라는 것 입니다. 반면, 한 번만 로드되거나 로딩 시퀀스 동안 드물게 로드되는 데이터에는 UpdateSubresource를 사용해야 합니다. 그 이유는 UpdateSubresource가 데이터를 고속 메모리에 넣어 캐시 유지 우선순위를 얻기 때문입니다. 이는 데이터가 곧 제거되거나 다시 로드되지 않을 것임을 알고 있기 때문입니다.
반면, Map과 Unmap은 DirectX가 곧 덮어쓸 것이라고 예상하기 때문에 메모리 위치를 캐시하지 않는 곳에 데이터를 넣습니다.
그래서 D3D11_USAGE_DEFAULT를 사용하면 DirectX에 UpdateSubresource를 사용하니 데이터가 자주 변경되지 않음을 알려주고, D3D11_USAGE_DYNAMIC을 사용하면 이 유형의 데이터가 일시적인 것임을 DirectX에 알립니다.
// targa 이미지 데이터를 텍스처에 복사합니다.
deviceContext->UpdateSubresource(m_texture, 0, NULL, m_targaData, rowPitch, 0);
텍스처가 로드된 후 셰이더에서 텍스처를 설정하기 위한 포인터를 가질 수 있는 셰이더 리소스 뷰를 생성합니다.설명에서 우리는 어떤 거리에서도 고품질 텍스처 렌더링을 위한 전체 Mipmap 수준 범위를 제공하는 두 가지 중요한 Mipmap 변수를 설정했습니다.
셰이더 리소스 뷰가 생성되면 GenerateMips를 호출하고 Mipmap이 생성됩니다. 그러나 원하는 경우 더 나은 품질을 찾고 있다면 자신의 Mipmap 레벨을 수동으로 로드할 수 있습니다.
// shader resource view description를 설정합니다.
srvDesc.Format = textureDesc.Format;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
// 텍스쳐를 위한 shader resource view를 생성합니다.
hResult = device->CreateShaderResourceView(m_texture, &srvDesc, &m_textureView);
if (FAILED(hResult))
{
return false;
}
// 이 텍스쳐의 mipmaps을 만듭니다.
deviceContext->GenerateMips(m_textureView);
// Release the targa image data now that the image data has been loaded into the texture.
delete[] m_targaData;
m_targaData = 0;
return true;
}
2. GetTexture
어느 셰이더나 렌더링하는데 필요로하는 Textrue view를 제공해주는 헬퍼 함수입니다.
Targa 이미지 로딩 기능입니다. Targa 이미지는 거꾸로 저장되므로 사용하기 전에 뒤집어야 한다는 점을 다시 한 번 참고하세요. 따라서 여기서는 파일을 열고 배열로 읽은 다음 해당 배열 데이터를 가져와 올바른 순서로 m_targaData 배열에 로드하겠습니다.
우리는 의도적으로 알파 채널이 있는 32비트 Targa 파일만 다루고 있으며, 이 기능은 24비트로 저장된 Targa 파일을 거부합니다.
bool TextureClass::LoadTarga32Bit(char* filename)
{
int error, bpp, imageSize, index, i, j, k;
FILE* filePtr;
unsigned int count;
TargaHeader targaFileHeader;
unsigned char* targaImage;
// 바이너리 모드로 targa 파일을 엽니다.
error = fopen_s(&filePtr, filename, "rb");
if (error != 0)
{
return false;
}
// targaFileHeader 형식의 내용을 저장합니다.
count = (unsigned int)fread(&targaFileHeader, sizeof(TargaHeader), 1, filePtr);
if (count != 1)
{
return false;
}
// 헤더에서 중요한 정보를 가져옵니다.
m_height = (int)targaFileHeader.height;
m_width = (int)targaFileHeader.width;
bpp = (int)targaFileHeader.bpp;
// 32bit Targa 이미지인지 확인합니다.
if (bpp != 32)
{
return false;
}
// 32bit 이미지의 데이터 크기를 계산합니다.
imageSize = m_width * m_height * 4; // 각 픽셀이 4바이트이기 때문에 4를 곱해줍니다.
// Targa 이미지 데이터를 저장할 메모리를 할당합니다.
targaImage = new unsigned char[imageSize];
// Targa 이미지 데이터를 읽어옵니다.
count = (unsigned int)fread(targaImage, 1, imageSize, filePtr);
if (count != imageSize)
{
return false;
}
// 파일을 닫습니다.
error = fclose(filePtr);
if (error != 0)
{
return false;
}
// targa destination data를 위한 메모리 할당을 합니다.
m_targaData = new unsigned char[imageSize];
// 우리의 빈 텍스쳐가 채워지기 시작할 인덱스 입니다.
index = 0;
// Targa 텍스쳐를 탐색하기 시작할 인덱스 입니다.
k = (m_width * m_height * 4) - (m_width * 4);
// targa 이미지 데이터를 올바른 순서로 targa 대상 배열에 복사합니다.
for (j = 0; j < m_height; j++)
{
for (i = 0; i < m_width; i++)
{
m_targaData[index + 0] = targaImage[k + 2]; // Red.
m_targaData[index + 1] = targaImage[k + 1]; // Green.
m_targaData[index + 2] = targaImage[k + 0]; // Blue
m_targaData[index + 3] = targaImage[k + 3]; // Alpha
// targa 데이터에 대한 인덱스를 증가시킵니다.
k += 4;
index += 4;
}
// targa 이미지 데이터 인덱스를 거꾸로 읽어서 열 시작 부분의 이전 행으로 다시 설정합니다.
k -= (m_width * 8);
}
// 대상 배열에 복사되었으므로 이제 targa 이미지 데이터를 해제합니다.
delete[] targaImage;
targaImage = 0;
return true;
}
ModleClass.h
이제 TextureClass가 ModelClass에 포함됩니다.
// 1. 구조 변경
struct VertexType
{
XMFLOAT3 position;
XMFLOAT2 texture;
};
// 2. 함수 추가
ID3D11ShaderResourceView* GetTexture();
// 텍스쳐 불러오기
bool LoadTexture(ID3D11Device*, ID3D11DeviceContext*, char*);
// 텍스쳐 해제
void ReleaseTexture();
// 3. 변수 추가
//m_Texture는 이 모델의 텍스처 리소스를 로드, 해제 및 액세스하는 데 사용됩니다.
TextureClass* m_Texture;
ModelClass.cpp
1. Initialize
매개변수가 추가 됐습니다.
// DeviceContext*와 char* 매개변수가 추가 됐습니다.
bool ModelClass::Initialize(ID3D11Device* device, ID3D11DeviceContext* deviceContext, char* textureFilename)
{
bool result;
result = InitializeBuffers(device);
// 이 모델의 텍스쳐를 불러옵니다.
result = LoadTexture(device, deviceContext, textureFilename);
return true;
}
2. InitializeBuffer
정점 데이터를 color에서 texture로 수정합니다.
bool ModelClass::InitializeBuffers(ID3D11Device* device)
{
///////////////////////////////////////////////////////////////////////////////
// Vertex Buffer 생성
///////////////////////////////////////////////////////////////////////////////
{
m_vertexCount = 4;
// Create the vertex array.
vertices = new VertexType[m_vertexCount];
// Load the vertex array with data. (x, y, z)
vertices[0].position = XMFLOAT3(-1.0f, 1.0f, 0.0f); // Top left.
vertices[0].texture = XMFLOAT2(0.0f, 0.0f);
vertices[1].position = XMFLOAT3(1.0f, 1.0f, 0.0f); // Top right
vertices[1].texture = XMFLOAT2(1.0f, 0.0f);
vertices[2].position = XMFLOAT3(-1.0f, -1.0f, 0.0f); // Bottom left.
vertices[2].texture = XMFLOAT2(0.0f, 1.0f);
vertices[3].position = XMFLOAT3(1.0f, -1.0f, 0.0f); // Bottom right.
vertices[3].texture = XMFLOAT2(1.0f, 1.0f);
/*----------------------------------------------*/
// 생략
/*----------------------------------------------*/
}
3. LoadTexture
이 함수는 텍스쳐 개체를 생성한 후 제공된 파일 이름으로 초기화하는 새로운 함수입니다. 이 함수는 초기화 중 호출됩니다.
void ModelClass::ReleaseTexture()
{
// Release the texture object.
if (m_Texture)
{
m_Texture->Shutdown();
delete m_Texture;
m_Texture = nullptr;
}
return;
}
ApplicationClass.h
// 삭제 됩니다.
//#include "ColorShaderClass.h"
// 추가 됩니다.
#include "TextureShaderClass.h"
class ApplicationClass
{
private:
// private 멤버 변수가 삭제됩니다.
//ColorShaderClass* m_ColorShader;
// private 멤버 변수가 추가됩니다.
TextureShaderClass* m_TextureShader;
}
ApplicaitionClass.cpp
1. Initialize
bool ApplicationClass::Initialize(int screenWidth, int screenHeight, HWND hwnd)
{
// 텍스쳐 파일의 이름을 저장할 버퍼
char textureFileName[128];
// ....
// 우리가 불러올 텍스쳐 파일의 이름을 세팅합니다.
strcpy_s(textureFileName, "stone01.tga");
result = m_Model->Initialize(m_Direct3D->GetDevice(), m_Direct3D->GetDeviceContext(), textureFileName);
// TextureShaderClass의 개체를 생성, 초기화 합니다.
m_TextureShader = new TextureShaderClass;
result = m_TextureShader->Initialize(m_Direct3D->GetDevice(), hwnd);
// ....
}
2. Render
bool ApplicationClass::Render()
{
// ....
// TextureShaderClass를 사용하여 모델을 렌더링 합니다.
result = m_TextureShader->Render(m_Direct3D->GetDeviceContext(), m_Model->GetIndexCount(), worldMatrix, viewMatrix, projectionMatrix, m_Model->GetTexture());
if (!result)
{
return false;
}
// 렌더링된 장면을 화면에 표시한다.
m_Direct3D->EndScene();
return true;
}
요약
텍스쳐 불러오기, 이를 폴리곤에 매핑한 다음 셰이더를 사용하여 렌더링하는 기본을 배웠습니다.
초기화
TextureShaderClass 1. 셰이더 컴파일 및 개체 GPU에 생성 2. 색상 대신 uv좌표값으로 Input layout 초기화 3. 상수 버퍼 생성 4. Sampler state 생성