[ZZ] 基于DirectX shader的Per-pixel lighting实现

这个特效需要用到DX11 UAV吗?

 

http://blog.tianya.cn/blogger/post_show.asp?BlogID=510979&PostID=5665974

Introduction: 此文讲述了怎样使用DirectX中的Asm shader实现Phong着色模式,即per-pixel lighting效果。如想得到完整的工程代码和程序效果,请联系zengfancy@126.com。
   广为接受的shader model共有两种,是Gauround shading model和Phong shading model。DirectX内置渲染管道只支持Gauround模式,该着色模式的原理是:对片元顶点独立的进行光照计算,然后对片元内部像素点亮度进行插值计算。Gauround模式缺点显而易见,当片元很大时,不能精确的产生镜面光高亮点,甚至有时候还能屏蔽掉聚光灯的光照效果。比如,拿一个手电筒对准一面墙壁,只能照亮墙壁的中心部分,而不能照亮四个角落。而在Gauround模式下,矩形内部的所有像素点的光照都是根据顶点光照插值计算得来的,所以在这种模式的渲染效果下,墙壁的任何像素点都不能被聚光灯照亮,这显然是不符合实际情况的。
  在Phong着色模式下,可以避免这种缺点,它的核心思想是per-pixel lighting,即对每个像素点都进行光照计算,因此不会带来局部插值误差。遗憾的是,DirectX预定义渲染管道并不支持Phong模式。但现代显卡的可编程渲染管道可以帮助我们实现per-pixel lighting效果。
  
  下面详细讲述per-pixel lighting是怎么实现的。首先写出vertex shader 和pixel shader文件。
  // vertex shader text file: vertex.vsd
  vs_2_0
  
  // c0,c1,c2,c3 hold the worldviewproj matrix
  // c8,c9,c10,c11 hold the world matrix
  // c4 hold the ambient light color
  
  dcl_position v0 //input position
  dcl_texcoord v2 //input texcoordinate 
  
  m4x4 r0, v0, c0 //transformation with the worldViewProj matrix
  // r0.x = v0.x*c0.x + v0.y*c0.y + v0.z*c0.z + v0.w*c0.w
  // r0.y = v0.x*c1.x + v0.y*c1.y + v0.z*c1.z + v0.w*c1.w
  // r0.z = v0.x*c2.x + v0.y*c2.y + v0.z*c2.z + v0.w*c2.w
  // r0.w = v0.x*c3.x + v0.y*c3.y + v0.z*c3.z + v0.w*c4.w
  
  m4x4 r1, v0, c8 //transformation with the world matrix
  
  mov oPos, r0 //output the position ,which is transformed by the WORLD_VIEW_PROJ matrix
  mov oD0, c4 //output the diffuse color
  mov oT0, v2 //output the texture coodinate
  mov oT1, r1 //output the position, which is transformed by the WORLD matrix
  // vertex shader end 
  
  m4x4 r0, v0, c0 //transformation with the worldViewProj matrix
  mov oPos, r0 //output the position ,which is transformed by the WORLD_VIEW_PROJ matrix
  这两条语句与预定义的顶点处理并无区别,无非是将顶点位置坐标进行世界-视图-投影矩阵变换,然后保存到oPos 输出寄存器中。
  mov oT0, v2 //output the texture coodinate
  这条语句与预定义的顶点处理也无区别,将顶点纹理坐标保存到oT0寄存器中。
  m4x4 r1, v0, c8 //transformation with the world matrix
  mov oT1, r1 //output the position, which is transformed by the WORLD matrix
  关键在这两条语句。其意思是,将顶点位置进行世界变换后保存到oT1寄存器中。DirectX vertex shader支持8个纹理坐标寄存器(oT1—oT7, 它对应pixel shader中的纹理坐标寄存器t0—t7)。一般这8个寄存器不可能都用于保存纹理坐标,可以使用其中的一个或几个保存其它信息。其作用在写完pixel shader后再详细的说。
  
  // pixel shader text file: pixel.psd
  ps_2_x
  
  // c0 hold the light position
  // c1.xyz hold the light direction, while c1.w hold the light area, c1.xyz should be normalized, c1.w larger, 
  dcl v0 // ambient diffuse color
  dcl_2d s0 // sampler
  dcl t0 // texture0
  dcl t1 // texture1, in fact it hold the position value of the pixel, transformed by world matrix
  
  def c10, 0.0, 0.0, 0.0, 0.0
  def c11, 0.5, 0.5, 0.5, 1.0
  
  texld r0, t0, s0 // sample the tex color
  
  ////////calculate the diffuse color from the spotlight/////////////////////////////
  sub r4, t1, c0 // get the vector from light to pixel
  mov r4.w, c10.x // set the 4th component of r4 to 0
  nrm r5, r4 // normalize the vector
  dp4 r5.w, r5, c1 // don't care the value of c1.w, r5.w is equal to 0
  // if( r5.w > c1.w )
  // lit the pixel with spotlight
  // else
  // doesnt lit the pixel with spotlight
  
  sub r6, c1, r5 // if (r5.w > c1.w) then lit the pixel, r6.w < 0
  mov r6.x, r6.w
  mov r6.y, r6.w
  mov r6.z, r6.w
  mov r8, c10
  mov r9, c11
  cmp r7, r6, r8, r9 //
  add r1, v0, r7 // increase the diffuse color
  min r1.w, r1.w, c11.x // clamp the alpha value to 1.0
  
  //////////////////////////////////////////////////////////////////////////////////
  mul r2, r0, r1 // dot product the tex and the diffuse
  mov oC0, r2 // output the pixel color
  // pixel shader end
  先看下列声明语句
  dcl t0 // texture0
  dcl t1 // texture1, in fact it hold the position value of the pixel, transformed by world matrix
  上面讲过,t0,t1分别对应于vertex shader中的oT1和oT1。pixel shader中的所有输入寄存器,包括color register(v0)和texture coordinate register(t0—t7),其值是根据顶点输出寄存器中的值插值计算得来的。所以,t0保存的是像素点的纹理坐标,t1保存的是像素点经过世界变换后的位置坐标。
  sub r4, t1, c0 // get the vector from light to pixel
  mov r4.w, c10.x // set the 4th component of r4 to 0
  nrm r5, r4 // normalize the vector
  c0保存的是聚光灯光源位置(其值通过在主程序中调用SetVertexConstantsF(..)设定)。所以r4保存了由光源指向像素点的向量。先将向量的w分量清零(空间位置坐标有三个分量确定即可),然后单位化保存在r5中。
  dp4 r5.w, r5, c1 // don't care the value of c1.w, r5.w is equal to 0
  c1保存了聚光灯的方向向量,其该向量是单位向量。将r5和c1点乘后的值即为图4(聚光灯模型图)中的sin(a)值,sin(a)越大,说明该像素点越偏离聚光灯的中心光线。
  sub r6, c1, r5 // if (r5.w > c1.w) then lit the pixel, r6.w < 0
  mov r6.x, r6.w
  mov r6.y, r6.w
  mov r6.z, r6.w
  mov r8, c10
  mov r9, c11
  cmp r7, r6, r8, r9 //
  add r1, v0, r7 // increase the diffuse color
  min r1.w, r1.w, c11.x // clamp the alpha value to 1.0
  上述语句是判断sin(a)是不是超越了某个值,如果超越了,说明像素点不在聚光灯的照明范围之内,反之。
  Shader文件讲述完毕。
  下面讲述怎样创建vertex shader和pixel shader。
  首先做出声明:
  IDirect3DVertexShader9* g_pVertexShader = NULL;
  IDirect3DPixelShader9* g_pPixelShader = NULL;
  成功的创建设备后,即可创建shader:
  ID3DXBuffer* pShaderBuffer, *pErrorBuffer;
  //创建vertex shader
  D3DXAssembleShaderFromFileA( "vertex.vsd", NULL, NULL, D3DXSHADER_DEBUG, &pShaderBuffer, &pErrorBuffer );
  pd3dDevice->CreateVertexShader( (DWORD*)(pShaderBuffer->GetBufferPointer()), &g_pVertexShader );
  //创建pixel shader
  D3DXAssembleShaderFromFileA( "pixel.psd", NULL, NULL, D3DXSHADER_DEBUG, &pShaderBuffer, &pErrorBuffer );
  pd3dDevice->CreatePixelShader( (DWORD*)(pShaderBuffer->GetBufferPointer()), &g_pPixelShader );
  
  在渲染帧内(OnFrameRender()中)设置shader中的constant register。
  // Set vertex constants
  ///// mView mProj分别是当前的视口矩阵和投影矩阵
  D3DXMATRIXA16 planeWorld, planeWorldViewProjection, planeWorldT, planeWorldViewProjectionT;
  D3DXMatrixIdentity( &planeWorld );
  D3DXMatrixTranspose( &planeWorldT, &planeWorld);
  pd3dDevice->SetVertexShaderConstantF( 8, (float*)&planeWorldT, 4 );
  planeWorldViewProjection = planeWorld * mView * mProj;
  D3DXMatrixTranspose( &planeWorldViewProjectionT, &planeWorldViewProjection);
  pd3dDevice->SetVertexShaderConstantF( 0, (float*)&planeWorldViewProjectionT, 4 );
  D3DXVECTOR4 ambientLight( 0.12f, 0.12f, 0.12f, 1.0f );
  pd3dDevice->SetVertexShaderConstantF( 4, (float*)&ambientLight, 1 );
  // Set pixel constants
  D3DVECTOR vecPos = g_spotLight.m_light.Position;
  D3DVECTOR vecDirection = g_spotLight.m_light.Direction;
  D3DXVECTOR4 lightPos( (float)vecPos.x, (float)vecPos.y, (float)vecPos.z, 1.0f );
  pd3dDevice->SetPixelShaderConstantF( 0, (float*)&lightPos, 1 );
  D3DXVECTOR4 lightDirection( (float)vecDirection.x, (float)vecDirection.y, (float)vecDirection.z,0.0f );
  // Normalize the lightDirection vector
  D3DXVec4Normalize( &lightDirection, &lightDirection );
  // Set the spotlight's Phi_angle
  #define Phi_angle 0.3f
  lightDirection.w = 1.0f - Phi_angle;
  pd3dDevice->SetPixelShaderConstantF( 1, (float*)&lightDirection, 1 );
  
  之后便是set shader的工作了:
  // Set shader
  pd3dDevice->SetVertexShader(/*g_pVertexShader*/ NULL);
  pd3dDevice->SetPixelShader( /*g_pPixelShader*/ NULL);
  g_plane.OnRender( pd3dDevice );
  pd3dDevice->SetVertexShader(NULL);
  pd3dDevice->SetPixelShader(NULL);
  
  当然,release device 之前不要放了release shader。
  SAFE_RELEASE( g_pVertexShader );
  SAFE_RELEASE( g_pPixelShader );

 

 

