2023年9月23日(土) 13:06 JST

滑らかなモデルを記述する方法 -GDLメモ vol.4

  • 2011年3月 5日(土) 13:38 JST
  • 投稿者:

今回は前回(こちら)告知したように、プリミティブ要素のコマンドを使って滑らかメビウスの輪を描く方法を紹介したいと思います。初回のGDLメモ(こちら)で紹介したメビウスの輪のモデルが下図左、今回紹介するプリミティブ要素によるメビウスの輪が右のものです。モデルの分割数はともに32分割ですが、今回のものは面と面のつながりがスムーズなのが確認できます。初回のGDLメモではモデリング方法を紹介する中で、レンダリング結果があまりきれいではない理由を分割数が少ないからだけではないとしましたが、もう一つの理由は、法線の定義の仕方にあります。

三次元モデルのある面がどのように見えるか、はその面の法線の方向などから計算されます。CGの世界では、面法線と頂点法線というものがあり、詳しい説明は専門のサイトに譲りますが、簡単にはBRICKなどの直方体などエッジが求められるモデルには面法線、SPHERE、CONEなど滑らかな面が求められるモデルには頂点法線が使われています。面法線では、面毎に法線が定義されるため、エッジで法線が切り替わります。このため、レンダリングするとエッジの目立つモデルとなります。頂点法線では、頂点毎に法線が定義され、面上の法線は描く頂点からの距離に応じて滑らかに変遷していきます。このためレンダリングするとエッジの目立たないモデルとなります。下図は、直方体を面法線(左)、頂点法線(右)でモデリングしたものです。頂点法線でモデリングしたものは同じ面の中でグラデーションのように薄い青から濃い青に変わっています。元の材質の色は同じなので、これは法線の変化によるものということができます。

さて、GDLでは、SPHERE、CONEなどの基本形状コマンドに頂点法線が採用されていて、滑らかなモデルを描くことができるようになっています。また、プリミティブ要素では、PGON n, vect, status, edge1, edge2, ... , edgen のstatusに「2」を指定することでその面に頂点法線が適応され、曲面の一部の面であると扱うことができるようになっています。直方体を頂点法線でモデリングしてもしょうがないので、さっそくメビウスの輪をプリミティブ要素を使って頂点法線でモデリングしてみましょう。コードは初回のコードを参考に作成しています。

DIM x1[], y1[], z1[]	!!配列の宣言
DIM x2[], y2[], z2[]	!!メビウスの輪の帯端の座標を格納

r = 10	!! 半径
d = 2	!! 帯の幅(4)の半分
q = 32	!! 分割数

BASE

!! q+1番目の点の座標は1番目と同じ(無駄)だがループ内を簡単にするため+1する !!
FOR i = 1 TO q + 1 	
	x1[i] = r*cos((360/q)*(i-1)) + d*cos((180/q)*(i-1))*cos((360/q)*(i-1))
	x2[i] = r*cos((360/q)*(i-1)) - d*cos((180/q)*(i-1))*cos((360/q)*(i-1))
	y1[i] = r*sin((360/q)*(i-1)) + d*cos((180/q)*(i-1))*sin((360/q)*(i-1))
	y2[i] = r*sin((360/q)*(i-1)) - d*cos((180/q)*(i-1))*sin((360/q)*(i-1))
	z1[i] = d * sin((180/q)*(i-1))  !!1 ひねりは180°です。≠360°ですから注意!
	z2[i] = -z1[i]
NEXT i

!! 各配列から頂点(VERT)を宣言していきます。
!! 上のループにまとめることもできますが、添字の関係でここではループを分けています。
FOR i = 1 TO q + 1 	
	VERT x1[i], y1[i], z1[i]
NEXT i

FOR i = 1 TO q + 1 	
	VERT x2[i], y2[i], z2[i]
NEXT i

