ThreeJS之動畫交互邏輯及特效
工作需要,研究了一下 threejs 簡單邏輯動畫交互方法。寫了一個小示例,分享一下,挺醜的。
第一步
當然就是初始化 threejs 的渲染場景了。
var camera; //相機 var scene;//場景 var renderer;//webGL渲染器 var controls;//軌道控件,用於特定場景,模擬軌道中的衛星,可以用鼠標和鍵盤在場景中遊走 var raycaster;//THREE.Raycaster對象從屏幕上的點擊位置想場景中發射一束光線,返回射線穿透物體的數組 var composer;//後期特效合成器,給場景選中物體添加發光特效
第二步
在 ThreeJs Editor 中建立簡單的示例模型,“Export Scene”,導出。並導入示例程序。免去了在示例程序中自己建模的麻煩,不過因為示例程序要加載本地的json,所以可以設置一個簡單的 nodejs 服務器。
在 nodejs 的 anywhere 下運行該示例:
加載模型文件,將文件中的相關 object 加入 group 中:
var url = ‘nofloor.json‘; var loader= new THREE.ObjectLoader();
var geometry = new THREE.Geometry();//存放objects的position坐標,為之後線條的起始點坐標服務 loader.load( url, function ( loadedScene ) {//scene = loadedScene; var objects = loadedScene.children; for(var i=0;i<objects.length;i++){ if(objects[i].type == ‘Mesh‘ ){ objects[i].receiveShadow = true; objects[i].castShadow= true; geometry.vertices.push(objects[i].position); group.add(objects[i]); } } } , onProgress, onError);
導入的模型差不多就是這樣子(醜一點,擔待),並在示例程序中加了stats 和 dat.gui 用來檢測渲染效果和改變特效參數。
第三步
完成的交互目標是,點擊上圖中某個柱體選中,出現相應的連線,並且讓選中的柱體和連線發光。
現在先利用 raycaster 選中物體:
var mouse = new THREE.Vector2(); //鼠標經過或者點擊的屏幕 canvas 上的位置 mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;//將 canvas 坐標系轉換為 WebGL 坐標系 mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; raycaster.setFromCamera( mouse, camera );// raycaster的作用場景 var intersects = raycaster.intersectObjects( [group], true );//從鼠標 mouse 位置發射線,選中 group 組中的objects ,並返回 objects 給 intersects
一旦 intersects 不為空,intersects[0].object 就是鼠標選中的物體,可以是上圖中的正方體,也可以是上圖中的地板。
接下來,皆可以根據選中物體來連線了。連線呢有兩種方法。
第一種
代碼如下:
var material = new THREE.LineBasicMaterial({ color: 0x0000ff }); var geometry = new THREE.Geometry(); geometry.vertices.push( //各個柱體的 position 坐標,也就是上面加載模型文件時候生成的 geometry.vertices
);
// THREE.Line 會將 geometry.vertices 中所有坐標點連成一條連續線,但不是首尾相接。
//比如 geometry.vertices 中存放了 v1,v2,v3,v4四個三維坐標點,就會生成v1->v2->v3->v4 連續線,中間有三條線。
var line = new THREE.Line( geometry, material );
但是有一個缺點就是由於受限於 角度層(ANGLE layer),在Windows平臺上使用 WebGL,線寬將總是為1而不管設置的值。這樣一旦模型是五六米高,可是線條只有不到1cm的寬度,看起來模型就會很奇怪了。這時候就要用第二種方法。
第二種
畫圓柱體,用圓柱體代替線段,但是生成圓柱體就比線段復雜多了。大家知道,threejs 只會指定一個object的position(中心),而不能指定兩端的位置(我還沒發現,若有錯誤,請指正),所以畫的初始圓柱體是直立的,如下圖
讓我們要的是下圖這樣的圓柱體(下面統稱為“柱子線”)
因為半徑是 0.02,所以看起來像線段,這也是為什麽要用圓柱體模擬線段的原因了,逼真,而且還能根據模型大小調整這個柱子線的“linewidth”。
下面我們看看怎麽怎麽根據正方體個球體的坐標動態生成柱子線吧。
上面我們講了如何選中物體,選中之後,我們把這個物體相鄰的物體(建模時候,將所有物體坐標順序放在 geometry.vertices 中,此處默認坐標相鄰就是物體相鄰)的 position 坐標加入 geometryChange.vertices 中
var object = intersects[0].object; geometryChange = new THREE.Geometry(); var position = intersects[0].object.position;//當前選中物體的坐標 //搜索 geometry.vertices 中的 position 重新繪制選中物體相關linet var p = geometry.vertices.length; for(i=0;i<p;i++){ if(geometry.vertices[i] == position){ if (i == p - 1){//將最後一個物體的前一個物體坐標加入 geometryChange.vertices.push(geometry.vertices[p - 2]); geometryChange.vertices.push(position); } else if(i == 0){//將第一個物體的後一個坐標加入 geometryChange.vertices.push(position); geometryChange.vertices.push(geometry.vertices[i+1]); } else{ geometryChange.vertices.push(geometry.vertices[i-1]); geometryChange.vertices.push(position);//將物體前後相鄰的加入 geometryChange.vertices.push(geometry.vertices[i+1]); } } }
這樣我們就把選中物體相鄰的物體坐標放在 geometryChange.vertices。現在我們知道柱子線的起點和終點坐標了,那柱子線怎麽畫,畫哪裏呢?
var temp = geometryChange.vertices.length; var xyz = geometryChange.vertices; //position(x,y,z),就是柱子線的中點位置,xw是起點和中點的 X 軸方向距離,zh是起點和中點的 Z 軸方向距離,cheight是起點和中點的空間距離 var x,y,z,xw,zh,cheight;
先知道柱子線的position(x,y,z),xyz[i]是柱子線起點,xyz[i+1]是柱子線終點。
x= (xyz[i].x+xyz[i+1].x)/2; y=0.1;//線我是畫在地面附近的,所以y默認0.1 z=(xyz[i].z+xyz[i+1].z)/2
再來求柱子線的長度
xw=xyz[i].x-xyz[i+1].x; zh=xyz[i].z-xyz[i+1].z; cheight=Math.sqrt(xw*xw+zh*zh);//圓柱體長度,勾股定理
這下畫柱子線
var material = new THREE.MeshPhongMaterial( { color: 0x156289, emissive: 0x00FFFF, side: THREE.DoubleSide, shading: THREE.FlatShading, vertexColors:THREE.FaceColors } ); var cylinder = new THREE.Mesh( geometryCylinderLine, material ); cylinder.position.set( x, y, z );//兩實體的中點,也就是柱子線的中點,自己理解
可是發現畫的柱子是豎直向上的
這個時候就需要改變柱子線的模型矩陣的,對它做旋轉,達到我們理想的效果。
我們先分析一下怎麽旋轉,首先將繞 x 軸轉90° ,讓柱子線躺地上。
cylinder.rotation.x -= Math.PI * 0.5;
之後如下圖分析所示,紅線就是躺地上的柱子(自行腦補3D場景)。
其中紅線和黑線長度相同,紅線只需要旋轉 θ 角度之後就可以和黑線重合,達到我們要的效果。
θ = Math.asin(xw/cheight);//弧度制
這個時候知道轉多少度了,轉就ok
//考慮到局部坐標系和全局坐標系的轉換,柱體是在全局坐標系下旋轉 if(xyz[i].x > xyz[i+1].x && xyz[i].z < xyz[i+1].z) cylinder.rotation.z -= Math.asin(xw/cheight);//Math.asin(xw/cheight)為柱體要旋轉的角度 else if(xyz[i].x > xyz[i+1].x && xyz[i].z > xyz[i+1].z) cylinder.rotation.z += Math.asin(xw/cheight); else if(xyz[i].x < xyz[i+1].x && xyz[i].z < xyz[i+1].z) cylinder.rotation.z -= Math.asin(xw/cheight); else cylinder.rotation.z += Math.asin(xw/cheight);
好了,柱子線畫出來了。
當然不止一條柱子線,把當前的都加入lineGroup 中
lineGroup.add( cylinder );
scene.add( lineGroup );
下次繪制的時候只要移除當前的 lineGroup 即可。
scene.remove( lineGroup );
第四步
加發光特效
借助threejs的 outlinePass 通道
composer = new THREE.EffectComposer( renderer ); var renderPass = new THREE.RenderPass( scene, camera ); composer.addPass( renderPass ); outlinePass = new THREE.OutlinePass( new THREE.Vector2( window.innerWidth, window.innerHeight ), scene, camera ); composer.addPass( outlinePass ); var onLoad = function ( texture ) { outlinePass.patternTexture = texture; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; }; var loader = new THREE.TextureLoader(); loader.load( ‘tri_pattern.jpg‘, onLoad ); effectFXAA = new THREE.ShaderPass( THREE.FXAAShader ); effectFXAA.uniforms[ ‘resolution‘ ].value.set( 1 / window.innerWidth, 1 / window.innerHeight ); effectFXAA.renderToScreen = true; composer.addPass( effectFXAA );
在選中時,將物體和相應柱子線加入 outlinePass 渲染目標中即可。
selectedObjects = []; selectedObjects.push( lineGroup );//給選中的線條和物體加發光特效 selectedObjects.push( intersects[ 0 ].object ); outlinePass.selectedObjects = selectedObjects;
ok,這就實現了,點擊交互的簡單特效。
當然,這只是個示例,要把它用到復雜的3D場景中,還需要很多的事情要做,加油。
有錯誤敬請指正。
ThreeJS之動畫交互邏輯及特效