Black Hole (Houdini Tutorial)

Black Hole (Houdini Tutorial)

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

Black Hole (Houdini Tutorial)

Далее я подробнее опишу процесс создания черной дыры в Houdini с последующим рендером в Karma.

К этому туториалу я буду прикреплять скриншоты экрана и подробнее останавливаться на написании VEX-кода, поскольку для меня это стало первым знакомством с программированием в рабочем процессе. Сразу отмечу, что основную часть кода я написала при помощи ChatGPT. Огромное спасибо современным технологиям за возможность так быстро осваивать новую сферу, не отрываясь от рабочего процесса.

Не будем зацикливаться на построении сцены в Solaris — лишь отмечу, что для всех моделей я использовал OBJ-контейнер (objnet_black_hole), а затем импортировал их в сцену через SOP Import, где уже назначал необходимые материалы.
Не будем зацикливаться на построении сцены в Solaris — лишь отмечу, что для всех моделей я использовал OBJ-контейнер (objnet_black_hole), а затем импортировал их в сцену через SOP Import, где уже назначал необходимые материалы.

А теперь основная часть....

1) Создаем само тело черной дыры: у меня это sphere (Primitive)

2) Теперь я хочу изменять радиус сферы либо ее массу для этого напишем VEX код применяя формулу Шварцшильда для вычисления радиуса горизонта события

Rs = 2GM/c², где:

  • Rs — радиус Шварцшильда;
  • G — гравитационная постоянная;
  • M — масса объекта;
  • c — скорость света в вакууме.

создадим AttributeWrangle назовем ее "attribwrangle_radius_g"

В VEXpression пишем такой код:
В VEXpression пишем такой код:
// ---------- ПАРАМЕТРЫ WRANGLE ---------- // Integer: mode (0 = из массы, 1 = из радиуса) // Float: mass_input (масса, кг) // Float: radius_input (радиус Шварцшильда, м) // Режим работы int mode = chi("mode"); float kef = chf("Kef"); // Физические константы (СИ) float G = 6.67430e-11; // гравитационная постоянная, м^3 / (кг * с^2) float c = 2.99792458e8; // скорость света, м/с float M; // масса float Rs; // радиус Шварцшильда if (mode == 0) { // РЕЖИМ 0: Вводим массу, считаем радиус M = chf("mass_sun") * 1.98847e30; Rs = 2.0 * G * M / (c * c); f@Rs_km = Rs / 1000; f@R_kef= (Rs / 1000) * kef; } else { // РЕЖИМ 1: Вводим радиус, считаем массу Rs = chf("radius_km")*1000; M = (Rs * c * c) / (2.0 * G); f@Rs_km = Rs / 1000; f@R_kef= (Rs / 1000) * kef; } // Записываем результаты в атрибуты, чтобы можно было использовать дальше f@mass_kg = M; f@mass_sun = M / 1.98847e30; f@GM_kef = ((f@R_kef) * c * c)/2; f@c_kef = sqrt((2*G*M)/(f@R_kef*1000));

Вводимые параметры:

  • Mode — режим работы: • 0 — вводим массу, считаем радиус; • 1 — вводим радиус, считаем массу.
  • Kef — коэффициент реального радиуса к отображаемому. Например, реальный радиус равен 30 км, Kef = 0.1, тогда отображаемый радиус в параметрах Houdini будет равен 3 единицам.
  • Mass Sun — масса в солнечных массах.
  • Radius_km — радиус Шварцшильда.

Выводимые параметры:

думаю тут подробнее не нужно останавливаться
думаю тут подробнее не нужно останавливаться

3) Применим наш радиус к нашей сфере

Для этого создаем ноду Transform (называем ее "transform_blak_hole") и в параметр Uniform Scale передаем созданный нами атрибут R_kef

detail("../attribwrangle_radius_g", "R_kef", 0)
Black Hole (Houdini Tutorial)

4) Создаем ноду Transform для общего управления и Null для вывода ее через SOP Import в сцену.

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

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

отдельное спасибо YouTube и туториалам по Blender за помощь в частичном решении этой задачи.

1) Удаляем все атрибуты кроме R_kef при помощи ноды Attribute Delete.

2) Создаем цикл foreach begin.

Black Hole (Houdini Tutorial)

применим такие параметры:

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

2.1 Внутри цикла создаем ноду Attribute Create в ней на Primitive создаем Integer параметр ID (который к каждому создаваемому примитиву будет присваивать свой ID).

Для этого в Value записываем:

detail("../foreach_begin2", "iteration", 0)
Black Hole (Houdini Tutorial)

2.2 Создаём Attribute Wrangle называем ее "attribwrangle_n_r": на первый вход подаём созданный ранее Attribute Create, а на второй — ноду из цикла с Metadata (пример можно посмотреть выше, нода называется "attribwrangle_n_r").

VEXpression

float iter = detail (1, "iteration", 0); float iters = detail (1, "numiterations", 0); float k = chf("power_of_impact"); float R_size = ((iter / (iters-1)) / k); @R_size = R_size;
Параметр Power Of Impact подбирается эмпирически: его значение должно быть таким, чтобы радиусы сфер оставались в разумных пределах и не выглядели чрезмерно крупными.
Параметр Power Of Impact подбирается эмпирически: его значение должно быть таким, чтобы радиусы сфер оставались в разумных пределах и не выглядели чрезмерно крупными.

