環境:Houdini 20.0.751
XZ平面上でポリラインに矩形を並べていきます。

ノードネットワーク

用意するものは、カーブと、Packした矩形インスタンス。
矩形のインスタンス

Z軸方向がカーブの進行方向に沿う方角になる。Z軸の長さをf@width、X軸の長さをf@heightとして、Pack後のポイントアトリビュートに設定しておく。またi@variantというアトリビュートに矩形の種類を入れておき、Copy to Points SOPで突き合わせます。
処理の流れ

矩形の幅を半径とした球でカーブを区切りながら、順番に並べていくということをしています。
球と線分の交点
球とカーブで交差を取ると、基本的には前後で2つ交点が見つかるので、進行方向のみに限定しています。

山折りで並んでいる時はそのまま並べても問題ありませんが・・


谷折りで並んだ場合はお互いが干渉するので、オフセットする必要があります。
矩形のオフセットは厳密な計算ではなく、重なっている部分の長さだけ「カーブ上」で進めています。なので急カーブになるほど誤差が発生します。あくまで緩いカーブを想定した仕様としています。

角度θはN1とN2の内積で求め、オフセットする長さはheight*tanθとしている。
コード
ポイントをカーブ上に配置する(place_rectangles)
//
// カーブを円で区切って座標を探していき、矩形を配置していく
// input1: Point Instances
// input2: Curve
// Run Over: Detail
//
// 球とポリラインの交差判定
int spherePolylineIntersection(int geometry; int primnum; vector position; float radius; export vector cross)
{
int result = -1;
int pts[] = primpoints(geometry, primnum);
for(int i = 0; i < len(pts)-1; i++)
{
// 球の中心
vector offset = position;
vector p0 = point(geometry, "P", pts[i]) - offset;
vector p1 = point(geometry, "P", pts[i+1]) - offset;
vector v = normalize(p0 - p1);
float a = v.x * v.x + v.y * v.y + v.z * v.z;
float b = 2 * (p0.x * v.x + p0.y * v.y + p0.z * v.z);
float c = p0.x * p0.x + p0.y * p0.y + p0.z * p0.z - radius * radius;
// 解があるかチェック
if((b * b - 4 * a * c) >= 0)
{
/*
// 判別式±の+の場合
float t = (-b + sqrt(b * b - 4 * a * c)) / 2 * a;
cross = p0 + v * t;
// 直線上にあるかチェック
if(dot(p0 - cross, p1 - cross) < 0)
{
cross += offset;
result = 1;
break;
}
*/
// 判別式±の-の場合
float t = (-b - sqrt(b * b - 4 * a * c)) / 2 * a;
cross = p0 + v * t;
// 直線上にあるかチェック
if(dot(p0 - cross, p1 - cross) < 0)
{
cross += offset;
result = 1;
break;
}
}
}
return result;
}
// カーブの全長
float curveLength = primintrinsic(2, "measuredperimeter", 0);
// 矩形のピボット座標
vector pos = primuv(2, "P", 0, set(0, 0));
vector oldPos = pos; // ひとつ前の矩形の座標
// 各矩形の処理
for(int i = 0; i < npoints(1); i++)
{
float width = prim(1, "width", i);
float height = prim(1, "height", i);
// ひとつ前の矩形と高さを比べ、低いほうを選ぶ
if(i > 0)
{
float prevHeight = prim(1, "height", i-1);
height = min(height, prevHeight);
}
// 矩形の長さを半径とした円とカーブとの交点を探す
vector cross;
if(spherePolylineIntersection(2, 0, pos, width, cross) > 0)
{
int index;
vector start_uv; // 開始位置のパラメトリック座標を記録
xyzdist(2, pos, index, start_uv);
vector oldN = normalize(pos - oldPos);
vector left = cross(set(0,1,0), oldN); // 従法線を外積から求め、これと比較することで谷折り山折りを判定する
// 角度が修正されるので、再計算を繰り返して補正していく
// (posとcrossはループ毎に更新されていく)
for(int j = 0; j < 8; j++)
{
vector N = normalize(cross - pos);
// 谷折りの場合はオフセットする
if(dot(left, N) > 0)
{
N = normalize(cross - pos);
float theta = acos(dot(N, oldN));
float offset = height * sin(theta);
// オフセット値をパラメトリック空間でオフセットする
vector uv = set((start_uv.x * curveLength + offset) / curveLength, 0, 0); // オフセット後のパラメトリック座標
pos = primuv(2, "P", index, uv); // パラメトリック座標からワールド座標を取得
spherePolylineIntersection(2, 0, pos, width, cross); // 交点を更新
}
}
int pt = addpoint(0, pos);
setpointattrib(0, "N", pt, normalize(cross - pos)); // 交点から現在位置へのベクトルへ矩形が傾くようにする
setpointattrib(0, "variant", pt, i); // copy to points SOPで突き合わせる
oldPos = pos; // 前回位置に現在位置を保存
pos = cross; // 現在位置を交点に移動する
}
}