转载  D3D 11中的MSAA    

http://www.cnblogs.com/effulgent/p/3289472.html

D3D11中的MSAA

 

    这两年我的工作都转到了D3D11,目前新出硬件几乎全部支持此标准,加上D3D11接口清晰,概念直观,等到windows7普及,想必未来都是D3D11的天下。最近时间较空,我陆续开始写些基础文章,希望对新学者有所帮助。但文章纯属我自己随意写写,错误肯定很多,请大家多多包涵。 

    所谓MSAA,就是让一个像素可以同时存储多个颜色,而最终的显示结果由多个颜色重建而成。具体存储颜色的数量由DXGI_SAMPLE_DESC中的Count来决定,其中的Quality则一般用来给硬件设计厂商作为非常规发挥的余地,比如NVDIA CSAA开启方式就是用Quality某些值来实现的。在D3D9中MSAA中即使一个像素被分成多个子片段来光栅化,但实际上覆盖此像素的每个三角形依然只执行一次pixel shader,子片段的位置只用来决定各种顶点属性的插值位置,以及进行覆盖率评定,这就是MSAA相比SSAA的最大不同之处,MSAA只会增加被多个三角形同时非完全覆盖时的计算率,而且不管其覆盖率有多高,每个三角形都只执行一次Pixel shader,并将Pixel shader返回的值存入相关覆盖子像素。需要注意的是,这里存在两个细节问题,一是Pixel shader输入的值如何插值而来(插值位置和插值算法);二是子像素到底位于像素中的何处,个数如何决定,是否每个子像素都对应一个color/z/stencil存储,如何将所有这些存储的子合成为最后的结果。对于第一个问题,就牵涉到MSAA光栅化规则的问题,注意如果没有任何特殊设置,对应像素Fragment的属性插值操作都将在像素中心位置上执行,MSAA中进行覆盖率评定时,很有可能三角形并未覆盖到像素中心位置,这就牵扯到一个外部插值(extrapolate)的问题,即插值位置根本就不在三角形内,很显然这样插值出来的属性结果是错误的,为了解决这个问题,D3D引入一个配置“centroid sample”,指定在rasterize时,相关属性进行插值的位置必须位于三角形与像素相交区域内,这个通常这个位置取在某个被覆盖的子像素位置,但并不保证永远不变,可能和具体硬件设计还有关,D3D11 reference rasterizer选择centroid sample位置的具体算法,可参考D3D文档。在D3D11中想要打开centroid sample,只需在对应pixel shader input attribute上加上centroid modifier即可;属性的插值算法,就是如何用三个顶点attribute值,以及中间点A的位置,使用某种算法插值出attribute在中心点A上的值,D3D中最常用的就是带透视矫正的线性插值(linear),所有attribute默认都使用此算法插值(linear modifier),当然D3D11还提供其他几种插值方式:nointerpolation(就是不插值,使用三角形中第一个顶点的属性值作为Fragment属性值),noperspective(不带透视矫正的线性插值,只使用屏幕2D坐标位置进行插值计算),sample(在每个子像素位置进行插值)。这些modifier可以加在PS input attribute前面,不过使用起来还是有些限制和规则,比如centroid、sample明显只能在MSAA模式下才能起作用,因为普通模式下不存在非中心覆盖和子像素位置问题;而centroid很显然也不能同nointerpolation一起使用,更多信息还请参考DX文档,毕竟知道这些背后原理后,更好记忆和理解这些限制。现在讨论第二个问题,子像素分布在像素区域中的位置是因硬件设计而变的,D3D标准并没有规定具体分布的位置,而个数按道理上来讲就是DXGI_SAMPLE_DESC种的count所变量指定。是否每个子像素都会在RT surface上有相应的存储位置(color/z/stencil),这个就有点悬了,毕竟这个是要增加硬件成本的事,而且D3D标准也没强制,硬件厂商说:OK,我可以给你指定的覆盖点数,我也可以把这些点的位置进行精心设计分布,但我不一定会给每个点都分配实际的存储位置。比如CSAA就将子像素数和实际存储数分开来了,以此来节省存储和带宽,CSAA和16x实际上只有4个存储位置(但它确实有16个子像素),16个子像素(覆盖率判断)如何分享4个存储位置呢?答案是硬件设计有关。最后一个问题,每个像素中存储的多个值如何重建为最终结果?答案还是硬件设计有关,但我们可以自己resolve(http://mynameismjp.wordpress.com/2012/10/28/msaa-resolve-filters/)。

    在D3D11中是可以指定pixel shader进行per-sample excution的,这个和D3D9完全不同,在pixel shader input中指定SV_SampleIndex属性或为属性指定sample modifier都会打开pixel shader逐子像素执行(这个在CSAA中就有点问题了,因为CSAA并不为每个子像素分配独立的存储)。MSAA并不由Pipeline中的一个stage完成,而牵涉到rasterization、pixel shader、output merger三个stage,D3D11对MSAA的操作进行了空前的增强,可以获取sample index, coverage mask, sub pixel value, 以及pixel shader新支持的UAV,综合这些我们可以完成一些很特别的算法。需要注意的是用centroid sample或per sample execution后会带来一个问题,就是GPU的某些地方的导数计算可能有误,比如ddx ddy以及texture lod计算,因为三角形边缘像素的采样位置会被偏置到某个sample的位置,而不再是像素中心,这样2x2像素中,变量相差之后的值就不再是基于单位的屏幕空间坐标了,这样在三角形边缘的像素上计算变量的导数就会出现跳跃起伏,这样会使ddx ddy的结果产生异常,所以要么你能容忍或解决这个问题,要么就不要在centroid sample的属性上进行导数计算。

    pixel shader输出Z会给MSAA带来一些麻烦。如果pixel shader没有开启per sample exctution,但却输出了SV_Depth,这就产生一个问题,本来每个子像素在depth stencil buffer中都会输出各自独立的Z值,此Z值为光栅化时插值产生,因此每个子像素都有一个正确的Z值,但如果pixel shader人工输出了Z,而这个pixel shader只执行一次,这样被此三角形覆盖的所有子像素的Z值都将是这个单一值,此值为像素中心的Z值(没有开启centroid sample的情况下),这就会导致一个问题,所有先绘制了更近三角形的边缘像素都可能失去或产生错误的抗锯齿效果!(特别是在三角形连续交界处)请看下图,绘制顺序为红、蓝、绿,这些几何体的pixel shader都输出了SV_Depth。请注意某些边缘已经失去了抗锯齿效果。

另外D3D10引入一个新的概念ALPHA-TO-COVERAGE,以及一个SV_Coverage的pixel shader输出变量。注定要把MSAA玩出花来了!以8x的MSAA为例,在z/stencil/color buffer上每个像素均有8个子像素,如果开启了ALPHA-TO-COVERAGE,pixel shader输出的ALPHA值会被转为一个8阶的值,表示此Fragment在像素上的mask,这个主要是用来解决Alpha Test边缘锯齿问题,其原理就是将光栅化阶段产生的MASK A,AND ALPHA转化的MASK B,AND SV_Coverage MASK C。看下面的例子,三块完全重叠的面片,打开Alpha-To-Coverage,并且都输出0.5的Alpha值,从近到远分别为红、绿、蓝,发现完全不会有互相半透的效果,原因很简单,本例开的是8x msaa,0.5的ALPHA会被GPU转化为00001111B的MASK,红绿蓝三个Mesh都输出相同MASK的话,子像素的值会被最近的Mesh覆盖掉。

我们修改下输出的Alpha值,红色0.25,绿色0.5,蓝色0.75,当红绿蓝视距从近到远排列时,输出结果如下:

很简单,因为红色的MASK为00000011B,绿色为00001111B,蓝色为00111111B,互相重叠的部分,近的颜色将占据MASK相对应的子像素,较远的会被覆盖掉。如果我们再反过来看,让红绿蓝视距变为从远到近排列,结果就变成这样了:

原因大家可以自己分析。综上,Alpha-To-Coverage注定是个悲催的OIT技术!

更多MSAA资料

http://mynameismjp.wordpress.com/2012/10/24/msaa-overview/

 

posted @ 2014-07-06 18:46  Gui Kai  阅读(555)  评论(0编辑  收藏  举报