3DCoat  3D-COAT 4.9.xx
3DCoat is the one application that has all the tools you need to take your 3D idea from a block of digital clay all the way to a production ready, fully textured organic or hard surface model.
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
Работа с VoxelExtension или Создание своего инструмента в 3DCoat
Author
Владимир Маковецкий

Введение

Новый инструмент для комнаты скульптинга можно создать добавлением в проект одного cpp файла и, желательно, хидера к нему.

Создать cpp и h файлы можно в Visual Studio прямо из Solution Explorer.

Через меню кнопки Browse определяем место на ПК где будем хранить файлы нашего мини-проекта. Для удобства назовем папку "Tutorials".

open-vs.png

Дальше создаем хидер также через кнопки контекстного меню: Add / New Item / Header, с названием "MyBoxTutorial.h". Также создаем файл с основным кодом нашего мини проекта через то же контекстное меню, но уже выбираем файл  с расширением ".cpp" : Add / New Item, и называем его соответственно предыдущему файлу с иным типом "MyBoxTutorial.cpp".

create-cpp-1.png


create-cpp-2.png


create-cpp-3.png


Код для создания нового инструмента

Рассмотрим необходимый код для создания нового инструмента 3DCoat. Здесь мы создаём инструмент, который только **отображается** на панели инструментов.

Код "MyBoxTutorial.h":

class MyBoxTutorialExt :public VoxelExtension{
public:
// Указываем позицию, после какого инструмента в воксельных инструментах будет находиться инструмент
virtual int GetPrevVoxelTool(){ return -2; }
// Указываем позицию, после какого инструмента в сурфейсных инструментах будет находиться инструмент
virtual int GetPrevSurfaceTool(){ return VOX_TRANSPOSE; }
// Отображаемое название нашего инструмента.
// Функция должна возвращать только ключевое слово, действительно название берется из языкового файла (по умолчанию [3DCoatBinFolder]/Languages/English.xml)
virtual const char* GetID(){ return "MyBoxTool"; }
// Описание инструмента, которое будет отображаться при наведении на него курсора.
// Функция должна возвращать только ключевое слово, само описание берется из языкового файла (по умолчанию [3DCoatBinFolder]/Languages/English.xml)
virtual const char* GetHint(){ return "MyBoxTool_HINT"; }
};

Код "MyBoxTutorial.cpp":

#include "../stdafx.h"
#include "MyBoxTutorial.h"
// Указатель на инструмент
MyBoxTutorialExt* m_MyBoxTool;
// Функция, которая создаст экземпляр класса с инструментом и зарегистрирует его
void InitMyBoxTool(){
// Создаем экземпляр класса с нашим инструментом и присваиваем ссылку на него указателю `m_MyBoxTool`.
m_MyBoxTool = new MyBoxTutorialExt;
// Регистрируем инструмент, для отображения в 3D Coat.
}
// С помощью макроса `CallItLater` вызываем функцию InitMyBoxTool в окне приветствия,
// при загрузке 3DCoat, после того как программа будет готова к инициализации инструментов
CallItLater _MyBoxTutorialExt(&InitMyBoxTool, VOX_EXTENSION);

После сборки и запуске 3DCoat видим созданный нами выше инструмент в левой панели UI:

tools-1.png

Создание формы с параметрами

Необходимо в хидере (файл "MyBoxTutorial.h")  зарегистрировать класс для создания куба, так как именно с этой формой мы будем работать. Дальше сможем заменить форму на любую удобную для нас.

Добавляем в хидер след. код:

// Объявляем переменные для записи размеров куба
float BoxWidth = 10.0f;
float BoxHeight = 10.0f;
float BoxLength = 10.0f;
// Объявляем переменные для записи позиции куба
float XPos = 0.0f;
float YPos = 0.0f;
float ZPos = 0.0f;
// С Помощью макроса SAVE регистрируем параметры класса. По зарегистрированным параметрам будет строиться форма для настройки инструмента
// Это один из нескольких способов зарегистрировать параметры класса, удобен тем, чтоб его можно помещать прямо в хидере
SAVE(MyBoxTutorialExt){
// Регистрируем параметры для записи размеров куба с помощью макроса REG_FSLIDER, привязываясь к вышеописанным переменным BoxWidth, BoxHeight, BoxLength
// Задаем минимальное значение 0, максимальное 50, по умолчанию 10
REG_FSLIDER(BoxWidth, 0, 50, 10);
REG_FSLIDER(BoxHeight, 0, 50, 10);
REG_FSLIDER(BoxLength, 0, 50, 10);
// Регистрируем параметры для записи позиции куба с помощью макроса REG_FSLIDER, привязываясь к вышеописанным переменным XPos, YPos, ZPos
// Задаем минимальное значение -50, максимальное 50, по умолчанию 0
REG_FSLIDER(XPos, -50, 50, 0);
REG_FSLIDER(YPos, -50, 50, 0);
REG_FSLIDER(ZPos, -50, 50, 0);
// Макрос ENDSAVE указывает на завершение блока SAVE. Без него будут ошибки!!!
}ENDSAVE;

