DMX in Your Own Shaders - towneh/vrsl-urp GitHub Wiki

DMX in Your Own Shaders

A world's own shader can react to the desk: a screen that takes its colour from a fixture's channels, a logo that strobes with the rig, a laser of your own. This page covers the include that reads the DMX, the values a fixture component hands your shader, and porting a shader written against VR Stage Lighting's VRSLDMX.cginc.

The include

#include "Packages/town.mr.vrsl-urp/Runtime/Shaders/VRSLDMX.hlsl"

Put it after URP's Core.hlsl, or on its own (it includes Core.hlsl and core's Color.hlsl itself). It reads what the DMX manager reads: the video grid the manager's DMX Input is set to, Horizontal, Vertical or Legacy, or the channels a stream sends under Stream data. A shader using it stays in step with the package's own fixtures, whichever input the world is on.

It declares only the manager's own globals, every one named _VRSLU_… or _VRSLDMX…. It declares no material properties, no UnityPerMaterial and no instancing buffer, so your shader keeps its own layout and stays SRP Batcher compatible, and it sits beside URP's LitInput.hlsl and DeclareDepthTexture.hlsl in either order.

The package's other includes, under Runtime/Shaders/Shared/, are its own. They declare VRSL's material properties and the depth texture, so they won't compile beside URP's Lit inputs, and they change between releases. Use VRSLDMX.hlsl.

Under Stream data the channels come from a GPU buffer, which needs shader model 4.5. Add #pragma target 4.5 to the pass. Below it the reads take the grid only, which is black under Stream data.

The four reads

Every read takes an absolute channel number, the one a fixture component writes as _DMXChannel (below), and returns a value from 0 to 1. Each samples mip 0 of a grid texture, or loads from a buffer under Stream data, so all four work in the vertex stage as well as the fragment stage, and a shader can move geometry with them.

Function Returns
half VRSLDMX_Channel(uint channel) The channel's value. On a grid this is the smoothed value, which eases from one value to the next over a few frames, as the package's fixtures do
half VRSLDMX_ChannelRaw(uint channel) The channel's value as it arrived, before smoothing: it jumps in the frame the desk changes it. Under Stream data, which has no smoothing, the same as VRSLDMX_Channel
half VRSLDMX_Strobe(uint channel) 1 open, 0 closed, for a strobe channel. Held open while the channel is below 0.2 and while strobe is switched off on the manager
half4 VRSLDMX_Color(uint channel) A 13-channel fixture's colour: red, green and blue (+7, +8, +9) times the dimmer (+5), alpha 1. Pass the fixture's first channel

VRSLDMX_Color applies no brightness of its own. Multiply by whatever intensity your shader uses.

Pick per channel which of the first two to read. A dimmer read smoothed fades where the desk cuts; a speed that multiplies _Time.y should be read raw, or every ease sweeps the pattern through the whole change in speed times the time since the world loaded.

Functions starting vrsldmx_ (lower case) are the package's own and may change.

What a channel at the edge reads

A grid carries a fixed number of channels: Horizontal 1560 (three universes), Vertical 871, Legacy 650. A channel past that, and channel 0, reads as a channel at 0: value 0 and strobe open. Under Stream data a channel past what the source sends reads the same way.

A universe is 520 flat channels: 512 slots and 8 of padding, because the grid is 13 channels wide and 512 doesn't divide by 13. Universe 2's channel 1 is flat channel 521. The fixture component does this sum for you.

The reads take the standard encoding. A grid carrying nine universes, three per colour component, isn't something the public reads follow.

Driving it from a fixture component

VRSLDMXStatic writes its settings onto every renderer in its Obj Renderers list through a material property block, whatever shader the renderer has. Put your renderer on that list, declare the properties you use, and your shader gets the fixture's address and switches. The ones a DMX shader usually wants:

Property What it holds
_DMXChannel The fixture's first channel, absolute: from Dmx Channel and Dmx Universe, or the sector under Use Legacy Sector Mode. Pass it straight to the reads
_EnableDMX 1 when Enable DMX Channels is on. With it off, the fixture is meant to show its own colour rather than the desk's
_EnableStrobe 1 when Enable Strobe is on
_Emission Light Color Tint, the fixture's own colour (HDR)
_GlobalIntensity Global Intensity, 0 to 1, for animating or scripting brightness
_FinalIntensity Final Intensity, 0 to 1, the ceiling on the above, for a viewer's brightness setting
_NineUniverseMode 1 when Nine Universe Mode is on. The public reads don't use it

