Уроки: 3ds Max Создание плагина 3ds Max

Введение

В уроке рассматривается технологическая цепочка создания плагина 3ds Max в среде Microsoft Visual Studio с употреблением 3ds Max SDK и языка программирования C++. Также реализуется MAXScript-версия плагина. Предоставляемый материал – это лишь начальная точка на пути освоения техники разработки 3ds Max SDK-плагинов.
Урок подготовлен по заявкам пользователей Render.ru.

Плагин – это программный модуль, подключаемый к основной программе и либо реализующий часть функционала основной программы, либо расширяющий ее возможности.
Так, в 3ds Max к первой группе плагинов относятся стандартные плагины, хранящиеся в папке stdplugs и загружаемые при запуске приложения. Их список отображается в приведенном на рис. 1 диалоге Plug-in Manager (меню Customize – Plug-in Manager).

Рис. 1. Plug-in Manager

Эти плагины, их около 350, реализуют значительную часть функционала 3ds Max. Например, плагин prim.dlo обеспечивает создание стандартных примитивов, сплайнов и стандартных источников света.
Прочие, нестандартные плагины предпочтительнее размещать в других папках и загружать по мере необходимости.
3ds Max SDK (Solution Development Kit, инструментарий разработчика) имеется в профессиональной версии 3ds Max и содержит заголовочные и библиотечные файлы, обеспечивающие доступ к классам и другим данным 3ds Max. Кроме того, в поставке имеется помощник и большое число примеров.
Плагин 3ds Max может быть написан на языке MAXScript и на языке C++ с употреблением 3ds Max SDK.
В первом случае плагин создается в среде 3ds Max, а во втором – в среде Microsoft Visual Studio.
Во многих случаях MAXScript может обеспечить такой же функционал, как и SDK. Однако скорость SDK-плагинов выше.
SDK-плагин – это откомпилированный и собранный dll-файл C++ (DLL, Dynamic Linked Library, динамически подключаемая библиотека). Рекомендованное расширение файла зависит от назначения плагина. В стандартной поставке 3ds Max плагины, оперирующие материалами и их картами, имеют расширение DLT, а плагинам, создающим объекты, дается расширение DLO, плагины-модификаторы выделяются расширением DLM и так далее.
Поставка 3ds Max SDK включает помощник создания плагинов Plug-in Wizard. Он поддерживает (или планирует поддерживать) создание около 40 следующих видов плагинов (в скобках указывается стандартное расширение):

  • Anti-Aliasing Filters – фильтры, сглаживающие изображение (DLK);
  • Atmospheric – атмосферные эффекты (DLV);
  • Cameras – камеры (DLO);
  • Color Selector – выбор цвета (DLU);
  • Construction Grid Objects – создание сеточных объектов (DLO);
  • Controllers – контроллеры (управляющие элементы, DLC);
  • File Export, File Import и File List – экспорт, импорт и список файлов (DLE, DLI);
  • Global Utility Plug-Ins – глобальные утилиты (GUP);
  • Helper Objects – помощники (DLO);
  • IK Solvers – решатели инверсной кинематики (DLC);
  • Image Filter / Compositor – фильтры (редакторы) образов (FLT);
  • Image Loader / Saver – загрузка и сохранение образов (BMI);
  • Image Viewer – просмотр образов (DLF);
  • Lights – источники света (DLO);
  • Manipulators – манипуляторы (DLO);
  • Materials – материалы (DLT);
  • Modifiers – модификаторы (DLM);
  • NURBS Objects – неоднородные рациональные сплайны Безье (DLO);
  • Particle Systems / Effects – системы частиц и эффекты на основе частиц (DLO);
  • Patch Objects – патчи (кусочные объекты, DLO);
  • Procedural Objects – процедурные объекты, например стандартные примитивы (DLO);
  • Renderer – воспроизведение изображения (DLR);
  • Rendering Effects – эффекты при воспроизведении изображения (DLV);
  • Samplers – образцы (DLH);
  • Shaders – шейдеры (определяют цветовые характеристики объекта, DLB);
  • Shadow Generator – генераторы теней (DLO);
  • Skin Deformer Gizmo – контейнеры модификаторов кожи (DLM);
  • Sound Plug-ins – звуки (DLO);
  • Space Warps – пространственные деформации (DLM);
  • Textures 2D, Textures 3D – двумерные и трехмерные текстуры (DLT);
  • Track View Utility – утилиты обозревателя дорожек (DLU);
  • Utility – утилиты (DLU).

После уяснения задачи написание SDK-плагина, как правило, предполагает создание надлежащего интерфейса пользователя и реализацию намеченных процедур. Например, плагин gSphere.dlo обеспечивает приведенный на рис. 2 интерфейс и программно поддерживает соответствующую реакцию приложения на предусмотренные интерфейсом действия.

Рис. 2. Пользовательский интерфейс плагина gSphere.dlo

При разработке SDK-плагинов каждая версия 3ds Max предполагает использование соответствующей версии Microsoft Visual Studio, что отражено в следующей таблице:

 Версия 3ds Max  Операционная система
(32 и 64 бит) 
 Совместимые версии
3ds Max SDK
 Microsoft Visual C++
версия компилятора
2011 Windows 7
Windows Vista
Windows XP Pro SP2
2011, 2010 Visual C++ 9.0 (Visual Studio 2008) Service Pack 1
с установленным от 28 июля 2009 security patch
2010 Windows Vista
Windows XP Pro SP2
2010 Visual C++ 9.0 (Visual Studio 2008) Service Pack 1
2009 Windows Vista
Windows XP Pro SP2
2009 Visual C++ 8.0 (Visual Studio 2005) Service Pack 1

 

Постановка задачи

Порядок разработки плагина рассмотрим на следующем примере: создать плагин, формирующий примитив куб (Cube).
Такой простой объект отвечает цели урока, заключающейся в демонстрации технологической цепочки разработки SDK-плагинов.
Примитив имеет один параметр Size, для управления которым используется редактируемое поле со счетчиком (рис. 3).

Рис. 3. Интерфейс плагина Cube

Как и другие примитивы, куб вводится в сцену мышкой с прижатой левой кнопкой.

MAXScript реализация плагина

Одну и ту же задачу можно решить средствами 3ds Max SDK и MAXScript. Для иллюстрации этого положения реализуем прежде плагин создания куба на MAXScript, дав плагину имя CubeMS.

