URP feedback request: Projects setting global shader variables via script

Hi everyone :slight_smile:

we’re improving URP internals by moving some shader globals into persistent constant buffers to improve consistency/performance.

As part of this, we want to understand real-world usage where projects set URP internal global shader variables from script (for example via Shader.SetGlobal*, CommandBuffer.SetGlobal*, Material.Set*, MaterialPropertyBlock, etc.).

If you rely on this pattern, could you briefly share your use case?

  • Which variable(s) you set (if known)
  • Which API you use
  • What feature/effect it enables in your project

Here is a list of variables we want to put in persistent Constant Buffers. We are also considering supported URP helpers to override some of them when needed.

// AdditionalLights CBUFFER
float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsColor[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsAttenuation[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsSpotDir[MAX_VISIBLE_LIGHTS];
half4 _AdditionalLightsOcclusionProbes[MAX_VISIBLE_LIGHTS];
float4 _AdditionalLightsLayerMasks[MAX_VISIBLE_LIGHTS];

// ReflectionProbe CBUFFER
float4 urp_ReflProbes_BoxMax[MAX_REFLECTION_PROBES];          // w contains the blend distance
float4 urp_ReflProbes_BoxMin[MAX_REFLECTION_PROBES];          // w contains the importance
float4 urp_ReflProbes_ProbePosition[MAX_REFLECTION_PROBES];   // w is positive for box projection, |w| is max mip level
float4 urp_ReflProbes_MipScaleOffset[MAX_REFLECTION_PROBES * 7];
float4 urp_ReflProbes_Rotation[MAX_REFLECTION_PROBES];

// LightShadows CBUFFER
float4x4 _AdditionalLightsWorldToShadow[MAX_VISIBLE_LIGHTS];  // Per-shadow-slice-data
float4   _AdditionalShadowParams[MAX_VISIBLE_LIGHTS];  

// LightCookies CBUFFER
float4x4 _AdditionalLightsWorldToLights[MAX_VISIBLE_LIGHTS];
float4 _AdditionalLightsLightTypes[MAX_VISIBLE_LIGHTS];
float4 _AdditionalLightsCookieAtlasUVRects[MAX_VISIBLE_LIGHTS]; // (xy: uv size, zw: uv offset)
float4 _AdditionalLightsCookieEnableBits[(MAX_VISIBLE_LIGHTS + 31) / 32];

// UnityInput.hlsl - present on native engine side
float4x4 unity_WorldToCamera;
float4x4 unity_CameraToWorld;
float4x4 unity_MatrixInvV;
float4x4 unity_MatrixV;
float4x4 _PrevViewProjMatrix;
float4x4 _NonJitteredViewProjMatrix;
float4x4 glstate_matrix_projection;
float4x4 unity_MatrixInvP;
float4x4 unity_MatrixVP;
float4x4 unity_MatrixInvVP;
float4x4 _MainLightWorldToLight;
float4 _Time;
float4 _SinTime;
float4 _CosTime;
float4 unity_DeltaTime;
float4 _TimeParameters;
float4 _LastTimeParameters;
float4 _ScreenParams;
float4 _ZBufferParams;
float4 unity_OrthoParams;
float4 _RTHandleScale;
float4 _ScaleBiasRt;
float4 _ProjectionParams;
float4 _ScreenSize;

// Input.hlsl and other includes - URP only 
float4 _ScaledScreenParams;
float4 _ScreenSizeOverride;
float4 _ScreenCoordScaleBias;
float4 unity_BillboardNormal;
float4 unity_BillboardTangent;
float4 unity_BillboardCameraParams;
float4 _MainLightPosition;
float4 _FPParams0;
float4 _FPParams1;
float4 _FPParams2;
float4 _WorldSpaceCameraPosition;
float4x4 _MainLightWorldToShadow[5];
float4 _CascadeShadowSplitSpheres0;
float4 _CascadeShadowSplitSpheres1;
float4 _CascadeShadowSplitSpheres2;
float4 _CascadeShadowSplitSpheres3;
float4 _CascadeShadowSplitSphereRadii;
float4 _MainLightShadowOffset0;
float4 _MainLightShadowOffset1;
float4 _MainLightShadowParams;
float4 _MainLightShadowmapSize;
float4 _AdditionalShadowOffset0;
float4 _AdditionalShadowOffset1;
float4 _AdditionalShadowFadeParams;
float4 _AdditionalShadowmapSize;
half4 _GlossyEnvironmentColor;
half4 _SubtractiveShadowColor;
half4 _GlossyEnvironmentCubeMap_HDR;
half4 _MainLightColor;
half4 _MainLightOcclusionProbes;
float2 _GlobalMipBias;
float _MainLightCookieTextureFormat;
float _AdditionalLightsCookieAtlasTextureFormat;
uint _MainLightLayerMask;
uint _RenderingLayerMaxInt;
uint _EnableProbeVolumes;  

// XR
float4x4 unity_StereoMatrixP[2];
float4x4 unity_StereoMatrixInvP[2];
float4x4 unity_StereoMatrixV[2];
float4x4 unity_StereoMatrixInvV[2];
float4x4 unity_StereoMatrixVP[2];
float4x4 unity_StereoMatrixInvVP[2];
float4x4 unity_StereoCameraProjection[2];
float4x4 unity_StereoCameraInvProjection[2];
float4x4 _PrevViewProjMatrixStereo[2];
float4x4 _NonJitteredViewProjMatrixStereo[2];
float4x4 _ViewProjMatrixStereo[2];
float4 unity_StereoWorldSpaceCameraPos[2];

We’re collecting this to help with this transition and document the right paths or find alternatives for your use cases.

The impact is only related if someone would override these internal shader constants. Reading these Unity provided values in your shaders is fine and is not changed.

Thanks a lot for any examples.

Best,
Oliver

7 Likes

Would this impact how we write custom shaders when using them ? I use a lot of those variables in a fair few custom shaders, though i never change their values.

1 Like

Already spoke with Adrien in the Slack channel but we also use them and not change them in some custom render passes.

2 Likes

The impact is only related if someone would override these internal shader constants. Reading these Unity provided values in your shaders is fine and is not changed.

(I added this sentence now also to the original message, so that no one misses this important detail. Thanks for asking :slight_smile: )

1 Like

I use them a lot for value that should apply to everything in the scene.
Mainly via Shader.SetGlobalFloat / Integer / Vector / Texture / Color.

I use them to set ambiant lightning (color) for custom Stylized Lightning , Player position (vector4) for things like water or invisible walls that appear when going near them. I use them for impact like shock waves via a global position, radius and intensity. I also use global texture to set tinting color to my shader to set a mood that affect all objects.
Some of thoses need to update every frame, while other just once.

When a system needs to set multiple value at once I try to package everything into arrays of vectors4 and use SetGlobalVectorArray, to reduce the number of set/get calls.

But to clarify, you set your own global variables, and not the Unity / URP internal variables from the list above, right? Because that is the thing, that we basically change them to “read-only”. But the functionality itself to set other (global) variables would be unchanged.

Exactly. I’d say the only one that I WOULD have though to modify was the _Time. I had a case where I was playing with slowMotion and I needed a unscaledTime value in my shader. But I just defaulted to set my own custom variable.

We use this code to modify the view matrix for our VR binoculars implementation, not sure whether this modifies any of the listed variables under the hood.

m_vrCamera = cameraData.camera;
Matrix4x4 viewLeft = cameraData.GetViewMatrix(0);
Matrix4x4 viewRight = cameraData.GetViewMatrix(1);
Matrix4x4 projLeft = cameraData.GetProjectionMatrix(0);
Matrix4x4 projRight = cameraData.GetProjectionMatrix(1);

projLeft = Matrix4x4.Translate(new Vector2(asymmetricFrustumParams.x - 0.5f, VRBinocularsData.CamOffsetV)) * projLeft;
projRight = Matrix4x4.Translate(new Vector2(asymmetricFrustumParams.z - 0.5f, VRBinocularsData.CamOffsetV)) * projRight;

XRBuiltinShaderConstants.UpdateBuiltinShaderConstants(viewLeft, projLeft, true, 0);
XRBuiltinShaderConstants.UpdateBuiltinShaderConstants(viewRight, projRight, true, 1);
XRBuiltinShaderConstants.SetBuiltinShaderConstants(cmd);

Of course developers need to override these variables.

Do you even realize that, in many cases, overriding URP/global shader variables is the only way to fix bugs that exist in Unity 6 LTS / Supported releases?
A recent issue we encountered was related to urp_TileData access errors. The root cause was that when rendering directly to a TargetTexture without BlitToBackBuffer, _ScaledScreenParams did not match the actual RenderTexture size.
The only viable workaround was overriding _ScaledScreenParams manually in the middle of the RenderPass pipeline.
You can investigate the exact version details yourselves.

From my experience, nearly every situation where we were forced to modify URP/global shader variables was caused by URP bugs. Not because developers wanted to rely on internal implementation details.
If Unity blocks these access paths while continuing to ship LTS / Supported releases with insufficient QA validation, a lot of capable developers are going to suffer because of it.

If Unity genuinely wants to improve engine performance, then the team seriously needs actual game production experience before making decisions like this.
People without real production experience cannot realistically understand what kinds of performance problems and engine limitations developers are actually dealing with in shipped games.

7 Likes

while Ive not had these issues, there are other engines who provide the code and if you need you can go customise it.. I respect that its your code unity, but, sometimes we need the tweaks for our stuff.. as not all of us should need to pay you more than i earn a year to get the code out of you to do so. Now sure, many of the packages we can make copies and tweaks, but then need to remake those tweaks each time

Maybe it’s relevant how much performance etc this saves. If it’s 1% already for all games, that’s massive. If it’s just 0.001%, I am not too sure how much that helps with reduced flexibility.
Of course people can always modify URP to have control over this

On the flip side, why would you not want your render pipeline to be customisable?

1 Like

I’m a developer behind the asset from Asset Store: Impostors - Runtime Optimization (Impostors - Runtime Optimization | Utilities Tools | Unity Asset Store)

And for my asset to function it is essential to be able to override some of these global variables to do custom rendering commands.
What my plugin does: it makes snapshots of scene objects and then renders these snapshots when objects are far away from the camera. Snapshot creation happens at RUNTIME, it is not baked in the editor.

What variables are overridden using CommandBuffer:

  • _MainLightShadowParams
  • _AdditionalLightsCount
  • _MainLightPosition
  • _MainLightColor
  • _MainLightLayerMask
  • unity_LightData
  • _WorldSpaceCameraPos
  • _ProjectionParams

Used in combination with CommandBuffer.DrawRenderer

2 Likes

I am the author of Dynamic Lighting a modernized Lighting System of Unreal Gold / Tournament (1996-1999) for Unity. It’s running on the built-in render pipeline.

When I tried to port my system to URP, I ran into the issue that there’s a presumption that a render feature only wants to keep a global texture assigned:

using (var builder = renderGraph.AddRasterRenderPass<PassData>("MyPass", out var passData))
{
    // Set a texture to the global texture
    builder.SetGlobalTextureAfterPass(texture, globalTextureID);
}

But my lighting system does not rely on textures, it’s running on Compute Buffers. I need to assign a global compute buffer and render the scene normally with forward rendering:

// Set a buffer to the global buffer
builder.SetGlobalComputeBufferAfterPass(buffer, globalBufferID);

This function does not exist. In fact, it appears that even Shader.SetGlobalComputeBuffer (and their URP equivalents) have no effect during the built-in forward rendering that occurs.

There are also many presumptions that people wish to use the default light sources. There’s a lot of work done behind the scenes binning them, work that my lighting system wouldn’t need performed whatsoever. I consistently run into these issues that BiRP never had. But the lack of buffer support is most important.

1 Like

Hey all, thanks a lot for providing feedback! This is super useful to us.

As I lead this initiative on engineering side, I can provide additional context.

By moving these global variables into persistent constant buffers, we will reduce main CPU cost in some key areas. For instance, we will speed up shader setup that happens before every draw call or SRP batch. We currently make constant buffers on the fly by gathering these global variables during the setup of the shaders that use them. By having persistent constant buffers, we will fill them once on C# side, and then only bind them during shader setup. This optimization will benefit every URP users.

As mentioned, by setting these variables into persistent constant buffers, you cannot modify them individually through the usual APIs (Shader.SetGlobal*, CommandBuffer.SetGlobal*, Material.Set*, MaterialPropertyBlock, etc.). But we are considering providing new public URP helpers so you can 1/ modify some of these variables (URP will push a custom version of the constant buffer for your pass behind the scenes) and 2/ reset to the default URP constant buffers when needed (at the end of your pass for instance, cheap switch of bindings behind the scenes). We are also considering a transition period with persistent constant buffers as an opt-in feature.

The current discussion helps us prioritizing things and make sure we are on the right track so again, thanks a lot for the feedback. Keep it up!

Hey @MUGIK, that’s a cool asset, well done. Thanks for providing the list of vars that you modify. So you set these vars with custom values using Material APIs, then call cmd.DrawRenderer with your material? I assume within a Render Graph pass? So you are probably using RasterCommandBuffer or UnsafeCommandBuffer?

Hey @PatHightreeXVR, this is fine. XRBuiltinShaderConstants modify the global vars behind the scenes using cmd.SetGlobal APIs, but I think we can switch it to a constant buffer workflow without any impact on your side.

1 Like

Hi @Henry00IS, thanks for your feedback. Global resources are not optimal with Render Graph where we aim at optimizing memory usage by doing intra-frame memory aliasing, pass culling based on resource usage and so on. Global texture API was brought to facilitate users transition to Render Graph, but this is not a recommended workflow if you can do without, hence the current absence of SetGlobalComputeBufferAfterPass().

Could you have a look at this sample and let me know if it would work for you? The sample is for textures, but the idea would be to add a custom contextItem in frameData where you would store your computeBuffer handle bufferHandle in pass A and then you could call builder.UseBuffer(bufferHandle) in pass B. No global involved.

Shouldn’t this be added already in the main post? It is pretty important to me (and i think to us) to already know than there will also be a way to modify them even with this change, even if it would not be as easy as before. The way the post talk about it seemed like there were no ways of working on it from the outside.

1 Like