3D游戏引擎系列十二

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN课程视频网址:http://edu.csdn.net/lecturer/144

对于3D游戏产品都需要阴影技术的实现,阴影的运行效率也成为判定游戏研发技术水平的手段之一。游戏中实现阴影的方式有很多种,主要分三种:一种是对于静态物体比如建筑物可以使用LightMap渲染,将建筑的阴影直接渲染到地面上这种技术广泛应用在移动端,Unity引擎本身就提供了此功能。另一种是对于游戏中动态的物体,实现方式是在移动端或者在网页游戏中为了优化效率,直接用一张带有Alpha通道的贴图放到角色的下面,可以实时跟随角色移动。第三种实现方式是该书重点讲解的实时阴影渲染,实时阴影在PC端游特别是次时代网游中很常见,鉴于PC端硬件的强大处理能力,应用实时阴影技术对整个游戏场景进行渲染,为的是增加游戏场景的真实性。当然实时阴影技术的运用会对CPU和GPU有一定的消耗,所以对于实时阴影的渲染,可以通过摒弃掉不需要实时渲染的建筑物进行效率优化。实时渲染技术常用的是PSSM(Parallel-Split ShadowMap)算法,实现阴影的算法非常多的,我就不一一列举了。PSSM通过字面意思知道就是平行切分视锥,游戏中实时阴影的渲染效果如下图:


要实现如此的效果,得从PSSM实现的原理讲起,PSSM算法的核心就是把视椎体进行分割,然后分别渲染组合。语言讲解不如看图直观,先通过视锥体分割说起。效果如下图:


PSSM实时阴影的绘制首先需要灯光,在现实生活中,白天只有太阳出来了才可以看到影子。在虚拟世界中也是一样的,场景使用的是Directional(平行光)相当于现实世界的太阳光。上图左边部分显示的是视景体的投影,利用PSSM算法将其平行的分割成多个部分,然后对每个部分进行渲染,分割成的块数是可以自己设置的。右半部分是顶视角观看的分割效果,把物体分成三块进行实时阴影的渲染。渲染的计算是GPU中执行的,在GPU中执行的流程如下图:


上图的处理流程首先是场景中的灯光照射到需要投影的物体上,接下来程序对投影的物体顶点进行矩阵变换将其转换到投影空间中,再转换到裁剪空间进行视口的平行分割,最后将其分别渲染出来。渲染阴影流程讲完了接下来解决Shader渲染的问题,我们把平行分割的计算放到GPU中执行,需要编写Shader脚本文件,新建一个文本文件把其扩展名字改成.fx。Shader的完整内容如下:

float4x4 g_mViewProj;

void VS_RenderShadowMap(
  float4 vPos : POSITION,
  out float4 vPosOut : POSITION,
  out float3 vPixelOut : TEXCOORD0)
{
  // pass vertex position through as usual
  vPosOut = mul(vPos, g_mViewProj);
  // output pixel pos
  vPixelOut=vPosOut.xyz;
}

float4 PS_RenderShadowMap(float3 vPixelPos : TEXCOORD0): COLOR
{
  // write z coordinate (linearized depth) to texture
  return vPixelPos.z;
}

// This technique is used when rendering meshes to the shadowmap
// 
technique RenderShadowMap
{
  pass p0
  {
    // render back faces to hide artifacts
    CullMode = CW;
    VertexShader = compile vs_2_0 VS_RenderShadowMap();
    PixelShader = compile ps_2_0 PS_RenderShadowMap();
  }
}

float3 g_vLightDir;
float3 g_vLightColor;
float3 g_vAmbient;
float g_fShadowMapSize;
float4x4 g_mShadowMap;

// no filtering in floating point texture
sampler2D g_samShadowMap  =
sampler_state
{
  MinFilter = Point;
  MagFilter = Point;
  MipFilter = None;
  AddressU = Border;
  AddressV = Border;
  BorderColor = 0xFFFFFFFF;
};


void VS_Shadowed(
  in float4 vPos : POSITION,
  in float3 vNormal : NORMAL,
  in float fAmbientIn : TEXCOORD0,
  out float4 vPosOut : POSITION,
  out float4 vShadowTex : TEXCOORD0,
  out float fAmbientOut : TEXCOORD1,
  out float3 vDiffuse : COLOR0)
{
  // pass vertex position through as usual
  vPosOut = mul(vPos, g_mViewProj);

  // calculate per vertex lighting
  vDiffuse = g_vLightColor * saturate(dot(-g_vLightDir, vNormal));

  // coordinates for shadowmap
  vShadowTex = mul(vPos, g_mShadowMap);

  // ambient occlusion
  fAmbientOut = saturate(0.5f+fAmbientIn);
}

