Снеговик

Введение

В уроке из частиц создаются снеговик и другие фигуры. Для получения результата использованы система частиц PF Source (поток частиц) и язык программирования MAXSсript.
Системы частиц позволяют моделировать явления реального мира и воплощать разного рода вымышленные образы.
Так, многие физики полагают, что в основе мироздания положены частицы. Ученые ставят опыты, ускоряя и сталкивая частицы, надеясь подтвердить свои теории. Системы частиц, в том числе и 3ds Max, вполне пригодны для отображения наблюдаемых физиками процессов (рис. 1).

Рис. 1. Бозон – частица Бога? (источник Яндекс. Картинки: бозон)

Весьма эффективно могут быть представлены частицами и работы из бисера (рис. 2).

Рис. 2. Мы в ответе за тех, кого приручаем (источник Яндекс. Картинки: бисероплетение)

MAXSсript не только предоставляет дополнительные возможности при работе с системами частиц, но и укоряет процесс получения результата. Кроме того, код компактно описывает моделируемую сцену, что весьма часто позволяет оценить генерируемые им образы, не имея под рукой 3ds Max.
Эти и иные преимущества будут ощутимы при наличии определенных навыков и достаточно продолжительной тренировки.
В качестве иллюстрации тезиса о компактности представления модели приведем код, обеспечивающей решение задачи, разобранной в размещенном на Render.ru уроке "Текст из частиц" от Shoohrat Yuldasheva.
Задача решается в следующей формулировке:

  1. Обеспечить генерацию цилиндрических частиц из ребер полигонального объекта.
  2. В качестве объекта употребить текст с модификатором Extrude (выдавить).
  3. Выполнить анимацию масштабирования частиц.

Получение результата обеспечивает следующий код:

-- Block 1
delete $*
animationRange = interval 0f 100f
ng = 10.0
t = text text:"Some T" size:100 rotation:((eulerAngles -ng ng 0) as quat)
addModifier t (extrude amount:0.5)
cl = cylinder radius:1 height:40
hide #(t, cl)
-- Block 2
pF = PF_Source enable_Particles:true quantity_Viewport:100 \
 rotation:((eulerAngles -ng (180 - ng) 0) as quat) isSelected:on
particleFlow.BeginEdit()
opBth = birth emit_Start:0 emit_Stop:0 type:0 amount:600
opPO = position_Object emitter_Objects:#(t) delete:on location:2 \
 apart_Placement:on apart_Distance:8
opSI = shape_Instance shape_Object:cl
opSP = scaleParticles type:2 constrain_Scale:off
opRt = rotation direction:2 euler_X:ng euler_Y:-ng
opRP = renderParticles type:2
opDP = displayParticles color:red type:6
evn = event()
particleFlow.EndEdit()
evn.AppendAction opBth
evn.AppendAction opPO
evn.AppendAction opSI
evn.AppendAction opSP
evn.AppendAction opRt
evn.AppendAction opDP
pF.AppendAction opRP
pF.AppendInitialActionList evn
-- Block 3
with redraw off (
 max tool animmode; set animate on
 sliderTime = 0f; opSP.Z_Scale_Factor = 0
 sliderTime = 50f; opSP.Z_Scale_Factor = 100
 sliderTime = 100f; opSP.Z_Scale_Factor = 50
 max tool animmode; set animate off
 sliderTime = 0f
)
max modify mode
max tool zoomExtents
playAnimation()

Последний кадр анимации приведен на рис. 3.

Рис. 3. Частицы на ребрах текста

В основной программе три блока.
Первый блок подготовительный. Он очищает сцену, создает Mesh в виде выдавленного текста t и цилиндра cl, форма которого будет затем использована для отображения частиц.
Второй блок отвечает за создание источника частиц. Источник содержит одно событие с 6-ю операторами (рис. 4).

Рис. 4. Источник частиц в задаче Текст из частиц

