0%

Git安装其实没什么好讲的,但是单纯只装Git基本上只能使用命令行,当然Git也有客户端,但是功能有限。
所以通常我们会一同安装Tortoisegit软件,作为Git的图形交互界面。下面地址是一位大佬写的安装流程,涉及到如何配置等操作。
下载安装Git及Tortoisegit
因为Git提供HTTPS、SSH两种方式来提交拉取Git资源,而文中提到通过使用 SSH URL 来提交代码便可以一劳永逸了。所以这里打算采用SSH的方式,下面的方法就是SSH的方式。

当然如果想使用HTTPS的方式,可以看这里:
Git Personal Access Tokens

但是使用上面SSH的方式,在从git服务拉取项目时,会报No supported authentication methods available的错误。这里有一篇文章讲述了如何解决这个问题:
TortoiseGit提示No supported authentication methods available错误

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
[MenuItem("Assets/Helper/Get Texture Path For Houdini Terrain",true,1)]
static bool _GetTexturePathForHoudiniTerrain()
{
if(Selection.assetGUIDs.Length>0)
{
string path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
string temp = "Resources/";
int index = path.IndexOf(temp);
if(index>-1)
{
return true;
}
}
return false;
}
[MenuItem("Assets/Helper/Get Texture Path For Houdini Terrain",false,1)]
static void _GetTexturePathForHoudiniTerrain()
{
string path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]);
string temp = "Resources/";
int index = path.IndexOf(temp);
int end = path.lastIndexOf(',');
index += temo.Length;
GUIUtility.systemCopyBuffer = path.Substring(index,end-index);
}

[MenuItem("Assets/Helper/Trim File Name")]
static void TrimFileName()
{
if(Selection.assetGUIDs.Length >0)
{
for(int i=0;i<Selection.assetGUIDs.Length;++i)
{
string path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[i]);
string fileName = Path.GetFileName(path);
int index = path.IndexOf(' ');
if(index>-1)
{
string newFileName = fileName.Replace(' ','_');
string rst = AssetDatabase.RenameAsset(path,newFileName);
}
}
AssetDatabase.SaveAssets();
}
}

HEU_BaseSync.cs

1
2
3
terrainData.size = new Vector3(terrainBuffers[t]._terrainSizeX, heightRange, terainBuffers[t]._terrainSizeY);
terrain.Flush();
UnityEditor.AssetDatabase.SaveAssets(); // add this line

HEU_HAPIUtility.cs(H19.0.434)

1
2
3
4
5
6
7
8
9
10
// Do not process the main display geo twice!
if (editGeoInfo.isDisplayGeo) continue;

// We only handle editable curves for now
//if (editGeoInfo.type != HAPI_GeoType.HAPI_GEOTYPE_CURVE) continue;
if (editGeoInfo.type != HAPI_GeoType.HAPI_GEOTYPE_CURVE && editGeoInfo.type != HAPI_GeoType.HAPI_GEOTYPE_INTERMEDIATE) continue; // modify this
//session.CookNode(editNodeID, HEU_PluginSettings.CookTemplatedGeos);

// Add this geo to the geo info array
editableGeoInfos.Add(editGeoInfo);

最近使用GitHub提交项目时,发现原先的账号密码的方式已经失效,GitHub官方表示不再支持使用账号密码的方式,而是改为使用Personal Access Tokens访问令牌的方式。

Personal Access Tokens是通过在GitHub上创建令牌,并且设置令牌的使用时限,来限制访问者的访问权限。也可以直接删除令牌来清除访问者的访问权限。而令牌的作用域时针对整个GitHub账号下的所有项目。所以创建方式也是在GitHub下的总Setting下设置。创造令牌的步骤如下:
1:登录GitHub账号;
2:选择右上角的头像图标 -> Profile Pic -> Setting

3:然后在Setting页面左侧菜单栏里面选择Developer Settings

4:然后在Developer Settings页面上选择Personal Access Token 选项;

5:然后选择Generate New Token按钮开始创建令牌;

6:给令牌选择一个名字,令牌的使用时限,以及令牌的访问权限范围;

7:然后点击Generate Token生成令牌;
8:生成的令牌是一串字符串,只有在令牌生成的时候才能看到令牌明码,我们需要复制这个字符串,后续再打开这个令牌界面的时候,就看不到令牌字符串了;

拿到令牌字符串后,我们就可以使用这个令牌来clone或者push该GitHub账号下的项目了。使用令牌时,我们需要使用令牌字符串来拼接git资源路径。
下面以https://github.com/Tyson-Wu/Unity_Jenkins这个项目为例。通常我们clone时直接使用一下命令:

1
git clone https://github.com/Tyson-Wu/Unity_Jenkins.git

但是这个命令只能拷贝资源,拷贝后并不能上传修改后的资源。想要拥有上传权限,那就要用令牌来修改访问权限了。而拼接的规则如下:

1
https://<username>:<personal_access_token>@github.com/<username>/<project_name>.git

假设此时我创建的令牌字符串为ghp_7qb6ZektsWIFkHzJiaL4gH2GKzNUqD3P8ZoA,对于上面子资源,拼接后的路径应该是:

1
git clone https://ghp_7qb6ZektsWIFkHzJiaL4gH2GKzNUqD3P8ZoA@github.com/Tyson-Wu/Unity_Jenkins.git

当拷贝完后,我们进入到Unity_Jenkins的本地拷贝根目录下,打开git命令行,然后修改它的orgion。

1
git remote set-url origin https://ghp_7qb6ZektsWIFkHzJiaL4gH2GKzNUqD3P8ZoA@github.com/Tyson-Wu/Unity_Jenkins.git

这时候就设置好了访问权限,假设我们本地文件修改后想提交,直接使用Push命令就行了。

1
git push

功能

在Unit内创建多个项目实例。

