Skip to main content

What are Serializers?

Serializers are the core plugins that convert DMX channel values (bytes) into pixel data in a video texture. They define the pixel mapping format - the spatial arrangement of how lighting channels are encoded into an image.

Serialization

Converts DMX bytes → Video pixels

Deserialization

Converts Video pixels → DMX bytes

Bidirectional

Can both encode and decode data

Configurable

Each serializer has custom settings

The IDMXSerializer Interface

All serializers implement the IDMXSerializer interface, which defines the contract for converting between DMX and video:
IDMXSerializer.cs:7-41
public interface IDMXSerializer : IUserInterface<IDMXGenerator>, IConstructable
{
    /// <summary>
    /// Serializes a channel from a raw byte representation, to a output video stream.
    /// </summary>
    void SerializeChannel(ref Color32[] pixels, byte channelValue, int channel, 
                         int textureWidth, int textureHeight);
    
    /// <summary>
    /// Deserializes a channel from a input video stream, to a raw byte representation.
    /// </summary>
    void DeserializeChannel(Texture2D tex, ref byte channelValue, int channel, 
                           int textureWidth, int textureHeight);
    
    /// <summary>
    /// Called at the start of each frame to reset any state.
    /// </summary>
    void InitFrame(ref List<byte> channelValues);
    
    /// <summary>
    /// Called after all channels have been serialized for the current frame.
    /// Can be used to for example generate a CRC block area, or operate on multiple channels at once.
    /// </summary>
    void CompleteFrame(ref Color32[] pixels, ref List<byte> channelValues, 
                       int textureWidth, int textureHeight);
}

Lifecycle Methods

1

InitFrame

Called once per frame before any channels are processed. Use to reset counters or prepare state.
2

SerializeChannel / DeserializeChannel

Called for each DMX channel. Write/read pixels in the texture at the appropriate location.
3

CompleteFrame

Called after all channels are processed. Use for post-processing like CRC blocks or global operations.
Serializers have access to the full channel array and can read/write any pixel. This enables advanced features like error correction codes or channel interdependencies.

Available Serializers

HNode includes several built-in serializers for different use cases:

VRSL (VR Stage Lighting)

Format: Grid-based, 16×16 pixel blocks per channel

Block Size

16×16 pixels per channel

Blocks Per Column

13 channels vertically

Channels Per Universe

512 (DMX standard)

Orientation

4 layout options
Features:
  • Gamma Correction: Optional sRGB ↔ Linear conversion
  • RGB Grid Mode: Encodes 3 universes as R/G/B channels on one grid
  • Layout Options: HorizontalTop, HorizontalBottom, VerticalLeft, VerticalRight
  • Alpha Encoding: Uses alpha channel to encode transparency
SerializerVRSL.cs:26-61
public void SerializeChannel(ref Color32[] pixels, byte channelValue, int channel, 
                             int textureWidth, int textureHeight)
{
    GetPositionData(channel, out int x, out int y, out int universeOffset);
    
    // Apply layout transformation
    switch (outputConfig)
    {
        case OutputConfigs.HorizontalTop:
            x += universeOffset;
            break;
        case OutputConfigs.VerticalLeft:
            int temp = x;
            x = y;
            y = temp;
            y += universeOffset;
            y = textureHeight - y - blockSize;
            break;
        // ...
    }
    
    var color = new Color(
        channelValue / 255f,
        channelValue / 255f,
        channelValue / 255f,
        Util.GetBlockAlpha(channelValue)
    );
    if (GammaCorrection) { color = color.linear; }
    TextureWriter.MakeColorBlock(ref pixels, x, y, color, blockSize);
}
VRSL is the most widely supported format in VRChat for stage lighting systems. It’s designed by Rollthered and used by many VR performance venues.

Binary

Format: Bit-expansion encoding, 4×4 pixel blocks per bit

Block Size

4×4 pixels per bit

Blocks Per Column

52 bits vertically

Bit Expansion

8 bits per channel = 8 blocks

Density

Very space-efficient
How it works: Each byte is split into 8 individual bits, and each bit gets its own 4×4 pixel block (white for 1, black for 0).
SerializerBinary.cs:16-38
public void SerializeChannel(ref Color32[] pixels, byte channelValue, int channel, 
                             int textureWidth, int textureHeight)
{
    // Split the value into 8 bits
    var bits = new BitArray(new byte[] { channelValue });
    
    for (int i = 0; i < bits.Length; i++)
    {
        GetPositionData(channel, i, out int x, out int y);
        if (x >= textureWidth || y >= textureHeight) continue;
        
        var color = new Color32(
            (byte)(bits[i] ? 255 : 0),
            (byte)(bits[i] ? 255 : 0),
            (byte)(bits[i] ? 255 : 0),
            Util.GetBlockAlpha(channelValue)
        );
        TextureWriter.MakeColorBlock(ref pixels, x, y, color, blockSize);
    }
}
Binary format provides the highest channel density per pixel, making it ideal for large DMX setups with limited texture resolution.

Ternary

Format: Base-3 encoding, 6 ternary digits per channel

Block Size

4×4 pixels per digit

Blocks Per Column

48 digits vertically

Base-3 Encoding

6 ternary digits per byte

Intensity Levels