float4 PS_Shadowed(
  float4 vShadowTex : TEXCOORD0,
  float fAmbientOcclusion : TEXCOORD1,
  float4 vDiffuse : COLOR0) : COLOR
{

  float fTexelSize=1.0f/g_fShadowMapSize;

  // project texture coordinates
  vShadowTex.xy/=vShadowTex.w;

  // 2x2 PCF Filtering
  // 
  float fShadow[4];
  fShadow[0] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex).r);
  fShadow[1] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(fTexelSize,0)).r);
  fShadow[2] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(0,fTexelSize)).r);
  fShadow[3] = (vShadowTex.z < tex2D(g_samShadowMap, vShadowTex + float2(fTexelSize,fTexelSize)).r);

  float2 vLerpFactor = frac(g_fShadowMapSize * vShadowTex);
  float fLightingFactor = lerp(lerp( fShadow[0], fShadow[1], vLerpFactor.x ),
                               lerp( fShadow[2], fShadow[3], vLerpFactor.x ),
                               vLerpFactor.y);

  // multiply diffuse with shadowmap lookup value
  vDiffuse*=fLightingFactor;

  // final color
  float4 vColor=1;
  vColor.rgb = saturate(g_vAmbient*fAmbientOcclusion + vDiffuse).rgb;
  return vColor;
}

// This technique is used to render the final shadowed meshes
//
technique Shadowed
{
  pass p0
  {
    /	/ render front faces
	CullMode = CCW;
	  VertexShader = compile vs_2_0 VS_Shadowed();
	  PixelShader = compile ps_2_0 PS_Shadowed();
  }
}

理论讲了很多,Shader代码实现起来比较简单,为了消除阴影锯齿,使用了PCF Filtering过滤技术。其他的代码跟以前讲的很类似这里就不一一分析了。接下来通过C++函数接口将参数传递给Shader文件,C++代码核心函数实现如下所示:

void RenderScene(D3DXMATRIX &mView, D3DXMATRIX &mProj)
{
  // Set constants
  //
  D3DXMATRIX mViewProj=mView * mProj;
  _pEffect->SetMatrix("g_mViewProj",&mViewProj);

  _pEffect->SetVector("g_vLightDir",&_vLightDir);
  _pEffect->SetVector("g_vLightColor",&_vLightDiffuse);
  _pEffect->SetVector("g_vAmbient",&_vLightAmbient);
  _pEffect->SetFloat("g_fShadowMapSize",(FLOAT)_iShadowMapSize);

  // enable effect
  unsigned int iPasses=0;
  _pEffect->Begin(&iPasses,0);

  // for each pass in effect 
  for(unsigned int i=0;i<iPasses;i++)
  {
    // start pass
    _pEffect->BeginPass(i);
    {
      // for each subset in mesh
      for(DWORD j=0;j<_iMeshMaterials;j++)
      {
        // draw subset
        _pMesh->DrawSubset(j);
      }
    }
    // end pass
    _pEffect->EndPass();
  }
  // disable effect
  _pEffect->End();
}

该函数主要是将Shader文件中需要使用的参数通过C++代码传递给GPU进行渲染,在介绍PSSM原理时对物体进行Split操作。在C++中的函数如下所示:

void CalculateSplitDistances(void)
{
  // Reallocate array in case the split count has changed
  //
  delete[] _pSplitDistances;
  _pSplitDistances=new float[_iNumSplits+1];
  _fSplitSchemeLambda=Clamp(_fSplitSchemeLambda,0.0f,1.0f);

  for(int i=0;i<_iNumSplits;i++)
  {
    float fIDM=i/(float)_iNumSplits;
    float fLog=_fCameraNear*powf((_fCameraFar/_fCameraNear),fIDM);
    float fUniform=_fCameraNear+(_fCameraFar-_fCameraNear)*fIDM;
    _pSplitDistances[i]=fLog*_fSplitSchemeLambda+fUniform*(1-_fSplitSchemeLambda);
  }

  // make sure border values are right
  _pSplitDistances[0]=_fCameraNear;
  _pSplitDistances[_iNumSplits]=_fCameraFar;
}
最后将上述实现的两个关键函数在Render函数中调用,完成最终的代码实现。渲染函数如下所示:
void Render(void)
{
  // move camera, adjust settings, etc..
  DoControls();

  // calculate the light position
  _vLightSource=D3DXVECTOR3(-200*sinf(_fLightRotation),120,200*cosf(_fLightRotation));
  _vLightTarget=D3DXVECTOR3(0,0,0);
  // and direction
  _vLightDir=D3DXVECTOR4(_vLightTarget-_vLightSource,0);
  D3DXVec4Normalize(&_vLightDir,&_vLightDir);

  // calculate camera aspect
  D3DPRESENT_PARAMETERS pp=GetApp()->GetPresentParams();
  float fCameraAspect=pp.BackBufferWidth/(float)pp.BackBufferHeight;

  AdjustCameraPlanes();
  CalculateSplitDistances();
  // Clear the screen
  //
  GetApp()->GetDevice()->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, D3DXCOLOR(0.5f,0.5f,0.5f,0.5f), 1.0f, 0);
