Unity URP 曲面细分学习笔记

Unity URP 曲面细分学习笔记

学百人时遇到了曲面着色器的内容,有点糊里糊涂,于是上知乎找到了两篇大佬的文章 Unity URP 曲面细分 和 Unity曲面细分笔记,本文只是自己做学习记录使用

1.曲面细分与镶嵌

曲面细分或细分曲面(Subdivision surface)是指一种通过递归算法将一个粗糙的几何网格细化的技术。镶嵌(Tessellation)则是实现曲面细分的具体手段,它能将场景中的几何物体顶点集划分为合适的渲染结构。

曲面细分分为三个阶段:外壳着色器(Hull Shader)、镶嵌器(Tessellator)、域着色器(Domain Shader)。

1.1 外壳着色器 Hull Shader

Hull shader实际上是两个阶段(Phase)组成:常量外壳着色器(Constant Hull Shader)和 控制点外壳着色器(Control point hull shader),两个阶段并行运行。

Constant Hull Shader 会对每一个面片进行处理,主要任务是输出网格的曲面细分因子(Tessellation Factor) ,曲面细分因子用于指导面片细分数。

假如要传入的面片是三角形,那么对于三角形就会有三个边 曲面细分因子,所以edgeFactor[3],对于内部 曲面细分因子,因为三角形是最小的图元所以内部是一个因子(个人理解)insideFactor,如果是矩形的话就是四个边因子和两个内部因子

常量外壳着色器会以面片的所有顶点(或控制点)为输入 ,所以有三个顶点InputPatch patch,数字为3

SV_PrimitiveID 提供传入面片的ID值,可以用来区分不同的Patch,这样你就可以根据Patch的ID来为每个Patch设置不同的细分因子,或者执行其他依赖于Patch ID的操作。它对于每个图元都是不同的

cpp

复制代码

struct PatchTess{

float edgeFactor[3];

float insideFactor;

};

PatchTess PatchConstant(InputPatch patch, uint patchID : SV_PrimitiveID)

{

PatchTess o;

o.edgeFactor[0] = 4;

o.edgeFactor[1] = 4;

o.edgeFactor[2] = 4;

o.insideFactor = 4;

return o;

}

Control Point Hull Shader 用来改变每个输出顶点的位置信息 ,例如将一个三角形变为三次贝塞尔三角面片

domain:面片类型,参数有三角面tri、四角面片quad、等值线isoline

partitioning:曲面细分方式 ,参数有integer、fractional_even、fractional_odd

integer,指新顶点的增加指取决于细分的整数部分(等分),图形可能会出现图片

fractional_even,向上取最近的偶数n,将整段切割为n-2个相等长度的部分,和两端较短的部分

fractional_odd,向上取最近的奇数n,将整段切割为n-2个相等长度的部分,和两端较短的部分

outputtopology:细分创建的三角形面片的绕序,参数有顺时针triangle_cw、逆时针triangle_ccw

patchconstantfunc:常量外壳着色器的函数名

outputcontrolpoints:外壳着色器的执行次数,即生成的控制点个数

maxtessfactor:程序会使用到最大的细分因子

SV_OutputControlPointID:当前正在操作的控制点索引ID

cpp

复制代码

[domain("tri")]

[partitioning("integer")]

[outputtopology("triangle_cw")]

[patchconstantfunc("PatchConstant")]

[outputcontrolpoints(3)]

[maxtessfactor(64.0f)]

HullOut ControlPoint (InputPatch patch,uint id : SV_OutputControlPointID){

HullOut o;

o.positionOS = patch[id].positionOS;

o.texcoord = patch[id].texcoord;

return o;

}

常量外壳着色器对每个片元执行一次,输出细分因子等信息;控制点外壳着色器对每个控制点执行一次,并输出对应或衍生的控制点。两个阶段并行运行。

1.2 镶嵌器阶段 Tessellator

这一阶段我们无法对其做出任何控制,全程由硬件控制。在这一阶段,硬件根据之前曲面细分因子对面片做出细分操作。

1.3 域着色器阶段 Domain Shader

