Экспорт геометрии коллизии с материалом из 3DS Max в формат XML.

Опубликовано: 01.09.2018

В предыдущей статье http://www.gamedev.ru/code/tip/Collision_material мы разобрали как написать простой скрипт, с помощью которого можно создать костюмный материал, но мы не разобрали, как его использовать. В данной статье мы разберём как создать скрипт для экспорта из 3DS MAX геометрии коллизии с наложенным материалом.

В MAX Script Help предлагаю ознакомиться со следующими материалами:

Standard Open and Save File Dialogs

https://knowledge.autodesk.com/search-result/caas/CloudHelp/cloud… D37C-htm.html

Rollout User-Interface Controls Types

http://docs.autodesk.com/3DSMAX/15/ENU/MAXScript-Help/index.html?… EA5380E32.htm ,topicNumber=d30e621026

Button UI Control

http://docs.autodesk.com/3DSMAX/15/ENU/MAXScript-Help/index.html?… 6A6602ACA.htm ,topicNumber=d30e621971

Create Dialog

http://docs.autodesk.com/3DSMAX/15/ENU/MAXScript-Help/index.html?… EA5380E32.htm ,topicNumber=d30e621026

Сам скрипт начнём писать с создания пользовательского интерфейса, где будет простое меню.

rollout exportCollisionRollout "Collision Exporter" width:175 height:142 ( ) createDialog exportCollisionRollout

Также нам понадобится всего одна кнопочка — «экспорт статической коллизии».