Чтобы открыть диалог Particle View (обозреватель систем частиц) и просмотреть состав источника, достаточно выбрать источник частиц PF Source, перейти на вкладку Modify командного окна и нажать на кнопку Particle View группы Setup сопутствующего диалога. Выбор источника и переход на вкладку Modify предусмотрены в приведенном выше коде (свойство источника IsSelected = on и выполнена команда max modify mode).
Созданный источник pF (конструктор PF_Source) размещен в начале координат и повернут (свойство Rotation) относительно осей X и Y соответственно на 10° и 170°. Причем относительно оси X поворот выполнен по часовой стрелке, относительно оси Y – против часовой стрелки.
Оператор Birth обеспечивает рождение всех 600 частиц (свойство Amount = 600) в нулевой момент времени (Emit_Start = Emit_Stop = 0).
Оператор Position_Object указывает на то, что частицы будут рождаться на ребрах выдавленного текста (Emitter_Objects = #(t), Location = 2). Кроме того, при размещении частиц на ребрах объекта буде приниматься во внимание значение свойства Apart_Distance, регулирующее расстояние между частицами.
Оператор Shape_Instance устанавливает объект (цилиндр cl), форму которого будут иметь частицы.
Оператор ScaleParticles отвечает за масштабирование частиц (выполняется анимация масштабирования по оси Z).
Оператор Rotation вызывает поворот всех частиц относительно осей X и Y соответственно на 10° и -10°. Этот поворот обеспечит перпендикулярность частиц тексту, используемого в качестве эмиттера.
Оператор RenderParticles, задаваемый для всей системы единожды, позволит отобразить частицы средствами одной из доступных программ воспроизведения.
Оператор DisplayParticles отвечает за отображение сцены в видовом порте. В нашем случае оператор устанавливает для частиц режим вывода Geometry (геометрия, type = 6) и красный цвет частиц (color = red).
В третьем блоке в точках 0f, 50f и 100f задаются ключи анимации масштабирования частиц по оси Z. Обеспечивается полная видимость сцены (max tool zoomExtents) и ее анимация (PlayAnimation).
Приведенный пример просто реализуется в диалоге Particle View и без применения какого-либо кода.

Запуск программы

Запуск программы выполняется в 3ds Max в следующем порядке:

  1. Открыть редактор кода (меню MAXScript – MAXScript Editor).
  2. Перейти в открывшийся диалог и при необходимости создать новую вкладку (Ctrl + N).
  3. Скопировать код, приведенный в уроке, в чистую вкладку редактора.
  4. Позиционироваться в любом месте скопированного кода и нажать Ctrl + E, либо воспользоваться меню редактора Tools – Evaluate All.

Снежинка

Реализуется следующая сцена: четыре потока частиц в виде разноцветных снежинок пересекаются в одной точке и порождают сферический источник снежинок, медленно удаляющихся от его центра (рис. 5).

Рис. 5. Большая снежинка

Задача решается по следующей схеме:

  1. Создаются 4 источника частиц: по одному на каждый поток. В каждом источнике частицы генерируются в одной точке и после рождении двигаются в начало мировой системы координат (в этой точке потоки частиц пересекаются).
  2. Форма частиц перенимается у стандартного объекта Hedra семейства Star 2 (разновидность объемной звезды).
  3. В декоративных целях в точках генерации частиц отображаются конусы, ориентированные вдоль соответствующих траекторий частиц.
  4. Частицы, оказавшиеся в точке пересечения потоков частиц, удаются.
  5. В точке пересечения потоков частиц создается еще один, пятый источник частиц. В качестве объекта, испускающего частицы берется сфера с небольшим числом сегментов (segs = 8). Частицы исходят из вершин этой сферы. Вектор скорости частиц направлен по радиусу сферы, проведенному из ее центра до соответствующей вершины (свойство Direction оператора Speed равно 1).
  6. Форма частиц 5-го источника перенимается у другого объекта Hedra семейства Star 1.
  7. Материал (цвет) частиц каждого источника задается оператором Material_Frequency и выбирается случайным образом из 10 материалов. Для этих целей создается материал Multimaterial с 10-ю компонентами (рис. 6).

Рис. 6. Образец материала для частиц

Эту схему реализует следующий код:

fn prps = (
 delete $*
 sliderTime = 0f
 animationRange = interval 0f 500f
 viewport.SetLayout #layout_4
 viewport.ActiveViewport = 4
 max vpt persp user
 viewport.SetGridVisibility 4 false
 backgroundColor = black
)
fn mkPF pRt ps pRtCn snwFlk mlt = (
 local pF, opDP
 rt = (eulerAngles pRt[1] pRt[2] 0) as quat
 rtCn = (eulerAngles pRtCn[1] pRtCn[2] 0) as quat
 h = 7
 cone radius1:1 radius2:2 height:-h rotation:rtCn pos:ps wireColor:white
 pF = PF_Source enable_Particles:true emitter_Type:0 rotation:rt \
  pos:ps quantity_Viewport:100 show_Logo:off show_Emitter:off
 particleFlow.BeginEdit()
 opBth = birth emit_Start:0 emit_Stop:(350 * 160) type:1 rate:5
 opPI = position_Icon location:0
 opSI = shape_Instance shape_Object:snwFlk
 opRt = rotation direction:0
 opSpn = spin rate:360 variation:0 direction:0
 opSpd = speed speed:30 direction:0
 opSO = script_Operator proceed_Script:"
 on channelsUsed pCont do pCont.UsePosition = true
 on proceed pCont do (
  nP = pCont.NumParticles()
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   if p[3] < 0 do pCont.DeleteParticle k
  )
 )"
 opMFrq = material_Frequency assigned_Material:mlt \
  mtl_ID_1:10 mtl_ID_2:10 mtl_ID_3:10 mtl_ID_4:10 \
  mtl_ID_5:10 mtl_ID_6:10 mtl_ID_7:10 mtl_ID_8:10 \
  mtl_ID_9:10 mtl_ID_10:10 show_In_Viewport:on
 opDP = displayParticles type:6
 opRP = renderParticles type:2
 evn = event()
 particleFlow.EndEdit()
 evn.AppendAction opBth
 evn.AppendAction opPI
 evn.AppendAction opSI
 evn.AppendAction opRt
 evn.AppendAction opSpn
 evn.AppendAction opSpd
 evn.AppendAction opSO
 evn.AppendAction opMFrq
 evn.AppendAction opDP
 pF.AppendAction opRP
 pF.AppendInitialActionList evn
)
-- Block 1
prps()
arrStd = for k = 1 to 10 collect \
 standard diffuse:[random 100 200, random 100 200, random 100 200] showInViewport:on
mlt = multimaterial numsubs:10 materialList:arrStd
meditMaterials[1] = mlt
hdr = hedra radius:2 family:4    -- Star 2
hdr2 = hedra radius:3 family:3    -- Star 1
lght = sphere radius:10 segs:8 wireColor:[200, 200, 200]
hide #(hdr, hdr2, lght)
zP = 100
arrPs = #([-100, 0, zP], [0, 100, zP], [100, 0, zP], [0, -100, zP])
ng = 45
arrRt = #([0, ng], [ng, 0], [0, -ng], [-ng, 0])
ngCn = -135
arrRtCn = #([0, ngCn], [ngCn, 0], [0, -ngCn], [-ngCn, 0])
-- Block 2
for k = 1 to 4 do mkPF arrRt[k] arrPs[k] arrRtCn[k] hdr mlt
-- Block 3
pF = PF_Source enable_Particles:true quantity_Viewport:100 show_Logo:off show_Emitter:off
particleFlow.BeginEdit()
opBth = birth emit_Start:(150 * 160) emit_Stop:(500 * 160) type:1 rate:25
opPO = position_Object emitter_Objects:#(lght) location:1
opSI = shape_Instance shape_Object:hdr2
opRt = rotation direction:0
opSpn = spin rate:360 variation:0 direction:0
opSpd = speed speed:1.0 direction:1  -- Icon center out
opMFrq = material_Frequency assigned_Material:mlt \
 mtl_ID_1:10 mtl_ID_2:10 mtl_ID_3:10 mtl_ID_4:10 \
 mtl_ID_5:10 mtl_ID_6:10 mtl_ID_7:10 mtl_ID_8:10 \
 mtl_ID_9:10 mtl_ID_10:10 show_In_Viewport:on