plugin simpleObject CubeMS name:"CubeMS" classID:#(145340, 543210) category:"Scripted Primitives" (
 parameters main rollout:params (
  size type:#worldUnits ui:size default:0
 )
 rollout params "Cube Size" (
  spinner size "Size: " type:#worldunits range:[0, 100, 0] scale:1
 )
 on buildMesh do (
  h = 0.5 * size
  arrVrts = #([-h, -h, -h], [h, -h, -h], [-h, h, -h], [h, h, -h], \
   [-h, -h, h], [h, -h, h], [-h, h, h], [h, h, h])
  arrFcs = #([1, 3, 4], [4, 2, 1], [5, 6, 8], [8, 7, 5], \
   [1, 2, 6], [6, 5, 1], [2, 4, 8], [8, 6, 2], \
   [4, 3, 7], [7, 8, 4], [3, 1, 5], [5, 7, 3])
  setMesh mesh verts:arrVrts faces:arrFcs
  setFaceSmoothGroup mesh 1 2; setFaceNormal mesh 1 [0, 0, -1]
  setFaceSmoothGroup mesh 2 2; setFaceNormal mesh 2 [0, 0, -1]
  setFaceSmoothGroup mesh 3 4; setFaceNormal mesh 3 [0, 0, 1]
  setFaceSmoothGroup mesh 4 4; setFaceNormal mesh 4 [0, 0, 1]
  setFaceSmoothGroup mesh 5 8; setFaceNormal mesh 5 [0, -1, 0]
  setFaceSmoothGroup mesh 6 8; setFaceNormal mesh 6 [0, -1, 0]
  setFaceSmoothGroup mesh 7 16; setFaceNormal mesh 7 [1, 0, 0]
  setFaceSmoothGroup mesh 8 16; setFaceNormal mesh 8 [1, 0, 0]
  setFaceSmoothGroup mesh 9 32; setFaceNormal mesh 9 [0, 1, 0]
  setFaceSmoothGroup mesh 10 32; setFaceNormal mesh 10 [0, 1, 0]
  setFaceSmoothGroup mesh 11 64; setFaceNormal mesh 11 [-1, 0, 0]
  setFaceSmoothGroup mesh 12 64; setFaceNormal mesh 12 [-1, 0, 0]
  for k = 1 to 12 do setEdgeVis mesh k 3 false
 )
 tool create (
  on mousePoint click do
   case click of (
    1: nodeTM.Translation = gridPoint
    3: #stop
   )
  on mouseMove click do
   case click of (
    2: size = amax gridDist.X gridDist.Y
    3: size = gridDist.Z
   )
 )
)

Программа содержит секцию (Rollout) params, обеспечивающую задание размера куба (счетчик Size). Значение счетчика связано с одноименным параметром, заданным в блоке Parameters.
Инструмент создания куба (мышка) задается блоком Tool Create. После выбора позиции (событие mousePoint с параметром click = 1) определяется часть Translation (перемещение) матрицы аффинных преобразований куба (nodeTM.Translation = gridPoint). Далее при нажатой левой кнопке мыши фиксируется ее перемещение в видовом порте; величина перемещения определяет размер куба.
После освобождения мыши сцена может принять очередной куб.
Родительским классом куба является класс SimpleObject. Класс куба имеет имя CubeMS.
Для получения идентификатора класса classID следует употребить имеющуюся в папке ..\Autodesk\3ds Max 20хх SDK\maxsdk\help программу gencid.exe (рис. 4).

Рис. 4. Генератор идентификатора класса плагина

Параметризованный куб создается обработчиком buildMesh в результате применения метода SetMesh, получающего массив arrVrts с координатами вершин примитива и массив его граней arrFcs. Размер куба определяется значением параметра Size. Сглаживающие группы и нормали куба заданы по аналогии с примитивом Box.
После копирования и запуска кода в MAXScript Editor, плагин будет доступен на вкладке Create – Geometry – Scripted Primitives (рис. 5).

Рис. 5. Вызов плагина CubeMS

Категория (Scripted Primitives) и положение элемента в командном окне определяются параметрами выражения Plugin. Так, класс SimpleObject указывает на принадлежность примитива к геометрическим объектам, понятно и назначение свойства Category.
После загрузки плагина кубом можно оперировать средствами языка MAXScript, например:

delete $*
cb = cubeMS size:40
animate on at time 100 (
 cb.Size = 60
 rotate cb 360 [0, 0, 1]
)
playAnimation()

Заметим, что созданный объект не имеет текстурных координат, поэтому при употреблении материала следует позаботиться о создании таких координат, например при помощи модификатора UVWmap:

delete $*
cb = cubeMS size:50
addModifier cb (UVWmap maptype:4 ui:on)
chk = checker()
chk.Coordinates.U_Tiling = chk.Coordinates.V_Tiling = 2
std = standard diffuseMap:chk showInViewport:true diffuseMapEnable:true
cb.Material = std

Настройка помощника Plug-in Wizard

Необходимые для использования помощника файлы расположены в папке MAXSDK\Howto\3DSMaxPluginWizard.
Откроем в текстовом редакторе имеющийся в этой папке файл 3dsmaxPluginWizard.vsz и определим в нем следующее значение параметра:

Param="ABSOLUTE_PATH = C:\Program Files\Autodesk\3ds Max 2009 SDK\maxsdk\howto\3dsmaxPluginWizard"

То есть укажем полный путь к папке с файлами помощника. Сохраним изменения и закроем файл.
Оставаясь в этой папке, скопируем три следующие файла:

  • 3dsmaxPluginWizard.ico;
  • 3dsmaxPluginWizard.vsdir;
  • 3dsmaxPluginWizard.vsz

и вставим их в VC/vcprojects директорию установки Microsoft Visual Studio (это может быть C:\Program Files\Microsoft Visual Studio 5\VC\vcprojects).
Этого достаточно, чтобы 3ds Max Plug-in Wizard оказался доступным как шаблон в Microsoft Visual Studio.
Проверим это, запустив Visual Studio и выбрав File > New:Projects > Visual C++ > 3ds Max Plug-in Wizard (рис. 6).

Рис. 6. Создание проекта Visual C++ с помощью 3ds Max Plug-in Wizard

Порядок создания 3ds Max SDK-плагина

После разработки проекта плагина и оформления проекта, например, в виде технического задания запускается Microsoft Visual Studio, где и выполняются все последующие действия.
В проекте SDK-плагина полезно указать следующие характеристики:

 Характеристика плагина
 Пример значения 
