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