Данный Attribute Wrangle создаёт параметр R_size, который мы затем прибавляем к Uniform Scale в следующей, созданной нами ноде Transform.

2.3 В ноде Transform в Uniform Scale пишем:

1 + prim("../attribwrangle_n_r", 0, "R_size", 0)

2.4 Создаем Attribute Wrangle в ней считаем новый радиус и записываем его на примитивы как "R"

VEXpression

// РАДИУСЫ ОКРУЖНОСТЕЙ // vector minv, maxv; getbbox(0, minv, maxv); float R = (maxv - minv)/2; f@R = R;

данную ноду подключаем на foreach_end

По итогу мы получаем 64 сферы распределенные ближе к центру.

3) Создаем ноду Attribute Wrangle на первый вход подключаем выход foreach_end на второй выход с ноды "transform_blak_hole"

VEXpression

float R = @R; float Rs = detail(1, "R_kef", 0); float nr = 1 + (Rs / R); @nr = nr;

В этой ноде мы получаем атрибут "nr" — он понадобится нам в дальнейшем. Это параметр показателя преломления, который мы будем назначать в материалах на эти сферы.

4) Снова создаем Attribute Wrangle где находим максимум и минимум атрибута "nr" (понадобиться далее)

VEXpression

float npr = nprimitives(0); float max_prim = -1; float max_val = -1e9; for (float i = 0; i < npr; i++) { float v = prim(0, "nr", i); if (v > max_val) { max_val = v; max_prim = i; } } setdetailattrib(0, "max_nr", max_val, "set"); float min_prim = -1; float min_val = 1e9; for (float i = 0; i < npr; i++) { float v = prim(0, "nr", i); if (v < min_val) { min_val = v; min_prim = i; } } setdetailattrib(0, "min_nr", min_val, "set");

5) Нормализуем показатель "nr", для этого:

в Attribute Wrangle пишем

VEXpression

float n_min = detail (0, "min_nr", 0); float n_max = detail (0, "max_nr", 0); float nr = @nr; float min_v = chf("Min"); float max_v = chf("Max"); float nr_n = fit(clamp(nr, n_min, n_max), n_min, n_max, min_v, max_v); @nr = nr_n;

Получаем две настройки "min и max"выставляем их как у меня:

"nr" - новый нормированный параметр
"nr" - новый нормированный параметр

6) Удаляем ненужные атрибуты нодой Attribute Delete

Black Hole (Houdini Tutorial)

7) Нодой Attribute Promote переносим атрибут "nr" с Primitive на Point и конвертируем наши примитивы нодой Convert в полигональную модель.

Black Hole (Houdini Tutorial)

8) Создаём очередной цикл foreach_begin с теми же параметрами, что и в предыдущем.

Этот цикл нужен для того, чтобы спроецировать атрибут "nr" с меньшей сферы на большую в направлении настраиваемого нами вектора.
Этот цикл нужен для того, чтобы спроецировать атрибут "nr" с меньшей сферы на большую в направлении настраиваемого нами вектора.

8.1 В цикле создаем три ноды Attribute Wrangle ("attribwrangle_ID_1", "attribwrangle_ID_63", "attribwrangle_ID_0") подключенные как показано выше:

attribwrangle_ID_1

VEXpression

float iter = 63-detail (1, "iteration", 0); if (f@ID != iter+1) removeprim(0, @primnum, 1);

нода выбирает один конкретный примитив по его ID, который вычисляется на основе detail-атрибута "iteration" из второго входа. Остальные примитивы удаляются.

attribwrangle_ID_63

VEXpression

int iter = 63-detail (1, "iteration", 0); if (f@ID == iter) removeprim(0, @primnum, 1); if (f@ID == iter+1) removeprim(0, @primnum, 1);

нода удаляет все примитивы, у которых ID равен iter или iter + 1, где iter = 63 - iteration (где iteration берётся как detail с второго входа).

attribwrangle_ID_0

VEXpression

float iter = 63-detail (1, "iteration", 0); if (f@ID != iter) removeprim(0, @primnum, 1);

нода находит значение iter = 63 - iteration (где iteration берётся как detail с второго входа) и удаляет все примитивы, у которых атрибут ID не равен этому числу.

8.2 Создаем ноду Attribute Wrangle называем ее "attribwrangle_Vector" на нее подаем ноду "attribwrangle_ID_1"

attribwrangle_Vector

VEXpression

@nr=1; v@V = chv("Vector");
направление вектора настраиваем так же, как в моём примере.
направление вектора настраиваем так же, как в моём примере.

8.3 Создаем ноду Ray на первый вход подаем "attribwrangle_Vector" на второй "attribwrangle_ID_0"

настраиваем так же, как в моём примере.
настраиваем так же, как в моём примере.

8.4 Нодой Attribute Delete удаляем созданный нами вектор "v"

8.5 Блюрим наше полученное новое значение "nr" нодой Attribute Blur

Black Hole (Houdini Tutorial)

8.6 Объединяем всё с помощью ноды Merge:

ноды attribwrangle_ID_63, attribwrangle_ID_0, а также результат, полученный из ноды Attribute Blur и подаем на выход цикла.

9) Далее я создал ноду для кэширования (можно и без нее обойтись).

Black Hole (Houdini Tutorial)

10) Создаем ноду Transform и в ее параметры translate копируем параметры из ноды Transform_Main.

11) Параметры translate из ноды Transform_Main я также скопировал на созданную мной точку с помощью ноды Add.

Пишем: ch("../Transform_Main/tx"),  ch("../Transform_Main/ty"),  ch("../Transform_Main/tz") в соответствующие  окна параметров
Пишем: ch("../Transform_Main/tx"),  ch("../Transform_Main/ty"),  ch("../Transform_Main/tz") в соответствующие  окна параметров

12) Создаем ноду Attribute Wrangle на нее подаем нашу точку

VEXpression

string cam_path = chs("camera_path"); if (opfullpath(cam_path) == "") return; vector Pgeo = @P; matrix cam_xform = optransform(cam_path); vector Pcam = cracktransform(0, 0, 0, {0,0,0}, cam_xform); vector look = normalize(Pcam - Pgeo); vector up = {0,1,0}; vector side = normalize(cross(look, up)); up = normalize(cross(side, look)); matrix3 m = set( side.x, side.y, side.z, up.x, up.y, up.z, -look.x, -look.y, -look.z ); @orient = quaternion(m);

в созданный нами параметр Camera Path подаем нашу копию камеры

/stage/objnet_blak_hole/cam_Copy
Обращаю внимание, что используется именно копия нашей камеры, так как с камерой из сцены этот сетап не работает. Если кто знает, почему так происходит, пожалуйста, объясните или подскажите, как можно реализовать это проще. Заранее благодарю).
Обращаю внимание, что используется именно копия нашей камеры, так как с камерой из сцены этот сетап не работает. Если кто знает, почему так происходит, пожалуйста, объясните или подскажите, как можно реализовать это проще. Заранее благодарю).

13) Создаём ноду Copy to Points: на первый вход подаём наш Transform, на второй — Attribute Wrangle. Это позволяет направить сферу в ту сторону, куда смотрит камера.

14) Создаем Null для вывода ее через SOP Import в сцену.

15) В сцене создаем и назначаем на наши сферы материал "karmamaterial_lens"

в нем мы подключаем все как будет показано далее
в нем мы подключаем все как будет показано далее
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

Все эти параметры я настроил эмпирическим путём, методом проб и ошибок.

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

Black Hole (Houdini Tutorial)

Буду очень признателен, если кто-то поделится способом избавиться от этого).

Перейдём к созданию аккреционного диска, который делает нашу модель чёрной дыры более узнаваемой и кинематографичной.

При создании аккреционного диска я во многом опирался на материалы из этого видео.

Тем, кто дошёл до этого этапа, рекомендую с ним ознакомиться — это видео  для вас будет интересным и познавательным.

1) Создаем Grid

Black Hole (Houdini Tutorial)

2) Накладываем на него UV Texture

Black Hole (Houdini Tutorial)

3) Создаем Attribute Wrangler

Rune Over: Points

VEXpression

v@P.x = fit01(v@P.x, -1, 1); v@P.z = fit01(v@P.z, chf("inner_rad"), chf("outer_rad"));

Эта нода даёт два параметра:

  • Inner Rad — внутренний радиус (в моём случае в 3 раза больше горизонта событий).
  • Outer Rad — внешний радиус (в 10 раз больше минимального радиуса).

3) Далее создаем Attribute VOP называем его "flat_disc" в нем:

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Вот что у нас получилось. На этом этапе, в принципе, можно остановиться: найдите или создайте текстуру аккреционного диска и наложите её на модель — этого будет достаточно.
Вот что у нас получилось. На этом этапе, в принципе, можно остановиться: найдите или создайте текстуру аккреционного диска и наложите её на модель — этого будет достаточно.

Мне хотелось более тяжёлого и реалистичного варианта визуализации аккреционного диска, поэтому я решил использовать частицы вместо обычной геометрии/шейдинга.

1) Создадим точку в центре нодой Add.

2) Создаем Attribute Wrangler

Rune Over: Detail

VEXpression

float minR = chf("minR"); // минимальный радиус float maxR = chf("maxR"); // максимальный радиус int numRings = chi("numRings"); // число окружностей int ptsPerRing= chi("ptsPerRing"); // точек на каждой окружности float biasToMin = chf("biasToMin"); // 0..1 - смещение к minR (0 = равномерно, 1 = сильно к minR) numRings = max(numRings, 1); ptsPerRing = max(ptsPerRing, 1); biasToMin = clamp(biasToMin, 0.0, 1.0); float bias(float t; float b) { float k = pow(1.0 - b, 4.0); return pow(t, 1.0 + k*4.0); } int npts = npoints(0); for (int i = npts-1; i >= 0; i--) removepoint(0, i); for (int r = 0; r < numRings; r++) { float t = (numRings == 1) ? 0.0 : float(r) / float(numRings - 1); float tb = bias(t, biasToMin); float radius = lerp(minR, maxR, tb); for (int i = 0; i < ptsPerRing; i++) { float angle = (2.0 * M_PI * float(i)) / float(ptsPerRing); vector pos; pos.x = radius * cos(angle); pos.z = radius * sin(angle); pos.y = 0.0; int pt = addpoint(0, pos); setpointattrib(0, "radius", pt, radius, "set"); setpointattrib(0, "ring", pt, r, "set"); setpointattrib(0, "angle", pt, angle, "set"); } } i@numRings = numRings; i@ptsPerRing = ptsPerRing; f@minR = minR; f@maxR = maxR;

