2D中的带符号距离场基础知识

尽管网格是最简单,最通用的渲染方式,但是还有其他一些选项可以用2d和3d表示形状。 一种常用的方法是有符号距离字段(SDF)。 带符号的距离场提供了更便宜的光线跟踪,允许不同的形状彼此平滑地流动,并节省了低分辨率纹理以获取高质量图像。

我们将从使用二维函数生成距离场的符号开始,但是稍后我们将继续以3D形式生成它们。 我将使用世界空间的坐标,以便尽可能减少对缩放比例和UV坐标的依赖,因此,如果您不了解它的工作原理,请在平坦的叠加层上研究本教程 ,以解释正在发生的事情。


基础准备


我们将暂时放弃基本平面叠加着色器的属性,因为现在我们将处理技术基础。 然后,我们将顶点在世界上的位置直接写入片段结构,而不会先将其转换为UV。 在准备工作的最后阶段,我们将编写一个新函数来计算场景并将距离返回到最近的曲面。 然后,我们调用函数并将结果用作颜色。

Shader "Tutorial/034_2D_SDF_Basics"{ SubShader{ //           Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //         o.position = UnityObjectToClipPos(v.vertex); //     o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { //      return 0; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback   ,       } 

我将在一个单独的文件中编写带符号距离字段的所有函数,以便我们可以重复使用它们。 为此,我将创建一个新文件。 我们不添加任何邪恶,然后设置它并完成条件包含保护,首先检查预处理器变量是否已设置。 如果尚未定义,则定义它并在要包含的函数之后完成条件if构造。 这样做的好处是,如果我们两次添加文件(例如,如果我们添加两个不同的文件,每个文件都有我们需要的功能,并且它们都添加了相同的文件),那么这将破坏着色器。 如果您确定这种情况永远不会发生,那么您将无法执行此检查。

 // in include file // include guards that keep the functions from being included more than once #ifndef SDF_2D #define SDF_2D // functions #endif 

如果包含文件与主着色器位于同一文件夹中,则可以使用pragma构造简单地将其包括在内。

 // in main shader #include "2D_SDF.cginc" 

因此,我们将在渲染的表面上仅看到一个黑色表面,准备在其上显示距离并带有符号。


圈圈


有符号距离字段的最简单函数是圆函数。 该函数将仅接收样本的位置和圆的半径。 我们首先获得样本位置向量的长度。 因此,我们在位置(0,0)处得到一个点,该点类似于半径为0的圆。

 float circle(float2 samplePosition, float radius){ return length(samplePosition); } 

然后,您可以在场景函数中调用圆函数并返回其返回的距离。

 float scene(float2 position) { float sceneDistance = circle(position, 2); return sceneDistance; } 


然后,我们将半径添加到计算中。 有符号距离函数的一个重要方面是,当我们在对象内部时,与表面的距离为负(这是“有符号”一词在“有符号距离”表达式中的含义)。 要将圆增加到半径,我们只需从长度中减去半径即可。 因此,函数返回0的所有位置的表面都向外移动。 大小为0的圆与表面的距离的单位为2,半径为1的圆与表面的距离只有一个单位,而半径为3的圆的圆内部为一个单位(值为-1);

 float circle(float2 samplePosition, float radius){ return length(samplePosition) - radius; } 


现在,我们唯一不能做的就是将圆从中心移开。 要解决此问题,您可以向圆函数添加新的参数,以计算样本位置与圆心之间的距离,并从该值中减去半径以定义圆。 或者,您可以通过移动采样点的空间来重新定义原点,然后在该空间中获得一个圆。 第二种选择看起来要复杂得多,但是由于移动对象是我们要用于所有图形的一种操作,因此它更具通用性,因此我将对其进行解释。

搬家


“点空间的变换”-听起来比实际情况要糟糕得多。 这意味着我们将点传递给函数,函数会对其进行更改,以便将来我们仍可以使用它。 在传输的情况下,我们只需从该点减去偏移量即可。 当我们想沿正方向移动形状时,会减去位置,因为我们在空间中渲染的形状与移动空间的方向相反。

例如,如果要在位置(3, 4)上绘制一个球体,则需要更改空间,以使(3, 4)变成(0, 0) ,为此我们需要减去(3, 4) 。 现在,如果我们围绕的原点绘制一个球体,那么它将是一个(3, 4)

 // in sdf functions include file float2 translate(float2 samplePosition, float2 offset){ return samplePosition - offset; } 

 float scene(float2 position) { float2 circlePosition = translate(position, float2(3, 2)); float sceneDistance = circle(circlePosition, 2); return sceneDistance; } 


长方形


另一个简单的形状是矩形。 首先,我们分别考虑这些组件。 首先,我们获取到中心的距离,取绝对值。 然后,类似于圆形,我们减去一半的大小(本质上类似于矩形的半径)。 为了仅显示结果的外观,我们现在仅返回一个组件。

 float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; return componentWiseEdgeDistance.x; } 


现在,我们只需返回最大的分量2就可以得到矩形的廉价版本。这在许多情况下都有效,但不能正确执行,因为它不能在拐角处显示正确的距离。


可以通过以下方法获得图形外部矩形的正确值:首先获取到边缘的距离与0之间的最大值,然后取其长度。

如果我们不将距离限制在0以下,那么我们只需计算到拐角的距离(edgeDistances为(0, 0) ),但是拐角之间的坐标不会低于0,因此将使用整个边缘。 这样做的缺点是,在整个图形内部,将0用作距边缘的距离。

要校正整个内部零件的距离0,您需要生成内部距离,只需使用便宜的矩形公式(从x和y分量获取最大值),然后保证它永远不会超过0,将最小值取为0。然后,我们将外部距离(永远不小于0)和内部距离(永远不超过0)相加,从而获得完成的距离函数。

 float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } 

