yum/3ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum/3ner
50e4514
master
1#ifUDON 2 3using System ; 4using System . Runtime . InteropServices ; 5using System . Collections . Generic ; 6using UdonSharp ; 7using UnityEngine ; 8using VRC . SDK3 . Rendering ; 9using VRC . Udon . Common . Interfaces ; 10 11public class DataDecoder : UdonSharpBehaviour 12{ 13public RenderTexture sourceTexture ; 14public MeshRenderer target ; 15 16private int tileSize = 8 ; 17// Minimum size (in pixels) of a tile. This is shared with our tixl operator. 18private const int kMinTileSize = 4 ; 19private const int kMaxTileSize = 128 ; 20private Color32 [] pixelData ; 21private bool hasData = false ; 22private int readWidth ; 23private int readHeight ; 24 25// The wall time at which we last saw a sync event 26private float wallSyncTime ; 27// The logical time corresponding to the last sync event 28private float logicalSyncTimeMs ; 29// The rate at which logical time passes every second. 30private float logicalTimeFactor ; 31 32// Top-level data types. 33private const int kT_TimeSyncData = 0 ; 34 35void Start () {} 36 37void Update () 38{ 39if ( sourceTexture == null ) return ; 40 41// Request the rectangular region which leaves either a right-justified 42// square, or a bottom-justified square. 43int requestWidth ; 44int requestHeight ; 45if ( sourceTexture . width < sourceTexture . height ) { 46requestWidth = sourceTexture . width ; 47requestHeight = sourceTexture . height - sourceTexture . width ; 48} else { 49requestHeight = sourceTexture . height ; 50requestWidth = sourceTexture . width - sourceTexture . height ; 51} 52int pixelCount = requestWidth * requestHeight ; 53 54if ( pixelCount <= 0 ) return ; 55 56if ( pixelData == null || pixelCount != pixelData . Length ) 57{ 58pixelData = new Color32 [ pixelCount ]; 59hasData = false ; 60} 61 62readWidth = requestWidth ; 63readHeight = requestHeight ; 64 65VRCAsyncGPUReadback . Request ( sourceTexture , 660 , 670 , readWidth , 680 , readHeight , 690 , 1 , 70( IUdonEventReceiver ) this ); 71 72if ( hasData ) 73{ 74ProcessTiles (); 75hasData = false ; 76} 77 78if ( wallSyncTime != null ) { 79float logicalTime = logicalSyncTimeMs * 0.001f + 80logicalTimeFactor * ( Time . time - wallSyncTime ); 81if ( target != null ) { 82target . material . SetFloat ( "_Logical_Time" , logicalTime ); 83} 84} 85} 86 87public override void OnAsyncGpuReadbackComplete ( VRCAsyncGPUReadbackRequest request ) 88{ 89if ( request . hasError ) return ; 90 91if ( pixelData != null && request . TryGetData ( pixelData )) 92{ 93hasData = true ; 94} 95} 96 97// Byte parsing logic 98private bool HasBytesLeft ( byte [] b , int offset , int bytesLeft ) { 99return b . Length - offset >= bytesLeft ; 100} 101 102private int GetInt ( ref byte [] b , ref int offset ) { 103int ret = BitConverter . ToInt32 ( b , offset ); 104offset += 4 ; 105return ret ; 106} 107 108private float GetFloat ( ref byte [] b , ref int offset ) { 109float ret = BitConverter . ToSingle ( b , offset ); 110offset += 4 ; 111return ret ; 112} 113 114// Convert a two's complement number in the lower 4 bits of a byte to a 115// gray code in the lower 4 bits. 116private byte ToGrayNibble ( byte twosComplementNibble ) 117{ 118int lowerN = twosComplementNibble & 0x0F ; 119int lowerG = lowerN ^ ( lowerN >> 1 ); 120return ( byte )(( twosComplementNibble & 0xF0 ) | lowerG ); 121} 122 123// Convert a gray code in the lower 4 bits of a byte to a two's complement 124// number in the lower 4 bits. 125private byte ToTwosComplementNibble ( byte grayNibble ) 126{ 127int temp = grayNibble & 0x0F ; 128temp ^= ( temp >> 1 ); 129temp ^= ( temp >> 2 ); 130temp &= 0x0F ; 131return ( byte )(( grayNibble & 0xF0 ) | temp ); 132} 133 134private void ProcessTiles () 135{ 136// Three reserved tiles: 137// 1. Size. The size of the tiles, in pixels. 138// 2. Length. The length of the payload, in subpixels. 139// 3. Checksum. The checksum of the size, length, and first third of the 140// payload. The payload is sent in triplicate to allow for some 141// forward error correction. 142const int kNumReservedTiles = 3 ; 143 144// Get the tile size. 145{ 146int oldTileSize = tileSize ; 147tileSize = kMinTileSize ; 148tileSize = Parse24BitTile ( 0 ); 149tileSize = Mathf . Clamp ( tileSize , kMinTileSize , kMaxTileSize ); 150if ( tileSize != oldTileSize ) { 151Debug . Log ( $"Tile size changed from { oldTileSize } to { tileSize } " ); 152} 153} 154 155// Get the length. This is in units of subpixels. So we will need to access 156// ceil(length/3) tiles. 157int lengthSubpixels = Parse24BitTile ( 1 ); 158int lengthTiles = ( int ) Mathf . Ceil ( lengthSubpixels / 3.0f ); 159 160// Get the checksum. This covers the size tile, length tile, and first 161// third of the payload. 162int checksumRemote = Parse24BitTile ( 2 ); 163int checksumLocal = tileSize + lengthSubpixels ; 164 165Color32 parsed_first = GetTileRGB ( 0 ); 166/* 167Debug.Log($"First tile: {parsed_first.r} {parsed_first.g} {parsed_first.b}"); 168Debug.Log($"Parsed size {tileSize}"); 169Debug.Log($"Parsed length {lengthSubpixels}"); 170Debug.Log($"Parsed checksum {checksumRemote}"); 171*/ 172 173// Collect all nibbles into a flat array. Note that these are still 174// encoded. 175int [] nibbles = new int [ lengthSubpixels ]; 176int nibbleCount = 0 ; 177for ( int tile_i = 0 ; tile_i < lengthTiles ; tile_i ++ ) { 178Color32 parsed_i = GetTileRGB ( tile_i + kNumReservedTiles ); 179nibbles [ nibbleCount ++ ] = parsed_i . r ; 180if ( nibbleCount < lengthSubpixels ) { 181nibbles [ nibbleCount ++ ] = parsed_i . g ; 182} 183if ( nibbleCount < lengthSubpixels ) { 184nibbles [ nibbleCount ++ ] = parsed_i . b ; 185} 186} 187 188// Compute checksum of nibbles. Match behavior in OperatorEncoding :: 189// Checksum. Note that we only look at the first third of our data, 190// since data is sent in triplicate. 191for ( int i = 0 ; i < nibbleCount / 3 ; i ++ ) { 192checksumLocal += ( nibbles [ i ] >> 4 ) & 0x0F ; 193} 194 195//Debug.Log($"Local checksum {checksumLocal}"); 196 197if ( checksumLocal != checksumRemote ) { 198//Debug.LogWarning($"Checksums don't match. Attempting error recovery."); 199 200// Data is submitted in triplicate. Perform a bitwise majority vote 201// with `(a & b) | (a & c) | (b & c)`. 202int nc3 = nibbleCount / 3 ; 203checksumLocal = tileSize + lengthSubpixels ; 204for ( int i = 0 ; i < nibbleCount / 3 ; i ++ ) { 205int copy0 = nibbles [ i ]; 206int copy1 = nibbles [ i + nc3 ]; 207int copy2 = nibbles [ i + nc3 * 2 ]; 208 209// Convert to gray codes before error correction to (hopefully) 210// minimize the number of bit flips which must be corrected. 211byte copy0Gray = ToGrayNibble (( byte )(( copy0 >> 4 ) & 0x0F )); 212byte copy1Gray = ToGrayNibble (( byte )(( copy1 >> 4 ) & 0x0F )); 213byte copy2Gray = ToGrayNibble (( byte )(( copy2 >> 4 ) & 0x0F )); 214 215int resolvedGray = 216( copy0Gray & copy1Gray ) | 217( copy0Gray & copy2Gray ) | 218( copy1Gray & copy2Gray ); 219 220byte resolved = ToTwosComplementNibble (( byte ) resolvedGray ); 221nibbles [ i ] = ( resolved << 4 ) & 0xF0 ; 222 223checksumLocal += ( nibbles [ i ] >> 4 ) & 0x0F ; 224} 225 226// Check result 227if ( checksumLocal != checksumRemote ) { 228Debug . LogError ( $"Checksums still don't match after recovery: " + 229$" { checksumRemote } vs { checksumLocal } . Bailing out." ); 230return ; 231} 232} 233 234// Convert nibbles to bytes. 235int byteCount = nibbleCount / 6 ; 236byte [] bytes = new byte [ byteCount ]; 237for ( int i = 0 ; i < byteCount ; i ++ ) { 238// See DataEncoder.cs. It puts the upper 4 bits before the lower 4 bits. 239bytes [ i ] = ( byte ) (( nibbles [ 2 * i ] & 0xF0 ) | (( nibbles [ 2 * i + 1 ] & 0xF0 ) >> 4 )); 240} 241//Debug.Log($"Parsed {bytes.Length} bytes from {nibbles.Length} subpixels"); 242 243// Parse input. 244int bOff = 0 ; 245while ( HasBytesLeft ( bytes , bOff , 8 )) { 246int type = GetInt ( ref bytes , ref bOff ); 247int length = GetInt ( ref bytes , ref bOff ); 248// Can't descend into value if there's not enough length.... 249if ( ! HasBytesLeft ( bytes , bOff , length )) { 250break ; 251} 252switch ( type ) { 253case kT_TimeSyncData : 254{ 255float syncTimeMs = GetFloat ( ref bytes , ref bOff ); 256float measureTime = GetFloat ( ref bytes , ref bOff ) * 1e-6f ; 257//Debug.Log($"Parsed time sync data: {syncTimeMs} {measureTimeUs}"); 258 259if ( logicalSyncTimeMs != syncTimeMs ) { 260// Indicate that we have seen a sync event. 261wallSyncTime = Time . time ; 262Debug . Log ( $"Sync time updated: t0= { logicalSyncTimeMs } ms, k=$ { measureTime } " ); 263} 264logicalSyncTimeMs = syncTimeMs ; 265logicalTimeFactor = 1.0f / measureTime ; 266break ; 267} 268} 269} 270} 271 272private int Parse24BitTile ( int tileIdx ) 273{ 274Color32 parsed = GetTileRGB ( tileIdx ); 275int data = 0 ; 276data |= DecodeNibble ( parsed . r ); 277data |= DecodeNibble ( parsed . g ) << 4 ; 278data |= DecodeNibble ( parsed . b ) << 8 ; 279return data ; 280} 281 282private int DecodeNibble ( int subpixel ) { 283return ( subpixel >> 4 ) & 0x0F ; 284} 285 286private Color32 GetTileRGB ( int tileIdx ) 287{ 288// Calculate which column and position within column this tile is in 289int tilesPerColumn = readHeight / tileSize ; 290int column = tileIdx / Math . Max ( 1 , tilesPerColumn ); 291int tileInColumn = tileIdx % tilesPerColumn ; 292 293// Calculate Y position (vertical position within column) 294int tileY = tileInColumn * tileSize ; 295int centerY = tileY + tileSize / 2 ; 296 297// Calculate X position (horizontal position based on column) 298int tileX = column * tileSize ; 299int centerX = tileX + tileSize / 2 ; 300 301if ( centerY >= readHeight ) return new Color32 (); 302if ( centerX >= readWidth ) return new Color32 (); 303 304int localX = centerX ; 305int localY = readHeight - 1 - centerY ; 306int index = localY * readWidth + localX ; 307 308if ( index < 0 || index >= pixelData . Length ) return new Color32 (); 309 310return pixelData [ index ]; 311} 312} 313 314#endif// UDON 315