при помощи наших настроек мы можем менять:

Minr - минимальный радиус окружности;

Maxr - максимальный радиус окружности;

numRings - число окружностей;

ptsPerRing - количество точек на каждой окружности;

biasToMin - смещение к центру окружности.

Обращаю внимание на значение параметра Minr — оно в 3 раза больше радиуса нашего горизонта событий. Параметр Maxr настраивается визуально, но я делаю его в 10 раз больше минимального радиуса.
Обращаю внимание на значение параметра Minr — оно в 3 раза больше радиуса нашего горизонта событий. Параметр Maxr настраивается визуально, но я делаю его в 10 раз больше минимального радиуса.
у меня это выглядит так
у меня это выглядит так

4) Следующий Attribute Wrangler объединит все точки в окружности

VEXpression

int numRings = i@numRings; int ptsPerRing= i@ptsPerRing; int total_pts = npoints(0); for (int r = 0; r < numRings; r++) { int prim = addprim(0, "polyline"); for (int i = 0; i < ptsPerRing; i++) { int pt = r * ptsPerRing + i; addvertex(0, prim, pt); } int first_pt = r * ptsPerRing; addvertex(0, prim, first_pt); }
Black Hole (Houdini Tutorial)

5) Нодой Attribute Delete удаляем все атрибуты кроме тех которые нам далее понадобятся.

Black Hole (Houdini Tutorial)

6) Создаем цикл Foreach

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

в цикле....

6.1 Создаем ноду Resample

её создавать не обязательно, но для лучшей детализации всё-таки я создал эту ноду.
её создавать не обязательно, но для лучшей детализации всё-таки я создал эту ноду.

6.2 После создаем ноду Fuse (она нужна чтобы замкнуть наши окружности).

Snap Distance: 0.001

6.3 Создаём Attribute Wrangle:

на первый вход подаём то, что приходит с ноды "Fuse", а на второй — то, что приходит с ноды "attribwrangle_radius_g".

VEXpression

vector v1 = normalize(@P)*-1; vector v2; float minR = detail (0, "minR", 0); float maxR = detail (0, "maxR", 0); float GM = detail (1, "GM_kef", 0); float r = @radius; if(@ptnum != @numpt-1){ v2 = normalize(@P - point(0, "P", @ptnum + 1)); }else{ v2 = normalize(@P - point(0, "P", 1)); } float amount = (fit(r, minR, maxR, 0.1, 1.1)-0.1) * chf("Amount"); float speed = sqrt(GM/r) * chf("Speed") * 1e-7; int Direction = chi("Direction"); v@v = lerp (Direction*v2,v1,amount); v@v = normalize (v@v) * speed; f@max_speed = speed;
параметры настраиваем либо так же, как у меня, либо так, чтобы направление вектора "v" у тебя совпадало с моим.
параметры настраиваем либо так же, как у меня, либо так, чтобы направление вектора "v" у тебя совпадало с моим.

Показатели:

Amount — степень наклона вектора к окружности.

Speed — сила вектора (его длина).

Direction — направление вектора (-1 — завитки по часовой стрелке, 1 — против часовой стрелки).

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

Black Hole (Houdini Tutorial)

Переходим к следующему этапу...

Black Hole (Houdini Tutorial)

7) Создаем Tube

Radius Scale должен быть равен минимальному радиусу аккреционного диска
Radius Scale должен быть равен минимальному радиусу аккреционного диска

8) Создаем PolyExtrude

В Distance мы задаём разницу между максимальным и минимальным радиусами аккреционного диска. Параметры Inset и Divisions подбираем эмпирически — меняем значения и смотрим, при каких результат выглядит лучше.
В Distance мы задаём разницу между максимальным и минимальным радиусами аккреционного диска. Параметры Inset и Divisions подбираем эмпирически — меняем значения и смотрим, при каких результат выглядит лучше.

9) Создаем ноду VDB from Polygons и Convert VDB

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

10) Далее создаём ноду Volume VOP: на первый вход подаём то, что приходит с ноды Convert, а на второй — наш вектор "v", созданный ранее в цикле (см. выше).

в Volume VOP:

Black Hole (Houdini Tutorial)
Подчеркнутые параметры мы выносим
Подчеркнутые параметры мы выносим
Нажимаем на нужный параметр средней кнопкой мыши и в появившемся меню выбираем Promote Parameter
Нажимаем на нужный параметр средней кнопкой мыши и в появившемся меню выбираем Promote Parameter
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

у нас получаться следующие настройки :

Black Hole (Houdini Tutorial)

11) Далее создаем ноду Null называем ее "OUT_V" (она нам пригодиться далее).