!! 配列の座標値をつないで3角形を作る
!! EDGEとPGONを作成していきます。
!! 表記がindexなので分かりにくいかと思いますが、
FOR i = 1 TO q
		 
	EDGE (i - 1) + 1, (i - 1) + 2, -1, -1, 2 !! ①
	EDGE (i - 1) + 2, (i - 1) +  (q + 1) + 2, -1, -1, 2 !! ②
	EDGE (i - 1) + (q + 1) + 2, (i - 1) + 1, -1, -1, 2 !! ③

	EDGE (i - 1) + (q + 1) + 2, (i - 1)+ (q + 1) + 1, -1, -1, 2 !! ④
	EDGE (i - 1) + (q + 1) + 1, (i - 1) + 1, -1, -1, 2 !! ⑤

	!!①、②、③のエッジを繋いで面を作っています。
	!!5 * (i - 1)は何を意味するでしょうか?
	!!これは、このループに入るまでに定義されたエッジの数を意味します。
	!!このため、この数に「1」を足すとそのループで宣言された①のエッジのindexとなります。
	PGON 3, 0, 2, 5 * (i - 1) + 1, 5 * (i - 1) + 2, 5 * (i - 1) + 3 
	!!-③、④、⑤のエッジを繋いで面を作っています。
	PGON 3, 0, 2, - ( 5 * (i - 1) + 3), 5 * (i - 1) + 4, 5 * (i - 1) + 5

NEXT i

BODY 2

EDGEとPGONのところは上の図のように作成しています。ループ毎に5本のエッジと2枚の面を作成していることがわかります。このコードによって、滑らかなメビウスの輪(ただし厚さがありません)のモデルを得ることができます。

ここからはおまけです。厚みがあって(中身が詰まっている)滑らかなメビウスの輪を作成することを目標にさらに手を加えてみましょう。まずは、重複するエッジを整理してみます。考えてみると、エッジ⑤は不要であることがわかります。エッジ⑤はひとつ前のループで作成した②の逆向きとして表すことができますね。これを整理したものが下のコードです。

DIM x1[], y1[], z1[]     !!配列の宣言
DIM x2[], y2[], z2[]     !!メビウスの輪の帯端の座標を格納

r = 10   !! 半径
d = 2    !! 帯の幅(4)の半分
q = 32  !! 分割数

BASE

!! q+1番目の点の座標は1番目と同じ(無駄)だがループ内を簡単にするため+1する !!
!! このサンプルではq+1番目の点は使用していません。
FOR i = 1 TO q + 1 	
	x1[i] = r*cos((360/q)*(i-1)) + d*cos((180/q)*(i-1))*cos((360/q)*(i-1))
	x2[i] = r*cos((360/q)*(i-1)) - d*cos((180/q)*(i-1))*cos((360/q)*(i-1))
	y1[i] = r*sin((360/q)*(i-1)) + d*cos((180/q)*(i-1))*sin((360/q)*(i-1))
	y2[i] = r*sin((360/q)*(i-1)) - d*cos((180/q)*(i-1))*sin((360/q)*(i-1))
	z1[i] = d * sin((180/q)*(i-1))  !!1 ひねりは180°です。≠360°ですから注意!
	z2[i] = -z1[i]
NEXT i
FOR i = 1 TO q + 1 	
	tx1 = x1[i] : ty1 = y1[i] : tz1 = z1[i]
	VERT tx1, ty1, tz1
NEXT i

FOR i = 1 TO q + 1 	
	tx2 = x2[i] : ty2 = y2[i] : tz2 = z2[i]
	VERT tx2, ty2, tz2
NEXT i

!! 配列の座標値をつないで3角形を作る
!!最初の一枚は、ひとつ前のループが存在しないため、エッジ⑤まで作成します。
ECount = 0
i = 1

EDGE (i - 1) + 1, (i - 1) + 2, -1, -1, 2 !! ①
EDGE (i - 1) + 2, (i - 1) +  (q + 1) + 2, -1, -1, 2 !! ②
EDGE (i - 1) + (q + 1) + 2, (i - 1) + 1, -1, -1, 2 !! ③
EDGE (i - 1) + (q + 1) + 2, (i - 1)+ (q + 1) + 1, -1, -1, 2 !! ④
EDGE (i - 1) + (q + 1) + 1, (i - 1) + 1, -1, -1, 2 !! ⑤