opDP = displayParticles type:6
opRP = renderParticles type:2
evn = event()
particleFlow.EndEdit()
evn.AppendAction opBth
evn.AppendAction opPO
evn.AppendAction opSI
evn.AppendAction opRt
evn.AppendAction opSpn
evn.AppendAction opSpd
evn.AppendAction opMFrq
evn.AppendAction opDP
pF.AppendAction opRP
pF.AppendInitialActionList evn
max tool zoomExtents
playAnimation()

В основной программе три блока.
В первом блоке функцией prps подготавливается сцена. Затем создается материал Multimaterial с 10-ю компонентами, хранимыми массивом arrStd. Каждый элемент массива – это стандартный материал. RGB-составляющие диффузионной компоненты каждого материала генерируются случайным образом из диапазона 100 – 200 функцией Random.
Далее создаются объекты hdr и hdr2 (3d-звезды), форму которых перенимают частицы, и сфера lght, употребляемая в качестве эмиттера частиц.
В массивы arrPs и arrRt заносятся координаты и углы поворота источников пересекающихся потоков частиц, обеспечивающие пересечение потоков в начале мировой системы координат.
Во втором блоке 4 раза вызывается функция mkPF, создающая при каждом обращении один источник частиц. Кроме того, функция вводит в цену конус, имитирующий излучатель частиц. С источником частиц этот конус никакой связи не имеет. Функция принимает позицию arrPs[k], углы поворота источника arrRt[k] и конуса arrRtCn[k], ссылки на объект с формой и на материал для частиц. (Углы поворота задаются относительно осей мировой системы координат.)
Оператор Birth обеспечивает рождение одной частицы каждые 5 кадров (свойство Rate = 5). Частицы генерируются на отрезке 0 – 350f. Время при задании свойств Emit_Start и Emit_Stop указывается в тиках: в одном кадре 160 тиков. (Число рождаемых частиц определяется в примере свойством Rate оператора Birth и свойствами оператора Position_Icon.)
Оператор Position_Icon указывает системе на необходимость генерировать частицы в одной точке – в базовой точке эмиттера (Location = 0).
Оператор Spin задает угловую скорость частиц и характер ее изменения.
Оператор Speed устанавливает скорость частицы (свойство Speed) и ее направление вдоль стрелки иконки эмиттера (Direction = 0).
Оператор Script_Operator содержит обработчики ChannelsUsed и Proceed, определяемые в свойстве Proceed_Script оператора.
Каждый обработчик получает в качестве параметра ссылку pCont на контейнер частиц системы. Благодаря этой ссылке мы получаем доступ к свойствам и методам контейнера в целом, а также возможность оперировать каждой частицей контейнера, читая и изменяя ее свойства, такие, как скорость, позиция, форма и др.
В обработчике ChannelsUsed указываются каналы обмена данными между обработчиками и контейнером частиц (активизируется канал передачи информации о позициях частиц, UsePosition = true). Второй обработчик (Proceed) постоянно вызывается в процессе работы системы частиц. В нашем случае в нем частица, Z-координата которой меньше нуля, покидает сцену (метод pCont.DeleteParticle k). Для доступа к частице с номером k в for-цикле выполняется присваивание pCont.ParticleIndex = k. Без этого выражения обработчик неработоспособен.
Оператор Material_Frequency задает материал частиц (свойство Assigned_Material): для каждой частицы ее материал случайным образом берется из 10 компонентом созданного ранее материала mlt. Вероятность выбора каждого компонента одинакова и равна 0.1 (Mtl_ID_1 = Mtl_ID_2 = … = Mtl_ID_10 = 10).
Третий блок создает пятый, расположенный в начале координат источник частиц. Частицы появляются в вершинах сферы lght (свойство Location оператора Position_Object равно 1) и с небольшой скоростью удаляются от сферы по радиальному направлению (свойство Direction оператора Speed равно 1). Назначение прочих операторов этого источника пояснено в ранее рассмотренных примерах.
При интерактивной реализации этого примера объем кодирования незначителен (см. свойство Proceed_Script оператора Script_Operator).
В следующем примере объем обязательного кодирования существенно выше.

Снеговик

В примере из частичек (снежинок) лепится снеговик (рис. 7).

Рис. 7. Сделан из снежинок

Как и в предшествующем примере, создаются лучи из частиц, которые направляются к центрам сферических частей снеговика. Однако на этот раз частицы, достигшие центра, не покидают сцену, а тормозятся и помещаются на поверхности снеговика.
Последовательность получения результат следующая:

  1. Создаются 4 источника частиц в позициях [-100, 0, 100], [0, -100, 100], [100, 0, 100] и [0, 100, 100]. Как и ранее, частицы перенимают форму у введенного в сцену стандартного примитива Hedra. Для отображения эмиттера употребляется сфера, выводимая в позиции источника.
  2. Анимационный интервал (его длина 1000f) разбивается на 4 части: в первых трех по триста кадров, а в последнем 100.
  3. На первой части анимационного интервала частицы, покидающие эмиттер, направляются в центр нижней части снеговика, на второй – в центр средней части снеговика, на третьей – в центр верхней части снеговика. Эти действия выполняются за счет задания надлежащего вектора скорости частиц (свойство ParticleSpeed). Центр нижней части снеговика расположен в точке [0, 0, 0].
  4. Частица, приобретя вектор скорости, направляется в центр соответствующей части снеговика. После прохождения центра скорость частицы обнуляется, а сама частица помещается на поверхность соответствующей части фигуры (свойство ParticlePosition) и остается в этой позиции до конца анимации.
  5. На четвертой части анимационного интервала снеговик дополняется носом, глазами и шапкой.

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

