Уроки: Maya

Про particle collision event procedures.

Про particle collision event procedures.

 

В качестве примера использования particle collision event procedures сделаем всем уже приевшуюся, но от этого не менее интересную и актуальную поверхность жидкости, которая реагирует на падающие в нее партиклы расходящимися кругами. Один из способов ее создания описан на http://cade.scope.edu. Принцип там вкратце такой: на партикл коллижн цепляется создание еще одного партикла с типом рендеринга «спрайт», а спрайт этот представляет из себя… точно, расходящиеся круги. Затем все это дело рендерится через ортографическую камеру и полученная авишка или последовательность кадров цепляется в качестве бампа на материал поверхности. Но. Во-первых, способ этот годится только для заранее покэшенных партиклов, то есть при изменении их параметров все придется делать заново. Все это можно автоматизировать, но времени-то жалко. А во-вторых, способ хоть и элегантный, но достаточно примитивный ;). Мы пойдем другим путем! К партикл коллижну мы прицепим скрипт, который будет динамически создавать узел процедурной текстуры «water» и экспрешн, контролирующий ее атрибут «.rippleTime». Узлы, «отработавшие свое», в процессе выполнения процедуры будут удаляться.

 

Начнем? Начнем!

 

Подготовим «поле деятельности». В качестве источника наших партиклов («капель») вполне сойдет эмиттер с такими параметрами (Particles→Create Emitter→□):

 

 

В качестве поверхности используем polyPlane. Чтобы круги были круглыми, вид на поверхность сверху должен совпадать с видом в UV Texture Editor. При создании polyPlane для этого нужно установить параметр «Texture» - «Preserve Aspect Ratio»:

 

 

 

Располагаем это все так, чтобы получилось примерно следующее:

 

 

Выделяем в Outliner’е particleShape1 и переименовываем в rainDrops.

 

 

Дальше с Ctrl’ом выделяем в Outliner’е (или с Shift’ом в Perspective View) наш pPlane1 и делаем Particles→Make Collide (с параметрами по умолчанию, поскольку они нас вообще не волнуют).

 

 

Далее. Присваиваем нашей поверхности новый материал (для жидкости лучше всего anisotropic) и называем его  waterMat:

 

 

 

Теперь устанавливаем нашим каплям тип рендеринга «Blobby Surface (s/w)» и добавляем несколько динамических атрибутов: radiusPP, collisionU, collisionV:

 

 

 

 

Теперь создаем для radiusPP Creation Expression, который будет создавать капли случайного радиуса в пределах (0,3…0,7) единиц измерения:

 

 

 

 

А чтобы капли падали вниз, цепляем к ним Gravity Field с атрибутами по умолчанию.

 

Вроде подготовку закончили. А теперь самое интересное. Со все еще выделенными партиклами заходим в Particles→Particle Collision Events…

Ставим галочку возле «Original Particle Dies», а в поле «Event Procedure» пишем «rainDrops». Нажимаем «Create Event»:

 

 

rainDrops - это процедура, которая будет вызываться при падении партиклов на поверхность. Нету? Ща напишем! Жмем кнопочку «хелп» возле поля и видим такой текст:

The Event Procedure is a MEL script procedure that will be executed whenever any particle in the particle object that owns the event collides with an object. The procedure must have the following format and argument list:

global proc myEventProc(string $particleName, int $particleID, string $objectName)

where $particleName is the name of the particle object that owns the event, $particleID is the ID number of the particle in that object that has collided, and $objectName is the name of the object against which the particle collided.

 

Все понятно? Все понятно. Когда партикл из объекта $particleName попадает на поверхность $objectName, вызывается процедура myEventProc, которая должна содержать в качестве аргументов те самые $particleName, $objectName, а также порядковый номер (ID) партикла $particleID. Ну что ж, будем писать процедуру. В нашем случае, как мы помним, она называется «rainDrops».

Открываем какой-нибудь редактор скриптов (только не стандартный майевский Script Editor! :)) и пишем:

 