Имя файла cube.dlo
Идентификатор класса утилиты Class_ID(0xd667c5aa, 0xb65e9ddb)
Описание IDS_LIBDESCRIPTION "Cube"
Категория IDS_CATEGORY "SDK simple object"
Имя класса IDS_CLASS_NAME "Cube"

Интерфейс пользователя, предоставляемый плагином, сформируем в соответствии с рис. 7, добавив возможность ручного ввода (Keyboard Entry) примитива Cube.

Рис. 7. Уточненный пользовательский интерфейс плагина

При вводе посредством нажатия на кнопку Create центр куба будет размещен в начале мировой системы координат.
Создадим теперь в Microsoft Visual Studio проект C++ Win32, компиляция и сборка которого (Compile and Link) обеспечат создание запрошенного плагина.
Проект создается как многониточная библиотека (Multy-threaded DLL). Используется гибридная (Hybrid) конфигурация, пригодная и для отладки (Debugging), и для построения готового решения (Solution).

Создание нового проекта

Запустим Visual Studio и используем для создания проекта помощник 3ds Max Plug-in Wizard (см. рис. 6). В поле Name введем имя Cube, а в поле Location укажем, например, имя папки C:\sdk. Снимем флажок Create directory for solution. Нажмем на ОК и в открывшемся диалоге выберем Procedural Objects (процедурные объекты, рис. 8).

Рис. 8. Выбор вида плагина

Нажмем на Next и введем в появившемся диалоге указанные на рис. 9 значения.

Рис. 9. Детализация описания плагина Cube. В качестве базового выбран класс SimpleObject2

Нажмем на Next и в появившемся диалоге проверим наличие указанных на рис. 10 значений.

Рис. 10. Некоторые детали проекта

В поле MAXSDK path указан путь

C:\Program Files\Autodesk\3ds Max 9 SDK\maxsdk

а в поле 3dsmax.exe path указан путь

C:\Program Files\Autodesk\3ds Max 2009

Ваши значения могут быть иными.
Нажмем на кнопку Finish.

Свойства (Properties) проекта Cube

Практически все свойства проекта Cube будут установлены помощником. Наша задача просмотреть эти свойства и внести незначительные коррективы.
На вкладке Solution Explorer расположится дерево решения (рис. 11).

Рис. 11. Дерево решения Cube

Первый заголовочный файл 3dsmaxsdk_preinclude.h удален, а файл cube.h приведен к следующему виду:

#include "Max.h"
#include "resource.h"
#include "istdplug.h"
#include "iparamb2.h"
#include "iparamm2.h"
#include "Simpobj.h"
extern TCHAR *GetString(int id);
extern HINSTANCE hInstance;

Удаление файла 3dsmaxsdk_preinclude.h потребует изъятия всех #pragma message из файла cube.cpp. Такие куски кода появятся в файле, если в качестве базового класса выбрать GeomObject.
Добавим теперь в проект конфигурацию Hybrid.
Выберем в дереве проекта вершину cube (см. рис. 11), нажмем на правую кнопку мыши и в появившемся меню выберем Properties.
В открывшемся окне нажмем на кнопку Configuration Manager и в списке Active Solution Configuration добавим новый вид конфигурации Hybrid; этот же вид выберем и в нижерасположенной таблице диалога (рис. 12).

Рис. 12. Добавление конфигурации Hybrid

Далее все настройки будут выполнены для этой конфигурации.
Ветвь Configuration Properties – General после внесенных изменений будет содержать следующие сведения (рис. 13):

Рис. 13. Ветвь Configuration Properties – General

Переместимся в ветвь C++ – General. В поле Additional Include Directories проверим путь к include-директории 3ds Max SDK:

"C:\Program Files\Autodesk\3ds Max 2009 SDK\maxsdk\include"

В вашем случае путь может быть иным. Если путь содержит пробелы, то его нужно заключить в кавычки.
Прочие поля диалога оставим без изменений. Нажмем на кнопку Применить.
В ветви C++ – Command Line поле Additional options (дополнительные опции) вставим следующие значения:

/GR /we4706 /we4390 /we4557 /we4546 /we4545 /we4295 /we4310 /we4130 /we4611 /we4213 /we4121 /w34701 /wd4244 /wd4018

Они взяты из файла AdditionalCompilerOptions.txt, имеющегося в поставке 3ds Max SDK. Применить.
Имя выходного файла (создаваемого плагина) найдем в ветви Linker – General – Output File. В рассматриваемом примере помощник указал имя C:\sdk\cube.dlo.
Проверим путь к SDK библиотекам (Additional Library Directories):

"C:\Program Files\Autodesk\3ds Max 2009 SDK\maxsdk\lib"

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

Файл ресурсов cube.rc

В этом файле создадим указанные на вышеприведенном рис. 7 диалоги Keyboard Entry и Parameters. Также отредактируем таблицу символов (String Table) с идентификаторами ресурса.
Перейдем в Visual Studio на вкладку Resource View. Откроем созданную помощником заготовку диалога IDD_PANEL, выберем форму диалога, в окне свойств изменим его идентификатор (свойство ID) на IDD_KBRD, а саму форму диалога приведем к следующему виду (рис. 14):

Рис. 14. Форма диалога IDD_KBRD

Этот диалог будет употреблен для ручного ввода примитива. Его размер 108*63 единицы (пикселя).
Свойства (ID и Caption) существующих в заготовке элементов (текст, поле ввода и счетчик) установим в соответствии с рис 15.

Рис. 15. Свойства элементов диалога IDD_KBRD

Ниже этих полей добавим кнопку класса CustButton (пользовательский класс). Это можно сделать, употребив инструмент Custom Control и приведя свойства (Caption, Class, ID и Style) добавленного элемента в соответствие с рис. 16.

Рис. 16. Добавление и настройка кнопки Create

Выделим теперь в дереве файла cube.rc ветвь IDD_KBRD, выполним ее копирование и вставку. Изменим идентификатор добавленного диалога на IDD_PARAMS (рис. 17), а его форму и свойства его элементов установим в соответствии с рис. 18.

Рис. 17. Добавлен диалог IDD_PARAMS

Рис. 18. Свойства элементов диалога IDD_PARAMS

