はじめに
1990年代後半のフリーウェアパッチモデラー sPatch と、その後継 HamaPatch は、
Bezierパッチベースのモデルを RenderMan RIB として出力できる。この RIB に含まれる
Patch "bicubic" データを、ポリゴン化を経由せずに、以下2つのレンダラーの
ネイティブ曲面プリミティブへ変換した記録である。
-
Sunflow 0.07.3(最終リリース版)の
bezier-mesh -
mental ray 3.14 standalone の free-form surface(
.mi直書き)
いずれも中間フォーマットを介さず Python スクリプトでテキスト変換するだけなので、
レンダラー内部でのパッチ評価・テッセレーションがそのまま検証できる。
教材として「同一の曲面データが各レンダラーでどう表現されるか」を比較する目的にも向く。
ソースデータ: RIBの Patch "bicubic"
sPatch が出力する vase.rib には、次の形式のパッチが32個並んでいる。
Basis "bezier" 3 "bezier" 3
Patch "bicubic" "P" [0.500000 1.062500 0.000000 ... ] # 16点 × xyz = 48値
- 1パッチ = 4×4 の制御点グリッド(bicubic Bezier、degree 3)
- 制御点は u 方向が先に変化する行優先順
- 隣接パッチは境界の制御点を完全に共有している(後述のクラック回避に効く)
パーサは正規表現で十分機能する。
patch_pattern = r'Patch\s+"bicubic"\s+"P"\s+\[([\d\s\.\-]+)\]'
# 抽出した数値列が48個であることを検証してからパッチとして採用
変換1: Sunflow bezier-mesh
Sunflow の bezier-mesh は「RenderMan spec と同じ順序」で制御点を受け取ると
明記されているため、座標値はそのままパススルーできる。
object {
shader myShader
transform {
rotatex -90
}
type bezier-mesh
n 4 4
wrap false false
points 0.5 1.0625 0.0 0.5 1.0625 -0.114805 ...
}
ポイント:
-
n 4 4で4×4グリッド、wrap false falseで非循環 - オブジェクトが横倒しになったため
rotatex -90を transform ブロックで補正
(Sunflow はシーン全体の transform に非対応。object 単位で指定する) - 生成した .sc は
include vase_sunflow.scでメインシーンから呼び出せる - 制約: 複数 object のグループ化と一括 transform はできないため、
全パッチに同一の transform を書き込む方式で対応
レンダリング結果は滑らかで、パッチ境界のアーティファクトもなかった。
変換2: mental ray free-form surface
必要な仕様はどこにあるか
最初、マニュアルの Geometry Shader Data Structures(C API の構造体定義)が
必要かと考えたが、テキストの .mi を書くだけなら不要。参照すべきは
シーン記述言語の Free-Form Surface Geometry のセクション(Objects 章)だけである。
出力構造
RIB の bicubic patch は degree 3 の Bezier surface にほぼ1:1で対応する。
object "vase"
visible on
shadow on
trace on
basis "bez3" bezier 3 # object直下、groupの前で宣言
group
# --- vector list: 全パッチの制御点を連結 ---
0.500000 1.062500 0.000000
...
# --- vertex list ---
v 0
v 1
...
# --- 1パッチ = 1 surface ---
surface "vase_s001" "vase_mtl"
"bez3" 0.0 1.0 0.0 1.0 # u: range + パラメータリスト
"bez3" 0.0 1.0 0.0 1.0 # v: 同上
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# --- パッチごとに近似指定 ---
approximate surface parametric 4.0 4.0 "vase_s001"
end group
end object
-
basis "bez3" bezier 3を宣言し、surface が u/v 両方向で名前参照する - パラメータ行は「グローバル範囲 0.0 1.0 + セグメント境界 0.0 1.0」
-
parametric近似の分割数は 係数 × degree。parametric 4 4なら
degree 3 × 4 = パッチあたり12分割 - 隣接パッチは境界制御点が一致しており、parametric 近似はパッチ単位で
均等分割のため、境界の分割も一致してクラックは発生しない
検証手順: 1パッチの最小構成から
いきなり32パッチを流さず、1パッチだけの minimal .mi でパース確認 →
レンダリング確認を行った。この段階で以下が確定した。
- パラメータリスト書式は上記の形で 3.14 のパーサに受理される
-
座標系は無変換パススルーで正しい。RIB も .mi も Y-up 右手系のため、
Sunflow で必要だった rotatex -90 は不要。1パッチの見え方
(+X軸から-Z方向へ45°の円弧セグメント)がデータと一致した -
surface レベルの material 指定は instance の material より優先される。
instance に別マテリアルを指定しても surface 文に書いた材質で描画された。
instance 側から差し替えたい場合は object にtaggedフラグを立て、
surface にはマテリアル名の代わりにラベル整数を書く方式になる
配置: instance transform は world-to-object
mental ray ではジオメトリ(object)は変換を持たず、配置は instance の
transform で行う。そしてこの行列は world-to-object(逆行列)規約である。
vase を +0.85 持ち上げて地面に載せる instance はこうなる。
instance "vase_inst" "vase"
material "green_plastic"
transform 1 0 0 0
0 1 0 0
0 0 1 0
0 -0.85 0 1 # ← 符号が反転している点に注目
end instance
mentalpy: TRS指定から逆行列を生成する
自作ラッパー mentalpy で translate/scale/rotate を直感的に指定できるよう、
逆変換を逆順で合成するヘルパーを用意した。
def world_to_object(translate=(0,0,0), rotate=(0,0,0), scale=(1,1,1)):
"""object-to-world側の適用順 scale -> rotX -> rotY -> rotZ -> translate
に対応する world-to-object 行列を生成"""
tx, ty, tz = translate
rx, ry, rz = (math.radians(v) for v in rotate)
sx, sy, sz = scale
m = _t(-tx, -ty, -tz) # 逆順で
m = _mul(m, _rz(-rz)) # 各逆変換を
m = _mul(m, _ry(-ry)) # 掛けていく
m = _mul(m, _rx(-rx))
m = _mul(m, _s(1/sx, 1/sy, 1/sz)) # スケールは逆数
return [v for row in m for v in row]
検算: world_to_object(translate=(0, 0.85, 0)) の最終行は 0 -0.85 0 1 となり、
手書きで動作確認済みの行列と一致する。これにより以下が動作した。
mi.NewObjectInstance("vase_inst", "vase", "green_plastic")
mi.SetProperty3("vase_inst", 'translate', 1, 1.3, 0)
mi.SetProperty3("vase_inst", 'scale', 1.2, 1.2, 1.2)
mi.SetProperty3("vase_inst", 'rotate', -25, 0, 0)
回転とスケールの複合でも破綻しないことが、合成順序の正しさの証明になる。
sPatch と HamaPatch の出力比較
変換パイプラインとは別に、同一の靴モデルを sPatch と HamaPatch それぞれから
RIB 出力し、パッチ生成アルゴリズムの違いを比較検証した。
基本統計
| sPatch | HamaPatch | 差分 | |
|---|---|---|---|
| パッチ数 | 82 | 96 | +14 (+17%) |
| 制御点数 | 1,312 | 1,536 | +224 |
| Y座標層数(緯度分割) | 212層 | 286層 | +35% |
| 制御点重複率 | 50.6% | 39.2% | -11.4pt |
パッチ分割の精度
HamaPatch は Y 座標の層数が 212 → 286 と 35% 増えており、より細かい
緯度方向の分割で滑らかな表面を実現している。
制御点の共有効率
パッチ数が 17% 増えているにもかかわらず、制御点の重複率は 50.6% → 39.2% と
大きく改善している。つまり HamaPatch は隣接パッチ間での制御点共有が効率的で、
パッチ密度を上げながらメモリ効率も同時に向上させている。
適応的パッチ配置
領域別のパッチ増減を見ると、一律の高密度化ではないことが分かる。
| 領域 | パッチ増減 | 意図 |
|---|---|---|
| かかと部 | +8 (+40%) | 複雑な曲面に密度を集中 |
| アッパー部 | +8 (+25%) | 上部の曲線表現を重視 |
| 靴底部 | ±0 | 平坦部は現状維持 |
| つま先部 | -2 (-20%) | 過剰だった分割を削減 |
つま先部でパッチを減らしている点が特徴的で、単純な全体増量ではなく、
曲率に応じてパッチを再配分する適応的アルゴリズムであることを示している。
設計思想の対比
-
sPatch: 均等なパッチ分布によるシンプルな分割戦略。古典的な
Bezier パッチアプローチ -
HamaPatch: 曲率に応じた適応的密度調整、効率的な制御点共有、
細かい分割による高品質な曲面表現

