Создание кастомного локатора

Внимание! Данной статья не является туториалом, а служит для передачи моего опыта и наработками.

В этой статье я хочу поделиться тем опытом, который приобрел при написании плагина на C++ для Maya.Задача заключалась в следующим. Написать плагин, с помощью которого можно будет создавать локаторы(маркеры) с определенным именем на месте выделенных вершин.Смысл подобных действий следующий: Выделяя вершины на модели персонажа я расставляю маркеры. Для рук с одним именем, для ног с другим и т.д. После автоматически создается почти завершенная оснастка .Хоть тема и называется «Создание кастомного локатора», о самом локаторе будет говориться немного, так как я брал за основу пример из набора разработчика. Некоторое моменты, такие как создание заглавного файла .h или файла main.cpp я упущу, так как предпологаю что у читателя есть минимальная база знаний в создании плагинов на С++ для Maya. Так же я не буду описывать классы и функции которые применял в коде. Об этом есть подробная документация на сайте Autodesk.Сама статья впервую очередь расчитана на новичков, кто только начал знакомиться с Maya API и хочет получить ответы на банальные вопросы.

Содержание:

  1. Создание локатора.

  2. Выделение вершин, получение их координат.

  3. Нахождение координат центра всех выделенных вершин.

  4. Переименование узлов transform и shape.

  5. Установка иконки для локатора.

  6. Перемещение локатора на заданную позицию.

  7. Создание диологового окна.

1. Создание локатора

Я создал свой локатор взяв за основу пример FootPrintNode из набора разработчика для Maya. Моя версия локатора выглядит следующим образом:

В оригинале локатор FootPrintNode состоит из двух частей и каждая часть имеет сплошную заливку. Так же каждая часть имеет замкнутую структуру, то есть начинается с одних координат и заканчивается теми же координатами.Для создания пустотелого локатора я каждую линию отрисовал отдельно. А так же удалил одну из ненужных частей. Каждая линия имеет три позиции координат и так же замкнутую структуру.

static double fLocator_0[][3] = { { 0.000000f, 1.000000f, 0.000000f }, 
                                  { 0.000000f, 0.866025f, 0.500000f }, 
                                  { 0.000000f, 1.000000f, 0.000000f } };

В моем локаторе 36 линий, от 0 до 35.

По сути, это одно из самых больших изменений которые я сделал в коде оригинального файла.Следующее что я сделал - разделил оригинальный фаил на три части: main.cpp где находится код инициальзации и деинициализации плагина, .cpp где находится основной код и .h заглавный фаил. Это нужно для того что бы код(локатор) на выходе, при компиляции получился не одним плагином, а был только частью плагина.

2. Выделение вертексов, получение их координат

Сначала объявляем целочислительные переменные для подсчета полигональных объектов и вершин выделяемых на этих объектах.

	int vertCount = 0, vertIndex = 0;

Далее объявляем массивы для хранения координат, как оснавные так и временные.

	MDoubleArray allX; allX.clear();
	MDoubleArray allY; allY.clear();
	MDoubleArray allZ; allZ.clear();

	MDoubleArray oneBuferX; oneBuferX.clear();
	MDoubleArray twoBuferX; twoBuferX.clear();
	MDoubleArray tempAllX; tempAllX.clear();

	MDoubleArray oneBuferY; oneBuferY.clear();
	MDoubleArray twoBuferY; twoBuferY.clear();
	MDoubleArray tempAllY; tempAllY.clear();

	MDoubleArray oneBuferZ; oneBuferZ.clear();
	MDoubleArray twoBuferZ; twoBuferZ.clear();
	MDoubleArray tempAllZ; tempAllZ.clear();

	MString txt;
	MPoint pt;
	MPointArray posArray; posArray.clear();

Обратите внимание на то, что я использую тим массивов MDoubleArray, а не double. Немного ниже я расскажу почему.

