ФЭНДОМ


ВведениеПравить

Вам наверное надоело смотреть на кубики на экране, поэтому в этом уроке мы загрузим обычную трехмерную модель на экран. В уроке будет мало теории так как он будет базироваться на всех предыдущих. А вот кода будет много:)

Мы напишем загрузчик модели из формата ms3d. Этот формат используется в программе MilkShape 3D. Я выбрал этот формат потому что он на мой взгляд наиболее простой и при этом может использоваться в реальных проектах. obj и 3ds - это слишком скучно, x - мертв, а колада слишком перегружена для нашего урока.

Будет одно очень важное ограничение к модели - у нее должен быть всего один материал и одна текстура, это ведь урок, вы сами сможете сделать загрузку модели использующей несколько материалов. Также мы пока не будем делать анимацию, только статическую модель.

ПодготовкаПравить

Как обычно, создайте проект, подключите фреймворк третьей версии, создайте main.cpp:

#include "MyRender.h"

int main()
{
	Framework framework;

	MyRender *render = new MyRender();

	FrameworkDesc desc;
	desc.render = render;

	framework.Init(desc);

	framework.Run();

	framework.Close();

	return 0;
}

Создайте MyRender.h:

#pragma once

#include "D3D11_Framework.h"
#include <xnamath.h>

using namespace D3D11Framework;

class StaticMesh;

class MyRender : public Render
{
public:
	MyRender();
	bool Init(HWND hwnd);
	bool Draw();
	void Close();

	void* operator new(size_t i)
	{
		return _aligned_malloc(i,16);
	}

	void operator delete(void* p)
	{
		_aligned_free(p);
	}

private:
	friend StaticMesh;

	StaticMesh *m_mesh;
	
	XMMATRIX m_View;
	XMMATRIX m_Projection;
};

Пока у нас нет еще класса StaticMesh, этот класс и будет отвечать за модель. Мы объявили его дружественным, чтобы иметь возможность обращаться к приватным членам рендера из него. Это решение немного кривовато, но нам пока сойдет:) Мы же не правильную архитектуру учим, а DirectX.

Еще мы добавили две матрицы пространства - видовую и проекционную. А вот мировая здесь не нужна, так как будет в StaticMesh.

Создайте MyRender.cpp:

#include "MyRender.h"
#include "StaticMesh.h"

MyRender::MyRender()
{
	m_mesh = nullptr;
}

bool MyRender::Init(HWND hwnd)
{
	XMVECTOR Eye = XMVectorSet( 0.0f, 0.0f, -2.8f, 0.0f );
	XMVECTOR At = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
	XMVECTOR Up = XMVectorSet( 0.0f, 1.0f, 0.0f, 0.0f );
	m_View = XMMatrixLookAtLH( Eye, At, Up );

	m_Projection = XMMatrixPerspectiveFovLH( 0.4f*3.14f, (float)640/480, 0.1f, 1000.0f);

	m_mesh = new StaticMesh();
	if ( !m_mesh->Init(this, L"mesh.ms3d") )
		return false;

	return true;
}

bool MyRender::Draw()
{
	m_mesh->Render();
	return true;
}

void MyRender::Close()
{
	_CLOSE(m_mesh);
}

В инициализации сначала задаем параметры видовой матрицы. Вы не забыли что видовая матрица - это наша камера из которой мы смотрим на наш мир?

Затем задаем проекционную матрицу и инициализируем наш меш. mesh.ms3d - это имя файла модели.

Спецификация ms3dПравить

Создайте ms3dspec.h и пишите:

#pragma once

#pragma pack(push, 1)

struct MS3DHeader
{
	char id[10];		// always "MS3D000000"
	unsigned int version;	// 3
};

struct MS3DVertex
{
	unsigned char flags;	// SELECTED | SELECTED2 | HIDDEN
	float vertex[3];
	char boneId;		// -1 = no bone
	unsigned char referenceCount;
};

struct MS3DTriangle
{
	unsigned short	flags;	// SELECTED | SELECTED2 | HIDDEN
	unsigned short	vertexIndices[3];
	float		vertexNormals[3][3];
	float		s[3];
	float		t[3];
	unsigned char	smoothingGroup;	// 1 - 32
	unsigned char	groupIndex;
};

struct MS3DGroup
{
	unsigned char	flags;                 // SELECTED | HIDDEN
	char            name[32];
	unsigned short	numtriangles;
	unsigned short  *triangleIndices;      // the groups group the triangles
	char            materialIndex;         // -1 = no material
};

