本章,我们将会学习如何渲染阴影(shadows)。同时还会介绍depth maps(深度纹理,2D纹理中存储了深度值而不是颜色值)和projective texture mapping(投影纹理映射,想像下Batman(蝙蝠侠) signal)。另外,在渲染阴影时还会遇到一引起常见的问题,我们需要确定如何解决这些问题。
图19.1显示了一个投影纹理映射shader的输出结果,其中把一个带有文字“Direct3D!”的纹理投影到一个棋盘样式的平面上。图中的绿色线条描述了投影仪的视锥体,白色的sphere表示场景中的一个point light。
图19.1 Output of the projective texture mapping shader (top) with the projected texture (bottom).在投影纹理映射中将会使用三个辅助类:Projector,Frustum和RenderableFrustum,图19.2显示了这三个类的结构图。在书中没有列出这三个类的代码,但是在配套网站上提供了完整的代码。
该方程式可以使用矩阵形式表示:
列表19.1 An Initial Projective Texture-Mapping Shader
#include "include\\Common.fxh" /************* Resources *************/ static const float4 ColorWhite = { 1, 1, 1, 1 }; cbuffer CBufferPerFrame { float4 AmbientColor = { 1.0f, 1.0f, 1.0f, 0.0f }; float4 LightColor = { 1.0f, 1.0f, 1.0f, 1.0f }; float3 LightPosition = { 0.0f, 0.0f, 0.0f }; float LightRadius = 10.0f; float3 CameraPosition; } cbuffer CBufferPerObject { float4x4 WorldViewProjection : WORLDVIEWPROJECTION; float4x4 World : WORLD; float4 SpecularColor : SPECULAR = { 1.0f, 1.0f, 1.0f, 1.0f }; float SpecularPower : SPECULARPOWER = 25.0f; float4x4 ProjectiveTextureMatrix; } Texture2D ColorTexture; Texture2D ProjectedTexture; SamplerState ProjectedTextureSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = BORDER; AddressV = BORDER; BorderColor = ColorWhite; }; SamplerState ColorSampler { Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressV = WRAP; }; RasterizerState BackFaceCulling { CullMode = BACK; }; /************* Data Structures *************/ struct VS_INPUT { float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD; float3 Normal : NORMAL; }; struct VS_OUTPUT { float4 Position : SV_Position; float3 Normal : NORMAL; float2 TextureCoordinate : TEXCOORD0; float3 WorldPosition : TEXCOORD1; float Attenuation : TEXCOORD2; float4 ProjectedTextureCoordinate : TEXCOORD3; }; /************* Vertex Shader *************/ VS_OUTPUT project_texture_vertex_shader(VS_INPUT IN) { VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.WorldPosition = mul(IN.ObjectPosition, World).xyz; OUT.TextureCoordinate = IN.TextureCoordinate; OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz); float3 lightDirection = LightPosition - OUT.WorldPosition; OUT.Attenuation = saturate(1.0f - (length(lightDirection) / LightRadius)); OUT.ProjectedTextureCoordinate = mul(IN.ObjectPosition, ProjectiveTextureMatrix); return OUT; } /************* Pixel Shaders *************/ float4 project_texture_pixel_shader(VS_OUTPUT IN) : SV_Target { float4 OUT = (float4)0; float3 lightDirection = LightPosition - IN.WorldPosition; lightDirection = normalize(lightDirection); float3 viewDirection = normalize(CameraPosition - IN.WorldPosition); float3 normal = normalize(IN.Normal); float n_dot_l = dot(normal, lightDirection); float3 halfVector = normalize(lightDirection + viewDirection); float n_dot_h = dot(normal, halfVector); float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); float4 lightCoefficients = lit(n_dot_l, n_dot_h, SpecularPower); float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb); float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb) * IN.Attenuation; float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w)) * IN.Attenuation; OUT.rgb = ambient + diffuse + specular; OUT.a = 1.0f; IN.ProjectedTextureCoordinate.xy /= IN.ProjectedTextureCoordinate.w; float3 projectedColor = ProjectedTexture.Sample(ProjectedTextureSampler, IN.ProjectedTextureCoordinate.xy).rgb; OUT.rgb *= projectedColor; return OUT; } /************* Techniques *************/ technique11 project_texture { pass p0 { SetVertexShader(CompileShader(vs_5_0, project_texture_vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_5_0, project_texture_pixel_shader())); SetRasterizerState(BackFaceCulling); } }
在列表19.1所列出的shader中有一些问题需要解决。第一个问题是,projector无论从视锥体哪一边投影,都会产生一个反向投影。图19.4显示了这种结果。
图19.4 Reverse projection.要修改这个问题,需要确保只有在projected texture coordinate的w分量大于或等于0时才进行homogenous divide运算。如下所示:
if (IN.ProjectedTextureCoordinate.w >= 0.0f) { IN.ProjectedTextureCoordinate.xy /= IN.ProjectedTextureCoordinate.w; float3 projectedColor = ProjectedTexture.Sample(ProjectedTextureSampler, IN.ProjectedTextureCoordinate.xy).rgb; OUT.rgb *= projectedColor; }
在列表19.1中另一个需要解决的问题是物体之间的遮挡问题。考虑如图19.5所示的输出结果,两张图中都包含了一个teapot放置到平面的前面,并且每一个object都使用同样的projected texture进行渲染,在上图中,该texture被错误的投影到了平面上被teapot遮挡的区域。在下图中,创建了一个depth map,并用于occlusion testing(遮挡测试)。这种occlusion testing不仅用在平面上,还用于teapot。例如,如果旋转teapot,使得teapot的handle(手柄)被pot(壶身,相对于projector投影方向)遮挡,那么handle就不会接收到projected texture。
列表19.2 Declaration of the DepthMap Class
#pragma once #include "Common.h" #include "RenderTarget.h" namespace Library { class Game; class DepthMap : public RenderTarget { RTTI_DECLARATIONS(DepthMap, RenderTarget) public: DepthMap(Game& game, UINT width, UINT height); ~DepthMap(); ID3D11ShaderResourceView* OutputTexture() const; ID3D11DepthStencilView* DepthStencilView() const; virtual void Begin() override; virtual void End() override; private: DepthMap(); DepthMap(const DepthMap& rhs); DepthMap& operator=(const DepthMap& rhs); Game* mGame; ID3D11DepthStencilView* mDepthStencilView; ID3D11ShaderResourceView* mOutputTexture; D3D11_VIEWPORT mViewport; }; }
列表19.3 Implementation of the DepthMap Class
#include "DepthMap.h" #include "Game.h" #include "GameException.h" namespace Library { RTTI_DEFINITIONS(DepthMap) DepthMap::DepthMap(Game& game, UINT width, UINT height) : RenderTarget(), mGame(&game), mDepthStencilView(nullptr), mOutputTexture(nullptr), mViewport() { D3D11_TEXTURE2D_DESC textureDesc; ZeroMemory(&textureDesc, sizeof(textureDesc)); textureDesc.Width = width; textureDesc.Height = height; textureDesc.MipLevels = 1; textureDesc.ArraySize = 1; textureDesc.Format = DXGI_FORMAT_R24G8_TYPELESS; textureDesc.SampleDesc.Count = 1; textureDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL| D3D11_BIND_SHADER_RESOURCE; HRESULT hr; ID3D11Texture2D* texture = nullptr; if (FAILED(hr = game.Direct3DDevice()->CreateTexture2D(&textureDesc, nullptr, &texture))) { throw GameException("IDXGIDevice::CreateTexture2D() failed.", hr); } D3D11_SHADER_RESOURCE_VIEW_DESC resourceViewDesc; ZeroMemory(&resourceViewDesc, sizeof(resourceViewDesc)); resourceViewDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS; resourceViewDesc.ViewDimension = D3D_SRV_DIMENSION_TEXTURE2D; resourceViewDesc.Texture2D.MipLevels = 1; if (FAILED(hr = game.Direct3DDevice()->CreateShaderResourceView(texture, &resourceViewDesc, &mOutputTexture))) { ReleaseObject(texture); throw GameException("IDXGIDevice::CreateShaderResourceView() failed.", hr); } D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc; ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc)); depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D; depthStencilViewDesc.Texture2D.MipSlice = 0; if (FAILED(hr = game.Direct3DDevice()->CreateDepthStencilView(texture, &depthStencilViewDesc, &mDepthStencilView))) { ReleaseObject(texture); throw GameException("IDXGIDevice::CreateDepthStencilView() failed.", hr); } ReleaseObject(texture); mViewport.TopLeftX = 0.0f; mViewport.TopLeftY = 0.0f; mViewport.Width = static_cast<float>(width); mViewport.Height = static_cast<float>(height); mViewport.MinDepth = 0.0f; mViewport.MaxDepth = 1.0f; } DepthMap::~DepthMap() { ReleaseObject(mOutputTexture); ReleaseObject(mDepthStencilView); } ID3D11ShaderResourceView* DepthMap::OutputTexture() const { return mOutputTexture; } ID3D11DepthStencilView* DepthMap::DepthStencilView() const { return mDepthStencilView; } void DepthMap::Begin() { static ID3D11RenderTargetView* nullRenderTargetView = nullptr; RenderTarget::Begin(mGame->Direct3DDeviceContext(), 1, &nullRenderTargetView, mDepthStencilView, mViewport); } void DepthMap::End() { RenderTarget::End(mGame->Direct3DDeviceContext()); } }
列表19.4 Declaration of the RenderTarget Class
#pragma once #include "Common.h" namespace Library { class RenderTarget : public RTTI { RTTI_DECLARATIONS(RenderTarget, RTTI) public: RenderTarget(); virtual ~RenderTarget(); virtual void Begin() = 0; virtual void End() = 0; protected: typedef struct _RenderTargetData { UINT ViewCount; ID3D11RenderTargetView** RenderTargetViews; ID3D11DepthStencilView* DepthStencilView; D3D11_VIEWPORT Viewport; _RenderTargetData(UINT viewCount, ID3D11RenderTargetView** renderTargetViews, ID3D11DepthStencilView* depthStencilView, const D3D11_VIEWPORT& viewport) : ViewCount(viewCount), RenderTargetViews(renderTargetViews), DepthStencilView(depthStencilView), Viewport(viewport) { } } RenderTargetData; void Begin(ID3D11DeviceContext* deviceContext, UINT viewCount, ID3D11RenderTargetView** renderTargetViews, ID3D11DepthStencilView* depthStencilView, const D3D11_VIEWPORT& viewport); void End(ID3D11DeviceContext* deviceContext); private: RenderTarget(const RenderTarget& rhs); RenderTarget& operator=(const RenderTarget& rhs); static std::stack<RenderTargetData> sRenderTargetStack; }; }
#include "RenderTarget.h" #include "Game.h" namespace Library { RTTI_DEFINITIONS(RenderTarget) std::stack<RenderTarget::RenderTargetData> RenderTarget::sRenderTargetStack; RenderTarget::RenderTarget() { } RenderTarget::~RenderTarget() { } void RenderTarget::Begin(ID3D11DeviceContext* deviceContext, UINT viewCount, ID3D11RenderTargetView** renderTargetViews, ID3D11DepthStencilView* depthStencilView, const D3D11_VIEWPORT& viewport) { sRenderTargetStack.push(RenderTargetData(viewCount, renderTargetViews, depthStencilView, viewport)); deviceContext->OMSetRenderTargets(viewCount, renderTargetViews, depthStencilView); deviceContext->RSSetViewports(1, &viewport); } void RenderTarget::End(ID3D11DeviceContext* deviceContext) { sRenderTargetStack.pop(); RenderTargetData renderTargetData = sRenderTargetStack.top(); deviceContext->OMSetRenderTargets(renderTargetData.ViewCount, renderTargetData.RenderTargetViews, renderTargetData.DepthStencilView); deviceContext->RSSetViewports(1, &renderTargetData.Viewport); } }
注意:原书中RenderTarget::End函数有问题,从stack中弹出之前的RenderTargets和Viewports之后,需要判断stack是否为空,如果为空则通过Game对象重置RenderTargets和Viewports,代码修改如下:
void RenderTarget::End(Game* game) { sRenderTargetStack.pop(); if (sRenderTargetStack.empty()) { game->ResetRenderTargets(); game->ResetViewports(); } else { ID3D11DeviceContext* deviceContext = game->Direct3DDeviceContext(); RenderTargetData renderTargetData = sRenderTargetStack.top(); deviceContext->OMSetRenderTargets(renderTargetData.ViewCount, renderTargetData.RenderTargetViews, renderTargetData.DepthStencilView); deviceContext->RSSetViewports(1, &renderTargetData.Viewport); } }
DepthMap类的使用方法非常简单:只需要把depth map绑定到output-merger阶段,并以projector的投影方向渲染场景(或者场景的一部分)。代码如下:
mDepthMap->Begin(); ID3D11DeviceContext* direct3DDeviceContext = mGame->Direct3DDeviceContext(); direct3DDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); direct3DDeviceContext->ClearDepthStencilView(mDepthMap->DepthStencilView(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); Pass* pass = mDepthMapMaterial->CurrentTechnique()->Passes().at(0); ID3D11InputLayout* inputLayout = mDepthMapMaterial->InputLayouts().at(pass); direct3DDeviceContext->IASetInputLayout(inputLayout); UINT stride = mDepthMapMaterial->VertexSize(); UINT offset = 0; direct3DDeviceContext->IASetVertexBuffers(0, 1, &mModelPositionVertexBuffer, &stride, &offset); direct3DDeviceContext->IASetIndexBuffer(mModelIndexBuffer, DXGI_FORMAT_R32_UINT, 0); XMMATRIX modelWorldMatrix = XMLoadFloat4x4(&mModelWorldMatrix); mDepthMapMaterial->WorldLightViewProjection() << modelWorldMatrix * mProjector->ViewMatrix() * mProjector->ProjectionMatrix(); pass->Apply(0, direct3DDeviceContext); direct3DDeviceContext->DrawIndexed(mModelIndexCount, 0, 0); mDepthMap->End();
列表19.6 The DepthMap.fx Shader
cbuffer CBufferPerObject { float4x4 WorldLightViewProjection; } float4 create_depthmap_vertex_shader(float4 ObjectPosition : POSITION) : SV_Position { return mul(ObjectPosition, WorldLightViewProjection); } technique11 create_depthmap { pass p0 { SetVertexShader(CompileShader(vs_5_0, create_depthmap_vertex_shader())); SetGeometryShader(NULL); SetPixelShader(NULL); } }
#include "include\\Common.fxh" /************* Resources *************/ static const float4 ColorWhite = { 1, 1, 1, 1 }; cbuffer CBufferPerFrame { float4 AmbientColor = { 1.0f, 1.0f, 1.0f, 0.0f }; float4 LightColor = { 1.0f, 1.0f, 1.0f, 1.0f }; float3 LightPosition = { 0.0f, 0.0f, 0.0f }; float LightRadius = 10.0f; float3 CameraPosition; float DepthBias = 0.005; } cbuffer CBufferPerObject { float4x4 WorldViewProjection : WORLDVIEWPROJECTION; float4x4 World : WORLD; float4 SpecularColor : SPECULAR = { 1.0f, 1.0f, 1.0f, 1.0f }; float SpecularPower : SPECULARPOWER = 25.0f; float4x4 ProjectiveTextureMatrix; } Texture2D ColorTexture; Texture2D ProjectedTexture; Texture2D DepthMap; SamplerState DepthMapSampler { Filter = MIN_MAG_MIP_POINT; AddressU = BORDER; AddressV = BORDER; BorderColor = ColorWhite; }; /************* Pixel Shaders *************/ float4 project_texture_w_depthmap_pixel_shader(VS_OUTPUT IN) : SV_Target { float4 OUT = (float4)0; float3 lightDirection = LightPosition - IN.WorldPosition; lightDirection = normalize(lightDirection); float3 viewDirection = normalize(CameraPosition - IN.WorldPosition); float3 normal = normalize(IN.Normal); float n_dot_l = dot(normal, lightDirection); float3 halfVector = normalize(lightDirection + viewDirection); float n_dot_h = dot(normal, halfVector); float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); float4 lightCoefficients = lit(n_dot_l, n_dot_h, SpecularPower); float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb); float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb) * IN.Attenuation; float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w)) * IN.Attenuation; OUT.rgb = ambient + diffuse + specular; OUT.a = 1.0f; if (IN.ProjectedTextureCoordinate.w >= 0.0f) { IN.ProjectedTextureCoordinate.xyz /= IN.ProjectedTextureCoordinate.w; float pixelDepth = IN.ProjectedTextureCoordinate.z; float sampledDepth = DepthMap.Sample(DepthMapSampler, IN.ProjectedTextureCoordinate.xy).x + DepthBias; float3 projectedColor = (pixelDepth > sampledDepth ? ColorWhite.rgb : ProjectedTexture.Sample(ProjectedTextureSampler, IN.ProjectedTextureCoordinate.xy).rgb); OUT.rgb *= projectedColor; } return OUT; } /************* Techniques *************/ technique11 project_texture_w_depthmap { pass p0 { SetVertexShader(CompileShader(vs_5_0, project_texture_vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_5_0, project_texture_w_depthmap_pixel_shader())); SetRasterizerState(BackFaceCulling); } }
图19.6 Output of the projective texture mapping shader, using a depth map and no depth bias.
Shadow acen是self-shadowing的一种错误,是由于shadow map有限的分辨率和量化的深度值导致的。当把实际的depth与smapled depth进行比较时,会产生各种不同的结果。因此,一些pixels depth值表示会收到投影纹理,一些表示会被遮挡。此外,浮点数精度误差也会导致产生shadow acne。一种简单的方法是使用某个固定值对深度值进行补偿。或者是使用一个slope-scaled depth biasing(根据斜率动态调整的深度偏差值),而不是使用一个固定的深度偏差值,在讨论shadow mapping时将会讲述slope-scaled depth biasing技术。