源码

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
namespace ProjectLinker
{
static class CmdHelper
{
public static void LinkFolder(string orgPath, string linkPath)
{
ProcessStartInfo start = new ProcessStartInfo("cmd.exe");
start.CreateNoWindow = true;
start.UseShellExecute = false;
start.RedirectStandardInput = true;
var process = Process.Start(start);
var sw = process.StandardInput;
sw.WriteLine(@$"mklink /D ""{linkPath}"" ""{orgPath}""");
sw.Close();
process.WaitForExit();
process.Close();
}
public static void LaunchUnityProject(string projectFullPath, string buildTarget)
{
string editorPath = EditorApplication.applicationPath;
System.Treading.ThreadPool.QueueUserWorkItem(delegate (object state)
{
Process p = null;
try
{
string arg = $"-projectPath \"{projectFullPath}\"";
if(!string.IsNullOrEmpty(buildTarget))
{
arg += $" -buildTarget {buildTarget}";
}
ProcessStartInfo start = new ProcessStartInfo(editorPath, arg);
start.CreateNoWindow = false;
start.UseShellExecute = false;

p = Process.Start(start);
}
catch(System.Exception e)
{
UnityEngine.Debug.LogException(e);
if(p != null)
{
p.Close();
}
}
})
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;
namespace ProjectLinker
{
[Serializable]
class CacheDataTable
{
public List<CacheDataElem> elems = new List<CacheDataElem>();
public CacheDataTable()
{
List<CacheDataElem> elems = new List<CacheDataElem>();
}
}
[Serializable]
class CacheDataElem
{
public string name;
public string buildTarget;
public string projectPath;
}
}
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
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
namespace ProjectLinker
{
class ProjectTreeView : TreeView
{
private CacheDataTable _tableData;
public event Action<int> OnSelected;
public event Action OnDataChange;
public ProjectTreeView(CacheDataTable table, TreeViewState state) : base(state)
{
_tableData = table;
showAlternatingRowBackgrounds = false;
ReLoad();
}
protected override TreeViewItem BuildRoot()
{
return new TreeViewItem {id = 0, depth = -1};
}
protected override IList<TreeViewItem> BuildRows(TreeviewItem root)
{
var rows = GetRows() ?? new List<TreeViewItem>(10);
rows.Clear();
var iconTex = EditorGUIUtility.FindTexture("Folder Icon");
for(int i = 0; i < _tableData.elems.Count; ++i)
{
var item = new TreeViewItem {id = i + 1, depth = 0, displyName = _tableData.elems[i].name};
item.icon = iconTex;
root.AddChild(item);
rows.Add(item);
}
SetupDepthsFromParentsAndChildren(root);
return rows;
}
protected override void RowGUI(RowGUIArgs args)
{
base.RowGUI(args);
float width = 16f;
rect deleRect = new Rect(args.rowRect.width - width - 10, args.rowRect.y, width, width);
Event evt = Event.current;
if(evt.type == EventType.MouseDown && deleRect.Contains(evt.mousePosition))
SelectionClick(args.item, false);
if(GUI.Button(deleRect, "x"))
{
_tableData.elems.RemoveAt(args.item.id - 1);
Reload();
OnDataChange?.Invoke();
}
}
protected override void SingleClickedItem(int id)
{
base.SingleClickedItem(id);
int index = id - 1;
OnSelected?.Invoke(index);
}
}
}
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEditor.IMGUI.Controls;
namespace ProjectLinker
{
public class ProjectLinker : EditorWindow
{
[MenuItem("Window/Project Linker")]
private static void Open()
{
var win = EditorWindow.GetWindow<ProjectLinker>("关联项目快捷启动");
win.Show();
}

private const string _CACHE_TABLE_HEADER = "ProjectLinker_Table_";
private string _copyProjectName = "";
private string _defaultCopyProjectName = "";
private string _curProjectPath = "";
private string _rootPath = "";
[SerializeField] TreeViewState _treeViewState;
ProjectTreeView _treeView;

CacheDataTable _cacheDataTable = null;
int _selectedElemIndex = -1;

CacheDataElem selectedElem
{
get
{
if(_selectedElemIndex > -1 && _selectedElemIndex < _cacheDataTable.elems.Count)
{
return _cacheDataTable.elems[_selectedElemIndex];
}
return null;
}
}
private void OnEnable()
{
_curProjectPath = Path.GetDirectoryName(Application.dataPath);
_rootPath = Path.GetDirectoryName(_curProjectPath);
_defaultCopyProjectName = Path.GetFileName(_curProjectPath) + "_copy";
_copyProjectName = _defaultCopyProjectName;
LoadCache();

if(_treeViewState == null)
_treeViewState = new TreeViewState();
_treeView = new ProjectTreeView(_cacheDataTable, _treeViewState);
_treeView.OnSelected -= OnSelected;
_treeView.OnSelected += OnSelected;
_treeView.OnDataChange -= SaveCache;
_treeView.OnDataChange += SaveCache;
}
private void OnSelected(int index)
{
_selectedElemIndex = index;
}
private void DrawTree()
{
Rect rect = GUILayoutUtility.GetRect(1000, 10000, 0, 10000);
_treeView.OnGUI(rect);
}
private void DrawElemInfo()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
GUILayout.Label("选中项目详情", EditorStyles.label);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextField(selectedElem.buildTarget, GUILayout.Width(100));
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();

EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
EditorGUILayout.BeginVertical();
EditorGUI.BeginChangeCheck();
selectedElem.name = EditorGUILayout.TextField("项目名称", selectedElem.name);
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.TextField("项目路径", selectedElem.projectPath);
EditorGUI.EndDisabledGroup();
if(EditorGUI.EndChangeCheck())
{
SaveCache();
_treeView.Reload();
}
EditorGUILayout.EndVertical();

EditorGUILayout.BeginVertical(GUILayout.Width(50));
if(GUILayout.Button("删除"))
{
_cacheDataTable.elems.RemoveAt(_selectedElemIndex);
SaveCache();
_treeView.Reload();
}
if(GUILayout.Button("重置"))
{
selectedElem.name = Path.GetFileName(selectedElem.projectPath);
SaveCache();
_treeView.Reload();
}
EditorGUILayout.EndVertical();

if(GUILayout.Button("打开", GUIlayout.Width(50), GUILayout.ExpandHeight(true)))
{
CmdHelper.LaunchUnityProject(selectedElem.projectPath, selectedElem.buildTarget);
Close();
}
EditorGUILayout.EndHorizontal();
EidtorGUILayout.EndVertical();
}
private void OnGUI()
{
DrawTree();
if(selectedElem != null)
{
DrawElemInfo();
}
GUILayout.FlexibleSpace();
DrawButton();
}
private void DrawButton()
{
EditorGUILayout.BeginHorizontal();
if(GUILayout.Button("关联已有项目", GUILayout.Width(100), GUIlayout.ExpandHeight(true)))
{
var folder = EditorUtility.OpenFolderPannel("", _rootPath, "");
if(!string.IsNullOrEmpty(folder))
{
folder = folder.Replace('/', '\\');
if(folder == _curProjectPath)
{
Debug.LogError("不能关联当前项目本身");
}
else
{
DirectoryInfo folderInfo = new DirectoryInfo(folder);
var childDirec = folderInfo.GetDirectories("Assets", SearchOption.TopDirectoryOnly);
bool valide = childDirec.Lenght == 1;
if(valid)
{
bool hasExisted = false;
for(int i = 0;i < _cacheDataTable.elems.Count; ++i)
{
if(_cacheDataTable.elems[i].projectPath == folderInfo.FullName)
{
hasExisted = true;
_selectedElemIndex = i + 1;
_treeView.SetSelection(new List<int>{_selectedElemIndex});
break;
}
}
if(!hasExisted)
{
AddItem(folderInfo);
}
}
else
{
Debug.LogError("关联的目录不是Unity项目");
}
}
}
}
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.Label("当前项目复制版");
_copyProjectName = EditorGUIlayout.TextField("项目名称:", _copyProjectName);
EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(_copyProjectName));
if(GUILayout.Button("创建"))
{
var folder = EditorUtility.OpenFolderPanel("", _rootPath, "");
if(!string.IsNullOrEmpty(folder))
{
string path = Path.Combine(folder, _copyProjectName);
if(Directory.Exists(path))
{
Debug.LogError("工程名已经存在");
}
else
{
DirectoryInfo folderInfo = new DirectoryInfo(path);
try
{
folderInfo.Create();
LinkProject(folderInfo);
AddItem(folderInfo, true);
}
catch
{
Debug.LogError("Error");
}
}
}
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndHorizontal();
}
private void LinkProject(DirectoryInfo folderInfo)
{
string orgPath = Path.Combine(_curProjectPath, "Assets");
string copyPath = Path.Combine(folderInfo.FullName, "Assets");
CmdHelper.LinkFolder(orgPath, copyPath);
string orgPath = Path.Combine(_curProjectPath, "ProjectSettings");
string copyPath = Path.Combine(folderInfo.FullName, "ProjectSettings");
CmdHelper.LinkFolder(orgPath, copyPath);
string orgPath = Path.Combine(_curProjectPath, "Packages");
string copyPath = Path.Combine(folderInfo.FullName, "Packages");
CmdHelper.LinkFolder(orgPath, copyPath);
}
private void AddItem(DirectoryInfo folderInfo, bool copyTarget = false)
{
CacheDataElem elem = new CacheDataElem
{
name = folderInfo.name,
projectPath = folderInfo.FullName
};
if(copyTarget)
{
elem.buildTarget = BuildPipeline.GetBuildTargetName(EditorUserBuildSettings.activeBuildTarget);
}
_cacheDataTable.elems.Add(elems);
SaveCache();
_treeView.Reload();
_treeView.SetSelection(new List<int>{_cacheDataTable.elems.Count});
_selectedElemIndex = _cacheDataTable.elems.Count - 1;
}
private void LoadCache()
{
var tableStr = EditorPrefs.GetString(_CACHE_TABLE_HEADER + Application.dataPath, null);
if(string.IsNullOrEmpty(tableStr))
{
_cacheDataTable = new CacheDataTable();
}
else
{
_cacheDataTable = JsonUtility.FromJson<CacheDataTable>(tableStr);
}
}
private void SaveCache()
{
var tableStr = JsonUtility.ToJson(_cacheDataTable);
EditorPrefs.SetString(_CACHE_TABLE_HEADER + Application.dataPath, tableStr);
}
}
}

功能

Unity自定义编辑器时,需要使用GUIStyle自定义UI组件的显示(包括区域大小、位置、以及字体等),这里自定义一个GUIStyle参数调节预览窗口,辅助自定义编辑器。
当然,Unity的EditorStyles类里面提供了一些内置的GUIStyle,我们也可以复制其中的一个GUIStyle,然后微调一些参数,如EditorStyles.toolbar
EditorGUIUtility.currentViewWidth可以获取当前窗口的宽度。

源码

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
using UnityEngine;
using UnityEditor;

public class GUIStyleEditor : EditorWindow
{
[MenuItem("Window/GUIStyleEditor")]
static void Open()
{
var win = GUIStyleEditor.GetWindow<GUIStyleEditor>();
win.titleContent = new GUIContent("GUIStyleEditor");
win.Show();
}
GUIStyle _GUIStyle = new GUIStyle();
GUIStyle _GUIContent = new GUIStyle();

GUIContent[] _GUIContents = null;
GUIStyle _GUIStyleBg = new GUIStyle();
Vector2 _scrollPos = Vector2.zero;

private void OnEnable()
{
_GUIStyleBg.margin = new RectOffset(-10, -10, 10, 10);
_GUIStyleBg.padding = new RectOffset(10, 10, 5, 5);
_GUIStyleBg.border = new RectOffset(10, 10, 10, 10);
_GUIStyleBg.normal.background = EditorGUIUtility.IconContent("0L box@2x").image as Texture2D;
_GUIContents = new GUIContent[]{_GUIContent, _GUIContent, _GUIContent};

_GUIStyle.border = new RectOffset(10, 10, 10, 10);
_GUIStyle.margin = new RectOffset(10, 10, 10, 10);
_GUIStyle.padding = new RectOffset(10, 10, 10, 10);
_GUIStyle.overflow = new RectOffset(5, 5, 0, 0);
_GUIStyle.normal.background = EditorGUIUtility.IconContent("0L box@2x").image as Texture2D;
_GUIContent.image = EditorGUIUtility.IconContent("BuildSettings.Editor").image;
}

private void OnGUI()
{
GUILayout.BeginVertical(_GUIStyleBg);
GUILayout.Label("GUIContent");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("GUIContent.text");
_GUIContent.text = EditorGUILayout.TextField(_GUIContent.text);
EditorGUILayout.EndHorizontal();
_GUIContent.image = EditorGUILayout.ObjectField("GUIContent.image", _GUIContent.image, typeof(Texture), false, GUILayout.Height(40)) as Texture;
GUILayout.Space(10);

GUILayout.Label("GUIStyle");
_GUIStyle.fontStyle = (FontStyle)EditorGUILayout.EnumPopup("GUIStyle.fontStyle", _GUIStyle.fontStyle);
_GUIStyle.alignment = (TextAnchor)EditorGUILayout.EnumPopup("GUIStyle.alignment", _GUIStyle.alignment);
_GUIStyle.imagePosition = (ImagePosition)EditorGUILayout.EnumPopup("GUIStyle.imagePosition", _GUIStyle.imagePosition);
_GUIStyle.contentOffset = EditorGUILayout.Vector2Field("GUIStyle.contentOffset", _GUIStyle.contentOffset);
_GUIStyle.border = DrawRectOffset("GUIStyle.border", _GUIStyle.border);
_GUIStyle.margin = DrawRectOffset("GUIStyle.margin", _GUIStyle.margin);
_GUIStyle.padding = DrawRectOffset("GUIStyle.padding", _GUIStyle.padding);
_GUIStyle.overflow = DrawRectOffset("GUIStyle.overflow", _GUIStyle.overflow);
_GUIStyle.normal.textColor = EditorGUILayout.ColorField("textColor", _GUIStyle.normal.textColor);
_GUIStyle.normal.backgrount = EditorGUILayout.ObjectField("GUIContent.image", _GUIStyle.normal.background, typeof(Texture2D), false, GUILayout.Height(40)) as Texture2D;
GUILayout.EndVertical();


GUILayout.Space(2);
_scrollPos = GUILayout.BeginScrollView(_scrollPos, _GUIStyleBg);
GUILayout.Button(_GUIContent, _GUIStyle);
GUILayout.Label(_GUIContent, _GUIStyle);
GUILayout.Box(_GUIContent, _GUIStyle);
GUILayout.TextField(_GUIContent.text, _GUIStyle);
GUILayout.TextArea(_GUIContent.text, _GUIStyle);
GUILayout.Toggle(ture, _GUIContent, _GUIStyle);
GUILayout.Toolbar(1, _GUIContents, _GUIStyle);
GUILayout.SelectionGrid(1, _GUIContents, 2, _GUIStyle);
GUILayout.RepeatButton(_GUIContent, _GUIStyle);
GUILayout.PaswordField(_GUIContent.text, '*', _GUIStyle);
GUILayout.BeginVertical(_GUIStyle);
GUILayout.Space(100);
GUILayout.EndScrollView();
GUILayout.EndVertical();
}
Vector4 RectOffsetToVector4(RectOffset rectOffset)
{
return new Vector4(rectOffset.left, rectOffset.right, rectOffset.top, rectOffset.bottom);
}
RectOffset Vector4ToRectOffset(Vector4 vector)
{
return new RectOffset((int)vector.x, (int)vector.y, (int)vector.z, (int)vector.w);
}
RectOffset DrawRectOffset(string text, RectOffset rectOffset)
{
Vector4 vec = RectOffsetToVector4(rectOffset);
vec = EditorGUILayout.Vector4Field(text, vec);
return Vector4ToRectOffset(vec);
}
}

功能

在Unity中实现SVN操作拓展。

源码

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
//https://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-automation.html
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace Svn
{
public static class SvnAssetsMenu
{
[MenuItem("Assets/SVN/Commit")]
static void Commit()
{
List<string> paths = new List<string>();
for(int i =0; i<Selection.assetGUIDs.Length; ++i)
{
paths.Add(AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[i]));
}
if(paths.Count == 0) return;
string workDir = Path.GetDirectoryName(Application.dataPath);
OpenCommitWindow(workDir, paths);
}
[MenuItem("Assets/SVN/Update")]
static void Update()
{
List<string> paths = new List<string>();
for(int i =0; i<Selection.assetGUIDs.Length; ++i)
{
paths.Add(AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[i]));
}
if(paths.Count == 0) return;
string workDir = Path.GetDirectoryName(Application.dataPath);
OpenUpdateWindow(workDir, paths);
}
[MenuItem("Assets/SVN/Revert")]
static void Revert()
{
List<string> paths = new List<string>();
for(int i =0; i<Selection.assetGUIDs.Length; ++i)
{
paths.Add(AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[i]));
}
if(paths.Count == 0) return;
string workDir = Path.GetDirectoryName(Application.dataPath);
OpenRevertWindow(workDir, paths);
}
public static void OpenCommitWindow(string wrokDirectory, List<string> paths)
{
string pathStr = CombinePathString(paths);
ExecuteTortoiseClient("/command:commit ". workDirectory, pathStr);
}
public static void OpenUpdateWindow(string wrokDirectory, List<string> paths)
{
string pathStr = CombinePathString(paths);
ExecuteTortoiseClient("/command:update ". workDirectory, pathStr);
}
public static void OpenRevertWindow(string wrokDirectory, List<string> paths)
{
string pathStr = CombinePathString(paths);
ExecuteTortoiseClient("/command:revert ". workDirectory, pathStr);
}
public static void ExecuteTortoiseClient(string cmd, string workDirectory, string path)
{
System.Threading.ThreadPool.QueueUserWorkItem(delegate(object state))
{
Process p = null;
try
{
ProcessStartInfo start = new ProcessStartInfo("TortoiseProc.exe");
start.WorkingDirectory = workDirectory;
start.Arguments = cmd + "/path:\"" + path + "\"";
p = Process.Start(start);
}
catch(System.Exception e)
{
UnityEngine.Debug.LogException(e);
if(p!=null) p.Close();
}
});
}
public static string CombinePathString(List<string> paths)
{
StringBuilder stringBuilder = new StringBuilder();
for(int i =0; i < paths.Count - 1; ++i)
{
stringBuilder.Append(paths[i]);
stringBuilder.Append("*");
stringBuilder.Append(string.Format("{0}.meta", paths[i]));
stringBuilder.Append("*");
}
if(paths.Count > 0)
{
stringBuilder.Append(paths[paths.Count - 1]);
stringBuilder.Append("*");
stringBuilder.Append(string.Format("{0}.meta", paths[paths.Count - 1]));
stringBuilder.Append("*");
}
return stringBuilder.ToString();
}
}
}
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
using System.Diagnostics;
namespace Svn
{
static class SvnClientHelper
{
public static void OpenCommitWindow(string workDirectory)
{
ExecuteTortoiseClient("/command:commit ", workDirectory);
}
public static void OpenUpdateWindow(string workDirectory)
{
ExecuteTortoiseClient("/command:update ", workDirectory);
}
public static void OpenRevertWindow(string workDirectory)
{
ExecuteTortoiseClient("/command:revert ", workDirectory);
}
private static void ExecuteTortoiseClient(string cmd, string workDirectory)
{
System.Threading.ThreadPool.QueueUserWorkItem(delegate(object state))
{
Process p = null;
try
{
ProcessStartInfo start = new ProcessStartInfo("TortoiseProc.exe");
start.Arguments = cmd + "/path:\"" + workDirectory + "\"";
p = Process.Start(start);
}
catch(System.Exception e)
{
UnityEngine.Debug.LogException(e);
if(p!=null) p.Close();
}
});
}
}
}
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
//https://svnbook.red-bean.com/en/1.6/svn.advanced.changelists.html
//https://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-cli-main.html#tsvn-cli-addignore
//https://blog.stone-head.org/svn-changelist/
//https://www.visualsvn.com/support/svnbook/ref/svn/#svn.ref.svn.sw.targets
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using UnityEndgine;
using System.Text;
using Debug = UnityEngine.Debug;
namesapce Svn
{
public static class SvnCmdHelper
{
enum TagType
{
Added,
UnTrack,
Modified,
Deleted
}
static string[] _tags = new string[] {"A", "?", "M", "!"};
private const string _defaultSvnGroupName = "_default_";

static string _tempFilePath = null;
static strng _tempFileName = "_svnTemp.txt";
static string GetTempFilePath();
{
if(_tempFilePath = null)
{
_tempFilePath = Path.Combine(Application.temporaryCachePath, _tempFileName);
}
return _tempFilePath;
}
static StreamReader OpenText()
{
//https://www.open.collab.net/scdocs/SVNEncoding.html
FileInfo fileInfo = new FileInfo(GetTempFilePath());
return new StreamReader(fileInfo.OpenRead(), Encoding.GetEncoding("gb2312"));
}
static StreamWriter CreateText()
{
FileInfo fileInfo = new FileInfo(GetTempFilePath());
return new StreamWriter(fileInfo.Create(), Encoding.GetEncoding("gb2312"));
}
static void CreateProcessStart(string workDirectory)
{
ProcessStartInfo start = new ProcessStartInfo("cmd.exe");
start.CreateNoWindow = true;
start.ErrorDialog = true;
start.UseShellExecute = false;
start.WorkingDirectory = workDirectory;

start.RedirectStandardOutput = true;
start.RedirectStandardError = true;
start.RedirectStandardInput = true;
start.StandardOutputEncoding = Endcoding.GetEncoding("GBK");
start.StandardErrorEncoding = Encoding.GetEncoding("GBK");
return start;
}
public static string GetSvnVertsion()
{
string workDirectory = Path.GetDirectoryName(Application.dataPath);
ProcessStartInfo start = CreateProcessStart(workDirectory);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
sw.WriteLine("svn --version");
}
using(var sr = process.StandardError)
{
string line;
do
{
line = sr.ReadLine();
if(!string.IsNullOrEmpty(line)) return null;
}
while(line != null);
}
string rst = "svn";
using(var sr = process.StandardOutput)
{
string line;
const string vStart = "svn, version";
do
{
line = sr.ReadLine();
if(line.StartsWith(vStart))
{
rst = line.Sbustring(vStart.Length);
break;
}
}
while(line != null);
}
process.Close();
return rst;
}
public static string GetSvnWorkDir()
{
string workDirectory = Path.GetDirectoryName(Application.dataPath);
ProcessStartInfo start = CreateProcessStart(workDirectory);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
sw.WriteLine("svn info");
}
using(var sr = process.StandardError)
{
string line;
do
{
line = sr.ReadLine();
if(!string.IsNullOrEmpty(line)) return null;
}
while(line != null);
}
string rst = "svn";
using(var sr = process.StandardOutput)
{
string line;
const string vStart = "Working Copy Root Path: ";
do
{
line = sr.ReadLine();
if(line.StartsWith(vStart))
{
rst = line.Sbustring(vStart.Length);
break;
}
}
while(line != null);
}
}
static void FillEmptyStatus(Dictionary<string, Dictionary<string, List<string>>> status)
{
if(!status.ContainsKey(_defaultSvnGroupName))
status[_defaultSvnGroupName] = new Dictionary<string, List<string>>();
foreach(var kv in status)
{
for(int i = 0; i < _tags.Length; ++i)
{
if(!kv.Value.ContainsKey(_tags[i]))
kv.Value[_tags[i]] = new List<string>();
}
}
}
public static Dictionary<string, Dictionary<string, List<string>>> GetStatus()
{
string workDirectory = Path.GetDirectoryName(Application.dataPath);
ProcessStartInfo start = CreateProcessStart(workDirectory);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
sw.WriteLine("svn status");
}
Dictionary<string, Dictionary<string, List<string>>> rst = new Dictionary<string, Dictionary<string, List<string>>>();
Dictionary<string, List<string>> levOne = null;
List<string> levTow = null;
Regex stateRegex = new Regex(@"^([M?A!])\s*(.*)$");
Regex stateRegex2 = new Regex(@"--- Changelist\s*'(.*)':$");
int state = 0;
try
{
using(var sr = process.StandardError)
{
string line;
do
{
line = sr.ReadLine();
if(state == 0)
{
if(line.StartsWith(workDirectory))
{
state = 1;
levOne = new Dictionary<string, List<string>>();
rst[_defaultSvnGroupName] = levOne;
}
}
else if(state == 1)
{
if(!string.IsNullOrEmpty(line))
{
var match = stateRegex.Match(line);
if(match.Groups.Count > 2)
{
string key = match.Groups[1].ToString();
string value = match.Groups[2].ToString();
string dir = value.Replace("\\", "/");
if(!levOne.ContainsKey(key))
{
levTow = new List<string>();
levOne[key] = levTow;
}
else
{
levTow = LevOne[key];
}
levTow.Add(dir);
}
}
else
{
state = 2;
}
}
else if(state == 2)
{
if(!string.IsNullOrEmpty(line))
{
var match = stateRegex2.Match(line);
if(match.Groups.Count > 1)
{
string value = match.Groups[1].ToString();
state = 1;
levOne = new Dictionary<string, List<string>>();
rst[value] = levOne;
}
}
}
}
while(line != null);
}
}
finally
{
process.Close();
FillEmptyStatus(rst);
return rst;
}
}
public static void SetChangelistForFilterData(SvnFilterData data)
{
int ignoreListLimitCount = 10000;
var workDir = GetSvnWrokDir();
var status = GetStatus(workDir);
ClearAllChangeList(workDir, ref status);
AddAllUnTracked(status, workDir);
List<List<string>> ignoreList = new List<List<string>>();
List<string> list;
foreach(var kv in status)
{
if(kv.Key == "ignore-on-commit") continue;
list = kv.Value[_tags[(int)TagType.Added]];
Filter(ignoreListLimitCount, list, data.ignorePatternAdd, data.ignoreFilesAdd, data.ignoreFoldersAdd, ref ignoreList);
list = kv.Value[_tags[(int)TagType.Deleted]];
Filter(ignoreListLimitCount, list, data.ignorePatternDelete, data.ignoreFilesDelete, data.ignoreFoldersDelete, ref ignoreList);
list = kv.Value[_tags[(int)TagType.Modified]];
Filter(ignoreListLimitCount, list, data.ignorePatternModify, data.ignoreFilesModify, data.ignoreFoldersModify, ref ignoreList);
}
foreach(var v in ignoreList)
{
if(v.Count > 0)
AddChangeList("ignore-on-commit", v, workDir, ref status);
}
}
static void Filter(int limitCount, List<string> list, List<string> ignorePattern, List<string> ignoreFiles, List<string> ignoreFolders, ref List<List<string>> outList)
{
if(outList == null) outList = new List<List<string>>();
List<string> temp = null;
if(outList.Count > 0)
{
temp = outList[outList.Count - 1];
if(temp.Count >= limitCount) temp =null;
}
if(temp == null)
{
temp = new List<string>();
outList.Add(temp);
}
bool find = false;
Regex stateRegex = new Regex(@"(\.[^\.]+)+$");
foreach(var v in list)
{
find = false;
if(!find)
{
foreach(var subFix in ignorePattern)
{
var match = stateRegex.Match(v);
if(match.Success)
{
var group = match.Groups[match.Groups.Count - 1]
int caCount = group.Captures.Count;
for(int k = 0; k <caCount; ++k)
{
if(group.Captures[k].Value == subFix)
{
find = true;
break;
}
}
if(find) break;
}
}
}
if(!find)
{
foreach(var subFix in ignoreFiles)
{
if(v == subFix)
{
find = true;
break;
}
}
}
if(!find)
{
foreach(var subFix in ignoreFolders)
{
if(v.StartsWith(subFix))
{
find = true;
break;
}
}
}
if(find)
{
//https://jeremy.hu/svn-at-character-peg-revision-is-not-allowed-here/
string path = v;
if(path.Contains("@"))
path = string.Format("{0}@", path);
if(temp.COunt < limitCount)
{
temp.Add(path);
}
else
{
temp = new List<string>();
outList.Add(temp);
temp.Add(path);
}
}
}
}
static void AddAllUnTracked(Dictionary<string, Dictionary<string, List<string>>> status, string wrokDir)
{
var defaultGroup = status[_defaultSvnGroupName];
var newGroup = defaultGroup["?"];
if(newGroup.Count == 0) return;
List<string> files = new List<string>();
foreach(var path in newGroup)
{
if(Directory.Exists(string.Format("{0}/{1}", wrokDir, path)))
{
files.Add(path);
}
else
{
files.Add(path);
}
}
if(files.Count > 0)
{
newGroup.Clear();
AddFile(files, workDir, ref status);
}
}
public static void AddChangeList(string changelist, List<string> paths, string workDir, ref Dictionary<string, Dictionary<string, List<string>>> rst)
{
ProcessStartInfo start = CreateProcessStart(workDir);
Process process = Process.Start(start);
using(var sw = process.StandardInput)
{
using(var write = CreateText())
{
for(int i = 0; i<paths.Count; ++i)
{
write.WriteLine(paths[i]);
}
}
sw.WriteLine(string.Format("svn changelist {0} --targets {1} -R", changelist, GetTempFilePath());
}
try
{
using(var sr = process.StandardOutput)
{
string line = sr.ReadToEnd();
}
}
finally
{
process.Close();
}
}
public static void ClearAllChangeList(string workDir, ref Dictionary<string, Dictionary<string, List<string>>> rst)
{
ProcessStartInfo start = CreateProcessStart(workDir);
Process process = Process.Start(start);
List<string> changelist = new List<string>();
var defaultList = rst[_defaultSvnGroupName];
foreach(var kv in rst)
{
if(_defaultSvnGroupName != kv.Key)
{
changelist.Add(kv.Key);
for(int i = 0; i<_tags.Length; ++i)
{
defaultList[_tags[i]].AddRange(kv.Value[_tags[i]]);
kv.Value[_tags[i]].Clear();
}
}
}
rst.Clear();
rst.Add(_defaultSvnGroupName, defaultList);
using(var sw = process.StandardInput)
{
for(int i= 0;i< changelist.Count; ++i)
{
sw.WriteLine(string.Format("svn changelist --remove --changelist {0} --depth infinity .", changelist[i]);
}
}
try
{
using(var sr = process.StandardOutput)
{
string line = sr.ReadToEnd();
}
}
finally
{
process.Close();
}
}
public static void AddFile(List<string> filePath, string workDir, ref Dictionary<string, Dictionary<string, List<string>>> rst)
{
ProcessStartInfo start = CreateProcessStart(workDir);
Process process = Process.Start(start);
using(var write = CreateText())
{
for(int i= 0;i<filePath.Count; ++i)
{
write.WriteLine(filePath[i]);
}
}
using(var sw = process.StandardInput)
{
sw.WriteLine(string.Format("svn add --targets {0} --depth infinity", GetTempFilePath()));
}
Dictionary<string, List<string>> LevOne = rst[_defaultSvnGroupName];
List<string> levTow = null;
Regex stateRegex = new Regex(@"^(A)(\s\(bin\))?\s*(.*)$");
try
{
using(var sr = process.StandardOutput)
{
string line;
do
{
line = sr.ReadLine();
if(!string.IsNullOrEmpty(line))
{
var math = stateRegetx.Match(line):
if(match.Groups.Count > 2)
{
string key = match.Groups[1].ToString();
string value = match.Groups[match.Groups.Count - 1].ToString();
string dir = value.Replace("\\","/");
if(!levOne.ContainsKey(key))
{
levTow = new List<string>();
levOne[key] = levTow;
}
else
{
levTow = levOne[key];
}
levTow.Add(dir);
}
}
}
while(line != null);
}
}
finally
{
process.Close();
}
}
}
}
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
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Svn
{
[System.Serializable]
public class SvnFilterData
{
public int careerIndex = 0;
public List<string> ignorePatternModify = new List<string>();
public List<string> ignoreFilesModify = new List<string>();
public List<string> ignoreFoldersModify = new List<string>();
public List<string> ignorePatternAdd = new List<string>();
public List<string> ignoreFilesAdd = new List<string>();
public List<string> ignoreFoldersAdd = new List<string>();
public List<string> ignorePatternDelete = new List<string>();
public List<string> ignoreFilesDelete = new List<string>();
public List<string> ignoreFoldersDelete = new List<string>();
public SvnFilterData()
{
careerIndex = 0;
ignorePatternModify = new List<string>();
ignoreFilesModify = new List<string>();
ignoreFoldersModify = new List<string>();
ignorePatternAdd = new List<string>();
ignoreFilesAdd = new List<string>();
ignoreFoldersAdd = new List<string>();
ignorePatternDelete = new List<string>();
ignoreFilesDelete = new List<string>();
ignoreFoldersDelete = new List<string>();
}
public static void Save(SvnFilterData data)
{
string jsonStr = JsonUtility.ToJson(data);
EditorPrefs.SetString(SaveKey, jsonStr);
}
public static bool Load(out SvnFilterData data)
{
string jsonStr = EditorPrefs.GetString(SaveKey, null);
bool hasData = !string.IsNullOrEmpty(jsonStr);
if(hasData)
{
data = JsonUtility.FromJson<SvnFilterData>(jsonStr);
}
else
data = new SvnFilterData();
return hasData;
}
public static string SaveKey
{
get {return string.Format("{0}_svnFilterData", Application.dataPath);}
}
}
}
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Text;
namespace Svn
{
public class SVNSettingWindow : EditorWindow
{
const string _MenuPath = "Svn";
const string _tip = "";
string[] _career = {"Artist", "Tester", "Programmer", "Designer"};
GUIContent[] _tabContents = new GUIContent[]{
new GUIContent("Artist"),
new GUIContent("Tester"),
new GUIContent("Programmer"),
new GUIContent("Designer"),
};
[MenuItem(_MenuPath + "Update", priority = 0, validate = false)]
public static void UpdateSVN()
{
SvnClientHelper.OpenUpdateWindow(SvnCmdHelper.GetSvnWorkDir());
}
[MenuItem(_MenuPath + "Revert", priority = 0, validate = false)]
public static void RevertSVN()
{
SvnClientHelper.OpenRevertWindow(SvnCmdHelper.GetSvnWorkDir());
}
[MenuItem(_MenuPath + "CommitAll", priority = 0, validate = false)]
public static void CommitAllSVN()
{
SvnClientHelper.OpenCommitWindow(SvnCmdHelper.GetSvnWorkDir());
}
[MenuItem(_MenuPath + "CommitAllWithFilter", priority = 0, validate = true)]
public static bool CommitAllWithFilterValidate()
{
if(string.IsNullOrEmpty(SvnCmdHelper.GetSvnVerison())) return false;
if(string.IsNullOrEmpty(SvnCmdHelper.GetSvnWorkDir())) return false;
return true;
}
[MenuItem(_MenuPath + "CommitAllWithFilter", priority = 0, validate = false)]
public static void CommitAllWithFilter()
{
if(SvnFilterData.Load(out var data))
{
SvnCmdHelper.SetChangelistForFilterData(data);
SvnClientHelper.OpenCommitWindow(SvnCmdHelper.GetSvnWorkDir());
}
else
Open();
}
[MenuItem(_MenuPath + "Settings", false, 1000)]
public static void Open()
{
SVNSettingWindow window = GetWindow<SVNSettingWindow>("Setting");
window._svnVersion = SvnCmdHelper.GetSvnVersion();
window._workDir = SvnCmdHelper.GetSvnWorkDir();
if(!string.IsNullOrEmpty(window._workDir))
window._workDir = window._workDir.Replace("\\","/");
window.minSize = new Vector2(520, 520);
window.InitConfigData();
window.Show();
}

[SerializeField] private string _svnVersion = null;
[SerializeField] private string _workDir = null;

[SerializeField] private int careerSelectedIdx = -1;
[SerializeField] private string ignorePatternModify = "";
[SerializeField] private List<string> ignoreFilesModify = new List<string>();
[SerializeField] private List<string> ignoreFoldersModify = new List<string>();
[SerializeField] private string ignorePatternAdd = "";
[SerializeField] private List<string> ignoreFilesAdd = new List<string>();
[SerializeField] private List<string> ignoreFoldersAdd = new List<string>();
[SerializeField] private string ignorePatternDelete = "";
[SerializeField] private List<string> ignoreFilesDelete = new List<string>();
[SerializeField] private List<string> ignoreFoldersDelete = new List<string>();

[SerializeField] private Vector2 scrollPos = Vector2.zero;
private GUIContent content = new GUIContent();
private StringBuilder stringBuilder = new StringBuilder();
private GUIContent GetGUIContent(string text, Texture image, string tooltip)
{
content.text = text;
content.image = image;
content.tooltip = tooltip;
return content;
}
private void InitConfigData()
{
if(SvnFilterData.Load(out var data))
{
careerSelectedIdx = data.careerIndex;
AddWorkDir(_workDir, data.ignoreFilesModify, ref ignoreFilesModify);
AddWorkDir(_workDir, data.ignoreFoldersModify, ref ignoreFoldersModify);
PatternCombine(data.ignorePatternModify, ref ignorePatternModify);
AddWorkDir(_workDir, data.ignoreFilesAdd, ref ignoreFilesAdd);
AddWorkDir(_workDir, data.ignoreFoldersAdd, ref ignoreFoldersAdd);
PatternCombine(data.ignorePatternAdd, ref ignorePatternAdd);
AddWorkDir(_workDir, data.ignoreFilesDelete, ref ignoreFilesDelete);
AddWorkDir(_workDir, data.ignoreFoldersDelete, ref ignoreFoldersDelete);
PatternCombine(data.ignorePatternDelete, ref ignorePatternDelete);
}
}
void PatternSplit(string pattern, ref List<string> patternList)
{
patternList.Clear();
var slpits = pattern.Split(' ");
foreach(var v in splits)
{
if(string.IsNullOrEmpty(v))
patternList.Add(v);
}
}
var PatternCombine(List<string> patternList, ref string pattern)
{
StringBuilder stringBuilder = new StringBuilder();
const string space = " ";
foreach(var v in patternList)
{
stringBuilder.Append(v);
stringBuilder.Append(space);
}
pattern = stringBuilder.ToString();
}
void RemoveWorkDir(string workDir, List<string> org, ref List<string> dst)
{
dst.Clear();
workDir = string.Format("{0}/", workDir);
workDir = workDir.Replace("\\","/");
for(int i = 0;i<org.Count; ++i)
{
dst.Add(org[i].Replace("\\", "/").Replace(workDir, ""));
}
}
void AddWorkDir(string workDir, List<string> org, ref List<string> dst)
{
dst.Clear();
for(int i = 0;i<org.Count; ++i)
{
if(org[i].StartsWith(workDir))
dst.Add(org[i]);
else
dst.Add(string.Format("{0}/{1}", workDir, org[i]));
}
}
private void OnGUI()
{
bool show = false;
EditorGUILayout.LabelField("Base Info");
if(_svnVersion == null)
{
EditorGUILayout.HelpBox("never find svn!", MessageType.Error, true);
}
else if(_workDir == null)
{
EditorGUI.indentLevel += 1;
GUI.enabled = false;
EditorGUILayout.TextField("svn version:", _svnVersion);
GUI.enabled = true;
EditorGUI.indentLevel -= 1;
EditorGUILayout.HelpBox("current project not in svn control", MessageType.Warning, true);
}
else
{
GUI.enabled = false;
EditorGUI.indentLevel += 1;
EditorGUILayout.TextField("svn version:", _svnVersion);
EditorGUILayout.TextField("workDir:", _workDir);
EditorGUI.indentLevel -= 1;
GUILayout.Space(10);
GUI.enabled = true;
show = true;
}
GUI.enabled = show;
DrawBody();
GUI.enabled = true;
}
void DrawBody()
{
EditorGUILayout.HelpBox(_tip, MessageType.Info, true);
EditorGUI.BeginChangeCheck();
careerSelectedIdx = GUILayout.Toolbar(careerSelectedIdx, _tabContents);
if(EditorGUI.EndChangeCheck())
{
PresetConfig(careerSelectedIdx);
EditorGUIUtility.editingTextField = false;
}

GUI.enbaled = careerSelectedIdx != -1;
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Ignore Setting");
if(GUILayout.Button(GetGUIContent("save", nul, "save"), GUILayout.Width(50)))
{
SaveConfig(careerSelectedIdx);
}
GUILayout.EndHorizontal();

scrollPos = GUILayout.BeginScrollView(scrollPos, EditorStyles.helpBox);
Panel("Modify Ignore", ref ignorePatternModify, ref ignoreFilesModify, ref ignoreFoldersModify);
Panel("Add Ignore", ref ignorePatternAdd, ref ignoreFilesAdd, ref ignoreFoldersAdd);
Panel("Delete Ignore", ref ignorePatternDelete, ref ignoreFilesDelete, ref ignoreFoldersDelete);
GUILayout.EndScrollView();
GUILayout.Space(10);
}
private void Panel(string name, ref string ignPat, ref List<string> ignFiles, ref List<string> ignFolders)
{
GUILayout.Space(10);
EditorGUILayout.LabelField(name);
EditorGUI.indentLevel += 1;
ignPat = EditorGUILayout.TextField(GetGUIContent("Ignore Pattern:", null, ""), ignPat);

EditorGUILayout.LabelField("Ignore Files");
EditorGUI.indentLevel += 1;
int removeIdx = -1;
for(int i = 0;i<ignFiles.Count; ++i)
{
EditorGUILayout.BeginHorizontal();
GUI.enabled = false;
EditorGUILayout.TextTield(GetGUIContent("Ignore Files:", null, ""), ignFiles[i]);
GUI.enabled = true;
if(GUILayout.Button("-", GUILayout.Width(20)))
removeIdx = i;
EditorGUILayout.EndHorizontal();
}
if(removeIdx != -1)
{
ignFiles.RemoveAt(removeIdx);
}
EditorGUILayout.BeginHorizontal();
GUILayout.Label("", GUILayout.Width(24));
if(GUILayout.Button(GetGUIContent("Add File", null, "")))
{
string file = EditorUtility.OpenFilePanel("Select File", _workDir, null);
if(file.Contens(_workDir) &&!ignFiles.Contents(file))
ignFiles.Add(file);
}
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel -= 1;

EditorGUILayout.LabelField("Ignore Folders");
EditorGUI.indentLevel += 1;
removeIdx = -1;
for(int i = 0;i<ignFolders.Count; ++i)
{
EditorGUILayout.BeginHorizontal();
GUI.enabled = false;
EditorGUILayout.TextTield(GetGUIContent("Ignore Folders:", null, ""), ignFolders[i]);
GUI.enabled = true;
if(GUILayout.Button("-", GUILayout.Width(20)))
removeIdx = i;
EditorGUILayout.EndHorizontal();
}
if(removeIdx != -1)
{
ignFolders.RemoveAt(removeIdx);
}
EditorGUILayout.BeginHorizontal();
GUILayout.Label("", GUILayout.Width(24));
if(GUILayout.Button(GetGUIContent("Add Folder", null, "")))
{
string file = EditorUtility.OpenFolderPanel("Select Folder", _workDir, null);
if(file.Contens(_workDir) &&!ignFolders.Contents(file))
ignFolders.Add(file);
}
EditorGUILayout.EndHorizontal();
EditorGUI.indentLevel -= 1;
}

private void PresetConfig(int careerIndex)
{
string career = _careers[careerIndex];
stringBuilder.Clear();
stringBuilder.Append(defaultPattern);
switch(career)
{
case "Artist":
break;
case "Tester":
case "Programmer":
case "Designer":
{
stringBuilder.Append(".mat .anim .png .jpg");
}
}
ignorePatternModify = stringBuilder.ToString();
ignoreFilesModify.Clear();
ignoreFoldersModify.Clear();
ignorePatternAdd = stringBuilder.ToString();
ignoreFilesAdd.Clear();
ignoreFoldersAdd.Clear();
ignorePatternDelete = stringBuilder.ToString();
ignoreFilesDelete.Clear();
ignoreFoldersDelete.Clear();
}

private void SaveConfig(int careerIndex)
{
SvnFilterData data = new SvnFilterData();
data.careerIndex = careerIndex;
RemoveWorkDir(_workDir, ignoreFilesModify, ref data.ignoreFilesModify);
RemoveWorkDir(_workDir, ignoreFoldersModify, ref data.ignoreFoldersModify);
PatternSplit(ignorePatternModify, ref data.ignorePatternModify);
RemoveWorkDir(_workDir, ignoreFilesAdd, ref data.ignoreFilesAdd);
RemoveWorkDir(_workDir, ignoreFoldersAdd, ref data.ignoreFoldersAdd);
PatternSplit(ignorePatternAdd, ref data.ignorePatternAdd);
RemoveWorkDir(_workDir, ignoreFilesDelete, ref data.ignoreFilesDelete);
RemoveWorkDir(_workDir, ignoreFoldersDelete, ref data.ignoreFoldersDelete);
PatternSplit(ignorePatternDelete, ref data.ignorePatternDelete);
}
}
}

功能

Unity中可以通过Scene窗口点击选择模型,但是UI之间是重叠关系,每次点击的时候选中的都是最前面的,即便这个UI并没有显示的内容。所以这里实现了一个UI过滤器,可以非常方便的快速选中看到的UI。

源码

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
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System;
using UnityEngine.UI;
using UnityEditor.SceneManagement;
using System.Linq;
using UnityEngine.SceneManagement;

public class SceneUISelectionFilter : EditorWindow
{
[MenuItem("Tools/Scene UI Selection Filter")]
public static void ShowWindow()
{
Rect rect = new Rect();
if(SceneView.lastActiveSceneView != null)
rect = SceneView.lastActiveSceneView.position;
rect.size = new Vector2(230, 100);
rect.position = rect.position + new Vector2(10, 10);
var win = EditorWindow.GetWindowWithRect<SceneUISelectionFilter>(rect, true);
win.titleContent = new GUIContent("UI Selector");
win.Show();
}

private MethodInfo Internal_PickClosestGO;

private void OnEnable()
{
Assembly editorAssembly = typeof(Editor).Assembly;
System.Type handleUtilityType = editorAssembly.GetType("UnityEditor.HandleUtility");

FieldInfo pickClosestDelegateInfo = handleUtilityType.GetField("pickClosestGameObjectDelegate", BindingFlags.Static | BindingFlags.NonPublic);
Delegate pickHandler = Delegate.CreateDelegate(pickClosestDelegateInfo.FieldType, this, "OnPick");
pickClosestDelegateInfo.SetValue(null, pickHandler);

Internal_PickClosestGO = handleUtilityType.GetMethod("Internal_PickClosestGO", BindingFlags.Static | BindingFlags.NonPublic);
}

private void OnDisable()
{
Assembly editorAssembly = typeof(Editor).Assembly;
System.Type handleUtilityType = editorAssembly.GetType("UnityEditor.HandleUtility");

FieldInfo pickClosestDelegateInfo = handleUtilityType.GetField("pickClosestGameObjectDelegate", BindingFlags.Static | BindingFlags.NonPublic);
pickClosestDelegateInfo.SetValue(null, null);
}

private void OnGUI()
{
GUILayout.Space(10);
EditorGUILayout.LabelField("当前优先选中Image、Text等UI组件");
GUILayout.Space(10);
EditorGUILayout.HelpBox("点击Scene窗口中的UI会自动过滤掉哪些遮挡在前面看不见的UI,从而可以快速选中其中的Image、Text等组件。", MessageType.Inof);
}

private GameObject OnPick(Camera cam, int layers, Vector2 position, GameObject[] ignore, GameObject[] filter, out int materialIndex)
{
materialIndex = -1;
filter = GetPickableObject();

return (GameObject)Internal_PickClosestGO.Invoke(null, new object[] { cam, layers, position, ignore, filter, materialIndex });
}

private GameObject[] GetPickableObject()
{
List<GameObject> gameObjects = new List<GameObject>();
for(int i = 0; EditorSceneManager.loadedSceneCount; ++i)
{
Scene scene = EditorSceneManager.GetSceneAt(i);
foreach(var root in scene.GetRootGameObjects())
{
gameObjects.AddRange(root.GetComponentsInChildren<Graphics>().Select((a)=>{return a.gameObject;}));
}
}
return gameObjects.ToArray();
}
}

原文:
Voronoi Noise

Summary

还有一种噪声叫做维诺噪声。维诺噪声需要一堆随机点,然后基于这些相互最近的点之间构成的图案来生成噪声。维诺噪声和之前的噪声类似,也是基于噪声色块,只不过值类噪声对应色块的随机值表示的是灰度,而泊林噪声对应色块的随机值表示的是灰度变化趋势,维诺噪声对应色块的随机值表示的是色块中的某个随机位置。基于色块的计算相对简单,同时可重复,适用于并行计算。在阅读本文之前,建议你先对着色器基础有所了解,并且知道如何在着色器中生成随机值

Get Cell Values

在我们实现维诺噪声的过程中,每一个色块都对应一个随机点。首先我们来实现一个二维维诺噪声。首先我们照常将其分割为无数的小网格,然后生成随机向量,这个向量表示在对应网格中的位置。然后我们计算当前网格随机位置与当前处理位置的距离。当然整个计算过程都是基于世界坐标系,同时我们可以通过色块尺寸参数来控制色块的密集度。

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
Shader "Tutorial/028_voronoi_noise/2d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;

struct Input {
float3 worldPos;
};

float voronoiNoise(float2 value){
float2 cell = floor(value);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;
float distToCell = length(toCell);
return distToCell;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float noise = voronoiNoise(value);

o.Albedo = noise;
}
ENDCG
}
FallBack "Standard"
}

