Shade3D 公式

22 掃引(1/3)[ Shade Labo ]


#1

22 - 1  進め方


Shade の掃引処理の内容の紹介に当たり、次の3ステップに分けて進めていきます。
  • 断面形状の配置
  • 交差方向線形状のハンドル長さ
  • コーナーでの処理

ハンドル長さの求め方に解明しきれていない部分があり、それらしい結果が得られるように線形補間で回避している部分があります。

これらの不明な部分の処理に対しては Custom 掃引 でより理にかなった処理方法を提案します。


#2

22 - 2  断面形状の配置


まず、掃引中心線にコーナー部分が無い状態( anchor point 両側の接線ベクトルが連続 )での掃引断面の配置方法についての解説です。

予め始端側にセットされた断面形状を、中心線の anchor point 上に順次 copy、中心線に沿って方向と傾きを整える処理を quaternion 回転で操作し、始端側からその操作を順次繰り返すことで、断面配置は処理されます。

          [ fig 22 - 1 ]


配置断面の方向は中心線の接線ベクトルに向けられ、その傾きは quaternion 回転の回転軸の選定によって決まり、中心線の形状により大きく3つのケースに分けられます。

これらの区分が Shade の処理手続きと同じであるかは不明ですが、一部の極めて特殊なケースを除き Shade 掃引の断面配置を再現できています。

  • Case 1 : 4つの bezier contral points に平坦性がある

      1a 両端の接線ベクトルの傾きが180度近くではない
      1b 両端の接線ベクトルの傾きが180度に近い

  • Case 2 : 4つの bezier control points に平坦性がない

また Case 1a, 1b 双方において、接線ベクトルの平行判定による処理の分岐があります。

**< 平坦性の判定 >**
両端のポイントを結ぶ方向 **Vn** に視線を置いたとき、両端ポイントでの接線ベクトル **V1**, **V2** のなす角の cos 値の絶対値が **0.98** を越えていれば平坦と判定します。

VnV1 / V2 が平行、あるいは、Vn に長さがない重点の場合も 平坦性がある と見なします。

     

     [ fig 22 - 2 ]




< 曲がりが180度に近いか否かの判定 >


両端ポイントでの接線ベクトル V1, V2 のなす角の cos 値が -0.9 以下であれば 180度に近い曲がりであると判定します。

V1, V2 に長さがない場合( handle の出ていない重点 )は、180度に近い曲がりではない と見なします。

          [ fig 22 - 3 ]




< 接線ベクトルの平行判定 >


接線ベクトル V1, V2 の平行判定が次の基準で行われています。


      | V1V2 | ≧ 0.99999994 ならば平行と見なす      [ 式 22 -1 ]


判定しきい値が何に基づいているのかは不明ですが、同じ値がコーナー部分での平行判定にも用いられています。



**< script >**
[ script 22 - 1 ] は Shade の掃引の断面配置をシミュレートし、original の掃引と比較する script です。
[ script 22 - 1 ]      Shade 上で 掃引中心線 , 掃引断面 の順で選択して実行
import labo
from matrix import *
from quaternion import *

		

#  def sweep_section_quaternion(bz as vec3 matrix ,atrix) as quaternion
#
#	bezier data bz から掃引時の断面形状の姿勢を与える quaternion を返す

