当前位置: 首页 > news >正文

[学习记录]Unity-Shader-曲面细分着色器

什么是曲面细分着色器?        

        曲面细分着色器是 DirectX 11 (以及 OpenGL 4.0) 引入的、位于顶点着色器和几何着色器之间可编程管线阶段。它的核心目的是在 GPU 上动态地增加模型的几何细节,而不需要在 CPU 端提前创建高模。

        左图是Unity中4个可编程着色器的执行顺序,右图进一步反映了曲面细分着色器的内部结构。

曲面细分管线的三个主要阶段

        曲面细分过程涉及三个关键的可编程着色器阶段,以及一个由硬件实现的固定功能阶段:

1.外壳着色器(Hull Shader - HS) —>2.曲面细分器(Tessellator) —>3.域着色器(Domain Shader - DS)

        其中Tessellation中两个可编程的Shader是外壳着色器hs域着色器ds。曲面细分器(Tessellator)阶段不可编程。

1.外壳着色器(Hull Shader) 

        外壳着色器是DirectX中的概念,OpenGL/Vulkan中习惯称为细分控制着色器(TCS-Tessellation Control Shader)

        主要作用是为曲面细分过程进行设置,具体包括定义“补丁”(Patch,即曲面细分的输入图元)如何被细分,以及为随后的域着色器(Domain Shader)准备必要的控制点数据。

1.主外壳着色器函数

[控制点阶段 - Control Point Phase]

1.核心作用

        主外壳着色器函数会为输入补丁的每个控制点执行一次。它的目的是输出每个控制点的属性,这些属性将传递给域着色器。还指定了每个补丁输出的控制点数量。

2.主外壳着色器属性

        主外壳函数必须有5个属性。 缺少任何一个HLSL 编译器就会报错,无法正确理解和配置曲面细分管线中的 Hull Shader 阶段。

主外壳函数常见格式(见下)

    [domain("tri")][partitioning("integer")][outputtopology("triangle_cw")][patchconstantfunc("hullConst")][outputcontrolpoints(3)]Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID){return input[id];}
1.[domain("parameter")]

[必需]告诉硬件补丁的几何类型。

parameter定义了输出补丁的几何类型。它告诉曲面细分器和域着色器如何解释补丁,并据此生成参数化坐标。

可选参数

(1)"tri"(Triangle / 三角形)

用途: 最常见的类型,用于细分由 3 个控制点定义的三角形补丁。

参数化: 硬件会生成重心坐标(u,v,w) ,其中u+v+w=1。域着色器会使用这些坐标在三角形内部进行插值。

(2)"quad"(Quadrilateral / 四边形)

用途: 用于细分由 4 个控制点定义的四边形补丁。

参数化: 硬件会生成(u,v)坐标,其中u和v的范围通常都在[0,1]之间。域着色器会使用这些坐标在四边形内部进行插值。

(3)"isoline"(Isoline / 孤立线)

用途: 用于细分由 2 个控制点定义的线段补丁。常用于生成光滑的曲线,如头发、电线、管道等。

参数化: 硬件会生成(u)坐标,通常范围在[0,1]之间。域着色器使用这个u值在两个控制点之间插值。

2.[partitioning("parameter")]

[必需]指定细分因子如何被转换为实际的细分步长。没有这个,硬件不知道如何执行细分。

parameter属性定义了细分的分区模式,即细分因子如何被取整并应用于实际的细分步骤。它影响了细分后生成网格的均匀性和过渡平滑度。

可选参数

(1)"integer"(整数细分)

特点: 生成的细分网格较为均匀,但过渡可能会有阶梯感。

示例: 如果计算出的细分因子是 4.7,则实际细分步数为 4。

用途: 这是最常用且性能稳定的模式。细分因子会向下取整到最近的整数

(2)"fractional_even"(分数偶数细分)

用途: 旨在提供更平滑的 LOD 过渡。细分因子会被取整到最近的偶数