global proc rainDrops (string $dropsName, int $id, string $waterSurface) {

 

  global int $startFrames[];

 

Массив $startFrames[] будет содержать номера кадров, в которых создаются новые узлы «water» для того, чтобы знать, когда их потом удалять. Он объявлен как global, потому что будет нужен все время, а не только в единичном столкновении.

 

  float $posU[]=`particle -at collisionU -id $id $dropsName -q`;

  float $posV[]=`particle -at collisionV -id $id $dropsName -q`;

  float $radius[]=`particle -at radiusPP -id $id $dropsName -q`;

  float $time=`currentTime -q`;

  $startFrames[$id]= $time;

 

С помощью команды particle мы получаем UV-координаты места столкновения конкретного партикла с поверхностью и радиус оного партикла. Переменные $posU[],$posV[] и $radius[] объявлены как массивы из-за того, что команда «particle» (как, впрочем, и многие другие команды MEL) в любом случае возвращает именно массив данных, даже если он состоит всего из одного элемента. В дальнейшем мы будем использовать значения первых элементов этих массивов, которые идут под номером [0]. Команда `currentTime -q` возвращает номер кадра, в котором было зафиксировано столкновение партикла с поверхностью.

 

  if (!`objExists "waterBump"`) {

        createNode "bump2d" -n "waterBump";

  };

  if (!`objExists "waterPMA"`) {

        createNode "plusMinusAverage" -n "waterPMA";

  };

  if (!`isConnected waterPMA.output1D waterBump.bumpValue`) {

        connectAttr waterPMA.output1D waterBump.bumpValue;

  };

  if (!`isConnected waterBump.outNormal waterMat.normalCamera`) {

        connectAttr waterBump.outNormal waterMat.normalCamera;

  };

 

Эти конструкции проверяют наличие в сцене узлов «waterBump» и «waterPMA» и соединений между их атрибутами, если их еще нет, то создают и соединяют. Обычно это происходит во время первого столкновения (или если какой-то из этих узлов случайно попал под Delete :)). Вообще говоря, их наличие можно было бы и не проверять и создавать их каждый раз заново, независимо от того, существовали они раньше или нет. Но это приводит к появлению в feedback-строке сообщений типа «Warning: file: C:/Documents and Settings/Sasha/My Documents/maya/7.0/scripts/rainDrops.mel line 17: 'waterPMA.output1D' is already connected to 'waterBump.bumpValue'.» на отвратительном пурпурном фоне :) . Ничего страшного, однако неприятно.

 

Устанавливаем для узла «waterPMA» операцию «Average»:

 

  setAttr waterPMA.operation 3;

 

Почему не «Sum»? Можно и «Sum», тут уж кому как больше нравится :). Вообще с «Sum» оно и физически как-то, наверно, правильнее было бы.  Принцип суперпозиции там и прочие всякие интерференции :). Короче, если хотите «Sum», вместо 3 ставите 1, и все.

Дальше создаем собственно узел «water», опять же предварительно проверив, не осталось ли с прошлого прогона анимации узла с таким именем, после чего коннектим и устанавливаем соответствующие атрибуты (клавиша F1 расскажет вам о них намного больше, чем я).

 

  string $waterName="water"+$id;

 

  if (!`objExists $waterName`) {

        createNode "water" -n $waterName;

        connectAttr ($waterName+".outAlpha") waterPMA.input1D[$id];

  };

 

  setAttr ($waterName+".rippleOriginU") $posU[0];

  setAttr ($waterName+".rippleOriginV") $posV[0];

  setAttr ($waterName+".dropSize") $radius[0];

 

  setAttr ($waterName+".numberOfWaves") 0;

  setAttr ($waterName+".waveVelocity") 0;

  setAttr ($waterName+".waveAmplitude") 0;

  setAttr ($waterName+".waveFrequency") 0;

  setAttr ($waterName+".subWaveFrequency") 0;

  setAttr ($waterName+".smoothness") 0;

  setAttr ($waterName+".windU") 0;

 

  setAttr ($waterName+".rippleAmplitude") (`rand 1 3`);

  setAttr ($waterName+".rippleFrequency") (`rand 10 15`);

  setAttr ($waterName+".groupVelocity") (`rand 1 2`);

  setAttr ($waterName+".phaseVelocity") (`rand 5`);

  setAttr ($waterName+".spreadStart") (`rand 0.05`);

  setAttr ($waterName+".spreadRate") (`rand 0.5 1`);

 