struct MS3DMaterial
{
	char            name[32];                           
	float           ambient[4];                         
	float           diffuse[4];                         
	float           specular[4];                        
	float           emissive[4];                        
	float           shininess;                          // 0.0f - 128.0f
	float           transparency;                       // 0.0f - 1.0f
	char            mode;                               // 0, 1, 2 is unused now
	char            texture[128];                       // texture.bmp
	char            alphamap[128];                      // alpha.bmp
};

#pragma pack(pop, 1)

Это не полная спецификация, а только та часть которая нам нужна для урока. Более полную вы можете посмотреть здесь либо скачать с оффициального сайта.

Данные структуры позволят нам быстро загрузить информацию из файла.

StaticMeshПравить

Создайте StaticMesh.h:

#pragma once

#include "MyRender.h"

class StaticMesh
{
public:
	StaticMesh();

	bool Init(MyRender *render, wchar_t *name);
	void Render();
	void Close();

private:
	bool m_loadMS3DFile(wchar_t* name);
	bool m_LoadTextures(wchar_t* name);
	bool m_InitShader(wchar_t* namevs, wchar_t* nameps);

	void m_RenderBuffers();
	void m_SetShaderParameters();
	void m_RenderShader();

	MyRender *m_render;

	ID3D11Buffer *m_vertexBuffer;
	ID3D11Buffer *m_indexBuffer;
	ID3D11VertexShader *m_vertexShader;
	ID3D11PixelShader *m_pixelShader;
	ID3D11InputLayout *m_layout;
	ID3D11Buffer *m_pConstantBuffer;
	ID3D11SamplerState* m_sampleState;
	ID3D11ShaderResourceView *m_texture;

	XMMATRIX m_objMatrix;
	unsigned short m_indexCount;
	float m_rot;
};

Почти все здесь должно быть вам уже знакомо и понятно, а если нет, то вернитесь к предыдущим урокам.

Из интересного здесь только m_objMatrix. Эта матрица указывает расположение нашего объекта в мире. То есть это мировая матрица. Трансформируя ее вы сможете двигать модель, вращать и масштабировать ее.

Создайте StaticMesh.cpp:

#include "StaticMesh.h"
#include "ms3dspec.h"
#include <fstream>

using namespace std;

struct Vertex
{
	XMFLOAT3 Pos;
	XMFLOAT2 Tex;
};

struct ConstantBuffer
{
	XMMATRIX WVP;
};

wchar_t * ToString(char* mbString)
{ 
	int len = 0; 
	len = (int)strlen(mbString) + 1; 
	wchar_t *ucString = new wchar_t[len]; 
	mbstowcs(ucString, mbString, len); 
	return ucString; 
}

StaticMesh::StaticMesh()
{
	m_vertexBuffer = nullptr;
	m_indexBuffer = nullptr;
	m_vertexShader = nullptr;
	m_pixelShader = nullptr;
	m_layout = nullptr;
	m_pConstantBuffer = nullptr;
	m_sampleState = nullptr;
	m_texture = nullptr;
	m_rot = 0.0f;
}

Вначале опишем вершину и структуру константного буфера. Затем вспомогательная функция ToString() которая перевод char* в wchar_t* что очень нам пригодится ниже.

Теперь инициализация:

bool StaticMesh::Init(MyRender *render, wchar_t *name)
{
	m_objMatrix = XMMatrixIdentity();
	m_render = render;
	if( !m_loadMS3DFile(name) )
		return false;
	if( !m_InitShader(L"mesh.vs", L"mesh.ps") )
		return false;

	return true;
}

В начале мы будем грузить файл меша (метод m_loadMS3DFile() ) А затем инициализировать шейдеры (метод m_InitShader() ).

Начнем с загрузки модели из файла:

bool StaticMesh::m_loadMS3DFile(wchar_t *Filename)
{
	unsigned short VertexCount = 0;
	unsigned short TriangleCount = 0;
	unsigned short GroupCount = 0;
	unsigned short MaterialCount = 0;
	MS3DVertex *pMS3DVertices = nullptr;
	MS3DTriangle *pMS3DTriangles = nullptr;
	MS3DGroup *pMS3DGroups = nullptr;
	MS3DMaterial *pMS3DMaterials = nullptr;

	ifstream fin;
	MS3DHeader header;

	fin.open( Filename,std::ios::binary );
	fin.read((char*)(&(header)), sizeof(header));
	if (header.version!=3 && header.version!=4)
		return false;

	fin.read((char*)(&VertexCount), sizeof(unsigned short));
	pMS3DVertices = new MS3DVertex[VertexCount];
	fin.read((char*)pMS3DVertices, VertexCount * sizeof(MS3DVertex));

	fin.read((char*)(&TriangleCount), sizeof(unsigned short));
	pMS3DTriangles = new MS3DTriangle[TriangleCount];
	fin.read((char*)pMS3DTriangles, TriangleCount * sizeof(MS3DTriangle));

	fin.read((char*)(&GroupCount), sizeof(unsigned short));
	pMS3DGroups = new MS3DGroup[GroupCount];
	for (int i = 0; i < GroupCount; i++)
	{
		fin.read((char*)&(pMS3DGroups[i].flags), sizeof(unsigned char));
		fin.read((char*)&(pMS3DGroups[i].name), sizeof(char[32]));
		fin.read((char*)&(pMS3DGroups[i].numtriangles), sizeof(unsigned short));
		unsigned short triCount = pMS3DGroups[i].numtriangles;
		pMS3DGroups[i].triangleIndices = new unsigned short[triCount];
		fin.read((char*)(pMS3DGroups[i].triangleIndices), sizeof(unsigned short) * triCount);
		fin.read((char*)&(pMS3DGroups[i].materialIndex), sizeof(char));		
	}

	fin.read((char*)(&MaterialCount),sizeof(unsigned short));
	pMS3DMaterials = new MS3DMaterial[MaterialCount];
	fin.read((char*)pMS3DMaterials, MaterialCount * sizeof(MS3DMaterial));
	
	fin.close();

Здесь происходит всего лишь чтение файла и заполнение структур описанных в спецификации данными из этого файла. Теперь ниже:

	m_indexCount = TriangleCount*3;
	WORD *indices = new WORD[m_indexCount];
	if(!indices)
		return false;
	Vertex *vertices = new Vertex[VertexCount];
	if(!vertices)
		return false;

	for (int i = 0; i < TriangleCount; i++ )
	{
		indices[3*i+0] = pMS3DTriangles[i].vertexIndices[0];
		indices[3*i+1] = pMS3DTriangles[i].vertexIndices[1];
		indices[3*i+2] = pMS3DTriangles[i].vertexIndices[2];
	}

	for (int i = 0; i < VertexCount; i++)
	{
		vertices[i].Pos.x = pMS3DVertices[i].vertex[0];
		vertices[i].Pos.y = pMS3DVertices[i].vertex[1];
		vertices[i].Pos.z = pMS3DVertices[i].vertex[2];

		for (int j = 0; j < TriangleCount; j++ )
		{
			if (i == pMS3DTriangles[j].vertexIndices[0])
			{
				vertices[i].Tex.x = pMS3DTriangles[j].s[0];
				vertices[i].Tex.y = pMS3DTriangles[j].t[0];
			}
			else if(i == pMS3DTriangles[j].vertexIndices[1])
			{
				vertices[i].Tex.x = pMS3DTriangles[j].s[1];
				vertices[i].Tex.y = pMS3DTriangles[j].t[1];
			}
			else if(i == pMS3DTriangles[j].vertexIndices[2])
			{
				vertices[i].Tex.x = pMS3DTriangles[j].s[2];
				vertices[i].Tex.y = pMS3DTriangles[j].t[2];
			}
			else
				continue;
			break;
		}
	}

	if( !m_LoadTextures(ToString(pMS3DMaterials[0].texture)) )
		return false;

	_DELETE_ARRAY(pMS3DMaterials);
	if (pMS3DGroups != nullptr)
	{
		for (int i = 0; i < GroupCount; i++)
			_DELETE_ARRAY(pMS3DGroups[i].triangleIndices);
		_DELETE_ARRAY(pMS3DGroups);
	}
	_DELETE_ARRAY(pMS3DTriangles);
	_DELETE_ARRAY(pMS3DVertices);

Мы получили данные из файла и теперь должны из них сделать данные которые передадим нашей видеокарте. Сначала определяем количество индексов. Дело в том что ms3d хранит треугольники. И каждый треугольник содержит три индекса. Поэтому мы умножаем количество треугольников на 3.

Затем создаем нужное количество вершин.

В циклах заполняем сначала наши идексы, а затем вершины.

После циклов мы вызовем метод m_LoadTextures() который загрузит текстуру из первого материала. Вы помните об условии что в этом уроке только один материал (а значит всего одна текстура на модель)?

И удаляем теперь ненужные данные.

Ниже:

	D3D11_BUFFER_DESC bd;
	ZeroMemory( &bd, sizeof(bd) );
	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof( Vertex ) * VertexCount;
	bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
	bd.CPUAccessFlags = 0;
	D3D11_SUBRESOURCE_DATA Data;
	ZeroMemory( &Data, sizeof(Data) );
	Data.pSysMem = vertices;
	HRESULT hr = m_render->m_pd3dDevice->CreateBuffer(&bd, &Data, &m_vertexBuffer);
	if( FAILED( hr ) )
		return false;

	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof( WORD ) * m_indexCount;
	bd.BindFlags = D3D11_BIND_INDEX_BUFFER;
	bd.CPUAccessFlags = 0;
	Data.pSysMem = indices;
	hr = m_render->m_pd3dDevice->CreateBuffer( &bd, &Data, &m_indexBuffer );
	if( FAILED( hr ) )
		return false;

	bd.Usage = D3D11_USAGE_DEFAULT;
	bd.ByteWidth = sizeof(ConstantBuffer);
	bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
	bd.CPUAccessFlags = 0;
	hr = m_render->m_pd3dDevice->CreateBuffer(&bd, NULL, &m_pConstantBuffer);
	if( FAILED( hr ) )
		return false;

	_DELETE_ARRAY(vertices);
	_DELETE_ARRAY(indices);

	return true;
}