Диалог IDD_PARAMS отражает текущее значение параметра Size и может быть, в частности, употреблен для изменения размера куба на вкладке Modify.
Откроем теперь таблицу символов ресурса и приведем ее в соответствие с рис. 19.

Рис. 19. Таблица символов ресурса cube.rc

Таблица символов содержит используемые плагином свойства ресурса (детали см. ниже).
В диалогах ресурса использованы следующие пользовательские элементы управления:

  • Custom Edit control класса CustEdit;
  • Custom Spinner Control класса SpinnerControl;
  • Custom Button control класса CustButton.

Файл resource.h

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

#define IDS_LIBDESCRIPTION  1
#define IDS_CATEGORY  2
#define IDS_CLASS_NAME  3
#define IDS_KBRD  4
#define IDS_PARAMS  5
#define IDS_CB_SIZE  6
#define IDD_PARAMS  101
#define IDD_KBRD  102
#define IDC_KBSZ  1001
#define IDC_KBSZSPIN  1002
#define IDC_KBSTATIC  1003
#define IDC_CREATE  1004
#define IDC_SZSTATIC  1005
#define IDC_SZ  1490
#define IDC_SZSPIN  1496

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

Код файла cube.rc

Перейдем на вкладку Solution Explorer и просмотрим код файла cube.rc (выбрать имя cube.rc – правая кнопка мыши – View Code). Код должен, помимо прочих, содержать следующие определения:

// Dialog
//
IDD_KBRD DIALOGEX 0, 0, 108, 63
STYLE DS_SETFONT | WS_CHILD | WS_VISIBLE
FONT 8, "MS Sans Serif", 0, 0, 0x0
BEGIN

  CONTROL   "",IDC_KBSZ,"CustEdit",WS_TABSTOP,43,17,35,10
  CONTROL   "",IDC_KBSZSPIN,"SpinnerControl",0x0,79,17,7,10
  LTEXT   "Size",IDC_KBSTATIC,21,18,14,8
  CONTROL   "Create",IDC_CREATE,"CustButton",WS_TABSTOP,31,36,44,12

END
IDD_PARAMS DIALOGEX 0, 0, 108, 48
STYLE DS_SETFONT | WS_CHILD | WS_VISIBLE
FONT 8, "MS Sans Serif", 0, 0, 0x0
BEGIN

  CONTROL   "",IDC_SZ,"CustEdit",WS_TABSTOP,43,17,35,10
  CONTROL   "",IDC_SZSPIN,"SpinnerControl",0x0,79,17,7,10
  LTEXT   "Size",IDC_SZSTATIC,21,18,14,8

END


// String Table
//
STRINGTABLE
BEGIN

  IDS_LIBDESCRIPTION   "Cube"
  IDS_CATEGORY   "SDK simple object"
  IDS_CLASS_NAME   "Cube"
  IDS_KBRD   "Keyboard Entry"
  IDS_PARAMS   "Parameters"
  IDS_CB_SIZE   "Size"

END

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

Файл cube.def

Файл cube.def генерируется помощником. Он содержит имя dll-файла и используемые плагином виды определений (описание библиотеки, число классов, описание классов, версия библиотеки, инициализация и закрытие библиотеки):

LIBRARY cube.dlo
EXPORTS

  LibDescription   @1  PRIVATE
  LibNumberClasses   @2  PRIVATE
  LibClassDesc   @3  PRIVATE
  LibVersion   @4  PRIVATE
  LibInitialize   @6  PRIVATE
  LibShutdown   @7  PRIVATE

SECTIONS
  .data READ WRITE

При изменении в свойствах проекта имени библиотеки соответствующую правку нужно выполнить и в def-файле.

Основные характеристики примитива

Создаваемый куб имеет следующие особенности:

  • имя класса в 3ds Max – Cube;
  • число вершин – 8;
  • число треугольных граней – 12;
  • имеет невидимые диагональные ребра (если взять сторону куба);
  • базовая точка расположена в центре куба;
  • группы сглаживания сформированы по аналогии с примитивом Box;
  • индекс материала всех граней равен 1;
  • имеет UVW-координаты;
  • может быть преобразован в Editable Mesh, Editable Poly или Editable Patch;
  • может быть тиражирован всеми доступными способами (Copy, Instance или Reference);
  • объект можно анимировать по его размеру (по параметру Size).

Код плагина cube.dlo

С позиции пользователя код, в частности, должен решать две следующие задачи:

  • создавать куб заданного размера после нажатия на кнопку Create;
  • обеспечивать ввод в сцену куба посредством мыши.

Сгенерированное помощником решение включает два cpp-файла с исходным кодом – это DllEntry.cpp и cube.cpp. Первый файл содержит код, обслуживающий dll-библиотеку. Это файл в нашем случае практически не требует изменений:

#include "cube.h"
extern ClassDesc2 *GetCubeDesc();
HINSTANCE hInstance;
// Функция DllMain вызывается Windows при загрузке DLL
// Также функция может вызываться во время таких операций,
// как воспроизведение изображения (Rendering)
BOOL WINAPI DllMain(HINSTANCE hinstDLL, ULONG fdwReason, LPVOID) {
 if (fdwReason == DLL_PROCESS_ATTACH) {
  hInstance = hinstDLL;
  DisableThreadLibraryCalls(hInstance);
 }
 return TRUE;
}
// Возвращает строку с описанием DLL
__declspec( dllexport ) const TCHAR* LibDescription() {return GetString(IDS_LIBDESCRIPTION);}
// Возвращает число классов плагина
// В нашем случае DLL позволяет оперировать одним классом Cube
__declspec( dllexport ) int LibNumberClasses() {return 1;}
// Возвращает описание i-го класса плагина
__declspec( dllexport ) ClassDesc *LibClassDesc(int i) {
 switch(i) {
  case 0: return GetCubeDesc();
  default: return 0;
 }
}
__declspec( dllexport ) ULONG LibVersion() {return VERSION_3DSMAX;}
// Вызывается один раз при загрузке плагина в 3ds Max
// Если в качестве результата указать FALSE, то система не будет загружать плагин,
// а DLL будет интерпретироваться как свободная библиотека
__declspec( dllexport ) int LibInitialize(void) {return TRUE;}
// Вызывается один раз при выгрузке плагина из 3ds Max
// Возвращаемый результат приложением не используется
__declspec( dllexport ) int LibShutdown(void) {return TRUE;}
//
// Возвращает строку таблицы символов ресурса
TCHAR *GetString(int id) {
 static TCHAR buf[256];
 if (hInstance)
  return LoadString(hInstance, id, buf, sizeof(buf)) ? buf : NULL;
 return NULL;
}

