如何10分钟入门3D游戏开发?
2017年年底,微信小程序再次發力推出了風靡大江南北的有趣應用,其中又以“跳一跳”尤為引人矚目。“跳一跳”以簡單有趣的游戲玩法設計以及流暢的交互體驗,借助微信宿主環境迅速的傳播在中國N億網民的手機中。
受微信小程序潮流的刺激,作為和微信小程序有著60%相似度定位的Light平臺也著力研究在H5端網頁運行環境下、宿主為瀏覽器時如小程序這種3D界面效果的游戲程序的開發實現方式;“跳一跳”作為風口上的標桿型應用,自然而然成為Light為3D應用開發鋪路所選用的標的。
【從2D到3D】
三維計算機圖形和二維計算機圖形的不同之處在于計算機存儲了幾何數據的三維表示,其用于計算和繪制最終的二維圖像。
除了游戲開發者之外,可能大部分的開發者所接觸的開發過程都是2D應用的開發過程。與2D應用開發相比,3D應用擁有更炫的運行效果,更加真實和沉浸式的交互體驗;除了在游戲開發中廣泛使用之外也可以使用在地圖、VR等領域。
在H5中開發3D應用需要借助于 canvas 提供的 webgl 上下文對象。
canvas
<canvas> 是一個可以使用JavaScript來繪制圖形的HTML元素,它可以用于繪制圖表、制作圖片構圖或者制作動畫。 <canvas> 因運行環境而異提供了多個上下文供開發者使用,比如我們通常使用到的繪制圖表如分時K線、柱狀圖、折線圖都是使用其2D上下文。而本文中要實現的“跳一跳復刻版”將要使用其webgl上下文以繪制3D圖形。
?
canvas.getContext("webgl");
webGL
WebGL(Web Graphics Library)在 GPU 中運行。因此需要使用能夠在 GPU 上運行的代碼。這樣的代碼需要提供成對的方法(其中一個叫頂點著色器, 另一個叫片段著色器),并且使用一種類 C/C++ 的強類型語言GLSL(OpenGL Shading Language)。 每一對方法組合起來稱為一個 program(著色程序)。
簡而言之,WebGL是一種在任何可兼容的網頁瀏覽器中渲染3D圖形的JavaScript API,但是直接使用WebGL來繪制圖形需要很多額外的知識以及大量的開發成本,由此 Three.js 這一3D應用的開發框架應運而生。
WebGL在瀏覽器中的支持情況如下圖所示:
?
three.js
three.js = three + js 。
three.js是使用js來繪制3D程序的庫。three.js將許多webgl中的概念封裝為易于操作的類目,讓開發者可以以面向對象的方法使用其開發3D應用。three.js的出現大大降低了開發基于WebGL的3D應用的門檻。
本文中重點的內容就是基于three.js開發實現“跳一跳”的Light版。但在正式的進入開發之前我們需要先了解一下兩組Three.js中的重要的概念,這是我們進入核心內容的關鍵知識儲備。
一、場景(Scene)、渲染器(Renderer)、攝像機(Camera)
此三大組件是創建3D圖形的必備組件。其中:
1. 場景用來容納圖形元素。場景是一切被渲染物體的容器,物體只有添加到場景中,才會被WebGL引擎渲染和處理。
2. 相機的作用是定義可視域,即確定哪些物體是可見的。
3. 渲染器則負責決定如何渲染出圖像。
二、形狀(Geometry)、材質(Material)、模型(Mesh)
此三大組件是構建可供渲染的物體的關鍵組件。其中:
1. 形狀即是點的集合,代表在3D的坐標系下的各種幾何形狀,幾何形狀和具體的物體展示是無關的。常用的幾何形狀包好立方體、球體、圓柱體等。
2. 材質與圖形表示方法是相關的,比如我們下文中將要使用到的MeshLambertMaterial材質就是常用材質的一種,材質決定了最終模型對光照的反應,Lambert材質一般用來表示只有漫反射的物體,如塑料;而phong材質用來表示有鏡面反射的物體,如鏡子。
3. 模型是指最終的網格模型,也就是最終可展示物體效果。模型=形狀+材質。
【使用Light完成跳一跳復刻版】
在Light體系下實現“跳一跳”小游戲并不復雜,除了以上需要的對Three.js的了解之外,其余知識要求與普通的light程序并無二致,在這里也不再涉及light工程具體的創建和開發流程,如有需要可以參考Light文檔;由微信中“跳一跳”中的視圖范圍可以看出游戲界面可以由單個Light視圖完成,具體的開發目錄如下所展示:
?
├── app.js
├── app.less
├── css
│ ├── reset.css
│ └── style.less
├── images
├── index.html
├── lib
│ ├── package.json
│ └── px2rem.js
├── project.json
├── ui
│ └── ui.vue
└── view
└── game.vue
其中 view/game.vue 為游戲界面,主要處理“挑一挑”工程初始化、計分、重置等關聯功能,是本實例的核心代碼的入口位置;如前所述,3D引擎初始化需要借助于 canvas 的3D渲染上下文,所以 view/game.vue 必須使用<canvas ref="canvas"></canvas> 來初始化canvas的可操作對象,并通過對canvas當前dom節點引用的操作完成獲取3D上下文的流程。
完成上下文環境的初始化之后就可以進行游戲具體功能的實現了,整個“跳一跳”的實現分為多個相互關聯的部分,既可以對應了前文描述各個要素,又可以劃分為多個執行的階段,下面就一一道來。
小程序中的游戲場景如以下的截圖所示:
?
當前游戲畫面中的關鍵場景要素可以抽象為:地板、箱子、jumper以及輔助元素光照、陰影等幾個方面,我們將從最基本的界面元素--地板開始一步步添加并完善游戲界面,并在最后實現跳躍動畫等步驟。
主場景
場景(Scene)是一個簡單但是不容忽視的概念。場景包含了展示、繪制的內容,同時有代表了其所包含的展示、繪制的內容。場景是一切內容的容器,要真正的運行產生可供觀察的內容,父容器必不可少。
以下所涉及的內容都被父容器所包含,可以通過 Three.js 的 Scene 構造其初始化一個父容器,不需要任何入參。
渲染器
作為游戲環節的關鍵實現步驟, Three.js 的3D引擎(渲染器)初始化是至關重要的第一步。 Three.js 提供的WebGLRenderer 是直接基于 WebGL 來展示內容,通過 canvas 的3D上下文來繪制內容。
WebGLRenderer 的構造器接受對象變量的入參,該對象支持多種參數。一般來說,我們實際使用到的參數主要有以下兩個:
1. antialias ,是否啟動防鋸齒特性。通過此屬性的配置可以提高內容展示的流暢度以及效果優化。開啟防鋸齒后會加重資源消耗。
2. canvas ,該參數制定當前內容繪制所使用的canvas對象,注意此參數的值為canvas節點的dom對象而非其context對象。在light框架中可以直接從this變量中的$refs取得針對dom節點中canvas的引用。
另外,為了獲得全屏展示的效果,還需要設置初始化后的渲染器--renderer的繪制區域。可以直接使用WebGLRenderer 的實例方法 setSize 來設置繪制區域,一般來說,應該將繪圖區域的寬度設置為屏幕的寬度--window.innerWidth ,將繪圖取悅的高度設置為屏幕的高度-- window.innerHeight 。
最后,為了確保最終渲染效果中有對陰影的展示和處理,還需要打開渲染器的渲染陰影開關。主要是針對WebGLRenderer 實例對象中的 shadowMap 屬性的配置, shadowMap 支持的可配置參數中可以通過 enabled 打開陰影的渲染開關,通過 type 屬性配置陰影處理的類型(不同的陰影類型的最終展示效果有較大的不同,具體可以參考官方的文檔)。
?
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas:this.$refs.canvas
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap
坐標系
完成了渲染器的初始化之后,接下來就需要對我們當前的場景中添加地板了。但是在此之前我們需要先來熟悉一下 WebGL 中的3D坐標系。
在 WebGL 坐標系中任何一個點的位置可以通過一個三維的向量來確認--如(1,2,-1)分別對應(x,y,z)三軸上的值。在WebGL中Z軸表示深度,z軸正值表示該點是在屏幕/觀眾近,而z的負值表示該點遠離屏幕。同樣地,x的正值表示該點是到屏幕右側的和負值表示點在屏幕左側;y軸的正值表示點在屏幕頂部,負值代表點在屏幕底部。簡單來記憶就是: WebGL 中的坐標系是個“右手坐標系”,伸出你的右手,除拇指外的四指并攏并和手臂垂直,拇指和四指也垂直。此時手臂所在的坐標系就是z軸,拇指是x軸,四指所在的坐標系就是y軸。注意,這里的坐標系和我們在初中數學課本中所了解到的坐標系是不一樣的。中學學習的坐標系是“左手坐標系”,具體的區別可以參考下圖。
?
地板
完成坐標系確認后,“地板”這一關鍵游戲元素的位置也就可以確認了。原則來說,地板可以放置在任何的位置以任何的角度(為什么?想像一瓶礦泉水是不是可以以任何角度擺放),但是為了方便我們對元素的繪制和處理,最后將地板放置在與(x,z)平面平行的平面上(以下簡稱零平面),這樣看起來更像“地板”,具體的位置為(x,-1,z)。也就是說,地板的位置在(x,z)平面的真正零平面的底部(屏幕底部),距離零平面1個標準距離。這里選擇1個標準距離主要是因為我們最終繪制的箱子的高度是2,箱子的排布會從坐標遠點開始排布(0,0,0),所以這樣保證后期放置的箱子可以保持一半在零平面以上一半在零平面以下,這個在我們設置箱子放置的時候會再次詳細說明,現在不需要關心。
確認地板所在的位置之后,使用 Three.js 的平面(PlaneGeometry)構建函數新建一個平面并放置到對應的位置即可。 PlaneGeometry 默認接收4個數字類型的入參,分別代表平面的寬度(x坐標)、長度(y坐標)、x向切分數(widthSegments)、y向切分數(heightSegments)。heightSegments和widthSegments的默認值都為1,一般來說并不需要設置,只需要設置長度和寬度即可。
單純的PlaneGeometry實例并不具有任何可以展示的效果,二手手機號碼拍賣平臺必須搭配材質(Material)才可以被展示和渲染以及針對不同的光照產生不同的反應,這里我們使用MeshLambertMaterial這個材質來生成對應的平面,MeshLambertMaterial可以針對特定的光照產生陰影的效果。關于Geometry、Material和Mesh的關系可以參考前文的內容。
另外,為了在地板中可以渲染出箱子和jumper投射的影子,還需要針對平面設置其 receiveShadow 屬性,打開接受陰影渲染的開關以產生合適的陰影。平面創建成功后可以通過父容器實例的add方法直接添加至父容器的場景中。只要添加至父容器的內容才可以被渲染和展示,否則將不會產生任何效果。
這里需要注意的是,默認平面的位置是在(x,y,0)平面上,平面創建完成后還需要進行簡單的角度轉置才能正確的放置到(x,-1,z)的位置上。這里又要回到中學數學中學習到的角度的概念,兩種表示角度的方法:角度和弧度。
在JavaScript中只能使用弧度表示法來處理旋轉問題,其中PI的值可以從 Math.PI 中讀取。
因為默認平面的位置是在(x,y,0)平面,要旋轉到(x,-1,z)需要先沿著X軸逆向旋轉90度( -0.5*Math.PI ),然后移動到Y軸的(0,-1,0)位置上。旋轉角度可以通過設置平面實例的rotation屬性中的對應的坐標軸的弧度數來完成,移動位置可以通過設置平面實例的position屬性的對應坐標軸的標準長度來完成。
?
const planeMaterial = new THREE.MeshLambertMaterial({color: config.background});
const plane = new THREE.Mesh(planeGeometry,planeMaterial);
plane.receiveShadow = true;
plane.rotation.x=-0.5*Math.PI;
plane.position.x=0;
plane.position.y=-1;
plane.position.z=0;
scene.add(plane);
放置箱子
“地板”準備好了,接下來我們就放一個箱子在地板上。
“箱子(CubeGeometry)”并不像平面一樣沒有高度,“箱子”是三維坐標下的立方體,有長、寬、高三個特征,包含8個定點和6個平面。我們可以使用 Three.js 中的 CubeGeometry 對象來實例化出不同長、寬、高的箱子,默認實例化出來的箱子的中心位置都是(0,0,0)。也就是說如果定義一個長寬高分別為4、4、2的箱子,那么其8個定點位置分別為:(2,2,1)、(2,-2,1)、(-2,-2,1)、(-2,2,1)以及(2,2,-1)、(2,-2,-1)、(-2,-2,-1)、(-2,2,-1)。CubeGeometry 的構造函數默認接收三個數字類型的參數,分別代表“箱子”的長、寬、高。默認創建出來的“箱子”的位置為(0,0,0),但是我們并不需要移動箱子的位置箱子就自然處于“地板”之上而且緊貼地板,這是因為我們在設置地板位置的時候沿著Y軸設置了-1的位置上,而箱子的下平面正好是(x,-1,z)和地板所在的平面是一致的。
另外,箱子是應該在地板中投影出陰影的,所以設置箱子實例的 castShadow 開關是至關重要的一步。同上,為了讓此“箱子”被WebGL引擎渲染,依然需要將此“箱子”添加到父容器的場景中。
?
let material = new THREE.MeshLambertMaterial({color: "#ddd"});
let box = THREE.Mesh(geometryCube, material);
box.castShadow=true;
scene.add(box);
放置jumper
“箱子”有了,接下來是“jumper”登場了。
微信“跳一跳”中的jumper是一個十分簡單的形象,生搬硬套從來不是light的風格。我們需要對jumper進行本地化處理,使其具備恒生特色,那就是“COOSS”寶。“COOSS”將作為一個恒生版本的“jumper”在游戲中迎風跳躍。當然,這并不是一個簡單的過程,在Three.js中并沒有“COOSS”這個模型,也就無從創建,好在Three.js引擎支持渲染加載各種3D建模工具如瑪雅等工具的導出對象(JSON),可以通過此導入對象來實例化模型。關于如何使用3D建模工具來制作模型可以參考對應的資料,在這里也就不詳細展開了。本文中使用的“COOSS”模型來源于“BOSS謝”半天的設計成果,不計版權,大家可以自由使用。
接下來就是需要導入外部工具創建的模型并實例化成可供Three.js渲染的對象了。對此我們可以使用Three.js默認提供的JSONLoader來加載導出的JSON文件,加載成功后會產生一個Geometry對象,然后結合特定的材料就可以產生可供渲染的“jumper”了。
另外,jumper也是是應該在地板中投影出陰影的,所以需要設置jumper實例的 castShadow 開關。
同上,為了讓此“jumper”被WebGL引擎渲染,依然需要將此“jumper”添加到父容器的場景中。
const loader=new THREE.JSONLoader();
let head,foot,glass;
loader.load("model/cooss_head_1x.json",function(geo){
const met=new THREE.MeshPhongMaterial();
head=new THREE.Mesh(geo,met);
head.name = "head";
jumper.add(head);
});
loader.load("model/cooss_foot_1x.json",function(geo){
const met=new THREE.MeshPhongMaterial();
foot=new THREE.Mesh(geo,met);
foot.name = "foot";
jumper.add(foot);
});
loader.load("model/cooss_glass_1x.json",function(geo){
const met=new THREE.MeshPhongMaterial();
glass=new THREE.Mesh(geo,met);
glass.name = "glass";
jumper.add(glass);
});
jumper.castShadow = true;
scene.add(jumper);
JSONLoader 處理導出模型的方式是異步的,但這并不會影響世界想過的展示,用戶并不會察覺內容是一部分一部分的緩慢出現。這是因為內容是按幀渲染的,而短時間內如1s內會渲染幾十次,這個幀數越高用戶的使用會越流暢。幀數的數值受設備性能和程序代碼的邏輯復雜度影響。一般來說30幀以上就不會感覺到明顯的卡頓。
jumper初始化完畢以后,還需要將jumper站立在第一個箱子上,有了前面的經驗,這個就簡單多了。只需要知道jumper的高度,然后調整jumper在Y軸上的坐標就可以了。
點亮這個世界
至此,游戲界面的場景裝配工作就做完了。我們依次完成了地板、箱子、jumper的初始化和位置、角度設定。但是,如果現在去運行已有的代碼,瀏覽器依然會是空空如也。我們還需要給這個“世界”內加上“光源(Light)”。
“光源”也是WebGL的一個重要的概念,如前文所述,有種類繁多的光源類型。當前的游戲了我們主要使用了兩種官員,一種散射光(AmbientLight),主要是用來給整個場景一個初始亮度添加出一種柔和的效果;另一種是直射光(DirectionalLight),主要使用來產生投影效果。光源的效果和現實中有這個很大的相似性,比如散射光是柔和的光線,各個角度都能覆蓋和照射到;而直射光是平行光線,照射到物體上可以產生投影效果。
散射光也叫環境光,可以通過Three.js的AmbientLight對象來實例化,其接收兩個參數來設置光源的效果。其一為光源的色值,也就是整個環境的色調;其二為官員的亮度,最亮為1,可以設置為小于1的值。
散射光的添加的實際效果調整最好是在運行中查看不斷尋找合適的值,這也是最有效的方法。
直射光可以通過Three.js的DirectionalLight的對象來實例化,其所接收的參數和AmbientLight一致,只不過可以設置光源的位置。直射光的光線是照向(0,0,0)遠點位置。
默認的直射光也是不能產生陰影效果的,需要配置DirectionalLight的實例屬性中的shadow屬性來調整陰影的參數,實際陰影的效果受這個陰影參數的應用比較大,使用過程中最好也是在運行中查看不斷尋找合適的值。
同上,為了讓光源的設置產生效果,依然需要分別將散射光源和直射光源添加到父容器的場景中。
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight( 0xffffff, 1 );
directionalLight.position.set( 25, 18, -15 );
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
scene.add( directionalLight );
這里需要注意的一點是: directionalLight.shadow.mapSize.width 和directionalLight.shadow.mapSize.height 必須是2的冪指數,如256/512/1024,這個數據的配置越大陰影的效果越精細,但是最好不要超過1024,否則將會造成消耗資源過大,游戲內容渲染的卡頓,以及較大的幀率縮水。
揭開這層幕布
萬事俱備,只欠東風。初始的內容的展示和配置都已經處理完畢,接下來才是見證奇跡的時刻。
Three.js的內容如需正常的渲染與運行,至少需要三個關鍵的元素:場景(Scene)、渲染器(Renderer)、攝像機(Camera)。有關場景和渲染器的東西我們上文中都已經講解完畢,下面來說一說攝像機(Camera)。理解上,攝像機就是觀察者眼鏡所在的位置,想象一下從不同的角度看同樣一個物體的場景,當眼鏡(攝像機)所處的角度不同的時候,所看(渲染)到的景象(投影-場景)也是不一樣的。Three.js所使用到的攝像機的概念也是基于同樣的類比。
在Three.js中提供了兩種主要的攝像機類型:透視投影攝像機(PerspectiveCamera)和正交投影攝像機(OrthographicCamera)。拋開其具體的指標、特性不談就單單從字面上理解透視投影攝像機應該是基于透視的原理,而正交投影攝像機應該和正交有關(正交依然是中學數學里的概念,正交就是垂直)。正交這個似乎不好理解,我們先來解釋透視攝像機。
我所理解的“透視”就是“近大遠小”,是符合人眼視物規律的一種說法。換言之,使用透視相機后,渲染出來的內容將會和人眼在實際世界中看到的景象很相符合。而正交相機則是“遠近皆同”,物體大小不會受到距離遠近的影響,所渲染出來的內容都是物體在相機平面的正投影。
換個方法來解釋,個人理解上西方的油畫應該都是屬于“透視相機”渲染的效果,很真實;而中國的國畫應該就是屬于“正交相機”渲染的效果,很不真實(抽象、寫意)。又比如我們經常玩的游戲,王者榮耀這種顯然是“正交投影”,而PC上的吃雞游戲必然是“透視投影”。
?
本游戲使用“正交投影攝像機”以保持觀察效果的一致,這也符合原版“跳一跳”的效果。正交投影攝像機(OrthographicCamera)的初始化可以使用Three.js中的OrthographicCamera類型來實例化,OrthographicCamera的構造函數接受六個參數的入參,分別對應了立方體的左右上下前后六個平面。這六個值的計算非常復雜,具體可以參考下圖(源圖來自Google):
?
這里需要注意的一點是:正交投影攝像機所處理的立方體的長寬比必須和渲染所在canvas的長寬比相等。
80, window.innerHeight / 80, window.innerHeight / -80, 0, 5000);
camera.position.set(100, 100, 100);
camera.lookAt(new THREE.Vector3(0, 0,0));
關于相機的說明就到這里,下面是時候解開畫布上的這塊幕布了,將我們的內容渲染在頁面上。
renderer.render(scene, camera);//scene就是上文中所提及的父容器通過以上的代碼內容會渲染在頁面當中,至此就可以看到我們前文所設置的各項內容了。
?
讓jumper動起來
靜止效果的游戲任誰都是不可能產生興趣的,所以我們的最后一步也是本文的最后一部分內容,讓鏡頭的內容動起來。
離成功只差這最后一步。磨刀不誤砍柴工,開工之前我們需要來了解一下WebGL中的動畫的原理是什么?眾所周知,“動畫”其實就是“靜畫”的組合連續播放,當連續播放超過可人眼識別的頻率(24赫茲)以后就是連續運動的效果。這個頻率就是靜畫的每秒播放張數,就是上文提及的幀率。
那在JavaScript中如何做到連續渲染?方法有二:
1. setInterval
2. requestAnimationFrame
同樣是循環觸發函數,requestAnimationFrame有著更大的優勢,其是專門為逐幀動畫實現設計的API,可以按幀重繪,從而節省系統資源,提高系統性能,改善視覺效果。
接下來的問題就簡單了,只要設置改變場景內物體的角度、位置后重新調用 renderer.render(scene, camera)就可以了。
jumper.rotation.y+=0.01;
renderer.render(scene, camera);
requestAnimationFrame(function () {
jumperMove();
})
}
jumperMove();
以上的代碼通過不斷的設置jumper在Y軸上的偏轉角度,來達成一種COOSS緩慢旋轉的效果。
【總結】
本文詳細介紹了在Light中使用Three.js框架實現一個小游戲的方法,以一個復刻版的微信“跳一跳”小游戲的實現為例介紹了游戲開發中的關鍵要素流程。本文中設計的代碼可以點擊這里獲取。
另外,游戲開發過程中還有一些關鍵的環節不容忽視,比如:碰撞處理、成功掉落、失敗掉落、場景切換、計分體系等可以參考以上鏈接地址中的代碼來了解。
總結
以上是生活随笔為你收集整理的如何10分钟入门3D游戏开发?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于行为树的新手引导设计
- 下一篇: 网易自动化UI测试解决方案Airtest