MAX SCRIPT. Пишем экспортер

Данным уроком я хочу продолжить тему геймдева, начатую мной ранее. В принципе урок будет интересен не только людям интересующихся этим направлением 3Д, а всем кто не знаком с MAX SCRIPT и хочет научится писать свои скрипты, которые могут отлично сэкономить время при выполнении рутинных операций и не только.
В прошлом уроке я рассказывал вам о использовании графического движка OGRE и утилиты/плагина под 3DS MAX для экспорта сцен - oFusion. Но у данного подхода есть небольшой, но довольно неприятный минус, это платность этого самого oFusion при использовании его в коммерческих целях. Данный факт не помеха пока вы изучаете и экспериментируете с OGRE, но когда вы захотите наконец написать свой супер мега шутер, или простенькую казуальную игрушку, возникнет вопрос о оплате n-ой суммы за использование oFusion, или же написать свой экспортер. Второй вариант я вижу на много предпочтительней, как в плане экономии денег, так и в плане полного контроля и интегрирования нужных вам функций в экспортер, таких как физика, звук и т.д.
Наш экспортер, как я упоминал ранее, мы будем писать на MAX SCRIPT-е. Это даёт нам много преимуществ: совсем не обязательно знание программирования, простота написания и редактирования, универсальность относительно версий 3DS MAX.
Давайте определимся с "планом работ", а точнее что должен уметь наш экспортер, какие мы будем реализовывать опции и т.д. Самое главное, это экспорт самих моделей в огровский .mesh формат и запись их положения в сцене. Экспорт моделей стандартным огровским экспортером разбит на 2 части, это експорт модели из макса в .xml файл, и последующей конвертации его в .mesh отдельной программкой, что является очень не удобным и забирает много времени. Тем более, что экспортировать можно только по одной модели за раз, что является совсем не допустимым при экспорте большой сцены с множеством объектов. Сама часть создания .xml и конвертации нареканий не вызывает, по этому мы не будем их переделывать, а создадим оболочку, которая по очереди будет экспортировать и конвертировать все имеющиеся в сцене модели. Данные о позиции, ориентации и других параметрах каждой модели мы сохраним в файл со структурой xml, что даст возможность без проблем загрузить всё это в вашу игру.
Начинать мы будем с простого, постепенно добавляя нужные нам возможности. Итак, запускаем 3DS MAX, открываем закладку "Utilities" и нажимаем кнопочку "MAXScript", после чего вы увидите появившийся роллаут MAXScript:



Значения кнопок "New Script", "Open Script", "Run Script" я думаю, в пояснении не нуждаются, их названия говорят сами за себя, а вот кнопочка "Open Listener" открывает окошко содержащее 2 поля. Верхнее поле представляет собой что то типа лога, в которое макс выводит информацию после каких либо действий пользователя, а в нижнем можно вводить команды макс скрипта для одноразового немедленного выполнения. При написании скриптов данное окно желательно держать открытым, так как там будут отображаться ваши ошибки и вообще много полезной информации. Нажимаем кнопочку "New Script", перед нами появится чистый, готовый к работе, текстовый редактор. Предлагаю сразу же сохранить его. Я сохранил его под именем LesExpScene.ms. Сейчас нам надо расставить визуальные элементы управления, кнопки, выпадающие списки ит.д. Для чего выберите Edit->Edit Rollaut из меню редактора скриптов. Это откроет окно Visual MAXScript, где мы быстро и наглядно расставим всё, что нам нужно. Серый квадрат по центру, это и есть наш будущий роллаут. Большинство элементов находящихся в низу данного окна мы разберём по ходу работы. Для начала нам необходимо текстовое поле, чтоб мы могли назвать файл нашей сцены. Для этого нажмите кнопочку "Edit Box" на нижней панели, и нарисуйте прямоугольник, он и будет служить нам полем для ввода текста. Также для более удобного размещения элементов можно использовать кнопку "Grid/Snap" находящуюся в верхней части.


