Уроки Коммент.

Создание игр » Featured, Toolset » Экспорт из 3d max – 3d max sdk

Экспорт из 3d max – 3d max sdk

Export 3D Studio Max SDKЭкспорт из 3d max можно сделать и на Си++. В прошлом уроке Экспорт из 3d max за 5 минут – max script я рассказал Вам, как можно сделать экспорт из 3d max с помощью maxscript, теперь же хочу объяснить как сделать примерно то же самое, но с помощью 3d max SDK. Для создания этого плагина экспорта из 3d max Вам, в первую очередь, нужно будет установить 3d max SDK, идущий в комплекте дистрибутива самого 3d Studio MAX. Не забудьте так же прописать пути к хидерам и библиотекам этого SDK в Visual Studio. В отличие от прошлого урока по экспорту из 3d max, на этот раз мы будем делать экспорт в бинарный формат – такие данные занимают меньше места и грузятся заметно быстрее.

Создание плагина экспорта из 3d max

В составе 3D Studio MAX SDK есть плагин для Visual Studio, который позволяет создавать новые плагины (в т.ч. и экспорта/импорта) для 3D MAX через специальный визард. Установка этого плагина совсем не сложная и описана в хелпе. Я лишь отмечу, что не раз замечал ситуацию, когда этот плагин совершенно беспричинно перестаёт (либо даже не начинает) работать и вываливается с ошибками. Экспорт из 3d maxЕсли у Вас будет нечто аналогичное – я пока не знаю, как помочь и от чего зависит наличие, либо отсутствие этого бага. Потому просто ставьте этот wizard и попробуйте заставить его работать – с большой долей вероятности всё должно получиться.

После того, как вы установили этот плагин, просто запускайте ваш Visual Studio и создавайте новый проект “3ds Max Plugin Wizard”, задайте вашему проекту имя, выберите тип плагина “File Export 3DXI”, заполните поля в которых указывается имя плагина, желаемый класс, категория и описание. В итоге Вы получите готовый каркас для плагина экспорта из 3d max. Если же визард по каким-то причинам не заработает – думаю, единственное, что вам остаётся, это взять проект с моими исходниками плагина и просто скомпилировать его.

Классы экспорта

Плагин-визард по умолчанию создаст в проекте два класса. У меня они называются ExportToGame, он унаследован от SceneExport и ExportToGameClassDesc – унаследован от ClassDesc2. Класс ExportToGame является, собственно, самим плагином экспорта из 3d max, а ExportToGameClassDesc это класс, описывающий наш экспорт-плагин для 3d MAX – он указывает какое расширение файлов будет у наших файлов, задаёт описание, категорию и т.д., в общем, несёт чисто информативный характер.

Процедура экспорта из 3d max

Я не буду цитировать сюда всех исходники экспортера, которые сделал для меня плагин-визард. Расскажу лишь о том, как этот экспорт вообще работает в 3d studio max. Я так же опущу описания всяких возможных опций и т.д. – если Вам они нужны, просто почитайте справку по 3d max SDK.

Основная процедура экспорта, это, в моём случае ExportToGame::DoExport:

int	ExportToGame::DoExport(const TCHAR* name,ExpInterface* ei,Interface* i, 
		BOOL suppressPrompts, DWORD options)
{
	// This is where the file export operation occur.
 
	if(!suppressPrompts)
		DialogBoxParam(hInstance, 
				MAKEINTRESOURCE(IDD_PANEL), 
				GetActiveWindow(), 
				ExportToGameOptionsDlgProc, (LPARAM)this);
 
	// Construct a tab with all this scene's nodes.
	// We could also only take the node currently selected, etc.
	INodeTab lNodes;
	GetSceneNodes(lNodes);
 
 
	// Initialise 3DXI (formerly IGame) object
	// For more information, please see 
	// 3ds Max SDK topic PG: Programming with 3DXI.
	IGameScene * pIgame = GetIGameInterface();	
	IGameConversionManager* cm = GetConversionManager();
	cm->SetCoordSystem( IGameConversionManager::IGAME_D3D );
	pIgame->InitialiseIGame(lNodes);
 
 
	// получаем список мешей
	Tab<IGameNode*> meshes = pIgame->
	 	GetIGameNodeByType( IGameObject::IGAME_MESH );
 
	// если мешей нет или их много
	if (meshes.Count()!=1)
		return FALSE; // не будем делать экспорт

Тут почти весь код сгенерил тот самый визард. А я лучше расскажу вам о том, что “нагенерил” я сам )