fn prps = (
 delete $*
 sliderTime = 0f
 animationRange = interval 0f 1000f
 timeConfiguration.PlaybackLoop = false
 viewport.SetLayout #layout_4
 viewport.ActiveViewport = 4
 max vpt persp user
 viewport.SetGridVisibility 4 false
 backgroundColor = white
)
fn mkPF ps snwFlk mlt scrpt = (
 local pF
 sph = sphere radius:2 pos:ps wireColor:white
 pF = PF_Source enable_Particles:true emitter_Type:0 \
  pos:ps quantity_Viewport:100 show_Logo:off show_Emitter:off
 particleFlow.BeginEdit()
 opBth = birth emit_Start:0 emit_Stop:(900 * 160) type:1 rate:6
 opPO = position_Object emitter_Objects:#(sph) location:0
 opSI = shape_Instance shape_Object:snwFlk
 opRt = rotation direction:0
 opSpn = spin rate:360 variation:0 direction:0
 opSO = script_Operator proceed_Script:scrpt
 opMFrq = material_Frequency assigned_Material:mlt \
  mtl_ID_1:10 mtl_ID_2:10 mtl_ID_3:10 mtl_ID_4:10 \
  mtl_ID_5:10 mtl_ID_6:10 mtl_ID_7:10 mtl_ID_8:10 \
  mtl_ID_9:10 mtl_ID_10:10 show_In_Viewport:on
 opDP = displayParticles type:6
 opRP = renderParticles type:2
 evn = event()
 particleFlow.EndEdit()
 evn.AppendAction opBth
 evn.AppendAction opPO
 evn.AppendAction opSI
 evn.AppendAction opRt
 evn.AppendAction opSpn
 evn.AppendAction opSO
 evn.AppendAction opMFrq
 evn.AppendAction opDP
 pF.AppendAction opRP
 pF.AppendInitialActionList evn
)
-- Block 1
prps()
arrStd = for k = 1 to 10 collect \
 standard diffuse:[random 100 200, random 100 200, random 100 200] showInViewport:on
mlt = multimaterial numsubs:10 materialList:arrStd
hR = 2
hdr = hedra radius:hR family:3
hide hdr
global dt = 300 * 160
global tm2 = dt, tm3 = tm2 + dt
global r = 10, r2 = 6, r3 = 3
global d2 = r + r2 + 2 * hR, d3 = d2 + r2 + r3 + 2 * hR
scrpt0 = "
 on channelsUsed pCont do (
  pCont.UsePosition = true
  pCont.UseSpeed = true
  pCont.UseAge = true
 )
"
scrpt = scrpt0 + "
 on proceed pCont do (
  nP = pCont.NumParticles()
  seed nP
  if nP > 0 then (
   pCont.ParticleIndex = 1
   sTm = pCont.ParticleAge as integer
  )
  else
   sTm = 0
  pSpd = [0.01, 0, -0.01]
  pSpd2 = [0.01, 0, -0.01 + d2 * 0.0001]
  pSpd3 = [0.01, 0, -0.01 + d3 * 0.0001]
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   case of (
    (sTm > tm3 and p[3] > 99): pCont.ParticleSpeed = pSpd3
    (sTm > tm2 and p[3] > 99): pCont.ParticleSpeed = pSpd2
    (p[3] > 99): pCont.ParticleSpeed = pSpd
   )
   if p[1] > 0 do (
    pSpd = pCont.ParticleSpeed
    case pSpd of (
     pSpd2: (rc = r2; z = d2)
     pSpd3: (rc = r3; z = d3)
     default: (rc = r; z = 0)
    )
    pCont.ParticleSpeed = [0, 0, 0]
    pNg = random [-90, 0] [90, 360]
    sn = rc * sin pNg[2]
    pCont.ParticlePosition = [sn * cos pNg[1], sn * sin pNg[1], z + rc * cos pNg[2]]
   )
  )
)"
scrpt2 = scrpt0 + "
 on proceed pCont do (
  nP = pCont.NumParticles()
  seed nP
  if nP > 0 then (
   pCont.ParticleIndex = 1
   sTm = pCont.ParticleAge as integer
  )
  else
   sTm = 0
  pSpd = [0, 0.01, -0.01]
  pSpd2 = [0, 0.01, -0.01 + d2 * 0.0001]
  pSpd3 = [0, 0.01, -0.01 + d3 * 0.0001]
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   case of (
    (sTm > tm3 and p[3] > 99): pCont.ParticleSpeed = pSpd3
    (sTm > tm2 and p[3] > 99): pCont.ParticleSpeed = pSpd2
    (p[3] > 99): pCont.ParticleSpeed = pSpd
   )
   if p[2] > 0 do (
    pSpd = pCont.ParticleSpeed
    case pSpd of (
     pSpd2: (rc = r2; z = d2)
     pSpd3: (rc = r3; z = d3)
     default: (rc = r; z = 0)
    )
    pCont.ParticleSpeed = [0, 0, 0]
    pNg = random [-90, 0] [90, 360]
    sn = rc * sin pNg[2]
    pCont.ParticlePosition = [sn * cos pNg[1], sn * sin pNg[1], z + rc * cos pNg[2]]
   )
  )
)"
scrpt3 = scrpt0 + "
 on proceed pCont do (
  nP = pCont.NumParticles()
  seed nP
  if nP > 0 then (
   pCont.ParticleIndex = 1
   sTm = pCont.ParticleAge as integer
  )
  else
   sTm = 0
  pSpd = [-0.01, 0, -0.01]
  pSpd2 = [-0.01, 0, -0.01 + d2 * 0.0001]
  pSpd3 = [-0.01, 0, -0.01 + d3 * 0.0001]
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   case of (
    (sTm > tm3 and p[3] > 99): pCont.ParticleSpeed = pSpd3
    (sTm > tm2 and p[3] > 99): pCont.ParticleSpeed = pSpd2
    (p[3] > 99): pCont.ParticleSpeed = pSpd
   )
   if p[1] < 0 do (
    pSpd = pCont.ParticleSpeed
    case pSpd of (
     pSpd2: (rc = r2; z = d2)
     pSpd3: (rc = r3; z = d3)
     default: (rc = r; z = 0)
    )
    pCont.ParticleSpeed = [0, 0, 0]
    pNg = random [-90, 0] [90, 360]
    sn = rc * sin pNg[2]
    pCont.ParticlePosition = [sn * cos pNg[1], sn * sin pNg[1], z + rc * cos pNg[2]]
   )
  )
)"
scrpt4 = scrpt0 + "
 on proceed pCont do (
  nP = pCont.NumParticles()
  seed nP
  if nP > 0 then (
   pCont.ParticleIndex = 1
   sTm = pCont.ParticleAge as integer
  )
  else
   sTm = 0
  pSpd = [0, -0.01, -0.01]
  pSpd2 = [0, -0.01, -0.01 + d2 * 0.0001]
  pSpd3 = [0, -0.01, -0.01 + d3 * 0.0001]
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   case of (
    (sTm > tm3 and p[3] > 99): pCont.ParticleSpeed = pSpd3
    (sTm > tm2 and p[3] > 99): pCont.ParticleSpeed = pSpd2
    (p[3] > 99): pCont.ParticleSpeed = pSpd
   )
   if p[2] < 0 do (
    pSpd = pCont.ParticleSpeed
    case pSpd of (
     pSpd2: (rc = r2; z = d2)
     pSpd3: (rc = r3; z = d3)
     default: (rc = r; z = 0)
    )
    pCont.ParticleSpeed = [0, 0, 0]
    pNg = random [-90, 0] [90, 360]
    sn = rc * sin pNg[2]
    pCont.ParticlePosition = [sn * cos pNg[1], sn * sin pNg[1], z + rc * cos pNg[2]]
   )
  )
)"
-- Block 2
mkPF [-100, 0, 100] hdr mlt scrpt
mkPF [0, -100, 100] hdr mlt scrpt2
-- mkPF [100, 0, 100] hdr mlt scrpt3
-- mkPF [0, 100, 100] hdr mlt scrpt4
-- Block 3
std = standard diffuse:red showInViewport:on
std2 = standard diffuse:black showInViewport:on
r32 = r3 + hR
cn = cone radius1:0.75 radius2:0 height:r32 material:std
rotate cn -90 [0, 1, 0]
cn2 = cone radius1:(0.75 * r32) radius2:(0.5 * r32) height:(r32 + hR) material:std
sp = sphere radius:0.5 material:std2
sp2 = copy sp
animate on (
 at time 0 (
  cn.Pos = cn2.Pos = sp.Pos = sp2.Pos = [0, 0, 0]
  std.Opacity = std2.Opacity = 0
 )
 at time 900 (
  cn.Pos = cn2.Pos = sp.Pos = sp2.Pos = [0, 0, 0]
  std.Opacity = std2.Opacity = 0
 )
 at time 1000 (
  cn.Pos = [-r32, 0, d3]
  cn2.Pos = [0, 0, d3 + (0.75 * r32)]
  pNg = [-25, -75]
  sn = r32 * sin pNg[2]
  sp.Pos = [sn * cos pNg[1], sn * sin pNg[1], d3 + r32 * cos pNg[2]]
  sp2.Pos = [sn * cos pNg[1], -sn * sin pNg[1], d3 + r32 * cos pNg[2]]
  std.Opacity = std2.Opacity = 100
 )
)
max tool zoomExtents
playAnimation()

