yum/3ner

A toon shader for Unity's BIRP.

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

yumImpostors: begin optimization work50e4514

master
9.6 KiB315 linesraw
1#if UDON
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{
13  public RenderTexture sourceTexture;
14  public MeshRenderer target;
15
16  private int tileSize = 8;
17  // Minimum size (in pixels) of a tile. This is shared with our tixl operator.
18  private const int kMinTileSize = 4;
19  private const int kMaxTileSize = 128;
20  private Color32[] pixelData;
21  private bool hasData = false;
22  private int readWidth;
23  private int readHeight;
24
25  // The wall time at which we last saw a sync event
26  private float wallSyncTime;
27  // The logical time corresponding to the last sync event
28  private float logicalSyncTimeMs;
29  // The rate at which logical time passes every second.
30  private float logicalTimeFactor;
31
32  // Top-level data types.
33  private const int kT_TimeSyncData = 0;
34
35  void Start() {}
36
37  void Update()
38  {
39    if (sourceTexture == null) return;
40
41    // Request the rectangular region which leaves either a right-justified
42    // square, or a bottom-justified square.
43    int requestWidth;
44    int requestHeight;
45    if (sourceTexture.width < sourceTexture.height) {
46      requestWidth = sourceTexture.width;
47      requestHeight = sourceTexture.height - sourceTexture.width;
48    } else {
49      requestHeight = sourceTexture.height;
50      requestWidth = sourceTexture.width - sourceTexture.height;
51    }
52    int pixelCount = requestWidth * requestHeight;
53
54    if (pixelCount <= 0) return;
55
56    if (pixelData == null || pixelCount != pixelData.Length)
57    {
58      pixelData = new Color32[pixelCount];
59      hasData = false;
60    }
61
62    readWidth = requestWidth;
63    readHeight = requestHeight;
64
65    VRCAsyncGPUReadback.Request(sourceTexture,
66        0,
67        0, readWidth,
68        0, readHeight,
69        0, 1,
70        (IUdonEventReceiver)this);
71
72    if (hasData)
73    {
74      ProcessTiles();
75      hasData = false;
76    }
77
78    if (wallSyncTime != null) {
79      float logicalTime = logicalSyncTimeMs * 0.001f +
80        logicalTimeFactor * (Time.time - wallSyncTime);
81      if (target != null) {
82        target.material.SetFloat("_Logical_Time", logicalTime);
83      }
84    }
85  }
86
87  public override void OnAsyncGpuReadbackComplete(VRCAsyncGPUReadbackRequest request)
88  {
89    if (request.hasError) return;
90
91    if (pixelData != null && request.TryGetData(pixelData))
92    {
93      hasData = true;
94    }
95  }
96
97  // Byte parsing logic
98  private bool HasBytesLeft(byte[] b, int offset, int bytesLeft) {
99    return b.Length - offset >= bytesLeft;
100  }
101
102  private int GetInt(ref byte[] b, ref int offset) {
103    int ret = BitConverter.ToInt32(b, offset);
104    offset += 4;
105    return ret;
106  }
107
108  private float GetFloat(ref byte[] b, ref int offset) {
109    float ret = BitConverter.ToSingle(b, offset);
110    offset += 4;
111    return 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.
116  private byte ToGrayNibble(byte twosComplementNibble)
117  {
118    int lowerN = twosComplementNibble & 0x0F;
119    int lowerG = lowerN ^ (lowerN >> 1);
120    return (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.
125  private byte ToTwosComplementNibble(byte grayNibble)
126  {
127    int temp = grayNibble & 0x0F;
128    temp ^= (temp >> 1);
129    temp ^= (temp >> 2);
130    temp &= 0x0F;
131    return (byte)((grayNibble & 0xF0) | temp);
132  }
133
134  private 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.
142    const int kNumReservedTiles = 3;
143
144    // Get the tile size.
145    {
146      int oldTileSize = tileSize;
147      tileSize = kMinTileSize;
148      tileSize = Parse24BitTile(0);
149      tileSize = Mathf.Clamp(tileSize, kMinTileSize, kMaxTileSize);
150      if (tileSize != oldTileSize) {
151        Debug.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.
157    int lengthSubpixels = Parse24BitTile(1);
158    int 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.
162    int checksumRemote = Parse24BitTile(2);
163    int checksumLocal = tileSize + lengthSubpixels;
164
165    Color32 parsed_first = GetTileRGB(0);
166    /*
167    Debug.Log($"First tile: {parsed_first.r} {parsed_first.g} {parsed_first.b}");
168    Debug.Log($"Parsed size {tileSize}");
169    Debug.Log($"Parsed length {lengthSubpixels}");
170    Debug.Log($"Parsed checksum {checksumRemote}");
171    */
172
173    // Collect all nibbles into a flat array. Note that these are still
174    // encoded.
175    int[] nibbles = new int[lengthSubpixels];
176    int nibbleCount = 0;
177    for (int tile_i = 0; tile_i < lengthTiles; tile_i++) {
178      Color32 parsed_i = GetTileRGB(tile_i+kNumReservedTiles);
179      nibbles[nibbleCount++] = parsed_i.r;
180      if (nibbleCount < lengthSubpixels) {
181        nibbles[nibbleCount++] = parsed_i.g;
182      }
183      if (nibbleCount < lengthSubpixels) {
184        nibbles[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.
191    for (int i = 0; i < nibbleCount / 3; i++) {
192      checksumLocal += (nibbles[i] >> 4) & 0x0F;
193    }
194
195    //Debug.Log($"Local checksum {checksumLocal}");
196
197    if (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)`.
202      int nc3 = nibbleCount / 3;
203      checksumLocal = tileSize + lengthSubpixels;
204      for (int i = 0; i < nibbleCount / 3; i++) {
205        int copy0 = nibbles[i];
206        int copy1 = nibbles[i+nc3];
207        int 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.
211        byte copy0Gray = ToGrayNibble((byte)((copy0 >> 4) & 0x0F));
212        byte copy1Gray = ToGrayNibble((byte)((copy1 >> 4) & 0x0F));
213        byte copy2Gray = ToGrayNibble((byte)((copy2 >> 4) & 0x0F));
214
215        int resolvedGray =
216          (copy0Gray & copy1Gray) |
217          (copy0Gray & copy2Gray) |
218          (copy1Gray & copy2Gray);
219
220        byte resolved = ToTwosComplementNibble((byte)resolvedGray);
221        nibbles[i] = (resolved << 4) & 0xF0;
222
223        checksumLocal += (nibbles[i] >> 4) & 0x0F;
224      }
225
226      // Check result
227      if (checksumLocal != checksumRemote) {
228        Debug.LogError($"Checksums still don't match after recovery: " +
229            $"{checksumRemote} vs {checksumLocal}. Bailing out.");
230        return;
231      }
232    }
233
234    // Convert nibbles to bytes.
235    int byteCount = nibbleCount / 6;
236    byte[] bytes = new byte[byteCount];
237    for (int i = 0; i < byteCount; i++) {
238      // See DataEncoder.cs. It puts the upper 4 bits before the lower 4 bits.
239      bytes[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.
244    int bOff = 0;
245    while (HasBytesLeft(bytes, bOff, 8)) {
246      int type = GetInt(ref bytes, ref bOff);
247      int length = GetInt(ref bytes, ref bOff);
248      // Can't descend into value if there's not enough length....
249      if (!HasBytesLeft(bytes, bOff, length)) {
250        break;
251      }
252      switch (type) {
253        case kT_TimeSyncData:
254          {
255            float syncTimeMs = GetFloat(ref bytes, ref bOff);
256            float measureTime = GetFloat(ref bytes, ref bOff) * 1e-6f;
257            //Debug.Log($"Parsed time sync data: {syncTimeMs} {measureTimeUs}");
258
259            if (logicalSyncTimeMs != syncTimeMs) {
260              // Indicate that we have seen a sync event.
261              wallSyncTime = Time.time;
262              Debug.Log($"Sync time updated: t0={logicalSyncTimeMs} ms, k=${measureTime}");
263            }
264            logicalSyncTimeMs = syncTimeMs;
265            logicalTimeFactor = 1.0f / measureTime;
266            break;
267          }
268      }
269    }
270  }
271
272  private int Parse24BitTile(int tileIdx)
273  {
274    Color32 parsed = GetTileRGB(tileIdx);
275    int data = 0;
276    data |= DecodeNibble(parsed.r);
277    data |= DecodeNibble(parsed.g) << 4;
278    data |= DecodeNibble(parsed.b) << 8;
279    return data;
280  }
281
282  private int DecodeNibble(int subpixel) {
283    return (subpixel >> 4) & 0x0F;
284  }
285
286  private Color32 GetTileRGB(int tileIdx)
287  {
288    // Calculate which column and position within column this tile is in
289    int tilesPerColumn = readHeight / tileSize;
290    int column = tileIdx / Math.Max(1, tilesPerColumn);
291    int tileInColumn = tileIdx % tilesPerColumn;
292
293    // Calculate Y position (vertical position within column)
294    int tileY = tileInColumn * tileSize;
295    int centerY = tileY + tileSize / 2;
296
297    // Calculate X position (horizontal position based on column)
298    int tileX = column * tileSize;
299    int centerX = tileX + tileSize / 2;
300
301    if (centerY >= readHeight) return new Color32();
302    if (centerX >= readWidth) return new Color32();
303
304    int localX = centerX;
305    int localY = readHeight - 1 - centerY;
306    int index = localY * readWidth + localX;
307
308    if (index < 0 || index >= pixelData.Length) return new Color32();
309
310    return pixelData[index];
311  }
312}
313
314#endif  // UDON
315