DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果
原文:
DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果
前言上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区。接下来我们继续利用它来实现一些新的效果,在这一章,你将了解:
在此之前需要额外了解的章节如下:
DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 实现雾效虽然这部分与几何着色器并没有什么关系,但是雾的效果在该Demo中会用到,并且前面也没有讲过这部分内容,故先在这里提出来。 有时候我们需要在游戏中模拟一些特定的天气条件,比如说大雾。它可以让物体平滑出现而不是突然蹦出来那样(物体的一部分留在视锥体内使得只能看到该部分,然后在逐渐靠近该物体的时候,该物体就像经过了一个无形的扫描门被逐渐构造出来那样)。通过让雾在某一范围内具有一定的层次(让不可见区域比视锥体裁剪区域还近),我们可以避免上面所说的情况。但即便是晴朗的天气,你可能仍希望包含一个较广范围的雾效,即距离达到很远的地方才逐渐看不清物体。 我们可以使用这种方式来实现雾效:指定雾的颜色,以摄像机为原点的雾开始的最小距离,雾效范围值(超过起始距离+雾效范围值的范围外的颜色皆被指定的雾色取代)。在需要绘制的三角形内,某一像素片元的颜色如下: 该函数对应HLSL中的lerp函数,s取0的时候最终颜色为litColor,然后逐渐增大并逼近1的时候,最终颜色就逐渐趋近于fogColor。然后参数s的值取决于下面的函数: 其中dist(p,E)指的是两点之间的距离值。配合下面的图去理解: 还有注意一点,在每次清空重新绘制的时候,要用雾的颜色进行清空。 HLSL代码与雾效相关的值存储在下面的常量缓冲区中,并且绘制3D物体的顶点没有发生变化: // Basic.fx // ... cbuffer CBDrawingStates : register(b2) { float4 g_FogColor; int g_FogEnabled; float g_FogStart; float g_FogRange; float g_Pad2; } // ... struct VertexPosNormalTex { float3 PosL : POSITION; float3 NormalL : NORMAL; float2 Tex : TEXCOORD; }; struct VertexPosHWNormalTex { float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD; };
// Basic_VS.hlsl #include "Basic.hlsli" // 顶点着色器 VertexPosHWNormalTex VS(VertexPosNormalTex vIn) { VertexPosHWNormalTex vOut; matrix viewProj = mul(g_View,g_Proj); vector posW = mul(float4(vIn.PosL,1.0f),g_World); vOut.PosW = posW.xyz; vOut.PosH = mul(posW,viewProj); vOut.NormalW = mul(vIn.NormalL,(float3x3) g_WorldInvTranspose); vOut.Tex = vIn.Tex; return vOut; } 而 // Basic_PS.hlsl #include "Basic.hlsli" // 像素着色器 float4 PS(VertexPosHWNormalTex pIn) : SV_Target { // 提前进行裁剪,对不符合要求的像素可以避免后续运算 float4 texColor = g_Tex.Sample(g_Sam,pIn.Tex); clip(texColor.a - 0.05f); // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 求出顶点指向眼睛的向量,以及顶点与眼睛的距离 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); float distToEye = distance(g_EyePosW,pIn.PosW); // 初始化为0 float4 ambient = float4(0.0f,0.0f,0.0f); float4 diffuse = float4(0.0f,0.0f); float4 spec = float4(0.0f,0.0f); float4 A = float4(0.0f,0.0f); float4 D = float4(0.0f,0.0f); float4 S = float4(0.0f,0.0f); [unroll] for (int i = 0; i < 4; ++i) { ComputeDirectionalLight(g_Material,g_DirLight[i],pIn.NormalW,toEyeW,A,D,S); ambient += A; diffuse += D; spec += S; } float4 litColor = texColor * (ambient + diffuse) + spec; // 雾效部分 [flatten] if (g_FogEnabled) { // 限定在0.0f到1.0f范围 float fogLerp = saturate((distToEye - g_FogStart) / g_FogRange); // 根据雾色和光照颜色进行线性插值 litColor = lerp(litColor,g_FogColor,fogLerp); } litColor.a = texColor.a * g_Material.Diffuse.a; return litColor; } 对于白天来说,我们可以使用 而对于黑夜来说,这个雾效更像是战争迷雾的效果,我们使用 具体的演示效果在最后可以看到。 树的公告板效果当一棵树离摄像机太远的话,我们可以使用公告板技术,用一张树的贴图来进行绘制,取代原来绘制3D树模型的方式。首先我们给出树的纹理贴图组成: 关注Alpha通道部分,白色区域指代Alpha值为1.0(完全不透明),而黑色区域指代Alpha值0.0(完全透明)。所以在渲染树纹理的时候,我们只需要对Alpha值为0.0的像素区域进行裁剪即可。 实现公告板的关键点在于:公告板要永远正向摄像机(即视线要与公告板表面垂直),使得用户的视线在x0z面上的投影一直与贴图表面垂直。这样做省去了大量顶点的输入和处理,显得更加高效,并且这个小技巧还能够欺骗玩家让人误以为还是原来的3D模型(眼尖的玩家还是有可能认得出来),只要你别一开始就告诉人家这棵树的绘制用了公告板原理就行了(→_→)。 现在不考虑坐标系的Y轴部分(即从上方俯视),从下面的图可以看到,公告板投影的中心部分的法向量是直接指向摄像机的。 因此我们可以得到公告板的u轴,v轴和w轴单位向量以及根据公告板构建的局部坐标系: 然后已知中心顶点位置、树宽度和高度,就可以求得2D树矩形的四个顶点了: // 计算出公告板矩形的四个顶点 // up // v1___|___v3 // | | | // right__|___| | // |__/____| // v0 / v2 // look v[0] = float4(center + halfWidth * right - halfHeight * up,1.0f); v[1] = float4(center + halfWidth * right + halfHeight * up,1.0f); v[2] = float4(center - halfWidth * right - halfHeight * up,1.0f); v[3] = float4(center - halfWidth * right + halfHeight * up,1.0f); 注意上面的加减运算是针对 若现在我们需要绘制公告板,则在输入的时候仅提供对应的中心顶点,然后图元类型选择 HLSL代码下面是 // Basic.hlsli #include "LightHelper.hlsli" Texture2D g_Tex : register(t0); Texture2DArray g_TexArray : register(t1); SamplerState g_Sam : register(s0); cbuffer CBChangesEveryDrawing : register(b0) { matrix g_World; matrix g_WorldInvTranspose; Material g_Material; } cbuffer CBChangesEveryFrame : register(b1) { matrix g_View; float3 g_EyePosW; float g_Pad; } cbuffer CBDrawingStates : register(b2) { float4 g_FogColor; int g_FogEnabled; float g_FogStart; float g_FogRange; float g_Pad2; } cbuffer CBChangesOnResize : register(b3) { matrix g_Proj; } cbuffer CBChangesRarely : register(b4) { DirectionalLight g_DirLight[5]; PointLight g_PointLight[5]; SpotLight g_SpotLight[5]; } struct VertexPosNormalTex { float3 PosL : POSITION; float3 NormalL : NORMAL; float2 Tex : TEXCOORD; }; struct VertexPosHWNormalTex { float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD; }; struct PointSprite { float3 PosW : POSITION; float2 SizeW : SIZE; }; struct BillboardVertex { float4 PosH : SV_POSITION; float3 PosW : POSITION; float3 NormalW : NORMAL; float2 Tex : TEXCOORD; uint PrimID : SV_PrimitiveID; }; 而几何着色器的代码如下: // Billboard_GS.hlsl #include "Basic.hlsli" // 节省内存资源,先用float4向量声明。 static const float4 g_Vec[2] = { float4(0.0f,1.0f,0.0f),float4(1.0f,0.0f) }; static const float2 g_TexCoord[4] = (float2[4])g_Vec; [maxvertexcount(4)] void GS(point PointSprite input[1],uint primID : SV_PrimitiveID,inout TriangleStream<BillboardVertex> output) { // 计算公告板所处的局部坐标系,其中公告板相当于 // 被投影在了局部坐标系的xy平面,z=0 float3 up = float3(0.0f,0.0f); float3 look = g_EyePosW - input[0].PosW; look.y = 0.0f; // look向量只取投影到xz平面的向量 look = normalize(look); float3 right = cross(up,look); // 计算出公告板矩形的四个顶点 // up // v1 ___|___ v3 // | | | // right__|___| | // | / | // |_/_____| // v0 / v2 // look float4 v[4]; float3 center = input[0].PosW; float halfWidth = 0.5f * input[0].SizeW.x; float halfHeight = 0.5f * input[0].SizeW.y; v[0] = float4(center + halfWidth * right - halfHeight * up,1.0f); v[1] = float4(center + halfWidth * right + halfHeight * up,1.0f); v[2] = float4(center - halfWidth * right - halfHeight * up,1.0f); v[3] = float4(center - halfWidth * right + halfHeight * up,1.0f); // 对顶点位置进行矩阵变换,并以TriangleStrip形式输出 BillboardVertex gOut; matrix viewProj = mul(g_View,g_Proj); [unroll] for (int i = 0; i < 4; ++i) { gOut.PosW = v[i].xyz; gOut.PosH = mul(v[i],viewProj); gOut.NormalW = look; gOut.Tex = g_TexCoord[i]; gOut.PrimID = primID; output.Append(gOut); } } 首先一开始不用float2数组是因为每个float2元素会单独打包,浪费了一半的空间,因此这里采取一种特殊的语法形式使得内存可以得到充分利用。 然后要注意 图元ID现在讲述系统值 在上面的例子中,我们将一个顶点产生的矩形四个顶点都标记为同一个图元ID,是因为到后续的像素着色器中,我们用该图元ID映射到纹理数组的索引值,来对应到要绘制的树的纹理。
float4 PS(Vertex3DOut pin,uint primID : SV_PrimitiveID) : SV_Target { // Pixel shader body… } 但如果像素着色器提供了图元ID,渲染管线又绑定了几何着色器,则几何着色器必须提供该参数。在几何着色器中你可以使用或修改图元ID值。 顶点ID紧接着是系统值 VertexOut VS(VertexIn vin,uint vertID : SV_VertexID) { // vertex shader body… } 最后给出像素着色器的代码: // Billboard_PS.hlsl #include "Basic.hlsli" float4 PS(BillboardVertex pIn) : SV_Target { // 每4棵树一个循环,尽量保证出现不同的树 float4 texColor = g_TexArray.Sample(g_Sam,float3(pIn.Tex,pIn.PrimID % 4)); // 提前进行裁剪,对不符合要求的像素可以避免后续运算 clip(texColor.a - 0.05f); // 标准化法向量 pIn.NormalW = normalize(pIn.NormalW); // 求出顶点指向眼睛的向量,以及顶点与眼睛的距离 float3 toEyeW = normalize(g_EyePosW - pIn.PosW); float distToEye = distance(g_EyePosW,S); ambient += A; diffuse += D; spec += S; } float4 litColor = texColor * (ambient + diffuse) + spec; // 雾效部分 [flatten] if (g_FogEnabled) { // 限定在0.0f到1.0f范围 float fogLerp = saturate((distToEye - g_FogStart) / g_FogRange); // 根据雾色和光照颜色进行线性插值 litColor = lerp(litColor,fogLerp); } litColor.a = texColor.a * g_Material.Diffuse.a; return litColor; } 这里加上了刚才的雾效,并使用了纹理数组。如果你对纹理数组这部分内容不熟悉的话,请回到开头阅读"深入理解与使用2D纹理资源"中的纹理数组部分 Alpha-To-Coverage在Demo运行的时候,仔细观察可以发现树公告板的某些边缘部分有一些比较突出的黑边。 这是因为当前默认使用的是Alpha Test,即HLSL中使用clip函数将Alpha值为0的像素点给剔除掉,这些像素也不是树的一部分。该函数决定某一像素是留下还是抛弃,这会导致不平滑的过渡现象,在摄像机逐渐靠近该纹理时,图片本身也在不断放大,硬边部分也会被放大,就像下面那张图: 当然,你也可以使用透明混合的方式,但是透明混合对绘制的顺序是有要求的,要求透明物体按从后到前的顺序进行绘制,即需要在绘制透明物体前先对物体按到摄像机的距离排个序。当然如果需要绘制大量的草丛的话,这种方法所需要的开销会变得非常大,操作起来也十分麻烦。 当然,我们可以考虑下使用MSAA(多重采样抗锯齿),并配合Alpha Test进行。MSAA可以用于将多边形的锯齿边缘平滑处理,然后让Direct3D开启alpha-to-coverage技术,标记边缘部分。 在创建后备缓冲区、深度/模板缓冲区前需要打开4倍多重采样的支持,目前的项目在d3dApp中已经默认开启好了。 然后在之前的例子里,我们已经在 D3D11_BLEND_DESC blendDesc; ZeroMemory(&blendDesc,sizeof(blendDesc)); auto& rtDesc = blendDesc.RenderTarget[0]; // Alpha-To-Coverage模式 blendDesc.AlphaToCoverageEnable = true; blendDesc.IndependentBlendEnable = false; rtDesc.BlendEnable = false; rtDesc.RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; HR(device->CreateBlendState(&blendDesc,BSAlphaToCoverage.ReleaseAndGetAddressOf())); 然后只需要在需要的时候绑定该状态即可。 BasicEffect的变化BasicEffect::SetRenderBillboard方法--公告板绘制该方法要考虑输入的是一系列顶点图元: void BasicEffect::SetRenderBillboard(ID3D11DeviceContext * deviceContext,bool enableAlphaToCoverage) { deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST); deviceContext->IASetInputLayout(pImpl->m_pVertexPosSizeLayout.Get()); deviceContext->VSSetShader(pImpl->m_pBillboardVS.Get(),nullptr,0); deviceContext->GSSetShader(pImpl->m_pBillboardGS.Get(),0); deviceContext->RSSetState(RenderStates::RSNoCull.Get()); deviceContext->PSSetShader(pImpl->m_pBillboardPS.Get(),0); deviceContext->PSSetSamplers(0,RenderStates::SSLinearWrap.GetAddressOf()); deviceContext->OMSetDepthStencilState(nullptr,0); deviceContext->OMSetBlendState( (enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr),0xFFFFFFFF); } 参数 GameApp类的变化GameApp::InitPointSpritesBuffer方法--初始化存放点精灵的缓冲区该方法会生成20个顶点,均匀并略带随机性地环绕在原点周围。这些顶点一经创建就不可以被修改了,它们将会被用于公告板的创建: void GameApp::InitPointSpritesBuffer() { srand((unsigned)time(nullptr)); VertexPosSize vertexes[16]; float theta = 0.0f; for (int i = 0; i < 16; ++i) { // 取20-50的半径放置随机的树 float radius = (float)(rand() % 31 + 20); float randomRad = rand() % 256 / 256.0f * XM_2PI / 16; vertexes[i].pos = XMFLOAT3(radius * cosf(theta + randomRad),8.0f,radius * sinf(theta + randomRad)); vertexes[i].size = XMFLOAT2(30.0f,30.0f); theta += XM_2PI / 16; } // 设置顶点缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd,sizeof(vbd)); vbd.Usage = D3D11_USAGE_IMMUTABLE; // 数据不可修改 vbd.ByteWidth = sizeof (vertexes); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = 0; // 新建顶点缓冲区 D3D11_SUBRESOURCE_DATA InitData; ZeroMemory(&InitData,sizeof(InitData)); InitData.pSysMem = vertexes; HR(m_pd3dDevice->CreateBuffer(&vbd,&InitData,mPointSpritesBuffer.GetAddressOf())); } GameApp::InitResource方法--初始化资源该方法集成了所有资源的初始化,注意树的纹理数组要提供到输入槽1,对应纹理寄存器t1的 bool GameApp::InitResource() { // ****************** // 初始化各种物体 // // 初始化树纹理资源 ComPtr<ID3D11Texture2D> test; HR(CreateDDSTexture2DArrayFromFile( m_pd3dDevice.Get(),m_pd3dImmediateContext.Get(),std::vector<std::wstring>{ L"Texturetree0.dds",L"Texturetree1.dds",L"Texturetree2.dds",L"Texturetree3.dds"},test.GetAddressOf(),mTreeTexArray.GetAddressOf())); m_BasicEffect.SetTextureArray(mTreeTexArray.Get()); // 初始化点精灵缓冲区 InitPointSpritesBuffer(); // 初始化树的材质 m_TreeMat.ambient = XMFLOAT4(0.5f,0.5f,1.0f); m_TreeMat.diffuse = XMFLOAT4(1.0f,1.0f); m_TreeMat.specular = XMFLOAT4(0.2f,0.2f,16.0f); ComPtr<ID3D11ShaderResourceView> texture; // 初始化地板 m_Ground.SetBuffer(m_pd3dDevice.Get(),Geometry::CreatePlane(XMFLOAT3(0.0f,-5.0f,XMFLOAT2(100.0f,100.0f),XMFLOAT2(10.0f,10.0f))); HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(),L"TextureGrass.dds",texture.GetAddressOf())); m_Ground.SetTexture(texture.Get()); Material material{}; material.ambient = XMFLOAT4(0.5f,1.0f); material.diffuse = XMFLOAT4(1.0f,1.0f); material.specular = XMFLOAT4(0.2f,16.0f); m_Ground.SetMaterial(material); // ****************** // 初始化不会变化的值 // // 方向光 DirectionalLight dirLight[4]; dirLight[0].ambient = XMFLOAT4(0.1f,0.1f,1.0f); dirLight[0].diffuse = XMFLOAT4(0.25f,0.25f,1.0f); dirLight[0].specular = XMFLOAT4(0.1f,1.0f); dirLight[0].direction = XMFLOAT3(-0.577f,-0.577f,0.577f); dirLight[1] = dirLight[0]; dirLight[1].direction = XMFLOAT3(0.577f,0.577f); dirLight[2] = dirLight[0]; dirLight[2].direction = XMFLOAT3(0.577f,-0.577f); dirLight[3] = dirLight[0]; dirLight[3].direction = XMFLOAT3(-0.577f,-0.577f); for (int i = 0; i < 4; ++i) m_BasicEffect.SetDirLight(i,dirLight[i]); // ****************** // 初始化摄像机 // auto camera = std::shared_ptr<FirstPersonCamera>(new FirstPersonCamera); m_pCamera = camera; camera->SetViewPort(0.0f,(float)m_ClientWidth,(float)m_ClientHeight); camera->SetPosition(XMFLOAT3()); camera->SetFrustum(XM_PI / 3,AspectRatio(),1000.0f); camera->LookTo( XMVectorSet(0.0f,XMVectorSet(0.0f,0.0f)); camera->UpdateViewMatrix(); m_BasicEffect.SetWorldMatrix(XMMatrixIdentity()); m_BasicEffect.SetViewMatrix(camera->GetViewXM()); m_BasicEffect.SetProjMatrix(camera->GetProjXM()); m_BasicEffect.SetEyePos(camera->GetPositionXM()); // ****************** // 初始化雾效和天气等 // m_BasicEffect.SetFogState(m_FogEnabled); m_BasicEffect.SetFogColor(XMVectorSet(0.75f,1.0f)); m_BasicEffect.SetFogStart(15.0f); m_BasicEffect.SetFogRange(75.0f); return true; } 其余方法限于篇幅就不放在这里了,读者可以查看源码观察剩余部分的代码实现。现在来看实现效果吧。 实现效果可以观察到,在与公告版近距离接触时可以很明显地看到公告板在跟着摄像机旋转。如果距离很远的话转动的幅度就会很小,用户才会比较难以分辨出远处物体是否为公告板或3D模型了。 下面演示了白天和黑夜的雾效 最后则是Alpha-To-Coverage的开启/关闭效果对比 DirectX11 With Windows SDK完整目录 Github项目源码 欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- Windows – 链接到User32.dll时链接错误2001
- .net – 在开发过程中发送测试电子邮件,没有垃圾
- windows-phone-7 – 如果我更改现有WP应用程序的
- windows-7 – 通过复制硬盘重新映像几台相同型号
- windows-server-2008 – 无关的_msdcs.正向查找区
- windows-server-2012 – 安装.NET Framework 4.5
- windows-server-2008-r2 – 如何在已经共享的文件
- windows – 需要一种可以驱动两台打印机的快速编
- UWP使用Skype来拨打电话号码
- windows-server-2008 – Windows Server 2008 R2