Для выделения вертексов я использовал следующий код:

	MDagPath dagPath;
	MObject component;

	MSelectionList selection;
	MGlobal::getActiveSelectionList(selection);
	MItSelectionList iter(selection, MFn::kMeshVertComponent);

	iter.getDagPath(dagPath, component);

	if (component.isNull()) {
		displayError("Nothing is selected or no vertex is selected");
		return MS::kFailure;
	}

	for (; !iter.isDone(); iter.next())
	{
		iter.getDagPath(dagPath, component);
		MItMeshVertex meshIter(dagPath, component, &status);

		if (status == MS::kSuccess)
		{
			vertCount += meshIter.count();
			                              
			for (; !meshIter.isDone(); meshIter.next())
			{
				pt = meshIter.position(MSpace::kWorld);
				posArray.append(MPoint(pt.x, pt.y, pt.z));
		
			}
		}
	}

Сначала я объявил MDagPath, MObject и MSelectionList. Имена dagPathcomponent и selection могут быть совершенно любыми.

	MDagPath dagPath;
	MObject component;

	MSelectionList selection;

getActiveSelectionList означает что в selection будет находится список выбранных объектов.

    MGlobal::getActiveSelectionList(selection); 

iter имеет функцию фильтра, в моем случае на данном этапе он отфильтровывает вершины от других типов компоентов и записывает их в selection.

   MItSelectionList iter(selection, MFn::kMeshVertComponent); 

Этого можно было бы и не делать и записать как:

   MItSelectionList iter(selection);  

Но в этом случае в selection записывальсь бы все типы компонентов и отфильтровывались бы уже в следующем цикле. Но для меня было важно присутствие следующего условия.

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

	iter.getDagPath(dagPath, component);

	if (component.isNull()) {
		displayError("Nothing is selected or no vertex is selected");
		return MS::kFailure;
	}

Далее идет цикл, который подсчитает количество выделенных вершин на полигонаьных объектах.

Первая часть работает согласно количеству объектов на которых выделены вершины. Может быть как один объект так и несколько. Если мы не фильтруем компоненты ранне, то с помощью класса MItMeshVertex отфильтруем на данном этапе. Но так как у меня в component записываются именно вершины, то строка MItMeshVertex meshIter(dagPath, component, &status); служит для записи из component в meshIter но в любом случае только вершин.

	for (; !iter.isDone(); iter.next())
	{
		iter.getDagPath(dagPath, component);
		MItMeshVertex meshIter(dagPath, component, &status);

		if (status == MS::kSuccess)
		{

Обратите внимание на += в строке vertCount += meshIter.count();. Это необходимо для того что бы вершины выделенные с разных объектах ссумировались.

    vertCount += meshIter.count();

И последний цикл служит для получения координат с помощью MPoint pt. В pt содержатся координаты XY и Z. Затем координаты перезаписываем в массив типа MPointArray.

			vertCount += meshIter.count();
			
			for (; !meshIter.isDone(); meshIter.next())
			{
				pt = meshIter.position(MSpace::kWorld);
				posArray.append(MPoint(pt.x, pt.y, pt.z));

				
			}
		}
	}

Теперь идет условие которое выдает предупреждение если выделено менее двух вершин. Если же выделено две и более вершины выполняется код где создается локатор, перемещается на позицию, а также выполняются другие действия.

	if (vertCount <= 1)
	{
	 MGlobal::displayWarning(MString(": ") + "Less than two vertices are selected!");
	 return MS::kFailure;
	}

3. Нахождение координат центра всех выделенных вершин

Теперь мы подошли к самой интересной части. Нахождение координат некого центра всех выделенных вершин. Полный код достаточно большой и поэтому сдесь будет только часть его. Этого будет достаточно что бы понять как работает весь код для поиска центра вершин.

Небольшое отступление.

Зададим вопрос, как нам найти этот самый центр? Ответ на самом деле прост, но для начала попробуйте в Maya выделить несколько вершин и создать cluster. Произошел некий расчет и cluster поместился как бы в центр выделенных вершин. Далее создайте полигональный куб и растяните его вершины в стороны так, что бы он изменил свою первоночальную форму. Теперь для куба создайте ограничивающую рамку(boundingBox). Так же выделите вершины куба и создайте cluster. Вы увидите что центр ограничивающей рамки и позиция кластера одинаковы. Эти экспиременты дают понимание почему cluster помещается туда куда помещается и что нам делать что бы наш локатор поместился в эту же позицию.Тоесть, нам нужно найти максимальные и минимальные величины оси X, оси Y и Z. После, найти их средние числа которые и будут координатами для нашего локатора.