由于我们以前以通用形式记录了传递函数,因此现在我们也可以使用它来将其中心移动到任何位置。

 float scene(float2 position) { float2 circlePosition = translate(position, float2(1, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


转弯


旋转形状类似于移动形状。 在计算到图形的距离之前,我们将坐标反向旋转。 为了尽可能简化对旋转的理解,我们将旋转乘以2 * pi以获得弧度角。 因此,我们将旋转传递给该函数,其中0.25是四分之一圈,0.5是半圈,而1是整圈(如果您觉得更自然,则可以执行不同的转换)。 我们还反转了旋转,因为出于与移动时相同的原因,我们需要沿与图形旋转相反的方向旋转位置。

为了计算旋转坐标,我们首先根据角度计算正弦和余弦。 Hlsl具有sincos函数,与单独计算相比,它们计算这两个值的速度更快。

在为分量x构造新矢量时,我们将原始分量x乘以余弦,将分量y乘以正弦。 如果您记得0的余弦为1,并且当旋转0时,我们希望新矢量的分量x与以前完全相同(即乘以1),就可以很容易记住这一点。 先前指向上方的分量y对分量x没有贡献,但向右旋转,其值从0开始,首先变大,也就是说,其运动完全由正弦描述。

对于新矢量的分量y,我们将余弦乘以旧矢量的分量y,然后减去正弦乘以旧分量x。 要理解为什么我们减去而不是加正弦乘以分量x,最好想象一下向量(1, 0)在顺时针旋转时如何变化。 结果的y分量从0开始,然后小于0。这与正弦的行为相反,因此我们更改符号。

 float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } 

现在我们已经编写了旋转方法,我们可以将其与传递结合使用来移动和旋转图形。

 float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


在这种情况下,我们首先围绕整个场景的中心旋转对象,以便旋转也影响转移。 要相对于其中心旋转图形,您首先需要移动图形,然后再旋转它。 由于旋转时顺序的改变,图形的中心将成为坐标系的中心。

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(2, 0)); circlePosition = rotate(circlePosition, _Time.y); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


缩放比例


缩放与变换形状的其他方式相似。 我们将坐标按比例划分,以缩小的比例在空间中绘制图形,然后在基本坐标系中它们变得更大。

 float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } 

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(0, 0)); circlePosition = rotate(circlePosition, .125); float pulseScale = 1 + 0.5*sin(_Time.y * 3.14); circlePosition = scale(circlePosition, pulseScale); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } 


