推薦:將?NSDT場(chǎng)景編輯器?加入你的3D開(kāi)發(fā)工具鏈。
由于 GSL 語(yǔ)法的復(fù)雜性麸祷,對(duì)于許多開(kāi)發(fā)人員來(lái)說(shuō) WebGL 是一個(gè)未知的領(lǐng)域选脊。但是有了 Three.js杭抠,在瀏覽器中 3D 的實(shí)現(xiàn)變得簡(jiǎn)單。下面將講述一下如何使用 Three.js 創(chuàng)建一個(gè)簡(jiǎn)單的 3D 飛機(jī)飛行的動(dòng)畫場(chǎng)景恳啥。
譯注:WebGL 是一項(xiàng)利用 JavaScriptAPI 渲染交互式 3D 電腦圖形和 2D 圖形的技術(shù)偏灿,可兼容任何的網(wǎng)頁(yè)瀏覽器,無(wú)需加裝插件钝的。通過(guò) WebGL 的技術(shù)翁垂,只需要編寫網(wǎng)頁(yè)代碼即可實(shí)現(xiàn) 3D 圖像的展示。GLSL-OpenGL Shading Language 也稱作 GLslang 硝桩,是一個(gè)以 C 語(yǔ)言為基礎(chǔ)的高階著色語(yǔ)言沿猜。它是由 OpenGL ARB 所建立,提供開(kāi)發(fā)者對(duì)繪圖管線更多的直接控制碗脊,而無(wú)需使用匯編語(yǔ)言或硬件規(guī)格語(yǔ)言邢疙。詳細(xì)麻煩谷歌或百度一下~
在本教程中,我們將創(chuàng)建一個(gè)簡(jiǎn)單的 3D 場(chǎng)景, 在兩個(gè)主要的部分會(huì)有一些交互望薄。在第一部分,我們會(huì)講解 Three.js 的基礎(chǔ)和如何創(chuàng)建一個(gè)簡(jiǎn)單的場(chǎng)景呼畸。第二部分會(huì)詳細(xì)講述如何優(yōu)化模型痕支,如何為場(chǎng)景中的不同元素增添氣氛以及更流暢的運(yùn)動(dòng)效果。
由于完整的游戲超出了本教程的范圍蛮原,但是你可以下載或 checkout 源碼卧须。它包含了許多額外有趣的部分如:碰撞,抓硬幣和增加得分儒陨。
在本教程中花嘶,我們將重點(diǎn)學(xué)習(xí) Three.js 中的一些基礎(chǔ)概念。這些基礎(chǔ)概念將帶你走進(jìn) WebGL 這新領(lǐng)域蹦漠!
事不宜遲椭员,我們馬上開(kāi)始~
HTML & CSS
本教程主要采用 Three.js 類庫(kù),Three.js 讓 WebGL 變得易于使用笛园。從官網(wǎng)或?GitHub repocheckout 獲取關(guān)于 Three.js 更多的信息隘击。
第一樣要做的事情就是在 HTML 標(biāo)簽中引入 Three.js:
<script type="text/javascript" src="js/three.js"></script>
然后在 HTML 中需要添加一個(gè)元素作為容器侍芝。
<div id="world"></div>
你可以像下面那樣寫一些簡(jiǎn)單的樣式,讓它填滿整個(gè) viewport:
#world {
? position: absolute;
? width: 100%;
? height: 100%;
? overflow: hidden;
? background: linear-gradient(#e4e0ba, #f7d9aa);
}
正如你所見(jiàn)的一樣埋同,背景有些漸變的效果州叠,就像天空。
以上是標(biāo)簽和樣式凶赁!
JavaScript
如果你已經(jīng)掌握了一些 JavaScript 的基礎(chǔ)知識(shí)咧栗,使用 Three.js 會(huì)變得相當(dāng)簡(jiǎn)單。來(lái)~我們看看實(shí)現(xiàn)不同部分的代碼虱肄。
The Color Palette
在開(kāi)始場(chǎng)景編碼之前致板,我覺(jué)得定義一個(gè)調(diào)色板是很有用的。因?yàn)樵谡麄€(gè)項(xiàng)目中會(huì)經(jīng)常使用到浩峡。在這個(gè)項(xiàng)目中可岂,我們會(huì)選擇以下這些顏色:
var Colors = {
? red:0xf25346,?
? white:0xd8d0d1,?
? brown:0x59332e,?
? pink:0xF5986E,
? brownDark:0x23190f,?
? blue:0x68c3c0
};
代碼結(jié)構(gòu)
雖然 JavaScript 代碼十分冗長(zhǎng),但是它的結(jié)構(gòu)很簡(jiǎn)單翰灾。我們需要?jiǎng)?chuàng)建所有主要的函數(shù)并放入初始函數(shù)中:
window.addEventListener('load', init, false);
function init() {
? // 創(chuàng)建場(chǎng)景缕粹,相機(jī)和渲染器
? createScene();
? // 添加光源
? createLights();
? // 添加對(duì)象
? createPlane();
? createSea();
? createSky();
? // 調(diào)用循環(huán)函數(shù),在每幀更新對(duì)象的位置和渲染場(chǎng)景
? loop();
}
創(chuàng)建場(chǎng)景
創(chuàng)建一個(gè) Three.js 的項(xiàng)目纸淮,我們至少需要以下這些:
場(chǎng)景: 把這看作一個(gè)舞臺(tái)平斩,將需要呈現(xiàn)的對(duì)象都添加進(jìn)去;
相機(jī): 在這情況下咽块,我們將使用透視相機(jī)绘面,但它也可能是正投影相機(jī);
渲染器: 使用 WebGL 渲染器顯示所有的場(chǎng)景侈沪;
渲染一個(gè)或多個(gè)對(duì)象: 在我們的例子中揭璃,我們會(huì)創(chuàng)建飛機(jī),大海亭罪,天空(一些云)瘦馍;
光源: 有不同類型可用的光源。在我們的項(xiàng)目中应役,我們主要用到營(yíng)造氛圍的半球光和制造陰影的方向光情组。
在?createScene?函數(shù)中創(chuàng)建場(chǎng)景,相機(jī)以及渲染器箩祥。
有了這三樣?xùn)|西院崇,才能使用相機(jī)將對(duì)象渲染到頁(yè)面中。
var scene, camera, fieldOfView, aspectRatio, nearPlane,
? ? farPlane, HEIGHT, WIDTH, renderer, container;
function createScene() {
? ? // 獲得屏幕的寬和高袍祖,
? ? // 用它們?cè)O(shè)置相機(jī)的縱橫比
? ? // 還有渲染器的大小
? ? HEIGHT = window.innerHeight;?
? ? WIDTH = window.innerWidth;
? ? // 創(chuàng)建場(chǎng)景
? ? scene = new THREE.Scene();? ? ?
? ? // 在場(chǎng)景中添加霧的效果底瓣;樣式上使用和背景一樣的顏色
? ? scene.fog = new THREE.Fog(0xf7d9aa, 100, 950);
? ? // 創(chuàng)建相機(jī)
? ? aspectRatio = WIDTH / HEIGHT;
? ? fieldOfView = 60;
? ? nearPlane = 1;?
? ? farPlane = 10000;
? ? /**
? ? * PerspectiveCamera 透視相機(jī)
? ? * @param fieldOfView 視角
? ? * @param aspectRatio 縱橫比
? ? * @param nearPlane 近平面
? ? * @param farPlane 遠(yuǎn)平面
? ? */
? ? camera = new THREE.PerspectiveCamera(?
? ? ? fieldOfView,
? ? ? aspectRatio,
? ? ? nearPlane,
? ? ? farPlane
? ? ? );
? ? // 設(shè)置相機(jī)的位置
? ? camera.position.x = 0;?
? ? camera.position.z = 200;?
? ? camera.position.y = 100;
? ? // 創(chuàng)建渲染器
? ? renderer = new THREE.WebGLRenderer({
? ? // 在 css 中設(shè)置背景色透明顯示漸變色
? ? ? alpha: true,
? ? // 開(kāi)啟抗鋸齒,但這樣會(huì)降低性能盲泛。
? ? // 不過(guò)濒持,由于我們的項(xiàng)目基于低多邊形的键耕,那還好 :)
? ? ? antialias: true
? ? });
? ? // 定義渲染器的尺寸;在這里它會(huì)填滿整個(gè)屏幕
? ? renderer.setSize(WIDTH, HEIGHT);
? ? // 打開(kāi)渲染器的陰影地圖
? ? renderer.shadowMap.enabled = true;
? ? // 在 HTML 創(chuàng)建的容器中添加渲染器的 DOM 元素
? ? container = document.getElementById('world');
? ? container.appendChild(renderer.domElement);
? ? // 監(jiān)聽(tīng)屏幕柑营,縮放屏幕更新相機(jī)和渲染器的尺寸
? ? window.addEventListener('resize', handleWindowResize, false);
}
由于屏幕的尺寸改變屈雄,我們需要更新渲染器的尺寸和相機(jī)的縱橫比。
function handleWindowResize() {
? // 更新渲染器的高度和寬度以及相機(jī)的縱橫比
? HEIGHT = window.innerHeight;
? WIDTH = window.innerWidth;? ? ? ?
? renderer.setSize(WIDTH, HEIGHT);
? camera.aspect = WIDTH / HEIGHT;? ? ? ?
? camera.updateProjectionMatrix();
}
光源
當(dāng)創(chuàng)建一個(gè)場(chǎng)景時(shí)官套,光源是最棘手的一部分酒奶。光源可以奠定整個(gè)場(chǎng)景的基調(diào),所以要適當(dāng)?shù)剡x取奶赔。在這部分我們要盡量制造足以讓對(duì)象可見(jiàn)的光源惋嚎。
var hemisphereLight, shadowLight;
function createLights() {
? // 半球光就是漸變的光;
? // 第一個(gè)參數(shù)是天空的顏色站刑,第二個(gè)參數(shù)是地上的顏色另伍,第三個(gè)參數(shù)是光源的強(qiáng)度
? hemisphereLight = new THREE.HemisphereLight(0xaaaaaa,0x000000, .9);
? // 方向光是從一個(gè)特定的方向的照射
? // 類似太陽(yáng),即所有光源是平行的
? // 第一個(gè)參數(shù)是關(guān)系顏色绞旅,第二個(gè)參數(shù)是光源強(qiáng)度
? shadowLight = new THREE.DirectionalLight(0xffffff, .9);
? // 設(shè)置光源的方向摆尝。?
? // 位置不同,方向光作用于物體的面也不同因悲,看到的顏色也不同
? shadowLight.position.set(150, 350, 350);
? // 開(kāi)啟光源投影
? shadowLight.castShadow = true;
? // 定義可見(jiàn)域的投射陰影
? shadowLight.shadow.camera.left = -400;
? shadowLight.shadow.camera.right = 400;
? shadowLight.shadow.camera.top = 400;
? shadowLight.shadow.camera.bottom = -400;
? shadowLight.shadow.camera.near = 1;
? shadowLight.shadow.camera.far = 1000;
? // 定義陰影的分辨率堕汞;雖然分辨率越高越好,但是需要付出更加昂貴的代價(jià)維持高性能的表現(xiàn)晃琳。
? shadowLight.shadow.mapSize.width = 2048;
? shadowLight.shadow.mapSize.height = 2048;
? // 為了使這些光源呈現(xiàn)效果讯检,只需要將它們添加到場(chǎng)景中
? scene.add(hemisphereLight);?
? scene.add(shadowLight);
}
正如你所見(jiàn),創(chuàng)建光源用到許多參數(shù)卫旱。不要再猶豫人灼,大膽嘗試用不同的顏色,強(qiáng)度的光源顾翼。你發(fā)現(xiàn)不同的光源在場(chǎng)景中能夠營(yíng)造有趣的氛圍和環(huán)境挡毅。而且你會(huì)找到感覺(jué):如何按照你的需求優(yōu)化它們。
用 Three.js 創(chuàng)建對(duì)象
Three.js 中已經(jīng)有大量的現(xiàn)成幾何體如:立方體暴构,球體,圓環(huán)面段磨,圓柱體以及飛機(jī)原型取逾。
對(duì)于我們的項(xiàng)目,所有的對(duì)象只需要通過(guò)這些幾何體組合而成苹支。這非常適合低多邊形的風(fēng)格砾隅,而且我們可以不必在 3D 建模軟件中創(chuàng)建對(duì)象。
用一個(gè)圓柱體代表大海
我們開(kāi)始創(chuàng)建大海模型债蜜,因?yàn)樗俏覀儗?shí)現(xiàn)中最簡(jiǎn)單的對(duì)象晴埂。為了簡(jiǎn)單起見(jiàn)究反,我們將大海看作一個(gè)簡(jiǎn)單的圓柱體放置在屏幕的底部儒洛。之后我們?cè)偕钊胙芯咳绾胃纳拼蠛5耐庥^精耐。
接著,讓我們使大豪哦停看起來(lái)更具吸引力卦停,海浪更加逼真。
//首先定義一個(gè)大海對(duì)象
Sea = function(){
? // 創(chuàng)建一個(gè)圓柱幾何體
? // 參數(shù)為:頂面半徑恼蓬,底面半徑惊完,高度,半徑分段处硬,高度分段
? var geom = new THREE.CylinderGeometry(600,600,800,40,10);
? // 在 x 軸旋轉(zhuǎn)幾何體
? geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
? // 創(chuàng)建材質(zhì)
? var mat = new THREE.MeshPhongMaterial({
? ? color:Colors.blue,
? ? transparent:true,
? ? opacity:.6,
? ? shading:THREE.FlatShading
? });
? // 為了在 Three.js 創(chuàng)建一個(gè)物體小槐,我們必須創(chuàng)建網(wǎng)格用來(lái)組合幾何體和一些材質(zhì)
? this.mesh = new THREE.Mesh(geom, mat);
? // 允許大海對(duì)象接收陰影
? this.mesh.receiveShadow = true;
}
//實(shí)例化大海對(duì)象,并添加至場(chǎng)景
var sea;
function createSea(){
sea = new Sea();
// 在場(chǎng)景底部荷辕,稍微推擠一下
sea.mesh.position.y = -600;
// 添加大海的網(wǎng)格至場(chǎng)景
scene.add(sea.mesh);
}
總結(jié)一下創(chuàng)建對(duì)象凿跳,需要什么東西。
我們需要:
創(chuàng)建幾何體
創(chuàng)建材質(zhì)
將它們傳入網(wǎng)格
將網(wǎng)格添加至場(chǎng)景
通過(guò)這些步驟桐腌,我們可以創(chuàng)建許多不同種類的幾何體≈粝裕現(xiàn)在,如果我們把它們組合起來(lái)案站,就可以創(chuàng)建更多復(fù)雜的形狀躬审。
在以下步驟中,我們將精確地學(xué)習(xí)如何創(chuàng)建復(fù)雜的形狀蟆盐。
把簡(jiǎn)單的正方體組合成復(fù)雜的形狀
云的制作會(huì)有一點(diǎn)點(diǎn)復(fù)雜承边,因?yàn)樗麄兪怯扇舾蓚€(gè)正方體組合而成的一個(gè)隨機(jī)形狀。
Cloud = function(){
// 創(chuàng)建一個(gè)空的容器放置不同形狀的云
this.mesh = new THREE.Object3D();
// 創(chuàng)建一個(gè)正方體
// 這個(gè)形狀會(huì)被復(fù)制創(chuàng)建云
var geom = new THREE.BoxGeometry(20,20,20);
// 創(chuàng)建材質(zhì)石挂;一個(gè)簡(jiǎn)單的白色材質(zhì)就可以達(dá)到效果
var mat = new THREE.MeshPhongMaterial({
? color:Colors.white,?
});
// 隨機(jī)多次復(fù)制幾何體
var nBlocs = 3+Math.floor(Math.random()*3);
for (var i=0; i<nBlocs; i++ ){
? // 通過(guò)復(fù)制幾何體創(chuàng)建網(wǎng)格
? var m = new THREE.Mesh(geom, mat);
? // 隨機(jī)設(shè)置每個(gè)正方體的位置和旋轉(zhuǎn)角度
? m.position.x = i*15;
? m.position.y = Math.random()*10;
? m.position.z = Math.random()*10;
? m.rotation.z = Math.random()*Math.PI*2;
? m.rotation.y = Math.random()*Math.PI*2;
? // 隨機(jī)設(shè)置正方體的大小
? var s = .1 + Math.random()*.9;
? m.scale.set(s,s,s);
? // 允許每個(gè)正方體生成投影和接收陰影
? m.castShadow = true;
? m.receiveShadow = true;
? // 將正方體添加至開(kāi)始時(shí)我們創(chuàng)建的容器中
? this.mesh.add(m);
}
}
現(xiàn)在博助,我們已經(jīng)創(chuàng)建一朵云,我們通過(guò)復(fù)制它來(lái)創(chuàng)建天空痹愚,而且將其放置在 z 軸任意位置富岳。
// 定義一個(gè)天空對(duì)象
Sky = function(){
? // 創(chuàng)建一個(gè)空的容器
? this.mesh = new THREE.Object3D();
? // 選取若干朵云散布在天空中
? this.nClouds = 20;
? // 把云均勻地散布
? // 我們需要根據(jù)統(tǒng)一的角度放置它們
? var stepAngle = Math.PI*2 / this.nClouds;
? // 創(chuàng)建云對(duì)象
? for(var i=0; i<this.nClouds; i++){
? var c = new Cloud();
? // 設(shè)置每朵云的旋轉(zhuǎn)角度和位置
? // 因此我們使用了一點(diǎn)三角函數(shù)
? var a = stepAngle*i; //這是云的最終角度
? var h = 750 + Math.random()*200; // 這是軸的中心和云本身之間的距離
? // 三角函數(shù)!U窖式!希望你還記得數(shù)學(xué)學(xué)過(guò)的東西 :)
? // 假如你不記得:
? // 我們簡(jiǎn)單地把極坐標(biāo)轉(zhuǎn)換成笛卡坐標(biāo)
? c.mesh.position.y = Math.sin(a)*h;
? c.mesh.position.x = Math.cos(a)*h;
? // 根據(jù)云的位置旋轉(zhuǎn)它
? c.mesh.rotation.z = a + Math.PI/2;
? // 為了有更好的效果,我們把云放置在場(chǎng)景中的隨機(jī)深度位置
? c.mesh.position.z = -400-Math.random()*400;
? // 而且我們?yōu)槊慷湓圃O(shè)置一個(gè)隨機(jī)大小
? var s = 1+Math.random()*2;
? c.mesh.scale.set(s,s,s);
? // 不要忘記將每朵云的網(wǎng)格添加到場(chǎng)景中
? this.mesh.add(c.mesh);
? }?
}
// 現(xiàn)在我們實(shí)例化天空對(duì)象动壤,而且將它放置在屏幕中間稍微偏下的位置萝喘。
var sky;
function createSky(){
? sky = new Sky();
? sky.mesh.position.y = -600;
? scene.add(sky.mesh);
}
更加復(fù)雜的形狀:創(chuàng)建飛機(jī)模型
壞消息是:創(chuàng)建飛機(jī)模型的代碼有點(diǎn)復(fù)雜有點(diǎn)長(zhǎng)。但是好消息是:為了創(chuàng)建它我們已經(jīng)學(xué)習(xí)了所有應(yīng)該知道的。這里所有都是關(guān)于組合和封裝形狀的代碼阁簸。
var AirPlane = function() {
? this.mesh = new THREE.Object3D();
? // 創(chuàng)建機(jī)艙
? var geomCockpit = new THREE.BoxGeometry(60, 50, 50, 1, 1, 1);
? var matCockpit = new THREE.MeshPhongMaterial({
? ? ? color: Colors.red,
? ? ? shading: THREE.FlatShading
? });
? var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
? cockpit.castShadow = true;
? cockpit.receiveShadow = true;
? this.mesh.add(cockpit);
? // 創(chuàng)建引擎
? var geomEngine = new THREE.BoxGeometry(20, 50, 50, 1, 1, 1);
? var matEngine = new THREE.MeshPhongMaterial({
? ? ? ? color: Colors.white,
? ? ? ? shading: THREE.FlatShading
? });
? var engine = new THREE.Mesh(geomEngine, matEngine);
? engine.position.x = 40;
? engine.castShadow = true;
? engine.receiveShadow = true;
? this.mesh.add(engine);
? // 創(chuàng)建機(jī)尾
? var geomTailPlane = new THREE.BoxGeometry(15, 20, 5, 1, 1, 1);
? var matTailPlane = new THREE.MeshPhongMaterial({
? ? ? color: Colors.red,
? ? ? shading: THREE.FlatShading
? });
? var tailPlane = new THREE.Mesh(geomTailPlane, matTailPlane);
? tailPlane.position.set(-35, 25, 0);
? tailPlane.castShadow = true;
? tailPlane.receiveShadow = true;
? this.mesh.add(tailPlane);
? ? // 創(chuàng)建機(jī)翼
? var geomSideWing = new THREE.BoxGeometry(40, 8, 150, 1, 1, 1);
? var matSideWing = new THREE.MeshPhongMaterial({
? ? ? color: Colors.red,
? ? ? shading: THREE.FlatShading
? });
? var sideWing = new THREE.Mesh(geomSideWing, matSideWing);
? sideWing.castShadow = true;
? sideWing.receiveShadow = true;
? this.mesh.add(sideWing);
? // 創(chuàng)建螺旋槳
? var geomPropeller = new THREE.BoxGeometry(20, 10, 10, 1, 1, 1);
? var matPropeller = new THREE.MeshPhongMaterial({
? ? ? color: Colors.brown,
? ? ? shading: THREE.FlatShading
? });
? this.propeller = new THREE.Mesh(geomPropeller, matPropeller);
? this.propeller.castShadow = true;
? this.propeller.receiveShadow = true;
? // 創(chuàng)建螺旋槳的槳葉
? var geomBlade = new THREE.BoxGeometry(1, 100, 20, 1, 1, 1);
? var matBlade = new THREE.MeshPhongMaterial({
? ? ? color: Colors.brownDark,
? ? ? shading: THREE.FlatShading
? });
? var blade = new THREE.Mesh(geomBlade, matBlade);
? blade.position.set(8, 0, 0);
? blade.castShadow = true;
? blade.receiveShadow = true;
? this.propeller.add(blade);
? this.propeller.position.set(50, 0, 0);
? this.mesh.add(this.propeller);
};
這飛機(jī)看起來(lái)很簡(jiǎn)單吧爬早?不要擔(dān)心它現(xiàn)在的樣子,接著我們將看到如何改進(jìn)形狀启妹,讓飛機(jī)更加好看!
現(xiàn)在筛严,我們可以實(shí)例化這飛機(jī)并添加到場(chǎng)景中:
var airplane;
function createPlane(){
? airplane = new AirPlane();
? airplane.mesh.scale.set(.25,.25,.25);
? airplane.mesh.position.y = 100;
? scene.add(airplane.mesh);
}
渲染
我們已經(jīng)創(chuàng)建了幾個(gè)對(duì)象并把它們添加到我們的場(chǎng)景中了,但是為啥運(yùn)行游戲的時(shí)候什么都看不到呢翅溺?那是因?yàn)槲覀冃枰秩緢?chǎng)景,添加一下這句簡(jiǎn)單的代碼:
renderer.render(scene, camera);
動(dòng)畫
通過(guò)使螺旋槳旋轉(zhuǎn)并轉(zhuǎn)動(dòng)大海和云讓我們的場(chǎng)景更具生命力咙崎。因此我們需要一個(gè)無(wú)限循環(huán)函數(shù)优幸。
function loop(){
? // 使螺旋槳旋轉(zhuǎn)并轉(zhuǎn)動(dòng)大海和云
? airplane.propeller.rotation.x += 0.3;
? sea.mesh.rotation.z += .005;
? sky.mesh.rotation.z += .01;
? // 渲染場(chǎng)景
? renderer.render(scene, camera);
? // 重新調(diào)用 render() 函數(shù)
? requestAnimationFrame(loop);
}
正如你看到的一樣,我們將渲染器的 render() 函數(shù)移動(dòng)到 loop() 函數(shù)中。因?yàn)槊看涡薷奈矬w的位置或顏色之類的屬性就需要重新調(diào)用一次 render() 函數(shù)虐沥。
隨著鼠標(biāo)的移動(dòng),添加交互
在這刻涩咖,我們已經(jīng)看見(jiàn)飛機(jī)在場(chǎng)景在中間撇他,接下來(lái)我們還需要實(shí)現(xiàn)什么呢蹋绽?就是監(jiān)聽(tīng)鼠標(biāo)的移動(dòng)實(shí)現(xiàn)交互路呜。
當(dāng)文檔加載完成,我們就需要為文檔添加監(jiān)聽(tīng)器织咧,檢測(cè)鼠標(biāo)是否有移動(dòng)胀葱。因此,我們需要對(duì)初始化函數(shù)作出以下的修改笙蒙。
function init(event){
? createScene();
? createLights();
? createPlane();
? createSea();
? createSky();
? //添加監(jiān)聽(tīng)器
? document.addEventListener('mousemove', handleMouseMove, false);
? loop();
}
另外抵屿,我們創(chuàng)建一個(gè)?mousemove?事件的事件處理函數(shù)。
var mousePos={x:0, y:0};
// mousemove 事件處理函數(shù)
function handleMouseMove(event) {
? // 這里我把接收到的鼠標(biāo)位置的值轉(zhuǎn)換成歸一化值手趣,在-1與1之間變化
? // 這是x軸的公式:
? var tx = -1 + (event.clientX / WIDTH)*2;
? // 對(duì)于 y 軸晌该,我們需要一個(gè)逆公式
? // 因?yàn)?2D 的 y 軸與 3D 的 y 軸方向相反
? var ty = 1 - (event.clientY / HEIGHT)*2;
? mousePos = {x:tx, y:ty};
}
現(xiàn)在獲得鼠標(biāo)的?x?,?y?坐標(biāo)值,我們可以適當(dāng)?shù)匾苿?dòng)飛機(jī)绿渣。
我們需要修改循環(huán)函數(shù)并添加一個(gè)新功能去更新飛機(jī)的位置朝群。
function loop(){
? sea.mesh.rotation.z += .005;
? sky.mesh.rotation.z += .01;
? // 更新每幀的飛機(jī)
? updatePlane();
? renderer.render(scene, camera);
? requestAnimationFrame(loop);
}
function updatePlane(){
? // 讓我們?cè)趚軸上-100至100之間和y軸25至175之間移動(dòng)飛機(jī)
? // 根據(jù)鼠標(biāo)的位置在-1與1之間的范圍,我們使用的 normalize 函數(shù)實(shí)現(xiàn)(如下)
? var targetX = normalize(mousePos.x, -1, 1, -100, 100);
? var targetY = normalize(mousePos.y, -1, 1, 25, 175);
? // 更新飛機(jī)的位置
? airplane.mesh.position.y = targetY;
? airplane.mesh.position.x = targetX;
? airplane.propeller.rotation.x += 0.3;
}
function normalize(v,vmin,vmax,tmin, tmax){
? var nv = Math.max(Math.min(v,vmax), vmin);
? var dv = vmax-vmin;
? var pc = (nv-vmin)/dv;
? var dt = tmax-tmin;
? var tv = tmin + (pc*dt);
? return tv;
}
恭喜你中符!到這里姜胖,已經(jīng)實(shí)現(xiàn)了飛機(jī)隨著鼠標(biāo)的移動(dòng)而移動(dòng)。到目前為止淀散,看看我們已經(jīng)實(shí)現(xiàn)了什么功能:
幾乎完成右莱!
正如你所看見(jiàn)的,使用 Three.js 對(duì)創(chuàng)建 WebGL 內(nèi)容有非常大的幫助档插。建立一個(gè)場(chǎng)景和渲染一些自定義對(duì)象不需要懂太多 WebGL 的知識(shí)慢蜓。到目前為止,我們已經(jīng)學(xué)會(huì)一些基礎(chǔ)概念和你已經(jīng)可以開(kāi)始通過(guò)調(diào)整一些參數(shù)類似光源的強(qiáng)度郭膛,霧的顏色和物體的大小掌握了一些基本的訣竅晨抡。或許現(xiàn)在你已經(jīng)很熟悉創(chuàng)建一些新的對(duì)象了则剃。
如果你想學(xué)習(xí)更加深入的技術(shù)耘柱,請(qǐng)繼續(xù)閱讀。因?yàn)槟銓?huì)學(xué)習(xí)到如何改進(jìn) 3D 場(chǎng)景棍现,使飛機(jī)飛行得更加平穩(wěn)调煎,并模仿低多邊形海浪對(duì)大海的影響。
一架更酷的飛機(jī)
好了~我們之前創(chuàng)建了非臣喊梗基礎(chǔ)的飛機(jī)士袄。我們現(xiàn)在知道如何創(chuàng)建對(duì)象并組合它們悲关,但是我們?nèi)匀恍枰獙W(xué)習(xí)如何修改幾何體令其更加符合我們的需求。
例如正方體娄柳,可以移動(dòng)它的頂點(diǎn)坚洽。在我們的案例中,我們需要使它更加像駕駛艙西土。
讓我們看一下駕駛艙這部分的代碼,還有看下我們是如何讓他的背部變得更窄的:
// 駕駛艙
var geomCockpit = new THREE.BoxGeometry(80,50,50,1,1,1);
var matCockpit = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});
// 我們可以通過(guò)訪問(wèn)形狀中頂點(diǎn)數(shù)組中一組特定的頂點(diǎn)
// 然后移動(dòng)它的 x, y, z 屬性:
geomCockpit.vertices[4].y-=10;
geomCockpit.vertices[4].z+=20;
geomCockpit.vertices[5].y-=10;
geomCockpit.vertices[5].z-=20;
geomCockpit.vertices[6].y+=30;
geomCockpit.vertices[6].z+=20;
geomCockpit.vertices[7].y+=30;
geomCockpit.vertices[7].z-=20;
var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
cockpit.castShadow = true;
cockpit.receiveShadow = true;
this.mesh.add(cockpit);
這就是如何操縱一個(gè)形狀以適應(yīng)我們的需求的一個(gè)例子鞍盗。
如果你看到飛機(jī)的完整代碼需了,你會(huì)看到幾個(gè)對(duì)象:更像窗口的對(duì)象和更美觀的螺旋槳。沒(méi)有什么復(fù)雜的東西般甲,試著調(diào)整相關(guān)的值找找感覺(jué)肋乍,制造屬于你自己的飛機(jī)。
但是敷存,是誰(shuí)在開(kāi)飛機(jī)呢墓造?
為我們的飛機(jī)添加一個(gè)飛行員,就好像添加幾個(gè)盒子一樣容易锚烦。
但是我們只需要一個(gè)酷酷的飛行員觅闽,頭發(fā)要很飄逸的!感覺(jué)它好像很難實(shí)現(xiàn)的樣子涮俄,但是由于我們開(kāi)始的時(shí)候是在低多邊形的場(chǎng)景下開(kāi)始的蛉拙,所以這就變得簡(jiǎn)單多了!嘗試通過(guò)幾個(gè)盒子模擬創(chuàng)建飄逸的頭發(fā)彻亲,同時(shí)會(huì)給予一種獨(dú)特的感覺(jué)孕锄。
讓我們看看源碼:
var Pilot = function(){
? this.mesh = new THREE.Object3D();
? this.mesh.name = "pilot";
? // angleHairs是用于后面頭發(fā)的動(dòng)畫的屬性
? this.angleHairs=0;
? // 飛行員的身體
? var bodyGeom = new THREE.BoxGeometry(15,15,15);
? var bodyMat = new THREE.MeshPhongMaterial({color:Colors.brown, shading:THREE.FlatShading});
? var body = new THREE.Mesh(bodyGeom, bodyMat);
? body.position.set(2,-12,0);
? this.mesh.add(body);
? // 飛行員的臉部
? var faceGeom = new THREE.BoxGeometry(10,10,10);
? var faceMat = new THREE.MeshLambertMaterial({color:Colors.pink});
? var face = new THREE.Mesh(faceGeom, faceMat);
? this.mesh.add(face);
? // 飛行員的頭發(fā)
? var hairGeom = new THREE.BoxGeometry(4,4,4);
? var hairMat = new THREE.MeshLambertMaterial({color:Colors.brown});
? var hair = new THREE.Mesh(hairGeom, hairMat);
? // 調(diào)整頭發(fā)的形狀至底部的邊界,這將使它更容易擴(kuò)展苞尝。
? hair.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,2,0));
? // 創(chuàng)建一個(gè)頭發(fā)的容器
? var hairs = new THREE.Object3D();
? // 創(chuàng)建一個(gè)頭發(fā)頂部的容器(這會(huì)有動(dòng)畫效果)
? this.hairsTop = new THREE.Object3D();
? // 創(chuàng)建頭頂?shù)念^發(fā)并放置他們?cè)谝粋€(gè)3*4的網(wǎng)格中
? for (var i=0; i<12; i++){
? ? ? var h = hair.clone();
? ? ? var col = i%3;
? ? ? var row = Math.floor(i/3);
? ? ? var startPosZ = -4;
? ? ? var startPosX = -4;
? ? ? h.position.set(startPosX + row*4, 0, startPosZ + col*4);
? ? ? this.hairsTop.add(h);
? }
? hairs.add(this.hairsTop);
? // 創(chuàng)建臉龐的頭發(fā)
? var hairSideGeom = new THREE.BoxGeometry(12,4,2);
? hairSideGeom.applyMatrix(new THREE.Matrix4().makeTranslation(-6,0,0));
? var hairSideR = new THREE.Mesh(hairSideGeom, hairMat);
? var hairSideL = hairSideR.clone();
? hairSideR.position.set(8,-2,6);
? hairSideL.position.set(8,-2,-6);
? hairs.add(hairSideR);
? hairs.add(hairSideL);
? // 創(chuàng)建后腦勺的頭發(fā)
? var hairBackGeom = new THREE.BoxGeometry(2,8,10);
? var hairBack = new THREE.Mesh(hairBackGeom, hairMat);
? hairBack.position.set(-1,-4,0)
? hairs.add(hairBack);
? hairs.position.set(-5,5,0);
? this.mesh.add(hairs);
? var glassGeom = new THREE.BoxGeometry(5,5,5);
? var glassMat = new THREE.MeshLambertMaterial({color:Colors.brown});
? var glassR = new THREE.Mesh(glassGeom,glassMat);
? glassR.position.set(6,0,3);
? var glassL = glassR.clone();
? glassL.position.z = -glassR.position.z;
? var glassAGeom = new THREE.BoxGeometry(11,1,11);
? var glassA = new THREE.Mesh(glassAGeom, glassMat);
? this.mesh.add(glassR);
? this.mesh.add(glassL);
? this.mesh.add(glassA);
? var earGeom = new THREE.BoxGeometry(2,3,2);
? var earL = new THREE.Mesh(earGeom,faceMat);
? earL.position.set(0,0,-6);
? var earR = earL.clone();
? earR.position.set(0,0,6);
? this.mesh.add(earL);
? this.mesh.add(earR);
}
// 移動(dòng)頭發(fā)
Pilot.prototype.updateHairs = function(){
? // 獲得頭發(fā)
? var hairs = this.hairsTop.children;
? // 根據(jù) angleHairs 的角度更新頭發(fā)
? var l = hairs.length;
? for (var i=0; i<l; i++){
? ? ? var h = hairs[i];
? ? ? // 每根頭發(fā)將周期性的基礎(chǔ)上原始大小的75%至100%之間作調(diào)整撮竿。
? ? ? h.scale.y = .75 + Math.cos(this.angleHairs+i/3)*.25;
? }
? // 在下一幀增加角度
? this.angleHairs += 0.16;
}
現(xiàn)在讓頭發(fā)動(dòng)起來(lái)贮懈,只需要在循環(huán)函數(shù)里添加以下這句代碼。
airplane.pilot.updateHairs();
制作海浪
或許你已經(jīng)注意到這大海不像真的大海那樣,但更像被壓路機(jī)壓平的表面咧擂。
它需要一些海浪。這需要結(jié)合我們之前用到的兩項(xiàng)技術(shù)來(lái)完成:
操縱幾何體的頂點(diǎn)就像我們處理飛機(jī)的駕駛艙那樣
每個(gè)頂點(diǎn)執(zhí)行循環(huán)移動(dòng)就像我們移動(dòng)飛行員的頭發(fā)一樣
為了制造海浪剂碴,我們將圍繞圓柱體的初始位置對(duì)每個(gè)頂點(diǎn)旋轉(zhuǎn)阳柔。通過(guò)給它們一個(gè)隨機(jī)旋轉(zhuǎn)速度和一個(gè)隨機(jī)距離(旋轉(zhuǎn)半徑)。很抱歉舀患,這里還是需要用到一些三角函數(shù)徽级!
讓我們對(duì)大海作出一些修改:
Sea = function(){
? var geom = new THREE.CylinderGeometry(600,600,800,40,10);
? geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
? // 重點(diǎn):通過(guò)合并頂點(diǎn),我們確保海浪的連續(xù)性
? geom.mergeVertices();
? // 獲得頂點(diǎn)
? var l = geom.vertices.length;
? // 創(chuàng)建一個(gè)新的數(shù)組存儲(chǔ)與每個(gè)頂點(diǎn)關(guān)聯(lián)的值:
? this.waves = [];
? for (var i=0; i<l; i++){
? ? ? // 獲取每個(gè)頂點(diǎn)
? ? ? var v = geom.vertices[i];
? ? ? // 存儲(chǔ)一些關(guān)聯(lián)的數(shù)值
? ? ? this.waves.push({y:v.y,
? ? ? ? ? ? ? ? ? ? ? x:v.x,
? ? ? ? ? ? ? ? ? ? ? ? z:v.z,
? ? ? ? ? ? ? ? ? ? ? ? // 隨機(jī)角度
? ? ? ? ? ? ? ? ? ? ? ? ang:Math.random()*Math.PI*2,
? ? ? ? ? ? ? ? ? ? ? ? // 隨機(jī)距離
? ? ? ? ? ? ? ? ? ? ? ? amp:5 + Math.random()*15,
? ? ? ? ? ? ? ? ? ? ? ? // 在0.016至0.048度/幀之間的隨機(jī)速度
? ? ? ? ? ? ? ? ? ? ? ? speed:0.016 + Math.random()*0.032
? ? ? });
? };
? var mat = new THREE.MeshPhongMaterial({
? ? ? color:Colors.blue,
? ? ? transparent:true,
? ? ? opacity:.8,
? ? ? shading:THREE.FlatShading,
? });
? this.mesh = new THREE.Mesh(geom, mat);
? this.mesh.receiveShadow = true;
}
// 現(xiàn)在我們創(chuàng)建一個(gè)在每幀可以調(diào)用的函數(shù)聊浅,用于更新頂點(diǎn)的位置來(lái)模擬海浪餐抢。
Sea.prototype.moveWaves = function (){
? // 獲取頂點(diǎn)
? var verts = this.mesh.geometry.vertices;
? var l = verts.length;
? for (var i=0; i<l; i++){
? ? ? var v = verts[i];
? ? ? // 獲取關(guān)聯(lián)的值
? ? ? var vprops = this.waves[i];
? ? ? // 更新頂點(diǎn)的位置
? ? ? v.x = vprops.x + Math.cos(vprops.ang)*vprops.amp;
? ? ? v.y = vprops.y + Math.sin(vprops.ang)*vprops.amp;
? ? ? // 下一幀自增一個(gè)角度
? ? ? vprops.ang += vprops.speed;
? }
? // 告訴渲染器代表大海的幾何體發(fā)生改變
? // 事實(shí)上现使,為了維持最好的性能
? // Three.js會(huì)緩存幾何體和忽略一些修改
? // 除非加上這句
? this.mesh.geometry.verticesNeedUpdate=true;
? sea.mesh.rotation.z += .005;
}
就好像我們對(duì)飛行員的頭發(fā)做的那樣,我們?cè)谘h(huán)函數(shù)中添加以下這句代碼:
sea.moveWaves();
現(xiàn)在好好欣賞海浪吧旷痕!
改善場(chǎng)景中的光源
在教程中的第一部分碳锈,我們已經(jīng)創(chuàng)建了一些光源。但是想為場(chǎng)景添加更好的氣氛欺抗,并使陰影更加柔和售碳。為了實(shí)現(xiàn)它,我們打算使用環(huán)境光源绞呈。
在?createLight?函數(shù)中贸人,我們添加以下幾行代碼:
// 環(huán)境光源修改場(chǎng)景中的全局顏色和使陰影更加柔和
ambientLight = new THREE.AmbientLight(0xdc8874, .5);scene.add(ambientLight);
別再猶豫了!調(diào)節(jié)環(huán)境光源的顏色和強(qiáng)度佃声,它會(huì)為你的場(chǎng)景增添獨(dú)特的潤(rùn)色艺智。
一次平穩(wěn)的飛行
我們的小小飛機(jī)已經(jīng)隨著我們的鼠標(biāo)移動(dòng)。但它總感覺(jué)不像真正的飛行圾亏。當(dāng)飛機(jī)改變它的飛行高度十拣,如何改變它的位置和方向時(shí)更加流暢就完美了。在教程的最后一點(diǎn)志鹃,我們將實(shí)現(xiàn)它夭问。
一個(gè)簡(jiǎn)單的方法就是讓它移動(dòng)到目標(biāo)位置,通過(guò)添加一點(diǎn)點(diǎn)距離讓它在每一幀與目標(biāo)位置分離弄跌。
基本上甲喝,相關(guān)的代碼會(huì)這樣(這是一個(gè)通用的公式,不要馬上添加到你的代碼中):
currentPosition += (finalPosition - currentPosition)*fraction;
更現(xiàn)實(shí)點(diǎn)來(lái)說(shuō)铛只,飛機(jī)旋轉(zhuǎn)也可以根據(jù)運(yùn)動(dòng)的方向埠胖。如果飛機(jī)很快的向上移動(dòng),它應(yīng)該很快地沿著逆時(shí)針?lè)较蛐D(zhuǎn)淳玩;如果飛機(jī)慢慢向下移動(dòng)直撤,它應(yīng)該慢慢地沿著順時(shí)針?lè)较蛐D(zhuǎn);為了準(zhǔn)確地實(shí)現(xiàn)它蜕着,我們應(yīng)該把旋轉(zhuǎn)比例值簡(jiǎn)單地分配給在目標(biāo)和飛機(jī)位置之間的剩余距離谋竖。
在我們的代碼里,updatePlane 函數(shù)需要像以下這樣:
function updatePlane(){
? var targetY = normalize(mousePos.y,-.75,.75,25, 175);
? var targetX = normalize(mousePos.x,-.75,.75,-100, 100);
? // 在每幀通過(guò)添加剩余距離的一小部分的值移動(dòng)飛機(jī)
? airplane.mesh.position.y += (targetY-airplane.mesh.position.y)*0.1;
? // 剩余的距離按比例轉(zhuǎn)動(dòng)飛機(jī)
? airplane.mesh.rotation.z = (targetY-airplane.mesh.position.y)*0.0128;
? airplane.mesh.rotation.x = (airplane.mesh.position.y-targetY)*0.0064;
? airplane.propeller.rotation.x += 0.3;
}
現(xiàn)在飛機(jī)的移動(dòng)看起來(lái)更加自然和真實(shí)承匣。通過(guò)修改一下小數(shù)值蓖乘,你可以使用飛機(jī)隨著鼠標(biāo)的移動(dòng)響應(yīng)速度更加快或更加慢。
看下我們場(chǎng)景中的最后一個(gè)階段:第二部分 Demo
很好H推<问恪!
接著要干嘛呢袍暴?
如果你看到這些侍,你已經(jīng)學(xué)會(huì) Three.js 中的通用的一些技術(shù)了隶症,能夠讓你創(chuàng)建您的第一個(gè)場(chǎng)景。現(xiàn)在你知道如何通過(guò)原始幾何體創(chuàng)建物體岗宣,如何激活它們蚂会,以及如何設(shè)置一個(gè)場(chǎng)景中的光源,你已經(jīng)知道如何改進(jìn)你的對(duì)象的外觀和運(yùn)動(dòng)耗式,還有如何調(diào)整環(huán)境氛圍胁住。
下一步已經(jīng)超出本文范圍了,由于它涉及到更多復(fù)雜的技術(shù)刊咳,它是實(shí)現(xiàn)一個(gè)游戲措嵌,大概思路是碰撞,收集點(diǎn)數(shù)芦缰,液位控制。下載源碼枫慷,看看實(shí)現(xiàn)的思路让蕾;你會(huì)看到到目前為止你學(xué)到過(guò)的概念和一些高階的知識(shí)點(diǎn),你可以研究一下和玩一下或听。請(qǐng)注意這游戲已經(jīng)優(yōu)化了以便桌面使用探孝。
但愿,這篇教程幫助你熟悉Three.js和激發(fā)你實(shí)現(xiàn)屬于你自己的項(xiàng)目誉裆。讓我看到你的創(chuàng)造力顿颅;我希望看到你做出什么來(lái)~
原文鏈接:https://zhuanlan.zhihu.com/p/21341483?from_voters_page=true&utm_id=0