纹理(Texturing)
最简单的纹理方式就是在建模程序中使用一张贴图。当你导出模型时,请留意egg文件与图片文件的相对路径,相对路径被编码到egg文件里。当panda载入egg文件时,它将搜索egg文件相对路径。Panda支持JPG、PNG、TIF等图片格式。
接下来将详细介绍各种纹理方法。
简单纹理(Simple Texturing)
纹理图(texture map)或纹理图像(texture image)是一张2维图像文件,比如一张JPEG或Windows BMP图,用来给三维模型上色。它被称为“纹理”是因为这种技术的最早使用者将一些有趣的纹理贴到墙和天花板上,好让墙和天花板不再是单调的、看起来像塑料一样的颜色。今天,在三维应用程序中纹理的应用已经相当普遍——没有纹理,模型全都是白色。
通过不同的纹理方法可以获得许多不同的渲染效果。在学习这些效果前,我们首先来了解纹理的基本概念。
简单纹理——目前为止最平常的形式——你可以把纹理贴图想象成蒙在模型表面上的一幅画。为了让显卡知道该怎样贴上这幅画,模型必须有纹理坐标——模型的每个顶点对应一对特殊的(u, v)坐标。每个顶点的(u, v)纹理坐标将顶点定位于纹理图的某个点上,正如该顶点的(x, y, z)坐标把它定位在三维空间的某一位置。
由于坐标的名为u和v,纹理坐标也被称为UV坐标。几乎任何建模软件在创建模型的同时都会创建纹理坐标,甚至都不用你命令它。
为方便使用,纹理图的(u, v)坐标值域这样限定:u从左到右取值0到1,v从下往上取值0到1。也就是说,纹理图左下角坐标为(0, 0),右上角为(0, 0)。下面是一些典型的纹理图:
你为顶点分配的(u, v)纹理坐标决定了纹理图将怎样贴到模型上。当绘制模型时,模型上的三角形根据顶点的(u, v)坐标在纹理图上相应得到一个三角形,模型三角形取得纹理图三角形的颜色来绘制。例如,Panda提供的smiley.egg模型例子将顶点的u坐标轴定义为它的赤道,v坐标自底部0到顶部的1。这样的定义使纹理图水平地包裹整个球体:
注意,无论多大的图片,(u, v) 的坐标范围总是从0到1。
选择纹理大小(Choosing a Texture Size)
绝大部分显卡要求纹理图像的大小必须是2的乘方,也就是说纹理大小只能是:1、2、4、8、16、32、64、128、256、512、1024、2048等(除非你的显卡特别高级,否则2048应该到头了)。
纹理不必都是正方形:不必长、高相等,但长和高必须都是2的乘方。例如64 × 128、512 × 32或256 × 256都是合法的。200 × 200像素的纹理图就不行,因为200不是2的乘方。
默认情况下,从磁盘读入纹理时,Panda3D会自动把它缩小成最接近实际的2的乘方大小,因此通常我们不需要考虑这个问题——但如果提前设置了正确的大小,程序的载入速度会更快。
一些新型显卡也能渲染非2的乘方的纹理。如果你拥有这样一块显卡,可以在你的Config.prc文件添加下面一行,从而停止Panda3D自动处理功能:
textures-power-2 none
注意,某些显卡看似可以渲染非2的乘方的纹理,但实际上驱动在载入时还是对纹理进行了缩小。这种情况下,最后让Panda来做处理,否则变化的纹理会使渲染变慢。
textures-power-2还有另一个选项down(缩小到最接近的2的乘方,默认为down)或up(放大到下一个2的乘方)。
最后值得注意的是,你选择的纹理图像的大小与显示在屏幕上的纹理图像的大小或形状毫不相干——它取决于使用该纹理的多边形的大小和形状。把纹理图像变大并不能使它在屏幕上显得更大,但会变得更细致更清晰。同理,较小的纹理将显得模糊。
纹理的wrap模式(Texture Wrap Modes)
前面讲过,你为顶点分配的(u, v)纹理坐标决定了纹理怎样贴到模型上。通常,你使用的纹理坐标在[0, 1]范围内,代表整个纹理图像的像素区域。但是,你也可以使用超出这个范围是坐标值,例如比1大的数或者一个负数。
所以,如果纹理图像限定在[0, 1]范围,在此之外的纹理是什么样子呢?你可以通过纹理的wrap模式来指定:
texture.setWrapU(wrapMode)
texture.setWrapV(wrapMode)
可对u、v分别指定wrapMode参数(对3维纹理还有一个setWrapW(),以后将提到)。wrapMode的值可以取下面几种:
Texture.WMRepeat |
纹理图像无限重复 |
Texture.WMClamp |
纹理图像最后的像素扩展到无限 |
Texture.WMBorderColor |
用texture.setBorderColor()指定的颜色填充空余地方 |
Texture.WMMirror |
纹理图像无限次翻转 |
Texture.WMMirrorOnce |
纹理图像向后翻转一次,其余地方使用边界颜色 |
默认的wrap模式为WMRepeat。
给定下面这张简单的纹理图:
我们将把它贴到一个大多边形的中心,该多边形2个方向上的纹理坐标都大大超出了[0, 1]范围。
WMRepeat
texture.setWrapU(Texture.WMRepeat)
texture.setWrapV(Texture.WMRepeat)
WMRepeat模式常用在一张较小的纹理反复拼贴(tile)到大表面上。
WMClamp
texture.setWrapU(Texture.WMClamp)
texture.setWrapV(Texture.WMClamp)
WMClamp模式对大多边形来说用不上,因为像素扩展后得到的效果很难看;但当纹理与多边形吻合时,这种模式是个正确的选择(见下文“注意一个常见的wrap错误”)。
WMBorderColor
texture.setWrapU(Texture.WMBorderColor)
texture.setWrapV(Texture.WMBorderColor)
texture.setBorderColor(VBase4(0.4, 0.5, 1, 1))
上图使用蓝色只起到说明的作用;你可以使用任何颜色。我们一般使用纹理的背景颜色作为边界颜色,如:
texture.setWrapU(Texture.WMBorderColor)
texture.setWrapV(Texture.WMBorderColor)
texture.setBorderColor(VBase4(1, 1, 1, 1))
有些老显卡不支持WMBorderColor,在这种情况下,Panda3D将使用WMClamp模式,只要你的纹理有足够多的空白边,得到的效果就和WMBorderColor模式差不多(我们这个例子不行,没有空白边)。
WMMirror
texture.setWrapU(Texture.WMMirror)
texture.setWrapV(Texture.WMMirror)
许多老显卡不支持WMMirror,在这种情况下,Panda3D将使用WMRepeat模式。
WMMirrorOnce
texture.setWrapU(Texture.WMMirrorOnce)
texture.setWrapV(Texture.WMMirrorOnce)
texture.setBorderColor(VBase4(0.4, 0.5, 1, 1))
只有少数显卡支持WMMirrorOnce,在这种情况下,Panda3D将使用WMBorderColor模式。
设置不同的wrap模式
可以对u、v设置不同的wrap模式:
texture.setWrapU(Texture.WMRepeat)
texture.setWrapV(Texture.WMClamp)
注意一个常见的wrap错误
当你应用一个刚好与多边形大小吻合的纹理——纹理坐标从0到1,没有越界——应该把wrap模式设为clamp。如果你沿用默认的repeat模式,颜色可能会从对面的边界渗出,在模型边界上形成一条细线,就像这样:
特别是把纹理当成alpha抠像时经常出现这个错误。在背景完全透明的图上:你经常会发现一条细细的、几乎看不出来的边漂浮在多边形上。这条边实际上是纹理的底边从顶部渗出造成的,因为设计者误用了WMRepeat模式,正确的模式应该用WMClamp。
纹理filter类型(Texture Filter Types)
在显示纹理时,纹理图像的像素跟屏幕像素一一对应的情况非常之罕见,大多数时候,不是纹理的一个像素被拉伸覆盖屏幕的多个像素(纹理放大(texture magnification)——纹理被拉伸放大),就是纹理的多个像素对应一个屏幕像素(纹理缩小(texture minification)——纹理被挤压缩小)。对于多边形来说,经常会遇到有些纹理像素需要放大,有些像素却需要缩小的情况(显卡可以同时处理)。
当纹理被放大或缩小时,你可以设定filterType控制它们的外观:
texture.setMagfilter(filterType)
texture.setMinfilter(filterType)
对放大或缩小都可以单独设定filterType。两者都适用的filterType是:
Texture.FTNearest |
采样最近的像素 |
Texture.FTLinear |
采样4个最近像素,然后线性插值 |
只供缩小使用的filterType是:
Texture.FTNearestMipmapNearest |
从最接近的mipmap 级别进行像素的点采样(Point sample) |
Texture.FTLinearMipmapNearest |
从最接近的mipmap 级别进行像素双线性过滤(Bilinear filter) |
Texture.FTNearestMipmapLinear |
从2个mipmap 级别进行像素的点采样(Point sample),然后线性混合(linearly blend) |
Texture.FTLinearMipmapLinear |
从2个mipmap 级别进行像素双线性过滤(Bilinear filter),所得结果线性混合(linearly blend)。又被称为三线性过滤(trilinear filtering)。 |
无论放大或缩小,默认的filter类型都是FTLinear。
给定下面的一张纹理,我们来看看不同的filter得到的效果:
FTNearest
texture.setMagfilter(Texture.FTNearest)
texture.setMinfilter(Texture.FTNearest)
FTNearest通常只是用来获得某种像素化的效果。
FTLinear
texture.setMagfilter(Texture.FTLinear)
texture.setMinfilter(Texture.FTLinear)
虽然还不够完美,但FTLinear是通用的比较好的选择。
Mipmaps
许多图形学教程都耗费大篇幅来讲解什么是mipmapping以及它的运作机理。在此我们不想详细探讨,但你应该理解下面几点:
(1)它需要多出33%的纹理内存(对每个mipmapped纹理),但渲染速度会加快。
(2)缩小时使用mipmap比只用单个图像过滤效果要平滑。
(3)mipmapping与放大没有任何关系。
(4)有可能会使缩小的纹理模糊,尤其当贴上纹理的多边形几乎侧立(edge-on)对着摄影机时。
Mipmapping包含4种filter类型,但其实我们一般都用最后一种FTLinearMipmapLinear。其他模式是为高级用户准备的,有时用来克服mipmap的缺点(尤其是上面第4点)。如果你对上面的内容不太明白,大可不必担心。
texture.setMinfilter(Texture.FTLinearMipmapLinear)
各向异性过滤(Anisotropic Filtering)
对纹理过滤公式加上最后一条:可以在前文所举的任何filter模式上应用anisotropic filtering,这将开启一种更昂贵也更缓慢的渲染模式,得到的效果最好。一般来讲,anisotropic filtering在处理纹理缩小时比mipmapping要好,它不产生严重的模糊现象。
打开anisotropic filtering,指定一个度数:
texture.setAnisotropicDegree(degree)
度数应为一个整数。默认值为1,表明没有anisotropic filtering;一个更大的数表明你需要过滤的量。数值越大代价越高,得到的结构也越好,这将取决于你的显卡性能。许多显卡只支持度数2,通常已经足够大了。
texture.setAnisotropicDegree(2)
目前,anisotropic filtering只被DirectX接口支持。一些旧显卡根本不能anisotropic filtering。
简单的纹理替换(Simple Texture Replacement)
虽然我们通常载入和显示的是已经贴好纹理的模型,但其实也可以在运行时对模型应用纹理或替换纹理。为此,必须首先获得一个纹理的句柄,例如直接载入纹理:
myTexture = loader.loadTexture("myTexture.png")
上面这个loadTexture()调用将在当前模型路径中搜索指定的图像(本例中的文件名为“myTexture.png”)。如果没有找到或是因为某些原因不能读取,函数将返回None。
一旦获得一个纹理,就可以调用setTexture()应用到模型上。例如,假设你使用CardMaker类生成一张纯白色卡片:
cm = CardMaker('card')
card = render.attachNewNode(cm.generate())
然后就可以载入纹理应用到卡片上:
tex = loader.loadTexture('maps/noise.rgb')
card.setTexture(tex)
(注意,调用时不必使用setTexture()的override参数——也就是不必写成card.setTexture(tex, 1)——因为在本例中,卡片上还没有其他纹理,因此你的纹理即使没有override也是可见的)
要使纹理有效,模型必须已经定义了纹理坐标(参考简单纹理部分)。在生成卡片时,CardMaker默认生成纹理坐标,因此没什么问题。
你也可以使用setTexture()来替换已有的纹理。在这样情况下,你必须给出setTexture()的第二个参数,它与其他函数的override参数一样用于改变panda的状态。一般你只要给setTexture()第二个参数传个1。如果没有这个override,直接分配在Geom层的纹理将优先于你对模型节点进行的状态改变,纹理将保持不变。
例如,要改变smiley的样子,这样做:
smiley = loader.loadModel('smiley.egg')
smiley.reparentTo(render)
tex = loader.loadTexture('maps/noise.rgb')
smiley.setTexture(tex, 1)
我们经常只想替换模型某个部分的纹理,而不是对每部分重设纹理。为此,只要获得想要改变的那部分的NodePath(在操作模型分部那一节有讲到),然后对那些NodePath调用setTexture()。
例如,下面这辆车有多张不同颜色的纹理:
汽车大部分组件的纹理保存在一张大图里,如下图:
同时我们也有相同的一个蓝色版纹理:
虽然很想用setTexture()把蓝色纹理赋予整个汽车,但同时蓝色纹理也会赋给汽车轮胎,而轮胎使用的是另一张纹理图。所以我们使用另一种办法,仅对需要改变的部分应用蓝色纹理:
car = loader.loadModel('bvw-f2004--carnsx/carnsx.egg')
blue = loader.loadTexture('bvw-f2004--carnsx/carnsx-blue.png')
car.find('**/body/body').setTexture(blue, 1)
car.find('**/body/polySurface1').setTexture(blue, 1)
car.find('**/body/polySurface2').setTexture(blue, 1)
结果如下所示:
To be continued……