Для того, что бы сделать экспорт, я создал отдельный класс, который назвал MyTriMesh – этот класс при экспорте будет промежуточным буфером между 3d max и файлом. Я решил сразу сделать возможность экспортировать модели, на которых лежит несколько материалов, наложенных с помощью MutiSubObject-материала (это стандартный метод использования нескольких материалов на одной модели для 3d max). Потому в итоге я пришёл к тому, что не надо хранить все вертексы и индексы в одном месте, а решил разбить модель на несколько частей – саб-мешей, каждый из которых будет хранить информацию по вертексами и граням с определённым MaterialID. Соответственно, сам MyTriMesh хранит в себе лишь список SubMesh’ей, в которых и хранится непосредственно информация о модели:

// структура для хранения вертекса
struct MyVertex
{
	float x, y, z; // pos
	float tx, ty, tz; // tangent
	float bx, by, bz; // binormal
	float nx, ny, nz; // normal
	float tu1, tv1, tu2, tv2; // texcoords
	DWORD color; // vertex color
 
};
 
bool operator==(const MyVertex& a, const MyVertex& v);
 
class SubMesh
{
	//! Идентификатор материала данного сабмеша
	DWORD dwMatID;
	//! Вертексы сабмеша
	std::vector<MyVertex> m_vertices;
	//! Индексы (для сборки граней) сабмеша
	std::vector<WORD> m_indices;
 
	//! Функция оптимизации вертексов под кэш видео-карты
	void OptimizeVertices();
	//! Функция оптимизации граней (индексов)
	void OptimizeFaces();
	//! Добавление нового вертекса и возвращение его индекса
	WORD AddNewVertex(const MyVertex& v);
 
public:
	SubMesh();
	SubMesh(DWORD iMatID);
	~SubMesh(void);
	//! Добавление нового вертекса в сабмеш
	void AddVertex(const MyVertex& v);
	//! Оптимизация сабмеша
	void Optimize();
	//! Запись сабмеша в указанный поток
	bool Save(std::ostream& out);
};
 
struct MeshHeader
{
	DWORD flags[16];
};
 
class MyTriMesh
{
	//! Сабмеши этой модели. Отдельный сабмеш для каждого материала
	std::vector<SubMesh*> vecSubMeshes;
 
public:
	MyTriMesh();
	~MyTriMesh();
 
	//! Функция "очистки" модели
	void Clear();
	//! Функция для поиска сабмеша (либо создания нового) по заданному ID
	SubMesh* AddOrFindSubMeshByMat(unsigned int iMatID);
 
	//! Оптимизация модели (всех сабмешей)
	void Optimize();
	//! Запись модели в указанный поток
	bool Save(std::ostream& out);
};

Комментариев сделал очень много – я думаю, они говорят обо всём, что Вам нужно знать в данном случае.

Ну а теперь код самого экспорта (продолжение функции ):

ofstream out(name, std::ios::binary);
 