0, 127, 255 per digit
How it works: Each byte (0-255) is converted to a 6-digit base-3 number, where each digit (0, 1, or 2) is mapped to intensity levels.
SerializerTernary.cs:89-104
public byte[] convertToTernary(byte N)
{
    if (N == 0) return new byte[6];
    
    byte[] bytes = new byte[6];
    int index = 5;
    for (int i = 0; i < 6; i++)
    {
        bytes[index--] = (byte)(N % 3);
        N /= 3;
    }
    return bytes;
}

BinaryStageFlight

Format: Optimized binary format for StageFlight systems Similar to Binary but with layout optimizations for StageFlight’s specific use cases in VR stage environments.

ColorBinary

Format: Binary encoding using RGB channels Encodes binary data into the R, G, and B channels separately, effectively tripling the data density at the cost of color-accurate rendering.

FuralitySomna

Format: Custom format for Furality Somna events Optimized layout for the Furality Somna virtual convention’s lighting infrastructure.

Spiral

Format: Spiral pattern pixel mapping Arranges channels in a spiral pattern from center outward, useful for certain radial fixture arrangements.

Choosing a Serializer

Use this guide to pick the right serializer:
  • Working with VRChat or other VR platforms
  • Need wide compatibility with existing systems
  • Want human-readable channel layout
  • Using gamma-corrected rendering pipelines
  • Working with existing VRSL fixtures
  • Need maximum channel density
  • Have limited texture resolution
  • Working with 10+ universes
  • Texture bandwidth is a concern
  • Color accuracy isn’t required
  • Need better error tolerance than binary
  • Want visual distinction between states
  • Working with noisy video transmission
  • Experimenting with alternative encodings
  • Working with specific event/venue systems
  • Need compatibility with existing infrastructure
  • Have custom fixture layouts
  • Venue requires specific format

Block Rendering System

Serializers use HNode’s block rendering utilities to draw channel data:

MakeColorBlock

Fills a square area with a solid color:
TextureWriter.cs:187-198
public static void MakeColorBlock(ref Color32[] pixels, int x, int y, 
                                  Color32 color, int size)
{
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++)
        {
            int index = PixelToIndex(x + i, y + j);
            if (index == -1) return;
            pixels[index] = color;
        }
    }
}

MixColorBlock

Writes to a specific color channel (RGB):
TextureWriter.cs:200-231
public static void MixColorBlock(ref Color32[] pixels, int x, int y, 
                                 byte channelValue, ColorChannel channel, int size)
{
    for (int i = 0; i < size; i++)
    {
        for (int j = 0; j < size; j++)
        {
            int index = PixelToIndex(x + i, y + j);
            if (index == -1) return;
            Color32 pixelColor = pixels[index];
            
            switch (channel)
            {
                case ColorChannel.Red:
                    pixelColor.r = channelValue;
                    break;
                case ColorChannel.Green:
                    pixelColor.g = channelValue;
                    break;
                case ColorChannel.Blue:
                    pixelColor.b = channelValue;
                    break;
            }
            pixelColor.a = 255;
            pixels[index] = pixelColor;
        }
    }
}
When writing custom serializers, always use these block utilities. They handle bounds checking and coordinate conversion automatically.

Serializer Configuration

Serializers are configured in the show configuration:
serializer: !VRSL
  gammaCorrection: true
  rgbGridMode: false
  outputConfig: 0  # HorizontalTop

deserializer: !VRSL
  gammaCorrection: true
  rgbGridMode: false
  outputConfig: 0
You can use different serializers for input (deserializer) and output (serializer) when in transcode mode. This enables format conversion between systems.

Alpha Channel Encoding

HNode uses a clever alpha channel encoding to represent zero values:
Util.GetBlockAlpha(byte channelValue)
{
    return channelValue == 0 ? (byte)0 : (byte)255;
}
This makes zero-value channels transparent in the output video, allowing:
  • Visual identification of active channels
  • Compositing multiple DMX streams
  • Overlay effects in video processing
When reading HNode video output, always check the alpha channel. Transparent pixels (alpha=0) represent channel value 0, regardless of RGB values.

Creating Custom Serializers

To create a custom serializer:
1

Implement IDMXSerializer

Create a class implementing the IDMXSerializer interface.
2

Define Block Layout

Determine your blockSize and blocksPerCol constants.
3

Implement GetPositionData

Write a method to convert channel index to (x, y) pixel coordinates.
4

Implement SerializeChannel

Use TextureWriter.MakeColorBlock to write pixels at the calculated position.
5

Implement DeserializeChannel

Read pixels using TextureReader.GetColor and convert back to byte values.
6

Test

HNode will automatically discover your serializer on next launch.
Study the existing serializers (VRSL, Binary, Ternary) as templates. They demonstrate different approaches to position calculation and encoding.

Performance Considerations

  • Block Size: Smaller blocks = more channels per texture, but slower rendering
  • Color Operations: Gamma correction adds computational cost
  • Bounds Checking: Always validate coordinates before writing pixels
  • Frame Buffer Size: Pre-allocate Color32[] arrays to avoid GC
TextureWriter.cs:61-67
// Clear texture efficiently
Array.Clear(pixels, 0, pixels.Length);
Serializer performance directly impacts overall frame rate. Profile your custom serializers using Unity’s Profiler tool.

Build docs developers (and LLMs) love