PGON 3, 0, 2, ECount + 1, ECount + 2, ECount + 3
PGON 3, 0, 2, - ( ECount + 3), ECount + 4, ECount + 5

ECount = 5

FOR i = 2 TO q - 1
	
	EDGE (i - 1) + 1, (i - 1) + 2, -1, -1, 2 !! ①
	EDGE (i - 1) + 2, (i - 1) +  (q + 1) + 2, -1, -1, 2 !! ②
	EDGE (i - 1) + (q + 1) + 2, (i - 1) + 1, -1, -1, 2 !! ③
	EDGE (i - 1) + (q + 1) + 2, (i - 1)+ (q + 1) + 1, -1, -1, 2 !! ④
	!!EDGE (i - 1) + (q + 1) + 1, (i - 1) + 1, -1, -1, 2

	PGON 3, 0, 2, ECount  + 1, ECount  + 2, ECount + 3

	!!二番目(i=2の時)のみひとつ前のループ(i=1の時)でエッジを5本作成しているため
	!!処理が異なります。
	IF i = 2 THEN
		PGON 3, 0, 2, - ( ECount + 3), ECount + 4, - (ECount - 5 + 2)
	ELSE
		PGON 3, 0, 2, - ( ECount + 3), ECount + 4, - (ECount - 4 + 2)
	ENDIF

	ECount = ECount +  4

NEXT i

!!最後の一枚
i = q

EDGE (i - 1) + 1, (q + 1) + 1, -1, -1, 2 !! ①
EDGE 1, (i - 1) + 1, -1, -1, 2 !! ②
EDGE 1, (i - 1)+ (q + 1) + 1, -1, -1, 2 !! ③

PGON 3, 0, 2, ECount + 1, 5, ECount + 2
PGON 3, 0, peu,  ECount  + 1,  5, ECount  + 2

BODY 2

さて、このようにエッジの重複がないようモデリングした場合、上図のようなイメージが得られます。手前に不自然に色の濃い部分がありますが、これは頂点法線から面の法線を求める処理がうまくいってないからだと考えられます。ArchiCADではポリゴンの表裏は余り意識されませんが、CGの世界では面には表裏があります。例えばOpenGLでは反時計周りに面を記述すると面の表は手前側になります(右手系と呼ばれます)。下図を見てください。上のコードではスタート地点から上側が面の表となるように描いていますが、今モデリングしているのはメビウスの輪だということを忘れてはいけません。一周するころには表裏が逆になってしまいます。図中の赤い矢印は各頂点に与えられた法線(を推定して図示したもの)ですが、やはり表裏が逆になります。この状態で頂点法線から面の法線を計算させると、三角形を構成する頂点の頂点法線に反転しているものが含まれるため上図のように不自然になってしまうようですね。二番目のコード(エッジに重複がないよう整理したもの)はこの記事を書きながら作成したものですが、思わずなるほどと唸ってしまいました。おそらくメビウスの輪を厚さのあるモデルとしてモデリングすればこの問題は解決するかと思います。検証は次回のGDLメモで行う予定です。

思いがけずいろいろ考えさせられてしまいましたが、ひとまずプリミティブ要素を使って滑らかなモデルを描く方法を紹介しました。前回と今回の記事を整理すると、プリミティブ要素を使うことで中身の詰まったモデル滑らかなモデルを描くことができる、ということですね。ちなみに(僕の経験からいうと)中身の詰まったモデルをプリミティブで作成する場合、少なくともエッジの重複がないようにモデリングする必要があるようです。プリミティブ要素を使うとなかなか面白いモデルを描くことができます。うちの研究室で行っているGDL作品の発表会のこちらこちらこちらは、プリミティブによってモデリングされたものです。滑らかなモデルは見栄えが良くていいですね。ぜひ試して見てください。次回のGDLメモは、プリミティブ要素によるモデリングのまとめとして、中身の詰まった滑らかなメビウスの輪をテーマにしたいと思います。