目的
- 从图片到纹理
- 多重纹理
- 用canvas作为纹理
- 纹理的更新
纹理
纹理对于我们多么重要。就如我们人类有皮肤一样,物体也有他们的 “皮肤” – 纹理。纹理之于 3D 世界,就如同皮肤于动物世界一样。3D 世界的物体有了纹理,才能那么的美丽。
1、纹理的组成
3D 世界的纹理是由图片组成的,或者说是贴图。将纹理以一定的规则映射到几何体上(一般是三角形上),那么这个几何体就有纹理皮肤了。
纹理是怎么实现的呢?之前学习过 OpenGL 的时候,纹理是通过着色器(shader)完成图片对物体的映射任务的。
在 Threejs 中, 首先有一个纹理类,调用加载图片的方法,将这张图片和这个纹理类捆绑起来,然后再进行下一步操作的。
Threejs 的纹理类由 THREE.Texture 表示,其构造函数如下:
1 |
THREE.Texture = function( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ); |
参数非常多:
- image:这是一个图片类型,基本上它是由 ImageUtils 来加载的
123// url 是一个网页地址 http://xxxx/a.jpg ,javascript 没有从本地加载数据的能力// 所以没办法从 C 盘加载数据var image = THREE.ImageUtils.loadTexture( url ); - mapping:是一个 THREE.UVMapping() 类型,它表示的是纹理坐标
- wrapS:表示x轴的纹理回环方式,就是当纹理的宽度小于需要贴图的平面的宽度的时候,平面剩下的部分应该 P 以何种方式的贴图的问题
- wrapT:表示y轴的纹理回环方式,就是当纹理的高度小于需要贴图的平面的高度的时候,平面剩下的部分应该 P 以何种方式的贴图的问题
- magFilter & minFilter:表示过滤的方式,这是 OpenGL 的基本概念
- format:表示加载的图片的格式,这个参数可以取值 THREE.RGBAFormat,RGBFormat 等
- type:表示存储纹理的内存的每一个字节的格式,有符号还是没有符号?整形还是浮点型?默认是无符号型(THREE.UnsignedByteType)
- anisotropy:各向异性过滤。使用各向异性过滤能够使纹理效果更好,但是会消耗更多的资源(CPU、GPU、Memory…)
2、纹理的坐标
之前学习 OpenGL 曾接触过,一张图片作为纹理图片的时候,正常情况下,用 [0,1] 的区间来表示宽高的比例,而不是具体用像素值来标定。
当用一幅图来做纹理的时候,那么这幅图就已经约定俗成的被隐示的被赋予了上图的 纹理坐标 ,这个纹理坐标将被对应到一个形状上。
3、实例
效果:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>texture</title> <style type="text/css"> body { margin: 0px; background-color: #000000; overflow: hidden; } </style> <script src="js/three.min.js"></script> </head> <body> <script> var renderer, camera, scene, light; var geometry; var WIDTH = window.innerWidth, HEIGHT = window.innerHeight; init(); animate(); function init() { // Init Renderer renderer = new THREE.WebGLRenderer(); document.body.appendChild( renderer.domElement ); // Init Camera camera = new THREE.PerspectiveCamera( 70, WIDTH / HEIGHT, 1, 1000 ); camera.position.z = 400; // Init Scene scene = new THREE.Scene(); // scene.backgorund = new THREE.Color( 0x111111 ); // Init Light light = new THREE.AmbientLight( 0xFFFFFF ); light.position.set( 100, 100, 200 ); scene.add( light ); // Init Object geometry = new THREE.PlaneGeometry( 500, 400, 1, 1 ); geometry.vertices[0].uv = new THREE.Vector2( 0, 0 ); geometry.vertices[1].uv = new THREE.Vector2( 2, 0 ); geometry.vertices[2].uv = new THREE.Vector2( 2, 2 ); geometry.vertices[3].uv = new THREE.Vector2( 0, 2 ); // Init Texture var texture = new THREE.TextureLoader(); texture.load( "texture/a.jpg", function(texture) { var material = new THREE.MeshBasicMaterial( { map: texture } ); var mesh = new THREE.Mesh( geometry, material ); scene.add( mesh ); }); window.addEventListener( 'resize', onWindowResize, false ); } function animate() { // rendener.renderClear(); renderer.render( scene, camera ); requestAnimationFrame( animate ); } function onWindowResize() { camera.aspect = WIDTH / HEIGHT; camera.updateProjectionMatrix(); renderer.setSize( WIDTH, HEIGHT ); } </script> </body> </html> |
代码解析:
- 画一个平面
- 为平面赋予纹理坐标
- 加载纹理
- 将纹理应用于材质
(1) 画一个平面
通过 PlaneGeomotry 可以画一个平面,代码如下:
1 |
var geometry = new THREE.PlaneGeometry( 500, 300, 1, 1 ); |
这个平面是一个宽度为500,高度为300的平面。
(2) 为平面赋予纹理坐标
平面有 4 个顶点,所以只需要指定 4 个纹理坐标就可以了。纹理坐标由顶点的 uv 成员来表示, uv 被定义为一个二维向量 THREE.Vector2(),代码如下:
1 2 3 4 |
geometry.vertices[0].uv = new THREE.Vector2( 0, 0 ); geometry.vertices[1].uv = new THREE.Vector2( 1, 0 ); geometry.vertices[2].uv = new THREE.Vector2( 1, 1 ); geometry.vertices[3].uv = new THREE.Vector2( 0, 1 ); |
四个顶点对应了纹理的四个顶点。注意坐标之间的顺序是逆时针方向,左下→右下→右上→左上。给平面赋予纹理坐标的时候也要注意方向问题,不然 Threejs 是不清楚的(OpenGL也是如此)。
(3) 加载纹理
纹理作为一张图片,可以来源于互联网,或者是本地服务器,但是不能来源于本地,即类似 C:\texture\a.jpg 这样的路径。这是因为 javascript 没有加载本地路径文件的权限。如果尝试这么做,将会在浏览器的控制窗口 console.log 中显示错误:
所以必须在电脑上搭建一个 Tomcat 或者 Apache 之类的网页服务器,并把图片资源放到服务器上,通过相对路径地址或者绝对路径地址去访问才可以正确加载。
旧版本的 Threejs 会用如下代码:
1 2 3 |
var texture = THREE.ImageUtils.loadTexture( "texture/a.jpg", null, function(t) { }); |
但是新版本用上面的代码是会显示这个函数已经取消了,需要用 THREE.TextureLoader 代替,故这部分代码应为:
1 2 3 4 5 6 |
var texture = new THREE.TextureLoader(); texture.load( "texture/a.jpg", function(texture) { var material = new THREE.MeshBasicMaterial( { map: texture } ); var mesh = new THREE.Mesh( geometry, material ); scene.add( mesh ); }); |
(4) 将纹理应用于材质
加载好纹理,就只需要将纹理映射到材质上就可以了。我们这里使用了一个普通的材质 THREE.MeshBasicMaterial (如上代码),材质中有一个 map 属性,可以直接接受纹理。所以实际上,纹理作用于 material 的。相关代码看上面的 texture.load() 其中蕴含的 function(…)。
4、时钟纹理实例
实现的步骤如下:
- 在 canvas 上画时钟
- 将 canvas 传递给 THREE.Texture 纹理
- 将纹理传递给 THREE.MeshBasicMaterial 材质
- 最后构造 THREE.Mesh
(1) 在 canvas 上画时钟
封装 在 js/clock.js 中,这是用 HTML5 代码绘制的 canvas 的基本函数。
(2) 将 canvas 传递给 THREE.Texture 纹理
canvas 可以作为纹理传递给 THREE.Texture 函数,其构造函数在前文已给出。
在 Image 参数上传递 canvas 变量,后面的参数不理会自然使用默认参数。
1 |
texture = new THREE.Texture( canvas ); |
那么纹理是怎么知道其每一个像素怎么映射到形状的表面的,默认情况下,纹理被均匀分配到四边形的各个顶点上。
(3) 将纹理传递给 THREE.MeshBasicMaterial 材质
将 texture 传递给材质,材质本身可以接受一个 属性名为 map 的参数,故:
1 |
var material = new THREE.MeshBasicMaterial( { map: texture } ); |
这样就将纹理赋给了材质了。
(4) 最后构造 THREE.Mesh
Mesh 就是个网格表面,它代表着我们渲染到 3D 世界中的各种模型。其构造模型为:
1 |
THREE.Mesh = function( geometry, material ); |
只有两个参数:geometry – 几何体;material – 材质,可以通过这样来构造它:
1 2 3 4 |
// var geometry = new THREE.CubeGeometry( ... ); // var material = 如上 var mesh = new THREE.Mesh( geometry, material ); |
后记
A、纹理原理
本质上来说,纹理只是图片而已,是由像素点组成的。无论实在内存中还是在显存中,都是由 4 个分量组成,就是 R、G、B、A。唯一不同的是,在显存里,会比内存更快的渲染到显示器上。