Before/After Runtime Expressions

Довольно долгое время различия между runtimeBeforeDynamics и runtimeAfterDynamics партикловыми экспрешенами оставались для меня делом довольно смутным. Ну т.е. из названий в принципе все понятно – первые исполняются до динамики, а вторые после… Но что, черт возьми, на практике это означает и зачем оно надо, как можно полезно применить?! 🙂

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

В целом, при переходе на очередной фрейм происходит следующее:
– исполняются непартикловые экспрешены и прочая анимация в сцене;
– для каждой уже существующей частицы исполняется runtimeBeforeDynamics экспрешен;
– вся динамика в сцене, включающая генерацию новых частиц эмиттерами, влияние полей, goal, коллизии и т.п.;
runtimeAfterDynamics;

Таким образом, частицы, рожденные до или во время этапа динамики, подвергнутся ее влиянию в том же фрейме. Т.е. если мы создаем частицу обычным эмиттером или командой emit в непартикловом или runtimeBeforeDynamics экспрешене – она появится сразу в позиции после обработки динамикой. Если же команда emit исполняется в runtimeAfterDynamics – частица появится строго в указанной позиции и только со следующего фрейма подвергнется действию динамики.

Предположим, в сцене существующая частица (id #0), которая в каждом фрейме рождает частицу в координате <<1, 0, 0>>, и на все это действует uniform field, направленный вверх:

if( particleId == 0 ) {
		emit -o particle1 -pos 1 0 0;
}

Если этот экспрешен находится в runtimeBeforeDynamics, в каждом фрейме рождается новая частица, затем она обрабатывается uniform field, затем нам выдается результат – частица появляются не в указанной <<1, 0, 0>> координате, а с учетом действия силы – чуть выше. Если поместить команду в runtimeAfterDynamics – каждая новая частица появляется без обработки силой строго в указанной <<1, 0, 0>> координате (только на следующий фрейм попадет под ее воздействие):

Difference between before and after runtime expressions

Difference between before and after runtime expressions

Во многих случаях эта разница несущественна, но при определенных условиях или для особых задач может иметь значение. К примеру:

1) rigidBody объект быстро падает вниз, из каких-то точек на его поверхности генерятся частицы, устремляющиеся вверх – при генерации их в runtimeBeforeDynamics получающийся шлейф постоянно будет отставать от точки на поверхности, так как частица родилась и сразу под действием ссилы сдвинулась вверх, а объект будучи rigidBody – вниз. При runtimeAfterDynamics калькуляции и эмиттинг произойдут после динамики rigidBody и влияния сил в этом фрейме – шлейф будет четко из точки на поверхности;

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

if( collidingParticles.collisionTime > 0 ) {
	float $collPos[] = collidingParticles.collisionPosition;
	float $collUV[] = { collidingParticles.collisionU,
		collidingParticles.collisionV };

	emit -o stickingParticles -pos $collPos[0] $collPos[1] $collPos[2]
		-at "goalU" -fv $collUV[0]
		-at "goalV" -fv $collUV[1];
}

Так как коллижены просчитываются как раз между runtimeBeforeDynamics и runtimeAfterDynamics, ранее всего мы можем воспользоваться результатом именно в последнем – тогда в том же фрейме, в котором произошла коллизия, в указанной точке появятся новые частицы, привязанные к ней через goal. Если же экспрешен воткнуть в runtimeBeforeDynamics, эти частицы появятся лишь на следующий фрейм (значения коллизии пересчитаются уже после runtimeBeforeDynamics, так что частицы прилипнут все равно в те же самые места). Все не так плохо, если мы не используем какие-нить анимированные данные из сцены при генерации этих частиц – допустим, позиция упомянутого анимированного объекта по Y определяет прицеплять ли частицы:

if( collidingParticles.collisionTime > 0 ) {
	float $sphPos[] = `pointPosition -w pSphere1.rotatePivot`;
	if( $sphPos[1] < 10 ) {
		float $collPos[] = collidingParticles.collisionPosition;
		float $collUV[] = { collidingParticles.collisionU,
			collidingParticles.collisionV };
		emit -o stickingParticles -pos $collPos[0] $collPos[1] $collPos[2]
			-at "goalU" -fv $collUV[0]
			-at "goalV" -fv $collUV[1];
	}
}

Так как перемещение объекта происходит после runtimeAfterDynamics, но до runtimeBeforeDynamics один фрейм прилепления частиц будет потерян при использовании runtimeBeforeDynamics в данном случае. В общем, все различия тут в нюансах в какой последовательности какие данные просчитываются:

Particle generation on collision with before and after runtime expressions

Particle generation on collision with before and after runtime expressions

Теперь упомянутый в самом начале пример – пуля не просто сталкивается с объектом, оставляя в точке коллижена дырку, но продолжает неизменно лететь насквозь, оставляя выходное отверстие с другой стороны. Можно сделать это следующим образом – так как коллизии просчитываются между runtimeBeforeDynamics и runtimeAfterDynamics, в первом мы имеем данные о частице до коллижена, во втором – после коллижена. Сохранив позицию до динамики в кастомный атрибут (beforePosition), используем ее для вычисления где частица должна была быть, если бы не коллайдилась и перемещаем ее туда после динамики, а данные от коллизии используем как хотим:

runtimeBeforeDynamics:

beforePosition = position;

runtimeAfterDynamics:

float $beforePos[] = beforePosition;
vector $accel = acceleration;
float $fps = `currentTimeUnitToFPS`;
float $noColl[] = $accel / pow($fps, 2);

if( collisionTime > 0 ) {
		position = <<($beforePos[0]+$noColl[0]), ($beforePos[1]+$noColl[1]),
			($beforePos[2]+$noColl[2])>>;
}

В данном случае можно поступить даже проще, воспользовавшись специальным атрибутом lastPosition, который возвращает значение position в предпоследней калькуляции и как результат может заменить необходимость в кастомном атрибуте beforePosition и runtimeBeforeDynamics экспрешене:

runtimeAfterDynamics:

float $beforePos[] = lastPosition;
vector $accel = acceleration;
float $fps = `currentTimeUnitToFPS`;
float $noColl[] = $accel / pow($fps, 2);

if( collisionTime > 0 ) {
		position = <<($beforePos[0]+$noColl[0]), ($beforePos[1]+$noColl[1]),
			($beforePos[2]+$noColl[2])>>;
}

Искомый результат получен – коллижен происходит, все необходимые collision* атрибуты калькулируются, но частица не изменяет своей траектории.

В дополнение хочу еще упомянуть про world* атрибуты (в частности worldPosition). По моим наблюдениям, по дефолту они калькулируются один раз в конце просчетов – после runtimeAfterDynamics, но если к партиклам подключены коллижен-объекты, происходит калькуляция после этапа применения динамики, причем без влияния коллижена 🙂 Разумное объяснение сему явлению дать не могу, но применить:

runtimeAfterDynamics:

if( collisionTime > 0 ) {
	position = worldPosition;
}

Прилагаю сцену с сетапом пули, проходящей куб насквозь:

Example scene with bullet penetrating a cube

example_scene

Некоторые примеры возможно не самые интересные и показательные, но надеюсь что мне удалось в той или иной степени разложить по полочкам тему различия before/after runtime экспрешенов 🙂

2 Responses

Subscribe to comments via RSS

  1. Written by Ruben
    on 4 July 2009 at 1:14
    Permalink

    Hey these tuts are amazing, but could you make some tutorials for beginners? just getting into scrpting got particles

  2. Written by Sagroth
    on 4 July 2009 at 3:25
    Permalink

    Thanks.
    I guess I can, but I think there’re a lot of good tutorials on particle expressions for beginners already. And they are pretty boring to write also 🙂

Subscribe to comments via RSS

Leave a Reply