前言

上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区。接下来我们继续利用它来实现一些新的效果,在这一章,你将了解:

  1. 实现公告板效果
  2. Alpha-To-Coverage
  3. 对GPU资源进行读/写操作
  4. 纹理数组
  5. 实现雾效

在此之前需要额外了解的章节如下:

章节回顾
深入理解与使用2D纹理资源(重点阅读纹理数组)
15 几何着色器初探

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

实现雾效

虽然这部分与几何着色器并没有什么关系,但是雾的效果在该Demo中会用到,并且前面也没有讲过这部分内容,故先在这里提出来。

有时候我们需要在游戏中模拟一些特定的天气条件,比如说大雾。它可以让物体平滑出现而不是突然蹦出来那样(物体的一部分留在视锥体内使得只能看到该部分,然后在逐渐靠近该物体的时候,该物体就像经过了一个无形的扫描门被逐渐构造出来那样)。通过让雾在某一范围内具有一定的层次(让不可见区域比视锥体裁剪区域还近),我们可以避免上面所说的情况。但即便是晴朗的天气,你可能仍希望包含一个较广范围的雾效,即距离达到很远的地方才逐渐看不清物体。

我们可以使用这种方式来实现雾效:指定雾的颜色,以摄像机为原点的雾开始的最小距离,雾效范围值(超过起始距离+雾效范围值的范围外的颜色皆被指定的雾色取代)。在需要绘制的三角形内,某一像素片元的颜色如下:

\[\begin{align}
foggedColor &= litColor + s(fogColor - litColor)\\
&= (1-s) \cdot litColor + s \cdot fogColor\\
\end{align}
\]

该函数对应HLSL中的lerp函数,s取0的时候最终颜色为litColor,然后逐渐增大并逼近1的时候,最终颜色就逐渐趋近于fogColor。然后参数s的值取决于下面的函数:

\[s = saturate(\frac{dist(\mathbf{p},\mathbf{E}) - fogStart}{fogRange}) \\
saturate(x) =
\begin{cases}
x, 0 \le x \le 1\\
0, x < 0\\
1, x > 1\\
\end{cases}
\]

其中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也与之前一样,没有什么变动:

// 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现在使用了4盏方向光以保证4种不同方向的光能够均匀照射,并添加了雾效部分的处理:

// 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, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 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;
}

对于白天来说,我们可以使用RGBA=(0.75f, 0.75f, 0.75f, 1.0f)来作为雾的颜色。

而对于黑夜来说,这个雾效更像是战争迷雾的效果,我们使用RGBA=(0.0f, 0.0f, 0.0f, 1.0f)来作为雾的颜色,这样远处的物体我们就让它看不见,而在可视范围内,距离越远的物体能见度越低。

具体的演示效果在最后可以看到。

树的公告板效果

当一棵树离摄像机太远的话,我们可以使用公告板技术,用一张树的贴图来进行绘制,取代原来绘制3D树模型的方式。首先我们给出树的纹理贴图组成:

关注Alpha通道部分,白色区域指代Alpha值为1.0(完全不透明),而黑色区域指代Alpha值0.0(完全透明)。所以在渲染树纹理的时候,我们只需要对Alpha值为0.0的像素区域进行裁剪即可。

实现公告板的关键点在于:公告板要永远正向摄像机(即视线要与公告板表面垂直),使得用户的视线在x0z面上的投影一直与贴图表面垂直。这样做省去了大量顶点的输入和处理,显得更加高效,并且这个小技巧还能够欺骗玩家让人误以为还是原来的3D模型(眼尖的玩家还是有可能认得出来),只要你别一开始就告诉人家这棵树的绘制用了公告板原理就行了(→_→)。

现在不考虑坐标系的Y轴部分(即从上方俯视),从下面的图可以看到,公告板投影的中心部分的法向量是直接指向摄像机的。

因此我们可以得到公告板的u轴, v轴和w轴单位向量以及根据公告板构建的局部坐标系:

\[\begin{align}
\mathbf{w}&=\frac{(E_x-C_x,0,E_z-C_z)}{E_x-C_x,0,E_z-C_z} \\
\mathbf{v}&=(0,1,0) \\
\mathbf{u}&=\mathbf{v}\times\mathbf{w}
\end{align}
\]

然后已知中心顶点位置、树宽度和高度,就可以求得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);

注意上面的加减运算是针对float3进行的,然后用1.0f填充成4D向量。并且由于每个公告板所处的局部坐标系不一样,我们需要对它们分别计算出对应的坐标轴向量。

若现在我们需要绘制公告板,则在输入的时候仅提供对应的中心顶点,然后图元类型选择D3D11_PRIMITIVE_TOPOLOGY_POINTLIST,在几何着色阶段我们直接将顶点直传到几何着色阶段,这些顶点传递给几何着色器后就会解释成一个个矩形(两个三角形),产生公告板。

HLSL代码

下面是Basic.hlsli的完整代码:

// 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, 0.0f), float4(1.0f, 1.0f, 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, 1.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元素会单独打包,浪费了一半的空间,因此这里采取一种特殊的语法形式使得内存可以得到充分利用。

然后要注意maxvertexcount的值要设为4,尽管Append的次数为4,但实际上输出的三角形顶点数为6。

图元ID

现在讲述系统值SV_PrimitiveID,我们可以将它作为函数的额外形参进行提供。它告诉我们在输入装配阶段下自动分配的图元ID值。当我们调用了一个draw方法,需要绘制n个图元,那么第一个图元对应的ID值为0,第二个为1,直到最后一个为n-1.当前的所有图元ID仅在当前的单次调用绘制是唯一的。其中该系统值的写入操作允许在几何着色器和像素着色器进行,而读取操作则允许在几何/像素/外壳/域着色器中进行。

在上面的例子中,我们将一个顶点产生的矩形四个顶点都标记为同一个图元ID,是因为到后续的像素着色器中,我们用该图元ID映射到纹理数组的索引值,来对应到要绘制的树的纹理。

注意: 如果几何着色器没有提供图元ID,在像素着色器中也可以将它加进参数列表中以使用:

float4 PS(Vertex3DOut pin, uint primID : SV_PrimitiveID) : SV_Target
{
// Pixel shader body…
}

但如果像素着色器提供了图元ID,渲染管线又绑定了几何着色器,则几何着色器必须提供该参数。在几何着色器中你可以使用或修改图元ID值。

顶点ID

紧接着是系统值SV_VertexID,在输入装配阶段的时候渲染管线就会为这些输入的顶点分配顶点ID值。若使用的是Draw方法,则这些顶点将会按顺序从0到n-1被标记(n为顶点数目);若使用的是DrawIndexed方法,则顶点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, pIn.PosW); // 初始化为0
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 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;
}

这里加上了刚才的雾效,并使用了纹理数组。如果你对纹理数组这部分内容不熟悉的话,请回到开头阅读"深入理解与使用2D纹理资源"中的纹理数组部分

Alpha-To-Coverage

在Demo运行的时候,仔细观察可以发现树公告板的某些边缘部分有一些比较突出的黑边。

这是因为当前默认使用的是Alpha Test,即HLSL中使用clip函数将Alpha值为0的像素点给剔除掉,这些像素也不是树的一部分。该函数决定某一像素是留下还是抛弃,这会导致不平滑的过渡现象,在摄像机逐渐靠近该纹理时,图片本身也在不断放大,硬边部分也会被放大,就像下面那张图:

当然,你也可以使用透明混合的方式,但是透明混合对绘制的顺序是有要求的,要求透明物体按从后到前的顺序进行绘制,即需要在绘制透明物体前先对物体按到摄像机的距离排个序。当然如果需要绘制大量的草丛的话,这种方法所需要的开销会变得非常大,操作起来也十分麻烦。

当然,我们可以考虑下使用MSAA(多重采样抗锯齿),并配合Alpha Test进行。MSAA可以用于将多边形的锯齿边缘平滑处理,然后让Direct3D开启alpha-to-coverage技术,标记边缘部分。

