Unity runs on various platforms, and in some cases there are differences in how things behave. Most of the time Unity hides the differences from you, but sometimes issues can occur.
Vertical texture coordinate conventions differ between Direct3D-like and OpenGL-like platforms:
Most of the time this does not really matter, except when rendering into a Render Texture. In that case, Unity internally flips rendering upside down when rendering into a texture on non-OpenGL, so that the conventions match between the platforms. Two common cases where this needs to be handled in the shaders are image effects and rendering things in UV space.
One case where the above “on non-OpenGL, render upside down when rendering into a texture” does not happen, is when Image Effects and anti-aliasing is used. In this case, Unity renders to screen to get anti-aliasing, and then “resolves” rendering into a RenderTexture for further processing with an Image Effect. The resulting source texture for an image effect is not flipped upside down on Direct3D/Metal (unlike all other Render Textures).
If your Image Effect is a simple one (processing one texture at a time) then this does not really matter, because Graphics.Blit takes care of that.
However, if you’re processing more than one RenderTexture together in your Image Effect, they are likely to come out at different vertical orientations (only in Direct3D-like platforms, and only when anti-aliasing is used). You need to manually “flip” the screen texture upside down in your vertex shader, like this:
// On non-GL when AA is used, the main texture and scene depth texture
// will come out in different vertical orientations.
// So flip sampling of the texture when that is the case (main texture
// texel size will have negative Y).
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
uv.y = 1-uv.y;
#endif
Refer to the Edge Detection scene in the Shader Replacement sample project for an example of this. Edge detection there uses both the screen texture and the Camera’s Depth+Normals texture.
When rendering in texture coordinate (UV) space for special effects or
tools, you might need to adjust your shaders so that the rendering is
consistent between D3D-like and OpenGL-like systems, and between rendering into the
screen vs. rendering into a texture. The built-in variable _ProjectionParams.x
contains a +1 or –1 value which indicates whether projection has been flipped upside down or not. You can check this value in your shaders if you need to do different things based on this.
float4 vert(float2 uv : TEXCOORD0) : SV_POSITION
{
float4 pos;
pos.xy = uv;
// we're rendering with upside-down flipped projection,
// so flip the vertical UV coordinate too
if (_ProjectionParams.x < 0)
pos.y = 1 - pos.y;
pos.z = 0;
pos.w = 1;
return pos;
}
Just like with texture coordinates, the clip space coordinates (also known as post-projection space coordinates) differ between Direct3D-like and OpenGL-like platforms:
Inside shader code, you can use UNITY_NEAR_CLIP_VALUE
macro to get the near plane value based on platform.
Inside script code, GL.GetGPUProjectionMatrix can be used to convert from Unity’s coordinate system (which follows OpenGL conventions) to what the platform expects.
PC GPUs treat all floating point types (float
, half
and
fixed
) as the same, and do all calculations using full 32 bit
precision. Many mobile GPUs do not do this, so make
sure to test your shaders on the target platform to avoid precision issues.
See the data types and precision page
for details.
Similarly, all samplers/textures declared in shader code default to “low precision”. This does not matter for PC GPUs, but on some mobile GPUs you might want to use sampler2D_half
(declares half-precision texture), sampler2D_float
(full precision texture) etc.
In HLSL const
has much the same meaning as it does in C# and C++, such that the variable declared is read-only within its scope, but can be initialised in any way.
In GLSL however, const
means that the variable is effectively a compile time constant, and so must be initialised with compile time constants - either literal values, or calculations on other constants.
It is best to follow the GLSL semantics and only declare a variable as const
when it is truly invariant, and avoid initialising a const
variable with some other mutable values e.g. as a local variable in a function. This will work just fine in HLSL and avoid confusing errors on only some platforms.
In HLSL const
has much the same meaning as it does in C# and C++, such that the variable declared is read-only within its scope, but can be initialised in any way.
In GLSL however, const
means that the variable is effectively a compile time constant, and so must be initialised with compile time constants - either literal values, or calculations on other constants.
It is best to follow the GLSL semantics and only declare a variable as const
when it is truly invariant, and avoid initialising a const
variable with some other mutable values e.g. as a local variable in a function. This will work just fine in HLSL and avoid confusing errors on only some platforms.
To get shaders working on all platforms, some special shader values should use these semantics:
SV_POSITION
. Sometimes shaders use POSITION
semantics for that, but this does not not work on Sony PS4 or when tessellation is used.SV_Target
. Sometimes shaders use COLOR
or COLOR0
for that, but again this does not work on PS4.When rendering meshes as Points, make sure to output PSIZE
semantics from the vertex shader (for example, set it to 1). Some platforms, such as OpenGL ES or Metal, treat point size as “undefined” when it’s not written to from the shader.
See the shader semantics page for more details.
Direct3D platforms use Microsoft’s HLSL shader compiler. The HLSL compiler is stricter than other compilers about various subtle shader errors. For example, it doesn’t accept function output values that aren’t initialized properly.
The most common places you might run into this are:
void vert (inout appdata_full v, out Input o)
{
**UNITY_INITIALIZE_OUTPUT(Input,o);**
// ...
}
float4
, but the code only sets the .xyz values of it. Make sure to set all values, or change to float3
if you only need three values.tex2D
in the vertex shader. This is not valid, as UV derivatives don’t exist in the vertex shader. You need to sample an explicit mip level instead; for example, use tex2Dlod (tex, float4(uv,0,0))
. You also need to add #pragma target 3.0
, as tex2Dlod
is a shader model 3.0 feature.Some parts of the surface shader compilation pipeline do not understand DX11-specific HLSL syntax. If you’re using HLSL features like StructuredBuffers, RWTextures and other non-DX9 syntax, wrap them in a DX11-only preprocessor macro:
#ifdef SHADER_API_D3D11
// DX11-specific code, e.g.
StructuredBuffer<float4> myColors;
RWTexture2D<float4> myRandomWriteTexture;
#endif
Some GPUs (most notably PowerVR based ones on iOS) allow you to do a form of programmable blending by providing current fragment color as input to the fragment shader (see EXT_shader_framebuffer_fetch).
It is possible to write shaders in Unity that use the framebuffer fetch functionality. When writing HLSL/Cg fragment shader, simply use the inout
color argument in it. For example:
CGPROGRAM
// only compile shader for platforms that can potentially
// do it (currently gles,gles3,metal)
#pragma only_renderers framebufferfetch
void frag (v2f i, inout half4 ocol : SV_Target)
{
// ocol can be read (current framebuffer color)
// and written into (will change color to that one)
// ...
}
ENDCG