加載和使用紋理需要了解以下幾個方面:在Three.js里加載紋理並應用到網格上;使用凹凸貼圖和法線貼圖為網格添加深度和細節;使用光照貼圖創建假陰影;使用環境貼圖在材質上添加反光細節;使用光亮貼圖,讓網格的某些部分變得“閃亮”;通過修改網格的UV貼圖,對貼圖進行微調;將HTML5畫布和視頻元素作為紋理輸入。本章節將會從以上幾方面來了解紋理的使用。
1.使用凹凸貼圖創建皺紋
之前我們學習了THREE.MeshPhongMaterial對象的map屬性,知道它用來設置外部資源作為材質的紋理。這里再介紹它的bumpMap屬性,用來實現凹凸貼圖效果。代碼和創建不同紋理一樣,僅僅多個bumpMap屬性的設置。代碼如下:
function createMesh(geom, imageFile, bump){ var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile); var material = new THREE.MeshPhongMaterial({ map: texture }); if(bump){ var bumpTex = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump); material.bumpMap = bumpTex; } var mesh = new THREE.Mesh(geom, material); return mesh; }
createMesh函數用來創建包含外部資源作為紋理的網格,第三個參數bump就是我們的凹凸貼圖的圖片名稱,如果該名稱不為空,則加載凹凸貼圖並設置到bumpMap屬性。
2.使用法向量貼圖創建更加細致的凹凸和皺紋
和使用凹凸貼圖非常相似,區別在於法向量設置的是材質的normalMap屬性,而凹凸貼圖設置的是bumpMap屬性。使用法向貼圖的問題時不容易創建。你要使用特殊的工具,例如Blender和Photoshop。它們可以將高度解析的渲染結果或圖片作為輸入,從中創建出法向的貼圖。
3.使用光照貼圖創建假陰影
光照貼圖是預先渲染好的陰影,你可以用它來模擬真實的陰影。光照陰影其實是事先准備好的陰影圖片。例如:
你可以用這種技術創建出解析度很高的陰影,而且不會損害渲染的性能。當時只能使用在靜態場景。光照貼圖的使用跟其他紋理基本一樣,只有幾處小小的不同:
var groundGeom = new THREE.PlaneGeometry(95, 95, 1, 1); var lm = THREE.ImageUtils.loadTexture("../assets/textures/lightmap/lm-1.png"); var wood = THREE.ImageUtils.loadTexture("../assets/textures/general/floor-wood.jpg"); var groundMaterial = new THREE.MeshBasicMaterial({ map: wood, color: 0x777777, lightMap: lm, }); groundGeom.faceVertexUvs[1] = groundGeom.faceVertexUvs[0];
應用貼圖時,只要將材質的lightMap屬性設置成剛才所示的紋理即可。但是要講光照貼圖顯示出來,我們需要為光照貼圖明確指定UV映射(將紋理的那一部分應用到表面)。只有這樣才能將光照貼圖與其他紋理獨立開來。設置代碼如下:
groundGeom.faceVertexUvs[1] = groundGeom.faceVertexUvs[0];
下面的地址詳細解釋了為什么需要明確指定UV映射:
http://stackoverflow.com/questions/15137695/three-js-lightmap-causes-an-error-webglrenderingcontext-gl-error-gl-invalid-op
4.用環境貼圖創建虛假的反光效果
計算環境反射光非常耗費CPU,而且通常會使用光線追蹤算法。如果你想在Three.js里邊使用反光,你可以做,但是你不得不做一個假的。要創建一個這樣的場景,需要執行以下步驟:
1)創建一個CubeMap對象:我們首先需要創建一個CubeMap對象。一個CubeMap是有6個紋理的集合,而這些紋理可以應用到方塊的每個面上。
2)創建一個帶有這個CubeMap對象的方塊:帶有CubeMap對象的方塊就是移動相機時你所看到的環境。你可以在你想四周看時制造一種身臨其境的感覺。
3)將CubeMap作為紋理:我們用來模擬環境的CubeMap對象也可以用來做網格的紋理。Three.js會讓它看上去像是環境的反光。
創建CubeMap對象,需要六張用來構建整個場景的額圖片。圖片分別是朝前的(posz)、朝后的(negz)、朝上的(posy)、朝下的(negy)、朝右的(posx)、朝左的(negx)。圖片有了,你就可以像相面這樣加載它們:
function createCubeMap(){ var path = "../assets/textures/cubemap/parliament/"; var format = ".jpg"; var urls = [ path + "posx" + format, path + "negx" + format, path + "posy" + format, path + "negy" + format, path + "posz" + format, path + "negz" + format ]; var textureCube = THREE.ImageUtils.loadTextureCube(urls, new THREE.CubeReflectionMapping()); return textureCube; }
這里我們用到了Three.ImageUtils的loadTextureCube函數,創建一個方塊紋理textureCube。接下來我們需要創建一個方塊作為我們的所看到的環境(看到的是方塊的內部):
var textureCube = createCubeMap(); var shader = THREE.ShaderLib["cube"]; shader.uniforms["tCube"].value = textureCube; var material = new THREE.ShaderMaterial({ vertexShader: shader.vertexShader, fragmentShader: shader.fragmentShader, uniforms: shader.uniforms, depthWrite: false, side: THREE.BackSide }); var cubeMesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100), material); sceneCube.add(cubeMesh);
Three.js提供了一個特別的着色器(Three.ShaderLib[“cube”]),結合THREE.ShaderMaterial類,我們可以基於CubeMap對象創建一個環境。我們用CubeMap配置這個着色器。
同一個CubeMap對象可以應用到某個網格上,用來創建虛假的放光:
var sphere1 = createMesh(new THREE.SphereGeometry(10, 15, 15), "plaster.jpg"); sphere1.material.envMap = textureCube; sphere1.rotation.y = -0.5; sphere1.position.y = 5; sphere1.position.x = 12; scene.add(sphere1); var sphere2 = createMesh(new THREE.BoxGeometry(10, 15, 15), "plaster.jpg", "plaster-normal.jpg"); sphere2.material.envMap = textureCube; sphere2.rotation.y = 0.5; sphere2.position.x = -12; sphere2.position.y = 5; scene.add(sphere2);
我們將材質頂點evnMap屬性設置為我們創建的cubeMap對象,結果看上去好像我們站在一個寬闊的室外環境中,而且這些網格上回映射環境。
5.使用CubeCamera模擬反光
CubeCamera一般都結合包含有CubeMap的虛假環境使用。用來作為某個物體的反光使用。例如下圖是一個用6個面CubeMap作為紋理的6面盒子環境。我想要中間的球實現動態的環境反射,旋轉場景,球中可以看到左右兩個網格的投影。
實現代碼如下,代碼創建了一個CubeCamera對象,模型position是(0, 0, 0)。后面再創建sphere的時候我們使用的紋理時dynamicEnvMaterial材質,該材質的envMap是從cubeCamera.renderTaget取紋理。cubeCamera的renderTarget實際就是這個攝像頭向四周看到的環境。直接用到sphere上,感覺就像是sphere反光的效果。
cubeCamera = new THREE.CubeCamera(0.1, 20000, 256); scene.add(cubeCamera); var sphereGeometry = new THREE.SphereGeometry(4, 15, 15); var boxGeometry = new THREE.BoxGeometry(5, 5, 5); var cylinderGeometry = new THREE.CylinderGeometry(2, 4, 10 ,20, 20, false); var dynamicEvnMaterial = new THREE.MeshBasicMaterial({ envMap: cubeCamera.renderTarget, side: THREE.DoubleSide }); var envMaterial = new THREE.MeshBasicMaterial({ envMap: textureCube, side: THREE.DoubleSide }); sphere = new THREE.Mesh(sphereGeometry, dynamicEvnMaterial); sphere.name = "sphere"; scene.add(sphere); var cylinder = new THREE.Mesh(cylinderGeometry, envMaterial); cylinder.name = "cylinder"; cylinder.position.set(10, 0, 0); scene.add(cylinder);
每次渲染 的時候我們還得去調用CubeCamera的updateCubeMap函數更新渲染。但在渲染時記得把球隱藏掉,不然就看不到反射了。
function render(){ orbit.update(); sphere.visible = false; cubeCamera.updateCubeMap(renderer, scene); sphere.visible = true; renderer.render(scene, camera); scene.getObjectByName("cube").rotation.x += control.rotationSpeed; scene.getObjectByName("cube").rotation.y += control.rotationSpeed; scene.getObjectByName("cylinder").rotation.x += control.rotationSpeed; requestAnimationFrame(render); }
7.定制UV映射
通過UV映射你可以指定文理的哪部分顯示在物體表面上。多數情況下,你不必修改默認的UV映射。UV映射的定制一般是在諸如Blender這樣的軟件中完成的,特別是當模型變得復雜時。這里需要記住的是UV映射有兩個維度,U和V,取值范圍是0到1.定制UV映射時,你需要為物體的每個面指定其需要顯示文理的哪個部分。為此你要為構成面的每個頂點指定u和v坐標。下面是一段加載文理的代碼:
this.loadCube1 = function(){ var loader = new THREE.OBJLoader(); loader.load("../assets/models/UVCube1.obj", function(object){ if(mesh) scene.remove(mesh); var material = new THREE.MeshBasicMaterial({ color: 0xffffff }); material.map = THREE.ImageUtils.loadTexture("../assets/textures/ash_uvgrid01.jpg"); object.children[0].material = material; mesh = object; object.scale.set(15, 15, 15); scene.add(mesh); }); }
8.重復映射
當你在Three.js幾何體上創建文理的時候,Three.js會盡量做到最優。例如,對於方塊,Three.js會在每個面上顯示完整的文理。但有些情況,你可能不想講文理遍布整個面或整個幾何體,而是讓文理自己重復。Three.js提供了一些功能可以實現這種控制。
在用這個屬性達到所需的效果之前,你需要保證將文理的包裹屬性設置為THREE.RepeatWrapping。例如:
cube.material.map.wrapS = THREE.RepeatWrapping;
cube.material.map.wrapT = THREE.RepeatWrapping;
wrapS定義了文理沿x軸方向的行為,而wrapT定義文理沿y軸方向的行為。Three.js提供了如下兩個選項:
TTREE.RepeatWrapping 允許文理重復自己
THREE.ClampToEdgeWrapping是默認設置。如果是THREE.ClampToEdgeWrapping,那么文理邊緣像素會被拉伸,以填滿剩下的空間。
如果使用THREE.RepeatWraping,我們可以用下面的代碼來設置repeat屬性:
cube.material.map.repeat.set(controls.repeatX, controls.repeatY);
sphere.material.map.repeat.set(controls.repeatX, controls.repeatY);
controls.repeatX變量指定文理在x軸方向多久重復一次,而變量controls.repeatY指定文理在y軸方向多久重復一次。如果設置為1,則文理不會重復;如果設置成大一點的值,你就會看到文理開始重復。你也可以將值設置成小於1.如果是這樣,你就會看到紋理被放大了。如果將這個值設置成負數,那么會產生一個文理的鏡像。
當你修改repeat屬性,Three.js會自動更新文理,並用新的設置進行渲染。但如果你把Three.RepeatWrapping改成THREE.ClampToEdgeWrapping,你要明確更新紋理:
cube.material.map.needsUpdate = true;
下面是一個使用紋理重復的示例代碼:
var sphere = createMesh(new THREE.SphereGeometry(5, 20, 20), "floor-wood.jpg"); scene.add(sphere); sphere.position.x = 7; var cube = createMesh(new THREE.BoxGeometry(5, 5, 5), "brick-wall.jpg"); cube.position.x = -7; scene.add(cube); var ambientLight = new THREE.AmbientLight(0x141414); scene.add(ambientLight); var light = new THREE.DirectionalLight(); light.position.set(0, 30, 20); scene.add(light); render(); function createMesh(geom, textureName){ var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + textureName); texture.wrapS = THREE.RepeatWrapping; texture.wrapS = THREE.RepeatWrapping; geom.computeVertexNormals(); var mat = new THREE.MeshPhongMaterial({map: texture}); var mesh = new THREE.Mesh(geom, mat); return mesh; } var step = 0; function render(){ stats.update(); step += 0.01; cube.rotation.y = step; cube.rotation.z = step; sphere.rotation.y = step; sphere.rotation.z = step; requestAnimationFrame(render); webGLRenderer.render(scene, camera); }
9.用畫布作為紋理
在介紹如何使用之前,先介紹個畫布工具,我們這里使用literally庫(http://literallycanvas.com)創建一個交互時畫布,你可以再上面繪圖。界面如下:
首先我們創建一個畫布元素,然后配置該畫布使用literally庫:
<div class="fs-container"> <div id="canvas-output" style="float:left"> </div> </div> ... var canvas = document.createElement("canvas"); document.getElementById("canvas-output").appendChild(canvas); $("#canvas-output").literallycanvas({imageURLPrefix: "../libs/literally/img"});
我們使用Javascript創建了一個canvas畫布,並將它添加到指定的div元素中。通過調用literallycanvas我們可以創建一個繪圖工具。接下來我們要將畫布上的繪制結果作為輸入創建一個紋理:
function createMesh(geom){ var canvasMap = new THREE.Texture(canvas); var mat = new THREE.MeshPhongMaterial(); mat.map = canvasMap; var mesh = new THREE.Mesh(geom, mat); return mesh; }
代碼唯一要做的就是在創建紋理時把canvas對象傳遞給紋理構造器。浙江就可以把畫布作為紋理來源。剩下要做的就是在渲染時更新材質,這樣畫布上最新的內容才會顯示在方塊上:
function render(){ stats.update(); cube.rotation.y += 0.01; cube.rotation.x += 0.01; cube.material.map.needsUpdate = true; requestAnimationFrame(render); webGLRenderer.render(scene, camera); }
10.用畫布作凹凸貼圖
我們可以使用凹凸貼圖創建簡單的有皺紋的紋理。貼圖像素的密集程度越高,貼圖看上去越皺。我們也可以使用畫布上的畫圖作為貼圖。我們可以在畫布上隨機生成一副灰度圖,並將該圖作為方塊上的凹凸貼圖的輸入。
這里介紹一個用一些隨機噪音填充畫布的庫,叫做Perlin噪音。Perlin噪音(http://en.wikipedia.org/wiki/Perlin_noise)可以產生看上去非常自然的隨機紋理,如下圖所示:
我們可以使用http://github.com/wwwtyro/perlin.js中的Perlin噪音函數如下所示:
function fillWidthPerlin(pn, ctx){ for(var x = 0; x < 512; x++){ for(var y = 0; y < 512; y++){ var base = new THREE.Color(0xffffff); var value = pn.noise(x/10, y/10, 0); base.multiplyScalar(value); ctx.fillStyle = "#" + base.getHexString(); ctx.fillRect(x, y, 1, 1); } } }
我們使用perlin.noise函數在畫布x坐標和y坐標的基礎上生成一個0到1之間的值。該值可以從來在畫布上畫一個像素點。可以用這個方法生成所有的像素點其結果如上圖所示。生成后直接使用這個canvas即可:
function createMesh(geom){ var bumpMap = new THREE.Texture(canvas); geom.computeVertexNormals(); var mat = new THREE.MeshPhongMaterial(); mat.color = new THREE.Color(0x77ff77); mat.bumpMap = bumpMap; bumpMap.needsUpdate = true; var mesh = new THREE.Mesh(geom, mat); return mesh; }
10.使用視頻輸出作為紋理
Three.js直接致辭HTML5視頻元素作為紋理。直接使用THREE.VideoTexture(videoElement)即可。如下面的代碼使用了一個video元素直接作為紋理輸出:
var video = document.getElementById("video"); texture = new THREE.VideoTexture(video);
由於視頻不是正方形,所喲要保證材質不會生成mipmap。由於材質變化的很頻繁,所以我們還需要設置簡單高效的過濾器。
texture.minFilter = THREE.LinearFilter; texture.magFilter = THREE.LinearFilter; texture.format = THREE.RGBFormat; texture.generateMipmaps = false;
接下來可以直接使用這個紋理作為材質的map:
function createMesh(geom){ var materialArray = []; materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); materialArray.push(new THREE.MeshBasicMaterial({map: texture})); materialArray.push(new THREE.MeshBasicMaterial({color: 0x0051ba})); var faceMaterial = new THREE.MeshFaceMaterial(materialArray); var mesh = new THREE.Mesh(geom, faceMaterial); return mesh; }
代碼創建了六個材質的數組,作為THREE.MeshFaceMaterial對象的構造產生,假如我們使用的是BoxGeometry,那么剛好對應六個面。第五個面的材質是:new THREE.MeshBasicMaterial({map: texture})。texture就是我們上面創建的視頻紋理。