На рисунке ниже пример создания кластера и ограничивающей рамки.

Как переместить локатор мы разобрались, теперь нужно это реализовать в C++ для Maya. Здесь я столкнулся с некоторыми трудностями.

Что бы найти максимальное и минимальное число можно просто использовать сортировку массива, после которой мы получим нужные значения. НО! Все те способы сортировки которые я нашел работают исключительно со статическими массивами типа double, размер которых указан явно. Такой способ никак не подходит для нас, потому как выделенных вершин всегда будет случайное количество. Если записать координаты в массив типа MDoubleArray то сортировка в том виде в каком она есть в C++ с этим типом массивов не работает. В итоге появляется банальный вопрос, как отсортировать данные? Мой решение следующее:

Сначала из MPointArray posArray переписываем координаты XY и Z в массивы allXallY и allZ типа MDoubleArray. Этот тип массивов является динамическим.

		for (int inArr = 0; inArr < vertCount; inArr++)
		{
			allX.append(posArray[inArr].x);
			allY.append(posArray[inArr].y);
			allZ.append(posArray[inArr].z);

		}

Далее основной массив копируем в один из временных массивов.

    oneBuferX.copy(allX);

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

Вместо написания полноценной сортировки я ограничелся поиском максимального и минимальноо числа. И это я сделал путем обычного сравнения чисел. В коде который я привел ниже, находится минимальное число в массиве AllX.

		int minus = 0;

        for (int a = 0; a < 1 + minus; a++)
		{
			int nm = oneBuferX.length();

			if (nm != 1)
			{
				for (int i = 0; i < (vertCount - 1) - minus; i++)
				{
					if (oneBuferX[i] < oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i]);
					}
					if (oneBuferX[i] > oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i + 1]);
					}
					if (oneBuferX[i] == oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i + 1]);
					}
				}
				minus += 1;
				oneBuferX.clear();
				oneBuferX.copy(twoBuferX);
				twoBuferX.clear();
			}
			if (nm == 1)
			{
				break;
			}
		}
		minus = 0;

		tempAllX.append(oneBuferX[0]);

На примере я покажу принцип его действия.

Есть некий массив : | -0.15 | 0.3 | 1.2 | -0.08 |. Первый цикл for изначально настроен так что бы сработать один раз.

		int minus = 0;

		for (int a = 0; a < 1 + minus; a++)
		{

Далее, записываем в переменную nm длинну массива.

			int nm = oneBuferX.length();

Если длинна масива не равна единице то срабатывает следующий цикл for который сравнивает числа таким образом | -0.15 и 0.3 |(-0.15), | 0.3 и 1.2 |(0.3), | 1.2 и -0.08 |(-0.08).

			if (nm != 1)
			{
				for (int i = 0; i < (vertCount - 1) - minus; i++)
				{
					if (oneBuferX[i] < oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i]);
					}
					if (oneBuferX[i] > oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i + 1]);
					}
					if (oneBuferX[i] == oneBuferX[i + 1])
					{
						twoBuferX.append(oneBuferX[i + 1]);
					}
				}

Цикл for завершается и к переменной minus добавляется единица для того, что бы первый цикл сработал еще раз. И он будет срабатывать до тех пор пока размер массива не будет равен единице.

				minus += 1;

Чистится массив oneBuferX, содержимое массива twoBuferX копируется в oneBuferX. Чистится массив twoBuferX.

				oneBuferX.clear();
				oneBuferX.copy(twoBuferX);
				twoBuferX.clear();

После первого прохода основного цикла наш массив приобретет вид | -0.15 | 0.3 | -0.08 |. После второго прохода | -0.15 | -0.08 |. На третьем проходе в массиве останется только число | -0.08 |. Но первый цикл for еще сработает один раз, так как к переменной minus снова добавилась единица. Только на этот раз сработает условие

			if (nm == 1)
			{
				break;
			}

