Category Archives: Computer Graphics

Unproject

Unproject即反投影,将一个坐标从投影空间中反投影到视图空间。对于Project操作,很容易,直接用投影矩阵乘以视图空间的坐标即可,即: posProj = posView * matrixProj 其中设posView = (x, y, z, 1)是视图空间的某一位置点的坐标,posProj = (x’, y’, z’, w’)为投影空间中该点的坐标,matrixProj为投影矩阵。我们知道,因为投影矩阵的特殊性,有w’ = z,即视图空间的深度值存储在投影空间的w值中,这样posPorj的xyz坐标值在除以了w值之后,就有了近大远小的透视效果。注意:只有在除以了w值之后才会有,x’/w’,y’/w’∈[-1,1]且z’/w’∈[0,1],即在那个所谓的半个正方体中了。 那么对于Unproject操作呢,是不是直接posView = posProj * inverse( matrixProj )就可以了呢,如果知道x’y’z’w’值,当然是正确,但关键问题是,大多数情况下,我们所知道的只是x’/w’,y’/w’以及z’/w’的值。 DX提供了一个函数D3DXVec3Unproject,可以做反投影,不过它只能在C++代码里使用(shader里没法使用),而且需要注意的是,这个函数是对屏幕空间的坐标进行的反投影(所以这个函数还需要传入一个viewport参数),务必不要将投影空间的坐标作为参数传递给这个函数。 我需要写一个自己的反投影函数,不妨从上面的这个公式推导:posProj = posView * matrixProj,因为我们现在已知的是x’/w’,y’/w’以及z’/w’的值,不妨设posProj’ = (x’/w’, y’/w’, z’/w’, 1 ),所以这个公式改为:posProj’ * w’ = posView * matrixProj。 因为一般投影矩阵是像这样一种形式: 于是有拆分posProj’ * w’ = posView * matrixProj后,有: posProj’.x * w’ [...]

顶点法线

本来以为前一段时间写的3d max导出插件已经没有什么大的问题了,但有没有问题,完善不完善,真不是凭感觉就OK的,必须有足够广泛的测试用例测试后,才能够证明。 这不,最近开始研究并写一些光线跟踪的例子了,这当然离不开模型的法线,于是在进行当中我就发现,我的导出插件所导出的法线是不完善的。 在原来的插件中,我让顶点的法线就直接等于其所在的三角形面的面法线(多个面共享1个顶点,则会导出多个顶点,这些顶点位置相同法线不同),但对于光照模型,面法线只能支持到Lambert,为了支持Gouraud, Phong等其他模型,则必须使用顶点法线(为什么?稍后解释)。 这里提出了顶点法线和面法线,首先需要清楚的理解并区分这两个概念。 面法线很容易理解,即垂直于三角形面的一条法线。那顶点法线又从何而来呢,严格的从法线的定义上来说,其实顶点是不存在法线的,那为何又有顶点法线这个概念的。让顶点也拥有法线,是为了在光照计算时,能够在多面体的表面获得一种平滑的效果。 更具体的说,如果不使用顶点法线(就像我的3dmax导出插件原来就直接让顶点法线等于其面法线一样),一个三角形面的三个顶点的光照计算按照其所在面的面法线来计算,因为三个顶点的法线相同,则与光照方向求点积之后的结果也相同,这样三个顶点将会获得相同的光照效果,之后,光栅化再怎么插值,整个表面也都只是一种光秃秃的效果。(如图1) 而如果使用顶点法线,同一个面的三个顶点的法线就不一定相同了,这样通过光栅化后,就能在多面体的表面获得一种平滑过渡的光照效果。(如图2) 于是,弄清楚了这个后,我需要再次修改我的3dmax导出插件了,需要计算并生成新的顶点法线。那顶点法线该如何计算呢,对于这个我在这里就不详述,Max Wagner的《Generating Vertex Normals》这篇文章说的很详细清楚,从最简单的到逐步优化的生成算法一一都有介绍,可以去google找一下。 而3dmax的SDK开发文档里对顶点法线的计算也有介绍,3dmax提供了一个smoothing group的概念,这对于像立方体盒子这种表面并不是平滑过渡的模型,计算它们的法线将会带来很大的帮助。Wagner的文章里也说了,对于像立方体这样的模型,顶点法线不能简单的等于(共享该顶点的面的)面法线的平均值,因为这些面之间的过渡并不平滑。按照3dmax的概念,这些面不属于同一个smoothing group。何为smoothing group,这是3dmax根据表面之间的平滑过度情况,进行的分组。比如立方体,因为6个面之间两两都是相互不平滑的,所以一共会有6个smoothing group。每个面所属的平滑组的ID,程序员是可以直接读出的。 下面这两张图,是我在修改前和修改后的光照效果,对比很明显。 图1:基于面法线的光照 图2:基于顶点法线的光照

