この記事は、 Houdini Apprentice Advent Calendar 2025 の19日目の記事になります。
はじめに
はじめまして、mmと申します。
今回がHoudini Apprentice Advent Calendar初参加となります。
普段はメーカー勤務のエンジニアで、趣味でHoudiniを使いながらCG制作をしています。
記事の内容
タイトルの通り、観覧車のイルミネーションをHoudiniで作ってみました。
下記動画は実際に作ってみたイルミネーションの一例となります。
今回はいくつかのパターンを紹介できたらと思います。
環境
- Linux Mint
- Houdini 21.0.512
ファイル
興味がある方はぜひ。
ファイル
全体像
今回作成した観覧車のイルミネーションは、以下のステップで作成しました。
- Step 1: ベースとなる観覧車モデルの作成
- Step 2: ライト(球体)を配置するPointの作成
- Step 3: GroupやVEXによるパターンの制御
- Step 4: マテリアルの設定
Step 1:観覧車のモデリング
まず、観覧車のモデリングとなります。今回はせっかくなので1からHoudiniでモデリングをしました。
構成要素を分かりやすくするため画像では色分けしていますが、 大きく分けて、車輪(緑色)、支柱(青色)、ゴンドラ(ピンク色)、時計(黄色)の4つで構成されています。
この中で、Step 2にも関わる車輪のモデリングについて説明したいと思います。

方針決め
まずモデリングを開始する前に、実際の観覧車の車輪部分を観察すると、棒やワイヤーが組み合わさって出来ていることが分かったため、各パーツをtube、もしくはlineとpolywireの組み合わせのどちらで作っていくかを考えました。(画像は、現地で実際に撮影した写真です。)

結果から言うと、今回はlineとpolywireの組み合わせで作ることにしました。
理由は、Step 2でライトを配置するPointを作成する際に観覧車の車輪に沿って配置をするためです。lineで観覧車の形を作れば、lineのpointがそのままライトを配置するためにも使えるため、手間が省けると考えました。
モデリング
方針を決めたので、実際にモデリングをしていきます。
①車輪のベースの作成
まず、lineを呼び出し、transformで形を整えて、resampleでlineにPointを生成、nullを繋いで名前をBASE_GEOとつけました。それぞれのノードの役割は下記の通りとなります。
-
transform→lineを傾けて、車輪の幅が中心から外側に向かって狭くする -
resample→各パーツのモデリングのためのpoint生成(ライト配置用のPointは別で生成する) -
null→車輪の各パーツを作る際にobject_mergeで参照する際に使用
②車輪の各パーツの作成
次に、BASE_GEOを使って、車輪の各パーツ(下記画像のPARTS_A〜L)を作成しました。
画像の2枚目、3枚目は少し分かりにくいですがPARTS_A〜Lをパーツごとに色分けしたものです。
ここまでが、観覧車のモデリングとなります。



Step 2:ライトを配置するPointの作成
ここでは車輪部分に沿ってライトを配置するPointの作成となりますが、やったことはシンプルです。
具体的にはStep1でlineで作成したパーツの骨格にresampleをつないでるだけです。
車輪の好きな位置にライトを配置してよいかと思いますが、今回はシンプルにPARTS_Aだけにライトを配置していきます。(下記画像、水色になっているのがpoint)
なお、今回は光らせたときの見た目を見ながら調整した結果、4470個のpointにライトを配置しました。

Step 3:GroupやVEXによるパターンの制御
ここからイルミネーションを作成していきます。
まず、このStepは、step1,2と同じくSOP、またはSolarisどちらでも出来る作業なのですが、「パターンを作成して、すぐにレンダリング結果を確認」がしたかったので、Solarisで作業しました。

この部分がイルミネーションの肝となる部分です。
それぞれのinstancer(オレンジ色のノード)で異なるイルミネーションを作っています。
各ノードの役割は以下の通りです。Step3では、instancerの中身を見ていきます。
-
sphere→イルミネーションのライトとして使うオブジェクト -
instancer→入力されたpointに対してライトを配置し、イルミネーションの挙動を制御する部分 -
materiallibrary→ライト用のマテリアル
instancerの中身
こちらは、記事の冒頭で紹介したイルミネーションのinstancerの中身です。
挙動を設定する部分以外は、他のパターンも同じ構成です。
なお、@emissionはイルミネーションの明るさを調整するために作成しています。
挙動の設定については、動画等で実際のイルミネーションを見ていると複雑な動きをしながら、様々な色に変化していることがわかります。このような挙動を作るためには、光らせたい「位置」と「色」をどのように指定するかが鍵となります。
パターンの紹介
今回作成したパターンのいくつかを紹介していきます。
複雑な動きをさせようとすると、どうしてもvexで計算式を書いていく必要があったのですが、
意図した位置のpointを選択できたときはとても嬉しかったです。
単にノードをつなぐだけでなく数学的な知識を使って作品が作れるというのも、Houdiniを好きになった理由の1つです。
A. 外周だけ光るイルミネーション