По тому же принципу создаем экспрешн для анимации кругов:

 

  string $exprName="expression"+$id;

 

  if (!`objExists $exprName`) {

        expression -s ($waterName+".rippleTime=(frame-"+$time+")*"+(1/200.0)+";") -n $exprName;

  }

  else {

        expression -e -s ($waterName+".rippleTime=(frame-"+$time+")*"+(1/200.0)+";") $exprName;

  };

 

Если экспрешн с именем $exprName  уже существует, он просто корректируется (флаг –e) для соответствия новому значению $time. Число 200 определяет длительность (в кадрах) «работы» узла «water». Оно написано в виде 200.0 для того, чтобы при расчетах Maya понимала, что результат должен быть float, а не integer (иначе в результате получится не 0.05, а 0 и никаких расходящихся кругов не будет). А вообще-то лучше вместо конкретного числа ввести переменную (назовем ее $dropDuration), которую и вставить на его место.

Для удаления устаревших узлов «water» и соответствующих им экспрешнов организуем цикл по массиву $startFrames[]. Если «возраст» узла больше 200 кадров, он удаляется:

 

  int $i=0;

 

  for ($i=0;$i<`size $startFrames`;$i++) {

        if (($startFrames[$i]+200)<$startFrames[$id]) {

              if (`objExists ("water"+$i)`) {

                    delete ("water"+$i);

              };

              if (`objExists ("expression"+$i)`) {

                    delete ("expression"+$i);

              };

        };

 

  };

 

Тут опять используется число 200, и вместо него тоже можем вставить $dropDuration.