Главная функция формирует DllMain hInstance – дескриптор экземпляра плагина, передаваемый файлу cube.cpp посредством заголовочного файла cube.h.
Имена declspec-функций файла отвечают имеющимся в def-файле определениям.
Функция GetString получает значение (Value) идентификатора ресурса и возвращает значение поля Caption таблицы символов (String Table) ресурса cube.rc проекта.
Код, обеспечивающий функционал плагина, размещен в файле cube.cpp.
Код перимущественно сформирован помощником и включает набор классов и функций (методов), необходимых для создания и управления процедурными объектами. При необходимости разработчик может добавить свои классы и методы, а также внести отвечающие цели проекта изменения в предоставленный помощником код.
Поскольку удален заголовочный файл 3dsmaxsdk_preinclude.h, то в файле cube.cpp не должны присутствовать #pragma message.
Код содержит определения следующих четырех классов:

  • cube;
  • cubeClassDesc;
  • cubeKBDlgProc;
  • cubeCreateCallBack.

На рис. 20 и 21 показаны иерархии классов, лежащих в основе классов cube и cubeClassDesc.

Рис. 20. Класс cube: иерархия родительских классов

Рис. 21. Класс cubeClassDesc: иерархия родительских классов

Класс cube обеспечивает создание и управление примитивом.
Класс cubeClassDesc обеспечивает регистрацию объекта в 3ds Max.
Класс cubeKBDlgProc отвечает за связь плагина с диалогом ручного создания куба: его функция DlgProc регистрирует нажатие на кнопку Create (IDC_CREATE) диалога и обеспечивает создание куба заданного размера с центром в начале мировой системы координат.
Класс cubeCreateCallBack отвечает за ввод в сцену примитива посредством мыши: его функция proc получает информацию о мышиных событиях – это сообщения 3ds Max с именами MOUSE_POINT, MOUSE_MOVE и MOUSE_ABORT и соответствующим образом реагирует на эти события. Метод SetObj класса ассоциирует созданную меш с кубом.
Обе функции (DlgProc и proc) употребляют метод BuildMesh класса cube, используя соответственно методы NonMouseCreate (класс IObjParam) и InvalidateUI (класс ParamBlockDesc2).
Прочие пояснения см. в комментариях к приводимому ниже коду. При этом прежде следует комментарий, а затем комментируемый код.