def sweep_section_quaternion(bz) :
	from math import acos
	
	v1 = labo.bezier_line_tangent(bz, 0)		#  outhandle 側接線ベクトル
	v2 = labo.bezier_line_tangent(bz, 1)		#  inhandle 側接線ベクトル
	vn = bz[3] - bz[0]							#   始端と終端を結ぶ vector
	u1 = vn*v1
	u2 = vn*v2
	vn.norm()
	u1.norm()
	u2.norm()
	cosA = u1.dot(u2)							#  4つの bezier control point の平坦性を表す角度の cos値						
	cosB = v1.dot(v2)							#  両端の接線ベクトルの交差角の cos値
	
	#  flat : 4 つの bezier control point の平坦性 flag
	if (u1.abs2() == 0) or (u2.abs2() == 0)  :
		flat = True
	elif abs(cosA) >= 0.98  :
		flat = True
	else :
		flat = False

	# pi_rotaion :  両端の接線ベクトルの回転角が180度に近い
	if (v1.abs2() == 0) and (v2.abs2() == 0) :	#  handle の出ていない重点						
		pi_rotation = False
	elif cosB >= -0.9 :
		pi_rotation = False
	else :
		pi_rotation = True
		
		
	if flat :									#  4 つの bezier control point  が平坦
		if not pi_rotation :					#  両端の接線ベクトルの回転角が180度に近くはない
			if (v1.abs2() == 0) and (v2.abs2() == 0) :	#  handle の出ていない重点
				print 'case 1a     重点'
				return quaternion()
			else :
				if v1.dot(v2) >= 0.99999994 :
					print 'case 1a     平坦性あり', abs(u1.dot(u2)), '>= 0.98', '    曲がりが0度平行に近い', v1.dot(v2), '>= 0.99999994'
					return quaternion()
				else :
					print 'case 1a     平坦性あり', abs(u1.dot(u2)), '>= 0.98', '    曲がりが180度に近くではない', v1.dot(v2), '>= -0.9'
					return quaternion().slerp(v1, v2)	
		
		else :									#  両端の接線ベクトルの回転角が180度に近い
			if vn.abs2() == 0 :					#  重点
				print 'case 1b       重点'
				return quaternion()
				
			elif abs(vn.dot(v1)) >= 0.99999994 or abs(vn.dot(v2)) >= 0.99999994 :		#  handle が point 間に平行
				if v1.dot(v2) > -0.99999994 :
					print 'case 1b      handle が point 間に平行', v1.dot(v2), '> - 0.99999994'
					return quaternion().slerp(v1, v2)
				else :
					print 'case 1b      handle が point 間に平行', v1.dot(v2), '<= - 0.99999994'
					return quaternion()			#  この特殊ケースのみ Shade の断面配置を再現できない
			
			else :
				print 'case 1b     平坦性あり', abs(u1.dot(u2)), '>= 0.98', '    曲がりが180度に近い', v1.dot(v2), '< -0.9'
				v = v2 - v1 
				u = u1 + u2
				axis = u*v
				if axis.abs2() <1e-24 :
					axis = v*vn
                
				phi = acos(u1.dot(-u2))/2
				if (u1*u2).dot(vn) < 0 :
					phi = -phi
				Q = quaternion().rotation(v, phi)			
				axis = Q*axis
				axis.norm()					#  掃引断面の quaternion 回転の回転軸	
				
				w1 = axis*v1
				w2 = axis*v2

				return quaternion().slerp(w1, w2)
			
	
	else :									#  4 つの bezier control point  が平坦でない
		print 'case 2     平坦性なし', abs(u1.dot(u2)), '< 0.98'
		t1 = 0
		v1 = labo.bezier_line_tangent(bz, t1)
		Q = quaternion()
		for i in range(16) :				#  bezier parameter を16等分して細かく回転を16回繰り返す
			t2 = float(i + 1)/16
			v2 = labo.bezier_line_tangent(bz, t2)
			Q = quaternion().slerp(v1, v2)*Q
			t1 = t2
			v1 = v2
	
		return Q
	
	
		

#  sweep(ob1 as shape, ob2 as shape) as shape
#
#	掃引体を Shade 上に作成し、その shape を返す
#		ob1 :	掃引中心線形状
#		ob2 :	掃引断面形状	

def sweep(ob1, ob2) :
	if (ob1 == None) or (ob2 == None) :
		return
	if (not isinstance(ob1, xshade.line)) or (not isinstance(ob2, xshade.line)) :
		return

	#  掃引中心線データ取得 nop, closed, bZ
	ob1.activate()
	nop = ob1.number_of_control_points		#  ポイント数
	if nop < 2 :
		return
	closed = ob1.closed						#  開閉情報
	bZ = labo.get_bezier(xshade, -1)		#  全区間の bezier data [matrix, matrix, … ]
	
	# mx1, mx2
	mx1 = matrix(ob2.world_to_local_matrix)
	mx2 = matrix(ob2.local_to_world_matrix)

	#  ob0 : 掃引体を格納する自由曲面
	ob2.activate()
	scene.create_surface_part(None)
	ob0 = scene.active_shape()				#   掃引体を格納する自由曲面
	ob0.surface_closed = closed
	ob2.place_child(1)

	#  掃引断面を配置
	p1 = bZ[0][0]
	Mt1 = matrix().translate(-p1[0], -p1[1], -p1[2])
	Q1 = quaternion()
	n = nop - 1
	
	for i in range(n) :
		p2 = bZ[i][3]
		v2 = labo.bezier_line_tangent(bZ[i], 1)
		Mt2 = matrix().translate(p2[0], p2[1], p2[2])
		Q2 = sweep_section_quaternion(bZ[i])
		Q2 = Q2*Q1
		Mr = Q2.matrix()
		M = mx2*Mt1*Mr*Mt2*mx1
	
		ob2.copy_object(M)
		ob = ob2.bro
		ob.place_brother(i)
	
		Q1 = Q2

	ob0.activate()
	ob0.disclosed = False
	
	return ob0
	
	
	
	
	