在创建后备缓冲区、深度/模板缓冲区前需要打开4倍多重采样的支持,目前的项目在d3dApp中已经默认开启好了。

然后在之前的例子里,我们已经在RenderStates类中预先创建好了混合状态:

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(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pBillboardPS.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(
(enableAlphaToCoverage ? RenderStates::BSAlphaToCoverage.Get() : nullptr),
nullptr, 0xFFFFFFFF); }

参数enableAlphaToCoverage决定是否要绑定渲染状态对象RenderStates::BSAlphaToCoverage

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的Texture2DArray

bool GameApp::InitResource()
{
// ******************
// 初始化各种物体
// // 初始化树纹理资源
HR(CreateDDSTexture2DArrayFromFile(
m_pd3dDevice.Get(),
m_pd3dImmediateContext.Get(),
std::vector<std::wstring>{
L"Texture\\tree0.dds",
L"Texture\\tree1.dds",
L"Texture\\tree2.dds",
L"Texture\\tree3.dds"},
nullptr,
mTreeTexArray.GetAddressOf()));
m_BasicEffect.SetTextureArray(mTreeTexArray.Get()); // 初始化点精灵缓冲区
InitPointSpritesBuffer(); // 初始化树的材质
m_TreeMat.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_TreeMat.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
m_TreeMat.specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f); ComPtr<ID3D11ShaderResourceView> texture;
// 初始化地板
m_Ground.SetBuffer(m_pd3dDevice.Get(), Geometry::CreatePlane(XMFLOAT2(100.0f, 100.0f), XMFLOAT2(10.0f, 10.0f)));
m_Ground.GetTransform().SetPosition(0.0f, -5.0f, 0.0f);
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\Grass.dds", nullptr, texture.GetAddressOf()));
m_Ground.SetTexture(texture.Get());
Material material{};
material.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
material.diffuse = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
material.specular = XMFLOAT4(0.2f, 0.2f, 0.2f, 16.0f);
m_Ground.SetMaterial(material); // ******************
// 初始化不会变化的值
// // 方向光
DirectionalLight dirLight[4];
dirLight[0].ambient = XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
dirLight[0].diffuse = XMFLOAT4(0.25f, 0.25f, 0.25f, 1.0f);
dirLight[0].specular = XMFLOAT4(0.1f, 0.1f, 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, 0.577f);
dirLight[2] = dirLight[0];
dirLight[2].direction = XMFLOAT3(0.577f, -0.577f, -0.577f);
dirLight[3] = dirLight[0];
dirLight[3].direction = XMFLOAT3(-0.577f, -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, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
camera->SetPosition(XMFLOAT3());
camera->SetFrustum(XM_PI / 3, AspectRatio(), 1.0f, 1000.0f);
camera->LookTo(XMFLOAT3(), XMFLOAT3(0.0f, 0.0f, 1.0f), XMFLOAT3(0.0f, 1.0f, 0.0f)); 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, 0.75f, 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,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果的更多相关文章

  1. DirectX11 With Windows SDK--15 几何着色器初探

    前言 从这一部分开始,感觉就像是踏入了无人深空一样,在之前初学DX11的时候,这部分内容都是基本上跳过的,现在打算重新认真地把它给拾回来. DirectX11 With Windows SDK完整目录 ...

  2. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  3. 翻译:探索GLSL-用几何着色器(着色器库)实现法线可视化

    翻译:探索GLSL-用几何着色器(着色器库)实现法线可视化 翻译自: Exploring GLSL – Normal Visualizer with Geometry Shaders (Shader ...

  4. Unity 几何着色器

    Unity 几何着色器 shaderGeometry Shader几何着色器 Unity 几何着色器 如果学习不能带来价值,那将毫无意义 简介     在顶点和片段着色器之间有一个可选的着色器,叫做几 ...

  5. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十二章:几何着色器(The Geometry Shader) 代码工 ...

  6. opengl 几何着色器

    绘制4条线段 #define GLEW_STATIC #include <GL/glew.h> #include <GLFW/glfw3.h> #include "S ...

  7. DirectX11 Windows Windows SDK--28 计算着色器:波浪(水波)

    前言 有关计算着色器的基础其实并不是很多.接下来继续讲解如何使用计算着色器实现水波效果,即龙书中所实现的水波.但是光看代码可是完全看不出来是在做什么的.个人根据书中所给的参考书籍找到了对应的实现原理, ...

  8. DirectX11 With Windows SDK--00 目录

    前言 (更新于 2019/4/10) 从第一次接触DirectX 11到现在已经有将近两年的时间了.还记得前年暑假被要求学习DirectX 11,在用龙书的源码配置项目运行环境的时候都花了好几天的时间 ...

  9. DirectX11 With Windows SDK--35 粒子系统

    前言 在这一章中,我们主要关注的是如何模拟一系列粒子,并控制它们运动.这些粒子的行为都是类似的,但它们也带有一定的随机性.这一堆粒子的几何我们叫它为粒子系统,它可以被用于模拟一些比较现象,如:火焰.雨 ...

随机推荐

  1. javascript Json和String互转

      var jsonText = "{\"id\":\"123\",\"name\":\"tom\",\&qu ...

  2. kernel笔记——内核编译与进程管理

    内核与操作系统 由于一些商业操作系统设计上的缺陷以及日益庞杂,“操作系统”的概念对很多人而言变得含糊不清.在进一步讨论Linux内核的话题前,我们先区分“内核”与“操作系统”这两个概念. 操作系统:指 ...

  3. centos7下kubernetes(11。kubernetes-运行一次性任务)

    容器按照持续运行的时间可以分为两类:服务类容器和工作类容器 服务类容器:持续提供服务 工作类容器:一次性任务,处理完后容器就退出 Deployment,replicaset和daemonset都用于管 ...

  4. 如何判断app的页面是原生的还是H5的webview页面

    1.看布局边界(在手机侧观察) 开发者选项->显示布局边界,页面元素很多的情况下布局是一整块的是h5的,布局密密麻麻的是原生控件.页面有布局的是原生的,否则为h5页面.(仅针对安卓手机试用)如下 ...

  5. Jenkins持续集成实践之java项目自动化部署

    关于Linux安装Jenkins可以参考我的这篇博文Ubuntu16.04环境安装jenkins 1.安装部署插件 进入插件管理,并搜索该插件Deploy to container Plugin进行安 ...

  6. Python脱产8期 Day13 2019/4/28

    一 函数的嵌套定义 1在一个函数的内部定义另一个函数. 2.为什么有函数的嵌套定义: # 1)函数fn2想直接使用fn1函数的局部变量,可以讲fn2直接定义到fn1的内部,这样fn2就可以直接访问fn ...

  7. SpringMVC项目读取不到外部CSS文件的解决办法及总结

    昨天,在写一个新需求的时候,因为没有前端同事的对接,无奈只有自己写css,js放到ssm结构的后台项目中,因为之前开发都是把前端文件放在一个专门的服务器上,一直没有关注服务端项目读取静态资源的问题,运 ...

  8. 基于Metronic的Bootstrap开发框架--工作流模块功能介绍(2)

    本篇继续<基于Metronic的Bootstrap开发框架--工作流模块功能介绍>,继续介绍基于Metronic的Bootstrap开发框架的工作模块功能,介绍工作流模块中相关业务表单的界 ...

  9. 几行c#代码,轻松搞定一个女大学生

    几行c#代码,轻松搞定一个女大学生 的作业... 哈哈,标题党了哈,但是是真的,在外面敲代码,想赚点外快,接到了一个学生的期末考试,是一个天气预报的程序.程序并不难. 看到这个需求第一个想法就是只要找 ...

  10. 一.从零认识XAML

    一.XMAL的简单了解 XAML读做zaml,,它是WPF中专门用于设计UI的语言,它简单易懂,结构清晰.是一种声明式语言,当你见到一个标签时,就意味着声明了一个对象:对象之间的层级关系要么是并列,要 ...