就和普通的顶点着色器要做的差不多,我们需要计算每一个控制点的顶点位置等信息。

功能

生成细分顶点,这些顶点是由外壳着色器确定的细分因子和细分模式所生成的

插值属性,根据控制点的属性进行插值,得到细分顶点的属性

输出顶点 ,域着色器生成最终的顶点数据,并传递给后面的着色器

cpp

复制代码

struct HullOut{

float3 positionOS : INTERNALTESSPOS;

float2 texcoord : TEXCOORD0;

};

struct DomainOut

{

float4 positionCS : SV_POSITION;

float2 texcoord : TEXCOORD0;

};

[domain("tri")]

DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch patch, float3 bary : SV_DOMAINLOCATION)

{

float3 positionOS = patch[0].positionOS * bary.x +

patch[1].positionOS * bary.y +

patch[2].positionOS * bary.z;

float2 texcoord = patch[0].texcoord * bary.x +

patch[1].texcoord * bary.y +

patch[2].texcoord * bary.z;

DomainOut output;

output.positionCS = TransformObjectToHClip(positionOS);

output.texcoord = texcoord;

return output;

}

域着色器输入:接受一个域(Domain)输入,代表了细分后顶点在原始补丁内的位置,这个位置通常用参数空间坐标表示,例如重心坐标(Barycentric Coordinates)(这里的重心坐标,是由硬件在细分过程的一个中间阶段 Tessellator 自动计算得出的)

属性插值:域着色器使用这些参数空间坐标来插值原始控制点的属性。

顶点输出:域着色器输出一个顶点,这个顶点包含计算后的位置和其他属性(如颜色、纹理坐标、法线等)

2.具体实现

2.1 不同的细分策略

2.1.1 平面镶嵌 Flat Tessellation

平面镶嵌只是线性插值位置信息,细分后的图案只比之前多了一些三角面片,单独使用并不能平滑模型。通常和置换贴图配合使用,创建凹凸不平的平面。

cpp

复制代码

Shader "Tessellation/Flat Tessellation"

