環境:Gaea 2.0.2.3, Houdini 20.0.751
Gaeaを使った地形と転石について。DOPシミュレーションを使って整えます。Unreal Engineに出力することを目的としています。
転石の分布マスク画像を作成する
最近新しいバージョンになったGaea2のDebrisノードを使う。
1km×1kmの山岳地形。
DebrisノードのDebris出力で転石の分布画像を作成できる。
Erosion2で浸食させたハイトマップと、デブリ(転石)のマスクマップが生成されます。
ハイトマップを読み込む
HeightField File SOPでGaeaから出力したハイトマップを読み込み、四方の長さと高さを入力する。Convert SOPでポリゴンメッシュへ変換する。その後Transform SOPのY軸回転に90を入力して方向を合わせる。
UV Texture SOPでUV値を合わせる。
Gaeaからのデブリ画像をAttribute from Map SOPで貼り付ける。Export Attributeにdensityと入力しておく。
Scatter SOPでdensityアトリビュートを使って、8000個のポイントを分布させたところ。
ポイント群に岩の種類を割り当てる
続けて、ポイントに岩の種類を割り振る。岩の種類は小、中、大の三種類とした。
岩の種類のインデックス番号をポイントに割り振るコード。ここでは小さい岩を10、中の岩を6、大きい岩を3の割合で分布させることにした。
//
// ポイントに設定した割合でグループ番号を割り振る
//
// Run Over: Points
// 量の割合を設定する(10:6:3の割合で0,1,2の番号が振られるようにする)
int ratio[] = {10, 6, 3};
// 割合の合計を計算
float sum = 0;
for(int i = 0; i < len(ratio); i++)
{
sum += ratio[i];
}
// 0から1の間に正規化する
float value[];
for(int i = 0; i < len(ratio); i++)
{
value[i]= ratio[i] / sum;
}
// 割合を尺度に変換する(例:[0.3, 0.5, 0.2]→[0.3, 0.8, 1.0])
sum = 0;
for(int i = 0; i < len(value); i++)
{
value[i] = value[i] + sum;
sum = value[i];
}
// インデックスを振っていく
for(int i = 0; i < len(value); i++)
{
if(@ptnum < value[i]*npoints(0))
{
i@type = i;
break;
}
}
ポイント同士の座標をシャッフルするコード(追記:Sort SOPを使った方がよい)
//
// ポイント座標をシャッフルする
//
// Run Over: Detail
for(int i = 0; i < npoints(0); i++)
{
int n = int(rand(i*6.58) * npoints(0));
vector pos = point(0, "P", n);
vector pos2 = point(0, "P", i);
setpointattrib(0, "P", i, pos);
setpointattrib(0, "P", n, pos2);
}
Color SOPで種類別でポイントに色を付けてみたところ。緑色:小、黄色:中、オレンジ:大。
岩アセットを用意する
File SOPから岩のメッシュを読み込む。小さい岩はMegascansからSandstone_Boulder_vmiefjcという岩モデルを使うことにした。
転がすために座標は1m等、少し高めにオフセットしておく。
計算の負担を減らすために、PolyReduce SOPでローポリゴンモデルに削減する。
岩のスケールにはランダムをかけている。アトリビュートには@pscaleを使う。
float min = 1.2;
float max = 2.0;
@pscale = random(@ptnum)*(max-min) + min;
またprimitiveのnameアトリビュートにはrock_smallといれた。のちにCSV出力するときに識別するため。
@name = "rock_small";
Copy to Points SOPではPack and Instancesにチェックを入れ、Pivot LocationはCenterに、Copyingでpscaleの値を追加しておく。
これを中サイズと大サイズの岩で同じように作成し、Merge SOPでまとめる。
Wrangleで初期のランダムな回転値(クォータニオン)を作成しておく。
//
// ランダム回転のクォータニオンを保存しておく
//
// Run Over: Points
// Y軸ランダム
vector axis = {0, 1, 0};
float angle = radians(360*rand(@ptnum * 6.358));
vector4 add_rotate = quaternion(angle, axis);
p@orient = qmultiply(p@orient, add_rotate);
// X軸ランダム
axis = {1, 0, 0};
angle = radians(360*rand(@ptnum * 3.256));
add_rotate = quaternion(angle, axis);
p@orient = qmultiply(p@orient, add_rotate);
そしてNull SOPにつなぎ、名前を「OUT_Rock」としておく。
DOPシミュレーションで岩を転がす
コリジョン用にメッシュを削減し、Null SOPをつなぎ、「OUT_Colli」としておく。
DOP Networkノードをつくり、クリックして中に入る。
中のネットワークはこのように構成する。左側のStatic ObjectのSOP Pathにはコリジョンメッシュの「OUT_Colli」をリンクする。右側はのRBD Packed ObjectのSOP Pathには「OUT_Rock」をリンクする。
Dop Importノードをつくり、DOP Networkにdopnet1をリンクし、Object Maskにrbdpackedobject1をリンクする。
この時点でアニメーションを再生するとシミュレーションが始まる。100フレームも回せば転石も落ち着いてくる。
そのあとにWrangleノードをつなげて、クォータニオンから軸のベクトルを取得する。
//
// フォワードとアップベクターを求める
//
@up = qrotate(@orient, set(0,1,0));
@N = qrotate(@orient, set(0,0,1));
NとUpベクターを視覚化したもの。これで出力するデータがそろった。
結果をCSVに出力する
ポイント群をCluster Points SOPでクラスタリングしておいた。
named primitiveごとにループさせ、Cluster Points SOPをはさんで任意の数で分け、Sort SOPでクラスター番号順に整える。これはCSVで書き出すときに昇順の並びにするため。直後にAttribute Promote SOPでnameをprimitiveからpointへ移しておく。
PythonノードでCSV出力する。出力する値は、名前、クラスター番号、トランスフォーム。これをUnreal Engineで読み込んでアセットを配置する情報にする。
node = hou.pwd()
geo = node.geometry()
# UnrealEngineで読み込む形式でCSVを出力する
filePath = hou.node(".").parm("file").eval()
# aなら追加書き込みモード
file = open(str(filePath), "w")
row = "," + "Name" + ",Cluster" + ",Position" + ",Up" + ",Forward" + ',Scale' + "\n"
file.write(row)
num = len(geo.points())
i= 0
for point in geo.points():
name = point.attribValue("name")
cluster = point.attribValue("cluster")
pos = point.position()
up = point.attribValue("up")
forward = point.attribValue("N")
scale = point.attribValue("pscale")
row = str(name)
row += ',' + str(cluster)
row += ',' + str(pos[0]) + "," + str(pos[2]) + "," + str(pos[1])
row += ',' + str(up[0]) + "," + str(up[2]) + "," + str(up[1])
row += ',' + str(forward[0]) + "," + str(forward[2]) + "," + str(forward[1])
row += ',' + str(scale) + "," + str(scale) + "," + str(scale)
file.write(row)
# 最後以外の行なら改行
if(i < num-1):
file.write("\n")
i+=1
file.close
出力されたCSVの中身はこのようになります。
,Name,Cluster,Position,Up,Forward,Scale
rock_large,0,-17755.26171875,21191.552734375,23533.630859375,-0.962436318397522,-0.26899343729019165,-0.03686538338661194,0.17768798768520355,-0.726696252822876,0.6635808944702148,1.063361406326294,1.063361406326294,1.063361406326294
rock_large,0,-24989.22265625,34606.2421875,24200.970703125,0.5980241894721985,0.5814560651779175,0.5516121983528137,0.26902106404304504,-0.7939407825469971,0.5452392101287842,1.1072680950164795,1.1072680950164795,1.1072680950164795
rock_large,0,-28005.95703125,18213.55078125,25254.4140625,0.5264458656311035,-0.8066633939743042,-0.26860547065734863,-0.23081330955028534,0.168465256690979,-0.9583029747009277,1.1269376277923584,1.1269376277923584,1.1269376277923584
rock_large,0,-26206.234375,13078.98828125,26113.857421875,0.19779109954833984,-0.8534136414527893,-0.4822486937046051,0.6014569997787476,-0.28281641006469727,0.7471708655357361,1.1120017766952515,1.1120017766952515,1.1120017766952515
rock_large,0,-24663.591796875,16217.54296875,25324.931640625,-0.5786615014076233,0.45900216698646545,-0.6741423606872559,0.10368727147579193,-0.7784808874130249,-0.6190447211265564,1.0884464979171753,1.0884464979171753,1.0884464979171753
rock_large,0,-29831.888671875,16830.966796875,25594.736328125,0.3826459050178528,0.32568761706352234,0.8645865321159363,0.06096058338880539,0.9248663783073425,-0.3753746449947357,1.0478107929229736,1.0478107929229736,1.0478107929229736
rock_large,0,-19907.146484375,19897.83984375,24035.396484375,0.6160900592803955,-0.740220844745636,-0.26926979422569275,0.27441728115081787,-0.11872908473014832,0.9542528986930847,1.0966349840164185,1.0966349840164185,1.0966349840164185
このデータを使って、ゲームエンジン上でアセットを配置することになります。
つづく・・。