for(int iSplit=0;iSplit<_iNumSplits;iSplit++)
  {
    // use numpad to skip rendering
    if(GetKeyDown(VK_NUMPAD1+iSplit)) continue;

    // near and far planes for current frustum split
    float fNear=_pSplitDistances[iSplit];
    float fFar=_pSplitDistances[iSplit+1];

    // Calculate corner points of frustum split

    float fScale=1.1f;
    D3DXVECTOR3 pCorners[8];
    CalculateFrustumCorners(pCorners,_vCameraSource,_vCameraTarget,_vCameraUpVector,
                            fNear,fFar,_fCameraFOV,fCameraAspect,fScale);

    // Calculate view and projection matrices
    CalculateLightForFrustum(pCorners);


    // Enable rendering to shadowmap
    _ShadowMapTexture.EnableRendering();
    // Clear the shadowmap
    GetApp()->GetDevice()->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, 0xFFFFFFFF, 1.0f, 0);

    // Set up shaders
    // To hide artifacts, only render back faces of the scene
    _pEffect->SetTechnique("RenderShadowMap");

    // Render the scene to the shadowmap
    RenderScene(_mLightView,_mLightProj);

    // Go back to normal rendering
    _ShadowMapTexture.DisableRendering();
    /
    // At this point we have the shadowmap texture rendered.   //
    /

    // Calculate a matrix to transform points to shadowmap texture coordinates
    // (this should be exactly like in your standard shadowmap implementation)

    float fTexOffset=0.5f+(0.5f/(float)_iShadowMapSize);

    D3DXMATRIX mTexScale(   0.5f,               0.0f,      0.0f,   0.0f,
                            0.0f,              -0.5f,      0.0f,   0.0f,
                            0.0f,               0.0f,      1.0f,   0.0f,
                            fTexOffset,    fTexOffset,     0.0f,   1.0f );

    D3DXMATRIX mShadowMap=_mLightView * _mLightProj * mTexScale;

    // store it to the shader
    _pEffect->SetMatrix("g_mShadowMap",&mShadowMap);

    // Since the near and far planes are different for each
    // rendered split, we need to change the depth value range
    // to avoid rendering over previous splits
    D3DVIEWPORT9 CameraViewport;
    GetApp()->GetDevice()->GetViewport(&CameraViewport);
    // as long as ranges are in order and don't overlap it should be all good...
    CameraViewport.MinZ=iSplit/(float)_iNumSplits;
    CameraViewport.MaxZ=(iSplit+1)/(float)_iNumSplits;
    GetApp()->GetDevice()->SetViewport(&CameraViewport);

    // use the current splits near and far plane
    // when calculating matrices for the camera
    CalculateViewProj(_mCameraView, _mCameraProj,
                      _vCameraSource,_vCameraTarget,_vCameraUpVector,
                      _fCameraFOV, fNear, fFar, fCameraAspect);

    // setup shaders
    _pEffect->SetTechnique("Shadowed");
    // bind shadowmap as a texture
    GetApp()->GetDevice()->SetTexture(0,_ShadowMapTexture.GetColorTexture());

    // render the final scene
    RenderScene(_mCameraView, _mCameraProj);

    // unbind texture so we can render on it again
    GetApp()->GetDevice()->SetTexture(0,NULL);

    // draw the shadowmap texture to HUD
    RenderSplitOnHUD(iSplit);
  }

  // render other HUD stuff
  RenderHUD();
}

整个PSSM的核心代码就实现完成了,最后本书实现了9级平行分割对物体阴影的实现,实现效果如下:



  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海洋_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值