{

Properties

{

[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {}

[Header(Tess)][Space]

[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 0

[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0

_EdgeFactor ("EdgeFactor", Range(1,8)) = 4

_InsideFactor ("InsideFactor", Range(1,8)) = 4

}

SubShader

{

Tags { "RenderType"="Opaque" }

Pass

{

HLSLPROGRAM

#pragma target 4.6

#pragma vertex FlatTessVert

#pragma fragment FlatTessFrag

#pragma hull FlatTessControlPoint

#pragma domain FlatTessDomain

#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD

#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

CBUFFER_START(UnityPerMaterial)

float _EdgeFactor;

float _InsideFactor;

CBUFFER_END

TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);

struct Attributes

{

float3 positionOS : POSITION;

float2 texcoord : TEXCOORD0;

};

struct VertexOut{

float3 positionOS : INTERNALTESSPOS;

float2 texcoord : TEXCOORD0;

};

struct PatchTess {

float edgeFactor[3] : SV_TESSFACTOR;

float insideFactor : SV_INSIDETESSFACTOR;

};

struct HullOut{

float3 positionOS : INTERNALTESSPOS;

float2 texcoord : TEXCOORD0;

};

struct DomainOut

{

float4 positionCS : SV_POSITION;

float2 texcoord : TEXCOORD0;

};

VertexOut FlatTessVert(Attributes input){

VertexOut o;

o.positionOS = input.positionOS;

o.texcoord = input.texcoord;

return o;

}

PatchTess PatchConstant (InputPatch patch, uint patchID : SV_PrimitiveID){

PatchTess o;

o.edgeFactor[0] = _EdgeFactor;

o.edgeFactor[1] = _EdgeFactor;

o.edgeFactor[2] = _EdgeFactor;

o.insideFactor = _InsideFactor;

return o;

}

[domain("tri")]

#if _PARTITIONING_INTEGER

[partitioning("integer")]

#elif _PARTITIONING_FRACTIONAL_EVEN

[partitioning("fractional_even")]

#elif _PARTITIONING_FRACTIONAL_ODD

[partitioning("fractional_odd")]

#endif

#if _OUTPUTTOPOLOGY_TRIANGLE_CW

[outputtopology("triangle_cw")]

#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW

[outputtopology("triangle_ccw")]

#endif

[patchconstantfunc("PatchConstant")]

[outputcontrolpoints(3)]

[maxtessfactor(64.0f)]

HullOut FlatTessControlPoint (InputPatch patch,uint id : SV_OutputControlPointID){

HullOut o;

o.positionOS = patch[id].positionOS;

o.texcoord = patch[id].texcoord;

return o;

}

[domain("tri")]

DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch patch, float3 bary : SV_DOMAINLOCATION)

{

float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z;

float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;

DomainOut output;

output.positionCS = TransformObjectToHClip(positionOS);

output.texcoord = texcoord;

return output;

}

half4 FlatTessFrag(DomainOut input) : SV_Target{

half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;

return half4(color, 1.0);

}

ENDHLSL

}

}

}

2.1.2 PN Tessellation(Per-Node Tessellation)

论文学习 Curved PN Triangles(Paper)

网站学习 OpenGL tutorial

在外壳着色器阶段,把一个三角面片(3个控制点)转换为一个3次贝塞尔三角面片(Cubic Bezier Triangle Patch,一种具有10个控制点的面片),这种策略称为 Curved Point-Normal Triangles(PN triangles),不同于Flat Tessellation,即使没有置换贴图,也能实现改变模型形状,平滑轮廓的作用。满足了资源有限以及环境限制的需求。

我们将使用一个Bezier曲面,Bezier三角形,形式如下

uvw代表的是质心坐标(u + v + w = 1),10个 b u v w b_{uvw} buvw是CPs,CPs形似如下图,类似于三角形顶部有一个点膨胀的表面

在镶嵌流水线中,

外壳着色器:我们将生成10个控制点 并确定细分因子

如何生成控制点?

1.三角形原始顶点B003、B030和B300保持不变

2.两个中点B012和B021在1/3和2/3的位置

3.将中点投影在初始顶点的切平面上

4.对于B111点,我们从原始的三角形中心(三个初始顶点取平均值)到6个中点的平均值(投影后的)取一个矢量

Tesellation Primitive Generator:PG中再根据细分因子对三角形域进行细分,并对每个新点执行域着色器;

域着色器:把来自PG的质心坐标 和来自外壳着色器的10个控制点 插入到贝塞尔三角形多项式中,得到的结果是膨胀表面上的坐标。

每个控制点的生成:

cpp

复制代码

float3 ComputeCP(float3 pA, float3 pB, float3 nA){

return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;

}

由于控制点的增多,在Hull Shader输出时每个顶点需要多携带两个顶点信息(中心控制点b111可以直接推算出来),例如:b030 可能需要携带b021和b012的顶点信息。修改一下控制点外壳着色器。

用TEXCOORD1和TEXCOORD2来存储额外的两个顶点信息

在PNTessControlPoint控制点着色器中,使用三元运算符来对nextID进行赋值,实现对每个新生成的控制点进行计算。

cpp

复制代码

struct HullOut{

float3 positionOS : INTERNALTESSPOS;

float3 normalOS : NORMAL;

float2 texcoord : TEXCOORD0;

float3 positionOS1 : TEXCOORD1; // 三角片元每个顶点多携带两个顶点信息

float3 positionOS2 : TEXCOORD2;

};

float3 ComputeCP(float3 pA, float3 pB, float3 nA){

return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;

}

[domain("tri")]

[partitioning("integer")]

[outputtopology("triangle_cw")]

[patchconstantfunc("PatchConstant")]

[outputcontrolpoints(3)]

[maxtessfactor(64.0f)]

HullOut PNTessControlPoint(InputPatch patch,uint id : SV_OutputControlPointID){

HullOut output;

const uint nextCPID = id < 2 ? id + 1 : 0;

output.positionOS = patch[id].positionOS;

output.normalOS = patch[id].normalOS;

output.texcoord = patch[id].texcoord;

output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);

output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);