こちらはジオメトリで囲んだ範囲を別の色で光らせてたり消したりするパターンです。
位置とサイズを調整したtubeをcopyで時間経過に応じて回転させながら複製したものをgroupにつなぐことで光らせたい場所のグループを作成しています。
2周目からは、1周目に複製されたtubeと重なる位置に再度tubeが生成されます。それによって重なった位置のpointがグループから外れるため、光っていた場所が順番に消えていくようになっています。
B. 渦巻状に動くイルミネーション

こちらは、grouprangeを使って一部分だけ別の色に光らせて、渦巻+放射状に動くパターンです。
grouprangeは指定した範囲のポイントやプリミティブを選択してグループを作るノードです。
grouprangeで作成したグループだけcolorで別の色にすることで渦巻状の模様を作成し、offsetに$Fを入力することで、フレームごとに模様を動かすことが出来ます。
grouprangeだけでも、こんなことが出来るとは、作ってみたときビックリしました。
C. 立方体が回転しているようなイルミネーション
こちらは、vexを使って立方体が回転しているように見えるよう光らせるパターンです。
最初、「A. 外周を発光させる」と同じように、光らせたい場所をcubeで囲んで、回転させたらできるのでは?と思っていたのですが、光る部分と暗い部分の境界線を綺麗に分けることが出来なかったので、vexを利用しました。
結構長いので、コードは折りたたんでいます。興味がある人は見てみてください。
vex
vector cube_verts[] = {
{1, 1, 1},
{-1, 1, 1},
{-1, -1, 1},
{1, -1, 1},
{1, 1, -1},
{-1, 1, -1},
{-1, -1, -1},
{1, -1, -1}
};
int faces[] = {
0, 1, 2, 3,
4, 7, 6, 5,
0, 3, 7, 4,
1, 5, 6, 2,
0, 4, 5, 1,
3, 2, 6, 7
};
vector face_colors[] = {
{1, 0, 0},
{0, 1, 1},
{0, 0, 1},
{1, 1, 0},
{0, 1, 0},
{1, 0, 1}
};
float angle_x = @Time * 0.5;
float angle_y = @Time * 0.7;
float angle_z = @Time * 0.3;
float scale = chf("scale");
if(scale == 0) scale = 1.0;
matrix3 rot_mat = ident();
rotate(rot_mat, angle_x, {1, 0, 0});
rotate(rot_mat, angle_y, {0, 1, 0});
rotate(rot_mat, angle_z, {0, 0, 1});
vector2 current_P = set(@P.x, @P.y);
vector final_color = {0, 0, 0};
float max_z = -10000.0;
int num_faces = len(faces) / 4;
for (int i = 0; i < num_faces; i++) {
vector2 p[];
float face_z_sum = 0;
for(int j=0; j<4; j++) {
int idx = faces[i*4 + j];
vector vert = cube_verts[idx];
vert = vert * rot_mat * scale;
p[j] = set(vert.x, vert.y);
face_z_sum += vert.z;
}
float avg_z = face_z_sum / 4.0;
if (avg_z <= max_z) continue;
int hit = 0;
vector2 v_tri[];
float d1, d2, d3;
int tri_check_pass = 0;
v_tri[0] = p[0]; v_tri[1] = p[1]; v_tri[2] = p[2];
d1 = (current_P.x - v_tri[1].x) * (v_tri[0].y - v_tri[1].y) - (v_tri[0].x - v_tri[1].x) * (current_P.y - v_tri[1].y);
d2 = (current_P.x - v_tri[2].x) * (v_tri[1].y - v_tri[2].y) - (v_tri[1].x - v_tri[2].x) * (current_P.y - v_tri[2].y);
d3 = (current_P.x - v_tri[0].x) * (v_tri[2].y - v_tri[0].y) - (v_tri[2].x - v_tri[0].x) * (current_P.y - v_tri[0].y);
if ( ((d1 < 0) && (d2 < 0) && (d3 < 0)) || ((d1 > 0) && (d2 > 0) && (d3 > 0)) ) {
hit = 1;
}
if (hit == 0) {
v_tri[0] = p[0]; v_tri[1] = p[2]; v_tri[2] = p[3];
d1 = (current_P.x - v_tri[1].x) * (v_tri[0].y - v_tri[1].y) - (v_tri[0].x - v_tri[1].x) * (current_P.y - v_tri[1].y);
d2 = (current_P.x - v_tri[2].x) * (v_tri[1].y - v_tri[2].y) - (v_tri[1].x - v_tri[2].x) * (current_P.y - v_tri[2].y);
d3 = (current_P.x - v_tri[0].x) * (v_tri[2].y - v_tri[0].y) - (v_tri[2].x - v_tri[0].x) * (current_P.y - v_tri[0].y);
if ( ((d1 < 0) && (d2 < 0) && (d3 < 0)) || ((d1 > 0) && (d2 > 0) && (d3 > 0)) ) {
hit = 1;
}
}
if (hit == 1) {
max_z = avg_z;
final_color = face_colors[i];
}
}
@Cd = final_color;
D. 複数の円が回転しているようなイルミネーション
こちらは、vexを使って複数の円が回転しているように光らせるパターンです。
こちらのvexについても、興味がある方はぜひ見てみてください。
vex
float r1_out = chf("r1_outer");
if(r1_out == 0) r1_out = 1.0;
float r1_in = chf("r1_inner");
if(r1_in == 0) r1_in = 0.75;
float r2_out = chf("r2_outer");
if(r2_out == 0) r2_out = 0.65;
float r2_in = chf("r2_inner");
if(r2_in == 0) r2_in = 0.4;
float r3_size = chf("r3_size");
if(r3_size == 0) r3_size = 0.3;
vector c1_f = {1.0, 0.0, 0.0};
vector c1_b = {1.0, 0.0, 0.0};
vector c2_f = {0.0, 1.0, 0.0};
vector c2_b = {0.0, 1.0, 0.0};
vector c3_f = {0.0, 0.0, 1.0};
vector c3_b = {0.0, 0.0, 1.0};
matrix3 m1 = ident();
matrix3 m2 = ident();
matrix3 m3 = ident();
rotate(m1, @Time * 0.5, {1, 0, 0});
rotate(m1, @Time * 0.2, {0, 1, 0});
rotate(m2, @Time * 1.0, {0, 0, 1});
rotate(m2, @Time * 0.8, {1, 1, 0});
rotate(m3, @Time * 2.0, {0, 1, 0});
rotate(m3, @Time * 1.5, {1, 0, 1});
float px = @P.x;
float py = @P.y;
vector final_color = {0, 0, 0};
float max_z = -10000.0;
{
vector n = {0, 0, 1} * m1;
if (abs(n.z) > 0.001) {
float pz = -(px * n.x + py * n.y) / n.z;
float dist = length(set(px, py, pz));
if (dist <= r1_out && dist >= r1_in) {
if (pz > max_z) {
max_z = pz;
final_color = (n.z > 0) ? c1_f : c1_b;
}
}
}
}
{
vector n = {0, 0, 1} * m2;
if (abs(n.z) > 0.001) {
float pz = -(px * n.x + py * n.y) / n.z;
float dist = length(set(px, py, pz));
if (dist <= r2_out && dist >= r2_in) {
if (pz > max_z) {
max_z = pz;
final_color = (n.z > 0) ? c2_f : c2_b;
}
}
}
}
{
vector n = {0, 0, 1} * m3;
if (abs(n.z) > 0.001) {
float pz = -(px * n.x + py * n.y) / n.z;
float dist = length(set(px, py, pz));
if (dist <= r3_size) {
if (pz > max_z) {
max_z = pz;
final_color = (n.z > 0) ? c3_f : c3_b;
}
}
}
}
@Cd = final_color;
Step 4: マテリアルの設定
最後に作成したイルミネーションをレンダリングするために、マテリアルの設定を行います。
Geometry Pathに/pattern_*とすることで、switchでイルミネーションのパターンを切り替えてもマテリアルのアサインが解除されないようにしています。
mtlxstandard_surfaceに接続している各ノードの役割は下記の通りです。
-
mtlxgeompropvalue→pointに設定されている@emission(イルミネーションの明るさ)を取得 -
mtlxgeomcolor→pointに設定されている@Cd(色)を取得
おわりに
自由自在にpointを選択できるHoudiniを使って、観覧車のイルミネーションを作ってみました。
みなさんもぜひオリジナルのイルミネーションを作ってみてください。