В результате получили инструмент в виде маленького куба. Теперь можно менять его позицию в пространстве и угол наклона по осям.

tools-2.png

Отрисовка куба

Для отрисовки перепишем функцию Render, которая вызывается каждый раз при перерисовке вьюпорта, когда инструмент активен. Саму сетку куба будем генерировать в отдельной функции (если в этом есть необходимость).

Добавим в "MyBoxTutorial.h" необходимые для этого методы и переменные:

    // RenderGuides - функция вспомогательных манипуляторов
    // Здесь мы будем создавать сетку нашего куба
    comms::cMeshContainer boxMeshContainer;
    // Указатель на структуру с буферами сетки в видеокарте для быстрой отрисовки
    StaticMesh* boxStaticMesh = 0;
    // Предыдущая контрольная сумма всех данных текущего класса, для сравнения с текущей
    //чтоб пересчитывать сетку куба если был изменен какой-то параметр
    DWORD LastHash;
    // Предыдущее положение манипулятора для сравнения с настоящим положением
    //чтобы пересоздать сетку куба (меш куба)  если она изменилась
    Matrix4D LastTransMatrix;
    // Перезаписываем функцию отрисовки.
    // Она вызывается каждый раз во время рендера при перерисовке сцены.
    // Здесь мы мы отображать модель куба
    virtual    void Render();
    // Функция создания структуры (меш) куба
    void MakeMesh();

А в "MyBoxTutorial.cpp" напишем реализацию:

void MyBoxTutorialExt::MakeMesh(){
//Узнаем  и записываем контрольную сумму из всех параметров  данного класса
    DWORD currentHash = GetHash();
//Отменяем выполнение функции если ни один параметр нашего класса не изменился //
//и положение манипулятора осталось прежним и также меш куба уже создан,
//чтобы не создавать куб при каждой отрисовке заново
    if (currentHash == LastHash && LastTransMatrix ==
voxtool().CHandler.CP.GlobalTransform && boxStaticMesh != 0) return;
// Присваиваем переменной класса LastHash текущую контрольную сумму всех параметров класса,
//чтоб при следующем вызове этой функции сравнить это значение из следующей контрольной сумме и
//выполнять ее если она изменилась (если какой то параметр был изменен)
    LastHash = currentHash;
//// Присваиваем переменной класса LastTransMatrix текущую матрицу преобразования,
//чтоб при следующем вызове этой функции сравнить ее с следующим положением манипулятора и
//выполнять ее если положение манипулятора изменилось
    LastTransMatrix = voxtool().CHandler.CP.GlobalTransform;
// Заменяем контейнер с боксом который уже был на новый (пустой),
    boxMeshContainer = comms::cMeshContainer();
// Создание куба с новыми параметрами (ширина, высота, длина)
    boxMeshContainer.CreateCube(Vector3D(BoxWidth, BoxHeight, BoxLength));
//Подразбиение сетки куба  на треугольники размером 0,5 юнитов
// чтоб форма куба выглядела более твердой и корректной работы скульптинга
    boxMeshContainer.QuadQuantSubd(Matrix4D::Identity, 0.5, 0);
// Инвертируем меш (особенность 3DCoat) последующие функции работают с инвертированным меш
    boxMeshContainer.InvertRaw();
// Перемещаем куб на заданную в параметрах позицию
    boxMeshContainer.Transform(Matrix4D::Translation(Vector3D(XPos, YPos, ZPos)));
// Перемещаем куб соответственно положению манипулятора
    boxMeshContainer.Transform(voxtool().CHandler.CP.GlobalTransform);
// Удаляем старые буфера на видеокарте если они уже были созданы (хранятся в классе StaticMesh)
    if (boxStaticMesh != 0) delete boxStaticMesh;
// Создаем новые буфера с на видеокарте с сеткой нашего бокса
    boxStaticMesh = boxMeshContainer.CreateStaticMeshMC();
}
void MyBoxTutorialExt::Render(){
// Вызываем функцию которая создаст или пересоздает меш (сетку куба) если необходимо
    MakeMesh();
// Получаем идентификатор шейдера  raw в статическую переменную поскольку переменная статическая
//функция GetShaderID будет вызвана только при первом вызове функции Render,
//что позволит избавиться от задержек в последующих вызовах
    static int shader = IRS->GetShaderID("raw");
// Назначаем шейдер "raw" для последующего рендера
    IRS->SetShader(shader);
// Получаем идентификатор параметра "Color" из шейдера "raw"
    static int vColor = IRS->GetShaderVarID(shader, "Color");
// Если параметр "Color" есть в шейдере  (если его нет в шейдере - его идентификатор будет равен -1)
// Назначаем ему желтый полупрозрачный цвет (по каналам красному 1, зеленому 0.65, синему 0, непрозрачность 0.5)
    if (vColor != -1) IRS->SetShaderVar(shader, vColor, Vector4D(1.0f, 0.65f, 0.0f, 0.5f));
// Получаем идентификатор параметра "LDir"(направление источника света) из шейдера "raw"
    static int vLightDir = IRS->GetShaderVarID(shader, "LDir");
    if (vLightDir != -1){
// Назначаем источнику света направление верхнего левого угла относительно камеры
        Vector3D V1 = GetViewer()->GetRight() + GetViewer()->GetUp() +  GetViewer()->GetForward();
        V1 *= -1;
        IRS->SetShaderVar(shader, vLightDir, V1);
    }
//Назначаем рендеру отображать только внутреннюю часть объекта (треугольники точки которых соединены
//относительно экрана против часовой стрелки ) для корректного отображения внутренней
//и наружной части полупрозрачного объекта сначала нужно отрисовать внутренние
    comms::cRender::SetCullMode(comms::cCullMode::CounterClockwise);
// отрисовываем куб из заданными параметрами
    boxStaticMesh->render();
// отрисовываем наружную часть куба
    comms::cRender::SetCullMode(comms::cCullMode::Clockwise);
    boxStaticMesh->render();
//Возвращаем режим отрисовки в стандартное положение (когда рисуются и внутренние и наружные треугольники одновременно)
    comms::cRender::SetCullMode(comms::cCullMode::None);
}