尽管这样可以正确执行缩放,但距离也可以缩放。 有符号距离字段的主要优点是,我们始终知道到最近表面的距离,但是缩小会完全破坏该属性。 通过将符号距离函数(在本例中为rectangle )获得的距离场乘以比例尺,可以轻松解决此问题。 出于相同的原因,我们不能轻易缩放不均匀(x和y轴的缩放比例不同)。

 float scene(float2 position) { float2 circlePosition = position; circlePosition = translate(circlePosition, float2(0, 0)); circlePosition = rotate(circlePosition, .125); float pulseScale = 1 + 0.5*sin(_Time.y * 3.14); circlePosition = scale(circlePosition, pulseScale); float sceneDistance = rectangle(circlePosition, float2(1, 2)) * pulseScale; return sceneDistance; } 


可视化


带符号的距离字段可用于多种用途,例如创建阴影,渲染3D场景,物理场和渲染文本。 但是我们还不希望深入了解复杂性,因此,我将仅解释其可视化的两种技术。 第一种是带有抗锯齿的清晰形式,第二种是根据距离绘制线条。

清除表格


此方法类似于呈现文本时经常使用的方法,它创建清晰的表单。 如果我们不希望从函数中生成距离场,而是要从纹理中读取距离场,则可以使我们使用分辨率比平常低得多的纹理,并获得良好的效果。 TextMesh Pro使用此技术来呈现文本。

为了应用此技术,我们利用了以下事实:距离字段中的数据已签名,并且我们知道了截止点。 我们从计算距离场到下一个像素的距离开始。 该值应与坐标更改的长度相同,但是用符号计算距离更容易且更可靠。

收到距离变化后,我们可以从一半距离变化到负/加一半距离变化。 这将执行约0的简单裁剪,但具有平滑效果。 然后,可以将此平滑值用于我们需要的任何二进制值。 在此示例中,我将着色器更改为透明着色器,并将其用于alpha通道。 我从正值到负值进行了平滑过渡,因为我们希望距离字段的负值可见。 如果您不太了解透明渲染的工作原理,那么建议阅读我的透明渲染教程

 //properties Properties{ _Color("Color", Color) = (1,1,1,1) } 

 //in subshader outside of pass Tags{ "RenderType"="Transparent" "Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off 

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); float distanceChange = fwidth(dist) * 0.5; float antialiasedCutoff = smoothstep(distanceChange, -distanceChange, dist); fixed4 col = fixed4(_Color, antialiasedCutoff); return col; } 


高程线


可视化距离场的另一种常用技术是将距离显示为线。 在我们的实现中,我将在它们之间添加一些粗线和一些细线。 我还将以不同的颜色绘制图的内部和外部,以便您可以看到对象的位置。

我们将从显示图的内部和外部之间的差异开始。 可以在材质中自定义颜色,因此我们将为图形的内部和外部颜色添加新属性以及着色器变量。

 Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) } 

 //global shader variables float4 _InsideColor; float4 _OutsideColor; 

然后,在片段着色器中,我们检查像素的位置,然后通过使用step函数将距离与带有0的符号进行比较来进行渲染。 我们使用此变量从内部到外部颜色进行插值并将其呈现在屏幕上。

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); return col; } 


要渲染线条,我们首先需要指定渲染线条的频率以及线条的粗细,设置属性和相应的着色器变量。

 //Properties _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 

 //shader variables float _LineDistance; float _LineThickness; 