В основной программе три блока.
В первом блоке создаются материал mlt для частиц и объект Hedra, определяющий форму частиц. Далее задаются начальные точки второй и третьей частей анимационного интервала (tm2 и tm3). Длина каждой части равна dt (время указывается в тиках).
Вслед вводятся параметры сцены – это радиусы r, r2 и r3 частей снеговика, расстояния d2 и d3 по оси Z центров второй и третьей частей снеговика от начала координат (центр первой части снеговика расположен в начале мировой системы координат).
Далее записаны скрипты, используемые оператором Script_Operator систем частиц.
Все скрипты однотипны и различаются лишь значениями векторов скоростей и условиями торможения частиц.
Каждый скрипт начинается фрагментом scrpt0, в котором определяются каналы обмена данными между обработчиками и контейнером частиц (делаются доступными каналы Position, Speed и Age):

scrpt0 = "
 on channelsUsed pCont do (
  pCont.UsePosition = true
  pCont.UseSpeed = true
  pCont.UseAge = true
 )
"

К этому фрагменту присоединяется код proceed-обработчика. В каждом proceed-обработчике определяется число частиц nP источника, меняется затравка датчика случайных чисел (seed nP), вычисляется текущее время sTm в тиках (sTm берется равным возрасту первой частицы) и задаются значения векторов скорости частиц pSpd, pSpd2 и pSp3 для каждой части анимационного интервала.
В for-цикле обработчика каждой только что покинувшей эмиттер частице назначается вектор скорости. Факт близости частицы к эмиттеру устанавливается условием p[3] > 99, где p[3] – это Z-координата частицы (напомним, что Z-координата каждого источника частиц равна 100). Используемое значение pSpd, pSpd2 или pSp3 вектора скорости зависит от текущего времени: если sTm находится в первой части анимационного интервала, то берется pSpd, если – во второй, то берется pSpd2, если – в третьей, то берется pSpd3.
Далее в этом же цикле проверяется, достигла ли частица точку назначения (центр соответствующей части снеговика). Если да (в первом скрипте оценивается выражение p[1] > 0, где p[1] – это X-координата частицы), то определяется, к какой анимационной части относится частица (это выполняется по значению вектора скорости частицы). В результате для последующих вычислений определяются надлежащие значения переменных rc и z (радиуса соответствующий части снеговика и Z-координаты его центра). Скорость частицы обнуляется, и частица помещается на поверхность соответствующей сферы. Для нахождения позиции частицы используется параметрическое уравнение сферы (с радиусом R и с центром в точке [x0, y0, z0]):

Значения углов тригонометрических функций этого уравнения определяются при помощи датчика случайных чисел.
Во втором блоке основной программы в результате обращений к функции mkPF создаются источники частиц. Все используемые в источниках операторы нам уже встречались. Поскольку программа требует значительных вычислительных ресурсов, то два последних вызова функции mkPF закомментированы. При наличии таких ресурсов этот комментарий можно снять.
В третьем блоке создаются и анимируются такие части снеговика, как нос, глаза и шапка. Первоначально они создаются в начале координат и делаются невидимыми за счет обнуления свойства Opacity (непрозрачность) каждого из примененных для этих частей материалов. К концу анимации каждая часть занимает положенное ей место на голове снеговика, а материалы std и std2 становятся совершенно непрозрачными (Opacity = 100).
На рис. 8 и 9 показаны промежуточные кадры создания снеговика при наличии в сцене 4-х источников частиц.

