├── .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 --------------------------------------------------------------------------------