Unity 创建外围轮廓面模拟挤出面的效果
Unity 创建外围轮廓面模拟挤出面的效果
效果预览
着色器基础
在 Unity 中,你可以使用 ShaderLab 语言来编写着色器。这种着色器包括顶点着色器(vert
)、片段着色器(frag
)和几何着色器(geom
)。
#pragma geometry geom
是在着色器代码中用来指示编译器使用特定的几何着色器函数的一种指令。
在 Unity 中,#pragma
指令用于控制编译器的行为,而 #pragma geometry geom
具体告诉编译器在这个地方要使用一个几何着色器函数,函数名为 geom
。这个几何着色器函数负责生成额外的几何形状或者修改输入的几何体。换句话说,这个指令告诉编译器将下面的代码视为几何着色器的主体部分。
几何着色器(Geometry Shader)是一种能够在图形渲染管线的几何处理阶段对几何体进行操作的程序。它能够在顶点和片段之间添加新的几何体信息,例如创建新的顶点、生成几何图形、调整顶点位置等。
#pragma geometry geom
告诉 Unity 的着色器编译器要使用名为 geom
的几何着色器函数来处理几何体的渲染。这个函数定义了如何在三角形的顶点周围创建新的顶点,以形成外围面轮廓面效果。
几何着色器(geom
)
[maxvertexcount(24)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream)
{
// 几何着色器逻辑
}
在几何着色器函数 geom
内部,有一个特殊的输入参数 triangle v2g IN[3]
。解释一下这些参数的含义:
-
triangle v2g IN[3]
:这里定义了一个名为IN
的数组,它包含了三个v2g
结构的元素。这个结构数组描述了输入的三角形的顶点信息。 -
maxvertexcount(24)
:规定几何着色器输出的最大顶点数量为 24。
v2g 结构体:
struct v2g
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
这个结构体定义了每个顶点的信息:
-
vertex
:顶点的世界空间位置(POSITION
)。 -
normal
:顶点的法线向量(NORMAL
)。 -
uv
:顶点的纹理坐标(TEXCOORD0
)。
geom函数输入参数的用途:
在几何着色器中,IN
数组包含了三个 v2g
结构体元素,每个元素描述了三角形的一个顶点。这个参数允许几何着色器函数直接访问每个输入顶点的位置、法线和纹理坐标等信息,从而能够基于这些数据进行操作,比如在顶点周围创建新的顶点以形成轮廓效果。
几何着色器的任务就是基于这些输入顶点的信息,创建新的几何体数据。在这个特定的几何着色器中,根据输入顶点计算了三角形的法线,然后在每个顶点周围生成新的顶点,从而构建出外围轮廓效果。
获取三角形面法线向量
首先,根据输入的三角形的顶点信息,计算两条边 edgeA
和 edgeB
。这两条边是由三角形的顶点计算而来,接着使用这两条边的叉乘得到了三角形面的法线向量 normalFace
。法线向量是垂直于三角形表面的向量,决定了表面的朝向。
float3 edgeA = IN[1].vertex - IN[0].vertex;
float3 edgeB = IN[2].vertex - IN[0].vertex;
float3 normalFace = normalize(cross(edgeA, edgeB));
在每个顶点周围创建轮廓顶点
接下来的步骤涉及在每个三角形顶点周围创建新的顶点,形成轮廓效果。
第一个for循环部分:
for(int i = 0; i < 3; i++)
{
// 在原始顶点处创建一个“内部”顶点(黑色部分)
o.pos = UnityObjectToClipPos(IN[i].vertex);
o.uv = IN[i].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
// 在法线方向上对原始顶点进行偏移,创建一个“外部”顶点(白色部分)
o.pos = UnityObjectToClipPos(IN[i].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[i].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
// 添加三角形的下一个顶点
int inext = (i+1) % 3;
o.pos = UnityObjectToClipPos(IN[inext].vertex);
o.uv = IN[inext].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
// 重启三角形带
tristream.RestartStrip();
// 添加外部顶点,形成轮廓
o.pos = UnityObjectToClipPos(IN[i].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[i].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
// 添加下一个三角形的顶点
o.pos = UnityObjectToClipPos(IN[inext].vertex);
o.uv = IN[inext].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
// 添加下一个外部顶点,形成轮廓
o.pos = UnityObjectToClipPos(IN[inext].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[inext].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
// 重启三角形带,准备下一组三角形
tristream.RestartStrip();
}
图片解释一下上述代码的原理
1.在unity中创建一个plane,并切换到wireframe渲染模式,观察每一个面都是由两个三角形构成
2.视角切换成45°视角观察这个其中一个三角形
3.针对这个三角形画图解释
当i为0时这三个顶点位置的代码对应的解释动图如下:
经过以上步骤就是现在在以一条边为基准沿法线方向生成了两个三角形构成的一个面。后续循环皆为此原理,可自行推理。
片段着色器(frag
)
fixed4 frag (g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv) * i.col;
return col;
}
处理输入的纹理坐标并返回最终颜色值。
创建轮廓效果
几何着色器的逻辑决定了如何创建外围轮廓效果。通常会根据输入的顶点信息生成额外的顶点,以形成轮廓。
应用着色器
- 将代码保存为
.shader
文件。 - 在 Unity 中创建一个新材质,并将着色器应用到该材质上。
- 将该材质应用到场景中的几何体上,观察外围轮廓效果的变化。
完整shader
Shader "Custom/Geometry/Extrude"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Factor ("Factor", Range(0.0001, 2.)) = 0.2
}
SubShader
{
Tags { "RenderType"="Opaque" }
Cull Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma geometry geom
#include "UnityCG.cginc"
struct v2g
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 uv : TEXCOORD0;
};
struct g2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed4 col : COLOR;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2g vert (appdata_base v)
{
v2g o;
o.vertex = v.vertex;
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
o.normal = v.normal;
return o;
}
float _Factor;
[maxvertexcount(24)]
void geom(triangle v2g IN[3], inout TriangleStream<g2f> tristream)
{
g2f o;
float3 edgeA = IN[1].vertex - IN[0].vertex;
float3 edgeB = IN[2].vertex - IN[0].vertex;
float3 normalFace = normalize(cross(edgeA, edgeB));
for(int i = 0; i < 3; i++)
{
o.pos = UnityObjectToClipPos(IN[i].vertex);
o.uv = IN[i].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
o.pos = UnityObjectToClipPos(IN[i].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[i].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
int inext = (i+1) % 3;
o.pos = UnityObjectToClipPos(IN[inext].vertex);
o.uv = IN[inext].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
tristream.RestartStrip();
o.pos = UnityObjectToClipPos(IN[i].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[i].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
o.pos = UnityObjectToClipPos(IN[inext].vertex);
o.uv = IN[inext].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
o.pos = UnityObjectToClipPos(IN[inext].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[inext].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
tristream.RestartStrip();
}
for(int i = 0; i < 3; i++)
{
o.pos = UnityObjectToClipPos(IN[i].vertex + float4(normalFace, 0) * _Factor);
o.uv = IN[i].uv;
o.col = fixed4(1., 1., 1., 1.);
tristream.Append(o);
}
tristream.RestartStrip();
for(int i = 0; i < 3; i++)
{
o.pos = UnityObjectToClipPos(IN[i].vertex);
o.uv = IN[i].uv;
o.col = fixed4(0., 0., 0., 1.);
tristream.Append(o);
}
tristream.RestartStrip();
}
fixed4 frag (g2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv) * i.col;
return col;
}
ENDCG
}
}
}
结语
这个教程提供了创建自定义外围轮廓着色器的基本步骤和框架。通过调整参数和计算方式,可以进一步改进和定制着色器,以满足不同项目的需求。