因为我们需要计算相邻近的随机点,所以我们不仅仅是要计算当前色块,还要计算相邻色块。因此我们使用for循环来遍历从-1到1的九宫格。在每次迭代中,我们都会计算色块随机点与输入值的距离,然后记录下最近距离的色块。用于记录最小距离的变量需要定义在循环外部,并且要有一个大于九宫格直径的默认值。然后我们使用unroll命令来将循环体展开,提升其执行效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
float voronoiNoise(float2 value){
float2 baseCell = floor(value);

float minDistToCell = 10;
[unroll]
for(int x=-1; x<=1; x++){
[unroll]
for(int y=-1; y<=1; y++){
float2 cell = baseCell + float2(x, y);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
}
}
}
return minDistToCell;
}

当然,处理最近距离,我们还想知道最近的随机点是哪个。因此同样在循环外部定义一个坐标变量,然后在循环中更新。在得到随机点的坐标后,我们还可以使用随机函数为该随机点生成一个随机变量,来作为它的唯一标识。然后将函数返回类型改为二维向量,x值用来存最小距离,y存这个唯一标识。在表面着色器中我们就用这个唯一标识来做区域区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
float voronoiNoise(float2 value){
float2 baseCell = floor(value);

float minDistToCell = 10;
float2 closestCell;
[unroll]
for(int x=-1; x<=1; x++){
[unroll]
for(int y=-1; y<=1; y++){
float2 cell = baseCell + float2(x, y);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
}
}
}
float random = rand2dTo1d(closestCell);
return float2(minDistToCell, random);
}
1
2
3
4
5
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float noise = voronoiNoise(value).y;
o.Albedo = noise;
}

