yum/3ner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum/3ner

yumAdd faster 3-in 1-out hasher for domain warping1784064

master
10.1 KiB250 linesraw
1using UnityEngine;
2using UnityEditor;
3using System.Threading.Tasks;
4
5public class GenerateNoise : EditorWindow
6{
7    enum NoiseType { WhiteNoise, WorleyNoise, WorleyEdgeSDF }
8    enum DistanceMetric { L1, L2, LInfinity }
9
10    NoiseType _type = NoiseType.WhiteNoise;
11    int _resolution = 32;
12    int _texelsPerCell = 8;
13    DistanceMetric _metric = DistanceMetric.L2;
14    float _sdfRadius = 0.5f;
15
16    [MenuItem("Tools/yum_food/Generate 3D Noise")]
17    static void Open() => GetWindow<GenerateNoise>("Generate 3D Noise");
18
19    void OnGUI()
20    {
21        EditorGUILayout.LabelField("Noise Settings", EditorStyles.boldLabel);
22        _type = (NoiseType)EditorGUILayout.EnumPopup("Type", _type);
23        if (_type == NoiseType.WhiteNoise)
24        {
25            _resolution = EditorGUILayout.IntField("Resolution", _resolution);
26            _resolution = Mathf.Clamp(_resolution, 2, 512);
27        }
28        else
29        {
30            _resolution = EditorGUILayout.IntField("Cell Count", _resolution);
31            _resolution = Mathf.Clamp(_resolution, 2, 512);
32            _texelsPerCell = EditorGUILayout.IntSlider("Texels Per Cell", _texelsPerCell, 2, 64);
33            _metric = (DistanceMetric)EditorGUILayout.EnumPopup("Distance Metric", _metric);
34            if (_type == NoiseType.WorleyEdgeSDF)
35                _sdfRadius = EditorGUILayout.Slider("SDF Radius (cells)", _sdfRadius, 0.1f, 2f);
36            EditorGUILayout.HelpBox($"Texture resolution: {_resolution * _texelsPerCell}\u00b3", MessageType.None);
37        }
38
39        EditorGUILayout.Space();
40
41        if (GUILayout.Button("Generate"))
42            DoGenerate();
43    }
44
45    void DoGenerate()
46    {
47        int res = _type == NoiseType.WhiteNoise ? _resolution : _resolution * _texelsPerCell;
48        Texture3D tex;
49
50        bool makeMipMaps = true;
51        switch (_type)
52        {
53            case NoiseType.WorleyNoise:
54            {
55                tex = new Texture3D(res, res, res, TextureFormat.R8, makeMipMaps);
56                GenerateWorleyField(res, _texelsPerCell, _metric, out var f1, out _);
57                var pixels = new byte[res * res * res];
58                for (int i = 0; i < pixels.Length; i++)
59                    pixels[i] = (byte)Mathf.RoundToInt(Mathf.Clamp01(f1[i]) * 255f);
60                tex.SetPixelData(pixels, 0);
61                break;
62            }
63            case NoiseType.WorleyEdgeSDF:
64            {
65                tex = new Texture3D(res, res, res, TextureFormat.R8, makeMipMaps);
66                GenerateWorleyField(res, _texelsPerCell, _metric, out var f1, out var f2);
67                var pixels = new byte[res * res * res];
68                GenerateEdgeSDF(pixels, f2, _sdfRadius);
69                tex.SetPixelData(pixels, 0);
70                break;
71            }
72            default:
73            {
74                tex = new Texture3D(res, res, res, TextureFormat.RGB24, makeMipMaps);
75                var pixels = new Color32[res * res * res];
76                GenerateWhiteNoise(pixels);
77                tex.SetPixels32(pixels);
78                break;
79            }
80        }
81
82        tex.wrapMode = TextureWrapMode.Repeat;
83        tex.filterMode = FilterMode.Trilinear;
84        tex.Apply();
85
86        string name;
87        switch (_type)
88        {
89            case NoiseType.WorleyNoise:    name = $"WorleyNoise3D_{_metric}_{res}"; break;
90            case NoiseType.WorleyEdgeSDF:  name = $"WorleyEdgeSDF3D_{_metric}_{res}"; break;
91            default:                       name = $"WhiteNoise3D_{res}"; break;
92        }
93
94        string path = $"Assets/yum_food/3ner/Textures/{name}.asset";
95        if (!AssetDatabase.IsValidFolder("Assets/yum_food/3ner/Textures"))
96            AssetDatabase.CreateFolder("Assets/yum_food/3ner", "Textures");
97
98        AssetDatabase.CreateAsset(tex, path);
99        AssetDatabase.SaveAssets();
100        EditorGUIUtility.PingObject(tex);
101        Debug.Log($"[GenerateNoise] Saved {_type} ({res}\u00b3) to {path}");
102    }
103
104    static void GenerateWhiteNoise(Color32[] pixels)
105    {
106        for (int i = 0; i < pixels.Length; i++)
107        {
108            byte r = (byte)Random.Range(0, 256);
109            byte g = (byte)Random.Range(0, 256);
110            byte b = (byte)Random.Range(0, 256);
111            byte a = (byte)Random.Range(0, 256);
112            pixels[i] = new Color32(r, g, b, a);
113        }
114    }
115
116    static void GenerateWorleyField(int res, int texelsPerCell, DistanceMetric metric,
117        out float[] f1, out float[] edgeDist)
118    {
119        int cellCount = Mathf.Max(1, res / texelsPerCell);
120        float cellSize = (float)res / cellCount;
121        float rcpCellSize = 1f / cellSize;
122
123        var points = new Vector3[cellCount * cellCount * cellCount];
124        for (int cz = 0; cz < cellCount; cz++)
125        for (int cy = 0; cy < cellCount; cy++)
126        for (int cx = 0; cx < cellCount; cx++)
127        {
128            int ci = cx + cellCount * (cy + cellCount * cz);
129            points[ci] = new Vector3(
130                (cx + Random.value) * cellSize,
131                (cy + Random.value) * cellSize,
132                (cz + Random.value) * cellSize);
133        }
134
135        float rcpMaxDist = rcpCellSize;
136        int count = res * res * res;
137        var f1Arr = new float[count];
138        var edgeArr = new float[count];
139        bool isL2 = metric == DistanceMetric.L2;
140        bool isL1 = metric == DistanceMetric.L1;
141
142        Parallel.For(0, res, z =>
143        {
144            for (int y = 0; y < res; y++)
145            for (int x = 0; x < res; x++)
146            {
147                float px = x + 0.5f, py = y + 0.5f, pz = z + 0.5f;
148                int cx0 = (int)(px * rcpCellSize);
149                int cy0 = (int)(py * rcpCellSize);
150                int cz0 = (int)(pz * rcpCellSize);
151
152                // Pass 1: Find closest point (metric determines cell ownership)
153                float bestDist = float.MaxValue;
154                float p1x = 0, p1y = 0, p1z = 0;
155                int p1nx = 0, p1ny = 0, p1nz = 0;
156
157                for (int dz = -1; dz <= 1; dz++)
158                for (int dy = -1; dy <= 1; dy++)
159                for (int dx = -1; dx <= 1; dx++)
160                {
161                    int ncx = cx0 + dx, ncy = cy0 + dy, ncz = cz0 + dz;
162                    int wcx = ((ncx % cellCount) + cellCount) % cellCount;
163                    int wcy = ((ncy % cellCount) + cellCount) % cellCount;
164                    int wcz = ((ncz % cellCount) + cellCount) % cellCount;
165                    var pt = points[wcx + cellCount * (wcy + cellCount * wcz)];
166                    float qx = pt.x + (ncx - wcx) * cellSize;
167                    float qy = pt.y + (ncy - wcy) * cellSize;
168                    float qz = pt.z + (ncz - wcz) * cellSize;
169
170                    float ex = qx - px, ey = qy - py, ez = qz - pz;
171                    float dist;
172                    if (isL1)
173                    {
174                        float ax = ex < 0 ? -ex : ex, ay = ey < 0 ? -ey : ey, az = ez < 0 ? -ez : ez;
175                        dist = ax + ay + az;
176                    }
177                    else if (isL2)
178                    {
179                        dist = ex * ex + ey * ey + ez * ez; // compare squared, defer sqrt
180                    }
181                    else
182                    {
183                        float ax = ex < 0 ? -ex : ex, ay = ey < 0 ? -ey : ey, az = ez < 0 ? -ez : ez;
184                        dist = ax > ay ? (ax > az ? ax : az) : (ay > az ? ay : az);
185                    }
186
187                    if (dist < bestDist)
188                    {
189                        bestDist = dist;
190                        p1x = qx; p1y = qy; p1z = qz;
191                        p1nx = ncx; p1ny = ncy; p1nz = ncz;
192                    }
193                }
194
195                float d1 = isL2 ? (float)System.Math.Sqrt(bestDist) : bestDist;
196
197                // Pass 2: Squared distance to nearest bisecting plane (Euclidean regardless
198                // of metric — linear under trilinear filtering). One sqrt at the end.
199                float r1x = p1x - px, r1y = p1y - py, r1z = p1z - pz;
200                float minEdgeSq = float.MaxValue;
201
202                for (int dz = -2; dz <= 2; dz++)
203                for (int dy = -2; dy <= 2; dy++)
204                for (int dx = -2; dx <= 2; dx++)
205                {
206                    int ncx = cx0 + dx, ncy = cy0 + dy, ncz = cz0 + dz;
207                    if (ncx == p1nx && ncy == p1ny && ncz == p1nz) continue;
208
209                    int wcx = ((ncx % cellCount) + cellCount) % cellCount;
210                    int wcy = ((ncy % cellCount) + cellCount) % cellCount;
211                    int wcz = ((ncz % cellCount) + cellCount) % cellCount;
212                    var pt = points[wcx + cellCount * (wcy + cellCount * wcz)];
213                    float qx = pt.x + (ncx - wcx) * cellSize;
214                    float qy = pt.y + (ncy - wcy) * cellSize;
215                    float qz = pt.z + (ncz - wcz) * cellSize;
216
217                    float r2x = qx - px, r2y = qy - py, r2z = qz - pz;
218                    float ax = r2x - r1x, ay = r2y - r1y, az = r2z - r1z;
219                    // num = dot(r1 + r2, axis), sign determines which side of the plane
220                    float num = (r1x + r2x) * ax + (r1y + r2y) * ay + (r1z + r2z) * az;
221                    if (num > 0f)
222                    {
223                        // d = num / (2 * |axis|), so d² = num² / (4 * |axis|²)
224                        float denSq = ax * ax + ay * ay + az * az;
225                        float dSq = num * num / (4f * denSq);
226                        if (dSq < minEdgeSq) minEdgeSq = dSq;
227                    }
228                }
229
230                int i = x + res * (y + res * z);
231                f1Arr[i] = d1 * rcpMaxDist;
232                edgeArr[i] = (float)System.Math.Sqrt(minEdgeSq) * rcpMaxDist;
233            }
234        });
235
236        f1 = f1Arr;
237        edgeDist = edgeArr;
238    }
239
240    // Convert bisecting-plane distance into [0,1]: 0.5 = on boundary, 1 = deep inside cell.
241    // sdfRadius controls how much distance maps to [0,1] — smaller = more precision near edges.
242    static void GenerateEdgeSDF(byte[] pixels, float[] edgeDist, float sdfRadius)
243    {
244        for (int i = 0; i < pixels.Length; i++)
245        {
246            float normalized = edgeDist[i] / sdfRadius * 0.5f + 0.5f;
247            pixels[i] = (byte)Mathf.RoundToInt(Mathf.Clamp01(normalized) * 255f);
248        }
249    }
250}