Game Programming with DirectX -- 06[纹理映射技术]
第六集 纹理映射技术
为使建立的3D模型更接近现实世界中的物体, 简单的颜色变换已经无能为力, 这时我们就需要纹理映射技术了.
这一集我们讲解基础的纹理映射技术的数学模型, 对于在粒子系统使用的过程纹理技术在高级部分讲解.
6.1 二维纹理映射
6.1.1 纹理映射的简单建模
二维纹理映射就是从二维纹理平面到三维物体表面的映射. 一般二维纹理平面是有范围限制的, 在这个平面区域内, 每点都可用数学函数表达, 从而可以离散的分离出每点的灰度值和颜色值, 这个平面区域称为纹理空间, 一般将纹理空间的平面区域定义在[0, 1] * [0, 1].
纹理映射是确定物体表面一点P在纹理空间中的对应点(u, v), 从而纹理空间中的点(u, v)处的纹理值就是物体表面点P的纹理属性. 这样屏幕上显示的像素的颜色可通过下面的映射得到,
F : Screen Space --> Object Space
G : Object Space --> Texture Space
其中F我们在第二集中已详细讲解过, 所以我们关心的是G. 只要确定了物体表面的纹理属性, 接着就是将物体表面上各点所对应的纹理值作为光照明模型中的相应参数进行光强度计算, 再绘制画面.
所以G函数的定义直接影响画面的效果, 如何确定G非常重要. G可表示为,
(u, v) = H(x, y, z)
其中(u, v)和(x, y, z)分别是纹理空间和物体空间中的点. 现在从简单的圆柱开始, 一个高h, 半径为r的圆柱的纹理映射方法, 我们用参数形式表示圆柱,
x = r * cos a
y = r * sin a
z = h b
0 <= a <= 2PI, 0<= b <= 1
从纹理空间 [0, 1] * [0, 1] 到 [0, 2PI] * [0, 1] 的线性变换为,
u = a / 2PI
v = b
这样, 我们建立了物体空间到纹理空间映射的表达式. 如图6.1
图6.1
物体表面没有象圆柱一样, 所以上面的方法不实用.
6.1.2 两步法纹理映射
1986年, Bier和Sloan提出了一种独立于物体表面的纹理影射技术, 将纹理空间到物体空间的映射分为两个简单的映射复合. 两步法纹理映射的核心是引进一个包围物体的中介三维曲面作为中间映射媒体, 主要过程分两步,
(a). 将二维纹理映射到一个简单的三维物体表面, 如平面, 球面, 圆柱面, 立方体表面, 采用不同的 中间映射媒体生成的纹理效果是不同的, 要根据目标物体表面来选择.
S : (u, v) --> (x', y', z') --- S-映射
如半径为R的球的S-映射为,
x = R * cos a * sin b
y = R * sin a * sin b
z = R * cos b
(0 <= a <= 2PI, 0<= b <= PI)
(b). 将(a)的三维物体表面上的纹理映射到目标物体表面,
O : (x', y', z') --> (x, y, z) --- O-映射
O-映射一般有4种, 如图6.2,
图6.2
(1). 取视线在物体表面可见点(x, y, z)处的反射与
中间映射媒体表面上的交点(x', y', z')作为
(x, y, z)的映射点.
(2). 取物体表面可见点(x, y, z)处的法线与中间映射
媒体表面上的交点作为映射点.
(3). 取物体中心到可见点(x, y, z)的射线与中间映射
媒体表面上的交点作为映射点.
(4). 中间映射媒体表面上的点(x', y', z')处的法线
与物体表面的交点为(x, y, z), (x', y', z')
为(x, y, z)的映射点.
其中(1)映射方式应用的最广, 称为环境映射, 我们下一节会简单的说明一下. 其余的三种O-映射和上面4种 S-映射有12种组成, 其中可用的为5种, 如下表.
三种O-映射和4种 S-映射有12种组成
_____________________________________________________________
| / | | | | |
| / S-映射 | 平面 | 圆柱面 | 立方体 | 球面 |
| O-映射 / | | | | |
|---------------|----------|----------|----------|----------|
| 表面法向(2) | 冗余 | 不适合 | 不太适合 | 不太适合 |
|---------------|----------|----------|----------|----------|
| 物体中心(3) | 冗余 | 不适合 | 适合 | 适合 |
|---------------|----------|----------|----------|----------|
| 中介面法向 (4) | 适合 | 适合 | 适合 | 冗余 |
+---------------+----------+----------+----------+----------+
6.1.3 环境映射
环境映射是两步法纹理映射的特例, 环境映射是近似的模拟光线跟踪, 但没有光线跟踪那么复杂, 所以能大大提高光线跟踪算法的效率, 目前在商业应用中广泛使用的纹理影射技术. 环境映射的过程如图6.3,
图6.3
(1). 物体被中介曲面球包围, 中介曲面球记录了物体表面的纹理值.
(2). 视点的光线经过一像素的四个角点投射到物体表面, 经物体表面的反射到达中介曲面球, 图6.3中P的纹理属性就由中介曲面球上的E点唯一确定.
(3). 中介曲面球的缺点是在球的两极点纹理变化太快, 形成纹理的突变.
为避免 中介曲面球的极点纹理突变, 环境映射中可采用立方体表面为中介表面, 整个环境纹理映射由六幅图(mip-map表)组成, 六幅图象是六个90度视域方向拍摄的或绘制的真实景物图象, 其映射过程简单的将入射光线束通过物体表面片投射到立方体上, 入射光线束形成的锥和立方体表面的交集就是该物体表面片的纹理, 如图6.4.
图6.4
立方体环境映射计算量比球面环境映射大很多, 主要因为光线和立方体表面的交集区域计算复杂. 为次可以以立方体中心为投影中心, 将立方体表面包含的纹理属性投影到立方体的外接球面上, 再由球面环境映射来确定物体表面纹理, 这样既省计算时间, 又可避免球面环境映射在极点的纹理突变.
6.2 特殊纹理映射
这节的例子以后给出.
6.2.1 凹凸纹理映射
两步法纹理映射方法只能实现物体表面的颜色(花纹)纹理, 但不能实现物体表面几何形状的凹凸不平而形成的粗糙质感. 为此, 出现了无须修改物体模型就能实现物体表面凹凸不平效果的纹理技术 -- 凹凸纹理映射(Bump mapping).
由于物体表面的光强度是由物体表面的法向量决定的, 凹凸纹理映射通过对物体表面各采样点的法向量作微小的调整来改变物体表面的光强度, 达到凹凸不平效果.
但不是任何调整都能产生凹凸不平的真实感效果, 我们需要建立数学模型, 定义物体表面参数方程, 在(m, n)点的法向量为,
V = V(m, n)
设Vm, Vn分别是V在m, n上的分量, 那么在(m, n)点的单位法向为,
N = N(m, n) = Vm X Vn / | Vm X Vn |
现在用一个微小调整的函数P(m, n), 一般这个函数是连续可微的, 那么通过函数P(m, n)调整的点(m, n)的新法向量为,
| V'm = Vm + Pm N
V '(m, n) = V(m, n) + P(m, n) N = |
| V'n = Vn + Pn N
从而得到新的单位法向, 如图6.5
N ' = V'm X V'n = N + D
D =Pm A - Pn B
A = N X Vn, B = N X Vm
图6.5
对于函数P(m, n), 可以用一张凹凸图来离散的给出各点的值.
6.2.2 位移纹理映射(Displacement mapping)
凹凸纹理映射能很好的模拟面的凹凸不平的真实感效果, 但在物体的轮廓线上的凹凸不平效果无法表达, 如图6.6
图6.6
我们将含有凹凸信息的面按一定方向旋转后, 原来的凹凸不平的真实感效果就不明显了.为此出现了位移纹理映射技术, 它和凹凸纹理映射技术相似, 只是位移纹理映射是沿法向方向调整的是采样点的位置, 将凹凸信息的显示用点坐标的调整来得到, 如图6.7
图6.7
当含有凹凸信息的面转过90度后, 我们还是能看出面的凹凸信息.
值得注意的是, 位移纹理映射对物体表面的造型进行了改变, 所以呈现的凹凸信息能实现阴影效果.
6.3 纹理映射中的反走样
纹理映射常常需要将纹理图案映射到不同大小的物体表面, 理想的是物体表面大小和纹理图案大小一致. 但实际物体表面大于或小于纹理图案的情况多, 这时必须取这区域的纹理属性值--如像素颜色的平均值作为对应表面的纹理属性, 但当物体表面形状复杂时, 计算平均值并不能精确得到物体表面的纹理属性.
另外将一张黑白的世界象棋图映射到物体表面, 当物体表面小到接近单位像素时, 表面只能显示黑色或白色, 当物体位移或旋转时, 会出现黑白闪烁现象.
上面都是纹理映射的走样问题, 相应的就有反走样技术, 我们讲解mip-map技术.
6.3.1 mip-map
mip是拉丁文multum in parvo(聚集在一块小区域内的许多东西)的缩写, mip-map技术根据初始的纹理图案每边的分辨率S, 形成一个四棱锥型的mip-map, 如图6.8
图6.8
图左边是四棱锥型的mip-map, 其中纹理图案的层数是[log 2 S] + 1, 也就是设置一张64X64的纹理图案, mip-map就以64X64为底的四棱锥, 一共有[log 2 64] + 1 = 7层, 最上层为一个像素, 同时我们也知道为何纹理图案都建议是2的幂次的原因.
mip-map的存储是以一张查找表的形式存储的, 如图6.8的右边, 一张64X64的纹理图案的查找表的大小是128X128. 存储时先提取纹理图案中每像素的R, G, B值, 然后对R, G, B值逐级压缩来得到.
mip-map在确定屏幕上可见表面的纹理过程如下,
(1). 计算屏幕上可见表面的中心在纹理空间上的映射点坐标(u, v).
(2). 确定纹理空间中以(u, v)为中心, 边长为d的正方形, 要求正方形能覆盖表面在纹理空间中映射的区域.(实际这样算d太复杂, 一般d为表面在纹理空间中映射的区域的最大边长)
(3). 根据d的大小确定使用哪一级的纹理map.
因为mip-map中的纹理图案存储的是特定的图案, 即只有边长d = 2^k, k = 0, 1, ..., [log 2 S]的图案, 对于在2^k < d < 2^(k + 1)的边长d, mip-map通过线性插值第k层的纹理和第k + 1层的纹理得到.
6.4 纹理映射的例子
6.4.1 代码更新
这集的例子是game4 project.
我们在这例子里改变了顶点的属性, 加了顶点的纹理属性, 纹理属性是确定顶点在纹理空间中的映射点的坐标的.
我们在确定顶点在世界中的坐标的同时, 还要确定顶点对应的纹理空间中的坐标. 一般纹理空间中的坐标都在[0, 1]之间的, 例子中可以该成[0, 2]或[0, 4]之间, 看看物体的纹理又会如何.
---------------------------------------------------------------
// gamedef.h 中改了顶点的属性
#define D3DFVF_MYVERTEXTEX (D3DFVF_XYZ | D3DFVF_DIFFUSE | D3DFVF_TEX1)
struct MYVERTEXTEX
{
FLOAT x, y, z;
DWORD colour;
FLOAT tu, tv;
};
// d9texobject.cpp 中, 加入load texture file的函数
HRESULT CD9TexObject::SetTexture(LPCTSTR pName)
{
if (m_bInit)
{
return D3DXCreateTextureFromFile(m_pD3DDev, pName, &m_pD3DTexture);
}
return E_FAIL;
}
// d9texobject.cpp 中, 确定顶点在世界中的坐标的同时,
// 确定顶点在纹理空间中映射点的坐标,
// 我们只是简单将纹理图案映射在四边形的表面上
HRESULT CD9TexObject::UpdateD3DVertex()
{
LPVOID pV = NULL;
UINT nSize = 18 * sizeof(MYVERTEXTEX);
MYVERTEXTEX aVertex[] =
{
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 1.0f },
{m_fx - m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 0.0f },
{m_fx + m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0), 1.0f, 1.0f },
{m_fx + m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0), 1.0f, 0.0f },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 1.0f },
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 0.0f },
{m_fx + m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 255, 0), 1.0f, 1.0f },
{m_fx + m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0), 1.0f, 0.0f },
{m_fx + m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 1.0f },
{m_fx + m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 0.0f },
{m_fx - m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0), 1.0f, 1.0f },
{m_fx - m_fW, m_fy + m_fH, m_fz + m_fD, D3DCOLOR_XRGB(255, 0, 0), 1.0f, 0.0f },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0), 0.0f, 1.0f },
{m_fx - m_fW, m_fy + m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 0.0f },
{m_fx + m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(0, 255, 0), 0.0f, 1.0f },
{m_fx + m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 0, 255), 0.0f, 0.0f },
{m_fx - m_fW, m_fy - m_fH, m_fz - m_fD, D3DCOLOR_XRGB(255, 0, 0), 1.0f, 1.0f },
{m_fx - m_fW, m_fy - m_fH, m_fz + m_fD, D3DCOLOR_XRGB(0, 255, 0), 1.0f, 0.0f }
};
if(FAILED(m_pD3DVBuffer->Lock(0, nSize, &pV, 0)))
{
return E_FAIL;
}
MoveMemory(pV, aVertex, nSize);
m_pD3DVBuffer->Unlock();
return S_OK;
}
// d9texobject.cpp 中, 渲染时设置物体纹理
VOID CD9TexObject::Render()
{
if (m_bInit)
{
m_pD3DDev->SetStreamSource(0, m_pD3DVBuffer, 0, sizeof(MYVERTEXTEX));
m_pD3DDev->SetFVF(D3DFVF_MYVERTEXTEX);
if (m_pD3DTexture != NULL)
{
m_pD3DDev->SetTexture(0, m_pD3DTexture);
m_pD3DDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
}
else
{
m_pD3DDev->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_DISABLE);
}
m_pD3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
m_pD3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 8);
m_pD3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 14, 2);
}
}
6.4.2 说明
6.4.2.1 函数
一般纹理文件是单独的存放的, 这是要创建纹理用到了
HRESULT D3DXCreateTextureFromFile(LPDIRECT3DDEVICE9 pDevice,
LPCTSTR pSrcFile,
LPDIRECT3DTEXTURE9 * ppTexture);
函数从单独的纹理文件中创建纹理, 用这函数的好处是随时可改纹理文件但不用编译代码. 如果为提高速度, 可以将纹理加入可执行文件中作为资源, 这是使用
HRESULT D3DXCreateTextureFromResource(LPDIRECT3DDEVICE9 pDevice,
HMODULE hSrcModule,
LPCTSTR pSrcResource,
LPDIRECT3DTEXTURE9 * ppTexture);
在渲染时, 要告诉IDirect3DDevice9物体的纹理, 使用
HRESULT SetTexture(DWORD Sampler,
IDirect3DBaseTexture9 * pTexture);
现在的DirectX Graphics最多能使用8各纹理的多重纹理, 现在我们直用一个, 所以Sampler设为0.
6.4.2.2 纹理过滤器
纹理到物体表面的映射需放大或缩小, 过滤器的作用就是确定纹理的缩放变化的平滑等级的. 纹理过滤器是使用 IDirect3DDevice9 中的SetSamplerState函数来设置的.
我们将平滑等级按一般到高级列出, 同时也要注意, 平滑等级越高, 计算越耗时.
(1). 使用最近点采样技术(这是系统默认值)
SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_POINT);
SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_POINT);
(2). 使用线性过滤技术(建议使用, 以点的2X2区域线性插值)
SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
(3). 使用各向异性过滤技术
SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_ANISOTROPIC);
SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC);
这时需要设置 D3DSAMP_MAXANISOTROPY 的等级 , 默认为 1
SetSamplerState(0, D3DSAMP_ MAXANISOTROPY , 4);
还有D3DTEXF_PYRAMIDALQUAD和D3DTEXF_GAUSSIANQUAD过滤技术.
6.4.2.3 mip-map设置
mip-map的过滤器平滑等级和上面的相同, 也是通过SetSamplerState函数来设置. 系统默认是D3DTEXF_NONE, 即没有过滤. 同时需要设置mip-map的层数
SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_POINT);
SetSamplerState(0, D3DSAMP_MIPMAPLODBIAS, 4);
SetSamplerState(0, D3DSAMP_MAXMIPLEVEL, 4);
6.4.2.4 texture-address mode
纹理空间总是在[0, 1]之间的, 但是物体表面点映射的纹理坐标可大于1, 这时, 对于大于1的纹理映射可使用纹理重复, 镜像, 最近点采样, 边界点采样, 单次镜像等. 通过函数SetSamplerState设置
SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_CLAMP);
SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_CLAMP);
可以修改例子中物体表面点映射的纹理坐标可大于1, 然后改变texture-address mode观察物体表面纹理映射的区别.
下面的图是使用[0, 2]的镜像纹理技术的截图
第六集 小结
这一集我们学习了要进行DirectX Graphics 3D编程中的二维纹理映射技术, 纹理空间总是在[0, 1]之间.