<item_type> <name> [<label_string>] [ <parameters> ]

  <item_type> — тип элемента пользовательского интерфейса (полный список:

  http://docs.autodesk.com/3DSMAX/15/ENU/MAXScript-Help/index.html?… F2A2C0A06.htm ,topicNumber=d30e621029)

  <name> — используется, чтобы называть автоматически построенную локальную переменную, которая будет содержать значение, представляющее элемент управления, и используется для связывания функций обработчика событий с элементом управления.

  <label_string> — используется в качестве заголовка, метки элемента или текстового содержимого, в зависимости от типа элемента управления, как описано в разделах для каждого типа.

  <parameters> — последовательность аргументов ключевого слова, используемых для установки параметров или влияния макета для элемента управления. Точные параметры, поддерживаемые каждым типом элемента управления, также определяются в разделах для каждого типа.

button 'btn1' "Export Static Collision" pos:[27,21] width:120 height:40 align:#left

Еще нам понадобится обработчик сообщения нажатии кнопки.

on <item_name> <event_name> [ <argument> ] do <expr>

где:

  <item_name> — указывает имя элемента, к которому будет подключен наш обработчик “btn1”.

  <event_name> — определяет тип события, которое необходимо обработать “pressed”, аргументов в данном случаи не будет.

  <expr> — тело функции заключённое в скобки ().

получаем следующее:

Всё вместе на данный момент:

rollout exportCollisionRollout "Collision Exporter" width:175 height:83 ( button 'btn1' "Export Static Collision" pos:[27,21] width:120 height:40 align:#left on btn1 pressed do ( ) ) createDialog exportCollisionRollout

Теперь нам предстоит создать функционал, который будет производить запись геометрии в файл формата XML. Дальше я не буду описывать всё в деталях и мелочах, так как это уже синтаксис cамого скриптового языка, буду добавлять некие замечания, объяснения, где это необходимо.

Для начала создадим новый метод “fn exportCollision =”. Его задачей будет получить имя файла, в который мы будем производить запись, получить 3д объект коллизии, который мы будем записывать, выставить трансформацию для объекта и, конечно, вызвать метод, который и произведёт запись.

fn exportCollision = ( clearListener() local fileName = (getSaveFileName types:"Static Collision|*.xml") local collObj = selection[1] if collObj != undefined then ( if fileName != undefined then ( local colTransform = collObj.transform collObj.transform = Matrix3 1 createXMLFile fileName collObj collObj.transform = colTransform ) else ( messagebox "Bad file name." title:"Information" ) ) else ( messagebox "Nothing to export! You haven't selected any object!" title:"Information" ) )

Пояснения:

local collObj = selection[1] — тут происходит захват первого объекта который выделен во воюпорте и запись его в локальную переменную.

local colTransform = collObj.transform — записываем трансформацию объекта во временную переменную

collObj.transform = Matrix3 1 — назначаем нашему объекту единичную матрицу для того что бы он встал по центру вюпорта x = y = z = 0, убераем также любой поворот, а также маштаб.

createXMLFile fileName collObj — вызываем наш метод для записи меша колизии во файл.

collObj.transform = colTransform — возвращаем объект в изначальное положение.

Примечание: Не всегда статическую коллизию надо выставлять по центру. Перед экспортом в не которых физических движках более эффективно оставлять статику на своём оригинальном положении, как, например, с Bullet Physics.

Метод “createXMLFile”

fn createXMLFile fileName collisionObj = ( print fileName print collisionObj.name local tmesh = Snapshotasmesh collisionObj local materialCount = (getColMaterialCount tmesh) as integer if doesFileExist(fileName) == false then ( global XMLFile = (createFile fileName) ) else ( global XMLFile= (openFile fileName mode:"w+") ) format "<?xml version=\"1.0\"?>" to:XMLFile format "\n<CollisionShape>" to:XMLFile format "\n\t<Name>%</Name>" collisionObj.name to:XMLFile format "\n\n\t<Bounds>" to:XMLFile format "\n\t\t<Min x=\"%\" y=\"%\" z=\"%\" />" \ collisionObj.min.x collisionObj.min.y collisionObj.min.z to:XMLFile format "\n\t\t<Max x=\"%\" y=\"%\" z=\"%\" />" \ collisionObj.max.x collisionObj.max.y collisionObj.max.z to:XMLFile format "\n\t</Bounds>" to:XMLFile -------------------------------------Writing geometry data---------------------------------- ---------------Writing vertices--------------- format "\n\n\t<VertexData count=\"%\" >" tmesh.numverts to:XMLFile for v = 1 to tmesh.numverts do ( local vert = (getvert tmesh v) format "\n\t\t<Vertex x=\"%\" y=\"%\" z=\"%\" />" vert.x vert.y vert.z to:XMLFile ) format "\n\t</VertexData>" to:XMLFile ---------------Writing faces------------- format "\n\n\t<FaceData count=\"%\" materialCount=\"%\" >" tmesh.numfaces materialCount to:XMLFile for f = 1 to tmesh.numfaces do ( tface = (getface tmesh f) faceMaterialID = getFaceMatId tmesh f newMaterialID = (getReindexedMatID faceMaterialID) format "\n\t\t<Face id1=\"%\" id2=\"%\" id3=\"%\" matID=\"%\" />" \ ((tface.x - 1) as integer) ((tface.y - 1)as integer) ((tface.z - 1) as integer) \ newMaterialID to:XMLFile ) format "\n\t</FaceData>" to:XMLFile -------------------------------------Writing Materials------------------------------------- format "\n\n\t<MaterialData count=\"%\" >" materialCount to:XMLFile for m = 1 to reindexedMaterialIDS.count do ( local mat = getMaterial collisionObj reindexedMaterialIDS[m].oldID format "\n\t\t<Material matID=\"%\" matTypeID=\"%\" generateProps=\"%\" />" \ reindexedMaterialIDS[m].newID mat.M_MAT_TYPE mat.M_FLAG_PROPS to:XMLFile ) format "\n\t</MaterialData>" to:XMLFile format "\n\n</CollisionShape>" to:XMLFile close XMLFile delete tmesh reindexedMaterialIDS = #() messageBox "Done !" )

Примечание: Если строка скрипта слишком длинна и вылезает за экран, это создаёт некие неудобства, поэтому её можно разделить на две или даже больше строк с помощью оператора "\".

format "\n\t\t<Min x=\"%\" y=\"%\" z=\"%\" />" \ collisionObj.min.x collisionObj.min.y collisionObj.min.z to:XMLFile

Вспомогательные методы и структуры

struct ColMatID ( oldID, newID ) global reindexedMaterialIDS = #() глобальный массив который будет хранить ColMatID структуры.

  fn getReindexedMatID oldMatID = ( local nID = 0 for m = 1 to reindexedMaterialIDS.count do ( if oldMatID == reindexedMaterialIDS[m].oldID do ( return reindexedMaterialIDS[m].newID ) ) return nID )

Один мультиматериал может быть наложен на несколько разных объектов в 3д максе. Этот же мультиматериал может содержать множество разных под-материалов, а это значит, что их индекс не всегда будет начинаться с нуля (единицы), и по порядку, как это будет записано в файле, поэтому нам нужно сделать некую переиндексацию, сохраняя старые ID материалов.

fn getColMaterialCount tmesh = ( local materialsUsed = #() for f = 1 to tmesh.numfaces do ( mat_id = getFaceMatId tmesh f appendIfUnique materialsUsed mat_id ) for m = 1 to materialsUsed.count do ( local nID = ColMatID() nID.oldID = materialsUsed[m] as integer nID.newID = ((m as integer) - 1) append reindexedMaterialIDS nID ) return materialsUsed.count )

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

fn getMaterial collObj oldMatID = ( case classof collObj.mat of ( Multimaterial: ( if oldMatID >= 0 and oldMatID <= collObj.mat.count then ( return collObj.mat[oldMatID] ) else ( tempMat = TUT_COL_MAT() tempMat.M_MAT_TYPE = 1 tempMat.M_FLAG_PROPS = false return tempMat ) ) TUT_COL_MAT: ( return collObj.mat ) Default: ( tempMat = TUT_COL_MAT() tempMat.M_MAT_TYPE = 1 tempMat.M_FLAG_PROPS = false return tempMat ) ) )

Данный метод принимает изначальный объект, а также начальный ID (поскольку списки материала не изменились). Дальше идёт switch case с тремя возможностями. Если материал является мультиматериалом, то следовательно будут и под-материалы (должны быть). Если материал является TUT_COL_MAT, то мы его просто возвращаем, если нет — создаём такой и назначаем некие начальные значения.

Полный скрипт:

struct ColMatID ( oldID, newID ) global reindexedMaterialIDS = #() fn getReindexedMatID oldMatID = ( local nID = 0 for m = 1 to reindexedMaterialIDS.count do ( if oldMatID == reindexedMaterialIDS[m].oldID do ( return reindexedMaterialIDS[m].newID ) ) return nID ) fn getColMaterialCount tmesh = ( local materialsUsed = #() for f = 1 to tmesh.numfaces do ( mat_id = getFaceMatId tmesh f appendIfUnique materialsUsed mat_id ) for m = 1 to materialsUsed.count do ( local nID = ColMatID() nID.oldID = materialsUsed[m] as integer nID.newID = ((m as integer) - 1) append reindexedMaterialIDS nID ) return materialsUsed.count ) fn getMaterial collObj oldMatID = ( case classof collObj.mat of ( Multimaterial: ( if oldMatID >= 0 and oldMatID <= collObj.mat.count then ( return collObj.mat[oldMatID] ) else ( tempMat = TUT_COL_MAT() tempMat.M_MAT_TYPE = 1 tempMat.M_FLAG_PROPS = false return tempMat ) ) TUT_COL_MAT: ( return collObj.mat ) Default: ( tempMat = TUT_COL_MAT() tempMat.M_MAT_TYPE = 1 tempMat.M_FLAG_PROPS = false return tempMat ) ) ) fn createXMLFile fileName collisionObj = ( print fileName print collisionObj.name local tmesh = Snapshotasmesh collisionObj local materialCount = (getColMaterialCount tmesh) as integer if doesFileExist(fileName) == false then ( global XMLFile = (createFile fileName) ) else ( global XMLFile= (openFile fileName mode:"w+") ) format "<?xml version=\"1.0\"?>" to:XMLFile format "\n<CollisionShape>" to:XMLFile format "\n\t<Name>%</Name>" collisionObj.name to:XMLFile format "\n\n\t<Bounds>" to:XMLFile format "\n\t\t<Min x=\"%\" y=\"%\" z=\"%\" />" \ collisionObj.min.x collisionObj.min.y collisionObj.min.z to:XMLFile format "\n\t\t<Max x=\"%\" y=\"%\" z=\"%\" />" \ collisionObj.max.x collisionObj.max.y collisionObj.max.z to:XMLFile format "\n\t</Bounds>" to:XMLFile -------------------------------------Writing geometry data---------------------------------- ---------------Writing vertices--------------- format "\n\n\t<VertexData count=\"%\" >" tmesh.numverts to:XMLFile for v = 1 to tmesh.numverts do ( local vert = (getvert tmesh v) format "\n\t\t<Vertex x=\"%\" y=\"%\" z=\"%\" />" vert.x vert.y vert.z to:XMLFile ) format "\n\t</VertexData>" to:XMLFile ---------------Writing faces------------- format "\n\n\t<FaceData count=\"%\" materialCount=\"%\" >" tmesh.numfaces materialCount to:XMLFile for f = 1 to tmesh.numfaces do ( tface = (getface tmesh f) faceMaterialID = getFaceMatId tmesh f newMaterialID = (getReindexedMatID faceMaterialID) format "\n\t\t<Face id1=\"%\" id2=\"%\" id3=\"%\" matID=\"%\" />" \ ((tface.x - 1) as integer) ((tface.y - 1)as integer) ((tface.z - 1) as integer) \ newMaterialID to:XMLFile ) format "\n\t</FaceData>" to:XMLFile -------------------------------------Writing Materials------------------------------------- format "\n\n\t<MaterialData count=\"%\" >" materialCount to:XMLFile for m = 1 to reindexedMaterialIDS.count do ( local mat = getMaterial collisionObj reindexedMaterialIDS[m].oldID format "\n\t\t<Material matID=\"%\" matTypeID=\"%\" generateProps=\"%\" />" \ reindexedMaterialIDS[m].newID mat.M_MAT_TYPE mat.M_FLAG_PROPS to:XMLFile ) format "\n\t</MaterialData>" to:XMLFile format "\n\n</CollisionShape>" to:XMLFile close XMLFile delete tmesh reindexedMaterialIDS = #() messageBox "Done !" ) fn exportCollision = ( clearListener() local fileName = (getSaveFileName types:"Static Collision|*.xml") local collObj = selection[1] if collObj != undefined then ( if fileName != undefined then ( local colTransform = collObj.transform collObj.transform = Matrix3 1 createXMLFile fileName collObj collObj.transform = colTransform ) else ( messagebox "Bad file name." title:"Information" ) ) else ( messagebox "Nothing to export! You haven't selected any object!" title:"Information" ) ) rollout exportCollisionRollout "Collision Exporter" width:175 height:83 ( button 'btn1' "Export Static Collision" pos:[27,21] width:120 height:40 align:#left on btn1 pressed do ( exportCollision() ) ) createDialog exportCollisionRollout

+ Показать

В итоге получится такой хмл файл:

<?xml version="1.0"?> <CollisionShape> <Name>RoundaBound_Block_102</Name> <Bounds> <Min x="-129.558" y="-76.0382" z="-2.82287" /> <Max x="129.558" y="76.0382" z="2.82287" /> </Bounds> <VertexData count="14036" > <Vertex x="-46.0013" y="18.6505" z="0.349249" /> <Vertex x="-32.7033" y="19.288" z="0.199249" /> <Vertex x="-23.1109" y="13.8019" z="0.199249" /> <Vertex x="-17.1507" y="2.18068" z="0.349249" /> <Vertex x="-13.5732" y="-8.3756" z="0.349249" /> <Vertex x="-14.7879" y="-18.5163" z="0.349249" /> … </VertexData> <FaceData count="22294" materialCount="3" > <Face id1="0" id2="2378" id3="1317" matID="0" /> <Face id1="1317" id2="2381" id3="0" matID="0" /> <Face id1="189" id2="2379" id3="1317" matID="0" /> <Face id1="1317" id2="2378" id3="189" matID="0" /> <Face id1="106" id2="2380" id3="1317" matID="0" /> <Face id1="1317" id2="2379" id3="106" matID="0" /> <Face id1="187" id2="2381" id3="1317" matID="0" /> <Face id1="1317" id2="2380" id3="187" matID="0" /> <Face id1="373" id2="3077" id3="1318" matID="0" /> <Face id1="1318" id2="2383" id3="373" matID="0" /> … </FaceData> <MaterialData count="3" > <Material matID="0" matTypeID="1" generateProps="false" /> <Material matID="1" matTypeID="3" generateProps="false" /> <Material matID="2" matTypeID="2" generateProps="false" /> </MaterialData> </CollisionShape>

В дальнейшем можно брать данный файл, возможно, преобразовать в некий бинарный файл, и использовать в физическом движке.

Примерный код, как конструировать много материальную геометрию коллизии для движка Bulley Physics:

void buildMultiMatShape ( ) { indexVertexArray = new btTriangleIndexVertexMaterialArray ( indiciesCount, indicies, 3 * sizeof ( int ) , vertexCount, vertices, 3 * sizeof ( float ) , materialsCount, ( unsigned char *) colMaterials, sizeof ( CollisionMaterial ), matIndicies, sizeof ( int ) ) ; collisionShape = new btMultimaterialTriangleMeshShape ( ( btTriangleIndexVertexMaterialArray* )indexVertexArray, true ) ; }

Здесь есть еще место для дальнейшего развития, создание специального плагина для чтения и записи специфических форматов используя с++ и пополняя макс-скрипт функционалом, а также экспорт 3д графических моделей для рендера, в скором времени я пополню нашу библиотеку данными темами.

rss