您的位置:首页 > 编程语言

【图形学与游戏编程】开发笔记-基础篇7:纹理映射

2016-10-26 11:41 393 查看
(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.NET/pancy12138)

上上一次(没错,就是往前数两次

)的教程为大家讲解了3D游戏是如何通过光照模拟的算法来对物体进行着色的,那么大家如果动手实现了那个亮晶晶的3D球的话,一定会对这种新奇的游戏设计技术灰常的感兴趣吧,当然接下来我们就得讨论上一次的着色方法的不足之处了。大家肯定会发现,通过这种“设置材质->设置光照->直接计算”的渲染方法虽然看上去比较直观,效果也显而易见。但是我们发现我们也只是在实现这种“纯色”的物体的时候好用,但是实际上,我们的生活中大部分物体并不是这种“纯红色”或者“纯蓝色”的,甚至像彩色书本或者广告纸,他们上面因为人为的添加了染料或者其他东西,整个物体的表面可以说聚集了各种各样的完全不同的漫反射材质。而且除了漫反射材质,有些合成物体由于组成的物质本身就差异很大(比如衣服和拉链),甚至导致同一个物体表面上各个部分的镜面反射材质都相差甚远。那么如何才能描述这种奇特的物体呢,对于我们来说,如果两个部分的材质和纹理不同,那么要么在顶点的格式里面记录下其材质,要么分成不同的drawcall来进行渲染。前者会浪费大量的位宽,后者会浪费大量的drawcall,这都是不可取的方法。因为材质的种类非常的多,大家现在了解的可能只有漫反射和镜面反射,事实上,真正的渲染过程还会有类似于法线啊,深度啊,反射系数啊等等用于描述材质的东西,如果要做到彻底的根据材质分解物体的话,那工作量之大是难以想象的,因此,人们提出了“纹理映射”的方法,来将复杂的材质存储在图片上,然后把图片整个的贴到三维空间里面的三角面上以代替复杂的材质系统。

事实上,纹理映射属于当今光栅图形学的几大“超级大杀器”之一,一大堆的快速优化和渲染算法都是以纹理映射作为基础的,因此,这一部分的知识非常的重要,应该算是图形设计者必须要熟悉的算法设计方式。接下来我们来探究一下,纹理映射是如何工作以及实现的。

首先,我们要想做纹理映射,至少得有一张图片,也就是决定究竟应该往几何体上贴上什么样子的东西。然后,我们还必须要指定这张图片的映射意义,也就是说这张图片究竟是作为什么材质被导入的,比如漫反射,镜面反射,直接反射等等。这里指定其类型是不需要进行任何编程的,这只是逻辑上的指定而已。有了这张被制定好用处的图片,我们就可以想办法把它贴到物体上面了。现在我们来分析一个比较简单的问题,图片上的颜色信息记录在像素点上,而几何体是一个一个的三角形。如何才能把这张图片,贴在每一个三角形上面呢?这里我们所使用的方法,就是所谓的UVN式的纹理映射方法。大家如果之前学习过建模知识的话,在使用3DSmax或者maya等建模软件的时候就会了解到“展UV”这种纹理制作的操作。这里的UV就是我们在程序里面做纹理映射的方法。也就是每一个三角形的顶点现在不仅仅要存储其位置和法线信息,还要多存储一个uv坐标用来标识其在纹理图片上的映射位置,这个坐标是二维的,理解起来也很简单。如下图:



只要我们定义了一个三角形的三个顶点在图片中对应的位置,我们就可以把图片中对应的区域映射到3D空间的三角面上面。这个时候就有人说了,这种算法听上去还是不错的,但是好似有很多漏洞。比如说首先,你这里只有三个点的坐标,点的内部怎么办,他们要想填充也需要很多的坐标才行。然后就是三角形是矢量的图形,它是可以无限放大缩小的,那你这张图片是光栅图形,根本没法放大缩小,怎么可能进行完美的映射算法呢?

