碛中作
[唐代][岑参]
走马西来欲到天,辞家见月两回圆。
今夜不知何处宿,平沙万里绝人烟。
Preface
This tutorial explains the theme using a DirectX effect and shader model 3.0. Knowledge of the implementation with render targets, setting effect variables and transformations of objects are essential for the understanding of the example. The example can be transferred easily to other shader languages.
Introduction
Shadow mapping is a technique used to render shadows in computer graphics. With this method a texture with depth values (shadow map) is created from the viewpoint of the light source. When drawing the scene the depth values of the pixels are then compared with the depth values in the shadow map. This requires that the pixels must be transformed to the coordinate system of the light source. Are the values lower, the pixels are in shadow.
Shadow Map是当前实现阴影渲染的主流手段。
This technique in use with only one shadow map, makes sense only with a spot light. If you want to realize a light which shines in several directions, e.g. a point light, this method is no longer sufficient. The most obvious solution is to use a shadow map for each direction in which the light shines. A point light would then result in six shadow maps. By the way, this method is then called cubical shadow mapping: although it has no impact on quality, it is very unperformant because you need to render six shadow maps.
在平行光以及聚光灯等光源照射的情况下,只需要使用一张Shadow Map就可以完成对应的阴影渲染工作;不过对于那些360度光照方向的点光灯而言,一张shadow map就不再够用。传统的实现方式是为点光源的每个方向指定一张shadow map,也就是使用一个cubemap来完成点光阴影的渲染。这个方法被称为cubical shadow mapping。虽然使用这种方式绘制的点光阴影质量上并无任何下降,不过由于需要操纵六张贴图,还是使得这种方法的性能表现与实现方式不尽人意。
Another technique to solve the problem is called spherical shadow mapping. This technique uses only one shadow map like the traditional shadow mapping technique and is therefore nearly as quickly but problematic in point of quality, because the uneven sample rate leads to distortions. Further research brought the dual paraboloid - technology, first time described 1998 in the publication "view-independent environment maps" from Wolfgang Heidrich and Hans-Peter Seidel.
点光阴影渲染的另一种方法是spherical shadow mapping。这种方式也是只采用一张shadow map进行点光阴影的渲染,不过由于其实现方式的原因,虽然渲染速度很快,但是采样率的不均匀导致了显示质量的问题(阴影形变)。后来有人提出了dual paraboloid shadow map方法,这个技术最早是在1998年提出,被用作环境贴图的渲染工作。
Here should be noted that the shadow mapping problem described until now, can be found in almost identical form with environment mapping, except that there are color values of the environment, used in the maps, instead of depth values. Theredo I want to show a picture presenting a paraboloid map used with dual paraboloid environment mapping. The purpose of this picture, however, is to give you an insight into the paraboloid projection, which is difficult with a paraboloid shadow map, as we will see later. But we want to remain on the subject dual paraboloid shadow mapping.
到目前为止,除了环境贴图存储的是颜色,而阴影贴图存储的是深度之外,在点光阴影渲染上的所有问题,都跟环境贴图的实现问题一模一样。为了让大家对paraboloid投影有一个直观的了解,下面给出一张dual paraboloid环境贴图,之所以给出的是环境贴图,而不是阴影贴图,是因为环境贴图展示的颜色数据更加直观,而阴影贴图的深度数据则容易让人视觉混乱。
Paraboloid maps provide a good performance and higher quality than spherical maps. First a picture of an elliptical paraboloid, similar we will use it:
相对于spherical map,paraboloid map的性能更好,质量更佳。下图是一张椭球形paraboloid,后面会用到。
An advantageous feature of paraboloids is that incident rays coming from the focal point, will be reflected parallel to the direction of the paraboloid, which facilitates the calculation of the projection:
paraboloid映射的一个优点是,从焦点出发的入射光线经过折射之后,将会以平行于paraboloid光轴的方向射出,这个特性会在后续的投影计算过程中被用到。利用这个特性,paraboloid map的相机使用的是正交矩阵,任何入射光线在paraboloid表面上的点都可以通过垂直于near clipping平面的的方向投影到paraboloid map上。
When using paraboloid mapping the space around the light source is divided into two hemispheres (paraboloids). There is a front and a rear hemisphere, which are put together with the open sides ahead, forming an enclosed space, as in the picture above.
在paraboloid shadow map的实现方法中,光源周边的空间被分割成两个半球(paraboloids)。这两个半球被称为前向(front)半球与后向(rear)半球,如上图所示,这两个半球开口处衔接起来,就形成了一个密闭的空间。
Mathematic derivation
First we choose a paraboloid that is suitable for this purpose:
我们为shadow map选定了一种合适的paraboloid形状,其公式给出如下(抛物线,两个抛物线组成的密闭形状,就形成了一个凸透镜,满足之前所说的焦点入射光线平行于光轴射出的特性):
Each vertex rendered must be projected onto the paraboloid surface. A picture as explanation:
需要产生点光投影的所有物体的顶点在渲染后,都能够投射到paraboloid表面上,如下图所示:
The x- and y- coordinate of the projection point must be found. These coordinates can be calculated using the normal vector at the point of projection. For this we first need the definition of the normal vector at the surface. This is determined by the cross product of the tangents. We will get the tangents from the partial derivation of the function with respect to x and y.
这个顶点在paraboloid表面上的投影点,是通过连接凸透镜焦点与顶点的直线与表面所相交的点,由于凸透镜的特性,这个入射光线的反射光线必定是垂直向上的(也就是平行于光轴)。
为了计算出对应的投影点,首先我们需要得到投影点对应的平面法线,平面法线可以通过平面上的两条切线通过叉乘得到,而切线则可以通过f(x,y)的偏微分得到,如下所示:
The direction vector of the incident projection ray corresponds to the normalized vertex position in the paraboloid coordinate system. If we add the reflection vector to the direction vector, we get a vector that corresponds to the normal at the point of projection, only the length is different. Because the reflection vector has the same direction as the z-axis, we only have to add 1 to the z-value of the incident vector.
入射光线的方向向量与表面上焦点在paraboloid的坐标是息息相关的。如果我们根据此点的法线方向推算入射光线的反射光线反向,我们就会发现,反射光线的方向是与Z轴平行的。而法线的方向实际上等于入射光线的方向(这里说的是入射光线的反方向)与反射光线的方向之和,也就是说,法线的方向实际上是入射光线的方向(归一化)在Z轴上加1即可。
If we now establish a relationship to the above derivation, it can be seen that a division of the added vector by its z-value calculates the x-and y-coordinate of the desired projection point.
按照上面的推导,我们可以得到如下的法线计算公式:
a.) Creation of the paraboloid shadow maps:
The paraboloid shadow maps must be created in the first two render passes. I used a resolution of 1024 x 1024, for the maps, but this can be chosen at will. The higher the resolution, the better the shadow quality, but the worse the performance. For the texture format it is recommended to use the R32F floating point format.
paraboloid shadow map必须是在渲染的最开始就开始生成(感觉没有必要吧,只要在使用之前生成不就行了,难道有其他的什么限制?)。此处使用的paraboloid map的分辨率是1024*1024的,实际上可以根据实际需要修正,分辨率太高了虽然会有助于提升阴影质量,但是同时也会导致性能的下降,至于贴图的格式,推荐使用32位的R32F浮点格式。
First of all, the render target for the front shadow map must be set. Next the world view matrix for the paraboloid space must be created and set as a parameter for the effect. The world matrix corresponds to a common transformation in the absolute space and the view matrix is the matrix that allows us to render the scene from the perspective of the light. The position of this matrix is the light position and the orientation can be chosen freely. I have chosen the orientation towards the objects that will be illuminated. A projection matrix is not needed, because we perform the projection manually.
以前向半球的shadow map渲染为例,其渲染大概需要分为以下几个步骤:
1.设置好前向半球的shadow map为rendertarget
2.设置paraboloid空间的world view矩阵。
世界矩阵就是通用的空间转换的世界矩阵,而相机矩阵则是将场景内容渲染到相机对应的透视空间的矩阵:这个矩阵的position通常都是设置为光源的position,而其朝向向量可以自由选择,通常都是设定为光源到投影物体的方向。
在paraboloid map的生成过程中,不需要设置投影矩阵,因为投影过程是通过shader手动计算的。
Now we can implement the shader. As so often, it looks much simpler in the shader:
first the vertex shader:
顶点shader给出如下:
float4x4 g_mDPWorldView;
float g_fFar;
float g_fNear;
float g_fDir; // direction of hemisphere
// DPDepth
struct VSDPDepthIn
{
float3 Pos : POSITION;
};
struct PSDPDepthIn
{
float4 Pos : POSITION;
float ClipDepth : TEXCOORD1;
float Depth : TEXCOORD2;
};
// DPDepth-vertex-shader
PSDPDepthIn DPDepthVS(VSDPDepthIn In)
{
PSDPDepthIn Out;
// transform vertex to DP-space
Out.Pos = mul(float4(In.Pos, 1.0f), g_mDPWorldView);//将世界坐标转换到相机空间中的坐标
Out.Pos /= Out.Pos.w;//归一化处理,转换为齐次坐标
// for the back-map z has to be inverted
Out.Pos.z *= g_fDir; //前向paraboloid map不需要进行这一步
// because the origin is at 0 the proj-vector
// matches the vertex-position
float fLength = length(Out.Pos.xyz);
// normalize
Out.Pos /= fLength;//入射光线反方向归一化处理
// save for clipping
Out.ClipDepth = Out.Pos.z; //存储相机空间的z轴距离,用于后续剔除处理
// calc "normal" on intersection, by adding the
// reflection-vector(0,0,1) and divide through
// his z to get the texture coords
Out.Pos.x /= Out.Pos.z + 1.0f;//为什么生成paraboloid map的时候不在像素shader中转换到paraboloid空间呢?因为使用了tessellation?
Out.Pos.y /= Out.Pos.z + 1.0f;//计算投影点所在的xy位置
// set z for z-buffering and neutralize w
Out.Pos.z = (fLength - g_fNear) / (g_fFar - g_fNear);//shadow map depth
Out.Pos.w = 1.0f;
// DP-depth
Out.Depth = Out.Pos.z;
return Out;
}
First the vertices are transformed in the paraboloid space and divided by w to get homogeneous coordinates. The meaning of g_fDir will be explained later. Now the vertex has to be projected on the paraboloid. As we know, the normalized vertex position in the paraboloid space corresponds to the direction vector of the incident projection ray. Therefore we normalize the vertex position now and we store the z-coordinate for the pixel shader.
Now the decisive part: we add the reflection vector (0/0/1) to the just calculated projection direction vector and we get the vector that corresponds to the normal on the projection point. We divide it by its z-value and we get the desired x- and y- coordinate. In the example I've done both steps in one, let this not confuse you. The calculated coordinates corresponds to the texture coordinates in the shadow map. For the subsequent z-buffer test we have to calculate the z-value of the vertex correctly. We do this by bringing it between zero and one, using the far and near clipping plane. We also store this value for the pixel shader. The w-value is neutralized.
Explanation of g_fDir: to avoid writing two almost identical shaders, we simply invert the z-coordinate of the vertex for the rear hemisphere. Also the back-face culling must be disabled to make this possible, because the order of the triangle vertices will be reversed when inverting the z-coordinate.
the pixel shader:
像素shader给出如下:
// DPDepth-pixel-shader
float4 DPDepthPS(PSDPDepthIn In) : COLOR
{
// clipping
clip(In.ClipDepth);//剔除ClipDepth<0的像素
return In.Depth;
}
Here, only the pixels located before the clipping plane of the respective hemisphere, have to be removed and the depth of the pixels have to be written in the shadow map.
Now we render the scene once with g_fDir = 1 for the front hemisphere and once with g_fDir = -1 for the rear hemisphere and the shadow maps are created. The result should then in principle look like the following picture:
采用上述shader分别针对g_fDir = 1/-1各运行一次,得到前向paraboloid map与后向paraboloid map,结果大概跟下图类似:
b.) rendering of the final scene:
First the render target for the scene and some parameters for the effect must be set. Namely they are:
the two previously created shadow maps (for PS)
if desired, the necessary textures (for PS)
the world and combined view projection matrix for the scene (for VS)
the paraboloid view matrix (for PS)
the light position in world coordinates, which is required for lighting (for PS)
light and material colors
the SHADOW_EPSILON, which is needed with shadow mapping to avoid unwanted artifacts
-
furthermore, the required samplers
paraboloid map的使用流程中,需要设置以下一些参数:
绑定两张paraboloid map
其他的一些需要的贴图绑定
世界矩阵与相机矩阵
paraboloid相机矩阵,用于转换比对
世界坐标系中的光源位置
光照与材质颜色
阴影误差范围SHADOW_EPSILON
#define SHADOW_EPSILON 0.0005f
texture g_DPFrontMap;
texture g_DPBackMap;
texture g_Terrain;
float4x4 g_mDPView;
float4x4 g_mWorld;
float4x4 g_mViewProj;
float3 g_vLightPos; // worldspace
float4 g_Material = float4(1.0f, 1.0f, 1.0f, 1.0f);
float4 g_LightAmbientColor = float4(0.2f, 0.2f, 0.2f, 1.0f);
float4 g_LightDiffuseColor = float4(1.0f, 1.0f, 1.0f, 1.0f);
// DPDepth
sampler2D DPFrontSampler =
sampler_state
{
Texture = <g_DPFrontMap>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Border;
AddressV = Border;
BorderColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
};
sampler2D DPBackSampler =
sampler_state
{
Texture = <g_DPBackMap>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Border;
AddressV = Border;
BorderColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
};
// DPSM
sampler2D TerrainSampler =
sampler_state
{
Texture = <g_Terrain>;
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Wrap;
AddressV = Wrap;
};
////////////////////
// Vertex-Shader:
// DPSM
struct VSDPSMIn
{
float3 Pos : POSITION;
float2 TexCoord : TEXCOORD0;
float3 Normal : NORMAL;
};
struct PSDPSMIn
{
float4 Pos : POSITION;
float3 PosWorld : TEXCOORD0;
float2 TexCoord : TEXCOORD1;
float3 Normal : TEXCOORD2;
float3 Light : TEXCOORD3;
};
// DPSM-vertex-shader
PSDPSMIn DPSMVS(VSDPSMIn In)
{
PSDPSMIn Out;
// world space
float3 vPosWorld = mul(float4(In.Pos, 1.0f), g_mWorld);//计算顶点的世界坐标
Out.Normal = normalize(mul(In.Normal, (float3x3)g_mWorld));//计算法线在世界坐标系中的数值
Out.Light = normalize(g_vLightPos - vPosWorld); //计算光线方向
Out.Pos = mul(float4(vPosWorld, 1.0f), g_mViewProj); //计算顶点经过投影后的位置坐标
Out.PosWorld = vPosWorld;
Out.TexCoord = In.TexCoord;
return Out;
}
In the vertex shader is nothing that is specific to paraboloid mapping, so an explanation is not necessary here.
Now the pixel shader:
Even if the pixel shader shown below does not give the impression, again there is nothing in it that would necessarily be explained, because the calculation of the texture coordinates is the same as we used it when creating the shadow maps, except that the calculation of both hemispheres is provided here and depending in which hemisphere the vertex is located, the coordinates for one of the two paraboloids are calculated and brought between zero and one, so that the shadow maps can be read correctly. The last part in the pixel shader is the same as in the traditional shadow mapping technique: the depth values of the pixel are compared. If it is greater than the value in the shadow map, the pixel is in shadow and it only gets the texture color and ambient light. Otherwise, it is lit properly and gets all a pixel dreams of. :)
// DPSM-pixel-shader
float4 DPSMPS(PSDPSMIn In) : COLOR
{
float3 Color;
// texcoord-calculation is the same calculation as in the Depth-VS,
// but texcoords have to be in range [0, 1]
// transform into lightspace
float3 vPosDP = mul(float4(In.PosWorld, 1.0f), g_mDPView); //将像素位置从世界坐标系转换到光源空间坐标系
float fLength = length(vPosDP);
// normalize
vPosDP /= fLength;//得到归一化法线方向
// compute and read according depth
float fDPDepth;
float fSceneDepth;
if(vPosDP.z >= 0.0f)//根据z轴判定当前像素位于哪个paraboloid map笼罩之下
{
float2 vTexFront;
vTexFront.x = (vPosDP.x / (1.0f + vPosDP.z)) * 0.5f + 0.5f;
vTexFront.y = 1.0f - ((vPosDP.y / (1.0f + vPosDP.z)) * 0.5f + 0.5f);//在像素shader中进行paraboloid空间的转换,有利于减少线性差值带来的数据误差
fSceneDepth = (fLength - g_fNear) / (g_fFar - g_fNear);//这一句可以放在if语句之外计算
fDPDepth = tex2D(DPFrontSampler, vTexFront).r;
}
else
{
// for the back the z has to be inverted
float2 vTexBack;
vTexBack.x = (vPosDP.x / (1.0f - vPosDP.z)) * 0.5f + 0.5f;
vTexBack.y = 1.0f - ((vPosDP.y / (1.0f - vPosDP.z)) * 0.5f + 0.5f);
fSceneDepth = (fLength - g_fNear) / (g_fFar - g_fNear);
fDPDepth = tex2D(DPBackSampler, vTexBack).r;
}
// lighting and shadowing
float3 vNormal = normalize(In.Normal);
float3 vLight = normalize(In.Light);
if((fDPDepth + SHADOW_EPSILON) < fSceneDepth)//判定是否处于阴影之中
{
Color = tex2D(TerrainSampler, In.TexCoord) * g_LightAmbientColor;
}
else
{
Color = tex2D(TerrainSampler, In.TexCoord) * saturate(dot(vLight, vNormal)) *
g_LightDiffuseColor * g_Material + g_LightAmbientColor;
}
return float4(Color, 1.0f);
}
To complete the effect, the compilation of the shaders:
technique DPDepth
{
pass p0
{
VertexShader = compile vs_2_0 DPDepthVS();
PixelShader = compile ps_2_0 DPDepthPS();
}
}
technique DPSM
{
pass p0
{
VertexShader = compile vs_3_0 DPSMVS();
PixelShader = compile ps_3_0 DPSMPS();
}
}
Now only the rendering of the scene must be done and that's it!
Conclusion to dual paraboloid shadow mapping (also valid for dual paraboloid environment mapping):
As mentioned above, paraboloid maps provide good performance and are qualitatively better as spherical maps, however, there are also drawbacks compared with cubical mapping, mainly related to the distortions from the paraboloids. One effect which may appear is, if you already transform the vertex positions to the paraboloid space in the vertex shader, when drawing the scene, a distortion would occur due to the interpolation in the rasterizer. Since we have transformed the position in the pixel shader in our example, this problem is eliminated for us. The other slightly heavier to handle problem are distortions that occur mainly at the clipping plane of the two hemispheres and become visible through cracks. But this effect is highly dependent on the resolution of the geometry. If it is high enough, the effect is smaller. You can also counteract this effect with skillful setting of this plane. You should, if possible, avoid collisions of the geometry with this plane.
总的来说,paraboloid map在质量上与性能上都要优于spherical map,但是相对于cubical mapping方法,其质量就略有不如了:主要表现在由于paraboloid而导致的一些阴影形变。
一个问题表现为:如果我们是在顶点shader中将顶点位置转换到paraboloid空间,那么由于计算公式的非线性,导致插值结果跟实际的正确结果存在出入,从而就会导致阴影形变的发生。由于我们上面的实现方案是将这一步放置在像素shader中进行,所以不会存在这个问题。
另外一个不那么好规避的问题则是:由于靠近near clipping平面导致物体阴影被分割成两个部分,分别落在两个paraboloid map上,从而在最终渲染采样的时候不能完美衔接上而导致的瑕疵。这个问题的严重程度跟物体占据的贴图分辨率有关,如果占据的分辨率足够高,衔接处的缝隙相对来说就不会那么突出。此外,也可以通过调整paraboloid的near clipping平面来避免物体被分割成两部分来降低这个问题出现的概率。
As we have seen, despite small drawbacks, paraboloid maps are very useful. Because of becoming faster and faster graphics cards the resolution of the geometry is getting higher, so nothing should stand in the way for paraboloid mapping in the future.
如我们所见,虽然存在一些小瑕疵,但是paraboloid map的方案依然非常优秀,且由于显卡进化的越来越先进高端,paraboloid map的分辨率也可以做得越来越高,使得这些问题的严重程度越来越低,以至于完全可以忽略,可以相信,paraboloid map方案在未来的yingyon
You can download the complete example as a DirectX effect file here.