И цикл завершится.

В итоге мы получим минимальное число по оси X | -0.15 |, которое будет записано в массив tempAllX.

		tempAllX.append(oneBuferX[0]);

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

		double mxX = tempAllX[1];
		double mnX = tempAllX[0];

		double mxY = tempAllY[1];
		double mnY = tempAllY[0];

		double mxZ = tempAllZ[1];
		double mnZ = tempAllZ[0];

После несложных расчетов получим средние значения осей XY и Z.

		double averageX = (mxX + mnX) / 2;
		double averageY = (mxY + mnY) / 2;
		double averageZ = (mxZ + mnZ) / 2;

4. Переименование узла transform и Shape.

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

Окно со списком имен:

Следующий код позволяет нам запросить имя из диологового окна.

		MCommandResult result;
		MStringArray isString;
		MString melCommand = "textScrollList - q - selectItem FUCMainTScrolList";
		MGlobal::executeCommand(melCommand, result, false, false);
		result.getResult(isString);

Далее, узнаем количество одинаковых объектов в сцене.

		MSelectionList allLocName;
		MGlobal::clearSelectionList();
		MGlobal::selectByName(isString[0] + "*");
		MGlobal::getActiveSelectionList(allLocName);
		MGlobal::clearSelectionList();

		MDagPath dagSelLocPath;
		MFnTransform transform;
		MString checkName;
		MStringArray nameArr;

		MItSelectionList iter(allLocName, MFn::kTransform);

		for (; !iter.isDone(); iter.next())
		{
			iter.getDagPath(dagSelLocPath);
			transform.setObject(dagSelLocPath);
			checkName = transform.name();

			nameArr.append(checkName);

		}
		if (nameArr.length() > 0)
		{
			count = nameArr.length() + 1;
		}

Код по сути не сложный. Сначала сбрасываем выделение с помощью clearSelectionList, затем выделяем объекты только с определенным именем selectByName(isString[0] + "*"), записываем их в allLocName и опять сбрасываем выделение.

Но здесь есть один нюанс. Выделятся как transform так и Shape. Что бы отделить transform от Shape мы используем фильтр MFn::kTransform. И полученный результат запишем в массив nameArr с помощью цикла for.

Затем у нас следует условие, при котором если размер массива nameArr больше нуля, к переменной добавляется единица count. Это означает что в сцене уже есть локаторы с подобными именами и номер нового будет на единицу больше. Если же похожих имен нет, то условие не сработает и новый объект будет с тем намером, которое задано по умолчанию переменной count.

Далее просто формируем имя в одну переменную для удобства.

		MString name = MString(isString[0]) + count;

Здесь стоит заметить, что если записать таким образом: MString name = isString[0] + count, то целочислительное значение не приплюсуется к строке и мы просто получим имя без номера.

Теперь можно создавать локатор в сцене.

		MObject locatorObj = MObject::kNullObj;
	
		locatorObj = dagMod.createNode(0x80007, MObject::kNullObj, &status);

		status = dagMod.doIt();

На что здесь нужно обратить внимание? На MObject locatorObj и MDagModifier dagMod.

Сначала мы объявляем класс MObject для того что бы в locatorObj был записан тот объект который мы создаем. А с помощью класса MDagModifier мы регистрируем в dagMod само создание локатора. Это обязательно для операции отмены и повтора(undoIt и redoIt). После мы создаем локатор status = dagMod.doIt(). Так же, вместо строкового имени локатора используется его id(0x80007), но это не принципиально важно. Можно использовать и имя.

Теперь можно мереименовать узел transform локатора:

    MFnDagNode fnLocator(locatorObj);    
    fnLocator.setName(name);

Что бы переименовать Shape зделаем следующее:

    MString fNodeName = "fLocator1";     
    MObject nodeShape = getObjFromName(fNodeName, status);

    MFnDagNode fnShape(nodeShape);     
    fnShape.setName(MString(isString[0]) + "Shape" + count);

fLocator1 это имя локатора по умолчанию, мы записываем его в переменную fNodeName.