示例: 细分因子 4.7 变为 4;5.3 变为 6。

特点: 细分级别在变化时,可以减少几何体“跳跃”的感觉。

(3)"fractional_odd"(分数奇数细分)

用途: 同样旨在提供更平滑的 LOD 过渡。细分因子会被取整到最近的奇数

示例: 细分因子 4.7 变为 5;5.3 变为 5。

特点: 与fractional_even类似,但保持奇数细分。

(4)"pow"(2 的幂细分)

用途: 细分因子会被取整到 2 的最近幂次

示例: 细分因子 4.7 变为 4;5.3 变为 4。

特点: 这种模式在某些特定渲染技术或优化中可能会用到。

3.[outputtopology("parameter")] 

[必需]定义细分器生成的小三角形的缠绕顺序。这直接影响光栅化阶段的背面剔除。没有这个,可能会导致渲染错误或性能问题。

可选参数

(1)"triangle_cw"(Clockwise Triangle / 顺时针三角形)

用途: 曲面细分器生成的小三角形的顶点顺序为顺时针。

注意: 如果你的渲染状态设置为剔除顺时针面(如Cull Back),那么通常会渲染逆时针面。因此,如果你希望细分后的模型保持正常可见,需要根据你的剔除设置进行匹配。

(2)"triangle_ccw"(Counter-Clockwise Triangle / 逆时针三角形)

用途: 曲面细分器生成的小三角形的顶点顺序为逆时针。注意: 如果你的渲染状态设置为剔除逆时针面,则通常会渲染顺时针面。

(3)"line"(Line / 线段)

用途: 当domain为“isoline”时使用,表示生成的是线段。

4.[patchconstantfunc("function_name")]

[必需]指定计算补丁常量的函数。这是 Hull Shader 阶段的核心职责之一,没有这个函数就无法计算细分因子。

function_name(任意合法的 HLSL 函数名)

用途: 这里的参数是你自己定义的补丁常量函数名。

要求: 这个函数必须存在于你的 HLSL 代码中,并且其签名(输入参数和返回类型)必须符合补丁常量函数的约定(通常输入是一个InputPatch,返回一个包含细分因子和自定义补丁常量的结构体)。

5.[outputcontrolpoints(N)]

[必需]定义每个输出补丁有多少个控制点。是 Hull Shader 阶段的输出结构,也是域着色器接收输入的依据。没有这个,GPU 不知道要传递多少数据给下一个阶段。

可选参数:

N(一个正整数)

用途:N是一个整数,表示每个输出补丁中控制点的精确数量。

示例:当domain("isoline")时,N通常是2;当domain("quad")时,N通常是4;当domain("tri")时,N通常是3。

注意: 这个数字必须与domain属性定义的补丁类型相匹配。如果主外壳着色器函数在处理输入控制点的同时,还改变了控制点的数量(比如从 3 个输入控制点计算并输出 4 个新的控制点来定义一个特殊的曲面),那么这里的N就应该反映输出的控制点数量。

3.函数输入

主外壳函数通常接收以下输入:

(1)InputPatch<VertexType,N_Input>

这是最主要的输入,代表了进入 Hull Shader 的原始补丁

VertexType定义了输入顶点(控制点)的数据结构,这通常是顶点着色器 (Vertex Shader) 的输出结构。例如Attributes结构体。

N_Input是一个整数,表示输入补丁中的控制点数量。例如3表示一个三角形补丁,4表示一个四边形补丁。

作用: 通过这个InputPatch可以访问补丁中所有原始控制点的属性,例如input[0].positionOS,input[1].normalOS等。

(2)uint id:SV_OutputControlPointID

SV_OutputControlPointID是一个系统值语义,提供当前正在处理的输出控制点的索引

作用: 如果你指定[outputcontrolpoints(N_Output)],那么这个id会从0循环到N_Output-1,从而能够独立地为每个输出控制点计算或设置属性。

