这一章讲纹理,纹理,不能顾名思义了,其实就是一张图片,我们要做的就是把这张图片贴到模型上面,从而让模型一下子生动起来,说得有文采点就是栩栩如生,这个过程就叫纹理映射(也可以叫贴图)。纹理映射有时候能产生非常神奇的效果,比如说凹凸贴图,和凹凸贴图衍生出来的法线贴图,能够让只有几百个几千个多边形的模型产生几万个,几十万个多边形的模型的效果。
接下来就要介绍怎么创建一个纹理采样器 (texture sampler) ,如何设置采样的属性,然后把一张图片(纹理)成功地贴到那个立方体上面,图片的格式可以是 TGA , JPEG , PNG , DDS ,其中像 TGA,PNG 这样的格式是支持半透明的,可以用来模拟阴影(当然只是简单地模拟)。
纹理采样器负责怎么把一张图片上的各个像素映射到模型的像素上,反过来说,就是为每个像素选取纹理像素。看似很简单,但是当显示的图片大于(或小于)要覆盖的那个面时,就得就结了,这个不能一一映射了,那得怎么做呢,这是门学问,我们可以通过设置这个纹理采样器的各个属性来达到较好的效果。
Address Mode
首先是 addressModeU 和 addressModeV 属性,这两个属性决定了当纹理坐标超出( 0 , 1 )后该怎么做。纹理坐标,顾名思义,就是在纹理上的坐标,这是我们截取纹理中的一部分是要用到的。它的范围是( 0 , 1 )。其中左下角的坐标是 (0.0 , 1.0) ,右上角的坐标是 (1.0 , 1.0)
这两个属性有下面四种可能值
Value
Meaning
WRAP
( 默认为此值)重复贴图来填充贴图区域
MIRROR
重复贴图,当遇到 UV 的边界时反向( UV 边界为 0.0 或者 1.0 )
CLAMP
用贴图的最后一行像素,重复覆盖没贴到的区域
BORDER
超过的贴图区域,用一种特殊颜色的边标记出来
Minification Filter
Minification filter, 即 minFilter 这个属性具体说明了怎么把纹理贴到一个像素比它少的模上面(这个上面有提到过),这个属性可能有下面 3 个值
Value
Meaning
POINT
用最近的那个像素
LINEAR
线性插值
ANISOTROPIC
各向异性过滤
这三个滤镜效果是单调递增的,具体是怎么实现的,这里不多讲了 ( 其实也不会讲 - -) ,想了解的话可以去看图像处理方面的书籍。。。。
Mipmap Filter
由于处理缩小纹理比处理放大纹理复杂多了,如果只是简单地取最近的那个像素,或者说只是和这个像素的周围几个像素简单的比较一下,获得平均值就插进去的话,会严重降低图片缩小后的质量,而如果去周围大量的像素,做大量的计算又会严重降低程序效率。于是有才的人就想到了 mipmap 这个方法。它先将这张图片按原来尺寸一半一半地压缩,举个例子,原来的是 64*64 的图片,就压缩成一张 32*32 的,一张 16*16 的,一张 8*8 的......当这张图片需要映射成 20*20 的大小的,就取 32*32 和 16*16 这两张图片来计算出 20*20 大小的图片,这比只是计算单张图片的效率和效果要好得多 .
下面是这个参数可能的值
Value of mipFilter
Which Mipmap Is Used
NONE
不用mipmap
POINT
取与需要映射的大小最近的那张图片,然后依据上面的minification filter 进行过滤
LINEAR
取与需要映射的大小最近的两张图片,为每张图片按照上面的miniFilter 进行过滤,然后把这两张图片一起进行线性插值等到最终的图片
Magnification Filter
放大纹理的处理就比较简单了,有下面两种可能的值。
Value
Meaning
POINT
Use the closest pixel.
LINEAR
Perform a linear interpolation between neighboring pixels and use the result.
跟 minFilter 差不多。
Border Color
当前面的 address mode 设成 border 时,这个属性就派上用场了,它可以用来设置那个边框的颜色。
Anisotropy
当前面的 minFilter 是 Anisotropy 时,这个值就是决定了各向异性过滤的质量。
这些属性介绍完了,下面就举个实例,把那张求是潮的 logo 贴到那个立方体上。
首先要建立一个 texture sampler ,同 shape 和 material 一样,这也是一个 Object ,非常好地体现了"面向对象"的思想。
g_sampler = g_pack . createObject ( 'Sampler' );
g_sampler . minFilter = g_o3d . Sampler . ANISOTROPIC ;
g_sampler . maxAnisotropy = 4 ;
可以看到第一句就是建立 sampler 对象,后面两句就是设置上面提到的属性, minFilter 设为各向异性过滤, maxAnisotropy 设为 4 。
这样一个简单的 sampler 就建立了,但是比较打击人的是这个还只是设了下最基本的参数,这个参数设了给谁用呢,又是在哪里把图片贴到那个模型上面去的呢
恩 ~ 在 o3d 里面纹理贴图就是在 pixel shader 里面用了一个函数 tex2D 把图片像素一个个根据上面设好的各个参数填进已经经过 vertex shader 变换和 primitive assembly (就是 shape 一章里讲过的把各个顶点按照一定规则连接成图元的操作)了的东西里面。而传给 tex2D 的参数有两个,一个是一开始提到的纹理坐标( texCoord ),还有一个就是采样器的各个参数了(这里面也包括了整个纹理素材),要把 js 中设的参数传到 shader 里面, o3d 中的做法是先在 shader 里面建一个变量,比如 texSampler0 ,然后在 js 中用 material 的 getParam 方法获取这个 texSampler0 变量,具体的如下:
effect . createUniformParameters ( material ); // 这句话是不可少的,否则就不能 获取shader 里面设的变量了
var samplerParam = material . getParam ( 'texSampler0' );
samplerParam . value = g_sampler ; // 把 g_sampler 的参数赋给 samplerParam
这里的 material 和 effect 就是前面几章为那个立方体建立的材质对象。所以可把上面几句代码加在原来的建立 material 和 effect 的代码的后面 ( 具体的 shader 代码在最后放出来 )
下面要做的是把纹理资源载入内存。
o3djs . io . loadTexture ( g_pack , textureUrl , function ( texture ) {
// set the texture on the sampler object to the newly created texture
// object returned by the request.
g_sampler . texture = texture ; // 可以看到这里把纹理资源也附到sampler 里 了以供shader 使用
})
其中 function(texuture) 。。。。。是回调函数,这里声明成匿名函数了( js 非常好用的一个特性),如果要处理的量比较大的话,可以独立出来写成一个函数。也可以在其它纹理载入的时候使用。 textureURL 顾名思义就是图片地址了。
最后是声明各个顶点的纹理坐标,前面说到过要传给 shader 的,这个和声明顶点坐标差不多(顶点坐标数组也得相应的改过,不能几个面公用同一个顶点了,具体的建 sample 里的代码吧,再放出来就太长了 = = )
var texCoordsArray = [
0 , 0 ,
1 , 0 ,
1 , 1 ,
0 , 1 ,
0 , 0 ,
1 , 0 ,
1 , 1 ,
0 , 1 ,
1 , 1 ,
0 , 1 ,
0 , 0 ,
1 , 0 ,
0 , 0 ,
1 , 0 ,
1 , 1 ,
0 , 1 ,
0 , 0 ,
1 , 0 ,
1 , 1 ,
0 , 1 ,
0 , 0 ,
1 , 0 ,
1 , 1 ,
0 , 1
];
var texCoordsBuffer = g_pack . createObject ( 'VertexBuffer' );
var texCoordsField = texCoordsBuffer . createField ( 'FloatField' , 2 );
texCoordsBuffer . set ( texCoordsArray );
streamBank . setVertexStream (
g_o3d . Stream . TEXCOORD , // semantic
0 , // semantic index
texCoordsField , // field
0 ); // start_index
不多做解释了。
最后的最后:
Shader 代码
// World View Projection matrix that will transform the input vertices
// to screen space.
float4x4 worldViewProjection : WorldViewProjection;
// The texture sampler is used to access the texture bitmap in the fragment
// shader.
sampler texSampler0;
// input for our vertex shader
struct VertexShaderInput {
float4 position : POSITION;
float2 tex : TEXCOORD0; // Texture coordinates
};
// input for our pixel shader
struct PixelShaderInput {
float4 position : POSITION;
float2 tex : TEXCOORD0; // Texture coordinates
};
/**
* The vertex shader simply transforms the input vertices to screen space.
*/
PixelShaderInput vertexShaderFunction(VertexShaderInput input) {
PixelShaderInput output;
// Multiply the vertex positions by the worldViewProjection matrix to
// transform them to screen space.
output.position = mul(input.position, worldViewProjection);
output.tex = input.tex;
return output;
}
/**
* Given the texture coordinates, our pixel shader grabs the corresponding
* color from the texture.
*/
float4 pixelShaderFunction(PixelShaderInput input): COLOR {
return tex2D(texSampler0, input.tex);
}
// Here we tell our effect file *which* functions are
// our vertex and pixel shaders.
// #o3d VertexShaderEntryPoint vertexShaderFunction
// #o3d PixelShaderEntryPoint pixelShaderFunction
// #o3d MatrixLoadOrder RowMajor
Shader 的代码就不解释了,下一章会比较详细地讲下 shading language 和图形渲软管线到底是怎么样的,下下章可能会讲光照,光照就是都在 shader 里面计算了,这个纯考数学物理知识啊,不过幸好已经有前人弄好的公式给我们套了。然后就是讲下阴影 (shadow) 的生成了,注意的是并不是有光照就会自然而然地产生阴影了,阴影的处理是游戏中比较关键的,也是很占资源的一个地方。阴影处理的好的话游戏的真实性倍增啊啊。。。。
还有要说的就是 shapes 和这章的纹理映射以后不会这么详细地用到,不会让你一个个顶点地去定义,更多的是载入一个用 3d 软件诸如 3DMAX 或 maya 做的模型,或者是用自带的函数简单地创个球,毕竟像这次这样创建一个带有贴图的立方体会让人崩溃的。