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