一、概述
有关着色器部分我们基本可以对其进行一下划分:
第一梯队(基础):顶点着色器,像素着色器(片元着色器)
第二梯队(进阶):几何着色器,曲面细分着色器
特殊用途(独立):计算着色器
第一梯队是所有喜欢图形的人都会涉猎和了解的,比如学习Unity Shader,可以说都在写顶点着色器和像素着色器,基本语法非常类似C语言,重点是计算的逻辑,比如顶点着色器里边的空间变换以及像素着色器里边的光照计算。
而对于第二梯队以及特殊的计算着色器,这往往就是进阶的话题,他不太像注重渲染特效的人要关注的话题,更像是重视效率的开发者进行的数学游戏。
二、几何着色器结构解析
1.几何着色器的用途
将点,线,面等完整几何图元作为输入,通过对图元的加工(丰富细节或者删除),形成新的顶点列表(经过了MVP变换的),得到新的输出图元。
将一种图元变换为另一种图元。
2.基本结构
[maxvertexcount(N)]//几何着色器输出点列表最大点数量
void GS(
PrimitiveType InputVertexStruct param[vertexNum],
inout StreamOutputObject<OutputVertexType> outputValue
)
{
//几何着色器实现
}
参数1:
PrimitiveType:点线面(point/line/triangle/lineadj/triangleadj)
InputVertexStruct:顶点着色器的输出类型
param[vertexNum]:组成输入图元的点数组
参数2:
inout:输入输出标记
StreamOutputObject:流泛型(PointStream/LineStream/TriangleStream)
OutputVertexType:传入下一阶段(比如PS)的点数据结构体类型
outputValue:点列表,类似于一个vector数组,使用Append添加新的点,注意加点顺序满足左手系顺时针绕序,如果不能满足要重新设置起点(使用函数RestartStrip())
为了形象,看下示例:
其中VertexOut就是顶点着色器的输出结构体(原来要传入像素着色器的顶点结构体),现在传入几何着色器,之后计算数据还是要继续往下使用GeoOut进行传输:
可以看出GeoOut与VertexOut比较相似,因为原来是指望用VertexOut向PS传输数据,现在是想通过GeoOut来传输数据,并且要求此时必须要包含齐次坐标系下的坐标(经过MVP),所以有一些拓展。
具体的几何着色器的主体基本逻辑:
- 针对传入的点数组,计算新图元的点;
- .新图元点变换到齐次坐标空间;
- 按照左手系顺时针绕序Append(注意我们在第一部分介绍的偶数次前两个索引颠倒)。
- 不能一次性Append构建的要使用ReStartStrip()函数。
- 根据一些条件,如果不选择输出任何数据,则表示销毁图元。
有关几何着色器的编译与绑定PSO都和VS及PS一致:
三、实践:公示牌效果
这部分的逻辑很简单:
1.将点作为GS输入,称为公示牌中心点;
2.根据相机坐标与中心点计算视线w,设置上向量v(0,1,0),叉乘得到右向量u;
3.中心点沿上下左右扩散出贴图角点,进行MVP投影,Append作为新图元点列表,传入PS;
关键代码如下:
struct VertexOut
{
float3 CenterW : POSITION;
float2 SizeW : SIZE;
};
struct GeoOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float2 TexC : TEXCOORD;
uint PrimID : SV_PrimitiveID;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// 构建传入GS的数据
return vout;
}
[maxvertexcount(4)]
void GS(point VertexOut gin[1],
uint primID : SV_PrimitiveID, //旧图元变为新图元但是编号ID不变,系统自动填充
inout TriangleStream<GeoOut> triStream)
{
//计算正对视线的平面
float3 up = float3(0.0f, 1.0f, 0.0f);
float3 look = gEyePosW - gin[0].CenterW;
look.y = 0.0f; // y-axis aligned, so project to xz-plane
look = normalize(look);
float3 right = cross(up, look);
//计算新图元角点
float halfWidth = 0.5f*gin[0].SizeW.x;
float halfHeight = 0.5f*gin[0].SizeW.y;
float4 v[4];
v[0] = float4(gin[0].CenterW + halfWidth*right - halfHeight*up, 1.0f);
v[1] = float4(gin[0].CenterW + halfWidth*right + halfHeight*up, 1.0f);
v[2] = float4(gin[0].CenterW - halfWidth*right - halfHeight*up, 1.0f);
v[3] = float4(gin[0].CenterW - halfWidth*right + halfHeight*up, 1.0f);
//对应纹理坐标
float2 texC[4] =
{
float2(0.0f, 1.0f),
float2(0.0f, 0.0f),
float2(1.0f, 1.0f),
float2(1.0f, 0.0f)
};
GeoOut gout;
[unroll]
for(int i = 0; i < 4; ++i)
{
gout.PosH = mul(v[i], gViewProj);//MVP变换
gout.PosW = v[i].xyz;
gout.NormalW = look;
gout.TexC = texC[i];
gout.PrimID = primID;
triStream.Append(gout);
}
}
注意primID的使用规则:
1.没有几何着色器,可在PS中使用;
2.如果有几何着色器,必须先在几何着色器中显示传递给PS,PS才能使用。(如果PS不使用可以全程不用)OK,先在GS已经大发神威了,把数据计算完了要往下到PS:
//注意输入参数就是GS的输出
float4 PS(GeoOut pin) : SV_Target
{
float3 uvw = float3(pin.TexC, pin.PrimID%4);
float4 diffuseAlbedo = gTreeMapArray.Sample(gsamAnisotropicWrap, uvw) * gDiffuseAlbedo;
//PS光照计算等
return litColor;
}
四、题外话
纹理数组:
注意此处使用的纹理数组gTreeMapArray,这是将多个纹理打包为一个数组传入显存供流水线使用的方法,在加载文理的地方可以通过DepthOrArraySize进行设置(我们以前总是设置为1),这里的纹理坐标uvw,uv表示纹理坐标,w表示纹理索引值,使用纹理数组的好处在于减少重新设置纹理及调用绘制的次数,提高效率。有关纹理数组的细节可以好好看看龙书,因为这个部分实际是美工来给我们准备的吧。
alpha-to-coverage
MSAA计算覆盖情况时考虑alpha通道,实现基于alpha通道的多重采样
(通常的MSAA是基于子像素几何覆盖性的)
要点:
1).启用MSAA(默认DX11以上就是启用的);
2).treeSpritePsoDesc.BlendState.AlphaToCoverageEnable = true;