Теперь с правой стороны вы можете видеть параметры нарисованного нами "Edit Box". Сначала дайте имя данному элементу в поле "name", но помните, что мы будем использовать его в тексте скрипта, поэтому давайте узнаваемые имена, чтоб потом долго не вспоминать какой же элемент вы назвали "xyz" или "abc". Я назвал его "scName". В поле "caption" впишите "Scene Name", это надпись которая будет отображаться возле нашего поля. В "width" и "height" установите 200 и 17 соответственно. Сейчас давайте установим два обьекта "Color Picker" которые будут отвечать за выбор Ambient и Background цветов для нашей сцены. Нажмите на нижней панели кнопку "ColorPicker" и нарисуйте 2 прямоугольника. Для первого установите такие параметры:
name - amColor
caption - Ambient Color
width - 120
height - 32

Для второго:
name - bcColor
caption - Back Color
width - 120
height - 32

Нам надо добавить элементы отвечающие за настройку теней, и для более красивого вида нашего экспортера мы объединим их в одну группу, для чего выберите "Group Box" и нарисуйте квадрат побольше. В "caption" впишите "Shadow Settings", поле "name" в данном случае значения не имеет, так как обращаться к нему из скрипта мы не собираемся. Внутри разместите 2 объекта "Drop Down List" один "ColorPicker" и один "Label". "Drop Down List" - это выпадающий список, а "Label" мы используем для более компактного размещения элементов внутри нашей группы, так как "ColorPicker" со стандартным "caption" будет занимать слишком много места. Чтоб вам стало понятней, посмотрите на скриншот:



И установите следующие параметры для данных объектов:
Первый "Drop Down List":

name - shadowTec
caption - Shadow Technique:
width - 136
height - 40
items - "NONE", "STENCIL_MODULATIVE", "STENCIL_ADDITIVE", "TEXTURE_MODULATIVE", "TEXTURE_ADDITIVE"

В поле "items"мы вписали те пункты, которые будут показаны при развороте списка. Вписывать их надо обязательно в кавычках.

Второй "Drop Down List":

name - shadowSyze
caption - Shadow Texture Syze:
width - 136
height - 40
items - "128", "256", "512", "1024", "2048"

Для элемента "Label" в поле "caption" напишите "Shadow Color". А элементу "ColorPicker" установите "name" - "shadowColor" и поле "caption" сделайте пустым. Разместите всё это так как показано на скриншоте выше. Сейчас самое время сохранить нашу работу. Для этого выбирете File->Save или просто закройте окно Visual MAXScript и на вопрос сохранять ли изменения ответьте Да. Всё нами проделанное выше окажется строками кода макс скрипта в нашем файле. Сохраните его.
Нам осталось разместить всего несколько элементов. Как это делается я думаю больше объяснять не надо, поэтому я приведу только скриншот с их размещением и параметры которые следует установить


Первый Edit Box, для отображения и/или ввода с клавиатуры пути для сохранения продуктов жизнедеятельности нашего скрипта.

name - saveText
caption - save to:
width - 255
height - 17

Кнопка "Browse" для выбора того самого пути.

name - butSave
caption - Browse
width - 80
heiht - 24

Check Box для выбора конвертировать ли модели в .mesh формат после экспорта, или оставить их в XML.

name - convert
caption - Convert XML
width - 96
height - 17
enabled - true
checked - true

Опция "enabled" указывает будет ли активен этот чекбокс или нет. А "checked" выбран ли он.

Второй Edit Box, для отображения пути к программе конвертирования XML в .mesh (Она идёт в комплекте с файлами Огра)

name - XMLText
caption - XML Converter
width - 255
height - 17

Ещё одна кнопка "Browse", для выбора пути к XML конвертеру.

name - butXML
caption - Browse
width - 80
height - 24

Второй чекбокс предназначен для выбора экспортировать ли модели в принципе, или только файл с их размещением и настройками сцены. Экономит время если вы изменили только цвет фона или позицию модели, например.

name - onlyxml
caption - Export XML Scene Only
width - 137
height - 17

И последняя, самая главная кнопка "Export". Её назначение, я думаю, в пояснении не нуждается.

name - expBut
caption - Export
width - 100
height - 30

Всё, с размещением элементов мы закончили. Давайте дадим имя нашему роллауту. Выделите тот серый квадрат в нутри которого мы размещали все элементы и установить ему: name - ExportRollout и caption - FreeS Export Dialog. Сохраните всё это, и окно визуального редактирования можно закрыть.

