使用 Three.js 的 3D 制作動(dòng)畫場(chǎng)景

推薦:將?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)了什么功能:

第一部分的 Demo

幾乎完成右莱!

正如你所看見(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)~

DEMO源碼下載

原文鏈接:https://zhuanlan.zhihu.com/p/21341483?from_voters_page=true&utm_id=0

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市足丢,隨后出現(xiàn)的幾起案子粱腻,更是在濱河造成了極大的恐慌,老刑警劉巖斩跌,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绍些,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡耀鸦,警方通過(guò)查閱死者的電腦和手機(jī)柬批,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)袖订,“玉大人氮帐,你說(shuō)我怎么就攤上這事÷骞茫” “怎么了上沐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)吏口。 經(jīng)常有香客問(wèn)我奄容,道長(zhǎng)冰更,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任昂勒,我火速辦了婚禮蜀细,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘戈盈。我一直安慰自己奠衔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布塘娶。 她就那樣靜靜地躺著归斤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刁岸。 梳的紋絲不亂的頭發(fā)上脏里,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音虹曙,去河邊找鬼迫横。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酝碳,可吹牛的內(nèi)容都是我干的矾踱。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼疏哗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼呛讲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起返奉,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤贝搁,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后芽偏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體徘公,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年哮针,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了关面。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡十厢,死狀恐怖等太,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蛮放,我是刑警寧澤缩抡,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站包颁,受9級(jí)特大地震影響瞻想,放射性物質(zhì)發(fā)生泄漏压真。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一蘑险、第九天 我趴在偏房一處隱蔽的房頂上張望滴肿。 院中可真熱鬧,春花似錦佃迄、人聲如沸泼差。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)堆缘。三九已至,卻和暖如春普碎,著一層夾襖步出監(jiān)牢的瞬間吼肥,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工麻车, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留潜沦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓绪氛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親涝影。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枣察,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容