Рис. 8. 335-й кадр анимации

Рис. 9. 635-й кадр анимации

Число строк, потраченных на запись скриптов scrpt, scrpt2, scrpt3 и scrpt4 операторов Script_Operator, можно сократить, выделив совпадающие части в отдельные куски scrpt0, scrpt01, scrpt02 и scrpt03:

scrpt0 = "
 on channelsUsed pCont do (
  pCont.UsePosition = true
  pCont.UseSpeed = true
  pCont.UseAge = true
 )
"
scrpt01= "
 on proceed pCont do (
  nP = pCont.NumParticles()
  seed nP
  if nP > 0 then (
   pCont.ParticleIndex = 1
   sTm = pCont.ParticleAge as integer
  )
  else
   sTm = 0
"
scrpt02= "
  for k = 1 to nP do (
   pCont.ParticleIndex = k
   p = pCont.ParticlePosition
   case of (
    (sTm > tm3 and p[3] > 99): pCont.ParticleSpeed = pSpd3
    (sTm > tm2 and p[3] > 99): pCont.ParticleSpeed = pSpd2
    (p[3] > 99): pCont.ParticleSpeed = pSpd
   )
"
scrpt03= "
    pSpd = pCont.ParticleSpeed
    case pSpd of (
     pSpd2: (rc = r2; z = d2)
     pSpd3: (rc = r3; z = d3)
     default: (rc = r; z = 0)
    )
    pCont.ParticleSpeed = [0, 0, 0]
    pNg = random [-90, 0] [90, 360]
    sn = rc * sin pNg[2]
    pCont.ParticlePosition = [sn * cos pNg[1], sn * sin pNg[1], z + rc * cos pNg[2]]
   )
  )
)"
scrpt = scrpt0 + scrpt01 + "
  pSpd = [0.01, 0, -0.01]
  pSpd2 = [0.01, 0, -0.01 + d2 * 0.0001]
  pSpd3 = [0.01, 0, -0.01 + d3 * 0.0001]" + scrpt02 + "if p[1] > 0 do (" + scrpt03
scrpt2 = scrpt0 + scrpt01 + "
  pSpd = [0, 0.01, -0.01]
  pSpd2 = [0, 0.01, -0.01 + d2 * 0.0001]
  pSpd3 = [0, 0.01, -0.01 + d3 * 0.0001]" + scrpt02 + "if p[2] > 0 do (" + scrpt03
scrpt3 = scrpt0 + scrpt01 + "
  pSpd = [-0.01, 0, -0.01]
  pSpd2 = [-0.01, 0, -0.01 + d2 * 0.0001]
  pSpd3 = [-0.01, 0, -0.01 + d3 * 0.0001]" + scrpt02 + "if p[1] < 0 do (" + scrpt03
scrpt4 = scrpt0 + scrpt01 + "
  pSpd = [0, -0.01, -0.01]
  pSpd2 = [0, -0.01, -0.01 + d2 * 0.0001]
  pSpd3 = [0, -0.01, -0.01 + d3 * 0.0001]" + scrpt02 + "if p[2] < 0 do (" + scrpt03

Заключение

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

Источники

  1. Autodesk® 3ds Max® 2009 MAXScript Reference.
  2. Бартеньев О. В. Программирование модификаторов 3ds Max. Учебно-справочное пособие. – М.:Физматкнига, 2009. – 341 с.
  3. Яндекс. Картинки.
  4. http://100byte.ru/
169 0 850 30
28
2010-12-13
Весьма интересно, но сложно в исполнении. Было интереснее если дополнить немного других личных примеров. Но мне всё равно понравилось!!! Спасибо.
2010-12-13
[smile=18] я в шоке..
2010-12-13
буду краток - дежавю [smile=07]
2010-12-13
пойду пробовать, через месяц вернусь))) [smile=04]
2010-12-13
Читать код без коментов, но с коментами под ним, постоянно мотая страницу туда-сюда - очень неприятный момент 5\3
2010-12-13
[smile=18] вот это да! какой невороятный способ!
2010-12-14
Это скорей всего из области програмирования. Привести в пользовательский вид попроще,а то времени маловато на изучение всех блин фишечек.
2010-12-14
Автор провел работу над ошибками. Взял более хитры но такой же бесполезный предмет. Если бы я на главной картинке увидел крутую модель то обязательно бы почитал урок, как же автор такое сделал. Но на черта мне нужен такой снеговик не ясно, поэтому я читать не стал. Автор возьми более жизненный и сложный пример и цены тебе не будет, ну если потянешь конечно)) зы это все мое имхо возможно найдется и масса других мнений.
2010-12-14
чо, а я согласен, более эфектно и "понятнее" было бы где это применять, если бы сделать более маштабненькую работу. К примеру, насколько я понял с урока, тут 2д заполнение, но это же всё таки 3дмакс) хотелось бы увидеть 3д заполнение снеговика частицами :)) или мож я урок не дочитал?
2010-12-15
Другой урок по частицам и MAXScript, почувствуйте разницу: [url]http://www.scriptattack.com/lessons/voronoi/voronoi.html[/url]
2010-12-15
Модельку хотите? Есть у меня! Я отмоделирую любого соловья! Читатели, страдающие по модельке, просто не в теме: рассматривается система частиц PF Source. Снежинка и снеговик выбраны в качестве результирующих поверхностей по простой причине: ребята, скоро Новый год! С таким же успехом можно употребить в качестве поверхности назначения синего человечка, себя любимого или прочую модельку. Программа отмоделирует вас, извините за каламбур, по полной программе. Что же касается иных моделей, то мои студенты записали на MAXScript все более-менее полезные 3ds Max-уроки, как собственные, так и размещенные в разных источниках. Это сделано в интересах учебного процесса, и позволяет показать, как посредством небольшого кода записать достаточно сложную, на первый взгляд, сцену. Например, имеющийся на Render.ru урок "Мигание лампочек: автоматизация процесса" записывается в виде следующего кода: fn mkFltX2 k arrM expr ob = ( std = standard diffuse:[135, 110, 8] showInViewport:true fltX = float_Expression() cnt = arrM.Count for k2 = 1 to cnt do ( p = "p" + k2 as string fltX.AddVectorConstant p arrM[k2] ) fltX.SetExpression expr fltX.Update() std.SelfIllumAmount.Controller = fltX ob.Material = std ) fn mkArrM n nL dT tS = ( arrM = #() t = tS for k2 = 1 to n do ( append arrM [t, t + dT, 0] t = t + dT * nL ) return arrM ) fn mkExpr arrM = ( cnt = arrM.Count expr = ""; addX2 = "" for k2 = 1 to cnt do ( p = "p" + k2 as string addX = "if(F >= comp(" + p + ", 0), if(F < comp(" + p + ", 1), 1, " addX2 += "), 0)" expr += addX ) return expr + " 0" + addX2 ) fn f3 h = ( delete $* sliderTime = 0f viewport.ActiveViewport = 4 max vpt persp user animationRange = interval 0f 100f max tool zoomExtents r = 10; ps = [120, 0, 0]; dx = 40; n = 3; nL = 5 dT = 100 / ((n - 1) * nL) for k = 1 to nL do ( ps = ps - [dx, 0, 0] sp = sphere radius:r segs:32 pos:ps arrM = mkArrM n nL dT (dT * (k - 1)) mkFltX2 k arrM (mkExpr arrM) sp ) playAnimation() ) f3 1
2010-12-16
Ссылка от 1acc на работу Морфинг по Вороному сделана так, словно работа – это шедевр народного творчества. Боюсь, что читателей ждет разочарование. 1. Никакого морфинга нет; просто одни куски поверхности заменяются другими. 2. Вороной проблемами морфинга не занимался. С таким же успехом можно употребить название Морфинг по Черному, упомянув поэта Сашу Черного, или Морфинг по Белому, вспомнив писателя Андрея Белого. 3. Нужно доказывать, что получаемое разбиение отвечает критерию Вороного, или приводит ссылки, где эти доказательства лежат. 4. Следует указывать вычислительную сложность алгоритма. Так, Простой алгоритм разбиения по Вороному имеет вычислительную сложность О(N*N*logN), а вычислительная сложность Рекурсивного алгоритма разбиения по Вороному равна О(N*logN). Поговаривают, что есть алгоритм с линейной вычислительной сложностью. 5. Уже первый скрипт (script_birth_explode) содержит ошибку. Так, не определена правая часть выражения global a = $PCloud_chunks --ОБЪЕКТ ОБЛАКА ЧАСТИЦ Правильно записать так: global a = undefined Кстати неясно, чем облако лучше случайного задания точек. Кроме того, в этой программе много мусора. Так, без цели вводятся и не используются переменные temp, curver, start и end, оставлен бесполезный обработчик Release и др. Природа этого мусора понятна: куски кода копировались их первоисточника (эти куски имеются в справке по MAXScript). В нем они имели соответствующее применение. Беспомощно выглядит часть кода, например вместо куска obj=bs tempmesh=copy obj temp_trans = tempmesh.transform tempmesh.transform = (matrix3 [1,0,0] [0,1,0] [0,0,1] [0,0,0]) addModifier tempmesh (XForm()) tempmesh.XForm.gizmo.transform = temp_trans collapseStack tempmesh temp=tempmesh.mesh достаточно записать tempmesh = copy sph convertToMesh tempmesh А вместо куска indexArray = #() indexArray[pp] = 0 indexArray = for i = 1 to pointsarr.count collect pointsarr[i].id достаточно записать indexArray = for i = 1 to numChunks collect pointsArr[i].id И так далее. Иными словами, всю эту кухню при надлежащей обработке можно сократить в 2 раза, получив выигрыш как по времени, так и по качеству. Как говорила героиня Советского фильма Небесный тихоход: "Я думала, Вы асс, а Вы У-двас." Указанные недостатки, однако, не снижают ценности работы, и мы с благодарностью воспринимаем ее результаты. С Новым годом!
2010-12-16
Уточнение. Прежде чем вносить изменения tempmesh = copy sph convertToMesh tempmesh следует предварительно создать объект с именем sph: global sph = sphere radius:150 segments:32 name:"Source" pos:[-900, -180, 150] и подать этот объект в качестве параметра конструктору a = pcloud emitter:sph ... (далее по тексту)
2010-12-16
Я не собирался придираться к коду, соревноваться в программировании и не ждал взаимностей от автора урока. Разница в оформлении, о чем и просил почувствовать. Впрочем, формат уроков на рендер.ру не позволяет применять стили, так что цветной текст тут оформить не выйдет, но комментариев добавить было бы совсем неплохо. Здесь все-таки в основном не программисты сидят. А недостатки при желании можно найти в любом коде, и мне приятно, что catBurdger потратил время и нашел их у меня. Студентам вашим привет.
2010-12-16
Стиль изложения определяется целевой функцией. Ясно, что нельзя на нескольких примерах научиться работать с частицами, особенно в среде PF Source. Поэтому цель урока – это придать заинтересованным лицам импульс для самостоятельного движения по пути освоения систем частиц. При этом сообщается, что 1) можно получить разнообразные и весьма привлекательные результаты; 2) задачи могут быть трудоемкими, и придется потратить немало сил и времени на приобретение специальных навыком; 3) наиболее интересные результаты потребует освоения MAXSript, причем в приличном объеме. Исходя из такого посыла, формируется стиль изложения. Основные выводы очевид-ны: либо серьезно заняться частицами, либо отложить это действо до лучших времен. Что же касается Вашей работы, 1асс, то, полагаю, несмотря на детальность изложения и красочность результата, она трудна для понимания по следующим причинам: 1. Такая тема, как разбиение Вороного, сложна для восприятия. 2. Применяется трудоемкий, требующий обязательного программирования на MAX-Sript способ получения формы частиц из кусков поверхности. 3. Наличие большого числа явно не раскрываемых деталей. Спросите, например, чи-тателя, зачем при формировании осколков применяется модификатор XForm, или что содержит cd=selection[1] (см. script_itself_voronoi_tesselation) и др. Полезным, думаю, для иллюстрации Вашего разбиения был бы следующий пример (два варианта). В первом варианте задаются регулярные координаты точек разбиения и результат предсказуем, во втором – координаты получаются случайным образом. Ваш код несколько упрощен, что, полагаю, делает его более доступным. В обоих случаях координаты точек разбиения хранит массив arrP. -- Case 1 -- x = 5; y = 5; z = 5 -- arrP = #([x, 0, 0], [-x, 0, 0], [0, y, 0], [0, -y, 0], [0, 0, z], [0, 0, -z]) -- Case 2 -- x = random 10 40; y = random -40 -10; z = random 10 40 -- arrP = #([x, 0, 0], [0.5 * x, 0, 0], [0, y, 0], [0, 0.5 * y, 0], [0, 0, z], [0, 0, 0.5 * z]) -- fn compareFN v1 v2 valArray: centerpoint: = ( local v1i = valArray[v1].coord - centerpoint local v2i = valArray[v2].coord - centerpoint local d = (length v1i) - (length v2i) case of ( (d < 0.): -1 (d > 0.): 1 default: 0 ) ) delete $* sph = sphere radius:50 segments:32 max tool zoomExtents numChunks = 6 struct pointProps (id, coord) fragmentMesh = #() simplexArr = #() tempMesh = copy sph convertToMesh tempMesh rotPlane = plane isHidden:true x = 5; y = 5; z = 5 arrP = #([x, 0, 0], [-x, 0, 0], [0, y, 0], [0, -y, 0], [0, 0, z], [0, 0, -z]) pointsArr = for i = 1 to numChunks collect pointProps id:i coord:arrP[i] for k = 1 to numChunks do ( indexArray = for i = 1 to numChunks collect pointsArr[i].id cp = pointsArr[k].coord qsort indexArray compareFN valArray:pointsArr centerpoint:cp deleteItem indexArray 1 append simplexArr indexArray ) for k = 1 to numChunks do ( cp = pointsArr[k].Coord thePart = editable_mesh pos:cp thePart.Mesh = tempMesh.Mesh addModifier thePart (XForm()) thePart.XForm.gizmo.pos = tempMesh.pos - cp collapseStack thePart mSlice = sliceModifier slice_type:2 for i = 1 to numChunks - 1 do ( cv = simplexArr[k][i] if cv != k then ( theVec = pointsArr[cv].Coord - cp rotPlane.dir = theVec addModifier thePart mSlice mSlice.slice_plane.Rotation = rotPlane.Rotation mSlice.slice_plane.pos = theVec / 2 addModifier thePart (cap_holes()) collapseStack thePart ) ) append fragmentMesh thePart ) delete #(rotPlane, tempMesh, sph) if numChunks == 6 do ( animate on ( dst = 100 at time 50 ( move fragmentMesh[1] [dst, 0, 0]; move fragmentMesh[2] [-dst, 0, 0] move fragmentMesh[3] [0, dst, 0]; move fragmentMesh[4] [0, -dst, 0] move fragmentMesh[5] [0, 0, dst]; move fragmentMesh[6] [0, 0, -dst] ) dst = -dst at time 100 ( move fragmentMesh[1] [dst, 0, 0]; move fragmentMesh[2] [-dst, 0, 0] move fragmentMesh[3] [0, dst, 0]; move fragmentMesh[4] [0, -dst, 0] mo
2010-12-17
К сожалению, после вставки текста пропали квадратные скобки и все, что было в них заключено. Кроме того, по той же причине испортился конец кода и вдобавок он обрезан программой вставки комментария. Иными словами, код безнадежно испорчен, и на него не следует обращать внимания. Жаль!
2010-12-17
а я думал что в кси айс сложный
2010-12-18
Совершенно справедливо, все очень просто: частица, добравшаяся до центра сферы, помещается на ее поверхности в случайно заданной точке.
2010-12-19
[b]2 catBurdger[/b] такое пожелание, просьба, напишите, простенький, но пошаговый, урок как создать для макса бинараный плагин на с++. чтобы он имел свой интерфейс и делал две простейшие вещи: создавал сферу, например, и скейлил её по осям.
2010-12-21
Ответ 2 iVAt.. Самый естественный путь использования С++ - это работа с 3ds Max SDK, и тогда задача решается тривиально, то есть так же, как и средствами MAXScript. У меня этого расширения нет. Другой вариант - это запуск подготовленного на стороне exe-файла. Тогда язык не важен, но нужны для начала интерфейсы методов MAXScript. Чем Вас не устраивает 3ds Max SDK?
2010-12-21
[quote=catBurdger] Чем Вас не устраивает 3ds Max SDK? [/quote] всем устраивает, просто нет нормального урока для начинающих по этой теме.
2010-12-21
[quote=iVAt] всем устраивает, просто нет нормального урока для начинающих по этой теме. [/quote] Возможно, я смогу получить доступ к этому ресурсу. В настоящее время его у меня нет и поэтому планировать шаги в этом направлении не могу.
2010-12-23
Ох... Этож, надо было всё так усложнять)) Pflow и без специального скрипта такое может сделать)))
2010-12-23
Уважаемый Thomas Tchou, неясно, что такого сложного Вы здесь обнаружили. Что может быть проще скрипта?
2010-12-31
Огромное спасибо автору за уроки по MAX Script. Пожалуйста продолжайте. Мне как человеку интересующемуся данной темой, очень интересно и полезно посмотреть на примеры решений в MAX Script. Полученные знания, можно использовать для решения других задач. А всем негативным и отрицательным - "что вы злые то такие как побитые ногами?" Специфика урока не подразумевает красивых картинок или масштабных сцен.
2011-01-01
А я считаю побольше бы таких уроков, т.к уроков по скрипту на русском не так уж и много. И неважно сложно написано или нет.
2011-01-20
Комментарии в тексте скрипта! Иначе это не урок, а выпендреж :)
2011-05-02
непонятный урок((побольше скринов)
RENDER.RU