Наверное вам не терпится увидеть в действии то, что вы так долго делали. Но пока это всего лишь набор строк. Чтоб "дать им жизнь" мы создадим окно, в котором разместится данный ролаут, и установим кнопку для его открытия. Сперва поместите содержимое архива в папку "3DSMAX\UI\Icons".

Icons

Это картинка которая будет отображаться на кнопке. Если вы закрыли ваш скрипт, откройте его. Сейчас мы имеем:

rollout ExportRollout "FreeS Export Dialog" width:296 height:528
(
--
-- Тут описание всех созданных нами элементов интерфейса.
-- Комментарии в МаксСкрипте начинаются с двойного дефиса.
--

)

В самом начале, перед данным текстом, мы должны вписать следующее:

macroScript ExportDialog
category:"FreeS Tools"
internalCategory:"Export Dialog"

-- Текст кнопки.
buttonText:"FreeS Ogre Export Dialog"

-- Надпись появляющаяся при наведении мыши на кнопку.
tooltip:"FreeS Ogre Export Dialog"

-- Иконка отображаемая на кнопке.
Icon:#("FreeS",3)
(

-- Имя и размеры создаваемого окна.
FreeSOgreExportFloater = newRolloutFloater "FreeS Export Dialog" 300 600;

И в самом конце:

-- Добавляем наш ролаут в окно.
addRollout ExportRollout FreeSOgreExportFloater ;

)

Сохраните изменения в тексте скрипта, и мы продолжим. Чтобы макс начал использовать данный скрипт нажмите File->Evaluate All. Если вы допустили какую либо ошибку, данные о ней появятся в окне MAXScript Listener. Чтоб не тратить много времени на поиск ошибки в случае её возникновения, сравните свой скрипт с моим:

LesExpScene_1

Нам осталось показать кнопку запуска скрипта, и делается это очень просто. Выберите пункт меню Customize->Customize User Interface... Сейчас мы создадим Toolbar который и будет содержать нашу кнопку, и любые другие, по вашему желанию. Нажмите кнопочку "New" и в появившемся окошке дайте имя создаваемому тулбару, и нажмите "Ок". Появится маленькое окошко, на которое мы можем переместить нужные нам кнопки. В окне "Customize User Interface" в "Category:" найдите "FreeS Tools" (Если вы следовали моим советам, и не назвали категорию в скрипте по другому) и увидите в низу список возможных кнопок в этой категории. В данном случае у вас будет только один пункт, который вы и перетащите мышью в только что созданный тулбар.


Теперь вы можете разместить этот тулбар в любом месте интерфейса 3D Max, а при нажатии на кнопку мы увидим созданный нами роллаут. Правда пока он никак не реагирует на нажатия кнопок, и не может сохранять свои настройки, но сейчас мы это исправим.

Когда передо мной стал вопрос как сохранять настройки отдельных объектов в сцене, то я решил записывать их в User Defined Properties, что очень удобно в плане использования и редактирования. Найти это можно щёлкнув правой кнопкой мыши на каком либо объекте, и в появившемся меню выбрать "Properties..." где вы и найдёте закладку "User Defined". А вот с сохранением настроек сцены было не всё так гладко, но в конце концов я решил что их тоже можно сохранить в User Properties специально созданного для этого объекта, который будет игнорироваться экспортером в качестве меша, и будет служить только контейнером нужных параметров. Я дал ему имя "SceneBox". Создавать его и записывать все параметры мы конечно же будем при помощи MAXScript.
Все наши дальнейшие действия будут базироваться на событиях, которые происходят при выполнении каких либо действий. Например, при нажатии кнопки открывающей наш тулбар происходит событие "open" в этот момент нам надо проверить есть ли в сцене "SceneBox", и если да, то установить всем элементам роллаута сохранённые в нём параметры, а если его нет, то есть роллаут открывается впервые для данной сцены, то надо его создать.
Для проверки присутствия "SceneBox" мы используем конструкцию "try()catch()". В скобках после "try" мы попробуем выбрать этот бокс, и если его не окажется, начнёт выполнятся код находящийся в "catch()", если же операция выбора пройдёт успешно, то "catch()" пропускается, скрипт продолжит свою работу дальше.
Выбирать объект в MAXScript можно многими способами, но в данном случае самым удобным для нас будет выбор по имени. Чтоб MAXScript понял что мы обращаемся именно к имени, перед ним надо ставить знак "$"
Записывать данные в User Properties мы будем таким образом:
setUserProp /переменная или имя объекта/ /имя параметра/ /значение параметра/

