實驗目的:按照一定規律生成類地行星地表地形區塊,並用合理的方式將地形塊顯示出來 涉及知識:Babylon.js引擎應用、著色器編程、正態分佈、數據處理、canvas像素操作 github地址:https://github.com/ljzc002/ljzc002.github.io/tree/mast ...
實驗目的:按照一定規律生成類地行星地表地形區塊,並用合理的方式將地形塊顯示出來
涉及知識:Babylon.js引擎應用、著色器編程、正態分佈、數據處理、canvas像素操作
github地址:https://github.com/ljzc002/ljzc002.github.io/tree/master/DataWar
一、在球體網格上顯示紋理的傳統方法:
1、常見的一種星球錶面繪製方法是這樣的:
首先用三角形近似的模擬一個球體網格:
這個簡單場景的代碼如下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>演示球體的繪製</title> 6 <link href="../../CSS/newland.css" rel="stylesheet"> 7 <link href="../../CSS/stat.css" rel="stylesheet"> 8 <script src="../../JS/LIB/babylon.32.all.max.js"></script> 9 <script src="../../JS/LIB/stat.js"></script> 10 </head> 11 <body> 12 <div id="div_allbase"> 13 <canvas id="renderCanvas"></canvas> 14 <div id="fps" style="z-index: 301;"></div> 15 </div> 16 </body> 17 <script> 18 var canvas,engine,scene,gl; 19 canvas = document.getElementById("renderCanvas"); 20 engine = new BABYLON.Engine(canvas, true); 21 gl=engine._gl;//決定在這裡結合使用原生OpenGL和Babylon.js; 22 scene = new BABYLON.Scene(engine); 23 var divFps = document.getElementById("fps"); 24 //全局對象 25 var light0//全局光源 26 ,camera0//主相機 27 ; 28 window.onload=webGLStart; 29 window.addEventListener("resize", function () { 30 engine.resize(); 31 }); 32 function webGLStart() 33 { 34 gl=engine._gl; 35 createScene(); 36 MyBeforeRender(); 37 } 38 var createScene = function (engine) { 39 camera0 =new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, -20), scene); 40 camera0.attachControl(canvas, true); 41 camera0.speed=0.5; 42 camera0.minZ=0.0001; 43 light0 = new BABYLON.HemisphericLight("Hemi0", new BABYLON.Vector3(0, 1, 0), scene); 44 sphere1=BABYLON.MeshBuilder.CreateSphere("sphere1",{segments:10,diameter:10.0},scene); 45 var groundMaterial = new BABYLON.StandardMaterial("groundMat", scene); 46 groundMaterial.wireframe=true; 47 sphere1.material=groundMaterial; 48 49 } 50 function MyBeforeRender() 51 { 52 scene.registerBeforeRender(function() { 53 if(scene.isReady()) 54 { 55 56 } 57 }); 58 engine.runRenderLoop(function () { 59 engine.hideLoadingUI(); 60 if (divFps) { 61 // Fps 62 divFps.innerHTML = engine.getFps().toFixed() + " fps"; 63 } 64 scene.render(); 65 }); 66 67 } 68 </script> 69 </html>View Code
然後將一張紋理貼圖的紋理坐標對應到球體網格中的每個三角形上,具體原理如下:
以上內容引用自吳亞峰著《OpenGLES3.x游戲開發》,Babylon.js中的紋理對應規則可以參考https://www.cnblogs.com/ljzc002/p/6884252.html中的代碼。
但是這種繪製方式存在以下幾個缺點:
a、為了將二維的圖片映射到三維的球面上,圖片或者紋理坐標必須經過複雜的“拓撲變換”(比如圖中的南極洲明顯經過了拉伸變換),這導致我們在行星錶面點擊一個點時,很難直觀的將它對應到圖片上的某個像素,同時生成適合球面的圖片也需要使用專門的工具進行計算。
b、如果把每個三角形作為一個可交互對象,極地區域的可交互對象將過於密集,想象一個回合制戰棋游戲,玩家會發現極地區域的格子太密而赤道附近的格子太稀疏。
c、在畫面拉近時紋理貼圖會因為信息不足出現不受控制的模糊或變形,當然,我們可以在視角拉近時用更多的細節貼圖來提供更多的信息,但那就是一個更浩大的工程了。
為了避開上述缺點,我決定採用另一種球體紋理繪製方式。
二、使用自定義著色器繪製自定義紋理:
1、在Babylon.js引擎中使用自定義著色器:
Babylon.js通過“著色器材質”對象提供對自定義著色器的支持:
1 var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene,{ 2 vertexElement: "sh1v4.sh", 3 fragmentElement: "sh1f4.sh", 4 }, 5 { 6 attributes: ["position"], 7 uniforms: ["worldViewProjection","worldView"] 8 }); 9 amigaMaterial.setVector4("uColor", new BABYLON.Vector4(0.0,1.0,0.0,1.0)); 10 sphere1.material=amigaMaterial;
其中ShaderMaterial構造方法的第一個參數是材質名稱、第二個參數是場景對象、第三個參數標明瞭頂點著色器和片元著色器的文件名稱,參考Babylon.js源碼可以看到引擎支持的幾種著色器代碼對象命名方式:
1 Effect.prototype._loadFragmentShader = function (fragment, callback) { 2 // DOM element ?著色器代碼是DOM標簽中的內容 3 if (fragment instanceof HTMLElement) { 4 var fragmentCode = BABYLON.Tools.GetDOMTextContent(fragment); 5 callback(fragmentCode); 6 return; 7 } 8 // Base64 encoded ?著色器代碼使用了base64編碼 9 if (fragment.substr(0, 7) === "base64:") { 10 var fragmentBinary = window.atob(fragment.substr(7)); 11 callback(fragmentBinary); 12 return; 13 } 14 // Is in local store ?著色器代碼在Babylon.js自帶的著色器代碼庫里 15 if (Effect.ShadersStore[fragment + "PixelShader"]) { 16 callback(Effect.ShadersStore[fragment + "PixelShader"]); 17 return; 18 } 19 if (Effect.ShadersStore[fragment + "FragmentShader"]) { 20 callback(Effect.ShadersStore[fragment + "FragmentShader"]); 21 return; 22 } 23 var fragmentShaderUrl;//著色器代碼是一個單獨的文件,需要通過Ajax載入 24 if (fragment[0] === "." || fragment[0] === "/" || fragment.indexOf("http") > -1) { 25 fragmentShaderUrl = fragment; 26 } 27 else {//預設情況下Engine.ShadersRepository = "src/Shaders/"; 28 fragmentShaderUrl = BABYLON.Engine.ShadersRepository + fragment; 29 } 30 // Fragment shader 31 BABYLON.Tools.LoadFile(fragmentShaderUrl + ".fragment.fx", callback); 32 };
我選擇了最後一種方式,將著色器文件放在/src/Shaders/下麵,不要忘記給著色器文件添加尾碼:
第四個參數是需要Babylon.js從記憶體向顯卡傳遞的“預設”變數,其中attributes里是Babylon.js的各種預設的頂點數據,可以選擇Mesh.geometry._vertexBuffers里的以下幾種頂點數據傳給著色器:
這裡我只選擇了將每個頂點的位置傳遞給著色器,Babylon.js引擎替我們進行了編譯鏈接著色器程式、綁定緩存等一系列操作(Babylon.js中以“_”開頭的變數一般都是在渲染過程中建立的,只有在渲染開始後才有值)。
假設一個網格有1000個頂點,那麼這1000個頂點的位置數據將被分別發送到顯卡上的1000個頂點著色器中,每個著色器使用收到的頂點數據進行計算。
uniforms里是Babylon.js向顯卡發送的預設通用變數,其中world對應網格的變換矩陣,View是相機的變換矩陣,Projection是相機的投影矩陣,worldViewProjection是三個矩陣變換的合併(關於矩陣變換可以參考https://www.cnblogs.com/ljzc002/p/8927221.html中的介紹,或者查看我在B站上傳的3D編程入門視頻教程https://space.bilibili.com/25346426/#/)
對於所有的著色器uniforms型數據都是通用的,比如上面提到的1000個頂點著色器都會使用相同的"worldViewProjection"和"worldView"變數。attributes和uniforms都屬於OpenGL的“存儲限定符”。
第九行代碼設定了一個非預設的uniforms型變數,第十行將這個材質交給球體網格。
2、WebGL版本選擇:
在進行glsl編程之前,一個重要的步驟是選擇要使用的WebGL版本:
OpenGL發展歷史如下:(源文件地址:https://docs.qq.com/sheet/B8uRgG1gE9T32RzNoW38xEnX2epfOY1cwvqG3)
可見WebGL1.0對應早期的OpenGL2.x,WebGL2.0對應較新的OpenGL4.x,顯然WebGL2.0的功能更為強大,但考慮到我的筆記本顯卡不支持WebGL2.0,只好使用舊的WebGL1.0。本文後面的glsl編程均使用OpenGL2.0的語法,OpenGL2.0存在很多缺陷,所以後面的部分內容也正是為瞭解決這些缺陷而編寫的。
Babylon.js可以自動檢測電腦支持的WebGL版本,並優先使用最新版:
1 // GL 2 if (!options.disableWebGL2Support) { 3 try { 4 this._gl = (canvas.getContext("webgl2", options) || canvas.getContext("experimental-webgl2", options)); 5 if (this._gl) { 6 this._webGLVersion = 2.0; 7 } 8 } 9 catch (e) { 10 // Do nothing 11 } 12 } 13 if (!this._gl) { 14 if (!canvas) { 15 throw new Error("The provided canvas is null or undefined."); 16 } 17 try { 18 this._gl = (canvas.getContext("webgl", options) || canvas.getContext("experimental-webgl", options)); 19 } 20 catch (e) { 21 throw new Error("WebGL not supported"); 22 } 23 } 24 if (!this._gl) { 25 throw new Error("WebGL not supported"); 26 }
Babylon.js源碼里包括很多實用的3D編程工具,即使不使用Babylon.js引擎也可以使用其中的工具簡化原生WebGL開發。
3、簡單glsl代碼:
測試用的頂點著色器代碼:
1 uniform mat4 worldViewProjection; 2 uniform mat4 worldView; 3 attribute vec3 position; 4 5 varying vec3 vPosition; 6 7 void main(){ 8 gl_Position=worldViewProjection*vec4(position,1); 9 vPosition=vec3(worldView*vec4(position,1)); 10 }
這裡varying是WebGL1.0中的第三種存儲限定符,它表示這個變數是頂點著色器的計算結果,經過插值後傳入片元著色器(關於WegGL1.0基礎知識,推薦觀看我以前發佈的3D編程入門教程)
gl_Position是OpenGL的內置變數,表示這個頂點經過各種矩陣變換之後在視口中渲染的位置,vPositon指頂點相對於相機的位置。
需要註意的是:除構造函數外,glsl不支持浮點數和整形數之間的自動轉換,浮點數通過“int i=int(f)”轉為整形數,整形數通過"float f=float(i)"轉換為浮點數,上述代碼中的vec4()和vec3()則分別是四元浮點數組和三元浮點數組的構造函數,另外WebGL1.0不具備內置的四捨五入函數,需要使用“floor(f+0.5)”代替四捨五入,並且四捨五入之後仍然是浮點數而非整形數。
除了數組的索引外,著色器中絕大部分的計算都是浮點計算,而將整形計算的結果作為數組索引時也會遇到問題,後面會詳細討論如何處理這一問題。
片元著色器代碼:
1 precision mediump float; 2 varying vec3 vPosition; 3 uniform vec4 uColor; 4 void main() 5 { 6 vec4 tempColor=uColor; 7 //對2取模,參數必須是浮點型 8 if(mod((vPosition.x+vPosition.y+vPosition.z),2.0)>1.0) 9 { 10 tempColor+=vec4(0.0,-0.4,0.0,0.0); 11 } 12 gl_FragColor=tempColor; 13 }
gl_FragColor是一個內置變數,表示片元的最終顏色,註意glsl中的顏色值從0.0到1.0,而不是html中的0到255。
執行代碼效果如下:
可見,隨著相機的移動,球體的紋理自動發生變化,這類效果是很難用貼圖方式實現的。
三、生成並保存簡單的棋盤地形
假設行星的周長為40000km,將每個地塊設為長寬均為100km的正方形,生成並保存一個包含50000多個地塊的棋盤型地面:
1、數據保存:
考慮到每個地塊都要具有獨立的交互能力,使用文件方式保存效率極低,嘗試了html5的本地存儲功能,發現Chrome瀏覽器的本地存儲空間只有5M,難以支持計劃中的對多個行星數據的保存,最終決定使用h2微型資料庫保存地塊數據(讀者可以自己搜索關於h2資料庫的知識,我的視頻教程里也提到了部分相關知識)。
a、在資料庫中建表:
將行星想象為一個一半在地上一半在地下的建築,不同的緯度對應了不同的層數,每一層有若幹個大小相同的房間
1 --建立地區塊表 2 create table tab_dqk ( 3 ID varchar(40) NOT NULL, 4 planetid varchar(40), 5 beta double, 6 pbeta double, 7 alpha double, 8 palpha double, 9 weight varchar(1000) 10 ); 11 comment on table tab_dqk is '地區塊表'; 12 comment on column tab_dqk.id is '主鍵ID'; 13 comment on column tab_dqk.planetid is '地區塊所屬的行星id'; 14 comment on column tab_dqk.beta is '地區塊的仰角'; 15 comment on column tab_dqk.pbeta is '地區塊仰角的區分度';--即這個beta仰角上下pbeta弧度都屬於這一層 16 comment on column tab_dqk.alpha is '地區塊水平轉角'; 17 comment on column tab_dqk.palpha is '地區塊水平轉角的區分度';--即這個alpha水平轉角左右palpha弧度都屬於這個房間 18 comment on column tab_dqk.weight is '用JSON表示的地形類型id權重'; 19 20 alter table tab_dqk add column floor int; 21 alter table tab_dqk add column room int; 22 alter table tab_dqk add column altitude double; 23 24 comment on column tab_dqk.floor is '地區塊位於第幾層'; 25 comment on column tab_dqk.room is '地區塊位於這一層的第幾個房間'; 26 comment on column tab_dqk.altitude is '地區塊的海拔高度'; 27 comment on column tab_dqk.weight is '地區塊類型';
1 --建立行星表 2 create table tab_planet 3 ( 4 id varchar(40) NOT NULL, 5 name varchar(20) NOT NULL, 6 coreid varchar(40), 7 min_floor int NOT NULL, 8 max_floor int NOT NULL, 9 width_room int NOT NULL, 10 radius double, 11 mass double, 12 gravity double, 13 orbit double, 14 cycle double 15 ); 16 comment on table tab_planet is '行星表'; 17 comment on column tab_planet.id is '主鍵ID'; 18 comment on column tab_planet.name is '行星名字'; 19 comment on column tab_planet.coreid is '圍繞旋轉的主星id'; 20 comment on column tab_planet.min_floor is '最低層數'; 21 comment on column tab_planet.max_floor is '最高層數'; 22 comment on column tab_planet.width_room is '數據寬度'; 23 comment on column tab_planet.radius is '半徑(km)'; 24 comment on column tab_planet.mass is '質量(t)'; 25 comment on column tab_planet.gravity is '重力加速度'; 26 comment on column tab_planet.orbit is '同步軌道高度'; 27 comment on column tab_planet.cycle is '自轉周期'; 28 29 alter table tab_planet add column perimeter int; 30 31 comment on column tab_planet.perimeter is '行星周長';
b、dao實現
傳統MVC思想認為瀏覽器端的安全沒有保證,必須在瀏覽器和資料庫之間加入一種“後臺程式”來提高安全性,這種程式通常由JAVA、C#實現,近些年也出現了許多由python和JavaScript實現的後臺程式。這些後臺程式一般包括三層:負責接收瀏覽器訪問的service層、負責將特定訪問參數和數據關聯起來的application層,負責訪問資料庫的dao層。
但是我認為這個實驗中的所有參與者都是可信任的,所以為了程式的簡潔要嘗試去掉後臺程式,我發現h2資料庫服務支持http協議通信,通過使用Fiddler對h2的網頁控制台進行抓包,編寫了直接用瀏覽器和資料庫通信的代碼:(代碼在Linkh2.js中)
1 /** 2 * Created by lz on 2018/5/15. 3 */ 4 var jsessionid=""; 5 var Url=""; 6 var UrlHead="http://127.0.0.1:8082/"; 7 var H2State="offline"; 8 var H2LoginCallback;//回調函數對象 9 function H2Login(func) 10 { 11 H2LoginCallback=func; 12 Url=UrlHead+""; 13 Argv=""; 14 Request(xmlHttp,"POST",Url,true,Argv,"application/x-www-form-urlencoded",H2LoginCallBack,0); 15 } 16 function H2LoginCallBack() 17 { 18 if(xmlHttp.readyState==4) { 19 if(isTimout=="1") 20 { 21 alert("登陸驗證請求超時!!"); 22 clearTimeout(timer); 23 xmlHttp.abort(); 24 } 25 else { 26 if (xmlHttp.status == 200) { 27 clearTimeout(timer);//停止定時器 28 try 29 { 30 var str_id=xmlHttp.responseText; 31 xmlHttp.abort(); 32 jsessionid=str_id.substr(str_id.search(/jsessionid/)+11,32) ;//從h2獲取一個jsessionid 33 console.log(jsessionid); 34 H2Login2(); 35 }catch(e) 36 { 37 alert(e); 38 console.error(e) 39 xmlHttp.abort(); 40 } 41 } 42 } 43 } 44 } 45 function H2Login2() 46 { 47 Url=UrlHead+"login.do?jsessionid="+jsessionid;//用這個jsessionid登錄 48 Argv="language=en&setting=Generic H2 (Embedded)&name=Generic H2 (Embedded)" + 49 "&driver=org.h2.Driver&url=jdbc:h2:tcp://127.0.0.1/../../datawar" + 50 "&user=datawar&password=datawar"; 51 Request(xmlHttp,"POST",Url,true,Argv,"application/x-www-form-urlencoded",H2Login2CallBack,0); 52 } 53 function H2Login2CallBack() 54 { 55 if(xmlHttp.readyState==4) { 56 if(isTimout=="1") 57 { 58 alert("登陸驗證請求超時!!"); 59 clearTimeout(timer); 60 xmlHttp.abort(); 61 } 62 else { 63 if (xmlHttp.status == 200) { 64 clearTimeout(timer);//停止定時器 65 try 66 { 67 var str_logres=xmlHttp.responseText;//這時已經在h2服務端建立登錄狀態 68 xmlHttp.abort(); 69 console.log("完成h2資料庫登錄"); 70 H2State="online"; 71 //Query(); 72 //CreateChess();//測試時將運算啟動放在這裡,實際使用時,通過渲染迴圈檢測H2State標誌來啟動運算 73 H2LoginCallback();//這樣可以執行函數對象嗎????《-可以 74 }catch(e) 75 { 76 alert(e); 77 console.error(e) 78 xmlHttp.abort(); 79 } 80 } 81 } 82 } 83 }
其中“Request”是一個Ajax請求函數,內容如下:
1 /** 2