重新划分的区域具有相同标识的表示同一区域。因此我们可以在表面着色器中根据这个唯一标识来生成彩色色块。这里我们只需要使用随机函数,将唯一表示转为为三维向量。

1
2
3
4
5
6
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float noise = voronoiNoise(value).y;
float3 color = rand1dTo3d(noise);
o.Albedo = color;
}

Getting the distance to the border

上面通过计算点到随机点之间的距离,来得到以随机点为中心的色块。但是在很多情况下,例如显示色块之间的边界,我们可能希望计算点到边界的距离。一个常用的做法是计算最近随机点的距离、以及第二近的随机点的距离,然后两个距离作差。这个方法效率非常高,但是无法得到精确的边界信息。我们将要使用的方法是计算点到所有临近边界的距离,然后得到最短距离。

为了计算采样点到边界的距离,我们需要再一次遍历最近色块周围的色块。前面我们已经得到了最近色块的数据。然后我们来计算最近色块与其相邻色块边界到采样点的距离。首先我们计算这两个色块中心点,也就是随机点,连线的中心点的位置。然后构造由采样点到该连线中心点的向量。同时计算这两个随机点连线的单位向量。

得到这两个向量后,我们使用点乘,就得到采样点到边界的垂直距离。

在上面我们通过计算采样点到色块中心随机点的距离,得到了新的色块。然后现在我们创建一个新的变量来记录采样点到新生成的色块的边界距离。两次都需要使用到循环语句,并且两次循环都类似,都是从-1到1。所以我们这里对其重新命名,前一次循环的变量命名为x1y1,后一次命名为x2y2