for (int i=0; i<meshes.Count(); i++)
{
	IGameMesh* mesh = static_cast<IGameMesh*>(meshes[i]->GetIGameObject());
	if (mesh->InitializeData())
	{
		int iFacesCount = mesh->GetNumberOfFaces();
		for (int iFace=0; iFace<iFacesCount; iFace++)
		{
			FaceEx* pFace = mesh->GetFace(iFace);
			for (int i=0; i<3; i++)
			{
				Point3 pos = mesh->GetVertex(pFace->vert[i], true);
 
				int iIndexTB = mesh->GetFaceVertexTangentBinormal(iFace, i);
				Point3 tangent = mesh->GetTangent(iIndexTB);
				Point3 binormal = mesh->GetBinormal(iIndexTB);
				Point3 normal = mesh->GetNormal(pFace->norm[i], true);
 
				Point3 color = mesh->GetColorVertex(pFace->color[i]);
 
				int iTexIndex = mesh->GetFaceTextureVertex(iFace, i, 1);
				Point2 tc1 = mesh->GetTexVertex(iTexIndex);
 
				iTexIndex = mesh->GetFaceTextureVertex(iFace, i, 2);
				Point2 tc2 = mesh->GetTexVertex(iTexIndex);
 
				// формируем вертекс
				MyVertex v;
				v.x = pos.x; v.y = pos.y; v.z = pos.z;
				v.nx = normal.x; v.ny = normal.y, v.nz = normal.z;
				v.tx = tangent.x; v.ty = tangent.y; v.tz = tangent.z;
				v.bx = binormal.x; v.by = binormal.y; v.bz = binormal.z;
				v.tu1 = tc1.x; v.tu1 = tc1.y;
				v.tu2 = tc2.x; v.tu2 = tc2.y;
				v.color = ((int(color.x*0xff)&0xff)<<16) | 
					((int(color.y*0xff)&0xff)<<8) | (int(color.z*0xff)&0xff);
 
 
				// добавляем вертекс в "модель"
				SubMesh* pSub = m_mesh.AddOrFindSubMeshByMat(
					mesh->GetFaceMaterialID(iFace));
				pSub->AddVertex(v);
			}
		}
	}
}
m_mesh.Save(out);
m_mesh.Clear();

Что тут происходит, в общем-то, тоже абсолютно очевидно, но я всё равно поясню:

  1. Мы в цикле перебираем все модели (хотя у нас она всего одна, но я сделал “задел на будущее”)
  2. Для каждой модели “инициализируем” её – без этого прочитать данные модели не получится
  3. Получаем количество граней в модели и пробегаемся по всем граням
  4. Для каждой грани перебираем все три её вертекса ( вертексы 0, 1, 2 )
  5. Для каждого вертекса с помощью разных функций получаем данные, которые нам и надо экспортировать из 3d max
  6. По идентификатору материала находим, либо добавляем новый сабмеш в экспортируемую модель
  7. Добавляем в этот сабмеш собранный вертекс
  8. Когда цикл закончен – записываем модель в файл и отчищаем её

Собственно, вот и весь экспорт. О том, как избавить модель от “лишних” данных, вроде дублирующихся вертексов, как правильно сделать индексы для модели – я уже говорил вам в прошлых уроках. Потому я просто приведу код реализации классов SubMesh и MyTriMesh:

bool operator==(const MyVertex& a, const MyVertex& v)
{
	return (a.x == v.x) && (a.y== v.y) && (a.z == v.z) &&
		(a.tx == v.tx) && (a.ty == v.ty) && (a.tz == v.tz) &&
		(a.bx == v.bx) && (a.by == v.by) && (a.bz == v.bz) &&
		(a.nx == v.nx) && (a.ny == v.ny) && (a.nz == v.nz) &&
		(a.tu1 == v.tu1) && (a.tv1 == v.tv1) && (a.tu2 == v.tu2) &&
		(a.tv2 == v.tv2) && (a.color == v.color);
}
 
 
SubMesh::SubMesh()
{
	dwMatID = 0;
}
 
SubMesh::SubMesh(DWORD iMatID)
{
	dwMatID = iMatID;
}
 
SubMesh::~SubMesh(void)
{
}
 
void SubMesh::OptimizeVertices()
{
	const unsigned int numFaces = (unsigned int )m_indices.size()/3;
	const unsigned int numVerts = (unsigned int)m_vertices.size();
 
	DWORD * pdwRemap = new DWORD [numVerts];
	D3DXOptimizeVertices( &m_indices[0], numFaces, numVerts, FALSE, 
		pdwRemap );
 
	// make a copy
	std::vector<MyVertex> copyVB;
	copyVB = m_vertices;
 
	// remap m_vertices
	for( unsigned int i = 0; i < numVerts; ++i )
		m_vertices[pdwRemap[i]] = copyVB[i];
//		memcpy(&m_vertices[pdwRemap[i]], &copyVB[i], sizeof(MeshVert));
 
	// remap triangles
	for( unsigned int i = 0; i < numFaces; ++i )
		for( int j = 0; j < 3; ++j )
			m_indices[i*3+j] = (WORD)pdwRemap[ m_indices[i*3+j] ];
	delete[] pdwRemap;
}
 