#include "cube.h"
#define cube_CLASS_ID Class_ID(0xd667c5aa, 0xb65e9ddb)
#define PBLOCK_REF 0
class cube : public SimpleObject2 {
 public:
  // Ссылка на интерфейс
  static IObjParam *ip;
  // Флаг ручного (по кнопке Create) ввода примитива
  static BOOL kbrdCreate;
  // Из класса BaseObject
  CreateMouseCallBack *GetCreateMouseCallBack();
  // Из класса Object
  BOOL HasUVW();
  void SetGenUVW(BOOL sw);
  int CanConvertToType(Class_ID obtype);
  Object *ConvertToType(TimeValue t, Class_ID obtype);
  void GetCollapseTypes(Tab<Class_ID> &clist,Tab<TSTR*> &nlist);
  // Из класса GeomObject
  int IntersectRay(TimeValue t, Ray &ray, float &at, Point3 &norm);
  // Возвращает структуру ObjectState
  ObjectState Eval(TimeValue t) {return ObjectState(this);};
  // Из класса Animatable
  void BeginEditParams(IObjParam *ip, ULONG flags, Animatable *prev);
  void EndEditParams(IObjParam *ip, ULONG flags, Animatable *next);
  // Из класса SimpleObject
  // Строит меш
  void BuildMesh(TimeValue t);
  // Проверяет корректность задания параметров объекта
  BOOL OKtoDisplay(TimeValue t);
  // Обновляет пользовательский интерфейс
  void InvalidateUI();
  // Загрузка и сохранение данных плагина
  IOResult Load(ILoad *iload);
  IOResult Save(ISave *isave);
  // Из класса Animatable
  Class_ID ClassID() {return cube_CLASS_ID;}
  SClass_ID SuperClassID() {return GEOMOBJECT_CLASS_ID;}
  void GetClassName(TSTR& s) {s = GetString(IDS_CLASS_NAME);}
  void DeleteThis() {delete this;}
  //
  RefTargetHandle Clone(RemapDir &remap);
  //
  // Получает из таблицы символов имя класса
  TCHAR *GetObjectName() {return GetString(IDS_CLASS_NAME);}
  // Число блоков параметров
  int NumParamBlocks() {return 1;}
  // Возвращает блок параметров по его номеру
  IParamBlock2 *GetParamBlock(int i) {return pblock2;}
  // Возвращает блок параметров по его идентификатору
  IParamBlock2 *GetParamBlockByID(BlockID id) {return (pblock2->ID() == id) ? pblock2 : NULL;}
  // Конструктор / Деструктор
  cube();
  ~cube();
};
class cubeClassDesc : public ClassDesc2 {
 public:
  int IsPublic() {return TRUE;}
  void *Create(BOOL) {return new cube();}
  const TCHAR *ClassName() {return GetString(IDS_CLASS_NAME);}
  SClass_ID SuperClassID() {return GEOMOBJECT_CLASS_ID;}
  Class_ID ClassID() {return cube_CLASS_ID;}
  const TCHAR *Category() {return GetString(IDS_CATEGORY);}
  const TCHAR *InternalName() {return _T("cube");}
  HINSTANCE HInstance() {return hInstance;}
};
static cubeClassDesc cubeDesc;
ClassDesc2 *GetCubeDesc() {return &cubeDesc;}
// Имена диалогов IDD_KBRD и IDD_PARAMS
enum {cube_kbrd, cube_params};
// Имена ассоцируемые с упраляющими элементами диалогов
// Элементы IDC_KBSZ, IDC_KBSZSPIN
enum {cube_kb_size};
// Элементы IDC_SZ, IDC_SZSPIN
enum {cube_size};
// Блок параметров диалога IDD_KBRD
static ParamBlockDesc2 cube_kbrd_blk (cube_kbrd, _T("cubeKbrd"), 0, &cubeDesc, P_CLASS_PARAMS + P_AUTO_UI,
 IDD_KBRD, IDS_KBRD, BEGIN_EDIT_CREATE, APPENDROLL_CLOSED, NULL,
 cube_kb_size, _T("kbSize"), TYPE_FLOAT, 0, IDS_CB_SIZE,
  p_default, 40.0, p_range, 0.0f, 100.0f,
  p_ui, TYPE_SPINNER, EDITTYPE_UNIVERSE, IDC_KBSZ, IDC_KBSZSPIN, 0.1f,
  end,
 end
);
// Блок параметров диалога IDD_PARAMS
static ParamBlockDesc2 cube_param_blk (cube_params, _T("params"), 0, &cubeDesc, P_AUTO_CONSTRUCT + P_AUTO_UI, PBLOCK_REF,
 IDD_PARAMS, IDS_PARAMS, 0, 0, NULL,
  cube_size, _T("size"), TYPE_FLOAT, P_ANIMATABLE, IDS_CB_SIZE,
  p_default, 0.0f, p_range, 0.0f, 100.0f,
  p_ui, TYPE_SPINNER, EDITTYPE_UNIVERSE, IDC_SZ, IDC_SZSPIN, 0.1f,
  end,
 end
);
// Инициализация свойств ip и kbrdCreate класса cube
IObjParam *cube::ip = NULL;
BOOL cube::kbrdCreate = FALSE;
// Создаем диалог с P_AUTO_CONSTRUCT-блоком параметров
// Диалог IDD_KBRD будет создан при обращении cubeDesc.BeginEditParams
cube::cube() {cubeDesc.MakeAutoParamBlocks(this);}
cube::~cube() { }
IOResult cube::Load(ILoad *iload) {return IO_OK;}
IOResult cube::Save(ISave *isave) {return IO_OK;}
class cubeKBDlgProc : public ParamMap2UserDlgProc {
 public:
  cube *ob;
  cubeKBDlgProc(cube *cb) {ob = cb;}
  INT_PTR DlgProc(TimeValue t, IParamMap2 *map, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
  void DeleteThis() {delete this;}
};
// Обеспечивает создание куба после нажатия на кнопку Create диалога IDD_KBRD
INT_PTR cubeKBDlgProc::DlgProc(TimeValue t, IParamMap2 *map, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
 if (msg != WM_COMMAND) return FALSE;
 if (LOWORD(wParam) != IDC_CREATE) return FALSE;
 float cbSize = cube_kbrd_blk.GetFloat(cube_kb_size);
 if (cbSize == 0.0) return TRUE;
 // Устанавливаем IDC_SZ = IDC_KBSZ
 if (ob->TestAFlag(A_OBJ_CREATING)) ob->pblock2->SetValue(cube_size, 0, cbSize);
 // Флаг ручного ввода
 ob->kbrdCreate = TRUE;
 // Формируем единичную матрицу
 Matrix3 tm(1);
 // Устанавливаем в матрице порцию Translate (перемещение) аффинных преобразований
 tm.SetTrans(Point3(0, 0, 0));
 ob->suspendSnap = FALSE;
 // Формируем куб
 ob->ip->NonMouseCreate(tm);
 return TRUE;
}
// Вызывается при создании очередного экземпляра куба (из класса Animatable)
void cube::BeginEditParams(IObjParam *ip,ULONG flags, Animatable *prev) {
 SimpleObject::BeginEditParams(ip, flags, prev);
 this->ip = ip;
 if (kbrdCreate) {
  // Если ранее был выполнен ручной ввод, то устанавливаем IDC_SZ = IDC_KBSZ
  pblock2->SetValue(cube_size, 0, cube_kbrd_blk.GetFloat(cube_kb_size));
  kbrdCreate = FALSE;
 }
 cubeDesc.BeginEditParams(ip, this, flags, prev);
 // Фиксируем пользовательскую процедуру,
 // ассоциированную с блоком параметров cube_kbrd_blk
 cube_kbrd_blk.SetUserDlgProc(new cubeKBDlgProc(this));
}
void cube::EndEditParams(IObjParam *ip, ULONG flags, Animatable *next) {
 SimpleObject::EndEditParams(ip, flags, next);
 cubeDesc.EndEditParams(ip, this, flags, next);
 // Плагин должен вызывать методы интерфейса ip только
 // между BeginEditParams и EndEditParams
 this->ip = NULL;
}
// Из класса Object
// Вернуть флаг наличия у объекта UVW-координат
BOOL cube::HasUVW() {return TRUE;}
// Можно модифицировать, исходя из целей проекта
void cube::SetGenUVW(BOOL sw) {if (sw == HasUVW()) return;}
// Класс обработки мышиных событий
class cubeCreateCallBack : public CreateMouseCallBack {
 cube *ob;  // Указатель на объект
 Point3 p0;  // Первая точка в мировой системе координат
 Point3 p1;  // Вторая точка в мировой системе координат
 public:
  int proc(ViewExp *vpt, int msg, int point, int flags, IPoint2 m, Matrix3 &mat);
  void SetObj(cube *cb) {ob = cb;}
};
int cubeCreateCallBack::proc(ViewExp *vpt, int msg, int point, int flags, IPoint2 m, Matrix3 &mat){
 if (msg == MOUSE_POINT || msg == MOUSE_MOVE) {
  switch(point) {
   case 0:
    ob->suspendSnap = TRUE;
    // m - позиция мыши в оконных координатах
    // p0 - позиция мыши в мировых координатах
    p0 = vpt->SnapPoint(m, m, NULL, SNAP_IN_PLANE);
    // Порция Translate (перемещение) аффинных преобразований позиции
    mat.SetTrans(p0);
    // Изменяем значение параметра cube_size диалога IDD_PARAMS
    ob->pblock2->SetValue(cube_size, ob->ip->GetTime(), 0.0f);
    break;
   case 1: {
    ob->suspendSnap = TRUE;
    // p1 - новая позиция мыши в мировых координатах
    p1 = vpt->SnapPoint(m, m, NULL, SNAP_IN_PLANE);
    // Управляем размером куба в зависимости от положения мыши
    ob->pblock2->SetValue(cube_size, ob->ip->GetTime(), 0.5f * Length(p1 - p0));
    // Создаем и отображаем меш в видовом порте
    cube_param_blk.InvalidateUI();
    break;
   }
   case 2:
    return CREATE_STOP;
  }
 }
 else
  if (msg == MOUSE_ABORT) return CREATE_ABORT;
 return TRUE;
}
static cubeCreateCallBack cubeCreateCB;
CreateMouseCallBack *cube::GetCreateMouseCallBack() {
 cubeCreateCB.SetObj(this);
 return &cubeCreateCB;
}
// Получает размер куба, строит его меш и формирует группы сглаживания
void cube::BuildMesh(TimeValue t) {
 float size, h;
 ivalid = FOREVER;
 pblock2->GetValue(cube_size, t, size, ivalid);
 h = 0.5 * size;
 // Число вершин и граней в меш
 mesh.setNumVerts(8);
 mesh.setNumFaces(12);
 // Координаты вершин
 mesh.setVert(0, Point3(-h, -h, -h));
 mesh.setVert(1, Point3(h, -h, -h));
 mesh.setVert(2, Point3(-h, h, -h));
 mesh.setVert(3, Point3(h, h, -h));
 mesh.setVert(4, Point3(-h, -h, h));
 mesh.setVert(5, Point3(h, -h, h));
 mesh.setVert(6, Point3(-h, h, h));
 mesh.setVert(7, Point3(h, h, h));
 // Состав граней
 mesh.faces[0].setVerts(0, 2, 3);
 mesh.faces[1].setVerts(3, 1, 0);
 mesh.faces[2].setVerts(4, 5, 7);
 mesh.faces[3].setVerts(7, 6, 4);
 mesh.faces[4].setVerts(0, 1, 5);
 mesh.faces[5].setVerts(5, 4, 0);
 mesh.faces[6].setVerts(1, 3, 7);
 mesh.faces[7].setVerts(7, 5, 1);
 mesh.faces[8].setVerts(3, 2, 6);
 mesh.faces[9].setVerts(6, 7, 3);
 mesh.faces[10].setVerts(2, 0, 4);
 mesh.faces[11].setVerts(4, 6, 2);
 // Группы сглаживания
 mesh.faces[0].setSmGroup(2);
 mesh.faces[1].setSmGroup(2);
 mesh.faces[2].setSmGroup(4);
 mesh.faces[3].setSmGroup(4);
 mesh.faces[4].setSmGroup(8);
 mesh.faces[5].setSmGroup(8);
 mesh.faces[6].setSmGroup(16);
 mesh.faces[7].setSmGroup(16);
 mesh.faces[8].setSmGroup(32);
 mesh.faces[9].setSmGroup(32);
 mesh.faces[10].setSmGroup(64);
 mesh.faces[11].setSmGroup(64);
 // Устанавливаем видимость внешних ребер граней (диагональные ребра не видны)
 for (int k = 0; k < 12; k++) {
  mesh.faces[k].setEdgeVisFlags(1, 1, 0);
  mesh.faces[k].setMatID(1);
 }
 // Назначаем UVW-координаты
 Matrix3 tm(1);
 tm.Scale(Point3(h, h, h));
 tm = Inverse(tm);
 mesh.ApplyUVWMap(MAP_BOX, 1.0f, 1.0f, 1.0f, 0, 0, 0, 0, tm);
 mesh.InvalidateTopologyCache();
}
// Добавлен код проверки корректности параметра size
BOOL cube::OKtoDisplay(TimeValue t) {
 float size;
 pblock2->GetValue(cube_size, t, size, FOREVER);
 return (size <= 0.0f) ? FALSE : TRUE;
}
void cube::InvalidateUI() {cube_param_blk.InvalidateUI(pblock2->LastNotifyParamID());}
// Находит точку пересечения луча ray с поверхностью (см. класс Ray)
// и нормаль к поверхности в этой точке
int cube::IntersectRay(TimeValue t, Ray &ray, float &at, Point3 &norm) {
 return SimpleObject::IntersectRay(t, ray, at, norm);
}
// Методы, обеспечивающие преобразование и копирование объекта
Object* cube::ConvertToType(TimeValue t, Class_ID obtype) {
 return SimpleObject::ConvertToType(t, obtype);
}
int cube::CanConvertToType(Class_ID obtype) {
 if (obtype == defObjectClassID || obtype == triObjectClassID)
  return 1;
 else
  return SimpleObject::CanConvertToType(obtype);
}
void cube::GetCollapseTypes(Tab<Class_ID> &clist, Tab<TSTR*> &nlist) {
 Object::GetCollapseTypes(clist, nlist);
}
RefTargetHandle cube::Clone(RemapDir &remap) {
 cube *newob = new cube();
 newob->ReplaceReference(0, remap.CloneRef(pblock2));
 newob->ivalid.SetEmpty();
 BaseClone(this, newob, remap);
 return newob;
}

Приведенный код можно скопировать в созданный помощником проект Visual Studio. При этом следует копировать коды обоих файлов – и DllEntry.cpp, и cube.cpp.
После настроек свойств проекта и создания диалогов построенная на основе этого кода dll-библиотека должна обеспечивать описанный в уроке функционал.
Построение DLL осуществляется после нажатия на F7 (меню Build - Build Solution).

Загрузка и вызов плагина

Созданный плагин, файл cube.dlo следует загружать в начале сеанса работы с 3ds Max, открыв меню Customize – Plug-in Manager – табличная часть диалога – правая кнопка мыши – Load New Plug-in – найти загружаемый файл.
Загруженный плагин доступен в командном окне: вкладка Create – Geometry – SDK simple object (рис. 22).

Рис. 22. Вызов плагина Cube

Если загружать плагин не вначале сеанса, а позже, то может возникнуть указанная на рис. 23 ошибка.

Рис. 23. Ошибка инициализации плагина

Заключение

Создание дополнительного инструмента 3ds Max – это достаточно трудоемкая работа. Поэтому должна быть серьезная мотивация для ее выполнения. Например, наличие идеи расширения функционала 3ds Max, интересной широкому кругу пользователей приложения. Удачное расширение может бы

27161 Автор: catBurdger
Актуальность: 987
Качество: 998
Суммарный балл: 1985
Выбор Публики
Голосов: 92 оценки

Отзывы посетителей:

аватар
 
catBurdger 0
Чтобы код стал работать, фигурные скобки нужно заменить на квадратные (квадратные скобки, помещенные в комментарии, исчезают).

delete $*
h = 40.0
n = 8
d = h / n
h2 = 0.5 * h
d2 = 0.5 * d
p0 = {h2 - d, h2 - d, -d}
for k = 1 to n do
for k2 = 1 to n do
for k3 = 1 to n do (
p = p0 + {d * k3, d * k2, d * k}
box height:d width:d length:d pos:p wireColor:(random (color 0 0 0) (color 255 255 255))
)
аватар
 
catBurdger 0
Фигуру можно собрать из кубиков
delete $*
h = 40.0
n = 3
d = h / n
h2 = 0.5 * h
d2 = 0.5 * d
p0 =
for k = 1 to n do
for k2 = 1 to n do
for k3 = 1 to n do (
p = p0 +
box height:d width:d length:d pos:p wireColor:(random (color 0 0 0) (color 255 255 255))
)
Для анимации ребер (нажать F4) можно употребить код
delete $*
bx = box height:40 width:40 length:40 wireColor:gray
animate on (
at time 0 (
bx.heightSegs = 1
bx.widthSegs = 1
bx.lengthSegs = 1
)
at time 100 (
bx.heightSegs = 10
bx.widthSegs = 10
bx.lengthSegs = 10
)
)
sliderTime = 0f
animationRange = interval 0f 100f
playAnimation()
Можно употребить разбиение Вороного
http://100byte.ru/mxscrptxmpls/vrn/vrn.html
Анимацию собирания большого куба из кубиков можно сделать при помощи системы частиц Particle Flow
аватар
 
Олег Павлов 0
catBurdger спасибо за ответ. Но нужно немного не то. В результате вашего скрипта получается куб с внутренними гранями, пересекающимися в центре. А нужно, чтобы они были перпендикулярны и параллельны друг другу. И есть ли возможность сделать анимацию увеличения количества этих граней. Если сможете, помогите пожалуйста. Или подскажите в каком направлении копать материал.
аватар
 
catBurdger 0
Такой куб можно построить, например, из 6 пирамид
fn oneP p angX angY = (
p2 = copy p
rotate p2 (eulerangles angX angY 0)
p.attach p2 p
)
delete $*
p = Pyramid widthsegs:1 depthSegs:1 heightsegs:1 width:30 depth:30 height:15 wireColor:gray
p.pivot.z = 15
convertToPoly p
oneP p 0 180
oneP p 0 90
oneP p 0 -90
oneP p 90 0
oneP p -90 0

Кроме того, можно употребить объект Hedra c переключателем Star1 или Star2
аватар
 
Гость 0
Статья очень полезная! А у меня вопрос. Здесь, в качестве примера, разбирается построение бокса с нужными параметрами. А можно ли с помощью скрипта создать бокс у которого эджи были бы не только снаружи, но и внутри. Т.е. получался бы объект на подобии кристаллической решетки. Если есть такая возможность, то наведите на мысль как это сделать, что прописать в скрипте, чтобы грани пересеклись. Заранее спасибо за ответ.
аватар
 
Klas
аватар
 
Fiz 0
ещё бы кто нибудь видео урок сделал цены бы небыло ему)
аватар
 