А читать так:
значение параметра = getUserProp /переменная или имя объекта/ /имя параметра/

Каждый из созданных нами ранее элементов интерфейса содержит множество параметров, и обращение к ним происходит через точку, то есть:

Цвет объекта "shadowColor" в формате RGB можно получить так:
shadowColor.color

Чтоб получить только синий параметр цвета:
shadowColor.color.b

При сохранении и загрузке цветов мы будем делать это отдельно для каждого элемента RGB, так как корректно загрузить цвет записанный одной строкой мне не удалось.
Итак, после строк описания интерфейса, перед скобкой закрывающей роллаут мы пишем:

 <span style="color: rgb(153, 0, 0);">-- Когда наш роллаут открылся</span>
     on ExportRollout open do 
     (
                   <span style="color: rgb(153, 0, 0);">-- пробуем</span>
                     try ( 
                   <span style="color: rgb(153, 0, 0);">-- выбрать объект с именем "SceneBox"</span>
                     scb = select $SceneBox
                         )
                   <span style="color: rgb(153, 0, 0);">-- если не получилось</span>
                         catch (
                  <span style="color: rgb(153, 0, 0);">-- создаём бокс</span>
                             ScBox = Box()
                   <span style="color: rgb(153, 0, 0);">-- даём ему имя</span>
                             ScBox.name = "SceneBox"
                  <span style="color: rgb(153, 0, 0);">-- и поместим его подальше, чтоб не мешал</span>
                             ScBox.pos.z = -5000
                   <span style="color: rgb(153, 0, 0);">-- после чего записываем все необходимые параметры.</span>
                              setUserProp ScBox "SHADOWT" shadowTec.selection
                             setUserProp ScBox "SHADOWS" shadowSyze.selection
                             setUserProp ScBox "SHADOW_R" shadowColor.color.r
                             setUserProp ScBox "SHADOW_G" shadowColor.color.g
                             setUserProp ScBox "SHADOW_B" shadowColor.color.b
                             setUserProp ScBox "AMBIENT_R" amColor.color.r
                             setUserProp ScBox "AMBIENT_G" amColor.color.g
                             setUserProp ScBox "AMBIENT_B" amColor.color.b
                                                 setUserProp ScBox "BCKGRND_R" bcColor.color.r    
                                                 setUserProp ScBox "BCKGRND_G" bcColor.color.g            
                             setUserProp ScBox "BCKGRND_B" bcColor.color.b
                             setUserProp ScBox "SCNAME" "defscene"
                             setUserProp ScBox "SAVEP" "C:\\"
                             setUserProp ScBox "XMLPATH" "C:\\"
                             setUserProp ScBox "CONVERT" true
                             setUserProp ScBox "ONLYXML" false
                                )
           <span style="color: rgb(153, 0, 0);">-- а тут мы читаем все параметры, и устанавливаем их соответствующим элементам.</span>
                           SC = getUserProp $SceneBox "SCNAME"
                  scName.text = (SC as String)
                   amColor.color.r = getUserProp $SceneBox "AMBIENT_R"
                  amColor.color.g = getUserProp $SceneBox "AMBIENT_G"
                  amColor.color.b = getUserProp $SceneBox "AMBIENT_B"
                  bcColor.color.r = getUserProp $SceneBox "BCKGRND_R"
                  bcColor.color.g = getUserProp $SceneBox "BCKGRND_G"
                  bcColor.color.b = getUserProp $SceneBox "BCKGRND_B"
                  shadowTec.selection = getUserProp $SceneBox "SHADOWT"
                  shadowSyze.selection = getUserProp $SceneBox "SHADOWS"
                  shadowColor.color.r = getUserProp $SceneBox "SHADOW_R"
                  shadowColor.color.g = getUserProp $SceneBox "SHADOW_G"
                  shadowColor.color.b = getUserProp $SceneBox "SHADOW_B"
                  saveText.text = getUserProp $SceneBox "SAVEP"
                  XMLText.text = getUserProp $SceneBox "XMLPATH"
                  convert.checked = getUserProp $SceneBox "CONVERT"
                  XMLText.enabled = convert.checked
                  butXML.enabled = convert.checked
                  onlyxml.checked = getUserProp $SceneBox "ONLYXML"
     )
 Теперь наш скрипт может читать сохранённые данные, но нам надо чтоб наш роллаут реагировал на нажатия кнопок, 
