个人博客地址: https://cxx001.gitee.io
前面我们介绍了各种可以用来覆盖对象的材质,也介绍了如何修改材质的颜色、关泽和不透明度,但是我们还没有详细介绍如何在材质中使用外部图片(也叫纹理).
将纹理应用于材质
1. 加载纹理并应用到网格
纹理最基础的用法是作为贴图被添加在材质上,当你使用这样的材质创建网格时,网格的颜色则来源于纹理。
注: 为了达到最好效果,图片最好使用2的次方的正方形。
- 纹理的放大和缩小:
由于纹理可以放大或缩小,所以纹理上的像素不会一对一映射到面的像素上。为此,Threejs提供了各种不同的选项,你可以指定magFilter或minFilter属性来设置纹理如何放大或缩小。
magFilter可选值:
名称 | 描述 |
---|---|
THREE.NearestFilter(最邻近过滤) | 会将纹理上最近的像素颜色应用于面上。在放大时,会导致方块化;在缩小时,会丢失很多细节 |
THREE.LinearFilter(线性过滤) | 最终颜色由周围四个像素值来决定。这样虽然在缩小时仍会丢失一些细节,但是在放大时会平滑很多,方块化也比较少出现(默认选择) |
minFilter属性是要使用mipmap,mipmap是把纹理按照2的倍数进行缩小,直到图形为1×1的大小,然后把这些图都存储起来,这些图片是在加载纹理时创建的,可以用于生成比较光滑的过滤效果。
过滤模式有下面这些:
名称 | 描述 |
---|---|
THREE.NearestMipMapNearestFilter | 选择最邻近的mip层,并执行上表中最邻近过滤。虽然放大时仍然会有方块化,但是缩小时效果会好很多 |
THREE.NearestMipMapLinearFilter | 选择最邻近的两个mip层,并分别在这两个mip层上运行最邻近过滤获取两个中间值,最后将这两个中间值传递到线性过滤器中获取最终值 |
THREE.LinearMipMapNearestFilter | 选择最邻近的mip层,并执行前表中的线性过滤 |
THREE.LinearMipMapLinearFilter | 选择最邻近的两个mip层,并分别在这两个mip层上运行线性过滤获取两个中间值,最后将这两个中间值传递到线性过滤器中获取最终值(默认选择) |
- 其它格式纹理加载器:
除了使用THREE.ImageUtils.loadTexture
方法加载标志格式的png,gif或jpeg图片,Threejs还提供了一些自定义加载器,以此来加载其它格式的纹理文件。
<!-- chapter-10-01.html -->
<!DOCTYPE html>
<html>
<head>
<title>Basic textures</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
// 二十面体
var polyhedron = createMesh(new THREE.IcosahedronGeometry(5, 0), "metal-rust.jpg");
polyhedron.position.x = 12;
scene.add(polyhedron);
// 球体
var sphere = createMesh(new THREE.SphereGeometry(5, 20, 20), "floor-wood.jpg");
scene.add(sphere);
// 立方体
var cube = createMesh(new THREE.BoxGeometry(5, 5, 5), "brick-wall.jpg");
cube.position.x = -12;
scene.add(cube);
console.log(cube.geometry.faceVertexUvs);
camera.position.x = 00;
camera.position.y = 12;
camera.position.z = 28;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x141414);
scene.add(ambiLight);
var light = new THREE.DirectionalLight();
light.position.set(0, 30, 20);
scene.add(light);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
// 使用外部纹理作为材质创建网格对象
function createMesh(geom, imageFile) {
// 如果加载其它格式纹理,使用上面介绍的对应加载器即可
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
var mesh = new THREE.Mesh(geom, mat);
return mesh;
}
var step = 0;
function render() {
stats.update();
polyhedron.rotation.y = step += 0.01;
polyhedron.rotation.x = step;
cube.rotation.y = step;
cube.rotation.x = step;
sphere.rotation.y = step;
sphere.rotation.x = step;
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
2. 使用凹凸贴图创建褶皱
凹凸贴图用于为材质添加厚度,网格面看起来有起伏、深度的感觉,更加立体。我们使用材质的bumpMap
属性和bumpScale
属性。
但是注意像素的密集程度定义的是凹凸的高度,但是凹凸图只包含像素的相对高度,没有任何倾斜的方向信息。所以使用凹凸贴图所能表达的深度信息有限,要想实现更多的细节可以使用法向贴图。
<!-- chapter-10-02.html -->
<!DOCTYPE html>
<html>
<head>
<title>Bump maps</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
// 右边没有使用凹凸贴图墙体
var sphere1 = createMesh(new THREE.BoxGeometry(15, 15, 2), "stone.jpg");
sphere1.rotation.y = -0.5;
sphere1.position.x = 12;
scene.add(sphere1);
// 左边使用凹凸贴图墙体
var sphere2 = createMesh(new THREE.BoxGeometry(15, 15, 2), "stone.jpg", "stone-bump.jpg");
sphere2.rotation.y = 0.5;
sphere2.position.x = -12;
scene.add(sphere2);
console.log(sphere2.geometry.faceVertexUvs);
// 地面
var floorTex = THREE.ImageUtils.loadTexture("../assets/textures/general/floor-wood.jpg");
var plane = new THREE.Mesh(new THREE.BoxGeometry(200, 100, 0.1, 30), new THREE.MeshPhongMaterial({
color: 0x3c3c3c,
map: floorTex
}));
plane.position.y = -7.5;
plane.rotation.x = -0.5 * Math.PI;
scene.add(plane);
camera.position.x = 00;
camera.position.y = 12;
camera.position.z = 28;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x242424);
scene.add(ambiLight);
var light = new THREE.SpotLight();
light.position.set(0, 30, 30);
light.intensity = 1.2;
scene.add(light);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var controls = new function () {
this.bumpScale = 0.2;
this.rotate = false;
this.changeTexture = function (e) {
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + e + ".jpg");
sphere2.material.map = texture;
sphere1.material.map = texture;
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + e + "-bump.jpg");
sphere2.material.bumpMap = bump;
};
this.updateBump = function (e) {
console.log(sphere2.material.bumpScale);
sphere2.material.bumpScale = e;
}
};
var gui = new dat.GUI();
gui.add(controls, "bumpScale", -2, 2).onChange(controls.updateBump);
gui.add(controls, "changeTexture", ['stone', 'weave']).onChange(controls.changeTexture);
gui.add(controls, "rotate");
function createMesh(geom, imageFile, bump) {
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
geom.computeVertexNormals();
var mat = new THREE.MeshPhongMaterial();
mat.map = texture;
// 加载使用凹凸贴图
if (bump) {
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + bump);
mat.bumpMap = bump; // 指定凹凸纹理
mat.bumpScale = 0.2; // 指定凹凸程度
console.log('d');
}
var mesh = new THREE.Mesh(geom, mat);
return mesh;
}
function render() {
stats.update();
if (controls.rotate) {
sphere1.rotation.y -= 0.01;
sphere2.rotation.y += 0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
3. 使用法向贴图创建更加细致的凹凸和褶皱
法线贴图保存的不是高度信息,而是法向量的方向。简单来讲,使用法向量贴图只需要使用很少的顶点和面就可以创建出细节很丰富的模型。
<!-- chapter-10-03.html -->
<!DOCTYPE html>
<html>
<head>
<title>Normal maps</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
// 右边普通纹理立方体
var sphere1 = createMesh(new THREE.BoxGeometry(15, 15, 15), "plaster.jpg");
sphere1.rotation.y = -0.5;
sphere1.position.x = 12;
scene.add(sphere1);
// 左边法向贴图立方体
var sphere2 = createMesh(new THREE.BoxGeometry(15, 15, 15), "plaster.jpg", "plaster-normal.jpg");
sphere2.rotation.y = 0.5;
sphere2.position.x = -12;
scene.add(sphere2);
console.log(sphere2.geometry.faceVertexUvs);
// 地面
var floorTex = THREE.ImageUtils.loadTexture("../assets/textures/general/floor-wood.jpg");
var plane = new THREE.Mesh(new THREE.BoxGeometry(200, 100, 0.1, 30), new THREE.MeshPhongMaterial({
color: 0x3c3c3c,
map: floorTex
}));
plane.position.y = -7.5;
plane.rotation.x = -0.5 * Math.PI;
scene.add(plane);
camera.position.x = 00;
camera.position.y = 12;
camera.position.z = 38;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x242424);
scene.add(ambiLight);
var light = new THREE.SpotLight();
light.position.set(0, 30, 30);
light.intensity = 1.2;
scene.add(light);
var pointColor = "#ff5808";
var directionalLight = new THREE.PointLight(pointColor);
scene.add(directionalLight);
// 光源标志小球
var sphereLight = new THREE.SphereGeometry(0.2);
var sphereLightMaterial = new THREE.MeshBasicMaterial({color: 0xac6c25});
var sphereLightMesh = new THREE.Mesh(sphereLight, sphereLightMaterial);
sphereLightMesh.castShadow = true;
sphereLightMesh.position = new THREE.Vector3(3, 3, 3);
scene.add(sphereLightMesh);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var controls = new function () {
this.normalScale = 1;
this.changeTexture = "plaster";
this.rotate = false;
this.changeTexture = function (e) {
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + e + ".jpg");
sphere2.material.map = texture;
sphere1.material.map = texture;
var bump = THREE.ImageUtils.loadTexture("../assets/textures/general/" + e + "-normal.jpg");
sphere2.material.normalMap = bump; // 指定法向贴图纹理
};
this.updateBump = function (e) {
sphere2.material.normalScale.set(e, e); // 指定法向贴图凹凸程度
}
};
var gui = new dat.GUI();
gui.add(controls, "normalScale", -2, 2).onChange(controls.updateBump);
gui.add(controls, "changeTexture", ['plaster', 'bathroom', 'metal-floor']).onChange(controls.changeTexture);
gui.add(controls, "rotate");
function createMesh(geom, imageFile, normal) {
if (normal) {
// 创建法向贴图网格
var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var m = THREE.ImageUtils.loadTexture("../assets/textures/general/" + normal);
var mat2 = new THREE.MeshPhongMaterial();
mat2.map = t; // 指定外部纹理
// 使用法向贴图最大问题是这个贴图的创建,需要使用如Blender或Photoshop这样的特殊工具。
// 这些工具可以讲高分辨率的效果图或纹理作为输入来创建法向贴图。
// Threejs同样提供了在运行期创建法向贴图的方法,THREE.ImageUtils对象的getNormalMap方法。
mat2.normalMap = m; // 指定法向贴图纹理
var mesh = new THREE.Mesh(geom, mat2);
return mesh;
} else {
// 创建普通纹理网格
var t = THREE.ImageUtils.loadTexture("../assets/textures/general/" + imageFile);
var mat1 = new THREE.MeshPhongMaterial({
map: t
});
var mesh = new THREE.Mesh(geom, mat1);
return mesh;
}
return mesh;
}
var invert = 1;
var phase = 0;
var step = 0;
function render() {
stats.update();
step += 0.1;
if (controls.rotate) {
sphere1.rotation.y -= 0.01;
sphere2.rotation.y += 0.01;
}
if (phase > 2 * Math.PI) {
invert = invert * -1;
phase -= 2 * Math.PI;
} else {
phase += 0.03;
}
sphereLightMesh.position.z = +(21 * (Math.sin(phase)));
sphereLightMesh.position.x = -14 + (14 * (Math.cos(phase)));
if (invert < 0) {
var pivot = 0;
sphereLightMesh.position.x = (invert * (sphereLightMesh.position.x - pivot)) + pivot;
}
// 平行光源随小球一起运动
directionalLight.position.copy(sphereLightMesh.position);
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
};
window.onload = init;
</script>
</body>
</html>
4. 使用光照贴图创建阴影效果
前面我们也介绍了可以受光照影响实时产生阴影的材质。本节来看看另一种产生阴影的方法–光照贴图。它是预先渲染好的阴影,可以用它来模拟真实的阴影,效率更高,主要用于静态场景的阴影。
<!-- chapter-10-04.html -->
<!DOCTYPE html>
<html>
<head>
<title>LightMap</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var renderer;
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
renderer = webGLRenderer;
// 地面,使用光照贴图
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(
{
color: 0x777777,
lightMap: lm, // 指定光照贴图
map: wood // 指定纹理
});
// 要让光照贴图显示出来还需要指定它的UV映射,详情请参考:
// http://stackoverflow.com/questions/15137695/three-js-lightmap-causes-an-error-webglrenderingcontext-gl-error-gl-invalid-op
// https://github.com/mrdoob/three.js/pull/2372
groundGeom.faceVertexUvs[1] = groundGeom.faceVertexUvs[0];
var groundMesh = new THREE.Mesh(groundGeom, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.position.y = 0;
scene.add(groundMesh);
// 创建两个立方体,并放到阴影正确位置
var cubeGeometry = new THREE.BoxGeometry(12, 12, 12);
var cubeGeometry2 = new THREE.BoxGeometry(6, 6, 6);
var meshMaterial = new THREE.MeshBasicMaterial();
meshMaterial.map = THREE.ImageUtils.loadTexture('../assets/textures/general/stone.jpg');
var cube = new THREE.Mesh(cubeGeometry, meshMaterial);
var cube2 = new THREE.Mesh(cubeGeometry2, meshMaterial);
cube.position.set(0.9, 6, -12);
cube2.position.set(-13.2, 3, -6);
scene.add(cube);
scene.add(cube2);
camera.position.x = -20;
camera.position.y = 20;
camera.position.z = 30;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);
document.getElementById("WebGL-output").appendChild(renderer.domElement);
function render() {
stats.update();
requestAnimationFrame(render);
renderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
5. 使用环境贴图创建反光效果
计算环境的反光效果对CPU耗费是非常巨大的,而且通常会使用光线追踪算法。本节我们用另一种方式实现反光效果,通过创建一个对象所处环境的纹理来伪装反光,并将它应用到指定的对象上。
<!-- chapter-10-05.html -->
<!DOCTYPE html>
<html>
<head>
<title>dynamic envmap</title>
<script src="../libs/three.js"></script>
<script src="../libs/dat.gui.js"></script>
<script src="../libs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<script>
var renderer;
var scene;
var camera, cubeCamera;
var control;
var orbit;
var sphere;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 10000);
orbit = new THREE.OrbitControls(camera);
renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x000000, 1.0);
renderer.setSize(window.innerWidth, window.innerHeight);
//1. 创建天空盒,你处于盒子中间,看到的是盒子里的纹理图形,就像你身处场景中一样。
// 创建盒子贴图纹理(6个面是场景的360全景图纹理)
var textureCube = createCubeMap();
textureCube.format = THREE.RGBFormat;
// 使用Shader材质
var shader = THREE.ShaderLib["cube"];
shader.uniforms["tCube"].value = textureCube;
var material = new THREE.ShaderMaterial({
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: shader.uniforms,
depthWrite: false,
side: THREE.DoubleSide
});
// 创建天空盒,使用上面的环境材质创建立方盒子
var skybox = new THREE.Mesh(new THREE.BoxGeometry(10000, 10000, 10000), material);
scene.add(skybox);
// 2. 将场景中的纹理反射到网格上(环境纹理获取两种方式:1.THREE.CubeCamera快照 2.直接使用上面创建的CubeMap纹理)
// 创建一个获取环境贴图的cubeCamera,可以为场景中的所有渲染的物体创建快照。
cubeCamera = new THREE.CubeCamera(0.1, 20000, 256);
scene.add(cubeCamera);
// 动态反射:将THREE.CubeCamera生成的环境贴图纹理指定到球体材质上
var sphereGeometry = new THREE.SphereGeometry(4, 15, 15);
var dynamicEnvMaterial = new THREE.MeshBasicMaterial({envMap: cubeCamera.renderTarget, side: THREE.DoubleSide}); // envMap环境贴图属性,通过cubeCamera的renderTarget对象获取生成的立方体纹理
sphere = new THREE.Mesh(sphereGeometry, dynamicEnvMaterial);
sphere.name = 'sphere';
scene.add(sphere);
// 静态反射:将加载全景图的6面生成的环境贴图纹理指定到圆锥材质上
var cylinderGeometry = new THREE.CylinderGeometry(2, 4, 10, 20, 20, false);
var envMaterial = new THREE.MeshBasicMaterial({envMap: textureCube, side: THREE.DoubleSide});
var cylinder = new THREE.Mesh(cylinderGeometry, envMaterial);
cylinder.name = 'cylinder';
scene.add(cylinder);
cylinder.position.set(10, 0, 0);
// 立方体使用同上材质
var boxGeometry = new THREE.BoxGeometry(5, 5, 5);
var cube = new THREE.Mesh(boxGeometry, envMaterial);
cube.name = 'cube';
scene.add(cube);
cube.position.set(-10, 0, 0);
camera.position.x = 0;
camera.position.y = 5;
camera.position.z = 33;
camera.lookAt(scene.position);
document.body.appendChild(renderer.domElement);
render();
}
// 创建环境贴图立方盒子
function createCubeMap() {
var path = "../assets/textures/cubemap/parliament/";
var format = '.jpg';
// 360全景图转换而来, 有在线工具可以转
var urls = [
path + 'posx' + format, path + 'negx' + format, // 左右面纹理
path + 'posy' + format, path + 'negy' + format, // 上下面纹理
path + 'posz' + format, path + 'negz' + format // 前后面纹理
];
// 创建CubeMap对象
var textureCube = THREE.ImageUtils.loadTextureCube(urls, new THREE.CubeReflectionMapping());
return textureCube;
/*
* threejs也可以直接使用全景图来创建CubeMap对象
* var textureCube = THREE.ImageUtils.loadTexture("360-degrees.png", new THREE.UVMapping());
*/
}
function render() {
orbit.update(); // 控制拖拽
sphere.visible = false;
cubeCamera.updateCubeMap(renderer, scene); // 当前场景快照
sphere.visible = true;
renderer.render(scene, camera);
scene.getObjectByName('cube').rotation.x += 0.005;
scene.getObjectByName('cube').rotation.y += 0.005;
scene.getObjectByName('cylinder').rotation.x += 0.005;
requestAnimationFrame(render);
}
window.onload = init;
</script>
<body>
</body>
</html>
6. 使用高光贴图创建闪亮效果
通过高光贴图,你可以为材质指定一个闪亮的、色彩明快的贴图。
下面示例:海洋区域反光、明亮;陆地不反光、暗淡。细节效果需要自调参数。
<!-- chapter-10-06.html -->
<!DOCTYPE html>
<html>
<head>
<title>Specular map</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
var sphere = createMesh(new THREE.SphereGeometry(10, 40, 40));
scene.add(sphere);
camera.position.x = 15;
camera.position.y = 15;
camera.position.z = 15;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = false;
var ambi = new THREE.AmbientLight(0x3300000);
scene.add(ambi);
var spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(350, 350, 150);
spotLight.intensity = 0.4;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
function createMesh(geom) {
var planetTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/Earth.png");
var specularTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/EarthSpec.png");
var normalTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/EarthNormal.png");
var planetMaterial = new THREE.MeshPhongMaterial();
planetMaterial.specularMap = specularTexture; // 高光贴图
planetMaterial.specular = new THREE.Color(0xff0000); // 通常与specularMap一起使用,决定反光的颜色
planetMaterial.normalMap = normalTexture; // 法向贴图
planetMaterial.map = planetTexture; // 使用的纹理
planetMaterial.shininess = 150; // 指定高光部分的亮度
var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [planetMaterial]);
return mesh;
}
var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
orbitControls.update(delta);
sphere.rotation.y += 0.005;
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
纹理的高级用途
1. 自定义UV映射
UV映射是指指定纹理的哪部分显示在物体的表面上。默认是将整个纹理显示到物体表面上,一般不需要修改,如果要自定义UV映射,指定部分纹理显示到物体表面,通常都是借助建模工具完成(Blender)。
原理是修改几何体对应面的每个顶点的faceVertexUvs
属性,这个属性有两个维度u/v
,对应faceVertexUvs
属性的x/y
,取值范围0~1。
如修改几何体第一个面的UV映射:
// 第一个面(由3个顶点组成)
geom.faceVertexUvs[0][0][0].x = 0.5; // 第一个面的第一个顶点的u值
geom.faceVertexUvs[0][0][0].y = 0.7; // 第一个面的第一个顶点的v值
geom.faceVertexUvs[0][0][1].x = 0.4; // 第一个面的第二个顶点...以此类推
geom.faceVertexUvs[0][0][1].y = 0.1;
geom.faceVertexUvs[0][0][2].x = 0.4;
geom.faceVertexUvs[0][0][2].y = 0.5;
很明显这样自定义设置很不直观,所以一般自定义UV映射我们一般通过专业工具完成,然后加载模型文件即可。
<!-- chapter-10-07.html -->
<!DOCTYPE html>
<html>
<head>
<title>UV mapping </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/OBJLoader.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xffffff, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 50;
camera.lookAt(new THREE.Vector3(0, 0, 0));
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var mesh;
var controls = new function () {
// 第一组自定义UV模型
this.loadCube1 = function () {
var loader = new THREE.OBJLoader();
// 加载自定义UV模型文件(修改的是面的顶点的uv值),决定纹理的哪些部分显示到表面上
loader.load('../assets/models/UVCube1.obj', function (geometry) {
if (mesh) scene.remove(mesh);
var material = new THREE.MeshBasicMaterial({color: 0xffffff});
var texture = THREE.ImageUtils.loadTexture("../assets/textures/ash_uvgrid01.jpg");
material.map = texture;
geometry.children[0].material = material;
mesh = geometry;
geometry.scale.set(15, 15, 15);
scene.add(geometry);
});
};
// 第二组自定义UV模型
this.loadCube2 = function () {
var loader = new THREE.OBJLoader();
loader.load('../assets/models/UVCube2.obj', function (geometry) {
if (mesh) scene.remove(mesh);
var material = new THREE.MeshBasicMaterial({color: 0xffffff});
var texture = THREE.ImageUtils.loadTexture("../assets/textures/ash_uvgrid01.jpg");
material.map = texture;
geometry.children[0].material = material;
mesh = geometry;
geometry.scale.set(15, 15, 15);
geometry.rotation.x = -0.3;
scene.add(geometry);
});
};
};
var gui = new dat.GUI();
gui.add(controls, 'loadCube1');
gui.add(controls, 'loadCube2');
controls.loadCube1();
function render() {
stats.update();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
loadCube1:
loadCube2:
2. 复制纹理
上面UV映射是指定纹理哪些部分显示到物体表面,Threejs会用选定的纹理包围整个物体表面。但是对于有些情形,你可能不想将纹理遍布整个面或整个几何体,而是让纹理进行重复(复制)。
通过material.map.wrapS
和 material.map.wrapT
属性来设置纹理的包裹类型(wrapS是x轴方向纹理的行为,wrapT是y轴方向纹理的行为):
纹理包裹类型 | 描述 |
---|---|
THREE.RepeatWrapping | 允许纹理重复自己 |
THREE.ClampToEdgeWrapping | 默认值,纹理不会重复,用选择的纹理尽量包裹物体整个表面,只是在边缘的像素用重复纹理来填充剩下的空间 |
<!-- chapter-10-08.html -->
<!DOCTYPE html>
<html>
<head>
<title>Repeat mapping</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = 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(6, 6, 6), "brick-wall.jpg");
cube.position.x = -7;
scene.add(cube);
console.log(cube.geometry.faceVertexUvs);
camera.position.x = 00;
camera.position.y = 12;
camera.position.z = 20;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x141414);
scene.add(ambiLight);
var light = new THREE.DirectionalLight();
light.position.set(0, 30, 20);
scene.add(light);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var controls = new function () {
this.repeatX = 1;
this.repeatY = 1;
this.repeatWrapping = true;
// 更新纹理重复方式
this.updateRepeat = function (e) {
// 设置重复纹理在x/y轴方向多久重复1次,值为1则不重复,大于1开始重复,小于1纹理被放大,负数则会产生一个纹理镜像
cube.material.map.repeat.set(controls.repeatX, controls.repeatY);
sphere.material.map.repeat.set(controls.repeatX, controls.repeatY);
if (controls.repeatWrapping) {
// 使用纹理重复
cube.material.map.wrapS = THREE.RepeatWrapping;
cube.material.map.wrapT = THREE.RepeatWrapping;
sphere.material.map.wrapS = THREE.RepeatWrapping;
sphere.material.map.wrapT = THREE.RepeatWrapping;
} else {
// 不使用纹理重复
cube.material.map.wrapS = THREE.ClampToEdgeWrapping;
cube.material.map.wrapT = THREE.ClampToEdgeWrapping;
sphere.material.map.wrapS = THREE.ClampToEdgeWrapping;
sphere.material.map.wrapT = THREE.ClampToEdgeWrapping;
}
cube.material.map.needsUpdate = true;
sphere.material.map.needsUpdate = true;
}
};
var gui = new dat.GUI();
gui.add(controls, "repeatX", -4, 4).onChange(controls.updateRepeat);
gui.add(controls, "repeatY", -4, 4).onChange(controls.updateRepeat);
gui.add(controls, "repeatWrapping").onChange(controls.updateRepeat);
function createMesh(geom, texture) {
var texture = THREE.ImageUtils.loadTexture("../assets/textures/general/" + texture);
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
geom.computeVertexNormals();
var mat = new THREE.MeshPhongMaterial();
mat.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.x = step;
sphere.rotation.y = step;
sphere.rotation.x = step;
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
3. 在画布上绘制图案并作为纹理
前面我们都是加载本地静态图片作为纹理,其实Threejs也支持将HTML5的画布作为纹理使用。
下面示例我们将用Literally库(http://literallycanvas.com) ,来创建一个交互式的画布。在画布上你可以进行绘图,它可以作为纹理实时显示到网格对象上。
<!-- chapter-10-09.html -->
<!DOCTYPE html>
<html>
<head>
<title>Canvas texture</title>
<link href="../libs/literally/css/literally.css" rel="stylesheet">
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/perlin.js"></script>
<script type="text/javascript" src="../libs/literally/jquery-1.8.2.js"></script>
<script type="text/javascript" src="../libs/literally/underscore-1.4.2.js"></script>
<script type="text/javascript" src="../libs/literally/js/literallycanvas.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.fs-container {
width: 300px;
height: 300px;
margin: auto;
bottom: 20px;
position: absolute;
x: 0;
y: 0;
"
}
#canvas-output {
width: 300px;
height: 300px;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<div class="fs-container">
<div id="canvas-output" style="float:left">
</div>
</div>
<div id="WebGL-output">
</div>
<script type="text/javascript">
var canvas = document.createElement("canvas");
document.getElementById('canvas-output').appendChild(canvas);
$('#canvas-output').literallycanvas({imageURLPrefix: '../libs/literally/img'}); // 使用Literally库的literallycanvas创建绘图工具
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xbbbbbb, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
var cube = createMesh(new THREE.BoxGeometry(10, 10, 10));
cube.position.x = 0;
scene.add(cube);
camera.position.x = 00;
camera.position.y = 12;
camera.position.z = 28;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x141414);
scene.add(ambiLight);
var light = new THREE.DirectionalLight();
light.position.set(0, 30, 20);
scene.add(light);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
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;
}
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);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>
4. 将视频输出作为纹理
Threejs也支持将HTML5的视频元素作为纹理输出。
<!-- chapter-10-10.html -->
<!DOCTYPE html>
<html>
<head>
<title>Video texture - non canvas</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="Stats-output">
</div>
<video id="video" style="display: none; position: absolute; left: 15px; top: 75px;"
src="../assets/movies/Big_Buck_Bunny_small.ogv" controls="true" autoplay="true"></video>
<div id="WebGL-output">
</div>
<script type="text/javascript">
var texture;
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;
var video = document.getElementById('video');
texture = new THREE.VideoTexture(video); // 将视频元素作为纹理输出
var cube = createMesh(new THREE.BoxGeometry(20, 10, 10));
cube.position.y = 2;
scene.add(cube);
camera.position.x = 00;
camera.position.y = 1;
camera.position.z = 28;
camera.lookAt(new THREE.Vector3(0, 0, 0));
var ambiLight = new THREE.AmbientLight(0x141414);
scene.add(ambiLight);
var light = new THREE.DirectionalLight();
light.position.set(0, 30, 20);
scene.add(light);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
var controls = new function () {
this.rotate = false;
};
var gui = new dat.GUI();
gui.add(controls, "rotate");
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: 0xff51ba}));
var faceMaterial = new THREE.MeshFaceMaterial(materialArray);
var mesh = new THREE.Mesh(geom, faceMaterial);
return mesh;
}
function render() {
stats.update();
if (controls.rotate) {
cube.rotation.x += -0.01;
cube.rotation.y += -0.01;
cube.rotation.z += -0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>