Наслаждаемся результатом:

tools-3.png

Создаём кнопку для добавления куба на сцену

Для этого создадим в классе процедуру, которая будет выполнять добавление куба в выбранный слой. Назовем ее Apply(). В файле "MyBoxTutorial.h" объявим ее в классе, а в регистрации класса, перед строкой }ENDSAVE;, зарегистрируем этот метода следующей строкой:

// макрос REG_CMETHOD добавляет кнопку на панель, первым параметром задается класс в котором находится функция,
// вторым - название функции, кнопка будет называться также как функция или название будет взято из языкового файла если таковое имеется
REG_CMETHOD(MyBoxTutorialExt, Apply);

И добавим тело функции в "MyBoxTutorial.cpp":

void MyBoxTutorialExt::Apply(){
// Поскольку у нас уже есть готовый меш с преобразованиями (поворотом, позицией, скейлом) -
// не нужно его дополнительно трансвормировать, создаем матрицу без трансформации
    Matrix4D m = Matrix4D::Identity;
// Вызываем функцию MergeModelSymm из CurVObj(выбранного скульпт объекта), которая вставит в него наш бокс
// Первым параметром передаем меш нашего бокса(boxMeshContainer), следующим матрицу преобразования,
// третьим параметров указываем, что бокс должен именно добавляться в сцену, а не вырезаться
// из уже вставленных объектов
    CurVObj->MergeModelSymm(boxMeshContainer, m, false);
// Указываем что выбранный слой был изменен, что сообщит движку 3DCoat о необходимости его обновить
    CurVObj->WholeDirty = true;
}

На панели инструмента появилась кнопка Apply, которая успешно добавляет образец бокса в выбранный слой:

tools-4.png

Управление манипулятором

В "MyBoxTutorial.h", в объявлении нашего класса, декларируем необходимые переменные и функции:

    bool NeedResetPosition = true; // Переменная сигнализирующая о том, что нужно сбрасить позицию манипулятора
    bool LocalSpace = true; // Включатель сохранения поворота манипулятора
    virtual void OnActivate(); // Перезапись функции срабатующей при активации инструмента
    void ResetAxis(); // Функция сброса осей (Поворота, наклона)
    void ResetPosition(); // Функция сброса позиций
    void ResetSize(); // Функция сброса размера

