きのこの山を襲撃するたけのこの里（ScriptSOPを用いて）



こんにちは、TouchDesigner Advent Calendar 8日目担当の[なるみん](https://twitter.com/narumin256 "なるみん")です。 

今年のアドベントカレンダーでは**ScriptSOP**と**Boidアルゴリズム**を使って**「きのこの山を襲撃するたけのこの里」**を実装してみました。

▼デモ動画
![takenoko.gif](https://qiita-image-store.s3.amazonaws.com/0/160720/65719a3a-55b3-ccea-2215-9535328dbcba.gif)
参考にしたプログラムは[こちら](https://www.derivative.ca/Forum/viewtopic.php?f=27&t=10355&p=39404&hilit=boid#wrap)です。
サンプルプログラムは[こちら](https://github.com/narumin256/touchdesigner_adventcalender)です。

今回の記事ではScriptSOPの使い方を中心に紹介したかったので、実装に使用したオペレータ全ての解説はしていません。サンプルプログラムを見てみて下さい。

#ScriptSOP
ScriptSOPは、cookされるたびにcallback関数のpythonスクリプトを実行します。ポイント、プリミティブ、その頂点を作成、削除、変更することができます。カスタム属性やuvやN（法線）などの組み込み属性を作成できます。
ScriptSOPのcallback関数にはデフォルトで以下の３つのメソッドが用意されています。

|メソッド|機能|
|:---|:---|
|setupParameters()|ScriptSOPのパラメータウィンドウの[Script]ページ->[Setup Parameters]をクリックすると実行されるメソッド。|
|onPulse()|カスタムのパラメータが押された時に実行されるメソッド。|
|onCook()|ScriptSOPがcookされた時に実行されるメソッド。|

#setupParameters(scriptOp)
ScriptSOPで扱うパラメータをカスタマイズするための関数です。



|メソッド|機能|
|:---------------------------|:---------------------|
|appendCustomPage(name)|カスタムパラメータの新しいページを追加します。|
|appendXYZ(name, label=None)|XYZ位置タイプのパラメータを作成します。 size = 3のfloatパラメータを作成するのと同様ですが、より適切なデフォルト命名が使用されます。|
|appendInt(name, label=None)|Int型のパラメータを作成します。|
|appendFloat(name, label=None)|Float型のパラメータを作成します。|
|appendPulse(name, label=None)|パルスボタンタイプのパラメータを作成します。|

カスタムページで扱える関数はまだまだたくさんあります！
詳しくは[こちら](https://docs.derivative.ca/Page_Class)を読んで下さい。


```python:setupParameters(scriptOp)
def setupParameters(scriptOp):
	page = scriptOp.appendCustomPage('Boids')
	target_pos = page.appendXYZ('Targetpos', label='Target Position')
	cohesion_scale = page.appendInt('Cohesion', label='Cohesion Scale')
	cohesion_scale[0].normMin = 1
	cohesion_scale[0].normMax = 300
	cohesion_scale[0].default = 150
	cohesion_scale[0].min = 1
	cohesion_scale[0].clampMin = True

	separation_dist = page.appendFloat('Separation', label='Separation Distance')
	separation_dist[0].default = 0.2

	alignment_level = page.appendInt('Alignment', label='Alignment Level')
	alignment_level[0].normMin = 1
	alignment_level[0].normMax = 100
	alignment_level[0].default = 30
	alignment_level[0].min = 1
	alignment_level[0].clampMin = True

	target_scale = page.appendInt('Targetscale', label='Target Scale')
	target_scale[0].normMin = 1
	target_scale[0].normMax = 500
	target_scale[0].default = 200
	target_scale[0].min = 1
	target_scale[0].clampMin = True

	boundary_scale = page.appendFloat('Boundary', label='Boundary Scale')
	boundary_scale[0].default = 0.05

	boid_minimum_distance = page.appendFloat('Distance', label='Boid Minimum Distance')
	boid_minimum_distance[0].default = 0.8

	boid_speed_limit = page.appendFloat('Speed', label='Boid Speed Limit')
	boid_speed_limit[0].default = 0.5

	boundary_size = page.appendFloat('Boundarysize', label='Boundary Size')
	boundary_size[0].normMin = 1
	boundary_size[0].normMax = 100
	boundary_size[0].default = 10
	boundary_size[0].min = 1
	boundary_size[0].clampMin = True

	restart = page.appendPulse('Restart', label='Restart')
	reset_parameter = page.appendPulse('Resetparameter', label='Reset Parameter')
	return
```

```python:onPulse(par)
	if (par.name == "Restart"):
		par.owner.clear()
	elif par.name == 'Resetparameter':
		par.owner.par.Cohesion = 150
		par.owner.par.Separation = 0.2
		par.owner.par.Alignment = 30
		par.owner.par.Targetscale = 200
		par.owner.par.Boundary = 0.05
		par.owner.par.Distance = 0.8
		par.owner.par.Speed = 0.5
		par.owner.par.Boundarysize = 10
```

#Boidアルゴリズムのルール

>ボイドアルゴリズムは Craig Reynolds によって提案された，群れを形成するためのアルゴリズムである．このアルゴリズムは“整列”，“結合”，“分離”という3 つの単純なルールを与えることによって，各ルールが個体間で相互作用することにより，群れを形成するアルゴリズムである．
引用元: [階層的ボイドアルゴリズムによる大規模な魚群のシミュレーション](http://www.cit.nihon-u.ac.jp/kouendata/No.41/6_MA/6-016.pdf)

**ルール1**「個体が群れの中心方向へ向かうように方向を変える」(結合)
**ルール2**「個体が他の個体とぶつからないように距離をとる」(分離)
**ルール3**「個体が他の個体と概ね同じ方向に飛ぶように合わせる」(整列)
これに、**ルール４**「TouchDesignerの描画領域から出ていかないようにする」と、**ルール5**「指定のターゲットに群れが向かっていく」を追加しています。

#cook(scriptOp)
```python:cook(scriptOp)
import numpy as np

def cook(scriptOp):
	#カスタムパラメータで取得した値を変数に入れて取り出す。
	cohesionScale = scriptOp.par.Cohesion.eval()
	separationScale = scriptOp.par.Separation.eval()
	alignmentScale = scriptOp.par.Alignment.eval()
	targetScale = scriptOp.par.Targetscale.eval()
	boundaryScale = scriptOp.par.Boundary.eval()
	boidMinDist = scriptOp.par.Distance.eval()
	speedLimit = scriptOp.par.Speed.eval()
	boundarySize = scriptOp.par.Boundarysize.eval()
	target = np.array([scriptOp.par.Targetposx.eval(),scriptOp.par.Targetposy.eval(),scriptOp.par.Targetposz.eval()])

	myPoints = scriptOp.points
	if not myPoints:
		scriptOp.copy(scriptOp.inputs[0])
		myPoints = scriptOp.points

	#からの配列にポイントと速度を格納する
	oldPoints = np.empty([len(myPoints),3])
	oldVelocity = np.empty([len(myPoints),3])

	count = 0
	for i in myPoints:
		oldPoints[count]=[i.x,i.y,i.z]
		oldVelocity[count]=[i.vel[0],i.vel[1],i.vel[2]]
		count += 1

	for j in range(len(myPoints)):

		#ルール1: 個体が群れの中心方向へ向かうように方向を変える
		a = np.delete(oldPoints,j,0)
		mean = a.mean(0)
		rule1 = (mean - oldPoints[j])/cohesionScale

		#ルール2: 個体が他の個体とぶつからないように距離をとる
		myPos = np.array(oldPoints[j])
		distance = (np.sqrt((oldPoints-myPos)**2)).sum(axis=1)
		np.place(distance,distance>boidMinDist,0)
		np.place(distance,distance>0,separationScale)
		a = np.delete(distance,j,0)
		b = np.delete(oldPoints,j,0)
		rule2 = -((b-myPos)*(a[:,np.newaxis])).sum(axis=0)

		#ルール3: 個体が他の個体と概ね同じ方向に飛ぶように合わせる
		a = np.delete(oldVelocity,j,0)
		mean = a.mean(0)
		rule3 = (mean - oldVelocity[j])/alignmentScale

		#ルール4: TouchDesignerの描画領域から出ていかないようにする
		limits = np.array([[boundarySize,boundarySize,boundarySize],[-boundarySize,-boundarySize,-boundarySize]])
		rule4 = np.array([0.0,0.0,0.0])
		if oldPoints[j,0] > limits[0,0]:
			rule4[0] = -boundaryScale
		elif oldPoints[j,0] < limits[1,0]:
			rule4[0] = boundaryScale
		if oldPoints[j,1] > limits[0,1]:
			rule4[1] = -boundaryScale
		elif oldPoints[j,1] < limits[1,1]:
			rule4[1] = boundaryScale
		if oldPoints[j,2] > limits[0,2]:
			rule4[2] = -boundaryScale
		elif oldPoints[j,2] < limits[1,2]:
			rule4[2] = boundaryScale

		#ルール5: 指定のターゲットに群れが向かっていく
		rule5 = (target - oldPoints[j])/targetScale

		#全てのルールをまとめる
		rules = oldVelocity[j]+rule1+rule2+rule3+rule4+rule5

		#限界の速度を定義する
		ruleslimit = np.linalg.norm(rules)
		if ruleslimit > speedLimit:
			rules = (rules / ruleslimit) * speedLimit

		#ポイントの位置と速度を更新する

		p = myPoints[j]
		p.P += rules
		p.vel = rules
		p.N = rules
	return
```

もし指摘点や不足点がありましたらコメントいただけますと嬉しいです。

今回このようなくだらないコンテンツのために消費されてしまった由緒正しきボイドアルゴリズム、きのこの山とたけのこの里のオブジェクトを作ってくださった上に、Qiitaでの共有を許可していただいた[津久井](https://twitter.com/katsuya_tsukui)さん、TouchDesigner Advent Cakender2018をオープンしてくれたけど一つも記事を書いていない小原くんへ大いなる感謝を込めて。メリークリスマス。


#TDSW 2018年お世話になりました。
私と[小原](https://twitter.com/Joe0hara)くん、他学生数名で毎月２回東京を中心にTouchDesigner Study WeekendというTouchDesignerのワークショップを開催しています。
私たちが講師を務める初級コース、中級コース、Arduinoやオーディオリアクティブコースをはじめ2018年は[川村](https://twitter.com/kenichikawamura)さん、[松岡](https://twitter.com/yuki736)さん、[高尾](https://twitter.com/kodai100_tw)さん、[比嘉](https://twitter.com/satoruhiga)さん、[Ben](https://www.facebook.com/brvoigt)さん、[喜々津](https://twitter.com/_kiryo)さんなどたくさんの講師の方をお招きして充実したワークショップを実現させることができました。講師の皆さん、参加者の皆さん、会場提供していただいた企業の皆さん、本当にありがとうございました。
来年もまた楽しい企画たくさんご用意する意気込みでいますので2019年もよろしくお願いいたします。


過去開催記録は[こちら](https://tdsw.peatix.com/)からご覧いただけます。
チュートリアル動画は[こちら](https://vimeo.com/tdsw/)から購入できます。
アドベントカレンダーを見ている人限定でチュートリアル動画の30%OFFクーポンをプレゼントさせていただきます！クーポンは[こちら](https://vimeo.com/r/2frN/MWQ1MFJuaE)！