使用Three.js在瀏覽器中組合3D場景就像在玩樂高玩具同樣。咱們將一些盒子放在一塊兒,添加燈光,定義相機,而後Three.js渲染3D圖像。javascript
在本教程中,咱們將從盒子中組裝一輛簡約的汽車,並學習如何在其上繪製紋理。java
DEMOcanvas
Three.js是一個外部庫,所以首先咱們須要將其添加到咱們的項目中。我使用NPM將其安裝到個人項目中,而後將其導入JavaScript文件的開頭。瀏覽器
import * as THREE from "three"; const scene = new THREE.Scene(); . . .
首先,咱們須要定義場景。場景是一個容器,其中包含咱們要與燈光一塊兒顯示的全部3D對象。咱們將向該場景添加汽車,但首先讓咱們設置燈光,相機和渲染器。app
咱們將向場景添加兩個燈光:環境光和定向光。咱們經過設置顏色和強度來定義。dom
顏色定義爲十六進制值。在這種狀況下,咱們將其設置爲白色。強度是介於0和1之間的數字,而且當它們同時發光時,咱們但願這些值在0.5左右。ide
. . . const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(200, 500, 300); scene.add(directionalLight); . . .
環境光從各個方向發光,爲咱們的幾何圖形提供了基礎色,而定向光則模擬了太陽。函數
定向光從很遠的地方發出平行光線。咱們爲此光線設置一個位置,以定義這些光線的方向。學習
這個位置可能有點使人困惑,因此讓我解釋一下。在全部平行光線中,咱們特別定義一個。該特定光線將從咱們定義的位置(200,500,300)發光到0,0,0座標。其他的將與之平行。編碼
因爲光線是平行的,而且它們從很遠的地方發出光線,所以此處的精確座標可有可無,而是它們的比例可有可無。
三個位置參數分別是X,Y和Z座標。默認狀況下,Y軸指向上方,而且Y軸的值最高(500),這意味着咱們的汽車頂部受到的光照最多。所以它將是最明亮的。
其餘兩個值定義爲沿X和Z軸彎曲的光線量,也就是汽車的前部和側面將接收的光線量。
接下來,讓咱們設置定義咱們如何看待該場景的攝像機。
這裏有兩個選項–透視相機和正交相機。電子遊戲主要使用透視相機,但咱們將使用正交攝影機以使外觀看起來更簡潔。
在上一篇文章中,咱們更詳細地討論了這兩個相機之間的區別。所以,在本教程中,咱們將僅討論如何設置正交攝影機。
對於相機,咱們須要定義一個視錐。這是3D空間中要投影到屏幕上的區域。
對於正交攝影機,這是一個盒子。相機會將此盒子內的3D對象投射到其一側。因爲每條投影線都是平行的,所以正交攝影機不會扭曲幾何形狀。
. . . // Setting up camera const aspectRatio = window.innerWidth / window.innerHeight; const cameraWidth = 150; const cameraHeight = cameraWidth / aspectRatio; const camera = new THREE.OrthographicCamera( cameraWidth / -2, // left cameraWidth / 2, // right cameraHeight / 2, // top cameraHeight / -2, // bottom 0, // near plane 1000 // far plane ); camera.position.set(200, 200, 200); camera.lookAt(0, 10, 0); . . .
要設置正交攝影機,咱們必須定義從視點到視錐的每一邊有多遠。咱們定義左側爲左側75個單位,右側平面爲右側75個單位,依此類推。
在這裏,這些單位不表明屏幕像素。渲染圖像的大小將在渲染器中定義。在這裏,這些值具備咱們在3D空間中使用的任意單位。稍後,當在3D空間中定義3D對象時,咱們將使用相同的單位來設置其大小和位置。
定義攝像機後,咱們還須要將其定位並朝一個方向旋轉。咱們將相機在每一個維度上移動200個單位,而後將其設置爲向回0,10,0座標。這幾乎是起源。咱們朝着略微高於地面的位置看,這就是咱們汽車的中心所在的位置。
咱們須要設置的最後一塊是渲染器,能夠根據相機將場景渲染到瀏覽器中。咱們定義一個WebGLRenderer像這樣:
. . . // Set up renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.render(scene, camera); document.body.appendChild(renderer.domElement);
在這裏,咱們還設置了畫布的大小。這是咱們惟一設置像素大小的地方,由於咱們要設置它在瀏覽器中的顯示方式。若是要填充整個瀏覽器窗口,請傳遞窗口的大小。
最後,最後一行將渲染的圖像添加到咱們的HTML文檔中。它建立一個HTML Canvas元素以顯示渲染的圖像並將其添加到DOM。
如今,讓咱們看看如何組成汽車。首先,咱們將建立沒有紋理的汽車。這將是一個簡約的設計–咱們將只放四個盒子。
首先,咱們建立一對輪子。咱們將定義一個表明左右輪的灰色框。因爲咱們從未從下方看到汽車,所以咱們不會注意到,除了擁有一個單獨的左右輪外,咱們只有一個大箱子。
咱們將在汽車的前部和後部都須要一對輪子,所以咱們能夠建立可重用的功能。
. . . function createWheels() { const geometry = new THREE.BoxBufferGeometry(12, 12, 33); const material = new THREE.MeshLambertMaterial({ color: 0x333333 }); const wheel = new THREE.Mesh(geometry, material); return wheel; } . . .
咱們將輪子定義爲網格。網格是幾何圖形和材料的組合,它將表明咱們的3D對象。
幾何形狀定義了對象的形狀。在這種狀況下,咱們經過將其沿X,Y和Z軸的尺寸設置爲十二、12和33個單位來建立一個盒子。
而後,咱們傳遞一種將定義網格外觀的材質。有不一樣的材料選擇。它們之間的主要區別是它們對光的反應方式。
在本教程中,咱們將使用MeshLambertMaterial
。在MeshLambertMaterial
計算每一個頂點的顏色。在畫一個盒子的狀況下,基本上就是每一面。
咱們能夠看到它是如何工做的,由於盒子的每一側都有不一樣的陰影。咱們定義了一個定向光,使其主要從上方發出光,所以盒子的頂部是最亮的。
其餘一些材料不只能夠爲每面並且還能夠爲面內的每一個像素計算顏色。對於更復雜的形狀,它們能夠產生更逼真的圖像。可是對於使用定向光照明的盒子,它們並無太大的區別。
而後以相似的方式讓咱們建立汽車的其他部分。咱們定義了createCar
返回Group的函數。該組是場景中的另外一個容器。它能夠容納Three.js對象。這很方便,由於若是咱們要在汽車中四處走動,咱們只需在集團內四處走動。
. . . function createCar() { const car = new THREE.Group(); const backWheel = createWheels(); backWheel.position.y = 6; backWheel.position.x = -18; car.add(backWheel); const frontWheel = createWheels(); frontWheel.position.y = 6; frontWheel.position.x = 18; car.add(frontWheel); const main = new THREE.Mesh( new THREE.BoxBufferGeometry(60, 15, 30), new THREE.MeshLambertMaterial({ color: 0x78b14b }) ); main.position.y = 12; car.add(main); const cabin = new THREE.Mesh( new THREE.BoxBufferGeometry(33, 12, 24), new THREE.MeshLambertMaterial({ color: 0xffffff }) ); cabin.position.x = -6; cabin.position.y = 25.5; car.add(cabin); return car; } const car = createCar(); scene.add(car); renderer.render(scene, camera); . . .
咱們使用咱們的功能生成兩對車輪,而後定義汽車的主要部分。而後,咱們將機艙的頂部添加爲第四網格。這些都是具備不一樣尺寸和顏色的盒子。
默認狀況下,每一個幾何都將在中間,而且它們的中心將在0,0,0座標處。
首先,咱們經過調整它們沿Y軸的位置來升高它們。咱們將輪子的高度提升一半-所以,輪子不會沉入地面,而是躺在地面上。而後,咱們還沿着X軸調整片斷以達到其最終位置。
咱們將這些片斷添加到汽車組中,而後將整個組添加到場景中。在渲染圖像以前將汽車添加到場景中很重要,不然修改場景後,咱們將須要再次調用渲染。
如今咱們有了基本的汽車模型,讓咱們爲機艙添加一些紋理。咱們要給窗戶塗油漆。咱們將爲側面定義紋理,爲機艙的正面和背面定義一個紋理。
當咱們使用材料設置網格的外觀時,設置顏色不是惟一的選擇。咱們還能夠映射紋理。咱們能夠爲每一側提供相同的紋理,也能夠爲陣列中的每一側提供一種材質。
做爲紋理,咱們可使用圖像。可是相反,咱們將使用JavaScript建立紋理。咱們將使用HTML Canvas和JavaScript對圖像進行編碼。
在繼續以前,咱們須要區分Three.js和HTML Canvas。
Three.js是一個JavaScript庫。它使用引擎蓋下的WebGL將3D對象渲染爲圖像,並將最終結果顯示在canvas元素中。
另外一方面,HTML Canvas是HTML元素,就像div
元素或段落標籤同樣。可是,讓它不同凡響的是,咱們可使用JavaScript在此元素上繪製形狀。
這就是Three.js在瀏覽器中渲染場景的方式,這就是咱們要建立紋理的方式。讓咱們看看它們是如何工做的。
要在畫布上繪製,首先咱們須要建立一個canvas元素。當咱們建立一個HTML元素時,該元素將永遠不會成爲咱們HTML結構的一部分。它自己不會顯示在頁面上。相反,咱們將其轉換爲Three.js紋理。
讓咱們看看如何在此畫布上繪製。首先,咱們定義畫布的寬度和高度。這裏的大小並無定義畫布將顯示多大,它更像是畫布的分辨率。不管大小如何,紋理都會拉伸到框的側面。
function getCarFrontTexture() { const canvas = document.createElement("canvas"); canvas.width = 64; canvas.height = 32; const context = canvas.getContext("2d"); context.fillStyle = "#ffffff"; context.fillRect(0, 0, 64, 32); context.fillStyle = "#666666"; context.fillRect(8, 8, 48, 24); return new THREE.CanvasTexture(canvas); }
而後,咱們得到2D繪圖上下文。咱們可使用此上下文執行繪圖命令。
首先,咱們將用白色矩形填充整個畫布。爲此,首先咱們將填充樣式設置爲while。而後經過設置矩形的左上角位置和大小來填充矩形。在畫布上繪製時,默認狀況下0,0座標將位於左上角。
而後,咱們用灰色填充另外一個矩形。這是從8,8座標開始的,它不填充畫布,僅繪製窗口。
就是這樣–最後一行將canvas元素轉換爲紋理並返回它,所以咱們能夠將其用於咱們的汽車。
function getCarSideTexture() { const canvas = document.createElement("canvas"); canvas.width = 128; canvas.height = 32; const context = canvas.getContext("2d"); context.fillStyle = "#ffffff"; context.fillRect(0, 0, 128, 32); context.fillStyle = "#666666"; context.fillRect(10, 8, 38, 24); context.fillRect(58, 8, 60, 24); return new THREE.CanvasTexture(canvas); }
以相似的方式,咱們能夠定義側面紋理。咱們再次建立一個canvas元素,獲取其上下文,而後首先將整個畫布填充爲基色,而後將窗口繪製爲矩形。
如今,讓咱們看看如何在汽車上使用這些紋理。當咱們爲機艙頂部定義網格時,咱們不僅設置一種材質,而是爲每一側設置一種材質。咱們定義了六種材料的陣列。咱們將紋理映射到機艙的側面,而頂部和底部仍將具備純色。
. . . function createCar() { const car = new THREE.Group(); const backWheel = createWheels(); backWheel.position.y = 6; backWheel.position.x = -18; car.add(backWheel); const frontWheel = createWheels(); frontWheel.position.y = 6; frontWheel.position.x = 18; car.add(frontWheel); const main = new THREE.Mesh( new THREE.BoxBufferGeometry(60, 15, 30), new THREE.MeshLambertMaterial({ color: 0xa52523 }) ); main.position.y = 12; car.add(main); const carFrontTexture = getCarFrontTexture(); const carBackTexture = getCarFrontTexture(); const carRightSideTexture = getCarSideTexture(); const carLeftSideTexture = getCarSideTexture(); carLeftSideTexture.center = new THREE.Vector2(0.5, 0.5); carLeftSideTexture.rotation = Math.PI; carLeftSideTexture.flipY = false; const cabin = new THREE.Mesh(new THREE.BoxBufferGeometry(33, 12, 24), [ new THREE.MeshLambertMaterial({ map: carFrontTexture }), new THREE.MeshLambertMaterial({ map: carBackTexture }), new THREE.MeshLambertMaterial({ color: 0xffffff }), // top new THREE.MeshLambertMaterial({ color: 0xffffff }), // bottom new THREE.MeshLambertMaterial({ map: carRightSideTexture }), new THREE.MeshLambertMaterial({ map: carLeftSideTexture }), ]); cabin.position.x = -6; cabin.position.y = 25.5; car.add(cabin); return car; } . . .
這些紋理中的大多數將正確映射,無需進行任何調整。可是,若是咱們將汽車轉過身,那麼咱們能夠看到窗戶以錯誤的順序出如今左側。
固定紋理先後的左右兩側
這是預期的,由於咱們在此處也將紋理用於右側。咱們能夠爲左側定義一個單獨的紋理,也能夠鏡像右側。
不幸的是,咱們不能水平翻轉紋理。咱們只能垂直翻轉紋理。咱們能夠經過3個步驟來解決此問題。
首先,咱們將紋理旋轉180度,這等於弧度的PI。可是,在旋轉它以前,咱們必須確保紋理圍繞其中心旋轉。這不是默認值–咱們必須將旋轉中心設置爲一半。咱們在兩個軸上都設置了0.5,這基本上意味着50%。最後,咱們將紋理上下顛倒以使其處於正確的位置。
那麼咱們在這裏作了什麼?咱們建立了一個包含咱們的汽車和燈光的場景。咱們用簡單的盒子建造了汽車。
您可能認爲這太基礎了,可是若是您考慮一下,其實是使用盒子建立了許多外觀時尚的手機遊戲。或者只是考慮一下Minecraft,看看將盒子放在一塊兒能走多遠。
而後,咱們使用HTML畫布建立紋理。HTML canvas的功能遠遠超過咱們在此使用的功能。咱們能夠用曲線和弧線繪製不一樣的形狀,可是有時咱們只須要一個最小的設計便可。
最後,咱們定義了一個相機來創建咱們如何看待該場景,以及一個渲染器,將最終圖像渲染到瀏覽器中。
若是您想使用該代碼,能夠在CodePen上找到源代碼。