最近一直在研究怎樣怎樣將程序化特效和動畫從houdini中轉移到touchdesigner中,前段時間拿着了leapmotion做開發,自己一個人自娛自樂也快玩瘋了。
今天講一講從法國一個虛擬交互舞團某個場景中得到靈感,設計一個簡單的通過斥力和彈力混合出來的平衡場效果。
首先看一看人家在舞台上達到的是個什么效果:
在應用之前,先講一講我通過這個視頻看到的原理。
人物的運動提供了一個位置信息P,也許群體運動的那個畫面中提取了運動的速度或者加速信息,這里先不做復雜的討論。畫面中的每個點pt與上述提供的信息位置P進行計算。主要產生兩種力,第一個是根據點P與pt的位置距離和方向,預設一定閾值,在某個半徑范圍內行程排斥力,這個力可以是F = k * pow((maxDistance - distance)/maxDistance, n)。這樣越接近點P的點受到的排斥力越大,同時通過調節n,可以讓這個斥力更想磁場的排斥力。接下來是第二種力,我們可以稱它為彈力,先記錄下每一個點pt的原始不動的位置。一旦點收到排斥力的影響而離開了原始點,這時候將點拉回原始點的彈力就產生作用了,這個力的公式直接可以使用彈力公式F = k * distance。斥力設計成指數型,彈力依舊使用線性方程,這樣的動畫看起來也更有趣味性一些。
上面的分析基本上奠定了之后再houdini里面以及touchdesigner里面如何實現的方法了。
先講一講在houdini里面實現的方法。
在Houdini里面可以直接在sop solver里面通過幀數的迭代達到這樣的效果,通過新建一個vex節點,分別讀取目標點位置以及前一幀點的位置,速度值來計算出當前幀每個點的位置。
其中我抽取出了幾個關鍵的參數來調整力的大小以及彈性的軟硬程度:
Damp - 阻尼大小,能夠影響減速的速度
Spring Index - 彈性指數,越大點抖起來越硬
Magnet Exponent - 用來調整斥力大小與距離的關系
Magnet Scale - 調整斥力大小
Max Distance - 立場半徑
這里是Houdini的vex代碼:
1 #pragma label damp "Damp" 2 #pragma label springIdx "Spring Index" 3 #pragma label magExp "Magnet Exponent" 4 #pragma label magnetScale "Magnet Scale" 5 #pragma label maxDist "Max Distance" 6 7 8 9 sop 10 SpringMagnet(float damp = 0.9; 11 float springIdx = 0.5; 12 float magExp = 1; 13 float magnetScale = 0.5; 14 float maxDist = 1; 15 ) 16 { 17 vector target = point(1, "P", 0); 18 vector rest = point(geoself(), "rest", ptnum); 19 20 // get the spring force based on the rest position 21 vector displace = rest - P; 22 23 vector springForce = displace * springIdx; 24 25 // get the magnet force from the target position 26 vector magDist = P - target; 27 float distance = length(magDist); 28 29 vector magnetForce = set(0,0,0); 30 31 if(length(distance) < maxDist){ 32 distance = pow(fit(distance, 0 , maxDist, 1, 0), magExp); 33 magnetForce = normalize(magDist) * distance; 34 } 35 36 vector mainAcc = magnetForce * magnetScale + springForce; 37 38 v += mainAcc; 39 v = v * damp; 40 P += v; 41 }
接下來就是在TouchDesigner(簡稱TD)中的應用了,這才是重點 :P
TD里面對外部硬件接口的能力是非常強大的,自己的leapmotion前段時間借給同事去玩去了,所以這里首選Ipad上面的OSC作為操作輸入口,提取了能夠調節兩個方向的slider值。
因為我的方法還是影響三維中的點,之后再把點渲染投射到平面上去。所以接下來就是常規的通過geo,light,camera和render搭建標准的三維場景。
完成之后將geo的連接給script sop。這里插一句,TD是全方位和python接軌,所以寫腳本什么的直接用python的就好了。之前是Tscript,由於TD和Houdini是同根同源的開發平台,很多思想和理念都是一脈相承,Tscript像極了在Houdini里面寫expression的感覺,但是沒有像VEX這樣完善的腳本語言支持,所以直接全部轉接到了Python中。好處是一步到位,什么都用python這個膠水做好,目前不太方便的地方是tdu模塊不是特別完善,很多vex中函數能夠一步到位的問題,到了這里需要繞很多個彎。這個案例中我就遇到了Vector類中的normalize函數怎么也出不來正確的值,永遠出來一個none對象,debug了很久最后查出原因了還是使用的老辦法自己算一遍模才得到准確的norm。也許設計者的想法是我們在python中調用其他第三方模塊來解決這些問題,但是作為一個Houdini玩家,被Houdini真的寵壞了,除了numpy,url,math這些常用的模塊,其他真的不怎么熟悉了 TvT
代碼這部分基本是和VEX里面的一個思路了,重新用python寫一遍。其實從這兩個功能一致但是環境不一樣的代碼可以看出houdini和TD的不同了。
1 # me is this DAT. 2 # 3 # scriptOP is the OP which is cooking. 4 5 def cook(scriptOP): 6 import math 7 8 #use button to refresh the screen 9 button = op(scriptOP.par.string2.eval()) 10 if button[0].eval() != 0: 11 scriptOP.clear() 12 13 #get the controller data 14 controlOp = op(scriptOP.par.string1.eval()) 15 damp = float(controlOp['Damp', 1]) 16 springIdx = float(controlOp['Spring Index', 1]) 17 magExp = float(controlOp['Magnet Exponent',1]) 18 magScale = float(controlOp['Magnet Scale', 1]) 19 maxDist = float(controlOp['Max Distance', 1]) 20 21 #get the target position 22 target = op(scriptOP.par.string0.eval()) 23 targetPos = tdu.Vector(target.points[0].x, target.points[0].y, target.points[0].z) 24 25 #get the me.points 26 myPoints = scriptOP.points 27 if not myPoints: 28 scriptOP.copy(scriptOP.inputs[0]) 29 myPoints = scriptOP.points 30 31 for i in range(len(myPoints)): 32 #get the current point position and rest position 33 ptPosition = tdu.Vector(myPoints[i].x, myPoints[i].y, myPoints[i].z) 34 restPosition = myPoints[i].rest 35 36 #define the spring force 37 displace = restPosition - ptPosition 38 springForce = displace * springIdx 39 #print(springForce) 40 41 #define the magnet force 42 magDirection = -(targetPos - ptPosition) 43 magDistance = magDirection.length() 44 magForce = tdu.Vector() 45 if magDistance < maxDist: 46 scaleDist = 1 - magDistance / maxDist 47 mode = math.sqrt(magDirection[0]*magDirection[0] + magDirection[1]*magDirection[1] + magDirection[2]*magDirection[2]) 48 magDirection /= mode 49 magForce = magDirection * scaleDist * magScale 50 51 52 #combine all forces together 53 acc = magForce + springForce 54 55 #add to vel and update the point position 56 myPoints[i].v[0] += acc[0] 57 myPoints[i].v[1] += acc[1] 58 myPoints[i].v[2] += acc[2] 59 60 myPoints[i].v[0] = myPoints[i].v[0] * damp 61 myPoints[i].v[1] = myPoints[i].v[1] * damp 62 myPoints[i].v[2] = myPoints[i].v[2] * damp 63 64 myPoints[i].x += myPoints[i].v[0] 65 myPoints[i].y += myPoints[i].v[1] 66 myPoints[i].z += myPoints[i].v[2] 67 68 return
因為TD是開發實時交互應用的平台,所以時效性是非常重要的一個關鍵。做好的一整套打包成程序在1280*720分辨率下跑起來的是后基本上速度是在30幀稍稍多一點,平均每幀計算時間是31.36ms,但是script sop中的腳本因為是每一幀都要計算一遍的,而消耗時間每幀將近21ms左右,盡然是總時長的2/3。這里我使用的點數是30 * 48 = 1440個。可想而知,腳本的優化很大成程度上會影響程序動畫的效果。這個在以后的日子里面得好好研究。
還有一個值得注意的地方是,這個案例中我把點的運動速度轉到chop中去,目的是想讓它在運動的時候可以通過速度的變化來改變自身的顏色值。把速度變化值變為波形之后,在這個基礎上進行變換效率是非常高的,可能是這一塊的內置計算方法做過非常大的優化,以后試試看能不能直接在波形上做計算,在把chop的channel傳給點的坐標上。
這一篇先到這里了。