для визуализации можете построить следующую нодовую структуру

меняем только выделенный параметр
меняем только выделенный параметр
ZX Plane
ZX Plane
YZ Plane
YZ Plane

Идем дальше...

Black Hole (Houdini Tutorial)

12) Переходим обратно к ноде "flat_disc" и создаем Attribute Delete и Subdivide (Depth = 2) (не обязательно)

13) Создаем Attribute VOP в нем:

выделенный параметр выносим (Speed у меня равен 0.5)
выделенный параметр выносим (Speed у меня равен 0.5)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

14) Создаем Attribute Wrangle

VEXpression

vector center = chv("center"); float radius = ch("radius"); float ramp_val = chramp("mask_ramp", 0.0); float dist = length(@P - center); float t = clamp(dist / radius, 0, 1.0); float mask = chramp("mask_ramp", t); @density = mask-@density;
Не забудьте включить отображение параметра density, а также при необходимости можно настроить его затухание с помощью выделенных справа параметров.
Не забудьте включить отображение параметра density, а также при необходимости можно настроить его затухание с помощью выделенных справа параметров.

15) Создаем Null и называем его "OUT_EMTTER"

16) Теперь создадим наши точки...

создаем DOP Network в нем:

Black Hole (Houdini Tutorial)

идем по порядку...

16.1 source_first_input

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

16.2 popadvectbyvolumes

Black Hole (Houdini Tutorial)

16.3 popforce

Black Hole (Houdini Tutorial)

16.4 popdrag

Black Hole (Houdini Tutorial)
float relage = @age/@life; airresist *= relage;

16.5 Результат должен быть приблизительно таким:

17) Нодой Attribute Delete удаляем ненужные атрибуты кроме "v", "density", "P"

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

Создадим Group

Black Hole (Houdini Tutorial)

на вход параметра Bounding Object подаём объект transform_black_hole, масштабированный нодой Transform в 3 раза (то есть до значения, равного минимальному радиусу горизонта событий).

19) Также при желании можно ограничить случайно вылетающие частицы нашим созданным объёмом. Для этого снова создаём Group, на вход параметра Bounding Object подаём объект из ноды polyextrude и инвертируем эту группу с помощью ноды Group Invert.

20) Созданные нами группы подаём на ноду Delete, а затем удаляем все группы с помощью ноды Group Delete, так как далее они нам уже не понадобятся.

21) Далее создаю файл кэша с помощью ноды File Cache. Длительность симуляции выбирайте под свои задачи — мне, к примеру, хватило 310 кадров, и финальный рендер я делал как раз на этом кадре.

Самым интересным этапом для меня стала настройка цвета излучения (температуры) и яркости аккреционного диска. Я хотел добиться максимально физически достоверного результата, поэтому использовал несколько способов вычисления температуры каждой точки: по скорости v, плотности газа density, а также в зависимости от расстояния до центра чёрной дыры. Дополнительно применил эффект Доплера для воссоздания красного смещения относительно наблюдателя.

1) Начнем...

общая схема подключения — можно к ней возвращаться, если дальше что‑то покажется непонятным.
общая схема подключения — можно к ней возвращаться, если дальше что‑то покажется непонятным.

Создаем ноду Attribute Wrangler называю ее "attribwrangle_pose_radius"

VEXpression

v@bh_pos = chv("bh_pos"); vector P_world = @P; vector v_world = @v; float r = length(P_world - v@bh_pos); float r_min = chf("r_min"); r = max(r, r_min); f@radius = r;
позицию (Bh Pos) я оставляю в нуле, а минимальный радиус задаю равным радиусу горизонта событий — в моём случае это значение равно 3. После создания сетапа вы можете покрутить этот параметр и посмотреть, как он влияет на общую картину.
позицию (Bh Pos) я оставляю в нуле, а минимальный радиус задаю равным радиусу горизонта событий — в моём случае это значение равно 3. После создания сетапа вы можете покрутить этот параметр и посмотреть, как он влияет на общую картину.

2) Создаём очередной Attribute Wrangler и называем его "attribwrangle_temperature".

на первый вход подаём ноду "attribwrangle_pose_radius",

на второй — выход нашего цикла, в котором мы создавали на окружностях вектор "v",

на третий — ноду "transform_black_hole".

VEXpression