#  Shade  上で 掃引中心線 , 掃引断面 の順で選択して実行

scene = xshade.scene()

scene.exit_modify_mode()
[ob1, ob2] = scene.active_shapes

#  Shade original sweep
ob1.activate()
scene.memory()
ob2.activate()
ob2.copy_object(None)
ob2.bro.activate()
scene.sweep()
ob3 = scene.active_shape().dad
ob3.activate()
ob3.name = 'Shade sweep'
ob3.disclosed = False


#  simulated sweep
ob2.copy_object(None)
ob4 = ob2.bro
ob5 = sweep(ob1, ob4)
ob5.name = 'simulated'

scene.active_shapes = [ob3, ob5]

#3

22 - 3  Case 1a

     ( 平坦性あり、曲がりが180度近くではない )


[ script 22 - 2 ] でサンプル形状を出力して [ script 22 - 1 ] を実行します。

ある程度の平坦性があり、曲がりも180度近くまでは達しないケースで、quaternion 回転軸は両端ポイントでの接線ベクトルに直交します。

          [ fig 22 - 4 ]


[ script 22 - 2 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([7500, -3500, -7500], [1000, -1000, -12500], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

#4

22 - 4  Case 1b

     ( 平坦性あり、曲がりが180度に近い )


Case 1a の回転操作での配置では、平坦性があると判定されても中心線の曲がりが180度に近くなると、接線ベクトルの向きの僅かな違いによって断面配置の方向が大きく変化し、場合によっては大きなねじれを生じてしまいます。
下図では、中心線の右側接線ベクトルが上段では水平に、下段ではやや上に向いています。

Case 1a と Case 1b を比較すると、断面配置の変化が Case 1b で緩和されているのがわかります。

両者はともに1回の quaternion 回転で断面配置を行っており、違いは回転軸の方向にあります。


          [ fig 22 - 5 ]



< 回転軸と回転角 >


最初に一般的な回転軸と回転角の求め方について述べます。


回転軸は次の条件を満たす面内に存在しなければなりません。

          その面に対して両端の接線ベクトルが対称な位置関係を持つ       [ 条件 22 - 1 ]


この条件を満たす面は次のように求められます。

          ある点を起点として両端の接線単位ベクトル V1, V2 を描き、その先端を結ぶベクトル V に垂直な面

            [ fig 22 - 6 ]


Case 1a での回転軸 **V1** x **V2** もこの面内に存在します。

回転軸がこの面内に存在すれば、その方向が360度どの方向を向いていても、quaternion 回転によって断面の面法線は接線ベクトル V2 と平行になります。

違いは断面の接線ベクトル回りの回転方向が異なるだけです。

            [ fig 22 - 7 ]


この時の quaternion 回転の回転角は回転軸 **Axis** の方向に視線を置いたときに見える **V1** と **V2** のなす角θで与えられます。
            [ fig 22 - 8 ]

**< Case 1b での回転操作 >**
実は Case 1b での回転軸の設定の解明については、完全ではありません。

Shade の処理と同じ状態をかなり高い精度で再現できていますが、その求め方の合理的な説明が思いつきません。

たまたま同じ状態を再現できただけで、実際の Shade の処理方法とは異なる可能性があります。

あるいは、**24 掃引(3/3)**で触れますが、コーナー部分での Shade の断面配置には、ある状況下で奇妙な結果を与えることがあり、それとここでの処理は関連しているのかもしれません。


ともあれ、以下の方法で Shade と同じ状態を再現できます。

回転軸を2つのステップで求め、これに対して [ fig 22 - 8 ] で示した式で回転角を求め、quaternion 回転を行います。

  • Step - 1       ベクトル V と U に直交する向きに回転軸 Axis を定める
  • Step - 2       Axis を V の回りに φ = ( π - α ) / 2 回転する


                [ fig 22 - 9 ]

[ script 22 - 3 ] でサンプル形状を出力して [ script 22 - 1 ] を実行します。

            [ fig 22 - 10 ]


[ script 22 - 3 ]
scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([8500, -7500, -5000], [3700, -2100, -20000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

[ script 22 - 4 ], [ script 22 - 5 ], [ script 22 - 6 ] によるサンプル形状は、[ fig 22 - 9 ] の式が適用できません( **Vn** に長さがない あるいは **Vn** と **V1** / **V2** が平行 )

これらの場合は、別の処理を行っており、[ script 22 - 1 ] で比較すると、[ script 22 - 6 ] の形状のみが Shade と同じ結果を再現できていません。


[ script 22 - 4 ]
scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([0, 0, 0], None, [0, 0, -7500], None, None)
scene.append_point([0, 0, 0], [4800, 0, -10000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[4000, 3000, 0], [4000, -3000, 0],[ -4000, -3000, 0], [-4000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

[ script 22 - 5 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([0, 0, 0], None, [0, 0, -2500], None, None)
scene.append_point([0, 0, -10000], [-2000, 0, -15000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[4000, 3000, 0], [4000, -3000, 0],[ -4000, -3000, 0], [-4000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

[ script 22 - 6 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([0, 0, 0], None, [0, 0, -2500], None, None)
scene.append_point([0, 0, -10000], [0, 0, -15000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[4000, 3000, 0], [4000, -3000, 0],[ -4000, -3000, 0], [-4000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

#5

22 - 5  Case 2

     ( 平坦性なし )


4つの bezier control point が3次元的に広がった平坦性のない中心線に対して Case 1a のように1回の回転操作のみでの配置をおこなうと、下図左のように掃引形状が大きくねじれてしまいます。

これを避けるために平坦性がない場合には、Case 2 として下図右のような別の方法を用います。( 平坦性の判定基準は 22 - 2 を参照 )


          [ fig 22 - 11 ]


[ script 22 - 7 ] でサンプル形状を出力して [ script 22 - 1 ] を実行します。
このケースでは、bezier 区間を parameter で16分割し、分割区間それぞれでの両端ポイントでの接線ベクトルに直交する軸を回転軸として、quaternion 回転操作を16回繰り返します。

つまり、16分割した上で Case 1a と同じ操作を16回繰り返します。

          [ fig 22 - 12 ]


実は、この分割数を無限に大きくして、無限小の区間で quaternion 回転を無限回繰り返すと、断面形状を曲線のねじれに沿って配置することになり、中心線に正確に追従し、中心線に対してねじれていない断面配置が得られます。

つまり、ここでの処理は16回に限定して曲線のねじれに沿った配置を近似的に得ようとするものです。

曲線の ねじれ は感覚的に捉えにくいこともあり、詳細な説明は省きますが、4つの bezier control point が平面上にない非平面曲線であれば、曲線には必ずねじれが生じます。


[ script 22 - 7 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([8500, -7500, -5000], [3600, -1900, -20000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

#6

22 - 6  掃引断面配置における留意点


最初に述べたように掃引断面の配置方法は中心線の形状により異なります。
  • 4つの bezier control points に平坦性があか否か
  • 両端の接線ベクトルの傾きが180度に近いか否か

[ script 22 - 8 ], [ script 22 - 9 ], [ script 22 - 10 ] でサンプル形状を出力して [ script 22 - 1 ] を実行します。

これら3つのサンプル形状では、ハンドルの傾きのわずかな違いで断面配置が Case 1a, 1b, 2 に分かれてしまい、断面配置の結果が大きな違いとなって現れます。

つまりハンドル方向の違いによる形状変化に、大きなギャップを持った不連続が生じてしまうことを示しています。

Custom 掃引 でこの問題に対する回避策を提案します。


     
          [ fig 22 - 13 ]


[ script 22 - 8 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([8500, -7500, -5000], [3600, -2100, -20000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

[ script 22 - 9 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([8500, -7500, -5000], [3600, -1900, -20000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]

[ script 22 - 10 ]
scene = xshade.scene()

scene.begin_creating()
scene.begin_line('中心線', False)
scene.append_point([-10000, 0, 0], None, [-10000, 0, -15000], None, None)
scene.append_point([8500, -7500, -5000], [3700, -2100, -20000], None, None, None)
scene.end_line()
scene.end_creating()
ob1 = scene.active_shape()		#  中心線

scene.begin_creating()
scene.create_line('断面',[[-6000, 3000, 0], [-6000, -3000, 0],[ -14000, -3000, 0], [-14000, 3000, 0]],True)
scene.end_creating()
ob2 = scene.active_shape()		#   断面

scene.active_shapes = [ob1, ob2]