Обратите внимание. Что если создать локатор в редакторе узлов, то узел transform будет иметь имя transform1, а у Shape имя fLocator1, то которое мы задали при инициализации плагина fLocator плюс единица.

С помощью функции getObjFromName мы находим объект с указанным именем - fLocator1. и записываем его в nodeShape. Затем переименовываем: fnShape.setName(MString(isString[0]) + "Shape" + count).

Результат переименования на изображении ниже:

5. Установка иконки для локатора

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

 fnShape.setIcon("C:/path/iconName.png");

Стоит заметить, что задаем иконку только узлу Shape, потому что только иконка этого узла а не transform отображается в Outliner.

6. Перемещение локатора на заданную позицию.

Как установить иконку мы разобрались. Теперь изменим цвет локатора и переместим его на нужную позицию с помощью класса MPlug.

    MPlug setAttrPlug;

    setAttrPlug = fnLocator.findPlug("overrideEnabled", true);     
    setAttrPlug.setBool(1);

    setAttrPlug = fnLocator.findPlug("overrideRGBColors", true);     
    setAttrPlug.setBool(1);

    setAttrPlug = fnLocator.findPlug("overrideColorR", true);     
    setAttrPlug.setDouble(1);

    setAttrPlug = fnLocator.findPlug("overrideColorG", true);      
    setAttrPlug.setDouble(1);

    setAttrPlug = fnLocator.findPlug("overrideColorB", true);     
    setAttrPlug.setDouble(0);

Смысл кода выше в том, что мы просто редактируем необходимые атрибуты.


Переместить локатор тоже можно либо отредактировав атребуты translateXtranslateY и translateZ, как в первом варианте, либо с помощью MFnTransform как во втором.

Вариант 1.

   setAttrPlug = fnLocator.findPlug("translateX", true);    
   setAttrPlug.setDouble(averageX);

   setAttrPlug = fnLocator.findPlug("translateY", true);    
   setAttrPlug.setDouble(averageY);

   setAttrPlug = fnLocator.findPlug("translateZ", true);    
   setAttrPlug.setDouble(averageZ);

Вариант 2.

   MSelectionList selectionLoc;    
   selectionLoc.add(name);

   MDagPath dagLocPath;    
   MFnTransform transformFn;    
   selectionLoc.getDagPath(0, dagLocPath);

   transformFn.setObject(dagLocPath);    
   transformFn.setTranslation(MVector(averageX, averageY, averageZ), MSpace::kWorld);

7. Создание диалогового окна

Создание окна происходит следующим образом. Сначала объевляем переменную MString.

   MString cmd;

В переменной cmd будут хранится все строки из которых состоит окно. Затем отдельно объявлям с помощью MString те строки которые должны быть заключены в ковычки.

   MString Command = "fnyCreateCmd";     
   MString winName = "FunnyMarkerCreate";

В MEL, команды, процедуры, названия окон и кнопок и т.п. все эти строки пишуться в ковычках. В С++ в ковычки заключена вся строка и поэтому подобные вещи необъодимо объявить отдельно.

cmd += MString("$FUCWindow = `window -t " + winName);

MString btLabel = "Create_Marker";
cmd += MString(("string $FUCMainButton = `button -l ") 
                                      + btLabel + (" - c") + Command 
                                      + (" - p $FUCMainColumnLay FUCMainButton`;"));

Знаки += обозначают что сколько бы небыло строк, все они будут ссумироваться.

Обратите внимание что строка "Create_Marker" записана с подчеркиванием. Если ее записать с использованием пробела "Create Marker" то Maya будет думать что это две различных друк от друга строки, а не одно целое.

В завершении используем executeCommand.

 MGlobal::executeCommand(cmd);

Если вы хотите использовать сложные по структуре окна, то их лучше создавать отдельно например в MEL и потом по имени процедуры в которую заключено окно вызывать в С++.

На этом все ^^

Комментарии (1)


  1. patyupin
    21.12.2022 02:38
    -3

    Кстати у нашей компании есть вакансия для с++ разработчика. Работа удаленная. Знание английского и линукса обязательно. Если интересно, шлите запросы мне на почту raymond@dcmsys.com