(3)uint patchID:SV_PrimitiveID(可选)

SV_PrimitiveID也是一个系统值语义,提供了当前正在处理的补丁的唯一 ID

作用: 如果你需要根据补丁的唯一性进行一些操作(比如从全局缓冲区读取补丁特有的数据,或者为每个补丁生成不同的随机值),这个patchID就非常有用。

4.函数输出

主外壳函数的输出类型通常与它所处理的输入控制点类型保持一致

[...]
[...]
[...]
[...]
[...]
Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID)
{// 或者进行一些简单的修改,但返回类型仍是 AttributesAttributes outputControlPoint = input[id];//对控制点的变换,例如,让控制点沿着Y轴轻微摆动outputControlPoint.positionOS.y += sin(_Time.y); return outputControlPoint;}

也可直接 return input[id];

[...]
[...]
[...]
[...]
[...]
Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID)
{// 最常见的简单转发:直接返回对应 id 的输入控制点return input[id];
}

2.补丁常量函数

[细分因子阶段 - Tessellation Factor Phase]

        这个函数会为每个补丁执行一次。计算细分因子 (Tessellation Factors),这些因子控制着几何体细分的程度,以及计算任何其他对整个补丁通用的“补丁常量”数据。

注意:这两个phase在硬件上 并行(parallel) 执行。

1.核心作用

每个补丁执行一次:

(1)计算细分因子(Tessellation Factors): 最重要的任务。负责计算补丁的边缘细分因子 (SV_TessFactor)内部细分因子 (SV_InsideTessFactor)。这些因子是浮点值,决定了硬件曲面细分器将如何将该补丁细分成更小的三角形。

这些因子通常是根据摄像机距离、屏幕空间误差、模型曲率等动态计算的,以实现自适应的 LOD。

(2)计算补丁级常量数据: 任何对整个补丁来说是常量的数据(不随补丁内单个控制点变化,但在补丁之间可能不同)都可以在这里计算,并传递给域着色器。

        比如像经常使用的PN-Triangles 算法所需的贝塞尔控制点和法线控制点,一般会放在补丁常量函数中计算。

2.函数输入

补丁常量函数通常接收以下输入:

(1)InputPatch<VertexType,N_Input>

与主外壳函数相同,它接收整个原始补丁作为输入。(主外壳函数和补丁常量函数是并行的)

作用: 允许访问补丁中所有原始控制点的属性,这是进行补丁级别计算的基础(例如,计算整个补丁的平均值、质心,或者像 PN-Triangles 那样利用所有控制点计算新的贝塞尔控制点)

(2)uint patchID:SV_PrimitiveID

SV_PrimitiveID:补丁的唯一 ID 在这里也可用。

作用: 与主外壳函数类似,用于访问补丁特有的数据或生成随机性。

3. 函数输出

        补丁常量函数的(计算结果)返回类型通常是一个自定义结构体,这个结构体必须包含细分因子语义。

自定义输出结构体举例

struct HsConstantOutput
{float edgeTess[N_Edges]    : SV_TessFactor;      // 边缘细分因子float insideTess          : SV_InsideTessFactor; // 内部细分因子// 以下是自定义的补丁常量数据,例如:float3 f3B210 : POS3;// ... 任何其他对整个补丁来说是常量的数据
};

3.细分因子语义

它们都直接控制着曲面细分的级别。其中:

SV_TessFactor 边缘细分因子语义。它用于传递补丁每条边的细分程度。

SV_InsideTessFactor内部细分因子语义。它用于传递补丁内部的细分密度。

        虽然控制着补丁的不同区域,但根本目的都是设定硬件曲面细分器的输入,决定最终生成的几何体细节量。

1.SV_TessFactor(边缘细分因子)