return output;

}

域着色器负责"Bezier三角形的实现",根据上面Bezier三角形的表达式

首先计算系数u,v,w(即质心坐标,上面已经介绍了,是PG阶段自动生成的)

把外壳着色器中得到的控制点赋值给变量,并计算E和V,得到b111点

此处对于法线的计算就简化为"最简单的三个控制点插值获取的方式"

cpp

复制代码

[domain("tri")]

DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch patch, float3 bary : SV_DOMAINLOCATION)

{

float u = bary.x;

float v = bary.y;

float w = bary.z;

float uu = u * u;

float vv = v * v;

float ww = w * w;

float uu3 = 3 * uu;

float vv3 = 3 * vv;

float ww3 = 3 * ww;

float3 b300 = patch[0].positionOS;

float3 b210 = patch[0].positionOS1;

float3 b120 = patch[0].positionOS2;

float3 b030 = patch[1].positionOS;

float3 b021 = patch[1].positionOS1;

float3 b012 = patch[1].positionOS2;

float3 b003 = patch[2].positionOS;

float3 b102 = patch[2].positionOS1;

float3 b201 = patch[2].positionOS2;

float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;

float3 V = (b003 + b030 + b300) / 3.0;

float3 b111 = E + (E - V) / 2.0f;

// 插值获得细分后的顶点位置

float3 positionOS = b300 * ww * w + b030 * uu * u + b003 * vv * v

+ b210 * ww3 * u

+ b120 * uu3 * w

+ b021 * uu3 * v

+ b012 * vv3 * u

+ b102 * vv3 * w

+ b201 * ww3 * v

+ b111 * 6.0 * w * u * v;

// 此处简化了法线的计算

float3 normalOS = patch[0].normalOS * u

+ patch[1].normalOS * v

+ patch[2].normalOS * w;

normalOS = normalize(normalOS);

float2 texcoord = patch[0].texcoord * u

+ patch[1].texcoord * v

+ patch[2].texcoord * w;

DomainOut output;

output.positionCS = TransformObjectToHClip(positionOS);

output.normalWS = TransformObjectToWorldNormal(normalOS);

output.uv = texcoord;

return output;

}

然而,目前的做法是有缺陷的,在面对一些相同位置有不同法线的模型时,细分后会造成模型边缘的不连续,形成裂缝(Crack)。

2.1.3 Phong Tessellation

Phong着色应该很熟悉,是一种利用法向量线性差值得到平滑的着色的技术。Phong细分的灵感来自Phong着色,将Phong着色这一概念扩展到空间域。

核心思想:是利用三角形每个角的顶点法线 来影响细分过程中新顶点的位置,从而创造出曲面而非平面。

三角形原始顶点的切平面上, P ′ = P − ( ( P − V ) ⋅ N ) N P' = P - ((P-V)\cdot N)N P′=P−((P−V)⋅N)N

P P P 是最初插值的平面位置

V V V 是平面上的一个顶点位置

N N N 是顶点 V V V 处的法线

P ′ P' P′是 P P P 在平面上的投影。

cpp

复制代码

float3 ProjectPointOnPlane(float3 flatPositionWS, float3 cornerPositionWS, float3 normalWS)

{

return flatPositionWS - dot(flatPositionWS - cornerPositionWS, normalWS) * normalWS;

}

投影在三个切平面的三个点重新组成一个新的三角形,再用当前顶点的重心坐标插值计算出新的点。

cpp

复制代码

real3 PhongTessellation(real3 positionWS, real3 p0, real3 p1, real3 p2, real3 n0, real3 n1, real3 n2, real3 baryCoords, real shape)

{

// 分别计算三个切平面的投影点

real3 c0 = ProjectPointOnPlane(positionWS, p0, n0);

real3 c1 = ProjectPointOnPlane(positionWS, p1, n1);

real3 c2 = ProjectPointOnPlane(positionWS, p2, n2);

// 利用质心坐标插值得到最终顶点位置

real3 phongPositionWS = baryCoords.x * c0 + baryCoords.y * c1 + baryCoords.z * c2;

// 通过shape 控制平滑程度

return lerp(positionWS, phongPositionWS, shape);

}

