加载和使用纹理

    加载和使用纹理需要了解以下几个方面:在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.使用光照贴图创建假阴影

    光照贴图是预先渲染好的阴影,你可以用它来模拟真实的阴影。光照阴影其实是事先准备好的阴影图片。例如:

image

    你可以用这种技术创建出解析度很高的阴影,而且不会损害渲染的性能。当时只能使用在静态场景。光照贴图的使用跟其他纹理基本一样,只有几处小小的不同:

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面盒子环境。我想要中间的球实现动态的环境反射,旋转场景,球中可以看到左右两个网格的投影。

image

    实现代码如下,代码创建了一个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)创建一个交互时画布,你可以再上面绘图。界面如下:

image

    首先我们创建一个画布元素,然后配置该画布使用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)可以产生看上去非常自然的随机纹理,如下图所示:

    image

    我们可以使用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就是我们上面创建的视频纹理。

posted @ 2017-04-28 01:42  heavi  阅读(3321)  评论(1编辑  收藏  举报