(1)作用:SV_TestFactor是一个数组 (常见如float fTessFactor[3] 用于三角形补丁),定义了补丁每条边的细分级别。

        这个值指示硬件曲面细分器将对应的边分割成多少段。(例如,如果fTessFactor[0]=5,那么补丁的第一条边就会被细分成 5 等份)

(2)对内部的影响: 边缘细分因子间接影响了补丁内部的网格生成。硬件曲面细分器会尝试根据所有边缘的细分因子来填充内部,确保内部网格与所有边缘对齐。

(3)边界一致性: 这是SV_TessFactor最重要的一个方面。如果两个相邻的补丁共用一条边,并且它们的细分因子在这条共用边上是相同的,那么细分后的几何体在这条公共边界上会完美对齐,不会出现裂缝或不连续。这是实现平滑、无缝几何体的关键。

2.SV_InsideTessFactor(内部细分因子)

(1)作用:SV_InsideTessFactor是一个浮点值,定义了补丁内部区域的细分级别。

        它控制了补丁内部生成的三角形网格的密度。对于三角形补丁,这个值决定了补丁内部到中心点的径向细分程度。

(2)独立性:SV_InsideTessFactor可以独立于SV_TessFactor设置。

2.镶嵌器阶段(Tessellator Stage)

        镶嵌器是一个固定功能阶段, 这意味我们无法对这一阶段进行任何控制,它全权交由给硬件处理。镶嵌器阶段接收常量外壳着色器输出的曲面细分因子,对面片进行镶嵌化处理。然后,它将镶嵌后生成的顶点传递给域着色器。

下方非常直观的图来源:(90 封私信 / 80 条消息) Unity 曲面细分着色器详解 - 知乎

        对于三角形面片,常量着色器的边缘细分因子分别指示右/下/左边的段数,内部细分因子指示三角形各边中线的段数:

        对于四边形面片,常量着色器的边缘细分因子分别指示左/上/右/下边的段数,内部细分因子分别指示横向和纵向中线的段数:

3.域着色器(Domain Shader)

域着色器函数常见格式

