最近一直在研究怎样怎样将程序化特效和动画从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传给点的坐标上。
这一篇先到这里了。