加载和使用纹理
加载和使用纹理需要了解以下几个方面:在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就是我们上面创建的视频纹理。