在顶点着色器中,使用模型的normal法线方向来在模型空间下偏移顶点的位置。来做出类似藤蔓生长的效果。同时使用ASE的Opacity Mask剔除掉藤蔓一些片元来模拟生长。

藤蔓生长原理

Local Vertex Offset

在ASE的输出节点中,可以看到Local Vertex Offset插槽,这就是用来在模型空间下偏移顶点的。

Local Vertex Offset

Opacity Mask

如果想要让ASE实现第二节中所讲的Alpha Test + Clip函数的功能,需要先将Render Queue设置为Alpha Test,会出现Mask Clip Value,这个是根据贴图的明度来剔除的阈值,我们这里设为0,这时ASE的输出节点中Opacity Mask就能使用了,我们可以给它一个黑白贴图,贴图中偏黑的部分就会被剔除。

在藤蔓生长的例子中,我们使用uv的v分量作为黑白贴图来剔除藤蔓的片元,因为v分量正好是顺着藤蔓生长的方向的。

同时,我们还需要将uv的v分量使用SmoothStep来提高一下对比度,使用这个值来控制顶点的偏移,这样藤蔓末梢的偏移就能平滑了。

ASE连线

上图并不是最终版本

Roughness和Smoothness

将Roughness贴图反向一下(One Minus)就能当作Smoothness贴图使用。

最终Shader代码

这里的Shader代码是Unlit的,没有实现PBR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Shader "KerryTA/ASE/05ViceCode"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Shrink("Shrink", Float) = 0.0
_Expand("Expand", Float) = 0.5
_Grow ("Grow", Range(-2.0, 2.0)) = 0.0
_GrowMin("GrowMin", Range(0, 1)) = 0.6
_GrowMax("GrowMax", Range(0, 1.5)) = 1.35
_EndMin("EndMin", Range(0, 1)) = 0.5
_EndMax("EndMax", Range(0, 1.5)) = 1.0
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct appdata
{
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 pos : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
float _Shrink;
float _Expand;
float _Grow;
float _GrowMin;
float _GrowMax;
float _EndMin;
float _EndMax;


v2f vert (appdata v)
{
v2f o;
float weight_expand = smoothstep(_GrowMin, _GrowMax, (v.texcoord.y - _Grow));
float weight_end = smoothstep(_EndMin, _EndMax, v.texcoord.y);
float weight_combine = max(weight_expand, weight_end);

float3 vertex_shrink = v.normal * _Shrink * 0.01 * weight_combine;
float3 vertex_expand = v.normal * _Expand *0.01;
float3 final_offset = vertex_shrink + vertex_expand;

v.vertex.xyz = v.vertex.xyz + final_offset;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;//这里的uv不可以做偏移,直接传给frag
return o;
}

fixed4 frag (v2f i) : SV_Target
{
clip(1.0 - (i.uv.y - _Grow));
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}

显示法线脚本

在Built-in渲染管线中,需要自定义脚本来显示模型法线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class DrawNormals : MonoBehaviour
{
#if UNITY_EDITOR
[SerializeField]
private MeshFilter _meshFilter = null;
//[SerializeField]
//private bool _displayWireframe = false;
[SerializeField]
private NormalsDrawData _vertexNormals = new NormalsDrawData(new Color32(0,0,255, 127), true);
[SerializeField]
private NormalsDrawData _vertexTangents = new NormalsDrawData(new Color32(0, 255, 0, 127), true);
[SerializeField]
private NormalsDrawData _vertexBinormals = new NormalsDrawData(new Color32(255, 0, 0, 127), true);

[System.Serializable]
private class NormalsDrawData
{
[SerializeField]
protected DrawType _draw = DrawType.Selected;
protected enum DrawType { Never, Selected, Always }
[SerializeField]
protected float _length = 0.3f;
[SerializeField]
protected Color _normalColor;
private Color _baseColor = new Color32(255, 133, 0, 255);
[SerializeField]
protected float _baseSize = 0.0125f;


public NormalsDrawData(Color normalColor, bool draw)
{
_normalColor = normalColor;
_draw = draw ? DrawType.Selected : DrawType.Never;
}

public bool CanDraw(bool isSelected)
{
return (_draw == DrawType.Always) || (_draw == DrawType.Selected && isSelected);
}

public void Draw(Vector3 from, Vector3 direction)
{
Gizmos.color = _baseColor;
Gizmos.DrawWireSphere(from, _baseSize);

Gizmos.color = _normalColor;
Gizmos.DrawRay(from, direction * _length);
}
}

void OnDrawGizmosSelected()
{
//EditorUtility.SetSelectedWireframeHidden(GetComponent<Renderer>(), !_displayWireframe);
OnDrawNormals(true);
}

void OnDrawGizmos()
{
if (!Selection.Contains(this))
OnDrawNormals(false);
}

private void OnDrawNormals(bool isSelected)
{
if (_meshFilter == null)
{
_meshFilter = GetComponent<MeshFilter>();
if (_meshFilter == null)
return;
}

Mesh mesh = _meshFilter.sharedMesh;

//Draw Vertex Normals

Vector3[] vertices = mesh.vertices;
Vector3[] normals = mesh.normals;
Vector4[] tangents = mesh.tangents;


for (int i = 0; i < vertices.Length; i++)
{
Vector3 view_world = Vector3.Normalize(Camera.current.transform.forward - vertices[i]);
Vector3 normal_world = Vector3.Normalize(transform.TransformVector(normals[i]));
float NdotV = Vector3.Dot(normal_world, view_world);
if (NdotV < 0.0)
{
Vector3 tangent_world = transform.TransformVector(new Vector3(tangents[i].x, tangents[i].y, tangents[i].z));
if (_vertexNormals.CanDraw(isSelected))
_vertexNormals.Draw(transform.TransformPoint(vertices[i]), normal_world);
if (_vertexTangents.CanDraw(isSelected))
_vertexTangents.Draw(transform.TransformPoint(vertices[i]), tangent_world);
Vector3 binormal_world = Vector3.Normalize(Vector3.Cross(normal_world, tangent_world) * tangents[i].w);
if (_vertexBinormals.CanDraw(isSelected))
_vertexBinormals.Draw(transform.TransformPoint(vertices[i]), binormal_world);
}
}
}
#endif
}