Зарегистрируем их в макросе SAVE(MyBoxTutorialExt){, перед кнопкой Apply:

    COLUMNS(3); // Размещаем три следующие кнопки в одну строку
    REG_CMETHOD(MyBoxTutorialExt, ResetSize); // Регистрация кнопки сброса размера
    REG_CMETHOD(MyBoxTutorialExt, ResetAxis); // Регистрация кнопки сброса осей
    REG_CMETHOD(MyBoxTutorialExt, ResetPosition); // Регистрация кнопки сброса позиции
    REG_MEMBER(_bool, LocalSpace); // регистрация булевого значения добавил птичку выбора на панель, для сохранения поворота манипулятора

Добавим реализацию новых функций в "MyBoxTutorial.cpp":

// Функция вызываемая в случае активации инструмент
void MyBoxTutorialExt::OnActivate(){
    // Во время активации инструмента сбросим состояние пивота
    // Все оси манипулятора и позицию заменяем на исходные
    voxtool().CHandler.CP.AxisX = Vector3D::AxisX;
    voxtool().CHandler.CP.AxisY = Vector3D::AxisY;
    voxtool().CHandler.CP.AxisZ = Vector3D::AxisZ;
    voxtool().CHandler.CP.Pos = Vector3D::Zero;
    // Трансформирующую матрицу манипулятора замениям на исходною (без трансформаций)
    voxtool().CHandler.CP.GlobalTransform = Matrix4D::Identity;
    // Помечаем что нужно сбросить позицию манипулятора во время рендера (иначе позиция может не быть сброшена)
    NeedResetPosition = true;
}
// Функция сброса осей (Поворота, наклона)
void MyBoxTutorialExt::ResetAxis(){
    // Длину всех осей, чтоб вернуть размеры после сброса
    float sx = voxtool().CHandler.CP.GlobalTransform.Row0().Length();
    float sy = voxtool().CHandler.CP.GlobalTransform.Row1().Length();
    float sz = voxtool().CHandler.CP.GlobalTransform.Row2().Length();
    // Устанавливаем исходные оси трансформирующей матрицы умноженные на скейл каждой из них
    voxtool().CHandler.CP.GlobalTransform.Row0().ToVec3() = Vector3D::AxisX * sx;
    voxtool().CHandler.CP.GlobalTransform.Row1().ToVec3() = Vector3D::AxisY * sy;
    voxtool().CHandler.CP.GlobalTransform.Row2().ToVec3() = Vector3D::AxisZ * sz;
    // Присваиваем осям манипулятора исходные направления
    voxtool().CHandler.CP.AxisX = Vector3D::AxisX;
    voxtool().CHandler.CP.AxisY = Vector3D::AxisY;
    voxtool().CHandler.CP.AxisZ = Vector3D::AxisZ;
}
// Функция сброса позиций
void MyBoxTutorialExt::ResetPosition(){
    // Сбрасываем позицию манипулятора
    voxtool().CHandler.ResetPosition();
    // Присваеваем в матрице преобразования вектору позиции нулевые значения
    voxtool().CHandler.CP.GlobalTransform.Row3().ToVec3() = Vector3D::Zero;
}
// Функция сброса размера
void MyBoxTutorialExt::ResetSize(){
    // Для сброса скейла просто нормализируем направляющие вектора матрицы преобразования
    voxtool().CHandler.CP.GlobalTransform.Row0().ToVec3().Normalize();
    voxtool().CHandler.CP.GlobalTransform.Row1().ToVec3().Normalize();
    voxtool().CHandler.CP.GlobalTransform.Row2().ToVec3().Normalize();
}

Добавляем в конце функции Render():

    if (NeedResetPosition){
        // Если переменная NeedResetPosition равна true - присваиваем позиции манипулятора нулевые значения
        voxtool().CHandler.CP.Pos = Vector3D::Zero;
        // NeedResetPosition присваиваем, чтоб позиция манипулятора не сбрасывалась больше одного раза
        NeedResetPosition = false;
    }
    // Назначаем сохранение поворота осей манипулятора такое как задано параметром LocalSpace
    voxtool().CHandler.CP.LocalSpace = LocalSpace;

Результат после запуска 3DCoat:

tools-5.png

Создание языковых файлов

Файлы локализации в 3DCoat - обычные XML файлы, где каждый TextItem - отдельный объект перевода, тег ID в нем - ключевое слово, а Text - переведенный текст.

Для добавления своих ключевых слов добавляем следующий текст в файл Languages/English.xml:

    <TextItem>
    <ID>MyBoxTool</ID>
        <Text>My Box Tool</Text>
    </TextItem>
    <TextItem>
        <ID>MyBoxTool_HINT</ID>
        <Text>VoxelExtension created for tutorial</Text>
    </TextItem>
    <TextItem>
        <ID>BoxWidth</ID>
        <Text>Width</Text>
    </TextItem>
    <TextItem>
        <ID>BoxHeight</ID>
        <Text>Height</Text>
    </TextItem>
    <TextItem>
        <ID>BoxLength</ID>
        <Text>Length</Text>
    </TextItem>
    <TextItem>
        <ID>XPos</ID>
        <Text>X Pos</Text>
    </TextItem>
    <TextItem>
        <ID>YPos</ID>
        <Text>Y Pos</Text>
    </TextItem>
    <TextItem>
        <ID>ZPos</ID>
        <Text>Z Pos</Text>
    </TextItem>

Результат:

tools-6.png

Назначаем иконку нашему инструменту

Для назначения значка инструменту скопируем значок в формате png размером 128х128 в папку  textures/icons64/, после чего добавить в файл ui2.xml в список icons информацию с ключевым словом названия инструмента и названием файла:

 <IconicText>
  <ID>MyBoxTool</ID>
  <ToolName></ToolName>
  <Texture>MyBoxTutorial.png</Texture>
 </IconicText>  
tools-7.png

Работа с курсором

Этот фрагмент создаём для удобности работы с нашим кубом с помощью курсора и клавиш клавиатуры, а также быстрого размещения и масштабирования бокса. Для этого нам необходимо расширить программный код.

Итак, добавим в хидер следующий код:

// Переназначаем функцию AllowCubeHandler, разрешить выводить гизмо, если не выбрана опция MoveToBrush
    virtual bool AllowCubeHandler() {
        return !MoveToBrush;
    };
// Обработка нажатия клавиши
    virtual bool OnKey(char KeyCode);
    // Объявляем переменные для записи размеров куба
    float BoxWidth = 10.0f;
    float BoxHeight = 10.0f;
    float BoxLength = 10.0f;
    // Объявляем переменные для записи позиции куба
    float XPos = 0.0f;
    float YPos = 0.0f;
    float ZPos = 0.0f;
    bool MoveToBrush = false; // переменная, которая будет означать, что мы хотим ставить бокс на позицию курсора
    REG_MEMBER(_bool, MoveToBrush); // чекбокс, который будет означать, что мы хотим ставить бокс на позицию курсора

Изменения, которые коснулись файла cpp:

// Функция добавления бокса при нажатии кнопки "ентер"
bool MyBoxTutorialExt::OnKey(char KeyCode){
    // если нажата кнопка "ентер" то вызывается функция "Apply()",
    // которая добавляет наш бокс в текущую позицию с определенными параметрами
    if (KeyCode == VK_RETURN) {
        Apply();
        return true;
    }
    // если кнопка "ентер" не нажата функция не вызывается
    return false;
}
void MyBoxTutorialExt::Render(){
    // Устанавливаем бокс соответсвенно позициии курсора
    if (MoveToBrush && (Widgets::lPressed || Widgets::rPressed)){
        // Коефициент скейлинга для подгонки куба под размер кисти
        float scaling = GeneralPen::PenRadius / (BoxWidth + BoxHeight + BoxLength)*3.0;
        // Помещаем позицию курсора для удобства в переменную "pos"
        Vector3D pos = PMS.LastPickInfo.Pos;
        // Помещаем направление кисти в переменную "z"
        Vector3D z = SurfaceModificator::LastNormal;
        z.Normalize();
        // Вычислям ось "х"  подразумевая вертикальную ось "У"
        Vector3D x = Vector3D::Cross(Vector3D::AxisY, z);
        x.Normalize();
        // Вычисляем ось "У" по соотношению осей  "z" и "х"
        Vector3D y = Vector3D::Cross(z, x);
        // Присваеваем марицу трансформирования ссылке "m"
        Matrix4D& m = voxtool().CHandler.CP.GlobalTransform;
        // Изменяем размер матрицы так чтобы подогнать размер куба под размер курсора
        x *= scaling;
        y *= scaling;
        z *= scaling;
        // Записываем направление осей (поворот) в матрицу
        m.SetRow0(x.x, x.y, x.z, 0.0f);
        m.SetRow1(y.x, y.y, y.z, 0.0f);
        m.SetRow2(z.x, z.y, z.z, 0.0f);
        // Записываем позицию в в четвертую строку матрицы
        m.SetRow3(pos.x, pos.y, pos.z, 1.0f);
    }

Вот что у нас получилось:

tools-8.png

Поздравляем! Ты создал свой первый инструмент в 3DCoat)

Исходники

Исходники проекта можешь запросто скачать по этой ссылке.

See Also
Создание эффекта слоёв рисования
Настройка IDE для сборки проекта
Работа с UI 3DCoat