float r = f@radius; vector P_world = @P; vector v_world = @v; vector bh_pos = @bh_pos; float max_speed = prim (1, "speed", 0); float speed = length(f@v); float dens = max(f@density, 0.0); f@max_speed = point (1, "max_speed", 0); float max_dens = chf("max_density"); float r_hot = chf("hot_radius"); float ns = clamp(speed / max_speed, 0.0, 1.0); f@nd = clamp(dens / max_dens, 0.0, 1.0); f@nr = clamp(1.0 - (r / r_hot), 0.0, 1.0); vector dir = normalize(bh_pos - P_world); vector vrad = dot(v_world, dir) * dir; vector vtang = v_world - vrad; float shear = length(vtang); float max_shear = chf("max_shear"); float nsh = clamp(shear / max_shear, -0.0, 1.0); float temp_raw = ns*0.4 + f@nd*0.2 + f@nr*0.2 + nsh*0.2; float T_min = chf("T_min"); float T_max = chf("T_max"); float M = detail(2, "mass_sun", 0); if (T_min == 0 && T_max == 0) { M = max(M, 1e-6); float m_sun = M; float T_max_n = 1.2e7 * pow(m_sun, -0.25); float T_min_n = 1.0e4 * pow(m_sun, -0.25); T_max = clamp(T_max_n, 1.0e5, 1.0e8); T_min = clamp(T_min_n, 1.0e3, 1.0e6); } else { float T_min = chf("T_min"); float T_max = chf("T_max"); } f@GM = detail (2, "GM_kef", 0); float potE = pow((sqrt(f@GM / r) * 1e-7),2); float v_orb = sqrt(f@GM / r) * 1e-7; float dv = abs(length(v_world) - v_orb); float max_dv = chf("max_dv"); float ndv = clamp(dv / max_dv, 0.0, 1.0); float pot_max = chf("pot_max"); float npot = clamp(potE / pot_max, 0.0, 1.0); float temp_raw2 = 0.5*npot + 0.3*ndv + 0.2*f@nd; int temperature_regim = chi ("Temperatur_regim"); if (temperature_regim == 0) { f@temperature = fit01((temp_raw), T_min, T_max); } else if (temperature_regim == 1) { f@temperature = fit01((temp_raw2), T_min, T_max); } else { f@temperature = fit01((temp_raw + temp_raw2)/2, T_min, T_max); }
советую для начала выставить параметры так же, как у меня. После создания ноды Color и её настройки вы сможете поэкспериментировать с каждым параметром и посмотреть, как именно он влияет на сцену.
советую для начала выставить параметры так же, как у меня. После создания ноды Color и её настройки вы сможете поэкспериментировать с каждым параметром и посмотреть, как именно он влияет на сцену.

Max Density - подбираешь под сцену

Hot Radius - характерный радиус нагрева

Max Shear - "трение" как мера сдвига скорости относительно центра (подбираешь под сцену)

T Min - минимальная температура

T Max - максимальная температура

Max Dv - добавочное трение к другому способу вычисления температуры (подбираешь под сцену)

Pot Max - потенциал (подбираешь под сцену)

Temperatur Regim - способы вычисления температуры (0 - первый способ (по скорости и плотности), 1 - второй способ (в зависимости от массы и расстояния до черной дыры), любое другое значение (кроме 0 и 1) — оба способа одновременно.

Если T Min и T Max оставить равными нулю (как в моём примере), то значения температур нода подбирает сама — на основе массы звезды, согласно формуле тонкого аккреционного диска Шакуры–Сюняева.

3) Вычисляем максимальное и минимальное значения температуры, полученной на предыдущей ноде, для этого создадим Attribute Wrangler и назовем его "attribwrangle_temp_max_min"

Run Over: Detail

VEXpression

string attr = "temperature"; int pts[] = pcfind(0, "P", {0,0,0}, 1e9, npoints(0)); float min_val = 1e9; float max_val = -1e9; int min_pt = -1; int max_pt = -1; int n = npoints(0); for (int i = 0; i < n; i++) { float v = point(0, attr, i); if (v < min_val) { min_val = v; min_pt = i; } if (v > max_val) { max_val = v; max_pt = i; } } setdetailattrib(0, "min_temp", min_val, "set"); setdetailattrib(0, "max_temp", max_val, "set");

4) Наконец мы сможем наглядно увидеть что у нас получилось, для этого создаем ноду Color

Black Hole (Houdini Tutorial)

В рампе мы уже настраиваем наш цвет. Сразу отмечу, что это художественный вариант: в реальности возле чёрных дыр газ разогревается до таких высоких температур, что максимум его излучения уходит в ультрафиолет и рентген, а видимый свет играет гораздо меньшую роль.

В Range пишем

detail("../attribwrangle_temp_max_min", "temp_min", 0)
detail("../attribwrangle_temp_max_min", "temp_max", 0)

должно получиться примерно вот так ...

можете поэкспериментировать с цветом
можете поэкспериментировать с цветом

Если не хочется сильно заморачиваться, можно просто создать Attribute Wrangler и назвать его "attribwrangle_Temp_to_Color" — он автоматически сгенерирует для нас цвет от темно-красного к голубоватому.

VEXpression

float tmin = detail(0,"temp_min",0); float tmax = detail(0,"temp_max",0); float T = fit(clamp(@temperature, tmin, tmax), tmin, tmax, 0.0, 1.0); vector c; // 0..0.3 — тёмно-красный → оранжевый if (T < 0.3) { float k = fit(T, 0.0, 0.3, 0.0, 1.0); c = lerp({0.1, 0.0, 0.0}, {1.0, 0.3, 0.0}, k); } // 0.3..0.7 — оранжевый → жёлтый → белый else if (T < 0.7) { float k = fit(T, 0.3, 0.7, 0.0, 1.0); c = lerp({1.0, 0.3, 0.0}, {1.0, 1.0, 1.0}, k); } // 0.7..1.0 — белый → голубоватый (перегоревший, очень горячий) else { float k = fit(T, 0.7, 1.0, 0.0, 1.0); c = lerp({1.0, 1.0, 1.0}, {0.7, 0.9, 1.0}, k); } @Cd = c;
Black Hole (Houdini Tutorial)

