├── .gitignore
├── README.md
├── SDF_Generator.cs
└── images
└── Demo
├── face_01.png
├── face_02.png
├── face_03.png
├── face_04.png
├── face_05.png
├── face_06.png
├── face_07.png
├── face_08.png
├── face_OUT.png
└── face_main.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Unity SDF Generator
2 |
3 |
4 |
5 | > Unity SDF generator and SDFs smooth
6 |
7 | Demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | How to use
16 |
17 | Drag `SDF_Generator.cs` anywhere you want.
18 |
19 | And then you can find it here: ***Windows > SDF Generator***
20 |
21 | - Drag all the textures to `Sources`. Remember to set the textures to `Read/Write`.
22 | - If `samples` is `0`, you will get a completely smooth output, otherwise the output is posterized according to the `samples`.
23 |
24 | - Texture with smaller white area should be on top.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | - I drew some pictures for this tool, if you are interested, you can find them here: `images/Demo/`
33 |
34 | Try it and have fun !
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | ### Todo
43 |
44 | - [ ] Compute shader based version
45 | - [ ] Shader based version
46 | - [x] Add editor
47 | > ⚠️ RWTexture2D or RWTexture2DArray doesn't work in Editor Mode ?
48 |
49 | Ref:
50 |
51 | >http://www.codersnotes.com/notes/signed-distance-fields/
52 | >
53 | >https://shaderfun.com
54 | >
55 | >https://zhuanlan.zhihu.com/p/337944099
56 |
--------------------------------------------------------------------------------
/SDF_Generator.cs:
--------------------------------------------------------------------------------
1 | using UnityEditor;
2 | using UnityEngine;
3 |
4 | ///
5 | /// Ref:
6 | /// 1. http://www.codersnotes.com/notes/signed-distance-fields/
7 | /// 2. https://shaderfun.com
8 | /// 3. https://zhuanlan.zhihu.com/p/337944099
9 | ///
10 |
11 | public class SDF_Generator : EditorWindow
12 | {
13 | Vector2 scrollPosition;
14 |
15 | struct Pixel
16 | {
17 | public float distance;
18 | public bool edge;
19 | }
20 | int m_x_dims;
21 | int m_y_dims;
22 | Pixel[] m_pixels;
23 | int samples;
24 |
25 | //
26 | public int targetSize = 256;
27 | int sampleTimes = 0;
28 | string outputName;
29 | [SerializeField]Texture2D[] Sources;
30 | SerializedProperty m_Sources;
31 | SerializedObject m_object;
32 | //
33 |
34 |
35 | [MenuItem("Window/SDF Generator")]
36 | public static void ShowWindow ()
37 | {
38 | EditorWindow.GetWindow (typeof(SDF_Generator));
39 | }
40 |
41 | private void OnEnable() {
42 | ScriptableObject target = this;
43 | m_object = new SerializedObject(target);
44 |
45 | }
46 | void OnGUI ()
47 | {
48 | m_object.Update();
49 | m_Sources = m_object.FindProperty("Sources");
50 |
51 | scrollPosition = GUILayout.BeginScrollView(scrollPosition,GUILayout.Width(0),GUILayout.Height(0));
52 |
53 | GUILayout.Space (20);
54 | //Body
55 | outputName = EditorGUILayout.TextField("Output Name", outputName);
56 | targetSize = EditorGUILayout.IntField("Target Size", targetSize);
57 | // samples - posterize
58 |
59 | GUI.color = Color.cyan;
60 | sampleTimes = EditorGUILayout.IntField(sampleTimes>0?"Samples (Posterize)":"Samples (Smooth)", sampleTimes<0? 0:sampleTimes);
61 |
62 |
63 | GUILayout.Space (10);
64 | //Explain
65 | GUI.color = Color.yellow;
66 |
67 | EditorGUILayout.LabelField("● Texture with smaller white area should be on top.", EditorStyles.helpBox);
68 | GUI.color = Color.white;
69 | //input textures
70 | EditorGUILayout.PropertyField(m_Sources, true);
71 | m_object.ApplyModifiedPropertiesWithoutUndo();
72 | GUILayout.Space (20);
73 |
74 | if (GUILayout.Button ("Generate!"))
75 | Generate();
76 | GUILayout.Space (30);
77 |
78 | GUILayout.EndScrollView();
79 | }
80 |
81 | public void Generate()
82 | {
83 | SaveTexture(Generator(Sources));
84 | }
85 |
86 | void LoadFromTexture(Texture2D texture)
87 | {
88 | Color[] texpixels = texture.GetPixels();
89 | m_x_dims = texture.width;
90 | m_y_dims = texture.height;
91 | m_pixels = new Pixel[m_x_dims * m_y_dims];
92 | for (int i = 0; i < m_pixels.Length; i++)
93 | {
94 | if (texpixels[i].r > 0.5f)
95 | m_pixels[i].distance = -99999f;
96 | else
97 | m_pixels[i].distance = 99999f;
98 | }
99 | }
100 |
101 | void BuildSweepGrids(out float[] outside_grid, out float[] inside_grid)
102 | {
103 | outside_grid = new float[m_pixels.Length];
104 | inside_grid = new float[m_pixels.Length];
105 | for (int i = 0; i < m_pixels.Length; i++)
106 | {
107 | if (m_pixels[i].distance < 0)
108 | {
109 | //inside pixel. outer distance is set to 0, inner distance
110 | //is preserved (albeit negated to make it positive)
111 | outside_grid[i] = 0f;
112 | inside_grid[i] = -m_pixels[i].distance;
113 | }
114 | else
115 | {
116 | //outside pixel. inner distance is set to 0,
117 | //outer distance is preserved
118 | inside_grid[i] = 0f;
119 | outside_grid[i] = m_pixels[i].distance;
120 | }
121 | }
122 | }
123 |
124 | Texture2D Generator(Texture2D[] source)
125 | {
126 | Texture2D dest = new Texture2D(targetSize, targetSize);
127 | if(source.Length== 0) return dest;
128 |
129 | var pixels_out = new Color[targetSize*targetSize];
130 |
131 | var dists_temp = new float[source.Length, targetSize*targetSize];
132 |
133 | // Calculate all the textures and store the results for further calculation
134 | for(int count = 0; count < source.Length; count++)
135 | {
136 | samples = (int)sampleTimes/source.Length;
137 | LoadFromTexture(source[count]);
138 | ClearAndMarkNoneEdgePixels();
139 | float[] outside_grid, inside_grid;
140 | BuildSweepGrids(out outside_grid, out inside_grid);
141 |
142 | //run the 8PSSEDT sweep on each grid
143 | SweepGrid(outside_grid);
144 | SweepGrid(inside_grid);
145 |
146 | int scaleX = source[count].width / targetSize;
147 | int scaleY = source[count].height / targetSize;
148 |
149 | for (int y = 0; y < targetSize; y++)
150 | {
151 | for (int x = 0; x < targetSize; x++)
152 | {
153 | int i = y * targetSize + x;
154 | float dist1 = inside_grid[y * scaleY * m_x_dims + x * scaleX];
155 | float dist2 = outside_grid[y * scaleY * m_x_dims + x * scaleX];
156 | var dist = Mathf.Clamp(128f + (dist1 - dist2), 0f, 255f);
157 | dist = Mathf.InverseLerp(0f, 255f, dist);
158 | dists_temp[count, i] = dist;
159 |
160 | // One SDF without smooth
161 | if (source.Length == 1) pixels_out[i] = new Color(dist, dist, dist, 1f);
162 | }
163 | }
164 | }
165 |
166 | // Smooth using SdF_Smooth function
167 | float[] dists_intrp = new float[targetSize*targetSize];
168 | if (source.Length >= 2)
169 | {
170 | float gap = 1f/(source.Length - 1);
171 | for(int count = 0; count < source.Length - 1; count++)
172 | {
173 | for (int y = 0; y < targetSize; y++)
174 | {
175 | for (int x = 0; x < targetSize; x++)
176 | {
177 | int i = y * targetSize + x;
178 | dists_intrp[i] += (float)SDF_Smooth(dists_temp[count, i], dists_temp[count+1, i], new Vector2(1f - (count+1)*gap , 1f - count*gap));
179 | }
180 | }
181 | }
182 |
183 | for (int y = 0; y < targetSize; y++)
184 | {
185 | for (int x = 0; x < targetSize; x++)
186 | {
187 | int i = y * targetSize + x;
188 | pixels_out[i] = new Color(dists_intrp[i], dists_intrp[i], dists_intrp[i], 1f);
189 | }
190 | }
191 | }
192 |
193 | dest.SetPixels(pixels_out);
194 | dest.Apply();
195 |
196 | return dest;
197 | }
198 |
199 | //compare a pixel for the sweep, and updates it with a new distance if necessary
200 | void Compare(float[] grid, int x, int y, int xoffset, int yoffset)
201 | {
202 | //calculate the location of the other pixel, and bail if in valid
203 | int otherx = x + xoffset;
204 | int othery = y + yoffset;
205 | if (otherx < 0 || othery < 0 || otherx >= m_x_dims || othery >= m_y_dims)
206 | return;
207 |
208 | //read the distance values stored in both this and the other pixel
209 | float curr_dist = grid[y * m_x_dims + x];
210 | float other_dist = grid[othery * m_x_dims + otherx];
211 |
212 | //calculate a potential new distance, using the one stored in the other pixel,
213 | //PLUS the distance to the other pixel
214 | float new_dist = other_dist + Mathf.Sqrt(xoffset * xoffset + yoffset * yoffset);
215 |
216 | //if the potential new distance is better than our current one, update!
217 | if (new_dist < curr_dist)
218 | grid[y * m_x_dims + x] = new_dist;
219 | }
220 |
221 | void SweepGrid(float[] grid)
222 | {
223 | // Pass 0
224 | //loop over rows from top to bottom
225 | for (int y = 0; y < m_y_dims; y++)
226 | {
227 | //loop over pixels from left to right
228 | for (int x = 0; x < m_x_dims; x++)
229 | {
230 | Compare(grid, x, y, -1, 0);
231 | Compare(grid, x, y, 0, -1);
232 | Compare(grid, x, y, -1, -1);
233 | Compare(grid, x, y, 1, -1);
234 | }
235 |
236 | //loop over pixels from right to left
237 | for (int x = m_x_dims - 1; x >= 0; x--)
238 | {
239 | Compare(grid, x, y, 1, 0);
240 | }
241 | }
242 |
243 | // Pass 1
244 | //loop over rows from bottom to top
245 | for (int y = m_y_dims - 1; y >= 0; y--)
246 | {
247 | //loop over pixels from right to left
248 | for (int x = m_x_dims - 1; x >= 0; x--)
249 | {
250 | Compare(grid, x, y, 1, 0);
251 | Compare(grid, x, y, 0, 1);
252 | Compare(grid, x, y, -1, 1);
253 | Compare(grid, x, y, 1, 1);
254 | }
255 |
256 | //loop over pixels from left to right
257 | for (int x = 0; x < m_x_dims; x++)
258 | {
259 | Compare(grid, x, y, -1, 0);
260 | }
261 | }
262 | }
263 |
264 |
265 | Pixel GetPixel(int x, int y)
266 | {
267 | return m_pixels[y * m_x_dims + x];
268 | }
269 | void SetPixel(int x, int y, Pixel p)
270 | {
271 | m_pixels[y * m_x_dims + x] = p;
272 | }
273 |
274 | bool IsOuterPixel(int pix_x, int pix_y)
275 | {
276 | if (pix_x < 0 || pix_y < 0 || pix_x >= m_x_dims || pix_y >= m_y_dims)
277 | return true;
278 | else
279 | return GetPixel(pix_x, pix_y).distance >= 0;
280 | }
281 |
282 | bool IsEdgePixel(int pix_x, int pix_y)
283 | {
284 | bool is_outer = IsOuterPixel(pix_x, pix_y);
285 | if (is_outer != IsOuterPixel(pix_x - 1, pix_y - 1)) return true; //[-1,-1]
286 | if (is_outer != IsOuterPixel(pix_x, pix_y - 1)) return true; //[ 0,-1]
287 | if (is_outer != IsOuterPixel(pix_x + 1, pix_y - 1)) return true; //[+1,-1]
288 | if (is_outer != IsOuterPixel(pix_x - 1, pix_y)) return true; //[-1, 0]
289 | if (is_outer != IsOuterPixel(pix_x + 1, pix_y)) return true; //[+1, 0]
290 | if (is_outer != IsOuterPixel(pix_x - 1, pix_y + 1)) return true; //[-1,+1]
291 | if (is_outer != IsOuterPixel(pix_x, pix_y + 1)) return true; //[ 0,+1]
292 | if (is_outer != IsOuterPixel(pix_x + 1, pix_y + 1)) return true; //[+1,+1]
293 | return false;
294 | }
295 |
296 | public void ClearAndMarkNoneEdgePixels()
297 | {
298 | for (int y = 0; y < m_y_dims; y++)
299 | {
300 | for (int x = 0; x < m_y_dims; x++)
301 | {
302 | Pixel pix = GetPixel(x, y);
303 | pix.edge = IsEdgePixel(x, y); //mark edge pixels
304 | if (!pix.edge)
305 | pix.distance = pix.distance > 0 ? 99999f : -99999f;
306 | SetPixel(x, y, pix);
307 | }
308 | }
309 | }
310 |
311 | float SDF_Smooth(float sdfA,float sdfB, Vector2 sdfEdge)
312 | {
313 | // Smooth sdf images and define threshold
314 | if (sdfA < .5f && sdfB < .5f)
315 | {
316 | return 0f;
317 | }
318 | if (sdfA > .5f && sdfB > .5f)
319 | {
320 | if(sdfEdge.y == 1f) return 1f;
321 | return 0f;
322 | }
323 |
324 | float result = 0;
325 | if (samples > 0)
326 | {
327 | for (int i = 0; i < samples; i++)
328 | {
329 | float t = (float)i/samples; // (float) is needed
330 | result += Mathf.Lerp(sdfA, sdfB, t) < .5f? sdfEdge.x : sdfEdge.y;
331 | }
332 | return result / samples;
333 | }
334 |
335 | float ratio = (sdfA - 0.5f) / (sdfA - sdfB);
336 | if (sdfA > sdfB)
337 | {
338 | result = sdfEdge.y * ratio + sdfEdge.x * (1f - ratio);
339 | }
340 | else
341 | {
342 | result = sdfEdge.x * ratio + sdfEdge.y * (1f - ratio);
343 | }
344 | return result;
345 | }
346 | void SaveTexture(Texture2D texture)
347 | {
348 | byte[] bytes = texture.EncodeToPNG();
349 | var dirPath = Application.dataPath.Replace("Assets", "") + AssetDatabase.GetAssetPath(Sources[0]).Replace(Sources[0].name, outputName);
350 | if (outputName == null || outputName == "")
351 | {
352 | //if the output name is blank, fill in the name based on the original texture
353 | dirPath = Application.dataPath.Replace("Assets", "") + AssetDatabase.GetAssetPath(Sources[0]).Replace(Sources[0].name, Sources[0].name + "_OUT");
354 | }
355 | System.IO.File.WriteAllBytes(dirPath, bytes);
356 | Debug.Log(bytes.Length / 1024 + "Kb was saved as: " + dirPath);
357 | #if UNITY_EDITOR
358 | UnityEditor.AssetDatabase.Refresh();
359 | #endif
360 | }
361 |
362 | }
363 |
--------------------------------------------------------------------------------
/images/Demo/face_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_01.png
--------------------------------------------------------------------------------
/images/Demo/face_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_02.png
--------------------------------------------------------------------------------
/images/Demo/face_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_03.png
--------------------------------------------------------------------------------
/images/Demo/face_04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_04.png
--------------------------------------------------------------------------------
/images/Demo/face_05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_05.png
--------------------------------------------------------------------------------
/images/Demo/face_06.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_06.png
--------------------------------------------------------------------------------
/images/Demo/face_07.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_07.png
--------------------------------------------------------------------------------
/images/Demo/face_08.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_08.png
--------------------------------------------------------------------------------
/images/Demo/face_OUT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_OUT.png
--------------------------------------------------------------------------------
/images/Demo/face_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xudxud/Unity-SDF-Generator/e12b3ab29beeab77be3d085e74272e39e414bb06/images/Demo/face_main.png
--------------------------------------------------------------------------------