日期:2014-05-17  浏览次数:20786 次

Windows 8 Directx 开发学习笔记(十一)地形纹理贴图

前一篇实现木箱贴图时,木箱的六个面都正好用一整张纹理图,即六个面的纹理坐标均在[0,1]内。然而在为比较大的模型贴图时,像山峰河谷模型,如果只用一张纹理图,那么每个三角形只得到几个纹理元素,无法为提供足够高的分辨率。这时可以在模型表面上平铺纹理贴图,像给墙面贴磁砖一样,只需要知道一个单位的贴图,就能铺满整个表面,从而获得较高的分辨率。实现起来其实很简单,只要变换纹理坐标的范围,同时将纹理寻址模式设置为重复即可。

纹理坐标范围变换有两种方式,很简单,如图1:


其中x,y是原始纹理坐标,为了能与4*4的变换矩阵相乘,将其扩展为4元向量。等号左边是一种变换方式,使用变换矩阵进行纹理坐标变换,可用GPU计算。等号右边是第二种变换方式,直接生成变换后的纹理坐标,由CPU计算。本文选择的是第一种变换方式。

下面就开始给学习笔记(九)实现的山峰和水面模型贴图。

依然是先修改HLSL代码。在顶点着色器的输入输出结构中添加用于存储纹理坐标的成员tex。并在ModelViewProjectionConstantBuffer里添加texTransform成员,用于存储缩放和平移变换矩阵。

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
    float2 tex : TEXCOOD;
};
 
struct VertexShaderOutput
{
    float4 posH : SV_POSITION;
    float3 posW   : POSITION;
    float3 normal : NORMAL;
    float2 tex : TEXCOOD;
};
 
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
    matrix texTransform;
};

变换纹理坐标时只要在main方法中添加下面的语句即可:

// 纹理坐标变换
output.tex = mul(float4(input.tex, 0.0f, 1.0f),texTransform).xy;

在像素着色器中要定义采样器和纹理资源,并更改输入结构体,与顶点着色器的输出对应。

SamplerState samplerLinear : register(s0);
Texture2D texDiffuse : register(t0);
 
struct PixelShaderInput
{
    float4 posH : SV_POSITION;
    float3 posW : POSITION;
    float3 normal : NORMAL;
    float2 tex : TEXCOOD;
};

为了减少更改,在main方法中还是使用Material结构进行光照计算,只是Diffuse成员由纹理采样获得。

Material textureMat = gMaterial;
textureMat.Diffuse = texDiffuse.Sample(samplerLinear,input.tex);

         HLSL代码修改至此完成,接下来就要修改C++代码。

结构先行,修改Directl3DBase.h中的顶点结构体和常量缓冲区定义,保证它们的结构和顶点着色器中定义的一致。

struct VertexPosition
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT2 tex;
};
struct ModelViewProjectionConstantBuffer
{
    DirectX::XMFLOAT4X4 model;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
    DirectX::XMFLOAT4X4 gTexTransform;
};

由于顶点结构体改变,HillModel和WaterModel里顶点初始化代码需要修改。HillModel的Initialize方法里,顶点初始化代码修改如下:

const float dx = 1.0f;
const float du = 1.0f/(xRange-1);
const float dv = 1.0f/(zRange-1);
 
VertexPosition *Vertices = new VertexPosition[xRange*zRange];
 
for(int row=0; row<zRange; ++row)
{
    float zPos = row*dx;
 
    for(int col=0;col<xRange; ++col)
    {
       float xPos = col*dx;
       float yPos = 0.3f *(zPos*sinf(0.1f*xPos) + xPos*cosf(0.1f*zPos));
       Vertices[xRange*row+ col].pos = XMFLOAT3(xPos, yPos, zPos);
       Vertices[xRange*row+ col].normal = GetHillNormal(xPos, zPos);
       Vertices[xRange*row+ col].tex.x = row*du;
       Vertices[xRange*row+ col].tex.y = col*dv;
    }
}

dudv可保证生成的纹理坐标正好在[0,1]范围内,目的是方便理解缩放比例,并不是必须的。WaterModel生成纹理坐标的过程与上面类似,是在Update方法中进行的。

// 更新顶点缓冲区
const float du = 1.0f/xRange;
const float dv = 1.0f/zRange;
 
for(uint32 i = 0; i < m_vertexCount; ++i)
{
    vertex[i].pos =mCurrSolution[i];
    vertex[i].tex.x= (i/xRange)*du;
    vertex[i].tex.y= (i%zRange)*dv;
    vertex[i].normal= mNormal[i];
}

这里虽然每次更新都要重新计算一样的纹理坐标,效率很低,不过鉴于规模小,方便理解就直接修改了。

完成以上工作后就可以开始载入纹理。与木箱贴图一样,向工程中添加WICTextureLoader的头文件和源文件。然后在Renderer类里添加以下成员,保存两种纹理资源,分别代表陆地和水面。