然后,为了绘制线条,我们将从计算距离变化开始,以便以后可以使用它进行平滑。 我们也已经将其除以2,因为稍后我们将其相加然后减去一半以覆盖1个像素的变化距离。

 float distanceChange = fwidth(dist) * 0.5; 

然后,我们计算距离并对其进行转换,以使其在重复点处具有相同的行为。 为此,我们首先将其除以线之间的距离,而在第一步中我们将不会获得整数,而是仅根据设置的距离获得整数。

然后我们将数字加0.5,取小数部分,然后再减去0.5。 在此需要小数部分和减法,以便线以重复模式穿过零。 我们添加0.5以获得小数部分,以中和0.5的进一步减法-偏移将导致以下事实:图形为0的值位于0、1、2等,而不是0.5、1.5,等

转换值的最后一步-我们取绝对值,然后再乘以线之间的距离。 绝对值使直线的点之前和之后的区域保持不变,这使创建直线的裁剪变得更加容易。 需要最后一次操作,我们再次将值乘以线之间的距离,以抵消等式开始处的除法,这要归功于此,值的变化仍与开始处相同,并且先前计算出的距离变化仍然正确。


 float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; 

现在,我们已经根据与图形的距离计算出了与线的距离,现在可以绘制线了。 我们从线厚度减去距离变化的一半到线厚度加上距离变化的一半,进行了平滑过渡,并使用刚计算出的线距离作为比较值。 计算完该值后,我们将其乘以颜色以创建黑线(如果需要多色线,也可以将其更改为其他颜色)。

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); return col * majorLines; } 


我们以相同的方式在粗线之间实现细线-我们添加了一个属性,该属性确定粗线之间应该有多少条细线,然后我们对粗线进行操作,但是由于细线之间的距离,我们将粗线之间的距离除以粗线之间的数量他们。 我们还将使细线的数量成为IntRange ,因此,我们只能分配整数值,而不会获得IntRange粗线不IntRange细线。 计算细线后,我们用与粗线相同的方式将它们乘以颜色。

 //properties [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 

 //shader variables float _SubLines; float _SubLineThickness; 

 fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } 


源代码


2D SDF功能



 #ifndef SDF_2D #define SDF_2D float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } float2 translate(float2 samplePosition, float2 offset){ //move samplepoint in the opposite direction that we want to move shapes in return samplePosition - offset; } float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } float circle(float2 samplePosition, float radius){ //get distance from center and grow it according to radius return length(samplePosition) - radius; } float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } #endif 

圆的例子



 Shader "Tutorial/034_2D_SDF_Basics/Rectangle"{ SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

矩形示例



 Shader "Tutorial/034_2D_SDF_Basics/Rectangle"{ SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = fixed4(dist, dist, dist, 1); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

截止



 Shader "Tutorial/034_2D_SDF_Basics/Cutoff"{ Properties{ _Color("Color", Color) = (1,1,1,1) } SubShader{ Tags{ "RenderType"="Transparent" "Queue"="Transparent"} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; fixed3 _Color; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.5); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); float distanceChange = fwidth(dist) * 0.5; float antialiasedCutoff = smoothstep(distanceChange, -distanceChange, dist); fixed4 col = fixed4(_Color, antialiasedCutoff); return col; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

距离线



 Shader "Tutorial/034_2D_SDF_Basics/DistanceLines"{ Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { float2 circlePosition = position; circlePosition = rotate(circlePosition, _Time.y * 0.2); circlePosition = translate(circlePosition, float2(2, 0)); float sceneDistance = rectangle(circlePosition, float2(1, 2)); return sceneDistance; } float4 _InsideColor; float4 _OutsideColor; float _LineDistance; float _LineThickness; float _SubLines; float _SubLineThickness; fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

我希望我设法用一个符号解释距离字段的基础知识,并且您已经在等待一些新的教程,在这些教程中,我将讨论其他使用它们的方法。

Source: https://habr.com/ru/post/zh-CN438316/


All Articles