TA探索第一个月——曲面细分着色器

记录本人在第一个UntiyChan项目后摸索学习的一个月的复杂过程


在跟随百人计划学习过程中,关于曲面细分部分,在实践过程中在发现一个好东西:Unity Tutorials and Portfolio - Roystan 跟随里面的细分草地进行学习

教程很细致我就不在这里多写,只按照顺序写一下自己的心得:

几何细分草地

首先要生成草地,直接点就是建模型,但这样的代价太大无论是人力还是算力很多时候都不值得。如果把草抽象一下,起始我们只需要一堆很窄的三角就可以了,把他们放在一起就差不多了。而这个功能几何着色器刚好可以胜任。

几何着色器

几何着色器是来应用阶段后(模型调整,参数设置等),光栅化前(连续的形状变成离散的片)的几何阶段(顶点相关计算)中一个可选功能。

他的作用就是图原的顶点进行变换,默认什么都不变,原来是三角形的三个点就按顺序输出三角形的三个点。但也可以将一个点变成两个点,输出一个线段,三角形的三个顶点就会输出6个线段;也可以一个顶点变成三个顶点,每个顶点都再长出一个三角形,这就是我们想要的效果。

函数看起来是这样:

#pragma geometry geo

...

[maxvertexcount(3)]
void geo(triangle vertexOutput IN[3], inout TriangleStream<geometryOutput> triStream)
{
}

[maxvertexcount(n)]中的n 是告诉GPU一个图元的所以顶点输进去,会有多少顶点输出来。由于我们要三角形里一个点变出个三个点就行了,所以填3,如果想每个顶点都多变三个顶点就要填9了。

我们输入的图元是三角形所以第一个是triangle,第二个是顶点着色器输出的数据,因为我的的数据是先经过顶点着色器计算才传到这一步的,完整顺序这样的:

下面一个inout 关键字很好猜,这个参数还会被输出出去,所以我们要做的就是对化名为triStream的变量进行操作。

把一个顶点变成三个输出:

float3 pos = IN[0];


// Update each assignment of o.pos.
o.pos = UnityObjectToClipPos(pos + float3(0.5, 0, 0));
triStream.Append(o);

o.pos = UnityObjectToClipPos(pos + float3(-0.5, 0, 0));
triStream.Append(o);

o.pos = UnityObjectToClipPos(pos + float3(0, 1, 0));
triStream.Append(o);

这样我们就用一个顶点(IN[0])变出了三个顶点构成一个三角形。就会这样:

根据高度进行颜色过度得到:

这其中还有一个切线空间转换的过程,但转换矩阵的数学原理不懂,就不多说了。如果我们的位移是希望相对一个局部顶点而言了话,使用这个转换一下比较好。

目前已经基本达到了我们最开始的设想,我们可以调节生成的三角形的形状让他跟细长一些并随机一些,再增加一些随机的旋转和倒伏,为了让三角形可以有弯下去的能力,可以增加顶点数量进行更细微的控制:

这时我们的[maxvertexcount(n)]中的n就要写7了,当然,还可以继续增加。

最后你可以得到这样的大概这样的效果:

具体的算法去大佬博客学更好。

这张图片的三角形明显多了很多,根据我们的实现原理,这意味着这个平面有很多顶点,但这显然就不是在unity点creat->plane可以做的了。但我们也不需要特地去建一个很多顶点的模型,因为我们渲染管线中还有一个可选的着色器:曲面细分着色器


曲面细分着色器

不讲太深入的(太深入的我也还不懂呢),曲面细分着色器,可以把一个三角形图元的每一个边(线段)平均或以别的方式分成几个小段,再将这些分好的点连成更小的三角形,这样就得到的更多的顶点同时了。更多的顶点除了在这个项目中可以有更多的草三角,还可以让模型在游戏中的顶点调整更加精细,就像一根钢筋变成锁链,就可以表达更多的曲线细节。

那么简单问一些这里面的细节:怎么知道分成几段?除了平均分还可以怎么分,怎么控制?我会按照我的经验和在网上的学习浅薄的解释一下。

我们从上到下看看与之前不同的地方做了什么:

...
#pragma target 4.6
#pragma hull hullProgram
#pragma domain ds

#include "Tessellation.cginc"
...

struct TessVertex{
    float4 vertex : INTERNALTESSPOS;
    float3 normal : NORMAL;
    float4 tangent :TANGENT;
    float2 uv: TEXCOORD0;
};
struct OutputPatchConstant {
        float edge[3]:SV_TESSFACTOR;
        float inside: SV_INSIDETESSFACTOR;
    };
...

float _tellesllation;
OutputPatchConstant hsconst(InputPatch<TessVertex, 3> patch)
{
    OutputPatchConstant o;
    o.edge[0] = _tellesllation;
    o.edge[1] = _tellesllation;
    o.edge[2] = _tellesllation;
    o.inside = _tellesllation;     
    return o;
}
...