mental rayでレンダリング、左hamapatch、右spatch
今回 mental ray へ変換した vase は HamaPatch 経由の RIB であり、
この生成品質の高さが、変換後の滑らかなレンダリング結果にも寄与している。
上記画像は、hamapatchとspatchで出力したribをmiファイルに変換し、レンダリングした。
mental ray で差が消えたのは、approximate 文による明示的なテッセレーション制御があるからで、parametric 4.0 ならどちらのモデルもパッチあたり12分割まで細分化されている。元データの分割密度の差(212層 vs 286層)はレンダラー側の再分割で吸収されてしまう。一方 Sunflow の bezier-mesh は近似制御の構文を持たず内部の固定分割に依存するため、元データのパッチ密度がそのまま画に出る。

mental rayでレンダリング、左factor 4.0、右factor 1.0
factor を 1.0 / 2.0 / 3.0 / 4.0 の階段で同一カメラ・同一モデルをレンダリングし、Sunflow 版の画とシルエットのカクつき(輪郭のファセット数)を目視比較してみた。
Sunflow の bezier-mesh の内部分割は「パッチあたり3分割(factor 1.0相当)と6分割(factor 2.0相当)の間」— おそらく4〜5分割程度、という実測値が得られた。構文上に制御手段がなく文書化もされていない値なので、これは小さいながらも独自の知見である。
factor 4.0 で履き口が「少し小さくなった」ように見える点は、幾何学的には factor 4.0 の方が真のBezier曲面に近い形であり、parametric 近似は曲面上の点をサンプリングして弦で結ぶので、粗い分割では凸な縁の弦が内側を通り、逆に言えば粗い版の履き口の「見え方」がわずかに歪んでいた、ということになる。つまり factor 4.0 が基準形状で、1.0〜2.0 はそこからの偏差、と捉えられる。なめらかになったことでハイライトや影の走り方が変わり、印象として小さく見える効果が出ている。
3レンダラーでのパッチ表現の比較
| RenderMan (RIB) | Sunflow 0.07.3 | mental ray 3.14 | |
|---|---|---|---|
| プリミティブ | Patch "bicubic" |
bezier-mesh |
free-form surface
|
| 基底の指定 | Basis "bezier" 3 |
型に内包 | basis "bez3" bezier 3 |
| 制御点順序 | u優先・行優先 | RIBと同一 | RIBと同一で動作 |
| テッセレーション制御 | ShadingRate | 内部固定 |
approximate 文で明示 |
| 配置変換 | 階層 transform | object 内 transform | instance(world-to-object) |
| マテリアル | Attribute 階層 | object の shader | surface 直接 or tagged |
まとめ
- RIB の bicubic patch は、Sunflow・mental ray とも数値パススルー + 書式変換のみで
ネイティブ曲面としてレンダリングできる - 検証は最小構成(1パッチ)から段階的に。書式 → 座標系 → material 優先順位 →
transform 規約、の順で一段ずつ潰すと混乱しない - mental ray の instance transform は world-to-object。ラッパーを書くなら
TRS の逆変換を逆順合成する - 30年近く前のパッチモデラーの資産が、現代でも複数のレンダラーで
そのまま活用できることが確認できた
一歩一歩です。いろいろ検証ですね。
ありがとうございます♪。
rib_to_mi_converter.py
githubに公開
追記
JPatch open source patch modelerの
JPatch 0.4 PREVIEW 1 written by Sascha Ledinsky
(c) Copyright 2005
jpatch0_4preview1.zipを試してみました。spatchからvaseモデルをimportし、RIB exportは、Bicubic patches(experimental)を選択しました。デフォルトだとQuadrilateralsでPointsPolygonsで出力されます。
python .\rib_to_mi_converter.py .\vtestj.rib
40個のパッチが見つかりました
変換完了: .\vtestj_model.mi
総surface数: 40
以下、mentalpyで記述して、mental rayでレンダリングできました。
japtchのRIB exportは、mesh densityで4, 16, 64, 256と細かく調整できます。デフォルトは16です。
mi.commands.append('$include "vtestj_model.mi"')
mi.NewObjectInstance("vase_inst", "vtestj", "green_plastic")
mi.SetProperty3("vase_inst", 'translate', 1.65, 1.1, -0.6)
mi.SetProperty3("vase_inst", 'scale', 1.2, 1.2, 1.2)
mi.SetProperty3("vase_inst", 'rotate', -25, 0, 0)
mi.NewObjectInstance("vase2_inst", "vtestj", "red_plastic")
mi.SetProperty3("vase2_inst", 'translate', 0.3, 0.85, 0)
mi.SetProperty3("vase2_inst", 'scale', 1.0, 1.0, 1.0)
mi.SetProperty3("vase2_inst", 'rotate', 0, 0, 0)
mi.NewObjectInstance("vase3_inst", "vtestj", "mdl::my_variants::plastic_hard_sakura_proto_mtl")
mi.SetProperty3("vase3_inst", 'translate', -1.0, 0.85, 0)
mi.SetProperty3("vase3_inst", 'scale', 1.0, 1.0, 1.0)
mi.SetProperty3("vase3_inst", 'rotate', 0, 0, 0)