Здесь мы создаем три буфера (вершинный, индексный и константный). Как вы видите, мы обращаемся к устройству DirectX через указатель на рендер ( m_render->m_pd3dDevice). Это работает, потому что StaticMesh - друг рендера:)

Метод загрузки текстуры простой:

bool StaticMesh::m_LoadTextures(wchar_t *textureFilename)
{
	HRESULT hr = D3DX11CreateShaderResourceViewFromFile( m_render->m_pd3dDevice, textureFilename, NULL, NULL, &m_texture, NULL );
	if( FAILED( hr ) )
		return false;

	return true;
}

Метод инициализации шейдеров:

bool StaticMesh::m_InitShader(wchar_t* vsFilename, wchar_t* psFilename)
{
	ID3DBlob *vertexShaderBuffer = nullptr;
	HRESULT hr = m_render->m_compileshaderfromfile(vsFilename,"VS", "vs_4_0", &vertexShaderBuffer);
	if( FAILED( hr ) )
		return false;

	ID3DBlob *pixelShaderBuffer = nullptr;
	HRESULT result = m_render->m_compileshaderfromfile(psFilename,"PS", "ps_4_0", &pixelShaderBuffer);
	if( FAILED( hr ) )
		return false;
	
	result = m_render->m_pd3dDevice->CreateVertexShader(vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), NULL, &m_vertexShader);
	if(FAILED(result))
		return false;

	result = m_render->m_pd3dDevice->CreatePixelShader(pixelShaderBuffer->GetBufferPointer(), pixelShaderBuffer->GetBufferSize(), NULL, &m_pixelShader);
	if(FAILED(result))
		return false;

	D3D11_INPUT_ELEMENT_DESC polygonLayout[2];
	polygonLayout[0].SemanticName = "POSITION";
	polygonLayout[0].SemanticIndex = 0;
	polygonLayout[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;
	polygonLayout[0].InputSlot = 0;
	polygonLayout[0].AlignedByteOffset = 0;
	polygonLayout[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[0].InstanceDataStepRate = 0;
	polygonLayout[1].SemanticName = "TEXCOORD";
	polygonLayout[1].SemanticIndex = 0;
	polygonLayout[1].Format = DXGI_FORMAT_R32G32_FLOAT;
	polygonLayout[1].InputSlot = 0;
	polygonLayout[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
	polygonLayout[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
	polygonLayout[1].InstanceDataStepRate = 0;

	unsigned int numElements = sizeof(polygonLayout) / sizeof(polygonLayout[0]);

	result = m_render->m_pd3dDevice->CreateInputLayout(polygonLayout, numElements, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &m_layout);
	if(FAILED(result))
		return false;

	_RELEASE(vertexShaderBuffer);
	_RELEASE(pixelShaderBuffer);

	D3D11_SAMPLER_DESC samplerDesc;
	samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
	samplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
	samplerDesc.MipLODBias = 0.0f;
	samplerDesc.MaxAnisotropy = 1;
	samplerDesc.ComparisonFunc = D3D11_COMPARISON_ALWAYS;
	samplerDesc.BorderColor[0] = 0;
	samplerDesc.BorderColor[1] = 0;
	samplerDesc.BorderColor[2] = 0;
	samplerDesc.BorderColor[3] = 0;
	samplerDesc.MinLOD = 0;
	samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;

	result = m_render->m_pd3dDevice->CreateSamplerState(&samplerDesc, &m_sampleState);
	if(FAILED(result))
		return false;


	return true;
}

Грузим и инициализируем шейдеры, описываем формат ввода (у нас есть две характеристики вершины - позиция и координаты текстуры). Далее создаем семплер описывающий вывод текстур.

Метод рендера меш у нас такой:

void StaticMesh::Render()
{
	m_rot += .0005f;
	if(m_rot > 6.26f)
		m_rot = 0.0f;

	m_RenderBuffers();
	m_SetShaderParameters();
	m_RenderShader();
}

Рендер буферов:

void StaticMesh::m_RenderBuffers()
{
	unsigned int stride = sizeof(Vertex); 
	unsigned int offset = 0;
	m_render->m_pImmediateContext->IASetVertexBuffers(0, 1, &m_vertexBuffer, &stride, &offset);
	m_render->m_pImmediateContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R16_UINT, 0);
	m_render->m_pImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
}

Установка шейдеров:

void StaticMesh::m_SetShaderParameters()
{
	XMVECTOR rotaxis = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
	XMMATRIX Rotation = XMMatrixRotationAxis( rotaxis, m_rot);
	m_objMatrix = Rotation;

	XMMATRIX WVP = m_objMatrix * m_render->m_View * m_render->m_Projection;	
	ConstantBuffer cb;
	cb.WVP = XMMatrixTranspose(WVP);
	m_render->m_pImmediateContext->UpdateSubresource( m_pConstantBuffer, 0, NULL, &cb, 0, 0 );
	
	m_render->m_pImmediateContext->VSSetConstantBuffers(0, 1, &m_pConstantBuffer);

	m_render->m_pImmediateContext->PSSetShaderResources(0, 1, &m_texture);
}

В начале вращаем матрицу меша, затем перемножаем матрицу меша с видовой и проекционной.

Вывод шейдеров:

void StaticMesh::m_RenderShader()
{
	m_render->m_pImmediateContext->IASetInputLayout(m_layout);
	m_render->m_pImmediateContext->VSSetShader(m_vertexShader, NULL, 0);
	m_render->m_pImmediateContext->PSSetShader(m_pixelShader, NULL, 0);
	m_render->m_pImmediateContext->PSSetSamplers(0, 1, &m_sampleState);
	m_render->m_pImmediateContext->DrawIndexed(m_indexCount, 0, 0);
}

Очистка ресурсов:

void StaticMesh::Close()
{
	_RELEASE(m_texture);
	_RELEASE(m_indexBuffer);
	_RELEASE(m_vertexBuffer);
	_RELEASE(m_pConstantBuffer);
	_RELEASE(m_sampleState);
	_RELEASE(m_layout);
	_RELEASE(m_pixelShader);
	_RELEASE(m_vertexShader);
}

ШейдерыПравить

У нас будет два шейдера. Мы их можем использовать для всех мешей.

mesh.vs:

cbuffer cbPerObject
{
	float4x4 WVP;
};

struct VertexInputType
{
    float4 pos : POSITION;
    float2 tex : TEXCOORD;
};

struct PixelInputType
{
    float4 pos : SV_POSITION;
    float2 tex : TEXCOORD;
};

PixelInputType VS(VertexInputType input)
{
    PixelInputType output;

    output.pos = mul(input.pos, WVP);
    output.tex = input.tex;

    return output;
}

mesh.ps:

Texture2D ObjTexture;
SamplerState SampleType;

struct PixelInputType
{
	float4 pos : SV_POSITION;
	float2 tex : TEXCOORD;
};

float4 PS(PixelInputType input) : SV_TARGET
{
    return ObjTexture.Sample( SampleType, input.tex );
}

ИтогПравить

Собирайте. Где взять сам меш? Вы можете сами создать его в MilkShape 3D (не забудьте - всего один материал), либо откуда-то скачать. Вот меш который использовал я при написании данного урока. Данный меш и текстура были взяты из игры World of Warcraft и все права принадлежат Blizzard. Используя программу WoW Model Viewer вы сможете сами взять любую модель из этой игры, просто выберите ее и экспортируйте в ms3d (но только ищите те у которых всего одна текстура).

Скачать

Вот так это выглядит у меня:

Test 2012-09-29 14-14-52-18

Материалы сообщества доступны в соответствии с условиями лицензии CC-BY-SA , если не указано иное.