和第一次循环一样,在我们第二次循环的内循环中也要计算每个色块的随机中心点、以及采样点到随机中心点的向量。然后在条件语句下计算采样点到边界的距离。因为我们是要计算最近色块和其他色块边界到采样点的距离,而这里的最近色块也包含在九宫格中,所以需要将其剔除。当两个色块的随机中心点坐标相同时,说明他们是同一个色块,但是因为我们处理的是小数,存在精度问题,所以不能用等号来判断。

然后在条件语句中,我们处理的是不相同的两个色块,计算他们随机中心点所构成的方向向量,然后计算单位向量。并且计算这两个随机中心点连线的中心点,然后和采样点构造一个方向向量。这两个方向向量点乘便是采样点到边界的距离了。然后我们记录下采样点到周围边界的距离最小值。

在得到距离边界的最小值后,我们将函数返回值扩展为三维向量,然后将最小值储存到z值中。

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
float3 voronoiNoise(float2 value){
float2 baseCell = floor(value);

//查找最近色块
float minDistToCell = 10;
float2 toClosestCell;
float2 closestCell;
[unroll]
for(int x1=-1; x1<=1; x1++){
[unroll]
for(int y1=-1; y1<=1; y1++){
float2 cell = baseCell + float2(x1, y1);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
toClosestCell = toCell;
}
}
}