void SubMesh::OptimizeFaces()
{
	unsigned short * pIB = (unsigned short *)&m_indices[0];
	const unsigned int numFaces = unsigned int(m_indices.size())/3;
	const unsigned int numVerts = unsigned int(m_vertices.size());
 
	DWORD * pdwRemap = new DWORD[numFaces];
	D3DXOptimizeFaces( pIB, numFaces, numVerts, FALSE, pdwRemap );
 
	unsigned short * pCopyIB = new unsigned short[numFaces*3];
	memcpy( pCopyIB, pIB, numFaces*6 );
 
	for( unsigned int i = 0; i < numFaces; ++i )
	{
		int newFace = (int)pdwRemap[i];
		for( int j = 0; j < 3; ++j )
			pIB[newFace*3+j] = pCopyIB[i*3+j];
	}
	delete[] pCopyIB;
	delete[] pdwRemap;
}
 
void SubMesh::Optimize()
{
	OptimizeFaces();
	OptimizeVertices();
}
 
bool SubMesh::Save(std::ostream& out)
{
	WORD iVertCount = (WORD)m_vertices.size();
	DWORD iIdxCount = (DWORD)m_indices.size();
 
// это дефайн что бы было проще писать в файл
#define _write(data) out.write((const char*)&data, sizeof(data))
 
	_write(dwMatID);
	_write(iVertCount);
	_write(iIdxCount);
 
	for (WORD i=0; i<iVertCount; i++)
	{
		_write(m_vertices[i].x);
		_write(m_vertices[i].y);
		_write(m_vertices[i].z);
		_write(m_vertices[i].tx);
		_write(m_vertices[i].ty);
		_write(m_vertices[i].tz);
		_write(m_vertices[i].bx);
		_write(m_vertices[i].by);
		_write(m_vertices[i].bz);
		_write(m_vertices[i].nx);
		_write(m_vertices[i].ny);
		_write(m_vertices[i].nz);
		_write(m_vertices[i].tu1);
		_write(m_vertices[i].tv1);
		_write(m_vertices[i].tu2);
		_write(m_vertices[i].tv2);
		_write(m_vertices[i].color);
	}
 
	out.write((const char*)&m_indices[0], sizeof(WORD)*m_indices.size());
 
	return true;
}
 
WORD SubMesh::AddNewVertex(const MyVertex& v)
{
	// ищем такой же вертекс
	for (size_t i=0; i<m_vertices.size(); i++)
		if (v == m_vertices[i])
			return WORD(i);
 
	// не нашли - добавляем новый
	m_vertices.push_back(v);
	return WORD(m_vertices.size()-1);
}
 
void SubMesh::AddVertex(const MyVertex& v)
{
	// добавляем вертекс и индекс
	m_indices.push_back(AddNewVertex(v));
}
 
MyTriMesh::MyTriMesh()
{
}
 
MyTriMesh::~MyTriMesh()
{
	Clear();
}
 
void MyTriMesh::Clear()
{
	const unsigned int iCount = (int)vecSubMeshes.size();
	for (unsigned int i=0; i<iCount; i++)
		if (vecSubMeshes[i]!=NULL)
			delete vecSubMeshes[i];
	vecSubMeshes.clear();
}
 
void MyTriMesh::Optimize()
{
	const unsigned int iCount = (unsigned int)vecSubMeshes.size();
	for (unsigned int i=0; i<iCount; i++)
		if (vecSubMeshes[i]!=NULL)
			vecSubMeshes[i]->Optimize();
}
 