И покажу ещё один вариант, более корректный в физическом смысле.

Создаем Attribute Wrangler и называем его "attribwrangle_temp_real"

VEXpression

float T = @temperature; float tmin = detail(0,"temp_min",0); float tmax = detail(0,"temp_max",0); // Ограничим диапазон температур, чтобы избежать артефактов T = clamp(T, tmin, tmax); float temp = T / 100.0; float r, g, b; // Красный канал if (temp <= 66.0) { r = 1.0; } else { float t = temp - 60.0; r = 1.292936186062745 * pow(t, -0.1332047592); } // Зелёный канал if (temp <= 66.0) { float t = temp; g = 0.3900815787690196 * log(t) - 0.6318414437886275; } else { float t = temp - 60.0; g = 1.129890860895294 * pow(t, -0.0755148492); } // Синий канал if (temp >= 66.0) { b = 1.0; } else if (temp <= 19.0) { b = 0.0; } else { float t = temp - 10.0; b = 0.5432067891101961 * log(t) - 1.19625408914; } r = clamp(r, 0.0, 1.0); g = clamp(g, 0.0, 1.0); b = clamp(b, 0.0, 1.0); @Cd = set(r, g, b);

получим вот такой результат....

Black Hole (Houdini Tutorial)

Бонусом рендер)....

Выглядит интересно, но я всё же предпочитаю более художественный подход — люблю, когда есть возможность тонко настраивать цветовые параметры (Color).
Выглядит интересно, но я всё же предпочитаю более художественный подход — люблю, когда есть возможность тонко настраивать цветовые параметры (Color).

4) Далее реализуем эффект Доплера

Первый вариант — через направление камеры и вектор скорости v. Второй вариант — через расстояние до центра (причём у меня второй вариант уже учитывает первый).

4.1 Создаем Attribute Wrangler, называем "attribwrangle_dopler_ef_V"

VEXpression

string cam_pos = chs("cam_path"); matrix cam_xform = optransform(cam_pos); vector Pcam = cracktransform(0, 0, 0, {0,0,0}, cam_xform); vector dir_to_cam = normalize(Pcam - @P); float c = chf("speed_of_light"); float v_rad = dot(@v, dir_to_cam); float beta = clamp(v_rad / c, -0.99, 0.99); float freq_factor = sqrt((1.0 - beta) / (1.0 + beta)); float lambda_factor = 1.0 / max(1e-6, freq_factor); float z = lambda_factor - 1.0; f@lambda_shift = lambda_factor; f@redshift = z; vector col = @Cd; float red_enh = clamp(1.0 + z * 2.0, 0.0, 3.0); float blue_enh = clamp(1.0 - z * 1.5, 0.0, 3.0); float bright = clamp(1.0 - z * 0.8, 0.2, 2.0); col.r *= red_enh; col.b *= blue_enh; col *= bright; col = clamp(col, 0.0, 10.0); @Cd = col;

в Cam Path подаем нашу копию камеры:

/stage/objnet_blak_hole/cam_Copy

Speed of Light — параметр подбирается экспериментально для достижения наилучшего визуального эффекта (в моей сцене значение равно 40).

4.2 Создаем Attribute Wrangler, называем"attribwrangle_dopler_r"

VEXpression

vector center = chv("center"); float k_grav = chf("grav_factor"); float r = length(@P - center); float z_grav = k_grav / max(r, 1e-3); float lambda_factor_grav = 1.0 + z_grav; f@z_grav = z_grav; f@lambda_grav = lambda_factor_grav; float lambda_total = lambda_factor_grav * max(f@lambda_shift, 1.0); f@lambda_total = lambda_total; vector col = @Cd; float red_boost = clamp(1.0 + z_grav * 3.0, 0.0, 5.0); float sat = 1.0 / (1.0 + z_grav * 2.0); float maxc = max(col.r, max(col.g, col.b)); float minc = min(col.r, min(col.g, col.b)); float v = maxc; float s = (maxc > 0.0) ? (maxc - minc) / maxc : 0.0; s *= sat; col.r *= red_boost; col = clamp(col, 0.0, 10.0); @Cd = col;

Center - я не трогаю (оставляю все параметры на 0)

Grav Factor - также как и в прошлой ноде Speed of Light подбирается экспериментально (у меня равно 1.3).

5) Теперь создадим ноду, которая будет формировать нашу яркость (emission).

создаем Attribute Wrangler, называем"attribwrangle_emis"

VEXpression

// Нормализованная температура 0..1 для ремапа в эмиссию float T_min = detail(0,"temp_min",0); float T_max = detail(0,"temp_max",0); f@temp_norm = clamp( (f@temperature - T_min) / (T_max - T_min), 0.1, 1.0 ); // Множитель от плотности float dens_boost = pow(f@nd, f@density * chf("density_power")); // например 0.5–2.0 // Множитель от радиуса (центр ярче) float r_boost = pow(f@nr, chf("radius_power")); // например 1–3 // Сырая эмиссия float emission_raw = f@temp_norm * dens_boost * r_boost; // Общий масштаб яркости float emit_scale = chf("emit_scale"); // например 10–100 f@emission = emission_raw * emit_scale;
параметры настройте как у меня
параметры настройте как у меня