выбор цвета и т.д. , и сохранял их. Реализовывать мы это будем по описанному выше принципу.
   <span style="color: rgb(153, 0, 0);">-- Когда в "Scene Name" введён текст, то</span>
     on scName entered text do
 (
  <span style="color: rgb(153, 0, 0);">-- записываем изменённый параметр.</span>
     setUserProp $SceneBox "SCNAME" scName.text
     )
  <span style="color: rgb(153, 0, 0);">-- когда изменился амбиент цвет</span>
     on amColor changed col do
 (
  <span style="color: rgb(153, 0, 0);">-- тоже записываем изменения</span>
     setUserProp $SceneBox "AMBIENT_R" amColor.color.r
     setUserProp $SceneBox "AMBIENT_G" amColor.color.g
     setUserProp $SceneBox "AMBIENT_B" amColor.color.b
     )
  <span style="color: rgb(153, 0, 0);">-- аналогично</span>
     on bcColor changed col do
 (
     setUserProp $SceneBox "BCKGRND_R" bcColor.color.r    
     setUserProp $SceneBox "BCKGRND_G" bcColor.color.g            
    setUserProp $SceneBox "BCKGRND_B" bcColor.color.b
     )
  <span style="color: rgb(153, 0, 0);">-- если произошел выбор техники теней</span>
     on shadowTec selected sel do
 (
     setUserProp $SceneBox "SHADOWT" shadowTec.selection
     )
  <span style="color: rgb(153, 0, 0);">-- если мы изменили размер теней</span>
     on shadowSyze selected sel do
 (
     setUserProp $SceneBox "SHADOWS" shadowSyze.selection
     )
  <span style="color: rgb(153, 0, 0);">-- а если поменяли цвет теней</span>
     on shadowColor changed col do
 (
     setUserProp $SceneBox "SHADOW_R" shadowColor.color.r
     setUserProp $SceneBox "SHADOW_G" shadowColor.color.g
     setUserProp $SceneBox "SHADOW_B" shadowColor.color.b
     )
  <span style="color: rgb(153, 0, 0);">-- если мы вписали путь сохранения сцены вручную то</span>
     on saveText entered text do
 (
     setUserProp $SceneBox "SAVEP" saveText.text
     )
  <span style="color: rgb(153, 0, 0);">-- а если нажали кнопку "Browse"</span>
     on butSave pressed  do
 (
  <span style="color: rgb(153, 0, 0);">-- получаем выбраный путь сохранения</span>
     pa=getSavePath()
  <span style="color: rgb(153, 0, 0);">-- проверяем его на корректность</span>
     if (pa!=undefined) then (
  <span style="color: rgb(153, 0, 0);">-- дописываем два обратных слеша в конец пути</span>
     pa=pa+"\\"
  <span style="color: rgb(153, 0, 0);">-- выводим полученный путь в поле "save to:"</span>
     saveText.text=pa
  <span style="color: rgb(153, 0, 0);">-- и записываем изменения</span>
     setUserProp $SceneBox "SAVEP" pa
                              )
     )
  <span style="color: rgb(153, 0, 0);">-- если мы поставили или сняли галочку</span>
     on convert changed state do
 (
  <span style="color: rgb(153, 0, 0);">-- записываем изменения</span>
     setUserProp $SceneBox "CONVERT" convert.checked
  <span style="color: rgb(153, 0, 0);">-- и устанавливаем зависимость активности "XMLText" и "butXML" от нашего выбора</span>
     XMLText.enabled = convert.checked
     butXML.enabled = convert.checked
     )
  <span style="color: rgb(153, 0, 0);">--если путь к XML конвертеру введён вручную</span>
     on XMLText entered text do
 (
  <span style="color: rgb(153, 0, 0);">-- записываем изменения</span>
     setUserProp $SceneBox "XMLPATH" XMLText.text
     )
  <span style="color: rgb(153, 0, 0);">-- если нажата кнопка "Browse" действуем аналогично выбора пути сохранения</span>
     on butXML pressed  do
 (
     pa=getSavePath()
     if (pa!=undefined) then (
     pa=pa+"\\"
     XMLText.text=pa
     setUserProp $SceneBox "XMLPATH" pa
     )
     )
  <span style="color: rgb(153, 0, 0);">-- если изменился статус "Export XML Scene Only"</span>
     on onlyxml changed state do
 (
  <span style="color: rgb(153, 0, 0);">-- тоже записываем изменения</span>
     setUserProp $SceneBox "ONLYXML" onlyxml.checked
     )
 <span style="color: rgb(153, 0, 0);">-- А тут мы будем вызывать саму функцию экспорта, если была нажата кнопка "Export"</span>
     on expBut pressed  do
 (
          )