域着色器修改为

cpp

复制代码

DomainOut PhongTriTessDomain (PatchTess tessFactors, const OutputPatch patch, float3 bary : SV_DOMAINLOCATION)

{

float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z;

positionWS =PhongTessellation(positionWS, patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, patch[0].normalWS, patch[1].normalWS, patch[2].normalWS, bary, _PhongShape);

float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;

DomainOut output;

output.positionCS = TransformObjectToHClip(positionWS);

output.texcoord = texcoord;

return output;

}

2.2 不同的细分因子

2.2.1 基于相机距离

为了让距离相机近的位置细分程度高一点,所以我们要先获取相机的位置,并且得到片元距相机的距离(三角形边缘中点到相机的距离),以此距离来调整细分因子

cpp

复制代码

real3 GetDistanceBasedTessFactor(real3 p0, real3 p1, real3 p2, real3 cameraPosWS, real tessMinDist, real tessMaxDist)

{

real3 edgePosition0 = 0.5 * (p1 + p2);

real3 edgePosition1 = 0.5 * (p0 + p2);

real3 edgePosition2 = 0.5 * (p0 + p1);

// In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,

// so the compiler will be able to optimize distance() to length().

real dist0 = distance(edgePosition0, cameraPosWS);

real dist1 = distance(edgePosition1, cameraPosWS);

real dist2 = distance(edgePosition2, cameraPosWS);

// The saturate will handle the produced NaN in case min == max

real fadeDist = tessMaxDist - tessMinDist;

real3 tessFactor;

tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);

tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);

tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);

return tessFactor;

}

real可以根据编译时的设置或宏定义来决定它具体代表哪种浮点数类型

cameraPosWS变量不需要在Shader中声明,因为它是由Unity渲染管线自动提供的

fadeDist是最大距离tessMaxDist和最小距离tessMinDist之间的差值,这个值定义了距离影响细分因子的范围

对于每个边缘,细分因子是通过将距离与tessMinDist相减,然后除以fadeDist(这个比例表示了当前距离相对于细分开始和结束的距离范围的位置 ),再用1减去该值(将比例翻转,即摄像机靠近相机时,细分因子接近1 ),最后使用saturate函数来限制结果在0到1之间(细分因子通常是介于0-1之间的值,0表示没有细分,1表示最大细分级别)。这样,当边缘中点距离相机较近时,细分因子接近1,表示细分程度较高;当边缘中点距离相机较远时,细分因子接近0,表示细分程度较低。

通过上述返回的细分因子,给三角形的边缘细分因子和内部细分因子赋值(内部细分因子设为三个边缘的平均)

cpp

复制代码

real4 CalcTriTessFactorsFromEdgeTessFactors(real3 triVertexFactors)

{

real4 tess;

tess.x = triVertexFactors.x;

tess.y = triVertexFactors.y;

tess.z = triVertexFactors.z;

tess.w = (triVertexFactors.x + triVertexFactors.y + triVertexFactors.z) / 3.0;

return tess;

}

现将常量外壳着色器的代码调整如下

cpp

复制代码

PatchTess PatchConstant (InputPatch patch, uint patchID : SV_PrimitiveID){

PatchTess o;

float3 cameraPosWS = GetCameraPositionWS();

real3 triVectexFactors = GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);

float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);

o.edgeFactor[0] = max(1.0, tessFactors.x);

o.edgeFactor[1] = max(1.0, tessFactors.y);

o.edgeFactor[2] = max(1.0, tessFactors.z);

o.insideFactor = max(1.0, tessFactors.w);

return o;

}

相关推荐

微信转账到银行卡,多久能到账?影响到账时间的因素深度解析
青字和什么字搭配最好
365bet日博亚洲

青字和什么字搭配最好

📅 08-28 👁️ 9399
手机积分可以保留多久 手机积分什么时候过期