为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Java

几何着色器

首先用一张图来回顾一下渲染管线的各个阶段,目前为止我们接触的着色器有顶点着色器和像素着色器,而接触到的渲染管线阶段有:输入装配阶段、顶点着色阶段、光栅化阶段、像素着色阶段、输出合并阶段.

webp

图1

可以看到,几何着色器是我们在将顶点送入光栅化阶段之前,可以操作顶点的最后一个阶段。它同样也允许我们编写自己的着色器代码。几何着色器可以做如下事情:

让程序自动决定如何在渲染管线中插入/移除几何体;

通过流输出阶段将顶点信息再次传递到顶点缓冲区;

改变图元类型(如输入点图元,输出三角形图元);

但它也有缺点,几何着色器输出的顶点数据很可能是有较多重复的,从流输出拿回到顶点缓冲区的话会占用较多的内存空间。它本身无法输出索引数组。

几何着色阶段会收到一系列代表输入几何体类型的顶点,然后我们可以自由选择其中的这些顶点信息,然后交给流输出对象重新解释成新的图元类型(或者不变),传递给流输出阶段或者是光栅化阶段。而几何着色器仅能够接受来自输入装配阶段提供的顶点信息,对每个顶点进行处理,无法自行决定增减顶点。

注意:离开几何着色器的顶点如果要传递给光栅化阶段,需要包含有转换到齐次裁剪坐标系的坐标信息(语义为SV_POSITION的float4向量)。

可编程的几何着色器

从一个看似没什么用的几何着色器代码入手

若我们直接从VS项目新建一个几何着色器文件,则可以看到下面的代码:

struct GSOutput

{

    float4 pos : SV_POSITION;

};

[maxvertexcount(3)]

void main(

    triangle float4 input[3] : SV_POSITION,

    inout TriangleStream< GSOutput > output

)

{

    for (uint i = 0; i < 3; i++)

    {

        GSOutput element;

        element.pos = input[i];

        output.Append(element);

    }

}

ps. 可能有些人会对void main的写法表示不爽,比如说我。不过这不是C语言的主函数......

若在输入装配阶段指定使用TriangleList图元的话,初步观察该代码,实际上你可以发现其实该着色器只是把输入的顶点按原样输出给流输出对象,即跟什么都没做(咸鱼)有什么区别。。不过从这份代码里面就已经包含了几何着色器所特有的绝大部分语法了。

首先,几何着色器是根据图元类型来进行调用的,若使用的是TriangleList,则每一个三角形的三个顶点都会作为输入,触发几何着色器的调用。这样一个TriangleList解释的30个顶点会触发10次调用。

对于几何着色器,我们必须要指定它每次调用所允许输出的最大顶点数目。我们可以使用属性语法来强行修改着色器行为:

[maxvertexcount(N)]

这里N就是每次调用允许产出的最大顶点数目,然后最终输出的顶点数目不会超过N的值。maxvertexcount的值应当尽可能的小。

关于性能上的表现,我根据龙书提供的引用找到了对应的说明文档:

NVIDIA08

虽然是10年前的文档,这里说到:在GeForce 8800 GTX,一个几何着色器的调用在输出1到20个标量的时候可以达到最大运行性能表现,但是当我们指定最大允许输出标量的数目在27到40个时,性能仅达到峰值的50%。比如说,如果顶点的声明如下:

struct V0

{

    float3 pos : POSITION;

    float2 tex : TEXCOORD;

};

这里每个顶点就已经包含了5个标量了,如果以它作为输出类型,则maxvertexcount为4的时候就可以达到理论上的峰值性能(20个标量)。

但如果顶点类型中还包含有float3类型的法向量,每个顶点就额外包含了3个标量,这样在maxvertexcount为4的时候就输出了32个标量,只有50%的峰值性能表现。

这份文档已经将近10年了,对于那时候的显卡来说使用几何着色器可能不是一个很好的选择,不过当初的显卡也早已不能和现在的显卡相提并论了。

注意:

maxvertexcount的值应当设置到尽可能小的值,因为它将直接决定几何着色器的运行效率。

几何着色器的每次调用最多只能处理1024个标量,对于只包含4D位置向量的顶点来说也只能处理256个顶点。

在HLSL编译器里,如果设置的maxvertexcount过大,会直接收到编译错误:

然后代码中的triangle是用于指定输入的图元类型,具体支持的关键字如下:

图元类型 描述

point Point list

line Line list or line strip

triangle Triangle list or triangle strip

lineadj Line list with adjacency or line strip with adjacency

triangleadj Triangle list with adjacency or triangle strip with adjacency

具体的图元类型可以到第2章回顾:点击此处

而参数类型可以是用户自定义的结构体类型,或者是向量(float4)类型。从顶点着色器传过来的顶点至少会包含一个表示齐次裁剪坐标的向量。

参数名inupt实际上用户是可以任意指定的。

对于该输入参数的元素数目,取决于前面声明的图元类型:

图元类型 元素数目

point [1] 每次只能处理1个顶点

line [2] 一个线段必须包含2个顶点

triangle [3] 一个三角形需要3个顶点

lineadj [4] 一个邻接线段需要4个顶点

triangleadj [6] 一个邻接三角形需要6个顶点

而第二个参数必须是一个流输出对象,而且需要被指定为inout可读写类型。可以看到,它是一个类模板,模板的形参指定要输出的类型。流输出对象有如下三种:

流输出对象类型 描述

PointStream 一系列点的图元

LineStream 一系列线段的图元

TriangleStream 一系列三角形的图元

流输出对象都具有下面两种方法:

方法 描述

Append 向指定的流输出对象添加一个输出的数据

RestartStrip 在以线段或者三角形作为图元的时候,默认是以strip的形式输出的,

如果我们不希望下一个输出的顶点与之前的顶点构成新图元,则需要

调用此方法来重新开始新的strip。若希望输出的图元类型也保持和原

来一样的TriangleList,则需要每调用3次Append方法后就调用一次

RestartStrip。

注意:

所谓的删除顶点,实际上就是不将该顶点传递给流输出对象

若传入的顶点中多余的部分无法构成对应的图元,则抛弃掉这些多余的顶点

在开始前,先放出Basic.fx文件的内容:

#include "LightHelper.hlsli"

cbuffer CBChangesEveryFrame : register(b0)

{

    row_major matrix gWorld;

    row_major matrix gWorldInvTranspose;

}

cbuffer CBChangesOnResize : register(b1)

{

    row_major matrix gProj;

}

cbuffer CBNeverChange : register(b2)

{

    DirectionalLight gDirLight;

    Material gMaterial;

    row_major matrix gView;

    float3 gEyePosW;

    float gCylinderHeight;

}

struct VertexPosColor

{

    float3 PosL : POSITION;

    float4 Color : COLOR;

};

struct VertexPosHColor

{

    float4 PosH : SV_POSITION;

    float4 Color : COLOR;

};

struct VertexPosNormalColor

{

    float3 PosL : POSITION;

    float3 NormalL : NORMAL;

    float4 Color : COLOR;

};

struct VertexPosHWNormalColor

{

    float4 PosH : SV_POSITION;

    float3 PosW : POSITION;

    float3 NormalW : NORMAL;

    float4 Color : COLOR;

};

实战1: 将一个三角形分割成三个三角形

现在我们的目标是把一个三角形分裂成三个三角形:

webp

图2

HLSL代码Triangle_VS.hlsl, Triangle_GS.hlsl和Triangle_PS.hlsl的实现如下:// Triangle_VS.hlsl#include "Basic.fx"VertexPosHColor VS(VertexPosColor pIn){    row_major matrix worldViewProj = mul(mul(gWorld, gView), gProj);    VertexPosHColor pOut;    pOut.Color = pIn.Color;    pOut.PosH = mul(float4(pIn.PosL, 1.0f), worldViewProj);    return pOut;}Triangle_GS.hlsl#include "Basic.fx"[maxvertexcount(9)]void GS(triangle VertexPosHColor input[3], inout TriangleStream output)

{

    //

    // 将一个三角形分裂成三个三角形,即没有v3v4v5的三角形

    //       v1

    //       /\

    //      /  \

    //   v3/____\v4

    //    /\xxxx/\

    //   /  \xx/  \

    //  /____\/____\

    // v0    v5    v2

    VertexPosHColor vertexes[6];

    int i;

    [unroll]

    for (i = 0; i < 3; ++i)

    {

        vertexes[i] = input[i];

        vertexes[i + 3].Color = (input[i].Color + input[(i + 1) % 3].Color) / 2.0f;

        vertexes[i + 3].PosH = (input[i].PosH + input[(i + 1) % 3].PosH) / 2.0f;

    }

    [unroll]

    for (i = 0; i < 3; ++i)

    {

        output.Append(vertexes[i]);

        output.Append(vertexes[3 + i]);

        output.Append(vertexes[(i + 2) % 3 + 3]);

        output.RestartStrip();

    }

}