Сохраните все изменения, сделайте File->Evaluate All и попробуйте поизменять параметры в созданном нами окне. Всё должно сохраняться и загружаться. Если у вас что то не работает, или вы получили сообщение о ошибке, посмотрите на мой файл скрипта, возможно вы упустили какую либо деталь.

LesExpScene_2

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

Если у вас возникнут какие либо вопросы, задавайте их на мой e-mail:
frees_les@mail.ru
А также читайте справку по макс скрипту, там много полезной информации, и всё довольно хорошо и подробно описано.

Спасибо всем кто дочитал до конца :) , удачи вам в ваших начинаниях.

727 0 850 31
15
2007-02-14
сейчас эта тема для меня становится актуальной Посему выставил 5-ки. Продолжай в том же духе
2007-02-14
Большое спасибо. В следующем уроке мы разберём сам процесс экспорта, науимся устанавливать параметры объектам, и создадим свой материал.
2007-02-14
урок ещё не прочитал, только вступление, но тема одна из наиболее привлекательных. считаю, что автор один из самых полезных пользователей рендера. огромное спасибо!
2007-02-14
В свое время писал экспортер меша модели в XML - столкнулся с неприятным моментом макс выделял очень бльшое колво памяти (разростался до 800М) а потом ее освобождал но не всю где то 100М не вычищал. Твой скрипт таким не страдает?
2007-02-14
Frees"у респект - голосую по максимому !!!!!!!!!!!!!!!!!!!!)))))))))))))))) Народ - что-то совсем не шевелимся 4 комента на урок!
2007-02-15
Да, отзывов маловато ( Я ждал больше. 2 Pavel Zabelin Не замечал такого глюка, но посмотрю в эту сторону повнимательней.
2007-02-15
А что тут комментирровать? Конкретная задача, ясное решение. Отличные листинги, подробно прокомментированные. 5/5
2007-02-16
поторопился и 5/5 поставил это тока на OGRE? если нет - то всё норм, если да - фтопку.
2007-02-16
То что я здесь описал может быть применено к любому движку. Все пункты раздельны между собой. Экспорт сцены в XML формат производится совсем независимо, и каким движком его потом грузить, зависит только от вас. Экспорт же самих моделей будет в .mesh формат Огра. Но, в силу раздельности компонентов экспортера сам процесс конвертации модели может быть заменён на то что вам нужно. В принципе цель данного урока не в написании экспортера для огра, а практическом применении, и изучении MAX Script на основе написания экспортера.
2007-03-01
Урок довольно хорош, так что 5/5. А автор молодец!
2007-03-04
Урок мне понравился. Правда хотель бы увидеть работу с MAX SDK. И еще, если не сложно напиши урок по взаимодействию плагинов. Например задаешь какие-то параметры для сцены, потом экспортируешь их или визуализируешь сцену использовав эти параметры. И еще очень хочется увидеть урок по созданию в MAX'e своего собственного объекта, со своими параметрами, функциями и т.д. Вобщем я как следует намекнул, осталось реализовать:) Вобщем спасибо автору.
2008-02-01
OK-5! Пока я недашол до этого, но когда пригодиться, есть куда посмотреть.
2009-01-15
Отличный урок, спасибо, гдеже продолжение? :)
2009-10-26
Тема очень полезная, жду продолжения.
2015-06-13
А не знает кто, можно ли сделать скрипт на авто импорт и автоэкспорт? Чтобы smd был входящим форматом, а fbx выходящим
RENDER.RU