//查找最近色块边界
float minEdgeDistance = 10;
[unroll]
for(int x2=-1; x2<=1; x2++){
[unroll]
for(int y2=-1; y2<=1; y2++){
float2 cell = baseCell + float2(x2, y2);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;

float2 diffToClosestCell = abs(closestCell - cell);
bool isClosestCell = diffToClosestCell.x + diffToClosestCell.y < 0.1;
if(!isClosestCell){
float2 toCenter = (toClosestCell + toCell) * 0.5;
float2 cellDifference = normalize(toCell - toClosestCell);
float edgeDistance = dot(toCenter, cellDifference);
minEdgeDistance = min(minEdgeDistance, edgeDistance);
}
}
}

float random = rand2dTo1d(closestCell);
return float3(minDistToCell, random, minEdgeDistance);
}
1
2
3
4
5
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float3 noise = implVoronoiNoise(value);
o.Albedo = noise.z;
}

Visualising vornoi noise

现在我们维诺噪声函数返回了三个值:采样点到色块随机中心点的距离、当前色块的唯一标识、采样点到色块边界的距离。前面已经介绍了使用唯一标识来生成彩色色块。我们还可以根据采样点到边界的距离来绘制色块边界。因此我们需要根据距离来判断哪些是边界,哪些不是。这个可以使用阶跃函数来实现,大于阈值则返回1,小于阈值则返回0。在得到边界判定值后,我们可以用来对边界颜色、和色块颜色进行插值。我们也可以将边界颜色暴露在材质面板上,这样方便我们后面调节。

1
2
3
4
5
6
7
8
9
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float isBorder = step(noise.z, 0.05);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}

这里有个问题,就是我们上面的边界计算是二选一的,非0即1,这样在颜色插值的时候也变成二选一,会产生明显的锯齿效果。我们可以提前对线条进行模糊操作,通过计算采样点周围边界距离的变化情况,然后将这个变化梯度,当作我们模糊区域的边界,在这个模糊区域外的,依然是二选一,但是在模糊区域以内的边界,则是进行颜色混合。

因为这里计算的边界距离和维诺噪声输入函数的输入值具有相同的缩放关系,所以我们以该输入值为参考,来计算比边界距离的梯度。这里我们还是使用fwidth来计算梯度,因为输入值是二维向量,所以梯度也是二维的,我们可以计算这个梯度向量的长度,然后以此来作为我们模糊区域的边界宽度。然后我们基于阈值进行上下偏移半个边界宽度。你也可以试着调整这个偏移宽度,看看会出现什么效果。

当我们计算出边界模糊区域的上下阈值,我们使用smoothstep来替代step函数,这样就可以应用边界模糊区域了。然后我们对结果翻转,这样我们的边界值就是1。

1
2
3
4
5
6
7
8
9
10
void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = length(fwidth(value)) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}

3d Voronoi

三维维诺噪声的实现方式类似,不过噪声函数的输入值要改成三维先向量,我们的色块也要变成三维的。另外原先平面上的九宫格要换成3x3x3的立方体。而函数返回值依然保持和二维维诺噪声一致。

在表面着色气函数中,我们将整个坐标值传入维诺噪声函数中。但是我们的边界梯度不能再用坐标值来求了,因为现在坐标是三维的,它的梯度方向也是三维的,而我们边界模糊区域应该是沿着二维屏幕方向,如果继续使用三维坐标来求梯度,会有部分区域的模糊区域过大或过小。所以我们这里直接使用边界距离来求解梯度。

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
float3 voronoiNoise(float3 value){
float3 baseCell = floor(value);

//查找最近色块
float minDistToCell = 10;
float3 toClosestCell;
float3 closestCell;
[unroll]
for(int x1=-1; x1<=1; x1++){
[unroll]
for(int y1=-1; y1<=1; y1++){
[unroll]
for(int z1=-1; z1<=1; z1++){
float3 cell = baseCell + float3(x1, y1, z1);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
toClosestCell = toCell;
}
}
}
}

//查找最近色块边界
float minEdgeDistance = 10;
[unroll]
for(int x2=-1; x2<=1; x2++){
[unroll]
for(int y2=-1; y2<=1; y2++){
[unroll]
for(int z2=-1; z2<=1; z2++){
float3 cell = baseCell + float3(x2, y2, z2);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;

float3 diffToClosestCell = abs(closestCell - cell);
bool isClosestCell = diffToClosestCell.x + diffToClosestCell.y + diffToClosestCell.z < 0.1;
if(!isClosestCell){
float3 toCenter = (toClosestCell + toCell) * 0.5;
float3 cellDifference = normalize(toCell - toClosestCell);
float edgeDistance = dot(toCenter, cellDifference);
minEdgeDistance = min(minEdgeDistance, edgeDistance);
}
}
}
}

float random = rand3dTo1d(closestCell);
return float3(minDistToCell, random, minEdgeDistance);
}
1
2
3
4
5
6
7
8
9
10
void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = fwidth(value.z) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}

Scrolling noise

像前面介绍过的噪声一样,这里的噪声也不局限于使用空间坐标。我们可以将前面两个维度的输入值使用空间坐标,而第三个维度使用时间。这样随着时间的推移,我们可以看到噪声在不断发生变化。

1
2
3
4
5
6
7
8
9
10
11
void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
value.y += _Time.y * _TimeScale;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = fwidth(value.z) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}

Source

2d Voronoi

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/028_Voronoi_Noise/voronoi_noise_2d.shader

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
Shader "Tutorial/028_voronoi_noise/2d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_BorderColor ("Border Color", Color) = (0,0,0,1)
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;
float3 _BorderColor;

struct Input {
float3 worldPos;
};

float3 voronoiNoise(float2 value){
float2 baseCell = floor(value);

//查找最近色块
float minDistToCell = 10;
float2 toClosestCell;
float2 closestCell;
[unroll]
for(int x1=-1; x1<=1; x1++){
[unroll]
for(int y1=-1; y1<=1; y1++){
float2 cell = baseCell + float2(x1, y1);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
toClosestCell = toCell;
}
}
}