SubMesh* MyTriMesh::AddOrFindSubMeshByMat(unsigned int iMatID)
{
	while(vecSubMeshes.size()<iMatID+1)
		vecSubMeshes.push_back(NULL);
 
	if (vecSubMeshes[iMatID]==NULL)
		vecSubMeshes[iMatID] = new SubMesh(iMatID);
 
	return vecSubMeshes[iMatID];
}
 
bool MyTriMesh::Save(std::ostream& out)
{
	unsigned int subMeshesCount = 0;
	const unsigned int iCount = (unsigned int)vecSubMeshes.size();
 
	for (unsigned int i=0; i<iCount; i++)
		if (vecSubMeshes[i]!=NULL)
			subMeshesCount++;
 
	MeshHeader meshHeader;
	ZeroMemory(&meshHeader, sizeof(MeshHeader));
 
	out.write((const char*)&meshHeader, sizeof(MeshHeader));
	out.write((const char*)&subMeshesCount, sizeof(unsigned int));
 
	for (unsigned int i=0; i<iCount; i++)
		if (vecSubMeshes[i]!=NULL)
			if (!vecSubMeshes[i]->Save(out))
				return false;
	return true;
}

Стурктуру meshHeader я сделал про запас. Пока в ней содержатся только нули, но в будущем, когда мы будем развивать движок, мы сможем записывать в неё какую-то полезную информацию – например, верисию формата файла, флаги того, какие доп. данные содержатся в файле и прочее.

На этом об экспорте пока всё. Я сам ещё не проверил, работает ли он, т.к. 3d max под рукой сейчас нет. Но Вы можете сделать это самостоятельно )))

Скачать проект с исходным кодом

Как всегда – жду ваших комментариев!

Ещё по этой теме:




Раздел: Featured, Toolset · Теги: 3d max

10 комментариев на "Экспорт из 3d max – 3d max sdk"
  1. L-ee-X пишет:

    Супер :) Спасибо :)

    1. Вячеслав пишет:

      L-ee-X, да не за что )
      Получилось разобраться что к чему? Понятно написал? А то у меня некоторые сомнения есть по поводу того, что эта статья получилась достаточно понятной и простой…

  2. Antony пишет:

    Лично я ничего не могу сказать, просто потому что не пользуюсь 3d max и его у меня нет :)

  3. Вячеслав пишет:

    Залил обновлённую версию плагина – теперь экспортится только текущий выбранный объект, так удобнее работать. Кроме того, сделал ещё несколько небольших правок кода.

  4. Everard пишет:

    Отличная статья! Читал про экспорт с помощью max script – там все по-минимуму описано было, а здесь все как надо, с тангентами и бинормалями. Тут, насколько я понял, экспортятся текс. координаты из первого и второго каналов.

    Интересно было бы увидеть апдейт, где экспортится N каналов (типа сколько в меше текс. координат используется – столько и экспортится).

    1. Вячеслав пишет:

      Everard, рад, что Вам понравилось.

      Доработать экспортер соответствующим образом, для экспорта N’ного количества текстурных каналов – задача не сложная. Думаю, будет лучше, если каждый из читателей блога попробует решить её сам – уверяю Вас, решённая самостоятельно задача приносит в сотни раз больше удовлетворения, чем решённая кем-то другим ;-)

  5. Роман пишет:

    А Вы не могли бы про создание плагина под ArchiCad написать?
    Заранее спасибо.

    1. Вячеслав пишет:

      К сожалению, не могу. Никогда не занимался этим.

  6. ASK'R пишет:

    Попытался собрать плагин из ваших исходников, получил error 2019 unresolved external symbol для D3DXOptimizeVertices и D3DXOptimizeFaces. Студия 2010 про, DXSDK Jun10, в проекте указал пути к max sdk и dx sdk, в функций переход к определению работает. Не понимаю, что не хватает? (не программист, моделлер, просто достало ждать, когда заэкспортится скриптом, хочу плагин – собрать ваш и потихоньку переписывать для соответствия нашему формату)

Оставить комментарий

*

Вы можете использовать это HTMLтеги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>