首先我们先说点的内部的问题。如果对这个问题有疑惑的童鞋一定是对光栅化管线了解的还不够多,实际上当我们在投影变换结束后对三角形进行光栅化插值的时候,所有的顶点结构体变量都会根据“硬件线性插值算法”获得其内部点的数据,也就是说,我们只要保证顶点的数据是“可插值的”就可以了。那么至于什么信息是可插值的神马是不可插值的,我们前面其实都已经说过了,凡是矢量信息均可以插值,比如坐标,法向量等等。而凡是标量信息均不可插值,比如距离,颜色等等。因此,我们的UV坐标是可插值的,所以可以借助光栅化的过程为内部的三角形进行UV坐标的填充工作。接下来是另一个问题,也就是图片是不可以放大缩小的,而三角形是可以放大缩小的,如何才能处理并解决他们的映射问题呢?这里我们注意,我们的UV坐标并不是根据图片分辨率来决定的,比如说一张1366*768的纹理和一张1920*1080的纹理,他们的UV范围都是[0,1]。也就是说我们并不管图片的原始分辨率是多少,统一认为他们的横纵坐标均属于[0,1]范围内。接下来,我们要对其采样的时候,根据其UV坐标比如[0.221,0.134],去图片上寻找对应数据所对应的像素颜色。当然,很明显一个任意的小数不会都对应图片上的像素点。这也就是我们前面提到的问题,那么接下来我们要做的就是根据这个坐标来映射像素,映射的方法有很多,比如最简单的就是最邻近映射,也就是找离它最近的一个像素点进行映射,当然这是最水的粗糙办法,其他的还有双线性插值映射以及各向异性差值映射,这些都是通过插值算法来解决不能一一对应到像素点的问题。这样我们就可以解决一些图片在放大缩小过程中的映射更改情况。还有一个问题就是在放大缩小的过程中由于同一个部分有可能因为映射不断地发生变化而产生动态的锯齿抖动现象。我们需要为同一张纹理做mipmap得到很多张不同分辨率的问题,这样在映射的过程中不至于因为放大缩小导致剧烈的抖动现象。

上面说了这么多,接下来我们就要进行正式的纹理映射的实现讲解了,首先我们来看的就是纹理的资源创建过程,也就是如何才能创建一份纹理资源并将其保存在GPU的显存上面备用和访问的问题。其实,纹理资源(texture)和缓冲区资源(buffer)同属于一种东西,也就是显存上的资源,这种资源在directx里的保存方式与buffer资源同属于一个基本的类,这个类叫做id3d11resource。也就是说两个人的数据保存方式是相同的,但是他们一个作为纹理资源,一个作为缓冲区资源,在保存格式上还是有些不同的。因此,在继承了Id3d11resource的基础上,纹理资源产生了一个子类叫做Id3d11tex2D(或者1D,2Darray,3D等),而缓冲区资源产生了一个子类叫做Id3d11buffer。这两个子类在显存上存储数据的方式是相同的,但是在访问格式上有很多的不同。而因此,当我们在创建tex2D的时候,也应该使用一个其特有的格式结构体来进行创建,也就是D3D11_TEXTURE2D_DESC:

<span style="font-size:18px;">typedef struct D3D11_TEXTURE2D_DESC
{
UINT Width;
UINT Height;
UINT MipLevels;
UINT ArraySize;
DXGI_FORMAT Format;
DXGI_SAMPLE_DESC SampleDesc;
D3D11_USAGE Usage;
UINT BindFlags;
UINT CPUAccessFlags;
UINT MiscFlags;
} 	D3D11_TEXTURE2D_DESC;</span>
这种格式对应了创建buffer的时候所用的D3D11_BUFFER_DESC,那么接下来我们继续分析这种格式的内部成员的含义。前两个是纹理的大小,非常简单。第三个是mipmap的数量,也就是我们前面所说的解决抖动所使用的算法需要创建几层不同分辨率的纹理。第四个参数是纹理数组的数量,这个参数用于实现GPU上的纹理数组功能,这个问题属于比较特殊的问题,也就是如何在一个drawcall里面使用大量纹理的问题,单纯的纹理映射数组并不能支持变量下标访问,也就是说你在GPU上定义了texture2D
rec[100],在使用的时候只能用rec[0]或rec[1]而不能说用rec[i]这种写法。因此我们需要在这里指定数组的大小以实现可以使用下标访问的纹理数组,不过这种做法属于比较高级的内容,这一次的博客大家可以先忽略。第五个参数是指纹理的格式,也就是RGBA16,RGBA32等标识一个像素到底占多大的空间以及用什么格式保存。第六个是指采样格式,也就是MSAA抗锯齿的信息如4X抗锯齿或者8X抗锯齿。第七个是使用方式,一般来说是default也就是默认方式,也有的时候我们会更改成其他的格式将纹理数据采样回cpu。第八个是绑定格式,这个格式决定了纹理是否能够被当做渲染目标,是否可以充当GPU常量等等。接下来的一个变量就是指定其是否可以被CPU访问,也就是说是否可以把显存资源拷贝回cpu或者被CPU修改,一般来说在播放视频的时候可能会用到。最后一个是用于标记cubemap等多张纹理资源的格式,一般单独的纹理都是直接设为零的。下面给大家展示创建一个纹理资源的过程:

<span style="font-size:18px;">HRESULT hr;
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = map_width;
texDesc.Height = map_height;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
ID3D11Texture2D* Tex = 0;
hr = device_pancy->CreateTexture2D(&texDesc, 0, &Tex);
if (FAILED(hr))
{
MessageBox(0, L"create texture error", L"tip", MB_OK);
return hr;
}</span>
上面就是一个标准的纹理创建方式,那么当我们创建了一个纹理,如何才能在GPU上,或者说在shader里面去访问它呢?这个问题就简单多了,当我们拥有了一个纹理资源之后,我们只要再根据这个资源创建一个shader上的资源访问器Id3d11shaderresourceview就好了。当然啦,很多人会问为什么不能在GPU上直接访问纹理资源呢,要这个shaderresourceview干什么?那么这里我们解释一下,因为纹理的本质实际上和buffer一样,是一堆存储在显存上的线性存储的数据,我们在CPU上可以很方便的对内存做各种各样的操作,但是在GPU上就很难了,所以HLSL以及directx为我们在封装了最基本的数据IO操作,并为我们暴露了一些常用的数据IO接口,比如要想把纹理图片作为GPU常量缓冲区使用就可以依靠shaderresourceview来读取数据,如果希望作为渲染目标来访问就可以使用rendertargetview(比如渲染到纹理),当然还有很多其余的用处,比如depthstencielview以及unorderedaccessview等,这些东西叫做数据访问接口。这里举一个最简单的例子吧。如果我们假设texture资源是一块malloc出来的void*的数据texdata,那么各种各样的view就是各种不同类型的指针,比如int*
shaderresourceview = (int*)texdata,或者char* rendertargetview = (char*)texdata。数据还是那一块数据,但是由于用来访问数据的指针不同,导致我们能够完成的工作也是各有不同。当然在CPU上面我们操作内存比较容易(或者说用C/C++操作起来比较容易),但是在GPU上没有那么容易,所以必须要创建一大堆的接口来封装这些访问格式的问题。当然,这只是为了让大家理解的更为清晰所做的工作,真正使用的时候其实还是很简单的,下面就是创建一个shaderresourceview的方法:

<span style="font-size:18px;">ID3D11ShaderResourceView *ambient_tex0;
ID3D11Texture2D* ambientTex0 = 0;
hr = device_pancy->CreateTexture2D(&texDesc, 0, &ambientTex0);
if (FAILED(hr))
{
MessageBox(0, L"create ambient map texture1 error", L"tip", MB_OK);
return hr;
}
hr = device_pancy->CreateShaderResourceView(ambientTex0, 0, &ambient_tex0);
if (FAILED(hr))
{
MessageBox(0, L"create ambient map texture1 error", L"tip", MB_OK);
return hr;
}</span>
可以看到如果没有特殊的要求(比如有些纹理访问视图需要修改UNORM格式为指定格式)创建流程非常简单,只要先创建一个texture2D,再根据这个纹理资源就可以创建访问视图了,这里注意,创建完访问视图如果以后不需要texture2D资源的信息的话,可以直接释放tex2D,而访问视图里面仍然还保存着纹理的资源信息,这里其实就相当于把指针的信息删除,如果其内部的存储资源没有被删除的话,保留另一个同样指向这块内存的指针也仍然可以访问这一片内存。而directx是基于COM的设计方式的,会自动记录一个资源有多少个指针指向它,当完全没有指针指向它的时候才会彻底释放显存资源。

我们在大部分时间希望能够从文件里面导入一些纹理资源,也就是directx里面的dds纹理资源。这个导入方法要比上述的纹理创建方式更加的简单,只需要一句话就可以搞定了。但是这个纹理导入函数不属于dx11核心库,我们需要导入directx xtk库来完成外部文件的导入,库的地址及使用方法在前面的基础课程第一课就已经提到了,大家可以自己去把这个库配置一下,配制完毕之后可以使用下面所说的方法来进行文件纹理导入:

hr_need = CreateDDSTextureFromFile(device_pancy, texture_name, 0, &matlist_need[i].tex_diffuse_resource, 0, 0);
hr_need = CreateDDSTextureFromFileEx(device_pancy, contex_pancy, texture_name,
0, D3D11_USAGE_DEFAULT, D3D11_BIND_SHADER_RESOURCE, 0, 0.0f,
false, NULL, &matlist_need[i].tex_diffuse_resource);上面的函数是最基本的创建函数,这种创建方法将得到一个不做自动mipmap的纹理资源,而下面的方法将得到一个经过mipmap的纹理资源。大家可以根据纹理的格式来选取创建的方法,这种方法创建起来非常简单,里面的参数和上面我们介绍的也没多大差别,大家看一看估计就能看懂了。
接下来我们分析如何才能将其应用到shader里面,这一步的话其实是工作于pixelshader的,因为前面我们也说了,要借助光栅化插值来得到填充后的UV坐标,那么在shader里面,我们只需要将插值完成之后的shader坐标拿出来,然后根据这个坐标以及纹理采样格式,就可以从相应的纹理图片中取出像素的颜色出来:

<span style="font-size:18px;">SamplerState samTex_liner
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
Texture2D texture_light_diffuse; //漫反射光照贴图
struct Vertex_IN_bone//含法线贴图顶点
{
float3 pos : POSITION; //顶点位置
float3 normal : NORMAL;
float2 tex1 : TEXCOORD; //顶点纹理坐标
};
struct VertexOut
{
float4 position : SV_POSITION; //变换后的顶点坐标
float2 tex : TEXCOORD; //纹理坐标
};
float4 PS(VertexOut pin) :SV_TARGET
{
float4 tex_color = texture_diffuse.Sample(samTex_liner, pin.tex);
return tex_color;
}</span>其中samplerstate就是我们前面所述的采样方式,这里我们使用的是线性采样方式,其他的还有最近点采样以及各向异性采样。下面的UV格式主要是定义了纹理的拓展格式,也就是对于超过[0,1]边界的坐标如何处理的问题,常见的比如截断,重复,拉伸等等。这里我们写了一个非常简单的shader,就是单纯的吧纹理颜色显示出来,效果如下:



上图我们直接把saber的图片作为立方体最终的颜色输出到了屏幕上,整个过程还是很简单的。当然,目前我们只需要快速的了解这种映射算法就可以了,后面我们会提到更多高级一些的应用,这个算法的重要性到时候会体现的淋漓尽致。那么今天的教程就算是结束了,seeyou
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