6) Так же создадим эффект доплера для нашей эмиссии.

создаем Attribute Wrangler, называем"attribwrangle_dopler"

VEXpression

string cam_pos = chs("cam_path"); matrix cam_xform = optransform(cam_pos); vector Pcam = cracktransform(0, 0, 0, {0,0,0}, cam_xform); vector to_cam = normalize(Pcam - @P); // Проекция скорости на луч зрения float v_los = dot(normalize(@v), to_cam); // Доплеровский множитель (очень упрощённо) float doppler = 1.0 + 0.5 * v_los; // v_los в диапазоне [-1,1] doppler = clamp(doppler, 0.2, 2.0); // Применить доплер к эмиссии @emission = fit01((@emission * doppler), 0, 1);

в Cam Path подаем нашу копию камеры:

/stage/objnet_blak_hole/cam_Copy

7) Очередной нодой Attribute Delete удаляем лишние атрибуты оставляем только "Cd", "emission", "v".

8) Для перемещения диска по сцене я создаю ноду Transform и копирую в её параметры translate и rotate значения из ноды Transform_Main. Благодаря этому движение диска синхронизировано с основным объектом.

9) Создаем Null называем его "geo_disk" и импортируем его в сцену нодой SOP Import.

про настройку материала расскажу позже...

Мы уже на финишной прямой: осталась заключительная деталь — фотонное кольцо, яркое кольцевое свечение вокруг чёрной дыры, возникающее из-за многократного огибания светом области сильного гравитационного поля.

10) Создаем circle с такими параметрами:

плоскость окружности долна совпадать с плоскостью аккреционного диска 
плоскость окружности долна совпадать с плоскостью аккреционного диска 

Uniform Scale:

ch("../transform_blak_hole/scale")*1.5

Divisions:

ch("scale")*100

11) Нодой fuse замыкаем circle

12) Создаём на точках вектор скорости "v": для этого добавляем ноду Attribute Wrangle на первый вход подаем fuse, на второй — выход нашего цикла, в котором мы создавали на окружностях вектор "v", и называем её "attribwrangle_foton_vec".

VEXpression

vector v1 = normalize(@P)*-1; vector v2; float speed = point(1, "max_speed", 0); if(@ptnum != @numpt-1){ v2 = normalize(@P - point(0, "P", @ptnum + 1)); }else{ v2 = normalize(@P - point(0, "P", 1)); } v@v = v2*speed; v@v_noise = -1*@v;

13) Создаем ноду Copy and Transform и ноду Scatter

параметры настраивал интуитивно (просто скопируйте)
параметры настраивал интуитивно (просто скопируйте)
Black Hole (Houdini Tutorial)

14) Создаём две ноды Attribute Noise, чтобы добавить немного шума нашим точкам, и выравниваем их в центр сцены с помощью ноды Match Size.

Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

15) Крайний элемент создаем ноду Transform называем ее "transform_Rotate_Anim" и анимируем ее круговое движение.

Black Hole (Houdini Tutorial)

16) Создаём ноду Null "photon_circle" и объединяем её с аккреционным диском нодой Merge до ноды "attribwrangle_pose_radius".

17) Настроим материал аккреционного диска + фотонное кольцо.

В сцене создаем и назначаем на наши диск материал "mtlxmaterial_disk"
В сцене создаем и назначаем на наши диск материал "mtlxmaterial_disk"
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)
Black Hole (Houdini Tutorial)

Рендер

Black Hole (Houdini Tutorial)

Сцену настраивайте на своё усмотрение (можете повторить мои настройки), но есть пара нюансов. В ноде karmarendersettings обязательно установите Refraction Limit равным 64 (количество создаваемых нами сфер в гравитационной линзе) или 128, 256, 512 и т.п. — главное, чтобы значение было кратно 64.

остальные настройки подстраивайте под возможности вашего железа и под конкретную сцену
остальные настройки подстраивайте под возможности вашего железа и под конкретную сцену

также, если вы хотите сохранить HDRI-карту при рендере в Dome Light, во вкладке Karma включите опцию Render Light Geometry.

Black Hole (Houdini Tutorial)

Хочу искренне поблагодарить всех, кто досмотрел этот туториал до конца или хотя бы пролистал его целиком. Мне очень приятно осознавать, что моя работа могла кому‑то пригодиться или хотя бы просто заинтересовать. При подготовке я старался опираться на реальные физические свойства и довольно долго изучал этот вопрос, чтобы сделать материал как можно более правдоподобным, а изображение — одновременно физически убедительным и художественно красивым. Для меня важно, чтобы за красивой картинкой стояла какая‑то логика и понимание процессов, а не просто набор эффектов. Поэтому, если хотя бы один человек смог почерпнуть из этого туториала что‑то новое, вдохновиться на свои эксперименты или посмотреть на знакомые вещи под другим углом, значит всё это время и усилия были вложены не зря.

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

Спасибо вам за внимание и интерес)

78
7
6
2
37 комментариев