// Triangle_PS.hlsl

#include "Basic.fx"

float4 PS(VertexPosHColor pIn) : SV_Target

{

    return pIn.Color;

}

这里输入和输出的图元类型都是一致的,但无论什么情况都一定要注意设置好maxvertexcount的值,这里固定一个三角形的三个顶点输出9个顶点(构成三个三角形),并且每3次Append就需要调用1次RestartStrip。

实战2: 通过圆线构造圆柱体侧面

已知图元类型为LineStrip,现在有一系列连续的顶点构成圆线(近似圆弧的连续折线),构造出圆柱体的侧面。即输入图元类型为线段,输出一个矩形(两个三角形)。

webp

图3

思路: 光有顶点位置还不足以构造出圆柱体侧面,因为无法确定圆柱往哪个方向延伸。所以我们还需要对每个顶点引入所在圆柱侧面的法向量,通过叉乘就可以确定上方向/下方向并进行延伸了。


HLSL代码

Cylinder_VS.hlsl, Cylinder_GS.hlsl和Cylinder_PS.hlsl的实现如下:

// Cylinder_VS.hlsl#include "Basic.fx"VertexPosHWNormalColor VS(VertexPosNormalColorpIn){    VertexPosHWNormalColor pOut;row_majormatrixviewProj = mul(gView, gProj);    pOut.PosW = mul(float4(pIn.PosL,1.0f), gWorld).xyz;    pOut.PosH = mul(float4(pOut.PosW,1.0f), viewProj);    pOut.NormalW = mul(pIn.NormalL, (float3x3)gWorldInvTranspose);    pOut.Color = pIn.Color;returnpOut;}

// Cylinder_GS.hlsl#include "Basic.fx"// 一个v0v1线段输出6个三角形顶点[maxvertexcount(6)]void GS(line VertexPosHWNormalColor input[2], inout TriangleStream output){// *****************************// 要求圆线框是顺时针的,然后自底向上构造圆柱侧面           //   -->      v2____v3//  ______     |\   |// /      \    | \  |// \______/    |  \ |//   <--       |___\|//           v1(i1) v0(i0)float3upDir = normalize(cross(input[0].NormalW, (input[1].PosW - input[0].PosW)));    VertexPosHWNormalColor v2, v3;matrixviewProj = mul(gView, gProj);    v2.PosW = input[1].PosW + upDir * gCylinderHeight;    v2.PosH = mul(float4(v2.PosW,1.0f), viewProj);    v2.NormalW = input[1].NormalW;    v2.Color = input[1].Color;    v3.PosW = input[0].PosW + upDir * gCylinderHeight;    v3.PosH = mul(float4(v3.PosW,1.0f), viewProj);    v3.NormalW = input[0].NormalW;    v3.Color = input[0].Color;    output.Append(input[0]);    output.Append(input[1]);    output.Append(v2);    output.RestartStrip();    output.Append(v2);    output.Append(v3);    output.Append(input[0]);}

// Cylinder_PS.hlsl#include "Basic.fx"float4PS(VertexPosHWNormalColor pIn) : SV_Target{// 标准化法向量pIn.NormalW = normalize(pIn.NormalW);// 顶点指向眼睛的向量float3toEyeW = normalize(gEyePosW - pIn.PosW);// 初始化为0 float4ambient =float4(0.0f,0.0f,0.0f,0.0f);float4diffuse =float4(0.0f,0.0f,0.0f,0.0f);float4spec =float4(0.0f,0.0f,0.0f,0.0f);// 只计算方向光ComputeDirectionalLight(gMaterial, gDirLight, pIn.NormalW, toEyeW, ambient, diffuse, spec);returnpIn.Color * (ambient + diffuse) + spec;}


实战3: 画出顶点的法向量

画出顶点的法向量可以帮助你进行调试,排查法向量是否出现了问题。这时候图元的类型为PointList,需要通过几何着色器输出一个线段(两个顶点)。由于顶点中包含法向量,剩下的就是要自行决定法向量的长度。

下图的法向量长度为0.5



作者:久伴必知情深
链接:https://www.jianshu.com/p/a02061c596da


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消