在WebGL場景中導入多個Babylon骨骼模型,在區域網用WebSocket實現多用戶交互控制。 ...
在WebGL場景中導入多個Babylon骨骼模型,在區域網用WebSocket實現多用戶交互控制。
首先是場景截圖:
上圖在場景中導入一個Babylon骨骼模型,使用asdw、空格、滑鼠控制加速度移動,在移動時播放骨骼動畫。
上圖在場景中加入更多的骨骼模型(兔子),兔子感知到人類接近後會加速遠離人類。
上圖,一個區域網中的新玩家進入場景,(他們頭上的數字是WebSocket分配的session id),兔子們受到0和1的疊加影響。
具體實現:
一、工程結構:
前臺WebStorm工程:
其中map.jpg是地形高度圖,tree.jpg不是樹而是地面泥土的紋理。。。
LIB文件夾里是引用的第三方庫(babylon.max.js是2.4版),MYLIB文件夾里是我自己編寫或整理修改的庫,PAGE里是專用於此網頁的腳本文件
其中FileText.js是js前臺文件處理庫(這裡只用到了其中的產生日期字元串函數)
MoveWeb.js是加速度計算庫
Sdyq.js里是對物體對象的定義和操作監聽
Player.js里是繼承了物體對象的玩家對象和動物對象的定義
utils是一些其他工具
View是頁面控制庫
MODEL文件夾里是人物和兔子的骨骼模型文件。
後臺MyEclipse工程:
使用JDK1.7,因為Tomcat v8.0里包含了WebSocket所用的庫,所以不需要引入額外jar包,只寫了一個類。
二、基本場景構建和骨骼模型導入:
html頁面文件:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>使用websocket聯網進行數據傳遞,這個節點應該既可以做主機也可以加入他人的主機</title> 6 </head> 7 <body> 8 <div id="all_base" style="position:fixed;top:0px;left: 0px;"> 9 <div id="div_canvas" style="float: left;width: 75%;border: 1px solid"> 10 <canvas id="renderCanvas" style="width: 100%;height: 100%"></canvas> 11 </div> 12 <div id="div_log" style="float: left;border: 1px solid;overflow-y: scroll"> 13 </div> 14 <div id="div_bottom" style="float: left;width: 100%;height: 100px;padding-top: 10px;padding-left: 10px"> 15 <input style="width: 200px" id="str_ip" value="localhost"> 16 <input id="str_name"> 17 <button id="btn_create" onclick="createScene()" disabled=true>啟動場景</button> 18 <button id="btn_connect" onclick="Connect()" >websocket連接</button> 19 <button id="btn_close" onclick="Close()" disabled=true>關閉連接</button> 20 <span id="str_id" style="display: inline-block"></span><br><br> 21 <input style="width: 400px" id="str_message"> 22 <button id="btn_send" onclick="Send()">發送</button> 23 </div> 24 </div> 25 <script src="../JS/LIB/babylon.max.js"></script> 26 <script src="../JS/MYLIB/View.js"></script> 27 <script src="../JS/LIB/jquery-1.11.3.min.js"></script> 28 <script src="../JS/MYLIB/FileText.js"></script> 29 <script src="../JS/MYLIB/Sdyq.js"></script> 30 <script src="../JS/MYLIB/player.js"></script> 31 <script src="../JS/MYLIB/MoveWeb.js"></script> 32 <script src="../JS/MYLIB/utils.js"></script> 33 <script src="../JS/PAGE/scene_link.js"></script> 34 <script src="../JS/PAGE/WebSocket.js"></script> 35 </body> 36 <script> 37 var username=""; 38 window.onload=BeforeLog; 39 window.onresize=Resize_Pllsselect; 40 function BeforeLog() 41 { 42 Resize_Pllsselect(); 43 //DrawYzm(); 44 //createScene(); 45 } 46 var str_log=document.getElementById("div_log"); 47 function Resize_Pllsselect() 48 { 49 var size=window_size(); 50 document.getElementById("all_base").style.height=(size.height+"px"); 51 document.getElementById("all_base").style.width=(size.width+"px"); 52 document.getElementById("div_canvas").style.height=(size.height-100+"px"); 53 str_log.style.height=(size.height-100+"px"); 54 str_log.style.width=((size.width/4)-4+"px"); 55 if(engine!=undefined) 56 { 57 engine.resize(); 58 } 59 } 60 61 var state="offline"; 62 63 var arr_myplayers=[]; 64 var arr_webplayers=[]; 65 var arr_animals=[]; 66 var arr_tempobj=[];//暫存對象初始化信息 67 var tempobj; 68 69 var canvas = document.getElementById("renderCanvas"); 70 var ms0=0;//上一時刻毫秒數 71 var mst=0;//下一時刻毫秒數 72 var schange=0;//秒差 73 74 var skybox, 75 scene, 76 sceneCharger = false, 77 meshOctree, 78 cameraArcRotative = [],//弧形旋轉相機列表 79 octree; 80 var engine; 81 var shadowGenerator ; 82 83 </script> 84 </html>View Code
其中包含對頁面尺寸大小變化的響應和一些全局變數的定義
scene_link.js文件中包含場景的構建和模型導入:
1、在createScene()方法的開頭部分建立了一個基本的PlayGround場景:
1 engine = new BABYLON.Engine(canvas, true); 2 engine.displayLoadingUI(); 3 scene = new BABYLON.Scene(engine); 4 5 //在場景中啟用碰撞檢測 6 scene.collisionsEnabled = true; 7 //scene.workerCollisions = true;//啟動webworker進程處理碰撞,確實可以有效使用多核運算,加大幀數!! 8 //但是worker是非同步運算的,其數據傳輸策略會導致movewithcollition執行順序與期望的順序不符 9 10 //定向光照 11 var LightDirectional = new BABYLON.DirectionalLight("dir01", new BABYLON.Vector3(-2, -4, 2), scene); 12 LightDirectional.diffuse = new BABYLON.Color3(1, 1, 1);//散射顏色 13 LightDirectional.specular = new BABYLON.Color3(0, 0, 0);//鏡面反射顏色 14 LightDirectional.position = new BABYLON.Vector3(250, 400, 0); 15 LightDirectional.intensity = 1.8;//強度 16 shadowGenerator = new BABYLON.ShadowGenerator(1024, LightDirectional);//為該光源建立陰影生成器,用在submesh上時一直在報錯,不知道為了什麼 17 18 //弧形旋轉相機 19 cameraArcRotative[0] = new BABYLON.ArcRotateCamera("CameraBaseRotate", -Math.PI/2, Math.PI/2.2, 12, new BABYLON.Vector3(0, 5.0, 0), scene); 20 cameraArcRotative[0].wheelPrecision = 15;//滑鼠滾輪? 21 cameraArcRotative[0].lowerRadiusLimit = 2; 22 cameraArcRotative[0].upperRadiusLimit = 22; 23 cameraArcRotative[0].minZ = 0; 24 cameraArcRotative[0].minX = 4096; 25 scene.activeCamera = cameraArcRotative[0]; 26 cameraArcRotative[0].attachControl(canvas);//控制關聯 27 28 //地面 29 //name,url,width,height,subdivisions,minheight,maxheight,updateble,onready,scene 30 ground = BABYLON.Mesh.CreateGroundFromHeightMap("ground", "../IMAGE/map.jpg", 1000, 1000, 100, 0, 60, scene, true);//地面類型的網格 31 var groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);//泥土材質 32 groundMaterial.diffuseTexture = new BABYLON.Texture("../IMAGE/tree.png", scene);//地面的紋理貼圖 33 groundMaterial.diffuseTexture.uScale = 50.0;//紋理重覆效果 34 groundMaterial.diffuseTexture.vScale = 50.0; 35 ground.material = groundMaterial; 36 ground.checkCollisions = true;//檢測碰撞 37 ground.receiveShadows = true;//接收影子 38 39 //牆 40 var Mur = BABYLON.Mesh.CreateBox("Mur", 1, scene); 41 Mur.scaling = new BABYLON.Vector3(15, 6, 1); 42 Mur.position.y = 20; 43 Mur.position.z = 20; 44 Mur.checkCollisions = true;
其中各個方法的具體用法可以參考官方的基礎教程
2、接下來是在場景中導入第一個人物的骨骼模型:
1 //角色導入,載入哪個mesh、文件目錄、文件名、加入場景、回調函數 2 BABYLON.SceneLoader.ImportMesh("", "../MODEL/him/", "him.babylon", scene, function (newMeshes, particleSystems, skeletons) 3 {//載入完成的回調函數 4 var Tom=new Player; 5 var obj_p={};//初始化參數對象 6 obj_p.mesh=newMeshes[0];//網格數據 7 obj_p.scaling=new BABYLON.Vector3(0.05, 0.05, 0.05);//縮放 8 obj_p.position=new BABYLON.Vector3(-5.168, 30.392, -7.463);//位置 9 obj_p.rotation=new BABYLON.Vector3(0, 3.9, 0);// 旋轉 10 obj_p.checkCollisions=true;//使用預設的碰撞檢測 11 obj_p.ellipsoid=new BABYLON.Vector3(0.5, 1, 0.5);//碰撞檢測橢球 12 obj_p.ellipsoidOffset=new BABYLON.Vector3(0, 2, 0);//碰撞檢測橢球位移 13 obj_p.skeletonsPlayer=skeletons; 14 obj_p.methodofmove="controlwitha"; 15 obj_p.name=username; 16 obj_p.id=id; 17 obj_p.p1=""; 18 obj_p.p2="../MODEL/him/"; 19 obj_p.p3="him.babylon"; 20 var len=newMeshes.length;//對於複雜的模型來說newMeshes的其他部分也必須保存下來 21 var arr=[]; 22 for(var i=1;i<len;i++) 23 { 24 arr.push(newMeshes[i]); 25 } 26 obj_p.submeshs=arr; 27 28 Tom.init( 29 obj_p 30 ); 31 arr_myplayers[username]=Tom; 32 33 if(state=="online") 34 { 35 var arr=[]; 36 arr.push("addnewplayer"); 37 arr.push(Tom.mesh.scaling.x); 38 arr.push(Tom.mesh.scaling.y); 39 arr.push(Tom.mesh.scaling.z); 40 arr.push(Tom.mesh.position.x); 41 arr.push(Tom.mesh.position.y); 42 arr.push(Tom.mesh.position.z); 43 arr.push(Tom.mesh.rotation.x); 44 arr.push(Tom.mesh.rotation.y); 45 arr.push(Tom.mesh.rotation.z); 46 arr.push(Tom.p1); 47 arr.push(Tom.p2); 48 arr.push(Tom.p3); 49 arr.push(Tom.meshname); 50 var dt=new Date(); 51 console.log(dt.getTime()+"send addnewplayer"+id); 52 doSend(arr.join("@")); 53 } 54 55 cameraArcRotative[0].alpha = -parseFloat(arr_myplayers[username].mesh.rotation.y) - 4.69;//初始化相機角度 56 57 });
其中BABYLON.SceneLoader.ImportMesh是一個非同步的把伺服器端場景文件導入本地記憶體的方法,第一個參數表示導入場景文件中的哪一個Mesh,為空表示都導入(一個場景文件里可能包含多個模型,但該示例中的場景文件里只有一個模型,所以也叫做模型文件),第二個參數是文件所在的相對路徑,第三個參數是文件名,第四個參數是文件加入的場景,第五個參數是導入完成後的回調函數。
回調函數的newMeshes參數是所有導入的Mesh組成的數組,skeletons參數是所有導入的骨骼動畫數組。事實上一個模型可能由多個mesh組合而成,比如示例中的him模型的newMeshes[0]只是一個空殼,newMeshes[1]到newMeshes[5]才是模型各個部分的實際Mesh,後五個Mesh是newMeshes[0]的“submesh”,newMeshes[0]是後五個Mesh的parent,在理想情況下這些Mesh之間的關係和Mesh與骨骼動畫(skeleton)之間的關係由Babylon引擎自動管理。
在回調函數中,定義Tom為一個Player“類”對象,第五行定義的obj_p對象是Player對象的初始化參數對象,Player.init()方法定義在player.js文件中:
1 //玩家對象 2 Player=function() 3 { 4 sdyq.object.call(this); 5 } 6 Player.prototype=new sdyq.object(); 7 Player.prototype.init=function(param) 8 { 9 param = param || {}; 10 sdyq.object.prototype.init.call(this,param);//繼承原型的方法 11 this.flag_standonground=0;//是否接觸地面 12 this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按鍵是否保持按下,考慮到多客戶端並行,那麼勢必每個player都有自己的keys!! 13 this.flag_runfast=1;//加快速度 14 this.name=param.name; 15 this.id=param.id; 16 this.p1=param.p1; 17 this.p2=param.p2; 18 this.p3=param.p3; 19 。。。
可以看到Player對象繼承自sdyq.object對象,Player對象的原型是sdyq.object對象,在Player對象的init方法中,先初始化屬於原型的屬性,再初始化自己這個“類”新添加的屬性。
sdyq.object對象的定義在Sdyq.js文件中:
1 //物體本身的屬性和初始化 2 sdyq={};//3D引擎 3 sdyq.object=function() 4 {//在地面上加速度運動的物體 5 6 } 7 sdyq.object.prototype.init = function(param) 8 { 9 this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按鍵是否保持按下 10 this.witha0={forward:0,left:0,up:-9.82};//非鍵盤控制產生的加速度 11 this.witha={forward:0,left:0,up:-9.82};//環境加速度,包括地面阻力和重力,現在還沒有風力 12 this.witha2={forward:0,left:0,up:0};//鍵盤控制加速度與物體本身加速度和非鍵盤控制產生的加速度合併後的最終加速度 13 this.v0={forward:0,left:0,up:0};//上一時刻的速度 14 this.vt={forward:0,left:0,up:0};//下一時刻的速度 15 this.vm={forward:15,backwards:5,left:5,right:5,up:100,down:100};//各個方向的最大速度 16 //this.flag_song=0;//是否接觸地面 17 this.flag_runfast=1;//加快速度 18 this.ry0=0;//上一時刻的y軸轉角 19 this.ryt=0;//下一時刻的y軸轉角 20 this.rychange=0;//y軸轉角差 21 this.mchange={forward:0,left:0,up:0};//物體自身坐標繫上的位移 22 this.vmove=new BABYLON.Vector3(0,0,0);//世界坐標系中每一時刻的位移和量 23 this.py0=0;//記錄上一時刻的y軸位置,和下一時刻比較確定物體有沒有繼續向下運動!! 24 25 param = param || {}; 26 this.mesh=param.mesh; 27 this.mesh.scaling=param.scaling; 28 this.mesh.position=param.position; 29 this.mesh.rotation=param.rotation; 30 this.mesh.checkCollisions=param.checkCollisions; 31 this.mesh.ellipsoid=param.ellipsoid; 32 this.mesh.ellipsoidOffset=param.ellipsoidOffset; 33 this.meshname=this.mesh.name; 34 this.skeletonsPlayer=param.skeletonsPlayer||[]; 35 this.submeshs=param.submeshs; 36 this.ry0=param.mesh.rotation.y; 37 this.py0=param.mesh.position.y; 38 this.countstop=0;//記錄物體靜止了幾次,如果物體一直靜止就停止發送運動信息 39 40 this.PlayAnnimation = false; 41 42 this.methodofmove=param.methodofmove||""; 43 switch(this.methodofmove) 44 { 45 case "controlwitha": 46 { 47 window.addEventListener("keydown", onKeyDown, false);//按鍵按下 48 window.addEventListener("keyup", onKeyUp, false);//按鍵抬起 49 break; 50 } 51 default : 52 { 53 break; 54 } 55 } 56 }
sdyq.object對象的初始化方法中包含了對mesh姿態的詳細設定、對鍵盤操作的監聽設定和適用於加速度運動的各項參數設定,各種加速度運動的物體都可以用sdyq.object對象來擴展產生。
在Player對象的初始化方法中還為每個玩家添加了id顯示(頭上的那個數字):
1 //在玩家頭上顯示名字,clone時這個也會被clone過去,要處理一下!!!! 2 var lab_texture=new BABYLON.Texture.CreateFromBase64String(texttoimg2(this.id),"datatexture"+this.id,scene);//使用canvas紋理!! 3 var materialSphere1 = new BABYLON.StandardMaterial("texture1"+this.id, scene); 4 materialSphere1.diffuseTexture = lab_texture; 5 var plane = BABYLON.Mesh.CreatePlane("plane"+this.id, 2.0, scene, false, BABYLON.Mesh.FRONTSIDE); 6 //You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE 7 materialSphere1.diffuseTexture.hasAlpha = true;//應用紋理的透明度 8 9 plane.position=new BABYLON.Vector3(0,75,0);//其父元素應用過0.05之縮放,故而這裡位移量要*20 10 plane.rotation.y = Math.PI; 11 plane.scaling.x=20; 12 plane.scaling.y=4; 13 plane.parent=this.mesh; 14 15 plane.material=materialSphere1; 16 this.lab=plane;
在這裡使用了canvas現場產生紋理(術語叫“程式貼圖”),其中texttoimg2()方法的定義在utils.js文件中:
1 //把文字轉變為圖片jpeg 2 function texttoimg(str) 3 { 4 var c=document.createElement("canvas"); 5 c.height=20; 6 c.width=100; 7 var context = c.getContext('2d'); 8 context.font="normal 15px sans-serif"; 9 context.clearRect(0, 0, canvas.width, canvas.height); 10 context.fillStyle="rgb(255,255,255)"; 11 context.fillRect(0,0,canvas.width,canvas.height); 12 context.fillStyle = "rgb(0,0,0)"; 13 context.textBaseline = 'top'; 14 context.fillText(str,(c.width-str.length*15)/2,0, c.width*0.9); 15 var str_src=c.toDataURL("image/jpeg"); 16 return str_src; 17 //return c; 18 } 19 //把文字轉變為圖片PNG 20 function texttoimg2(str) 21 { 22 var c=document.createElement("canvas"); 23 c.height=20; 24 c.width=100; 25 var context = c.getContext('2d'); 26 context.font="normal 20px sans-serif"; 27 context.clearRect(0, 0, canvas.width, canvas.height); 28 //context.fillStyle="rgb(255,255,255)"; 29 //context.fillRect(0,0,canvas.width,canvas.height); 30 context.fillStyle = "rgb(255,255,255)"; 31 context.textBaseline = 'middle';// 32 context.fillText(str,(c.width-str.length*20)/2,10, c.width*0.9); 33 var str_src=c.toDataURL("image/png"); 34 return str_src; 35 //return c; 36 }
該代碼綜合網上多個教程修改而來,其中生成jpeg的難點在於canvas預設生成四通道圖像,而jpeg在去除透明度通道時會自動將透明度通道變成黑色,於是jpeg一片漆黑,解決方法是先畫一個不透明的白色矩形背景,擋住所有透明通道,再在白色背景上畫圖。
在模型導入完畢後把Tom設為玩家列表對象arr_myplayers的一個屬性,如果當前玩家處於線上狀態,則還要把其載入狀態同步給其他玩家,具體同步方式稍後介紹。
最後把玩家的相機定位到玩家模型的身後,做第三方跟隨視角狀。
三、加速度運動控制
在scene_link.js文件的中部可以看到scene.registerBeforeRender()方法,這個方法的作用是在每次渲染前調用作為它的參數的方法,我們通過這個方法在每次渲染前對物體的下一步運動情況進行計算:
1 scene.registerBeforeRender(function() 2 {//每次渲染前 3