由于图片数量较为多,很多的图片这里都没有加载出来,因此请移步到我的github观看此笔记,效果更佳:我的github笔记
3.3 曲面细分与几何着色器 大规模草渲染
曲面细分代码初尝试
参考:Unity渲染基础:URP下曲面细分的实现 - 知乎 (zhihu.com)
效果
-
-
- 直接从Unity新建的球
- 简单的演示效果
- 可以细分更多的三角面
- 细分之后可以添加置换贴图,对物体进行真实的顶线修改
- 通过调整数值可以变化曲面细分的数量
- 支持动态调整置换贴图的强度
代码
Shader "Stardy/URP/Text/3.3 Test_Tessellation"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
[Space(20)]
[Header(Tessellation)]
_TessFactor("Tess Factor",Range(1,64)) = 4
_Displacement("Displacement", Range(0, 1)) = 0.1
}
SubShader
{
Tags
{
"RenderType"="Opaque"
"RenderPipeline" = "UniversalRenderPipeline"
}
HLSLINCLUDE
//include
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
//pragmas
//新增加了hull,domain和标注需要tessellation
#pragma target 4.6 //使用细分时的最低着色器目标级别为4.6。如果我们不手动设置,Unity将发出警告并自动使用该级别。但我一开始用的是4.5也没有报错
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
uniform float4 _MainTex_ST;
uniform float _TessFactor;
uniform float _Displacement;
CBUFFER_END
//structs
struct Attributes
{
float4 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
float3 normalOS : NORMAL;
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float2 uv : TEXCOORD1;
float3 normalWS : TEXCOORD2;
};
//---------------new----------------
//新增加了Domain属性
struct DomainAttributes
{
float4 positionOS : TEXCOORD0;
float2 uv : TEXCOORD1;
float3 normalOS : NORMAL;
};
//---------------new----------------
struct TessControlPoint //这个结构体作为Hull Shader的输入/输出控制点
{
float4 positionOS : INTERNALTESSPOS; //曲面细分专用语义,标记这个位置数据将参与细分计算
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
// float4 color : COLOR;
};
//---------------new----------------
struct TessFactors //细分因子
{
float edge[3] : SV_TessFactor; //三个细分因子
float inside : SV_InsideTessFactor; //第四个细分因子
};
//---------------new----------------
// 顶点着色器,此时只是将Attributes里的数据递交给曲面细分阶段
TessControlPoint vert_Tess(Attributes IN)
{
TessControlPoint OUT;
OUT.positionOS = IN.positionOS;
OUT.uv = IN.texcoord;
OUT.normalOS = IN.normalOS;
return OUT;
}
//---------------new----------------
//主要的壳着色器,可以处理三角形,四边形或等值线。我们必须告诉它必须使用什么表面并提供必要的数据。这是 hull 程序的工作。
[domain("tri")]
[outputcontrolpoints(3)]
[patchconstantfunc("patchConstantFunc")]
[outputtopology("triangle_cw")]
[partitioning("integer")]
TessControlPoint hull_Tess(
InputPatch<TessControlPoint, 3> patch, //向Hull 程序传递曲面补丁的参数
uint id : SV_OutputControlPointID)
{
return patch[id];
}
//---------------new----------------
TessFactors patchConstantFunc(InputPatch<TessControlPoint, 3> patch) //决定了Patch的属性是如何被细分的,每个Patch调用一次
{
TessFactors OUT;
OUT.edge[0] = OUT.edge[1] = OUT.edge[2] = _TessFactor;
OUT.inside = _TessFactor;
return OUT;
}
//---------------new----------------
//让domain传给几何程序与插值器的数据仍然是Varyings结构体
//但是得写在domain_Tess前面,不然会报错
Varyings vert_AfterTess(DomainAttributes IN)
{
Varyings OUT;
OUT.positionWS = TransformObjectToWorld(IN.positionOS);
OUT.positionCS = TransformWorldToHClip(OUT.positionWS);
OUT.uv = IN.uv;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
return OUT;
}
//---------------new----------------
[domain("tri")]
Varyings domain_Tess(
TessFactors factors, //由patchConstantFunc函数生成的细分因子
OutputPatch<TessControlPoint, 3> patch, //Hull着色器传入的patch数据。第二个参数需要和InputPatch第二个参数对应
float3 barycentricCoordinates : SV_DomainLocation) //由曲面细分阶段阶段传入的顶点位置信息
{
DomainAttributes OUT;
//初始化OUT
OUT = (DomainAttributes)0;
//根据重心坐标插入法线数据
float3 normalOS = patch[0].normalOS * barycentricCoordinates.x +
patch[1].normalOS * barycentricCoordinates.y +
patch[2].normalOS * barycentricCoordinates.z;
//根据重心坐标进行位置和UV的插值
float4 positionOS = patch[0].positionOS * barycentricCoordinates.x +
patch[1].positionOS * barycentricCoordinates.y +
patch[2].positionOS * barycentricCoordinates.z;
float2 uv = patch[0].uv * barycentricCoordinates.x +
patch[1].uv * barycentricCoordinates.y +
patch[2].uv * barycentricCoordinates.z;
//置换贴图替换
float displacement = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_MainTex, uv, 0).r * _Displacement;
positionOS.xyz += displacement * normalOS;
OUT.positionOS = positionOS;
OUT.uv = uv;
OUT.normalOS = normalOS;
return vert_AfterTess(OUT);
}
float4 frag_Tess(Varyings IN) : SV_Target
{
// 简单显示法线方向
return float4(IN.normalWS * 0.5 + 0.5, 1.0);
}
ENDHLSL
Pass
{
Name "TESSPass"
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert_Tess
#pragma fragment frag_Tess
#pragma hull hull_Tess // 声明hull shader
#pragma domain domain_Tess // 声明domain shader
ENDHLSL
}
}
}
代码解析
1. #pragma target 4.6
使用细分时的最低着色器目标级别为4.6。如果我们不手动设置,Unity将发出警告并自动使用该级别
2. #pragma
的声明要多两个阶段
#pragma hull hull_Tess
和 #pragma domain domain_Tess
,接下来会对这两个阶段进行完善
3. 顶点细分着色器
TessControlPoint vert_Tess(Attributes IN)
{
TessControlPoint OUT;
OUT.positionOS = IN.positionOS;
OUT.uv = IN.texcoord;
OUT.normalOS = IN.normalOS;
return OUT;
}
-
将原始顶点的位置,纹理坐标和法线等需要的数据进行简单的传递给
TessControlPoint
。 -
不再像以前那样负责把顶点坐标从ObjectSpace转换到ClipSpace,或是贴图UV转换等工作,此处只是简单地将
Attributes
中的数据传递给曲面细分阶段的ControlPoint
。 -
ControlPoint
的结构体如下:-
struct TessControlPoint //这个结构体作为Hull Shader的输入/输出控制点 { float4 positionOS : INTERNALTESSPOS; //曲面细分专用语义,标记这个位置数据将参与细分计算 float2 uv : TEXCOORD0; float3 normalOS : NORMAL; // float4 color : COLOR; };
- 对于物体空间位置信息,需要用专用的语义
INTERNALTESSPOS
注明
- 对于物体空间位置信息,需要用专用的语义
-
-
Attribute
的结构体如下:-
//structs struct Attributes { float4 positionOS : POSITION; float2 texcoord : TEXCOORD0; float3 normalOS : NORMAL; };
- 在
Varyings
结构体里面,对于裁切空间的positionCS
,会使用SV_POSITION
语义进行特别注明:float4 positionCS : SV_POSITION;
- 在
-
对比可见,两者结构体几乎相同,只是
ControlPoint
中的vertex
使用INTERNALTESSPOS
代替POSITION
语意,否则编译器会报位置语义的重用
-
4. Hull着色器
- 细分阶段,确定如何细分补丁
//主要的壳着色器,可以处理三角形,四边形或等值线。我们必须告诉它必须使用什么表面并提供必要的数据。这是 hull 程序的工作。
//仅仅函数声明是不行的,编译器会报错,要求我们指定详细的参数,
[domain("tri")]
[outputcontrolpoints(3)]
[patchconstantfunc("patchConstantFunc")] //补丁常量函数,与Hull并行运行的子阶段
[outputtopology("triangle_cw")]
[partitioning("integer")]
TessControlPoint hull_Tess(
InputPatch<TessControlPoint, 3> patch, //向Hull 程序传递曲面补丁的参数
uint id : SV_OutputControlPointID)
{
return patch[id];
}
[domain("tri")]
:指定patch的类型,可选的有:tri
(三角形)、quad
(四边形)、isoline
(线段)[outputcontrolpoints(3)]
:指定输出的控制点的数量(每个图元),不一定与输入数量相同,也可以新增控制点。此处设置为3,是明确地告诉编译器每个补丁输出三个控制点[patchconstantfunc("patchConstantFunc")]
:指定补丁常数函数。GPU必须知道应将补丁切成多少部分。这不是一个恒定值,每个补丁可能有所不同。必须提供一个评估此值的函数,称为补丁常数函数(Patch Constant Functions),与Hull并行运行的子阶段[outputtopology("triangle_cw")]
: 输出拓扑结构。当GPU创建新三角形时,它需要知道我们是否要按顺时针或逆时针定义它们。有三种:triangle_cw
(顺时针环绕三角形)、triangle_ccw
(逆时针环绕三角形)、line
(线段)。[partitioning("integer")]
:分割模式,起到告知GPU应该如何分割补丁的作用呢,共有三种:integer
(硬分割),fractional_even
(软渐变分割),fractional_odd
(软渐变分割)。TessControlPoint hull_Tess(InputPatch<TessControlPoint, 3> patch,uint id : SV_OutputControlPointID)
:InputPatch参数是向Hull 程序传递曲面补丁的参数。Patch是网格顶点的集合。必须指定顶点的数据格式。- 在处理三角形时,每个补丁将包含三个顶点,此数量必须指定为
InputPatch
的第二个模板参数,所以第二个参数设置为3。 - Hull程序的工作是将所需的顶点数据传递到细分阶段。尽管向其提供了整个补丁,但该函数一次仅应输出一个顶点。补丁中的每个顶点都会调用一次它,并带有一个附加参数,该参数指定应该使用哪个控制点(顶点)。该参数是具有
SV_OutputControlPointID
语义的无符号整数。
- 在处理三角形时,每个补丁将包含三个顶点,此数量必须指定为
5. 补丁常数函数(Patch Constant Function)
TessFactors patchConstantFunc(InputPatch<TessControlPoint, 3> patch)
{
TessFactors OUT;
OUT.edge[0] = OUT.edge[1] = OUT.edge[2] = _TessFactor;
OUT.inside = _TessFactor;
return OUT;
}
- 决定Patch的属性是如何细分的。
- 这意味着它每个Patch仅被调用一次,而不是每个控制点被调用一次。这就是为什么它被称为常量函数,在整个Patch中都是常量的原因。
- 这里的
InputPatch
是输入
edge[3]
:控制三角形每条边的细分因子inside
:控制内部区域的细分密度- 这是决定曲面细分程度的核心控制点
输出的结构体:
struct TessFactors //细分因子
{
float edge[3] : SV_TessFactor; //三个细分因子
float inside : SV_InsideTessFactor; //第四个细分因子
};
- 为了确定如何细分三角形,GPU使用了四个细分因子。三角形面片的每个边缘都有一个因数。三角形的内部也有一个因素。三个边缘向量必须作为具有
SV_TessFactor
语义的float数组传递。内部因素使用SV_InsideTessFactor
语义- 实际上,此功能是与HullProgram并行运行的子阶段。
- 实际上,此功能是与HullProgram并行运行的子阶段。
6. Domain着色器
- 已知如何细分补丁,评估结果并生成最终三角形的顶点。
Varyings vert_AfterTess(DomainAttributes IN)
{
Varyings OUT;
OUT.positionWS = TransformObjectToWorld(IN.positionOS);
OUT.positionCS = TransformWorldToHClip(OUT.positionWS);
OUT.uv = IN.uv;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
return OUT;
}
[domain("tri")]
DomainAttributes domain_Tess(
TessFactors factors,
OutputPatch<TessControlPoint, 3> patch,
float3 barycentricCoordinates : SV_DomainLocation)
{
DomainAttributes OUT;
OUT = (DomainAttributes)0;
//根据重心坐标插入法线数据
float3 normalOS = patch[0].normalOS * barycentricCoordinates.x +
patch[1].normalOS * barycentricCoordinates.y +
patch[2].normalOS * barycentricCoordinates.z;
//根据重心坐标进行位置和UV的插值
float4 positionOS = patch[0].positionOS * barycentricCoordinates.x +
patch[1].positionOS * barycentricCoordinates.y +
patch[2].positionOS * barycentricCoordinates.z;
float2 uv = patch[0].uv * barycentricCoordinates.x +
patch[1].uv * barycentricCoordinates.y +
patch[2].uv * barycentricCoordinates.z;
//置换贴图替换
float displacement = SAMPLE_TEXTURE2D_LOD(_MainTex, sampler_MainTex, uv, 0).r * _Displacement;
positionOS.xyz += displacement * normalOS;
OUT.positionWS = TransformObjectToWorld(positionOS.xyz);
OUT.positionCS = TransformWorldToHClip(OUT.positionWS);
OUT.uv = uv;
OUT.normalWS = TransformObjectToWorldNormal(normalOS);
return OUT;
}
- 每个顶点都会调用一次Domain着色器。
- 计算量很大,而且所有的顶点信息都会在这里重新计算,最后将顶点坐标转换到投影空间。
- 解析
OutputPatch
,是输出补丁,即生成补丁。[domain("tri")]
:Hull着色器和Domain着色器都作用于相同的域,即三角形。通过domain属性再次发出信号TessFactors factors
:由patchConstantFunc
补丁常数函数输入,细分参数OutputPatch<TessControlPoint, 3> patch
:由Hull着色器传入Patch数据,尖括号的第二个参数与Hull着色器中的InputPatch
对应。SV_DomainLocation
:由曲面细分阶段阶段传入的顶点位置信息。
- 说明
- Hull只是确定补丁的细分方式,不会产生任何新的顶点。
- 但是它会为这些顶点提供重心坐标。
[domain("tri")]
提供重心坐标,用于三角形细分[domain("quad")]
提供UV坐标[0,1],用于四边形细分[domain("isoline")]
提供UV坐标,用于曲线细分
- 使用这些坐标来导出最终顶点,取决于Domain着色器。为了使之成为可能,每个顶点都会调用一次域函数,并为其提供重心坐标。
- 但是它会为这些顶点提供重心坐标。
- 经过了Domain之后,就有了新的顶点,之后这些新生成的顶点会被发送到几何着色器或者是光栅化插值器。但我们不应该让它来替代顶点着色器,因为此阶段之后的几何程序或插值器需要Varyings数据,而不是Attributes。
- 因此我们的解决办法是,让Domain着色器接管原始顶点的程序的指责。
- 即,结尾调用新写的顶点着色器
AfterTessVertProgram
。
- 即,结尾调用新写的顶点着色器
- 因此我们的解决办法是,让Domain着色器接管原始顶点的程序的指责。
- Hull只是确定补丁的细分方式,不会产生任何新的顶点。
总结
- 三个结构体
TessControlPoint
给Hull Shader输入输出,以及作为patch的参数TessFactors
定义细分因子在补丁常数函数被用来定义细分方式DomainAttributes
用来Domain Shader真正的生成顶点,并且传回给顶点着色器- 也可以自行调用顶点着色器,保证传出来的是
Varyings
- 也可以自行调用顶点着色器,保证传出来的是
- 一个函数
patchConstantFunc
对顶点的如何细分,对结构体TessFactors
操作- 需传入
InputPatch
,并且指定输出定点数:(InputPatch<TessControlPoint, 3> patch)
- 需传入
- 两个shader
TessControlPoint hull
:定义细分的方式,但是不进行细分。- 传入
- 补丁的参数
InputPatch<TessControlPoint, 3> patch
, - 新生成的控制点的ID:
uint id : SV_OutputControlPointID
- 补丁的参数
- 需要很多前置设置
[domain("tri")]
:指定patch类型[outputcontrolpoints(3)]
:指定每个图元输出的控制点的数量。[patchconstantfunc("patchConstantFunc")]
:指定面片常数函数。[outputtopology("triangle_cw")]
:指定输出拓扑结构。[partitioning("integer")]
:指定分割模式。
- 传入
domain
:按照Hull Shader提供的规则,生成顶点。- 传入
TessFactors factors
:由patchConstantFunc
函数指定的细分因子OutputPatch<TessControlPoint, 3> patch
:Hull传过来的patch数据,第二个参数需要和InputPatch
相对应。float3 barycentricCoordinates : SV_DomainLocation
:三角形类型的SV_DomainLocation
还会带有重心坐标
- 前置设置
[domain("tri")]
:指定patch类型
- 传入
几何着色器代码初尝试
用几何着色器稍微移动一下物体,达到一种不改变网格而移动网格顶点的效果
前置知识
**几何着色器阶段(Geometry Shader)**位于顶点和片段阶段之间。GS会调用单个图元作为输入,并可能会输出多个,也可能不输出图元。
应用:输入顶点,然后对这些顶点做处理。可以删除,移动或生成顶点。
- 可以按照某种规律生成顶点,比如拿来做草
效果
代码
Shader "Stardy/URP/Text/3.3 Test_GES"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_NormalControl ("Normal Control", float) = 0.5
}
SubShader
{
Tags
{
"RenderType"="Opaque"
"RenderPipeline" = "UniversalRenderPipeline"
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
};
struct Varyings
{
float4 positionOS : SV_POSITION;
float3 normalOS : NORMAL;
};
//-------------new code-------------
struct GeometryOutput
{
float4 positionCS : SV_POSITION;
float3 normalOS : TEXCOORD0;
};
CBUFFER_START(UnityPerMaterial)
uniform float4 _Color;
uniform float _NormalControl;
CBUFFER_END
Varyings vert_Common (Attributes IN)
{
Varyings OUT;
OUT.positionOS = IN.positionOS;
OUT.normalOS = IN.normalOS;
return OUT;
}
//-------------new code-------------
[maxvertexcount(3)]
void geom(
triangle Varyings IN[3],
inout TriangleStream<GeometryOutput> stream)
{
GeometryOutput OUT;
//通过扩展几何创建新三角形
for(int i = 0; i < 3; i++)
{
float4 positionEXT = IN[i].positionOS + float4(IN[i].normalOS * _NormalControl ,1.0);
OUT.positionCS = TransformObjectToHClip(positionEXT);
OUT.positionCS += float4(IN[i].normalOS * _NormalControl ,1.0);
OUT.normalOS = IN[i].normalOS;
stream.Append(OUT); //这是什么?
}
stream.RestartStrip();//这是什么?
}
half4 frag_Common (GeometryOutput IN) : SV_Target
{
return _Color * float4(abs(IN.normalOS), 1.0);
}
ENDHLSL
Pass
{
Name "GES_Pass"
Cull Off
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert_Common
#pragma fragment frag_Common
#pragma geometry geom
ENDHLSL
}
}
}
1. 目标着色器版本
#pragma target 4.0
仅当目标着色器模型为4.0或更高版本时才支持几何着色器。
2. 声明函数
自定义函数名,只需要声明了之后,编译调用(geometry
)它即可。
#pragma geometry geom //函数的调用
……
[maxvertexcount(3)]
void geom(
triangle Varyings IN[3],
inout TriangleStream<GeometryOutput> stream)
{
GeometryOutput OUT;
//通过扩展几何创建新三角形
for(int i = 0; i < 3; i++)
{
float4 positionEXT = IN[i].positionOS + float4(IN[i].normalOS * _NormalControl ,1.0);
OUT.positionCS = TransformObjectToHClip(positionEXT);
OUT.positionCS += float4(IN[i].normalOS * _NormalControl ,1.0);
OUT.normalOS = IN[i].normalOS;
stream.Append(OUT); //每个Append()调用生成一个新顶点
}
stream.RestartStrip();
}
-
[maxvertexcount(3)]
:表示输出顶点数的最大值。因为我们正在处理三角形,所以每次调用总是输出三个顶点。 -
triangle Varyings IN[3]
:triangle
表示我们正在处理的原始数据,在这里是三角形,其他类型还有line
和point
。必须在输入类型之前指定。- 由于三角形每个都有三个顶点,因此我们正在研究三个结构的数组。必须明确定义它。
-
inout TriangleStream<GeometryOutput> stream
:由于几何着色器可以输出的顶点数量各不相同,因此我们没有统一的返回类型。- 相反,几何着色器将写入图元流。在例子中,它是一个
TriangleStream
,其他的类型还有PointStream
、LineStream
,必须将其指定为inout参数。 stream.Append(OUT)
写流。每次调用Append,就会添加一个顶点到当前正在构建的图元中。- 例如,如果处理一个点并想生成一个三角形,就需要调用Append三次,每次传入一个顶点。
stream.RestartStrip();
结束该图元,生成新图元。- 比如,添加了三个顶点形成一个三角形后,调用
RestartStrip
来结束这个三角形,接下来的Append调用会开始构建新的图元,三角形或线条。
- 比如,添加了三个顶点形成一个三角形后,调用
- 相反,几何着色器将写入图元流。在例子中,它是一个
-
GeometryOutput
的代码:-
struct GeometryOutput { float4 positionCS : SV_POSITION; float3 normalOS : TEXCOORD0; };
-
总结
- 一个函数直接调用
#pragma geometry geom
:调用- 函数
[maxvertexcount(3)]
前置顶点数量设置- 传入
triangle Varyings IN[3]
:处理对象和结构体inout TriangleStream<GeometryOutput> stream
:输入输出的流,通过stream.Append(OUT);
进行加流,加入对应泛型里的结构体。stream.Append
方法用于将新的顶点添加到输出流中。
曲面细分+几何着色器生成交互草案例
参考:Unity Grass 着色器教程 (roystan.net)
菜鸡都能学会的Unity草地shader - 知乎 (zhihu.com)
-
几何着色器生成一个简单的三角形
-
//----------geo Shader --------- [maxvertexcount(3)] void geom( triangle Varyings input[3] , inout TriangleStream<geometryOutput> triStream) { geometryOutput output; output = (geometryOutput)0; output.positionCS = TransformObjectToHClip(float4(0.5, 0, 0 , 1)); triStream.Append(output); output.positionCS = TransformObjectToHClip(float4(-0.5, 0, 0 , 1)); triStream.Append(output); output.positionCS = TransformObjectToHClip(float4(0, 1, 0 , 1)); triStream.Append(output); }
-
为了使得原本的物体的网格能够显示出来,需要对物体原有的三角形进行遍历
-
先
[maxvertexcount(6)]
改为6个 -
然后添加
//----------geo Shader --------- //保留原始三角形 for(int i = 0; i < 3; i++) { output.positionCS = input[i].positionCS; output.positionWS = input[i].positionWS; triStream.Append(output); } triStream.RestartStrip();
-
-
保持这个小三角形而不显示原来的网格
-
-
分散三角形
-
计算的时候没有用原始的顶点位置作为三角形的起始位置,所以很多很多个顶点的三角形都长到了一起
-
加入偏倚解决此问题
-
output.positionCS = TransformObjectToHClip(input[0].positionOS + float3(0.5, 0, 0 )); output.positionCS = TransformObjectToHClip(input[0].positionOS + float3(-0.5, 0, 0 )); output.positionCS = TransformObjectToHClip(input[0].positionOS + float3(0, 1, 0 ));
-
只在平面上看上去是对的,它的形状都是固定朝向
(圆柱)
-
-
-
转换到切线空间
-
我们需要让三角形长在顶点上,并且能够在mesh的所处的空间生长
-
自行构建一个TBN矩阵,然后右乘(发现左乘的时候方向反过来了)
-
//----------geo Shader --------- //用来定义生成草叶的位置,封装函数方便重复调用 inline geometryOutput VertexOutput(float3 positionOS,float2 uv) { geometryOutput output; output.positionCS = TransformObjectToHClip(positionOS); output.positionWS = TransformObjectToWorld(positionOS); output.uv = uv; return output; } float3 positionOS = input[0].positionOS; float3 normalOS = input[0].normalOS; float4 tangentOS = input[0].tangentOS; float3 BinormalOS = normalize(cross(normalOS, tangentOS.xyz) * tangentOS.w); //乘切线的W判断方向用 float3x3 TBN_T2O = float3x3(tangentOS.xyz,BinormalOS,normalOS);//列向量排布 triStream.Append(VertexOutput(positionOS + mul(float3(0.5,0.0,0.0) , TBN_T2O) , float2(0.0,0.0))); triStream.Append(VertexOutput(positionOS + mul(float3(-0.5,0.0,0.0) , TBN_T2O) , float2(1.0,0.0))); triStream.Append(VertexOutput(positionOS + mul(float3(0.0,0.0,1.0) , TBN_T2O) , float2(0.5,1.0)));
- 写了一个额外的内置函数,方便重复调用
- UV用来后面做渐变
- UV长这样,逆时针三角形:
- UV长这样,逆时针三角形:
-
然后用UV来lerp输出颜色
-
-
half4 frag_Common (geometryOutput input) : SV_Target { return lerp(_BottomColor, _TopColor, input.uv.y); }
-
-
-
-
随机数的应用
-
随机数生成
//随机数生成函数——输出[0,1] inline float rand(float3 co) { return frac(sin(dot(co.xyz, float3(12.9898,78.233,45.5432))) * 43758.5453); }
- 后面再乘个2π,
-
轴旋转矩阵
//围绕轴旋转矩阵——罗德里格斯旋转矩阵 inline float3x3 AngleAxis3x3(float angle , float3 axis) { float c,s; sincos(angle , s , c); float t = 1.0 - c; float x = axis.x; float y = axis.y; float z = axis.z; return float3x3( t*x*x + c, t*x*y - s*z, t*x*z + s*y, t*x*y + s*z, t*y*y + c, t*y*z - s*x, t*x*z - s*y, t*y*z + s*x, t*z*z + c ); }
-
应用随机旋转轴
//----------geo Shader --------- //随机旋转角度 float3x3 facingRotationMatrix = AngleAxis3x3(rand(positionOS + _RotateSeed.xyz) * CUSTOM_TWO_PI, float3(0.0,0.0,1.0)); //随机旋转矩阵和TBN矩阵相乘(需考虑顺序,这里是右乘) float3x3 transformationMatrix = mul(facingRotationMatrix,TBN_T2O); triStream.Append(VertexOutput(positionOS + mul(float3(0.5,0.0,0.0) , transformationMatrix) , float2(0.0,0.0)));
-
应用随机弯折
//----------geo Shader --------- float3x3 bendRotationMatrix = AngleAxis3x3(rand(positionOS + _RotateSeed.yzx) * CUSTOM_PI * _BendRotationRandom * 0.5 , float3(-1.0,0.0,0.0)); //矩阵的混合(需考虑顺序) float3x3 transformationMatrix = mul(bendRotationMatrix,mul(facingRotationMatrix,TBN_T2O));
-
应用随机高度宽度
//----------geo Shader --------- //随机宽高 float height = (rand(positionOS.xyz) * 2 - 1) * _BladeHeightRandom + _BladeHeight ; float width = (rand(positionOS.xyz) * 2 - 1) * _BladeWidthRandom + _BladeWidth ; //逆时针三角形 triStream.Append(VertexOutput(positionOS + mul(float3(-width,0.0,0.0) , transformationMatrix) , float2(0.0,0.0))); triStream.Append(VertexOutput(positionOS + mul(float3(width,0.0,0.0) , transformationMatrix) , float2(1.0,0.0))); triStream.Append(VertexOutput(positionOS + mul(float3(0.0,0.0,height) , transformationMatrix) , float2(0.5,1.0)));
-
效果
-
- 每个顶点有随机朝向,随机宽度,随机高度
- 下一步增加顶点
-
-
-
曲面细分增加顶点
-
-
把之前的曲面细分的代码复制粘贴过来,主要是生成更多的顶点。
-
struct Tess_Attributes { float4 positionOS : POSITION; float2 texcoord : TEXCOORD0; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; }; struct Tess_Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 positionOS : TEXCOORD1; float2 uv : TEXCOORD2; float3 normalOS : TEXCOORD3; float4 tangentOS : TEXCOORD4; }; struct Tess_ControlPoint //这个结构体作为Hull Shader的输入/输出控制点 { float4 positionOS : INTERNALTESSPOS; float2 uv : TEXCOORD0; float3 normalOS : TEXCOORD1; float4 tangentOS : TEXCOORD2; }; struct Tess_TessFactors //细分因子 { float edge[3] : SV_TessFactor; //三个细分因子 float inside : SV_InsideTessFactor; //第四个细分因子 }; struct Tess_DomainAttributes { float4 positionOS : TEXCOORD0; float2 uv : TEXCOORD1; float3 normalOS : TEXCOORD2; float4 tangentOS : TEXCOORD3; }; Tess_ControlPoint vert_Tess(Tess_Attributes IN) { Tess_ControlPoint OUT; OUT.positionOS = IN.positionOS; OUT.uv = IN.texcoord; OUT.normalOS = IN.normalOS; OUT.tangentOS = IN.tangentOS; return OUT; } [domain("tri")] //指定patch的类型 [outputcontrolpoints(3)] //指定输出的控制点的数量(每个图元) [patchconstantfunc("patchConstantFunc")] //指定面片常数函数。 [outputtopology("triangle_cw")] //输出拓扑结构。 [partitioning("integer")] //分割模式,起到告知GPU应该如何分割补丁的作用。硬分割 Tess_ControlPoint hull_Tess( InputPatch<Tess_ControlPoint, 3> patch, //向Hull 程序传递曲面补丁的参数 uint id : SV_OutputControlPointID) { return patch[id]; } Tess_TessFactors patchConstantFunc(InputPatch<Tess_ControlPoint, 3> patch) //决定了Patch的属性是如何被细分的,每个Patch调用一次 { Tess_TessFactors OUT; OUT.edge[0] = OUT.edge[1] = OUT.edge[2] = _TessFactor; //控制三角形每条边的细分数量 OUT.inside = _TessFactor; //控制内部边的细分数量 return OUT; } //Domainy着色器,Domain→Geometry Tess_Varyings vert_AfterTess(Tess_DomainAttributes IN) { Tess_Varyings OUT; OUT.positionWS = TransformObjectToWorld(IN.positionOS); OUT.positionCS = TransformWorldToHClip(OUT.positionWS); OUT.positionOS = IN.positionOS; OUT.uv = IN.uv; OUT.normalOS = IN.normalOS; OUT.tangentOS = IN.tangentOS; return OUT; } [domain("tri")] Tess_Varyings domain_Tess( Tess_TessFactors factors, //由patchConstantFunc函数生成的细分因子 OutputPatch<Tess_ControlPoint, 3> patch, //Hull着色器传入的patch数据。第二个参数需要和InputPatch第二个参数对应 float3 barycentricCoordinates : SV_DomainLocation) //由曲面细分阶段阶段传入的顶点位置信息 { Tess_DomainAttributes OUT; //初始化OUT OUT = (Tess_DomainAttributes)0; //根据重心坐标插入法线数据 OUT.normalOS = patch[0].normalOS * barycentricCoordinates.x + patch[1].normalOS * barycentricCoordinates.y + patch[2].normalOS * barycentricCoordinates.z; //根据重心坐标进行位置 OUT.positionOS = patch[0].positionOS * barycentricCoordinates.x + patch[1].positionOS * barycentricCoordinates.y + patch[2].positionOS * barycentricCoordinates.z; //根据重心坐标插入切线数据 OUT.tangentOS = patch[0].tangentOS * barycentricCoordinates.x + patch[1].tangentOS * barycentricCoordinates.y + patch[2].tangentOS * barycentricCoordinates.z; //根据重心坐标插入UV OUT.uv = patch[0].uv * barycentricCoordinates.x + patch[1].uv * barycentricCoordinates.y + patch[2].uv * barycentricCoordinates.z; return vert_AfterTess(OUT); }
-
因为对结构进行了调整,所以需要复制粘贴新的结构体,并且把原始的
Attributes
结构体,Varyings
结构体以及原始的顶点着色器vert_Common
注释掉,使用Tesselation的Tess_Attributes
结构体,Tess_Varyings
结构体和vert_Tess
顶点着色器。 -
void geom(triangle Varyings input[3],inout TriangleStream<geometryOutput> triStream)
里面的Varyings
结构体修改成曲面细分传过来的Tess_Varyings
结构体 -
此时的编译修改为:
#pragma vertex vert_Tess //曲面细分的顶点shader #pragma hull hull_Tess //Hull→Domain #pragma domain domain_Tess //Domain→Geometry #pragma geometry geom //Geometry→Vertex #pragma fragment frag_Common //保持原版的片元着色器
-
-
效果
-
-
添加风动效果
-
//UV float2 uv = positionOS.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency.xy * _Time.y; //Wind Construct float2 windSample = SAMPLE_TEXTURE2D_LOD(_WindDistortionMap, sampler_WindDistortionMap, uv * 2 - 1, 0) * _WindStrength; float3 wind = normalize(float3(windSample.x,windSample.y,0.0));//wind Vector float3x3 windRotationMatrix = AngleAxis3x3(CUSTOM_PI * windSample,wind); //矩阵的混合(需考虑顺序) float3x3 transformationMatrix = mul(windRotationMatrix,mul(facingRotationMatrix,mul(bendRotationMatrix,TBN_T2O))); float3x3 transformationMatrixFacing = mul(facingRotationMatrix,TBN_T2O);//用于底部的两个顶点特殊处理,不让其风动和弯曲 triStream.Append(VertexOutput(positionOS + mul(float3(-width,0.0,0.0) , transformationMatrixFacing) , float2(0.0,0.0)));//两个底部的顶点不做风动效果以及弯折 triStream .Append(VertexOutput(positionOS + mul(float3(width,0.0,0.0) , transformationMatrixFacing) , float2(1.0,0.0)));
- 因为不能让底部的两个顶点受到风动,因此对于它俩采用不同的矩阵,并且加入到
triStream.Append
中 - 采样了一张
_WindDistortionMap
- 效果
- 因为不能让底部的两个顶点受到风动,因此对于它俩采用不同的矩阵,并且加入到
-
7 bba.png叶曲率,多片草叶**
-
-
代码
-
for(int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; float segmentHeight = height * t; float segmentWidth = width * (1 - t); //底部的两个顶点不动,也就是i = 0的情况 float3x3 transformationMatrix_T = i == 0 ? transformationMatrixFacing : transformationMatrix; triStream.Append(GenerateGrassVertex(positionOS , segmentWidth , segmentHeight , float2(0.0,t) , transformationMatrix_T));//两个底部的顶点不做风动效果以及弯折 triStream.Append(GenerateGrassVertex(positionOS , -segmentWidth , segmentHeight , float2(1.0,t) , transformationMatrix_T)); } //最后一个顶点的三角形 triStream.Append(GenerateGrassVertex(positionOS , 0.0 , height , float2(0.5,1.0) , transformationMatrix));
- 在三角形Stream之前,我们使用一个for循环来增加顶点:
- 循环的每一次迭代都会增加两个顶点:一个是左边,一个是右边。
- 在顶部完成后,我们将在叶片的顶端添加最后一个顶点。
- 在uv上,它实际上是从下往上的矩形。而顶点的生成上,因为是宽高发生了变化,所以看上去好像向内收缩了一样。
- 目前还缺少一个轴向的信息,前后的Z轴的信息,我们拿来做弯曲用。
- 在三角形Stream之前,我们使用一个for循环来增加顶点:
-
-
效果
-
弯曲
-
新增一个构造函数,添加了新的参数
Forward
-
inline geometryOutput GenerateGrassVertex(float3 vertexPositionOS , float width , float height ,float forward , float2 uv , float3x3 transformationMatrix) { float3 tangentPoint = float3(width , forward , height); float3 localPositionOS = vertexPositionOS + mul(tangentPoint , transformationMatrix); return VertexOutput(localPositionOS,uv); }
-
-
修改一下原来的函数
-
// Add as new properties. _BladeForward("Blade Forward Amount", Float) = 0.38 _BladeCurve("Blade Curvature Amount", Range(1, 4)) = 2 … // Add to the CGINCLUDE block. float _BladeForward; float _BladeCurve; … // Add inside the geometry shader, below the line declaring float width. float forward = rand(pos.yyz) * _BladeForward; … // 这个写到循环里面去 float segmentForward = pow(t, _BladeCurve) * forward; … // Modify the GenerateGrassVertex calls inside the loop. triStream.Append(GenerateGrassVertex(pos, segmentWidth, segmentHeight, segmentForward, float2(0, t), transformMatrix)); triStream.Append(GenerateGrassVertex(pos, -segmentWidth, segmentHeight, segmentForward, float2(1, t), transformMatrix)); … // Modify the GenerateGrassVertex calls outside the loop. triStream.Append(GenerateGrassVertex(pos, 0, height, forward, float2(0.5, 1), transformationMatrix));
-
效果
- 可以看到已经有段落式弯曲的感觉了
-
-
-
光照与阴影
-
新建了一个pass即可投影
-
Pass { Tags { "LightMode" = "ShadowCaster" } HLSLPROGRAM #pragma vertex vert_Tess #pragma hull hull_Tess #pragma domain domain_Tess #pragma geometry geom #pragma fragment frag_Common #pragma multi_compile_shadowcaster ENDHLSL }
-
-
接收阴影
-
额
-
效果
-
- 可以正确的接收和投射阴影了
- 但是自己对自己的遮挡,使得它产生了一个个条纹
-
解决方法
-
//在VertexOutput的Return的上面,对其进行阴影偏移 //光照数据 Light mainLight = GetMainLight(); float3 lightDir = normalize(mainLight.direction).xyz; //确保只在阴影通道的时候才进行稍微的偏移 #if UNITY_PASS_SHADOWCASTER output.positionWS = float4(ApplyShadowBias(output.positionWS,output.normalWS,lightDir) , 1.0); #endif
-
效果
-
-
-
-
-
光照计算
-
计算需要确保法线的正确。
-
运用之前底部顶点的法线给所有的点:
return VertexOutput(localPositionOS,uv,normalOS); //直接return传过来的法线数据
- 每个顶点都是同样的发现数据
-
为每一个顶点设置相同的数据,然后统一应用原来的旋转缩放矩阵:
- 每个顶点有各自的法线数据,接下来才能正确的计算光照
-
-
最后直接进行光照计算即可
-
//阴影数据 float4 shadowCoord = input.shadowCoord; half shadow = MainLightRealtimeShadow(shadowCoord); //光照数据 Light mainLight = GetMainLight(); float3 lightDir = normalize(mainLight.direction).xyz; float NdotL = saturate(dot(normalWS, lightDir)) * shadow; float3 ambient = SampleSH(normalWS); float4 lightTerm = float4(( NdotL * mainLight.color + ambient) , 1.0); half4 color = lerp(_BottomColor, _TopColor * lightTerm , input.uv.y); return color ;
-
-
-
-
最终效果
-
-
最终代码
-
CustomTess.HLSL
-
#ifndef CUSTOM_UNIVERSAL_PIPELINE_TESSELLATION_SHADER #define CUSTOM_UNIVERSAL_PIPELINE_TESSELLATION_SHADER #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl" //========================================== // 曲面细分数据结构 //========================================== struct Tess_Attributes { float4 positionOS : POSITION; float2 texcoord : TEXCOORD0; float3 normalOS : NORMAL; float4 tangentOS : TANGENT; }; struct Tess_Varyings { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float3 positionOS : TEXCOORD1; float2 uv : TEXCOORD2; float3 normalOS : TEXCOORD3; float4 tangentOS : TEXCOORD4; }; struct Tess_ControlPoint //这个结构体作为Hull Shader的输入/输出控制点 { float4 positionOS : INTERNALTESSPOS; float2 uv : TEXCOORD0; float3 normalOS : TEXCOORD1; float4 tangentOS : TEXCOORD2; }; struct Tess_TessFactors //细分因子 { float edge[3] : SV_TessFactor; //三个细分因子 float inside : SV_InsideTessFactor; //第四个细分因子 }; struct Tess_DomainAttributes { float4 positionOS : TEXCOORD0; float2 uv : TEXCOORD1; float3 normalOS : TEXCOORD2; float4 tangentOS : TEXCOORD3; }; #endif
-
-
TessGeo_Grass.Shader
-
Shader "Study/URP/Text/3.3 TessGeo Grass" { Properties { [Space(20)] [Header(Colors)] _BottomColor ("Bottom Color", Color) = (1,1,1,1) _TopColor ("Top Color", Color) = (1,1,1,1) [Space(20)] [Header(Noise)] _RotateSeed ("Rotate Seed", Vector) = (0,0,0,0) _BendRotationRandom("Bend Rotation Random", Range(0,1)) = 0.5 _BladeWidth ("Blade Width", Range(0,1)) = 0.5 _BladeWidthRandom ("Blade Width Random", Range(0,1)) = 0.5 _BladeHeight ("Blade Height", Range(0,1)) = 0.5 _BladeHeightRandom ("Blade Height Random", Range(0,1)) = 0.5 [Space(20)] [Header(Tessellation)] _TessFactor ("Tessellation Factor", Range(1,64)) = 16 [Space(20)] [Header(Wind)] _WindDistortionMap ("Wind Distortion Map", 2D) = "white" {} _WindFrequency ("Wind Frequency", Vector) = (0.05,0.05,0.0,0.0) _WindStrength ("Wind Strength", Range(0,1)) = 1 [Space(20)] [Header(Blade)] _BladeForward ("Blade Forward", float) = 0.38 _BladeCurve ("Blade Curve", Range(1,4)) = 1 [Space(20)] [Header(Interaction)] _Radius ("Radius", Range(0,10)) = 0.5 } SubShader { Cull Off ZTest LEqual ZWrite On Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalRenderPipeline" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" #include "Assets/MyStudy/Scene/EverybodyAddsFuel/3.3 TessAndGeom/CustomTess.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" #pragma target 4.6 #pragma multi_compile _ _MAIN_LIGHT_SHADOWS #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE #pragma multi_compile _ _SHADOWS_SOFT #pragma multi_compile _ _Anti ALIASING #pragma multi_compile _ _ADDITIONAL_LIGHT_SHADOWS #define CUSTOM_TWO_PI 6.283185307179586 #define CUSTOM_PI 3.141592653589793 #define BLADE_SEGMENTS 3 TEXTURE2D(_WindDistortionMap); SAMPLER(sampler_WindDistortionMap); CBUFFER_START(UnityPerMaterial) uniform half4 _BottomColor ; uniform half4 _TopColor ; uniform float4 _RotateSeed ; uniform half _BendRotationRandom ; uniform half _BladeWidth ; uniform half _BladeWidthRandom ; uniform half _BladeHeight ; uniform half _BladeHeightRandom ; uniform half _BladeForward ; uniform half _BladeCurve ; uniform half _TessFactor ; uniform float2 _WindFrequency ; uniform half _WindStrength ; uniform float4 _WindDistortionMap_ST; uniform float4 _PositionMoving; CBUFFER_END //Geo Struct struct geometryOutput { float4 positionCS : SV_POSITION; float3 positionWS : TEXCOORD0; float2 uv : TEXCOORD1; float4 shadowCoord : TEXCOORD2; //阴影坐标 float3 normalWS : TEXCOORD3; }; //随机数生成函数——输出[0,1] inline float rand(float3 co) { return frac(sin(dot(co.xyz, float3(12.9898,78.233,45.5432))) * 43758.5453); } //围绕轴旋转矩阵 inline float3x3 AngleAxis3x3(float angle , float3 axis) { float c,s; sincos(angle , s , c); float t = 1.0 - c; float x = axis.x; float y = axis.y; float z = axis.z; return float3x3( t*x*x + c, t*x*y - s*z, t*x*z + s*y, t*x*y + s*z, t*y*y + c, t*y*z - s*x, t*x*z - s*y, t*y*z + s*x, t*z*z + c ); } //========================================== // 曲面细分部分 //========================================== // 顶点着色器,顶点→Hull Tess_ControlPoint vert_Tess(Tess_Attributes IN) { Tess_ControlPoint OUT; OUT.positionOS = IN.positionOS; OUT.uv = IN.texcoord; OUT.normalOS = IN.normalOS; OUT.tangentOS = IN.tangentOS; return OUT; } //Hull着色器,Hull→Domain [domain("tri")] //指定patch的类型 [outputcontrolpoints(3)] //指定输出的控制点的数量(每个图元) [patchconstantfunc("patchConstantFunc")] //指定面片常数函数。 [outputtopology("triangle_cw")] //输出拓扑结构。 [partitioning("integer")] //分割模式,起到告知GPU应该如何分割补丁的作用。硬分割 Tess_ControlPoint hull_Tess( InputPatch<Tess_ControlPoint, 3> patch, //向Hull 程序传递曲面补丁的参数 uint id : SV_OutputControlPointID) { return patch[id]; } Tess_TessFactors patchConstantFunc(InputPatch<Tess_ControlPoint, 3> patch) //决定了Patch的属性是如何被细分的,每个Patch调用一次 { Tess_TessFactors OUT; OUT.edge[0] = OUT.edge[1] = OUT.edge[2] = _TessFactor; //控制三角形每条边的细分数量 OUT.inside = _TessFactor; //控制内部边的细分数量 return OUT; } //Domainy着色器,Domain→Geometry Tess_Varyings vert_AfterTess(Tess_DomainAttributes IN) { Tess_Varyings OUT; OUT.positionWS = TransformObjectToWorld(IN.positionOS); OUT.positionCS = TransformWorldToHClip(OUT.positionWS); OUT.positionOS = IN.positionOS; OUT.uv = IN.uv; OUT.normalOS = IN.normalOS; OUT.tangentOS = IN.tangentOS; return OUT; } [domain("tri")] Tess_Varyings domain_Tess( Tess_TessFactors factors, //由patchConstantFunc函数生成的细分因子 OutputPatch<Tess_ControlPoint, 3> patch, //Hull着色器传入的patch数据。第二个参数需要和InputPatch第二个参数对应 float3 barycentricCoordinates : SV_DomainLocation) //由曲面细分阶段阶段传入的顶点位置信息 { Tess_DomainAttributes OUT; //初始化OUT OUT = (Tess_DomainAttributes)0; //根据重心坐标插入法线数据 OUT.normalOS = patch[0].normalOS * barycentricCoordinates.x + patch[1].normalOS * barycentricCoordinates.y + patch[2].normalOS * barycentricCoordinates.z; //根据重心坐标进行位置 OUT.positionOS = patch[0].positionOS * barycentricCoordinates.x + patch[1].positionOS * barycentricCoordinates.y + patch[2].positionOS * barycentricCoordinates.z; //根据重心坐标插入切线数据 OUT.tangentOS = patch[0].tangentOS * barycentricCoordinates.x + patch[1].tangentOS * barycentricCoordinates.y + patch[2].tangentOS * barycentricCoordinates.z; //根据重心坐标插入UV OUT.uv = patch[0].uv * barycentricCoordinates.x + patch[1].uv * barycentricCoordinates.y + patch[2].uv * barycentricCoordinates.z; return vert_AfterTess(OUT); } //========================================== // 几何着色器部分 //========================================== //用来定义生成草叶的位置,封装函数方便重复调用 inline geometryOutput VertexOutput(float3 positionOS,float2 uv) { geometryOutput output; output = (geometryOutput)0; output.positionCS = TransformObjectToHClip(positionOS); output.positionWS = TransformObjectToWorld(positionOS); output.uv = uv; output.shadowCoord = TransformWorldToShadowCoord(output.positionWS); //世界坐标转换到阴影坐标 return output; } inline geometryOutput VertexOutput(float3 positionOS,float2 uv,float3 normalOS) { geometryOutput output; output = (geometryOutput)0; output.positionCS = TransformObjectToHClip(positionOS); output.positionWS = TransformObjectToWorld(positionOS); output.uv = uv; output.normalWS = TransformObjectToWorldNormal(normalOS); output.shadowCoord = TransformWorldToShadowCoord(output.positionWS); //世界坐标转换到阴影坐标 //光照数据 Light mainLight = GetMainLight(); float3 lightDir = normalize(mainLight.direction).xyz; //确保只在阴影通道的时候才进行稍微的偏移 #if UNITY_PASS_SHADOWCASTER output.positionWS = float4(ApplyShadowBias(output.positionWS,output.normalWS,lightDir) , 1.0); #endif return output; } //用于更加简便的方式书写的封装函数 inline geometryOutput GenerateGrassVertex(float3 vertexPositionOS , float width , float height ,float forward , float3 normalOS , float2 uv , float3x3 transformationMatrix) { float3 tangentPoint = float3(width , forward , height); //自生成法线 //跟原来的差别很大!!! //原来的用的都是底部的顶点的法线,在计算光照的时候会造成法线的错误。 float3 normalTS = float3(0,-1,forward); // float3 localNormalOS = mul( normalOS , transformationMatrix); float3 localNormalOS = mul( normalTS , transformationMatrix); float3 localPositionOS = vertexPositionOS + mul(tangentPoint , transformationMatrix); return VertexOutput(localPositionOS,uv,localNormalOS); } //几何着色器,Geometry→Vertex [maxvertexcount(BLADE_SEGMENTS * 2 + 1)] void geom( triangle Tess_Varyings input[3], inout TriangleStream<geometryOutput> triStream) { geometryOutput output; output = (geometryOutput)0; //参数传入 float3 positionOS = input[0].positionOS; float3 positionWS = input[0].positionWS; float3 normalOS = input[0].normalOS; float4 tangentOS = input[0].tangentOS; float3 BinormalOS = cross(normalOS, tangentOS.xyz) * tangentOS.w; //随机宽高 float height = (rand(positionOS.zyx) * 2 - 1) * _BladeHeightRandom + _BladeHeight ; float width = (rand(positionOS.xzy) * 2 - 1) * _BladeWidthRandom + _BladeWidth ; float forward = rand(positionOS.yyz) * _BladeForward; //UV float2 uv = positionOS.xz * _WindDistortionMap_ST.xy + _WindDistortionMap_ST.zw + _WindFrequency.xy * _Time.y; //Wind Construct float2 windSample = SAMPLE_TEXTURE2D_LOD(_WindDistortionMap, sampler_WindDistortionMap, uv * 2 - 1, 0) * _WindStrength; float3 wind = normalize(float3(windSample.x,windSample.y,0.0));//wind Vector float3x3 windRotationMatrix = AngleAxis3x3(CUSTOM_PI * windSample,wind); //随机旋转角度矩阵 float3x3 facingRotationMatrix = AngleAxis3x3(rand(positionOS + _RotateSeed.xyz ) * CUSTOM_TWO_PI, float3(0.0,0.0,1.0)); //随机弯折矩阵 float3x3 bendRotationMatrix = AngleAxis3x3(rand(positionOS + _RotateSeed.yzx) * CUSTOM_PI * _BendRotationRandom * 0.5 , float3(-1.0,0.0,0.0)); //TBN矩阵 float3x3 TBN_T2O = float3x3(tangentOS.xyz,BinormalOS,normalOS);//列向量排布 //矩阵的混合(需考虑顺序) float3x3 transformationMatrix = mul(windRotationMatrix,mul(facingRotationMatrix,mul(bendRotationMatrix,TBN_T2O))); float3x3 transformationMatrixFacing = mul(facingRotationMatrix,TBN_T2O);//用于底部的两个顶点特殊处理,不让其风动和弯曲 //三角形顶点的增加 for(int i = 0; i < BLADE_SEGMENTS; i++) { float t = i / (float)BLADE_SEGMENTS; float segmentHeight = height * t; float segmentWidth = width * (1 - t); float segmentForward = forward * pow(t , _BladeCurve); //底部的两个顶点不动,也就是i = 0的情况 float3x3 transformationMatrix_T = i == 0 ? transformationMatrixFacing : transformationMatrix; triStream.Append(GenerateGrassVertex(positionOS , segmentWidth , segmentHeight , segmentForward , normalOS , float2(0.0,t) , transformationMatrix_T));//两个底部的顶点不做风动效果以及弯折 triStream.Append(GenerateGrassVertex(positionOS , -segmentWidth , segmentHeight , segmentForward , normalOS , float2(1.0,t) , transformationMatrix_T)); } //最后一个顶点的三角形 triStream.Append(GenerateGrassVertex(positionOS , 0.0 , height , forward , normalOS , float2(0.5,1.0) , transformationMatrix)); } //========================================== // 原始的片段着色器,Vertex→Fragment //========================================== half4 frag_Common (geometryOutput input,half facing:VFACE) : SV_Target { float3 normalWS = facing > 0 ? input.normalWS : -input.normalWS; //阴影数据 float4 shadowCoord = input.shadowCoord; half shadow = MainLightRealtimeShadow(shadowCoord); //光照数据 Light mainLight = GetMainLight(); float3 lightDir = normalize(mainLight.direction).xyz; float NdotL = saturate(dot(normalWS, lightDir)) * shadow; float3 ambient = SampleSH(normalWS); float4 lightTerm = float4(( NdotL * mainLight.color + ambient) , 1.0); half4 color = lerp(_BottomColor, _TopColor * lightTerm , input.uv.y); return color ; } float4 frag_ShadowCaster (geometryOutput input) : SV_Target { float4 shadowCoord = input.shadowCoord; half shadow = MainLightRealtimeShadow(shadowCoord); return shadow; } ENDHLSL Pass { Name "TessGeo_Grass" Tags { "LightMode" = "UniversalForward" } HLSLPROGRAM #pragma vertex vert_Tess #pragma hull hull_Tess #pragma domain domain_Tess #pragma geometry geom #pragma fragment frag_Common ENDHLSL } Pass { Tags { "LightMode" = "ShadowCaster" } HLSLPROGRAM #pragma vertex vert_Tess #pragma hull hull_Tess #pragma domain domain_Tess #pragma geometry geom #pragma fragment frag_ShadowCaster #pragma multi_compile_shadowcaster ENDHLSL } } }
-
-
-
-
与草的交互
-
参考与草的交互:BIRP - 具有交互性的草地几何着色器 |Patreon 公司
-
核心代码
-
//_PositionMoving是从CPU端传入过来的物体的位置 //简单的交互效果——距离计算 float3 dis = distance(_PositionMoving , positionWS); //与顶点的半径距离 float3 radiusFalloff = 1 - saturate(dis / _Radius); //和顶点的半径衰减 float3 sphereDisp = positionWS - _PositionMoving; //移动位置指向顶点 sphereDisp *= radiusFalloff * 0.5;//衰减距离,乘0.5效果好一点 sphereDisp = clamp(sphereDisp, -0.8, 0.8);
- 对顶点交互的位置进行影响区域界定
-
然后组装到传入过去的物体位置即可
-
//-----写在for循环里面 //尝试直接修改顶点位置 float3 newPositionOS = i == 0 ? positionOS : positionOS + sphereDisp * t;//两个底部的顶点不做风动效果以及弯折 triStream.Append(GenerateGrassVertex(newPositionOS , segmentWidth , segmentHeight , segmentForward , normalOS , float2(0.0,t) , transformationMatrix_T)); triStream.Append(GenerateGrassVertex(newPositionOS , -segmentWidth , segmentHeight , segmentForward , normalOS , float2(1.0,t) , transformationMatrix_T));
-
-
C#端简单的代码
-
void Update() { Shader.SetGlobalVector("_PositionMoving", transform.position); }
-
-
效果演示
-
-
做得更好
- 可以对于水体应用曲面细分,可以增加距离因素。根据距离动态调整
Tessellation Factor
- 可以对于水体应用曲面细分,可以增加距离因素。根据距离动态调整
曲面细分生成交互雪地案例
参考基础实现:URP-Shader案例三:雪地轨迹效果 - 知乎 (zhihu.com)
-
完整的实现曲面细分效果,并且确保能够读取Albedo贴图,法线贴图以及Displacement贴图
-
完成一个可以针对不同的uv纹理坐标距离,进行相应的绘制的Shader
-
代码
float4 frag (v2f i) : SV_Target { float4 col = _MainTex.Sample(smp, i.uv); float draw = pow(saturate(1 - distance(i.uv, _Coordinate.xy)), _DrawStength); float4 drawCol = _Color * draw; return col + drawCol; }
-
-
编辑C#代码进行数据传递
-
代码
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DrawTrack : MonoBehaviour { [SerializeField] private Material DrawMat; [SerializeField] private Material LandMat; private RenderTexture TrackRT; private Transform m_LastPos = null; public float DrawStength = 1.0f; public Color DrawColor = Color.white; void Start() { m_LastPos = transform; TrackRT = new RenderTexture(2048, 2048, 0, RenderTextureFormat.Default); } // Update is called once per frame void Update() { DrawMat.SetFloat("_DrawStength", DrawStength); //设置绘制强度 DrawMat.SetColor("_Color", DrawColor); //设置绘制颜色 if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, 10)) //z轴向下射线检测地面 { DrawMat.SetVector("_Coordinate", new Vector4(hit.textureCoord.x, hit.textureCoord.y, 0, 0)); //设置纹理坐标 RenderTexture tmp = RenderTexture.GetTemporary(TrackRT.width, TrackRT.height, 0, RenderTextureFormat.Default); //创建临时渲染纹理 Graphics.Blit(TrackRT, tmp); Graphics.Blit(tmp, TrackRT, DrawMat); //绘制轨迹 RenderTexture.ReleaseTemporary(tmp); //释放临时渲染纹理 LandMat.SetTexture("_SunkenMap", TrackRT); //设置纹理贴图 } m_LastPos.position = transform.position; } }
-
-
实现效果
-
不足
- 我没有做对于碰撞物的体积检测
- 雪地是初步的渲染效果,不真实
-
做得更好
- Unity-雪地效果的实现 - 知乎 (zhihu.com)——这篇文章讲了绘制更多的通道,根据计算后的深度值图,进行两次轮廓线检测,采用的是最基本简单的sobel算子,然后用g和b通道分别存储其颜色。这样做的目的是,可以根据最后的深度图的不同通道,将雪地分为基本雪面(无颜色值)、凸起部分(g通道)、侧边(b通道)、凹陷底部(a通道)四个部分。通过卷积核对图像的处理,拓宽边缘等方法。
- Unity动态雪地(沙地) - 知乎 (zhihu.com)——这篇文章提到了使用Compute Shader的方式实现,对象是人物,提到了双摄像头的动态捕捉。而且是面向于3A大作里面的雪地实现思路。
- 入门图形学:雪地特效(一)_unity雪地-优快云博客——很适合新手,很详细的提到了从顶部摄像机的照射,根据体积的下陷,而且有完整的代码。
- unity雪景渲染适合作为毕业设计吗? - 知乎 (zhihu.com)——甚至能拿来当毕设?
- Unity-雪地效果的实现 - 知乎 (zhihu.com)——这篇文章讲了绘制更多的通道,根据计算后的深度值图,进行两次轮廓线检测,采用的是最基本简单的sobel算子,然后用g和b通道分别存储其颜色。这样做的目的是,可以根据最后的深度图的不同通道,将雪地分为基本雪面(无颜色值)、凸起部分(g通道)、侧边(b通道)、凹陷底部(a通道)四个部分。通过卷积核对图像的处理,拓宽边缘等方法。
-
完整代码
- 太长了不放了……有机会上传到Github自取吧