XNA Shader编程教程14-透射
上个教程我们使用了alpha贴图和alpha通道让物体变得透明,这次我们将通过实现透射(transmittance)更深入地学习透明。
透射(Transmittance)
像玻璃、水、水晶、空气等物体会在光线穿过它们时吸收一定的光线,在教程13中,我们使用alpha贴图使物体透明并使用颜色为RGB(0.5,0.5,0.5)的alpha贴图创建了一个透明球,这个方法能用在很多场合,但这样做会使透明效果太平淡。
真实世界中的物体,比如说玻璃球,当光线穿过它时还会吸收/散射一定的光线,光线进入玻璃球越深,在光线穿出前吸收和散射的越多,这叫做透射 ( http://en.wikipedia.org/wiki/Transmittance)。要计算透射( T ),我们可以使用Beer-Lamberts定律(http://en.wikipedia.org/wiki/Beer's_law )处理透射的光线,让我们看一下Beer-Lamberts定律更好地理解原理!(译者注:在计算漫反射时我们使用的是Lambert定律,这个定律又叫做Lambert余弦定律,简单地说就是反射强度是法线和入射光方向的点乘,Lambert全名Johann Heinrich Lambert,生于1728年8月26日,死于1777年9月25日,是瑞士数学家、物理学家和天文学家,而Beer-Lamberts定律是光线透射的规律,这个规律是由Pierre Bouguer在1729年前发现的,但常常被误认为是Lambert 发现的,事实上Lambert只是于1760年引用了Bouguer的文章,1852年August Beer改进了这个规律)
T=e-a′cd (公式1)
这个公式中的T表示透射,a'是吸收因子,c是物体的浓度(译者:原文是consistensy,没这个单词,怀疑是consistency),d是物体的厚度。所以要使用这个公式,我们需要知道a'、c和d。
先来看c。c控制光线的吸收程度,这个值可以设置为大于0的任何值。然后是a',我们可以通过公式1的变形计算a':
(公式2)
公式2中的T是透射中最暗的颜色,对应最远的距离。
最后获取d。这个值设置物体的厚度。
本教程中,我们将计算任何简单物体(不包含孔或突起物,如球,简单玻璃形状等)的c。这个shader很复杂,因为我们将在后面的教程中也要正确运行。
现在我们获取计算给定点的T的所有变量,可以使用T表示光线的吸收程度,这可以通过将T乘以光线(透射后的像素)的混合颜色做到。
那么我们如何计算每根光线在透射体中的前进距离?可以使用深度缓冲 ( http://en.wikipedia.org/wiki/Z-buffer)!
深度缓冲(即Z缓冲)可以看成一张包含场景的灰度图,灰度值表示物体距离相机的远近。所以,本文最开始的一张图片我们看到的是一个复杂的玻璃物体,而场景深度缓冲看起来应像下图所示:
深度缓冲需要正确的介于近裁平面和远裁平面的值,最完美的是近裁平面是透射体的最近顶点,远裁平面是最远顶点。
知道了这些,我们就可以通过使用两个深度缓冲纹理找到任何角度对应的透射体的厚度。通过使用剔除,我们可以在一个深度缓冲纹理中绘制透射体的前表面,使用另一个深度缓冲纹理中绘制透射体的后表面。下面两张图显示了这两个不同的纹理:
灰度值显示了光线能在透射体中前进多远,白色代表长而黑色代表短或不前进。
实现Shader
本教程使用三个technique。一个只是处理镜面反射,另一个将场景绘制到一张深度纹理,第三个是post process shader将透射效果应用到物体上。我们并不想让场景中的所有物体都有透射效果。所以使用post process shader时,我们首先将没有透射体的场景绘制到一张纹理(背景纹理),然后在第二个pass中单独绘制透射体,最后将在post process shader中把两者组合起来。首先处理镜面反射:
float4x4 matWorldViewProj; float4x4 matInverseWorld; float4 vLightDirection; float4 vecLightDir; float4 vecEye; float4 vDiffuseColor; float4 vSpecularColor; float4 vAmbient; texture ColorMap; sampler ColorMapSampler = sampler_state { Texture = <ColorMap>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; struct OUT { float4 Pos : POSITION; float2 Tex : TEXCOORD0; float3 L : TEXCOORD1; float3 N : TEXCOORD2; float3 V : TEXCOORD3; }; OUT VertexShader( float4 Pos: POSITION, float2 Tex :TEXCOORD, float3 N: NORMAL ) { OUT Out = (OUT) 0; Out.Pos = mul(Pos, matWorldViewProj); Out.Tex = Tex; Out.L = normalize(vLightDirection); Out.N = normalize(mul(matInverseWorld, N)); Out.V = vecEye - Pos; return Out; } float4 PixelShader(float2 Tex: TEXCOORD0,float3 L: TEXCOORD1, float3 N: TEXCOORD2, float3 V: TEXCOORD3) : COLOR { float3 ViewDir = normalize(V); // Calculate normal diffuse light. float4 Color = tex2D(ColorMapSampler, Tex); float Diff = saturate(dot(L, N)); float3 Reflect = normalize(2 * Diff * N - L); float Specular = pow(saturate(dot(Reflect, ViewDir)), 128); // R.V^n //I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n return Color*vAmbient + Color*vDiffuseColor * Diff + vSpecularColor * Specular; } technique EnvironmentShader { pass P0 { VertexShader = compile vs_2_0 VertexShader(); PixelShader = compile ps_2_0 PixelShader(); } }
接着是Depth Texture shader。这个shader只将场景绘制成灰度,每个顶点/像素的深度用一个介于0.0至1.0之间的值表示,1.0表示最靠近相机而0.0 表示在远裁平面(Pos.w)。 所以要获取顶点深度值,我们只需获取顶点的Z值,将Z值除以W值使深度值在投影矩阵的近裁平面和远裁平面之间。 vertex-shader计算两个值:Position和Distance。
struct OUT_DEPTH { float4 : POSITION; float Distance : TEXCOORD0; };
下面就可以实现Depth texture vertex shader:
OUT_DEPTH RenderDepthMapVS(float4 vPos: POSITION) { OUT_DEPTH Out; // Translate the vertex using matWorldViewProj. Out.Position = mul(vPos, matWorldViewProj); // Get the distance of the vertex between near and far clipping plane in matWorldViewProj. Out.Distance.x = 1-(Out.Position.z/Out.Position.w); return Out; }
首先我们将顶点乘以world*view*projection矩阵进行转换。然后将距离值设置为正确的深度值,这可以通过Position.z / Position.w得到,让我们获得了介于近裁平面和远裁平面之间 的深度值。下面是pixel shader!我们将OUT_DEPTH中的Distance值转换到纹理,以便接下来使用:
float4 RenderDepthMapPS( OUT_DEPTH In ) : COLOR { return float4(In.Distance.x,0,0,1); }
下面是technique:
technique DepthMapShader { pass P0 { ZEnable = TRUE; ZWriteEnable = TRUE; AlphaBlendEnable = FALSE; VertexShader = compile vs_2_0 RenderDepthMapVS(); PixelShader = compile ps_2_0 RenderDepthMapPS(); } }
Technique中没有新的东西,只是打开Z缓冲,并使之可写。最后是透射的post process shader!首先我们需要作为背景的场景纹理、透射体场景(包含所以具有透射效果的物体的纹理)和两个深度纹理!
texture D1M; sampler D1MSampler = sampler_state { Texture = <D1M>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture D2M; sampler D2MSampler = sampler_state { Texture = <D2M>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture BGScene; sampler BGSceneSampler = sampler_state { Texture = &kt;BGScene>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture Scene; sampler SceneSampler = sampler_state { Texture = <Scene>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; };
DM1是第一张深度贴图纹理,包含透射体的后表面。DM2是第二张深度贴图纹理,包含透射体的前表面,BGScene包含背景,Scene包含透射体场景/颜色。 然后添加两个变量,一个包含用于计算吸收因子的距离因子,另一个包含透射体的稠度:
float Du = 1.0f; float C = 12.0f;
因为这个一个post process shader,所以无需vertex shader只用到pixel shader:
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR { float4 Color=tex2D(SceneSampler, Tex); float4 BGColor=tex2D(BGSceneSampler, Tex); float depth1=tex2D(D1MSampler, Tex).r; float depth2=tex2D(D2MSampler, Tex).r; }
没有新东西,我们从不同的纹理获取像素的颜色,深度纹理shader返回depth1和depth的r通道。再次看一下公式:
T=e-a′cd (公式1)
(公式2)
我们只计算了c变量和distance变量,让我们解决剩余的。变量d包含透射体的厚度,能够简单地使用depth1和depth2计算得出:
float distance = ((depth2-depth1));
depth2 and depth1的差就是物体的厚度!
T变量用来找到公式2中的最大吸收因子,它包含最暗的颜色。这可以是一个颜色的硬编码,或作为一个参数传递到shader。在本教程中,我们使用Color(透射体,绘制到一个纹理)中的一个值,最后通过公式2得到最后的吸收因子a′:
float3 a; a.r = (-log(Color.r))/Du; a.g = (-log(Color.g))/Du; a.b = (-log(Color.b))/Du;
这让我们获得了最终的变量T!
float4 T; T.r = exp((-a.r)*C*distance)+0.000001; T.g = exp((-a.g)*C*distance)+0.000001; T.b = exp((-a.b)*C*distance)+0.000001; T.w = 1;
我们使用公式1在每个颜色通道中计算透射值。要避免T值为零(使物体变得全黑),我们在每个通道加0.000001。完成这步后,我们就可以提取透射体背后(光线能穿过透射体)的像素并将它乘以T,最后从pixel shader中返回这个值:
return T*BGColor;
最后是technique:
technique PostProcess { pass P0 { // A post process shader only needs a pixel shader. PixelShader = compile ps_2_0 PixelShader(); } }
这个shader虽然长但不是非常复杂,但应该是教程至今最高级的一个了。如果你不理解,试着改变一下参数看看每个量是如何工作的。完整的shader如下:
// Global variables float Du = 1.0f; float C = 12.0f; // This will use the texture bound to the object( like from the sprite batch ). sampler ColorMapSampler : register(s0); texture D1M; sampler D1MSampler = sampler_state { Texture = <D1M>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture D2M; sampler D2MSampler = sampler_state { Texture = <D2M>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture BGScene; sampler BGSceneSampler = sampler_state { Texture = &kt;BGScene>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; texture Scene; sampler SceneSampler = sampler_state { Texture = <Scene>; MinFilter = Linear; MagFilter = Linear; MipFilter = Linear; AddressU = Clamp; AddressV = Clamp; }; // Transmittance float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR { float4 Color=tex2D(SceneSampler, Tex); float4 BGColor=tex2D(BGSceneSampler, Tex); float depth1=tex2D(D1MSampler, Tex).r; float depth2=tex2D(D2MSampler, Tex).r; float distance = ((depth2-depth1)); float3 a; a.r = (-log(Color.r))/Du; a.g = (-log(Color.g))/Du; a.b = (-log(Color.b))/Du; float4 T; T.r = exp((-a.r)*C*distance)+0.000001; T.g = exp((-a.g)*C*distance)+0.000001; T.b = exp((-a.b)*C*distance)+0.000001; T.w = 1; return T*BGColor; } technique PostProcess { pass P0 { // A post process shader only needs a pixel shader. PixelShader = compile ps_2_0 PixelShader(); } }
使用shader
最后,看看如何使用shader并设置深度缓冲。我们从定义渲染目标和渲染纹理开始:
RenderTarget2D depthRT; DepthStencilBuffer depthSB; RenderTarget2D depthRT2; DepthStencilBuffer depthSB2; Texture2D depth1Texture; Texture2D depth2Texture;
我们使用两个RenderTarget2D,两个DepthStencilBuffer和两个纹理存储深度纹理。如果想简化过程也可以只使用一个DepthStencilBuffer。在这个shader中,我们要设置两个变量设置使用哪个technique:
EffectTechnique environmentShader; EffectTechnique depthMapShader;
我们还要设置距离因子用于计算吸收和透射体的稠度:
float Du = 1.0f; float C = 12.0f;
现在可以制作场景和使用shader了。在LoadContent 方法中我们需要初始化和创建渲染目标:
// Create our render targets PresentationParameters pp = graphics.GraphicsDevice.PresentationParameters; renderTarget = new RenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, graphics.GraphicsDevice.DisplayMode.Format); depthRT = new RenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, SurfaceFormat.Single);// 32-bit float format using 32 bits for the red channel. depthRT2 = new RenderTarget2D(graphics.GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, 1, SurfaceFormat.Single);// 32-bit float format using 32 bits for the red channel.
还需要DepthStencilBuffers:
depthSB = CreateDepthStencil(depthRT, DepthFormat.Depth24Stencil8); depthSB2 = CreateDepthStencil(depthRT2, DepthFormat.Depth24Stencil8);
上面的代码使用渲染目标创建两个DepthStencilBuffers,将深度格式设置为Depth24Stencil8,将DepthBuffer通道设置为24bit,模板缓冲(stencil buffer)通道设置为8-bit。下面是DepthFormat的列表:
Depth15Stencil1 | 16-bit depth-buffer bit depth,其中15 bits保留给depth通道,1 bit用于stencil通道。 |
Depth16 | 16-bit depth-buffer bit depth |
Depth24 | 32-bit depth-buffer bit depth,使用24 bits的depth通道 |
Depth24Stencil4 | 32-bit depth-buffer bit depth使用24 bits的depth通道,4bits的stencil通道 |
Depth24Stencil8 | non-lockable格式,包含24 bits depth(以24-bit 浮点数格式 − 20E4),8 bits的stencil |
Depth24Stencil8Single | 32-bit depth-buffer bit depth,使用24 bits的depth通道,8bits的stencil通道 |
Depth32 | 32-bit depth-buffer bit depth |
Unknown | 未知格式 |
我们使用了两个自定义函数创建深度缓冲,第一个函数CreateDepthStencil(RenderTarget2D target) 使用传入的渲染目标创建DepthStencilBuffer :
private DepthStencilBuffer CreateDepthStencil(RenderTarget2D target) { return new DepthStencilBuffer(target.GraphicsDevice, target.Width,target.Height, target.GraphicsDevice.DepthStencilBuffer.Format, target.MultiSampleType, target.MultiSampleQuality); }
第二个函数CreateDepthStencil(RenderTarget2D target, DepthFormat depth)检查计算机支持格式并使用CreateDepthStencil(RenderTarget2D target) 创建DepthStencilBuffer:
private DepthStencilBuffer CreateDepthStencil(RenderTarget2D target, DepthFormat depth) { if (GraphicsAdapter.DefaultAdapter.CheckDepthStencilMatch(DeviceType.Hardware, GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Format, target.Format,depth)) { return new DepthStencilBuffer(target.GraphicsDevice, target.Width,target.Height, depth, target.MultiSampleType, target.MultiSampleQuality); } else return CreateDepthStencil(target); }
下一步需要将shader中的techniques存储在变量中:
// Get our techniques and store them in variables. environmentShader = effect.Techniques["EnvironmentShader"]; depthMapShader = effect.Techniques["DepthMapShader"];
因为要多次用到渲染场景,所以我还将渲染场景的代码移至一个函数中:
void DrawScene(bool transmittance) { // Begin our effect effect.Begin(SaveStateMode.SaveState); // A shader can have multiple passes, be sure to loop trough each of them. foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Begin current pass pass.Begin(); foreach (ModelMesh mesh in m_Model.Meshes) { foreach (ModelMeshPart part in mesh.MeshParts) { // calculate our worldMatrix.. worldMatrix = bones[mesh.ParentBone.Index] * renderMatrix; // Render our meshpart graphics.GraphicsDevice.Vertices[0].SetSource(mesh.VertexBuffer, part.StreamOffset, part.VertexStride); graphics.GraphicsDevice.Indices = mesh.IndexBuffer; graphics.GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, part.BaseVertex, 0, part.NumVertices,part.StartIndex, part.PrimitiveCount); } } // Stop current pass pass.End(); } // Stop using this effect effect.End(); }
我们使用镜面反射shader(EnvironementShader)绘制透射体,使用深度缓冲shader(DepthMapShader)进行深度缓冲:
// create depth-map 1 effect.CurrentTechnique = depthMapShader; GraphicsDevice.RenderState.CullMode = CullMode.CullClockwiseFace; depth1Texture = RenderDepthMap(depthSB,depthRT); // create depth-map 2 effect.CurrentTechnique = depthMapShader; GraphicsDevice.RenderState.CullMode = CullMode.CullCounterClockwiseFace; depth2Texture = RenderDepthMap(depthSB2, depthRT2); // render our trasmitting objects graphics.GraphicsDevice.SetRenderTarget(0, renderTarget); graphics.GraphicsDevice.Clear(Color.White); effect.CurrentTechnique = environmentShader; DrawScene(true); graphics.GraphicsDevice.SetRenderTarget(0, null); SceneTexture = renderTarget.GetTexture();
现在我们已经有了所有的纹理,做好了进行post process transmittance shader的准备。本教程中我只使用一张纹理存储场景背景。你可能注意到,我们使用了一个自定义函数RenderDepthMap绘制DepthMap。这个函数只是设置将渲染目标设置为传入的DepthStencilBuffer,绘制场景,恢复旧的渲染目标状态并将 DepthBuffer 作为一张纹理返回:
private Texture2D RenderDepthMap(DepthStencilBuffer dsb, RenderTarget2D rt2D) { GraphicsDevice.RenderState.DepthBufferFunction = CompareFunction.LessEqual; GraphicsDevice.SetRenderTarget(0, rt2D); // Save our DepthStencilBuffer, so we can restore it later DepthStencilBuffer saveSB = GraphicsDevice.DepthStencilBuffer; GraphicsDevice.DepthStencilBuffer = dsb; GraphicsDevice.Clear(Color.Black); DrawScene(true); // restore old depth stencil buffer GraphicsDevice.SetRenderTarget(0, null); GraphicsDevice.DepthStencilBuffer = saveSB; return rt2D.GetTexture(); }
最后,使用transmittance shader生成最后的场景:
spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.SaveState); { // Apply the post process shader effectPost.Begin(); { effectPost.CurrentTechnique.Passes[0].Begin(); { effectPost.Parameters["D1M"].SetValue(depth1Texture); effectPost.Parameters["D2M"].SetValue(depth2Texture); effectPost.Parameters["BGScene"].SetValue(m_BGScene); effectPost.Parameters["Scene"].SetValue(SceneTexture); effectPost.Parameters["Du"].SetValue(Du); effectPost.Parameters["C"].SetValue(C); spriteBatch.Draw(SceneTexture, new Rectangle(0, 0, 800, 600), Color.White); effectPost.CurrentTechnique.Passes[0].End(); } } effectPost.End(); } spriteBatch.End();
以上代码不多,只是设置shader参数并绘制场景。下面是一些其他例子:
下次我们将在透射体上添加反射。
几个Buffer的区别
译者注:这是我自己补充的,如果有说错的地方请及时指正。
Buffer在计算机领域的意思就是缓冲区,也就是一块内存,通常是为了大量的信息计算与搬迁而用,而Buffer里的每笔数据通常都有着相同的规格,以利于大量且快速处理,在3D程序中,从效能上的考虑,通常将Buffer放在显存中。
计算机屏幕其实是由一个个(或一组组)光点组成的,这些光点叫做像素(Pixel),每个像素均可表现出千万种不同的颜色,由此才构成丰富多彩的图片。虽然3D程序在运算结构上都以三维坐标来思考,但最终还是要呈现在二维屏幕上,因此无论3D场景多复杂,最终都要将3D场景对应到窗口的像素上(这也是为什么在pixel shader中总是输出一个float4类型color的原因),换言之,就是要决定像素是什么颜色。这样我们会准备一块Buffer存储像素信息,这个Buffer被称之为ColorBuffer或PixelBuffer或FrameBuffer。
要达到快速反应与类似平行处理的机制,一般ColorBuffer有两块,当第一块ColorBuffer填充完信息并呈现在窗口上以后,第二块ColorBuffer就赶紧处理下一个画面的信息。等到第二块也填充完信息后,与第一块交换,同时呈现在窗口上,而原本第一块Buffer就继续处理下一个画面,两块ColorBuffer轮流替换。凡是正在呈现在窗口上的ColorBuffer叫做FrontBuffer,幕后做准备的叫做BackBuffer(后备缓冲)。如果不是全屏模式,ColorBuffer有三块,这也是非全屏模式比全屏模式慢的原因。今天不少新游戏采用的是三重缓冲,因为它没有Vsync(屏幕的垂直刷新频率)等待的时间,游戏也将更加流畅,当然这个三重缓冲和非全屏模式中的三个缓冲意思不同。
DepthBuffer(深度缓冲)用来判断3D场景中每个对象离观察者的距离,决定哪个该画,哪个不该画,你可以试着在XNA程序中关闭DepthBuffer,看看3D物体会有什么变化,就能更好地理解DepthBuffer的作用。在3D坐标系中,深度或远近用Z值表示,所以DepthBuffer又叫做ZBuffer。
而StencilBuffer翻译为模板缓冲,它都是与ColorBuffer搭配使用的,是提供ColorBuffer在运算时做记号用的。做记号为了什么?如何做记号?做什么记号?具体解释我认为要通过代码才能理解,StencilBuffer通常用在反射、阴影中,XNA的帮助文件中就有一个例子是使用StencilBuffer实现了阴影效果。
其他还有shader中的VertexBuffer和IndexBuffer,顾名思义,这两个缓冲分别用来存储顶点信息和索引信息。
文件下载(已下载 1711 次)发布时间:2009/5/7 下午1:52:17 阅读次数:12768