И последнее: если по окончании анимации заглянуть в Hypershade или в Outliner с выключенной галочкой «Show DAG objects only», мы увидим несколько (или не несколько :)) оставшихся узлов «water» и соответствующих им экспрешнов. Они остаются потому, что их «возраст» в конце анимации (а точнее, в момент последнего столкновения) меньше, чем значение $dropDuration. Их можно оставить (если их удалить, то все круги, которые оставались на «воде» к концу анимации, пропадут). Эти узлы никому не мешают и при последующих прогонах анимации все равно будут обновлены (или до них вообще дело не дойдет). Но если они уж сильно мозолят глаза, удалить их труда не составит. Для этого воспользуемся командой scriptJob. Она позволяет занести в память некую последовательность команд, которые следует выполнить при изменении какого-либо условия… довольно путано, но сейчас станет понятнее. Что мы хотим сделать? Нам нужно, чтобы при остановке анимации все лишние узлы удалялись. Поэтому в качестве условия мы устанавливаем «playingBack» с флагом «-cf», то есть «conditionFalse». Что сие значит. А то и значит, что при невыполнении условия «playingBack», то есть при остановке анимации, будет выполнена последовательность команд, приведенная в кавычках в качестве одного из аргументов команды scriptJob. Да, и еще. Каждый новый scriptJob добавляется в память и засоряет ее. Поэтому при каждом выполнении процедуры rainDrops мы будем перехватывать номер созданного scriptJob’а в переменную $jobNum и при следующем выполнении убивать его и создавать новый. Под номером 0 хранится один из встроенных майевских scriptJob’ов, у которого установлен флаг «permanent», то есть убить его мы не сможем при всем нашем желании, поэтому и пытаться не будем (а если попытаемся, то получим кучу матов в feedback-строке :)):

 

  global int $jobNum;

 

  if (`scriptJob -exists $jobNum`&&$jobNum!=0) {

        scriptJob -kill $jobNum;

  };

  $jobNum=`scriptJob -kws -ro 1 -cf "playingBack" "for ($i=0; $i<`size $startFrames`; $i++) {if (`objExists (\"water\"+$i)`) {delete (\"water\"+$i);}; if (`objExists (\"expression\"+$i)`) {delete (\"expression\"+$i);};};"`;

 

Вот и все. Полный текст процедуры:

 

global proc rainDrops (string $dropsName, int $id, string $waterSurface)

{

 

  global int $startFrames[];

  global int $jobNum;

 

  float $dropDuration=200;

 

  float $posU[]=`particle -at collisionU -id $id $dropsName -q`;

  float $posV[]=`particle -at collisionV -id $id $dropsName -q`;

  float $radius[]=`particle -at radiusPP -id $id $dropsName -q`;

  float $time=`currentTime -q`;

  $startFrames[$id]=$time;

 

  if (!`objExists "waterBump"`) {

        createNode "bump2d" -n "waterBump";

  };

  if (!`objExists "waterPMA"`) {

        createNode "plusMinusAverage" -n "waterPMA";

  };

  if (!`isConnected waterPMA.output1D waterBump.bumpValue`) {

        connectAttr waterPMA.output1D waterBump.bumpValue;

  };

  if (!`isConnected waterBump.outNormal waterMat.normalCamera`) {

        connectAttr waterBump.outNormal waterMat.normalCamera;

  };

 

  setAttr waterPMA.operation 3;

 

  string $waterName="water"+$id;

 

  if (!`objExists $waterName`) {

        createNode "water" -n $waterName;

        connectAttr ($waterName+".outAlpha") waterPMA.input1D[$id];

  };

 

  setAttr ($waterName+".rippleOriginU") $posU[0];

  setAttr ($waterName+".rippleOriginV") $posV[0];

  setAttr ($waterName+".dropSize") $radius[0];

 

  setAttr ($waterName+".numberOfWaves") 0;

  setAttr ($waterName+".waveVelocity") 0;

  setAttr ($waterName+".waveAmplitude") 0;

  setAttr ($waterName+".waveFrequency") 0;

  setAttr ($waterName+".subWaveFrequency") 0;

  setAttr ($waterName+".smoothness") 0;

  setAttr ($waterName+".windU") 0;

 

  setAttr ($waterName+".rippleAmplitude") (`rand 1 3`);

  setAttr ($waterName+".rippleFrequency") (`rand 10 15`);

  setAttr ($waterName+".groupVelocity") (`rand 1 2`);

  setAttr ($waterName+".phaseVelocity") (`rand 5`);

  setAttr ($waterName+".spreadStart") (`rand 0.05`);

  setAttr ($waterName+".spreadRate") (`rand 0.5 1`);

 

  string $exprName="expression"+$id;

 

  if (!`objExists $exprName`) {

        expression -s ($waterName+".rippleTime=(frame-"+$time+")*"+(1/$dropDuration)+";") -n $exprName;

  }

  else {

        expression -e -s ($waterName+".rippleTime=(frame-"+$time+")*"+(1/$dropDuration)+";") $exprName;

  };

 

  int $i=0;

 

  for ($i=0;$i<`size $startFrames`;$i++) {

        if (($startFrames[$i]+$dropDuration)<$startFrames[$id]) {

              if (`objExists ("water"+$i)`) {

                    delete ("water"+$i);

              };

              if (`objExists ("expression"+$i)`) {

                    delete ("expression"+$i);

              };

        };

  };

 

  if (`scriptJob -exists $jobNum`&&$jobNum!=0) {

        scriptJob -kill $jobNum;

  };

  $jobNum=`scriptJob -kws -ro 1 -cf "playingBack" "for ($i=0; $i<`size $startFrames`; $i++) {if (`objExists (\"water\"+$i)`) {delete (\"water\"+$i);}; if (`objExists (\"expression\"+$i)`) {delete (\"expression\"+$i);};};"`;

 

};

 

Сохраняем под именем rainDrops.mel в папку со скриптами (путь к которой можно посмотреть с помощью команды «internalVar –usd» или установить самому с помощью переменной MAYA_SCRIPT_PATH – читайте хелп). Рендерим анимацию. Смотрим. Если все правильно, то должно получиться что-то типа:

 

rippleAnim.mov

 

Ну и в заключение несколько слов о возможных вариантах. Во-первых, если UV-координаты поверхности все-таки покорежены, то для того, чтобы круги были круглыми, вместе с узлом «water» придется создавать projection с соответствующим 3d-placement’ом и располагать его в координатах, полученных с помощью все той же команды «particle», но уже с флагом «-at position», который вернет нам массив из трех элементов – угадайте каких :). Можно при этом еще и задать случайный (в разумных пределах) масштаб для создаваемых 3d-placement’ов (но желательно одинаковый по всем осям – опять же чтобы круги получались круглыми, а не овальными :)). При этом атрибутам узла «water» «.rippleOriginU» и «.rippleOriginV» следует присвоить значения 0.5, 0.5.

Во-вторых, если нужны корректные отражения и преломления, то выход узла «PlusMinusAverage» нужно цеплять не к бампу материала waterMat, а к дисплэйсменту шейдинг-группы, к которой он относится (примерно так сделана самая верхняя завлекательная картинка ;)).

В-третьих, можно увеличить резолюцию поверхности «воды», в месте каждого столкновения создавать wave-деформер и анимировать экспрешном его атрибуты таким образом, чтобы получались расходящиеся затухающие круги (хотя это, конечно, уже полный изврат :)).

Но это все мелочи, да и урок в общем-то не о том был… а так вроде бы все.

Спасибо за внимание.

 

PS. Читайте хелп, люди!!! Майский хелп выше всяческих похвал!

 

18986 Автор:
Актуальность: 221
Качество: 301
Суммарный балл: 522
Голосов: 32 оценки

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

аватар
 
mupik 2 0
хмм что не урок по maya, то сплошные скрипты :), а так красота
аватар
 
R.A.V.E.Design 94 0
Урок судя по анимашке очень крутой, но для меня - человека, который ничего непонимает в скриптах, этот урок полезен лишь наполовину (я скопирую всю писанину из этой странички напрямую в maya, но самостоятельно сделать вот также не смогу). Можно урок, который объясняет что такое скрипт и как его надо писать, а не про то, как автор (который знает скрипты) делает что-то скриптами?
Ничего плохого про урок сказать нехочу, скачал анимашку, посмотрел и мне понравилось!
аватар
 
fastfoot 11 0
Только начал разбираться с MEL, expressions.
По поводу-(Открываем какой-нибудь редактор скриптов (только не стандартный майевский Script Editor!)
вопрос-почему не стандартный и каким например воспользоваться
аватар
 
michael-tv 7 0
Очень серьезный и качественный урок.
Жаль народ плохо голосует за такие вещи...
Пугаются скриптов судя по всему. А скриптов пугаться не надо.
аватар
 
DrJones 6 0
Спасибо за хорошие отзывы :)
аватар
 
maxer 7 0
Трудно разобраться. Но думаю, результат себя оправдает.
аватар
 
scripterBB 11 0
Кое-что интересное.
аватар
 
crest 7 0
Человек работал - виден результат.
аватар
 
rendermax80 -8 0
Узнал кое-что новое. Спасибо.
аватар
 
chur 2 0
Последней картинки не видать %(
аватар
 
chur 2 0
Очень хороший и полезный урок. Автор, пиши еще %)
аватар
 
Lamez 61 0
Kian Bee Ng проще.
Зарегистрируйтесь, чтобы добавить комментарий.
Эту страницу просмотрели: * уникальных посетителей