//查找最近色块边界
float minEdgeDistance = 10;
[unroll]
for(int x2=-1; x2<=1; x2++){
[unroll]
for(int y2=-1; y2<=1; y2++){
float2 cell = baseCell + float2(x2, y2);
float2 cellPosition = cell + rand2dTo2d(cell);
float2 toCell = cellPosition - value;

float2 diffToClosestCell = abs(closestCell - cell);
bool isClosestCell = diffToClosestCell.x + diffToClosestCell.y < 0.1;
if(!isClosestCell){
float2 toCenter = (toClosestCell + toCell) * 0.5;
float2 cellDifference = normalize(toCell - toClosestCell);
float edgeDistance = dot(toCenter, cellDifference);
minEdgeDistance = min(minEdgeDistance, edgeDistance);
}
}
}

float random = rand2dTo1d(closestCell);
return float3(minDistToCell, random, minEdgeDistance);
}

void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = length(fwidth(value)) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}
ENDCG
}
FallBack "Standard"
}

3d Voronoi

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/028_Voronoi_Noise/voronoi_noise_3d.shader

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
Shader "Tutorial/028_voronoi_noise/3d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_BorderColor ("Border Color", Color) = (0,0,0,1)
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;
float3 _BorderColor;

struct Input {
float3 worldPos;
};

float3 voronoiNoise(float3 value){
float3 baseCell = floor(value);

//查找最近色块
float minDistToCell = 10;
float3 toClosestCell;
float3 closestCell;
[unroll]
for(int x1=-1; x1<=1; x1++){
[unroll]
for(int y1=-1; y1<=1; y1++){
[unroll]
for(int z1=-1; z1<=1; z1++){
float3 cell = baseCell + float3(x1, y1, z1);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
toClosestCell = toCell;
}
}
}
}

//查找最近色块边界
float minEdgeDistance = 10;
[unroll]
for(int x2=-1; x2<=1; x2++){
[unroll]
for(int y2=-1; y2<=1; y2++){
[unroll]
for(int z2=-1; z2<=1; z2++){
float3 cell = baseCell + float3(x2, y2, z2);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;

float3 diffToClosestCell = abs(closestCell - cell);
bool isClosestCell = diffToClosestCell.x + diffToClosestCell.y + diffToClosestCell.z < 0.1;
if(!isClosestCell){
float3 toCenter = (toClosestCell + toCell) * 0.5;
float3 cellDifference = normalize(toCell - toClosestCell);
float edgeDistance = dot(toCenter, cellDifference);
minEdgeDistance = min(minEdgeDistance, edgeDistance);
}
}
}
}

float random = rand3dTo1d(closestCell);
return float3(minDistToCell, random, minEdgeDistance);
}

void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = fwidth(value.z) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}
ENDCG
}
FallBack "Standard"
}

Scrolling Voronoi

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/028_Voronoi_Noise/voronoi_noise_scrolling.shader

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
Shader "Tutorial/028_voronoi_noise/scrolling" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_BorderColor ("Border Color", Color) = (0,0,0,1)
_TimeScale ("Scrolling Speed", Range(0, 2)) = 1
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

float _CellSize;
float _TimeScale;
float3 _BorderColor;

struct Input {
float3 worldPos;
};

float3 voronoiNoise(float3 value){
float3 baseCell = floor(value);

//查找最近色块
float minDistToCell = 10;
float3 toClosestCell;
float3 closestCell;
[unroll]
for(int x1=-1; x1<=1; x1++){
[unroll]
for(int y1=-1; y1<=1; y1++){
[unroll]
for(int z1=-1; z1<=1; z1++){
float3 cell = baseCell + float3(x1, y1, z1);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;
float distToCell = length(toCell);
if(distToCell < minDistToCell){
minDistToCell = distToCell;
closestCell = cell;
toClosestCell = toCell;
}
}
}
}

//查找最近色块边界
float minEdgeDistance = 10;
[unroll]
for(int x2=-1; x2<=1; x2++){
[unroll]
for(int y2=-1; y2<=1; y2++){
[unroll]
for(int z2=-1; z2<=1; z2++){
float3 cell = baseCell + float3(x2, y2, z2);
float3 cellPosition = cell + rand3dTo3d(cell);
float3 toCell = cellPosition - value;

float3 diffToClosestCell = abs(closestCell - cell);
bool isClosestCell = diffToClosestCell.x + diffToClosestCell.y + diffToClosestCell.z < 0.1;
if(!isClosestCell){
float3 toCenter = (toClosestCell + toCell) * 0.5;
float3 cellDifference = normalize(toCell - toClosestCell);
float edgeDistance = dot(toCenter, cellDifference);
minEdgeDistance = min(minEdgeDistance, edgeDistance);
}
}
}
}

float random = rand3dTo1d(closestCell);
return float3(minDistToCell, random, minEdgeDistance);
}

void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos.xyz / _CellSize;
value.y += _Time.y * _TimeScale;
float3 noise = voronoiNoise(value);

float3 cellColor = rand1dTo3d(noise.y);
float valueChange = fwidth(value.z) * 0.5;
float isBorder = 1 - smoothstep(0.05 - valueChange, 0.05 + valueChange, noise.z);
float3 color = lerp(cellColor, _BorderColor, isBorder);
o.Albedo = color;
}
ENDCG
}
FallBack "Standard"
}

希望本文有助你理解什么是维诺噪声。

希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!

原文:
Layered Noise

Layered Noise

目前为止,我们创建的噪声要么非常光滑,要么过于随机。我们可以将它们结合起来,从而实现一种多层次的噪声图。这样我们可以得到既平滑又细节丰富的噪声图。我们前面介绍过的值类噪声和泊林噪声都可以应用到本章的叠加噪声中。虽然叠加噪声产生的效果可能更符合你的预期,但是因为叠加了多个噪声图,所以其性能消耗也是叠加的。

Layered 1d Noise

在前面的教程中,我们实现的噪声图可以通过控制色块的大小,进而控制噪声的频率。

和之前一样,我们首先从一维开始做起。不过我们需要采集两次噪声,一次是和原来一样,一次是乘以2。乘以2的操作实际上就是提高它的频率。然后我们将高频噪声的值降低,高频低强度的噪声可以用来表示细节,然后将其叠加到低频噪声上。

为了便于大家理解,这里我还是照常贴出相关源码。

1
2
3
4
5
6
float sampleLayeredNoise(float value){
float noise = gradientNoise(value);
float highFreqNoise = gradientNoise(value * 6);
noise = noise + highFreqNoise * 0.2;
return noise;
}
1
2
3
4
5
6
7
8
9
void surf (Input i, inout SurfaceOutputStandard o) {
float value = i.worldPos.x / _CellSize;
float noise = sampleLayeredNoise(value);

float dist = abs(noise - i.worldPos.y);
float pixelHeight = fwidth(i.worldPos.y);
float lineIntensity = smoothstep(2*pixelHeight, pixelHeight, dist);
o.Albedo = lerp(1, 0, lineIntensity);
}

上面简单的叠加已经能达到一些效果了,一次叠加获得比较粗糙的细节。不过我们还可以继续叠加。我们将叠加的层数称为阶,所以我们现在是二阶叠加噪声。

为了实现高阶叠加噪声,我们将使用循环语句。同时我们的阶数也可以使用宏命令来设置为常量,这样可以在材质面板上调节。当然我们也可以将阶数完全用变量来表示,但是使用变量控制的循环语句,是没办法进行展开优化的,所以应该尽量避免。这里每个层的频率变量乘以上一个层的实际频率,得到当前层的实际频率。我们叫这个频率变量为粗糙度,或者说相对于上一层的粗糙度。我们每一层之间的相对粗糙度取同一个值,所以所有层的相对粗糙度都为_persistance。然后还有一个从整体上控制粗糙度的量_Routhness

1
2
3
4
5
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
}
1
2
3
4
5
6
//公共变量
#define OCTAVES 4