诡异的画面

对shader文件做了一些修改,但还只是简单的渲染一个带纹理的盒子。出来的结果居然是这样的诡异,荧光闪闪,还带着雪花满盒子飘…… shader的错误的确很难查找,尤其是在编译通过只剩下逻辑错误的时候。最后错误的原因却又是如此的简单,我把PS的返回值写成了float,而应该是float4,我不小心漏掉了4,于是造成了这样诡异却绚烂的艺术效果,留文以作纪念。

Some Tests

在最近的项目中,尝试进行了一些小实验,并记录了数据作为比较。实验机器的显卡是NVidia Geforce 8800 GT。 1. Skinning                           skinning on CPU                           skinning on GPU 从图中可以清晰的看出,对于骨骼蒙皮计算,CPU和GPU的差距可见一斑,GPU比CPU要快上10倍之多!这就是GPU并行计算的魅力! 2. Instancing & Stream Output 人物模型延用上面的这个模型(该模型差不多有1300个面),采用instancing技术渲染上千人(所有的Instance的动画在每一帧保持一致),并且实现了SO的版本,对它们的FPS进行比较。 Instancing without SO          1,000 Instances              2,000 Instances             3,000 Instances Instancing with SO          1,000 Instances              2,000 Instances             3,000 Instances 因为所有的Instance的动画在每一帧都是一致的,所以如果不使用SO技术,则不得不对每个instance都需要进行一次蒙皮动画,这显然是一种浪费。Stream Output技术使得所有的Instance的蒙皮动画只需要执行一次,所以在效率上得到了一定的提高,如下表。 Number of Instances FPS(Without SO) FPS(With SO) 1,000 [...]

Get streaming output statistics

有时候需要知道stream output了多少个数据,这需要借助于DX10中的ID3D10Query,步骤如下: 创建D3D10_QUERY_DESC结构,设置D3D10_QUERY_DESC::Query为D3D10_QUERY_SO_STATISTICS,表明要调查的是SO的数据,设置D3D10_QUERY_DESC::MiscFlags为0; 通过ID3D10Device::CreateQuery()创建ID3D10Query; 用ID3D10Query::Begin()和ID3D10Query::End()函数包裹需要调查的SO代码; 通过ID3D10Query::GetData()获取SO的统计数据,填充在一个D3D10_QUERY_DATA_SO_STATISTICS 示例代码如下: // 创建ID3D10Query ID3D10Query *d3dQuery; D3D10_QUERY_DESC d3dQueryDesc; d3dQueryDesc.Query = D3D10_QUERY_SO_STATISTICS; d3dQueryDesc.MiscFlags = 0; m_pD3DDevice->CreateQuery( &d3dQueryDesc, &d3dQuery ); // …… …… // 统计SO信息 d3dQuery->Begin(); // ……draw something with SO…… d3dQuery->End(); // ……最好在这里放置一些代码,填置CPU的空闲…… // 获取SO信息 D3D10_QUERY_DATA_SO_STATISTICS soData; while ( S_OK != d3dQuery->GetData( ( void* )&soData, sizeof( soData ), 0 ) ); [...]

ConstructGSWithSO

如果要使用SO(Stream Output),则在shader中必须使用ConstructGSWithSO函数来构造SO的GS,ConstructGSWithSO有两个参数。 VertexShader/GeometryShader – shader变量,因为SO的数据可以来自GS,也可以来自VS(如果GS为NULL),所以该参数可以是一个VertexShader,也可以是一个GeometryShader。(shader变量一般通过CompileShader函数获得) Semantics – 描述SO的数据的semantic,该semantic须与上一个参数(shader变量)的shader函数的输出相一致。这个参数的格式要求比较变态,所以举例子说明比较好。 现有一个VS如下,需要SO它的输出数据: struct VSInput { ……(略) }; struct VSOutput { float3 pos : POSITION; float2 tex : TEXCOORD; }; VSOutput VS VSInput { VSOutput o; ……(略) return o; } 那么ConstructGSWithSO函数应该这么写: GeometryShader gsSkinningSO = ConstructGSWithSO( CompileShader( vs_4_0, VSSkinning_SO() ), “POSITION.xyz; TEXCOORD.xy” ); 看粗体部分的第二个参数,首先是双引号的字符串形式的,双引号内逐个列出了VSOutput的semantic,每个semantic同时要用.xyz,.xy这样的形式标明出它的维数(特别注意,如果只是一个float或uint的一维数据,同样也需要用.x来标出它是一维的!)。各个semantic之间用分号隔开,但注意,最后一个semantic后面不需要加分号(不然创建shader失败!)。