[UNITY_domain("tri")] // 确定图元,quad,triangle等
[UNITY_partitioning("fractional_odd")] // 拆分edge的规则, equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")] // 一个patch一共三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] // 不同的图元对应不同的控制点
TessVertex hullProgram(InputPatch<TessVertex, 3> patch, uint id:SV_OutputControlPointID)
{
    return patch[id];
}


[UNITY_domain("tri")]
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> patch, float3 bary : SV_DOMAINLOCATION)
{
    VertexInput v;
    v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
    v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
    v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
    v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
    VertexOutput o = vert(v);
    return o;
}

#pragma target 4.6

这是一个编译命令,指定shader支持的最小Shader Modle 版本,而Shader Modle 版本定义GPU 能够支持的功能特性。如果我们的版本不够是不能编译的(虽然我试了一下只是会有警告,可能做了处理吧)


#pragma hull hullProgram  #pragma domain ds

这个编译命令制定细分着色器额外需要实现的部分:外壳着色器(Hull Shader)和域着色器(Domain Shader)

事实上,整个细分着色器是由三个部分组成的:外壳着色器,镶嵌器,域着色器,但镶嵌器是不可编程。


外壳着色器(Hull shader)

这里又分成两份,一个常量外壳着色器用来制定细分的份数,一个控制点外壳着色器用来对原本的图元的控制点进行修改

float _tellesllation;
OutputPatchConstant hsconst(InputPatch<TessVertex, 3> patch)
{
    OutputPatchConstant o;
    o.edge[0] = _tellesllation;
    o.edge[1] = _tellesllation;
    o.edge[2] = _tellesllation;
    o.inside = _tellesllation;     
    return o;
}

这里我把设置希望每个边被分成_tellesllation段,同时每个边对应的中线也别分成_tellesllation段,有分出的这些顶点来细分三角形。

[UNITY_domain("tri")] // 确定图元,quad,triangle等
[UNITY_partitioning("interger")] // 拆分edge的规则, equal_spacing,fractional_odd,fractional_even
[UNITY_outputtopology("triangle_cw")]
[UNITY_patchconstantfunc("hsconst")] // 一个patch一共三个点,但是这三个点都共用这个函数
[UNITY_outputcontrolpoints(3)] // 不同的图元对应不同的控制点
TessVertex hullProgram(InputPatch<TessVertex, 3> patch, uint id:SV_OutputControlPointID)
{
    return patch[id];
}

这里我们告诉unity:

  • 我的图元是三角形(tri)
  • 各个边使用平均分的方式分成n段(向上取整)(interger)还有其他细分方案fractional_even,fractional_odd...
  • 各个细分三角形的顶点按照顺时针方向输出,这可以用来判断正反面
  • 我设置的细分参数通过hsconst这个函数获取
  • 我输入的图元对应的控制点有3个

这里我们也发现了我们在#pragma中告诉unity的hull shader 其实是控制点外壳着色器,这里我们只将图元的控制点直接输出,我们也可以进行一些修改,如把他向y+移动一下:

 TessVertex hullProgram(InputPatch<TessVertex, 3> patch, uint id:SV_OutputControlPointID)
{
    TessVertex o;
    o.vertex = patch[id].vertex+float4(0,2,0,0);
    o.normal = patch[id].normal;
    o.tangent = patch[id].tangent;
    o.uv = patch[id].uv;

    return o;
}

这个操作没什么意义,只是打破固有思维而已


域着色器

[UNITY_domain("tri")]
VertexOutput ds (OutputPatchConstant tessFactors, const OutputPatch<TessVertex, 3> patch, float3 bary : SV_DOMAINLOCATION)
{
    VertexInput v;
    v.vertex = patch[0].vertex*bary.x + patch[1].vertex*bary.y + patch[2].vertex*bary.z;
    v.tangent = patch[0].tangent*bary.x + patch[1].tangent*bary.y + patch[2].tangent*bary.z;
    v.normal = patch[0].normal*bary.x + patch[1].normal*bary.y + patch[2].normal*bary.z;
    v.uv = patch[0].uv*bary.x + patch[1].uv*bary.y + patch[2].uv*bary.z;
    VertexOutput o = vert(v);
    return o;
}

每当有新的坐标通过镶嵌器就会调用一次域着色器。

这里域着色器使用镶嵌器生成的信息来真正计算出新的顶点的位置,是的,镶嵌阶段其实没有真的生成顶点,而是计算出了新顶点的重心坐标float3 bary : SV_DOMAINLOCATION)。我们利用这个来实际计算顶点空间坐标以及其他信息(其实直接加权计算就好了)。

他的输入分别是细分的参数(我们所指定的),图元信息(依旧是最开始的图元信息),重心坐标。

重心坐标可视化一下就是

实际我们也可以在这里进行一些更复杂的运算,而不是简单的加权。可以看另一个大佬的文章,讲的很细致:https://zhuanlan.zhihu.com/p/629364817


最后在加一下光影就OK了。

但也是我这一个月的开始。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值