float _CellSize;
float _Roughness;
float _Persistance;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
float sampleLayeredNoise(float value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + gradientNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

Layered multidimensional Noise

多维噪声图的实现方式类似,只不过相应的噪声函数输入参数要改为对应维度的向量。这里我们以二维为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float sampleLayeredNoise(float2 value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + perlinNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

下面是三维。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float sampleLayeredNoise(float3 value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + perlinNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

Special Use Case

另一个经常用到噪声的场景是高度图。通常我们处理纹理是在片段着色器中,但是当处理高度图时,我们是在顶点着色器中采样的,然后将采样结果叠加到顶点的y坐标。关于这部分的内容,你可以参考之前关于顶点偏移的介绍。

首先,我们修改表面着色器的宏命令部分。添加顶点处理函数,#pragma surface surf Standard fullforwardshadows vertex:vert addshadow。然后实现顶点处理函数,其中将采样噪声叠加到顶点的y坐标,然后计算顶点世界坐标。然后在表面着色器中,将光照参数albedo设置为白色。如果我们想让其表现的更细致,应该使用分辨率更高的网格数据,否者的话看到的将是很明显的多边形网格。

1
2
3
4
5
6
Properties {
_CellSize ("Cell Size", Range(0, 10)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
_Amplitude("Amplitude", Range(0, 10)) = 1
}
1
2
//噪声强度
float _Amplitude;
1
2
3
4
5
6
7
void vert(inout appdata_full data){
float4 worldPos = mul(unity_ObjectToWorld, data.vertex);
float3 value = worldPos / _CellSize;
//将噪声值映射到0-1区间
float noise = sampleLayeredNoise(value) + 0.5;
data.vertex.y += noise * _Amplitude;
}
1
2
3
void surf (Input i, inout SurfaceOutputStandard o) {
o.Albedo = 1;
}

到目前为止,虽然我们修改了顶点位置,但是在阴影处理的时候还是被当做平面来看。我们可以让其根据新的顶点位置重新计算阴影,这一点在顶点偏移中有介绍。

在这里我遇到一个问题,就是传入的顶点齐次坐标的w值并不为1,也就是说该坐标是缩放后的坐标。因此我们需要执行齐次除法。

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
void vert(inout appdata_full data){
//执行齐次除法,得到真实的顶点坐标
float3 localPos = data.vertex / data.vertex.w;

//计算新的顶点坐标
float3 modifiedPos = localPos;
float2 basePosValue = mul(unity_ObjectToWorld, modifiedPos).xz / _CellSize;
float basePosNoise = sampleLayeredNoise(basePosValue) + 0.5;
modifiedPos.y += basePosNoise * _Amplitude;

//计算新坐标的切向量方向的临近点
float3 posPlusTangent = localPos + data.tangent * 0.02;
float2 tangentPosValue = mul(unity_ObjectToWorld, posPlusTangent).xz / _CellSize;
float tangentPosNoise = sampleLayeredNoise(tangentPosValue) + 0.5;
posPlusTangent.y += tangentPosNoise * _Amplitude;

//计算新坐标的 bitangent方向的临近点
float3 bitangent = cross(data.normal, data.tangent);
float3 posPlusBitangent = localPos + bitangent * 0.02;
float2 bitangentPosValue = mul(unity_ObjectToWorld, posPlusBitangent).xz / _CellSize;
float bitangentPosNoise = sampleLayeredNoise(bitangentPosValue) + 0.5;
posPlusBitangent.y += bitangentPosNoise * _Amplitude;

//计算切向量和bitangent
float3 modifiedTangent = posPlusTangent - modifiedPos;
float3 modifiedBitangent = posPlusBitangent - modifiedPos;

//计算新的法向
float3 modifiedNormal = cross(modifiedTangent, modifiedBitangent);
data.normal = normalize(modifiedNormal);
data.vertex = float4(modifiedPos.xyz, 1);
}

当然,我们也可以引入时间变量,从而达到滚动动画效果。

1
2
//材质属性
_ScrollDirection("Scroll Direction", Vector) = (0, 1)
1
2
//公共变量,滚动方向
float2 _ScrollDirection;
1
2
//计算水平位置
float2 basePosValue = mul(unity_ObjectToWorld, modifiedPos).xz / _CellSize + _ScrollDirection * _Time.y;
1
2
//计算切向临近坐标
float2 tangentPosValue = mul(unity_ObjectToWorld, posPlusTangent).xz / _CellSize + _ScrollDirection * _Time.y;
1
2
//计算bitangent方向临近坐标
float2 bitangentPosValue = mul(unity_ObjectToWorld, posPlusBitangent).xz / _CellSize + _ScrollDirection * _Time.y;

Source

1d layered noise

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/027_Layered_Noise/layered_perlin_noise_1d.shader

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
Shader "Tutorial/027_layered_noise/1d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

//公共变量
#define OCTAVES 4

float _CellSize;
float _Roughness;
float _Persistance;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator * interpolator * interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float gradientNoise(float value){
float fraction = frac(value);
float interpolator = easeInOut(fraction);

float previousCellInclination = rand1dTo1d(floor(value)) * 2 - 1;
float previousCellLinePoint = previousCellInclination * fraction;

float nextCellInclination = rand1dTo1d(ceil(value)) * 2 - 1;
float nextCellLinePoint = nextCellInclination * (fraction - 1);

return lerp(previousCellLinePoint, nextCellLinePoint, interpolator);
}

float sampleLayeredNoise(float value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + gradientNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float value = i.worldPos.x / _CellSize;
float noise = sampleLayeredNoise(value);

float dist = abs(noise - i.worldPos.y);
float pixelHeight = fwidth(i.worldPos.y);
float lineIntensity = smoothstep(2*pixelHeight, pixelHeight, dist);
o.Albedo = lerp(1, 0, lineIntensity);
}
ENDCG
}
FallBack "Standard"
}

2d layered noise

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/027_Layered_Noise/layered_perlin_noise_2d.shader

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
Shader "Tutorial/027_layered_noise/2d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

//公共变量
#define OCTAVES 4

float _CellSize;
float _Roughness;
float _Persistance;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float perlinNoise(float2 value){
//计算色块的四个顶点
float2 lowerLeftDirection = rand2dTo2d(float2(floor(value.x), floor(value.y))) * 2 - 1;
float2 lowerRightDirection = rand2dTo2d(float2(ceil(value.x), floor(value.y))) * 2 - 1;
float2 upperLeftDirection = rand2dTo2d(float2(floor(value.x), ceil(value.y))) * 2 - 1;
float2 upperRightDirection = rand2dTo2d(float2(ceil(value.x), ceil(value.y))) * 2 - 1;

float2 fraction = frac(value);

//计算四个顶点在当前位置的贡献
float lowerLeftFunctionValue = dot(lowerLeftDirection, fraction - float2(0, 0));
float lowerRightFunctionValue = dot(lowerRightDirection, fraction - float2(1, 0));
float upperLeftFunctionValue = dot(upperLeftDirection, fraction - float2(0, 1));
float upperRightFunctionValue = dot(upperRightDirection, fraction - float2(1, 1));

float interpolatorX = easeInOut(fraction.x);
float interpolatorY = easeInOut(fraction.y);

//二次插值
float lowerCells = lerp(lowerLeftFunctionValue, lowerRightFunctionValue, interpolatorX);
float upperCells = lerp(upperLeftFunctionValue, upperRightFunctionValue, interpolatorX);

float noise = lerp(lowerCells, upperCells, interpolatorY);
return noise;
}

float sampleLayeredNoise(float2 value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + perlinNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float2 value = i.worldPos.xz / _CellSize;
//将噪声值映射到0-1区间
float noise = sampleLayeredNoise(value) + 0.5;

o.Albedo = noise;
}
ENDCG
}
FallBack "Standard"
}

3d layered noise

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/027_Layered_Noise/layered_perlin_noise_3d.shader

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
Shader "Tutorial/027_layered_noise/3d" {
Properties {
_CellSize ("Cell Size", Range(0, 2)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows
#pragma target 3.0

#include "Random.cginc"

//公共变量
#define OCTAVES 4

float _CellSize;
float _Roughness;
float _Persistance;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float perlinNoise(float3 value){
float3 fraction = frac(value);

float interpolatorX = easeInOut(fraction.x);
float interpolatorY = easeInOut(fraction.y);
float interpolatorZ = easeInOut(fraction.z);

float cellNoiseZ[2];
[unroll]
for(int z=0;z<=1;z++){
float cellNoiseY[2];
[unroll]
for(int y=0;y<=1;y++){
float cellNoiseX[2];
[unroll]
for(int x=0;x<=1;x++){
float3 cell = floor(value) + float3(x, y, z);
float3 cellDirection = rand3dTo3d(cell) * 2 - 1;
float3 compareVector = fraction - float3(x, y, z);
cellNoiseX[x] = dot(cellDirection, compareVector);
}
cellNoiseY[y] = lerp(cellNoiseX[0], cellNoiseX[1], interpolatorX);
}
cellNoiseZ[z] = lerp(cellNoiseY[0], cellNoiseY[1], interpolatorY);
}
float noise = lerp(cellNoiseZ[0], cellNoiseZ[1], interpolatorZ);
return noise;
}

float sampleLayeredNoise(float3 value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + perlinNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

void surf (Input i, inout SurfaceOutputStandard o) {
float3 value = i.worldPos / _CellSize;
//将噪声值映射到0-1区间
float noise = sampleLayeredNoise(value) + 0.5;

o.Albedo = noise;
}
ENDCG
}
FallBack "Standard"
}

Scrolling height noise

https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/027_Layered_Noise/layered_noise_special.shader

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Shader "Tutorial/027_layered_noise/special_use_case" {
Properties {
_CellSize ("Cell Size", Range(0, 16)) = 2
_Roughness ("Roughness", Range(1, 8)) = 3
_Persistance ("Persistance", Range(0, 1)) = 0.4
_Amplitude("Amplitude", Range(0, 10)) = 1
_ScrollDirection("Scroll Direction", Vector) = (0, 1, 0, 0)
}
SubShader {
Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

CGPROGRAM

#pragma surface surf Standard fullforwardshadows vertex:vert addshadow
#pragma target 3.0

#include "Random.cginc"

//global shader variables
#define OCTAVES 4

float _CellSize;
float _Roughness;
float _Persistance;
float _Amplitude;

float2 _ScrollDirection;

struct Input {
float3 worldPos;
};

float easeIn(float interpolator){
return interpolator * interpolator;
}

float easeOut(float interpolator){
return 1 - easeIn(1 - interpolator);
}

float easeInOut(float interpolator){
float easeInValue = easeIn(interpolator);
float easeOutValue = easeOut(interpolator);
return lerp(easeInValue, easeOutValue, interpolator);
}

float perlinNoise(float2 value){
//计算色块的四个顶点
float2 lowerLeftDirection = rand2dTo2d(float2(floor(value.x), floor(value.y))) * 2 - 1;
float2 lowerRightDirection = rand2dTo2d(float2(ceil(value.x), floor(value.y))) * 2 - 1;
float2 upperLeftDirection = rand2dTo2d(float2(floor(value.x), ceil(value.y))) * 2 - 1;
float2 upperRightDirection = rand2dTo2d(float2(ceil(value.x), ceil(value.y))) * 2 - 1;

float2 fraction = frac(value);

//计算四个顶点在当前位置的贡献
float lowerLeftFunctionValue = dot(lowerLeftDirection, fraction - float2(0, 0));
float lowerRightFunctionValue = dot(lowerRightDirection, fraction - float2(1, 0));
float upperLeftFunctionValue = dot(upperLeftDirection, fraction - float2(0, 1));
float upperRightFunctionValue = dot(upperRightDirection, fraction - float2(1, 1));

float interpolatorX = easeInOut(fraction.x);
float interpolatorY = easeInOut(fraction.y);

//二次插值
float lowerCells = lerp(lowerLeftFunctionValue, lowerRightFunctionValue, interpolatorX);
float upperCells = lerp(upperLeftFunctionValue, upperRightFunctionValue, interpolatorX);

float noise = lerp(lowerCells, upperCells, interpolatorY);
return noise;
}

float sampleLayeredNoise(float2 value){
float noise = 0;
float frequency = 1;
float factor = 1;

[unroll]
for(int i=0; i<OCTAVES; i++){
noise = noise + perlinNoise(value * frequency + i * 0.72354) * factor;
factor *= _Persistance;
frequency *= _Roughness;
}

return noise;
}

void vert(inout appdata_full data){
//执行齐次除法,得到真实的顶点坐标
float3 localPos = data.vertex / data.vertex.w;

//计算新的顶点坐标
float3 modifiedPos = localPos;
float2 basePosValue = mul(unity_ObjectToWorld, modifiedPos).xz / _CellSize;
float basePosNoise = sampleLayeredNoise(basePosValue) + 0.5;
modifiedPos.y += basePosNoise * _Amplitude;

//计算新坐标的切向量方向的临近点
float3 posPlusTangent = localPos + data.tangent * 0.02;
float2 tangentPosValue = mul(unity_ObjectToWorld, posPlusTangent).xz / _CellSize;
float tangentPosNoise = sampleLayeredNoise(tangentPosValue) + 0.5;
posPlusTangent.y += tangentPosNoise * _Amplitude;

//计算新坐标的 bitangent方向的临近点
float3 bitangent = cross(data.normal, data.tangent);
float3 posPlusBitangent = localPos + bitangent * 0.02;
float2 bitangentPosValue = mul(unity_ObjectToWorld, posPlusBitangent).xz / _CellSize;
float bitangentPosNoise = sampleLayeredNoise(bitangentPosValue) + 0.5;
posPlusBitangent.y += bitangentPosNoise * _Amplitude;

//计算切向量和bitangent
float3 modifiedTangent = posPlusTangent - modifiedPos;
float3 modifiedBitangent = posPlusBitangent - modifiedPos;

//计算新的法向
float3 modifiedNormal = cross(modifiedTangent, modifiedBitangent);
data.normal = normalize(modifiedNormal);
data.vertex = float4(modifiedPos.xyz, 1);
}

void surf (Input i, inout SurfaceOutputStandard o) {
o.Albedo = 1;
}
ENDCG
}
FallBack "Standard"
}

高阶叠加噪声可以很好的模拟复杂的噪声图案。你也可以试着将不同类型的噪声进行叠加,然后看看会产生什么现象。总之,我希望你们能够理解其中的基本原理,然后肉会贯通。如果你发现有任何理解不了的地方,可以给我留言。

希望你能喜欢这个教程哦!如果你想支持我,可以关注我的推特,或者通过ko-fi、或patreon给两小钱。总之,各位大爷,走过路过不要错过,有钱的捧个钱场,没钱的捧个人场:-)!!!