关于对std::vector的遍历

上图是通过Instancing渲染了10000个低精度模型(低于200个面),有skin动画,但是人物没有AI。在实验室Geforce 8800GT的显卡上fps可以跑到80帧。 接着,我给人群加上点简单的AI,每个人物进行向一个目标点移动,于是我在每帧更新的时候添加了如下的这些代码。代码中,MeshInstance是instance的类,对应于一个人物实例,Move是移动人物实例的简单AI函数。对于所有的Instancing数据,我使用一个vector列表存储——m_vpInstancingData。代码通过vector的iterator(迭代器)遍历所有的instance,对每个instance执行Move函数。 for( vector< MeshInstance* >::iterator i = m_vpInstancingData.begin(); i != m_vpInstancingData.end(); i ++ ) { ( *i )->Move(); } 结果,加上这段代码之后,程序的效率居然骤降,如下图,fps只剩下44帧。这让我很是纳闷,因为在加上代码之前,CPU基本上是空闲的,因为所有的骨骼蒙皮+渲染全部都是GPU扛着,而在CPU加上一个10000次的for循环后,整体效率大打折扣。它的杀伤力有这么大么……CPU不太可能这么低能。 然后,我把(*i)->Move()这行代码注释掉了,仍然只有40多帧,即一个只是10000次的空for循环,仍然是效率的瓶颈,10000次的Move根本不是问题。 难道是迭代器在影响效率?于是把代码改成了下面这样,不用迭代器遍历vector,而直接使用数组形式访问vector来遍历。 for( int i = 0; i < NUM_INSTANCE; i ++ ) { m_vpInstancingData[i]->Move(); } 再次执行之后,fps又回归80帧!! 对于vector的遍历,一直以来一直都是通过迭代器遍历,但对于大型vector它居然会如此的影响效率,也是到今天才刚发现。但是STL的设计本来就是奔着方便高效的啊,迭代器不至于效率影响这么大吧,可能与Debug模式有关。于是,我做了一个小实验,代码如下。 #include <iostream> #include <vector> #include <time.h> using std::vector; using std::cout; using std::endl; #define MAX_NUM 1000000 [...]

关于D3D10_MAPPED_TEXTURE2D的RowPitch

当对一个ID3D10Texture2D进行Map操作时,会遇到D3D10_MAPPED_TEXTURE2D结构。该结构有一个属性是UINT RowPitch,如果没有很好的理解这个属性的含义,Map操作的结果很有可能是不对的。 这是DX10 SDK文档对RowPitch的解释: The pitch, or width, or physical size (in bytes), of one row of an uncompressed texture. 一个普通texture一行的字节总数就是它的RowPitch。但要特别注意的是:RowPitch并不就等于Texture2D的width乘以其每个纹元(texel)的字节数,即: RowPitch ≠ width* sizeof (pixelFormat) RowPitch总是大于等于后者,并且一般是等于一个2的n次幂。从上面也可以看出Pitch是以字节为单位,而width是以像素为单位的。 举例说明: 一个ID3D10Texture2D,创建它时所使用的D3D10_TEXTURE2D_DESC结构的Format属性是DXGI_FORMAT_R32G32B32A32_FLOAT,即一个纹元占16(4×4)个字节,Width属性是400,即每一行有400个纹元,则可计算每一行16 * 400 = 6400bytes。但如果对Texture2D进行Map操作时,可以发现,Map后所得到的D3D10_MAPPED_TEXTURE2D结构的RowPitch的值却是8192(是大于6400的最小的2的n次幂)。 所以在进行Map操作时,需要针对RowPitch,而不要依赖定义texture时的width。 但是,在fx文件中对纹理进行采样的时候,针对的则是width,见如下fx代码。其中offset是相对于起点的偏移量,g_TexWidth是一个二维纹理的width,可见为了获得offset在纹理中的uv坐标,计算都是相对于width的,这时不用考虑pitch。 uint baseU = offset % g_TexWidth; uint baseV = offset / g_TexWidth;