It writes the rest of its settings too (_PanInvert, _TiltInvert, _EnableSpin, _EnableFineChannels, _ProjectionSelection, _FixtureRotationX, _FixtureBaseRotationY, _EmissionDMX, _ConeWidth, _ConeLength, _MaxConeLength, _MaxMinPanAngle, _MaxMinTiltAngle, _LegacyGoboRange), for shaders that want them.

Five things to watch:

  • With Final Intensity Component Mode on, _FinalIntensity depends on the renderer's GameObject name: one whose name contains "Volume" or "Flare" gets Final Intensity Volumetric, "Project" gets Final Intensity Projection, and anything else Final Intensity Fixture.
  • The block replaces the renderer's whole property block each time the component updates, so a value your own script set on the same renderer through a property block is lost.
  • A renderer with a property block draws outside the SRP Batcher, even though the shader is compatible. That's the cost of driving it from the component.
  • The switch is _EnableDMX. A shader ported from one that switched on _EnableOSC should read _EnableDMX instead.
  • Declare _DMXChannel as a float and take (uint) round(_DMXChannel), which reads it correctly whichever way Unity stores it.

A fragment that glows in the fixture's colour, strobes with it and follows its intensity settings:

CBUFFER_START(UnityPerMaterial)
    float  _DMXChannel;
    float  _EnableDMX;
    float  _EnableStrobe;
    float4 _Emission;
    float  _GlobalIntensity;
    float  _FinalIntensity;
CBUFFER_END

half3 Glow()
{
    uint  first  = (uint) round(_DMXChannel);
    half3 colour = _EnableDMX > 0.5 ? VRSLDMX_Color(first).rgb : _Emission.rgb;
    half  strobe = _EnableDMX > 0.5 && _EnableStrobe > 0.5 ? VRSLDMX_Strobe(first + 6) : 1.0;
    return colour * strobe * _GlobalIntensity * _FinalIntensity;
}

The 13-channel layout the offsets come from is in the Fixture Configuration Reference: pan +0 and +1, tilt +2 and +3, zoom +4, dimmer +5, strobe +6, red, green and blue +7 to +9, gobo spin +10, gobo select +11 and movement smoothing +12.

Porting from VR Stage Lighting

A shader written against VR Stage Lighting's VRSLDMX.cginc changes its include line (the package path, and .hlsl for .cginc) and its reads:

VR Stage Lighting Here
ReadDMX(ch, _Udon_DMXGridRenderTexture) VRSLDMX_Channel(ch)
ReadDMX(ch, …RAW…), the unsmoothed grid VRSLDMX_ChannelRaw(ch)
GetStrobeOutput(ch) VRSLDMX_Strobe(ch). The old function also held strobe open when _EnableDMX or _EnableStrobe was off; check those in your shader, as the example above does
GetImmediateStrobeOutput(ch) VRSLDMX_Strobe(ch)
GetDMXColor(ch) Not the same function. The old one read ch, ch + 1 and ch + 2 with no dimmer: write half3(VRSLDMX_Channel(ch), VRSLDMX_Channel(ch + 1), VRSLDMX_Channel(ch + 2)). VRSLDMX_Color takes the fixture's first channel and applies the dimmer
ConvertToRawDMXChannel(chan, universe) Not needed: the fixture component writes the absolute channel

What doesn't carry over:

  • Texture arguments and the _Udon_DMXGrid* textures. The manager publishes its own grid globals and the reads find them; a material no longer points at a grid texture. Under Stream data there is no grid at all, and the reads still work.
  • _EnableCompatibilityMode and _EnableVerticalMode. The manager's DMX Input chooses the layout for every shader at once.
  • The Props instancing buffer and the getters on it (isDMX(), getGlobalIntensity() and the rest). Declare the properties you read in your own UnityPerMaterial.
  • The IF macro. Use the ternary operator.
  • Pan and tilt (GetPanValue, GetTiltValue, calculateRotations) and ReadDMXRaw on the timer textures. Movement and the spin and strobe phases aren't part of the include. A shader with its own movement channels reads them as plain values with VRSLDMX_Channel.

A shader that worked out cells on the grid itself, or applied its own gamma step to a read, gets the package's decode instead: every channel from its own cell, as the package's fixtures read it. Expect small differences from the old maths.

Grid Textures and the raw grid

The raw read reads the Raw slot of the manager's Grid Textures asset, the grid as it arrives from the video. The package's own asset fills it for all three layouts. In a Grid Textures asset of your own, fill Raw with the render texture your video lands in; left empty, the raw read gets the smoothed values instead. A camera that renders into a Raw texture gets no VRSL lighting, as one rendering into the other grid textures doesn't.