Блажко Станислав 0
Ох ёжкин свет млин !!!
аватар
 
Timur Mikhtsev 0
Замечательно. Давно хотел узнать как пишут эти плагины, молодец огромный труд.
аватар
 
Fiz 0
огромное спасибо давно ждал подобного урока 5/5
аватар
 
IgoTM 0
геморойный путь как - ни крути... лучше скриптом... сохраняется совместимось... а так попадаешь в рабство к SDK, новым версиям Макса и Студии... гонка и геморой... геморой и гонка...

надо писать инструменты так, что бы не становится их заложниками их разработки, улучшения, перекомпиляции под новые версии...
но если вы хотите прям нырнуть в это и быть там мега-мастером - то да, это тот самый путь... приготовьте мешок времени...
аватар
 
Strannick 0
спасибо! Балуя себя скриптами недавно начал задаваться вопросом ка же сделать "плуг" и тут оба... урок ))
еще раз спасибо и конечно же пятёрки )
аватар
 
catBurdger 0
Цитирую iVAt:
у меня такой вопрос - для макса 2010 обязательно нужна Visual Studio 2008 или подойдёт Visual Studio 2010?

Это можно проверить, например, следующим образом.
1. Найти проект Widget.vcproj (или иной) (скорее всего, он в папке C:\Program Files\Autodesk\3ds Max 20хх SDK\maxsdk\howto\objects\widget) и открыть его двойным ударом мыши.
2. Изменить конфигурацию на Hybrid (см. в уроке).
3. Создать widget.dlo (кнопка F7). По умолчанию файл будет помещен в папку C:\Program Files\Autodesk\3ds Max 20хх SDK\maxsdk\plugin. Предварительно прежний widget.dlo, лежащий в этой папке, переименовать или временно переместить в иное место.
4. Открыть widget.dlo в 3ds Max.
5. Проверить. Плагин доступен в командном окне на вкладке Create - Geometry - MAX SDK Tutorials - Widget.
аватар
 
