ПрограммированиеПодсказкиФизика

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

Автор:

В предыдущей статье 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> — тело функции заключённое в скобки ().

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

on btn1 pressed do
(
             
)

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

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д графических моделей для рендера, в скором времени я пополню нашу библиотеку данными темами.

#3D Studio MAX, #bullet physics, #collision, #MAX Script, #физика, #скрипты

6 декабря 2017 (Обновление: 31 дек 2018)