[domain("tri")]Varyings domain(
HsConstantOutput hsConst, 
const OutputPatch<Attributes, 3> i,
float3 bary : SV_DomainLocation)
{Varyings o = (Varyings)0;// 位置插值位置float4 interpolatedPosOS = i[0].positionOS * bary.z +i[1].positionOS * bary.x +i[2].positionOS * bary.y;// 将位置从对象空间转换到裁剪空间o.positionCS = TransformObjectToHClip(interpolatedPosOS);//其他操作return o;}

1.域类型声明

[domain("domain_type")]

这是域着色器函数的第一个属性,告诉 GPU 这个域着色器处理哪种类型的补丁。

可选类型

"tri"(Triangle - 三角形): 最常用的域类型。表示域着色器处理的是三角形补丁。

"quad"(Quad - 四边形): 用于处理四边形补丁。

"isoline"(Isoline - 独立线): 用于处理线段补丁。

2.函数参数

域着色器函数通常有三个主要参数

(1) HsConstantOutput(可选)

[补丁常量](可选)

类型: 必须匹配外壳着色器(Hull Shader)中由patchconstfunc指定的补丁常量函数的返回类型。

作用: 接收外壳着色器的补丁常量函数计算出的每个补丁的全局数据,例如细分因子,以及更高级细分算法(如 PN-Triangles)所需的贝塞尔控制点。

语义: 无需特定语义,因为它是通过名称/类型匹配来接收的。

可选性: 如果你的域着色器不需要任何补丁常量数据,可以省略这个参数。但在大多数实际应用中,为了获取细分因子或贝塞尔控制点,这个参数是必不可少的。

示例:

// 假设 Hull Shader 的补丁常量函数返回 HsConstantOutput
HsConstantOutput hsConst, // 接收补丁常量

(2)InputPatch<ControlPointType,NumControlPoints> patch

[补丁控制点]

类型:InputPatch<T,N>是 HLSL的类型,用于表示一个补丁。

T:是单个控制点的类型,必须与外壳着色器主函数的输出类型相匹配(例如可能是Attributes结构体)。

N:是补丁中控制点的数量。对于三角形补丁是3,四边形补丁是4。

作用: 这个参数包含了定义当前补丁的原始控制点的数组。域着色器通过这些控制点的位置、法线、UV 等属性来插值生成细分后的顶点。

语义: 无需特定语义,因为它也是通过类型匹配来接收的。

示例:

const OutputPatch<Attributes, 3> i, // 接收3个Attributes类型的控制点

注意:通常使用const关键字表示这是一个只读的输入。在 Unity URP 中,OutputPatch通常用于表示外壳着色器“输出”到域着色器的数据,尽管它在域着色器中是输入。

(3) floatN domainLocation:SV_DomainLocation

[域位置 / 参数化坐标]

类型:float/float2/float3具体取决于domain_type。

语义:SV_DomainLocation是一个系统值语义,它是必需的。GPU 硬件细分器会自动为每个生成的顶点填充这个值。(对于float3 bary:SV_DomainLocation 这里的bary指的就是输入三角形的重心坐标

作用: 这个参数是细分器的输出,它告诉域着色器当前正在处理的这个新顶点位于补丁的哪个参数化位置

示例:

float3 bary : SV_DomainLocation // 三角形域使用重心坐标

        如果domain_type是"tri"(三角形),domainLocation是float3,通常表示重心坐标 (u, v, w),例如flaot3 bary: SV_DomainLocation,bary.x,bary.​​​y,bary.z分别对应补丁的三个原始顶点(通常是patch[1],patch[2],patch[0]的权重,具体映射可能因驱动或约定而异,但x+y+z=1)。

        如果domain_type是"quad"(四边形),domainLocation是float2,通常表示 UV 坐标 (u, v),例如flaot2 uv_domain: SV_DomainLocation。

        如果domain_type是"isoline"(独立线),domainLocation是float,例如float t: SV_DomainLocation ,表示新顶点在线段上的位置 (0.0 到 1.0)。

4.简单应用总结

        这里实现一个最简单的网格细分功能Shader,仅利用了曲面细分着色器,通过暴露两个属性分别控制补丁边上的细分程度和补丁内部的三角形网格的密度。效果如下:

Unity版本:6000.0.43f1   

Shader "Unlit/TessTest"
{Properties{_MainTex ("Texture", 2D) = "white" {}_TessFactor("TessFactor",Float)=1_InsideTessFactor("InsideTessFactor",Float)=1}SubShader{Tags { "RenderType"="Opaque" }LOD 100Pass{HLSLPROGRAM#pragma vertex vert#pragma fragment frag#pragma  hull hull#pragma  domain domain#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"struct Attributes{float4 positionOS : POSITION;//(可选)float2 uv : TEXCOORD0;//float3 normalOS:NORMAL;};struct Varyings{float4 positionCS : SV_POSITION;//(可选)float2 uv : TEXCOORD0;//float3 normalWS:TEXCOORD1;//float3 positionWS:TEXCOORD2;};//常量属性区CBUFFER_START(unityperMaterial)float4 _MainTex_ST;float _TessFactor;float _InsideTessFactor;CBUFFER_ENDTEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);Attributes vert (Attributes v){// 这里不做任何复杂的变换,直接返回输入。// 最终的裁剪空间位置计算将由 Domain Shader 完成。return v;}[domain("tri")][partitioning("integer")][outputtopology("triangle_cw")][patchconstantfunc("hullConst")][outputcontrolpoints(3)]Attributes hull(InputPatch<Attributes, 3> input, uint id : SV_OutputControlPointID){return input[id];}struct HsConstantOutput{float fTessFactor[3]    : SV_TessFactor;//必须有的语义,定义补丁三条边的细分因子float fInsideTessFactor : SV_InsideTessFactor;//定义了补丁内部区域的细分因子。它控制了补丁内部的三角形网格的密度};HsConstantOutput hullConst(InputPatch<Attributes, 3> i){HsConstantOutput o = (HsConstantOutput)0;o.fTessFactor[0] = o.fTessFactor[1] = o.fTessFactor[2] = _TessFactor;o.fInsideTessFactor = _InsideTessFactor;return  o;}[domain("tri")]Varyings domain(HsConstantOutput hsConst, const OutputPatch<Attributes, 3> i,float3 bary : SV_DomainLocation){Varyings o = (Varyings)0;// 位置插值位置float4 interpolatedPosOS = i[0].positionOS * bary.z +i[1].positionOS * bary.x +i[2].positionOS * bary.y;// 将位置从对象空间转换到裁剪空间o.positionCS = TransformObjectToHClip(interpolatedPosOS);return o;}float4 frag (Varyings i) : SV_Target{//采样纹理float4 col = SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.uv);return col;}ENDHLSL}}
}

主观经验

        在大多数情况下,如果使用了曲面细分着色器(产生新顶点),一般会将物体空间顶点转换到裁剪空间这一步放在域着色器中实现,顶点着色器一般都十分简单(直传控制点)。

顶点着色器:非常简单,通常只负责直传原始顶点数据作为控制点给外壳着色器。

外壳着色器:定义细分级别,并可能计算一些平滑所需的额外控制点或常量。

域着色器:根据细分级别和控制点,生成所有新的高精度顶点的位置、法线、UV 等属性,并在该阶段执行最终的坐标转换(从对象/世界空间到裁剪空间),输出 SV_POSITION。

片段着色器:接收域着色器输出的插值数据并进行像素着色。

参考资料

(90 封私信 / 80 条消息) Unity 曲面细分着色器详解 - 知乎

http://www.dtcms.com/a/266764.html

相关文章:

  • IDEA-常用的开发组件
  • 用户进程的借壳挂靠之术
  • JAVA-springboot 整合Redis
  • 大数据在UI前端的应用创新研究:基于图神经网络的用户关系网络分析
  • [C++] C++多重继承:深入解析复杂继承关系
  • Blob分析及形态学分析
  • AWS 中如何添加一个内部域名
  • Spring AI Alibaba 来啦!!!
  • 本地区块链服务在物联网中的应用实例
  • M30280F8HP#U5B 瑞萨16位工业MCU微控制器,CAN 2.0B+专用PWM,电机控制专家!
  • 使用mindie:2.0.RC2-800I-A2-py311-openeuler24.03-lts制作一个通用的模型推理性能测试的镜像
  • Flynn分类法知识点梳理
  • 微服务架构的演进:迈向云原生
  • 【Spring Boot】Druid 连接池 YAML 配置详解
  • 马尔可夫链:随机过程的记忆法则与演化密码
  • 在LinuxMint 22.1(Ubuntu24.04)上安装使用同花顺远航版
  • 力扣刷题记录【1】146.LRU缓存
  • 【机器人】复现 DOV-SG 机器人导航 | 动态开放词汇 | 3D 场景图
  • 设计模式-应用分层
  • 【狂飙AGI】第8课:AGI-行业大模型(系列2)
  • NumPy-核心函数np.dot()深入理解
  • 【三维重建】【3DGS系列】【深度学习】3DGS的理论基础知识之高斯椭球的颜色表达
  • 鸿蒙开发BindSheet选择章节效果
  • 服务器间接口安全问题的全面分析
  • 数据集-目标检测系列- 卡车 数据集 truck >> DataBall
  • 代码随想录算法训练营第四十六天|动态规划part13
  • 【LeetCode 热题 100】238. 除自身以外数组的乘积——(解法一)前缀积与后缀积
  • 算法学习笔记:7.Dijkstra 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
  • 物联网数据安全区块链服务
  • AI Agent意图识别