iVAt 0
Цитирую G.R.:
Сам никогда этим заниматься не буду,

ой не зарекайся))
у меня такой вопрос - для макса 2010 обязательно нужна Visual Studio 2008 или подойдёт Visual Studio 2010?
аватар
 
GeorgeThreeD 0
Сам никогда этим заниматься не буду, да и вообще с максом не в ладах, но за труд не могу не поставить. Отлично. 5/5
аватар
 
Ironix 0
Увлекался раньше написание скриптов для макса, в основном в виде utilites, есть один которым и сейчас пользуюсь - выбираешь нужные объекты во вьюпорте, и жмёшь attach, намного удобнее чем тыкать по одному объекты в mech-edit-attach, или выбирать их в списке attach multi
но до плагинов не добрался. за статью 5/5
аватар
 
Mr.Core 0
Отличный материал!
аватар
 
megavitus 0
Спасибо, более чем актуально.
аватар
 
dimson3d 0
Ньюсмейкер
Да, тема разработки подключаемых модулей на C\C++ для 3ds Max, да и для Maya тоже, это очень интересная тема. Автор молодец что не поленился написать. Редко такие материалы появляются в сети. 5\5 и от меня.
аватар
 
iVAt 0
О, вот эта тема!
Автору большое спасибо, буду изучать, надеюсь на возникающие вопросы у автора будет желание и время отвечать (скорее всего не тут, а в форуме).
ну и 5/5 от меня.
Зарегистрируйтесь, чтобы добавить комментарий.
Эту страницу просмотрели: 27161 уникальных посетителей