Can anyone give me a quick rundown of the basics of a handwritten URP shader vs In-built?

Hi guys I am trying to learn how to write shaders by hand for URP but lack of documentation and varying info all over the place is confusing me.

As an exercise to learn this I am trying to convert this shader from minionsart:

Shader "Unlit/SimpleWater"
{
    Properties
    {
        _Tint("Tint", Color) = (1, 1, 1, .5)
        _MainTex ("Main Texture", 2D) = "white" {}
        _NoiseTex("Extra Wave Noise", 2D) = "white" {}
        _Speed("Wave Speed", Range(0,1)) = 0.5
        _Amount("Wave Amount", Range(0,1)) = 0.5
        _Height("Wave Height", Range(0,1)) = 0.5
        _Foam("Foamline Thickness", Range(0,3)) = 0.5
     
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"  "Queue" = "Transparent" }
        LOD 100
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
         
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float4 scrPos : TEXCOORD1;//
            };
            float4 _Tint;
            uniform sampler2D _CameraDepthTexture; //Depth Texture
            sampler2D _MainTex, _NoiseTex;//
            float4 _MainTex_ST;
            float _Speed, _Amount, _Height, _Foam;//
         
            v2f vert (appdata v)
            {
                v2f o;
                float4 tex = tex2Dlod(_NoiseTex, float4(v.uv.xy, 0, 0));//extra noise tex
                v.vertex.y += sin(_Time.z * _Speed + (v.vertex.x * v.vertex.z * _Amount * tex)) * _Height;//movement
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.scrPos = ComputeScreenPos(o.vertex); // grab position on screen
                UNITY_TRANSFER_FOG(o,o.vertex);
             
                return o;
            }
         
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
         
                half4 col = tex2D(_MainTex, i.uv) * _Tint;// texture times tint;
                half depth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos))); // depth
                half4 foamLine =1 - saturate(_Foam * (depth - i.scrPos.w));// foam line by comparing depth and screenposition
                col += foamLine * _Tint; // add the foam line and tint to the texture
                return col ;
            }
            ENDCG
        }
    }
}

However I am struggling to find decent info on what is required to get this to work for URP. In another thread here someone explained how I can get an alternative to SAMPLE_DEPTH_TEXTURE_PROJ and UNITY_PROJ_COORD

But right now I am having trouble working out what includes, pragma etc statements I need, where in general everything is located (as in which hlsl files contain the useful stuff) as well as how to set up the appdata and v2f structs properly as none of that compiles.

Right now I am trial and erroring it so if anyone has any bits of knowledge I would appreciate it and I am certain this will also be useful to other users too!

TLDR

Whats the minimum I need to get this working (or at very least compiling) with URP?

What parts of this cannot be converted at all, and what alternatives do I have other than ripping those bits out (I am sure the fog bits wont work for example)?

I am happy to work some stuff out on my own, but I definately need to be pointed in the right direction as all the varying info flying around has me super confused

Thanks for any help you can provide!

1 Like

Unfortunately, its not exactly trivial to upgrade shaders to the new pipelines. Not only is a different shading language used, but most of the common macros and functions that were previously commonplace in shaders no longer exist. Thankfully, your shader doesn’t interact with lighting - so we don’t actually need to change much;

Shader "Unlit/SimpleWater"
{
    Properties
    {
        _Tint("Tint", Color) = (1, 1, 1, .5)
        _MainTex ("Main Texture", 2D) = "white" {}
        _NoiseTex("Extra Wave Noise", 2D) = "white" {}
        _Speed("Wave Speed", Range(0,1)) = 0.5
        _Amount("Wave Amount", Range(0,1)) = 0.5
        _Height("Wave Height", Range(0,1)) = 0.5
        _Foam("Foamline Thickness", Range(0,3)) = 0.5
        
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"  "Queue" = "Transparent" }
        LOD 100
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            Name "Unlit"

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float fogCoords : TEXCOORD1;
                float4 scrPos : TEXCOORD2;//
            };

            float4 _Tint;
            Texture2D _CameraDepthTexture; SamplerState sampler_CameraDepthTexture; //Depth Texture
            Texture2D _MainTex, _NoiseTex; SamplerState sampler_MainTex;//
            float4 _MainTex_ST;
            float _Speed, _Amount, _Height, _Foam;//
           
            //This is a replacement for the old 'UnityObjectToClipPos()'
            float4 ObjectToClipPos (float3 pos)
            {
                return mul (UNITY_MATRIX_VP, mul (UNITY_MATRIX_M, float4 (pos,1)));
            }

            v2f vert (appdata v)
            {
                v2f o;
                float4 tex = _NoiseTex.SampleLevel (sampler_MainTex, v.uv.xy, 0);//extra noise tex
                v.vertex.y += sin (_Time.z * _Speed + (v.vertex.x * v.vertex.z * _Amount * tex)) * _Height;//movement

                VertexPositionInputs vertInputs = GetVertexPositionInputs (v.vertex.xyz);    //This function calculates all the relative spaces of the objects vertices
                o.vertex = vertInputs.positionCS;
                //o.vertex = ObjectToClipPos (v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.scrPos = vertInputs.positionNDC; // grab position on screen
                o.fogCoords = ComputeFogFactor (o.vertex.z);
                
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
            
                half4 col = _MainTex.Sample (sampler_MainTex, i.uv) * _Tint;// texture times tint;
                half depth = LinearEyeDepth (_CameraDepthTexture.Sample (sampler_CameraDepthTexture, i.scrPos.xy / i.scrPos.w).r); // depth
                half4 foamLine = 1 - saturate (_Foam * (depth - i.scrPos.w));// foam line by comparing depth and screenposition
                col += foamLine * _Tint; // add the foam line and tint to the texture
                return MixFog (col, i.fogCoords);
            }
            ENDHLSL
        }
    }
}

A few things to note - aside from some of the internal functions and practices changing, there are two key things to getting a shader to work with the new pipelines. Number 1 (and probably the most important) is naming your pass. SRPs no longer render by whole materials; instead they only render specific passes, and to make sure unsupported shaders aren’t rendered, the pipeline will only render passes with specific names - one of which is “Unlit”. The other key thing is the switch from CG to HLSL. It won’t look like much has changed (because both languages have the same syntax), but they aren’t identical. As you can see, the old include files no longer work, so you’ll need to use the HLSL includes from the SRP package. Probably the main thing you’ll notice though is that textures now work differently. Textures come in two parts - the texture and the sampler that stores information about how to draw that texture. In CG, texture objects came with these two forcibly combined, so you didn’t need to worry about it. However, in HLSL, you have to declare sampler states separately and sample the textures using them. This does mean that you can reuse sampler states, as I have between _MainTex and _NoiseTex.

There might be some errors here; I don’t use the new pipelines, so I wrote this entirely off the github shaders, but it should work.

11 Likes

Who says so? My CGish shaders works perfectly fine with LWRP.

CG is still supported, but the new frameworks are based entirely around HLSL and that is what Unity is pushing everyone to switch over to, not to mention that the core libraries and shaders are all written in HLSL and often rely on HLSL functionality that isn’t part of CG (like separate sampler-states).

1 Like

As far I know, Unity don’t use CG nor HLSL, just CG/HLSL ish syntax, so forcing to other “ish” sounds kinda nonsensical.
Did Unity Technologies make recommendations about migrating to HLSL ish mode?

Separate sampler states is present in CGPROGRAM as well, the key is that you need to be handling the platform differences in definition and thus would have to wrap the declaration in the appropriate platform defines that use that syntax. There were several macros for handling platform differences in separate sampler states, like UNITY_DECLARE_TEX2D(_Name); which on most platforms would define a Texture2D and SamplerState property in your CG shader, this syntax works fine in CG. Or UNITY_DECLARE_TEX2D_NOSAMPLER(_Name); for just a texture and no sampler. UNITY_SAMPLE_TEX2D_SAMPLER(_tex, _sampler, uv) to sample using separate texture and sampler.

Unity has simply changed the naming of these macros for the render pipelines. It’s now just TEXTURE2D(_Name) for just a texture and SAMPLER(_name) for just a sampler, or define both at once with TEXTURE2D_PARAM(_texName, _sampleName), among many other macros.
You can find the main Macros in com.unity.render-pipelines.core\ShaderLibrary\API\D3D11.hlsl, there’s a different .hlsl file in that directory for each platform that has different results needed for the macros.

6 Likes

Ah right. I’m not formally trained on this stuff, but I remember Unity used to always push for using DX9 style ‘sampler2D’ declarations over the separated methods, so I misinterpreted that as a language difference between CG and HLSL.

Cg is not supported, and hasn’t been for several years. I believe Unity 4.7 was the last version of Unity to support Cg, and then only when compiling to desktop OpenGL. GLES and DirectX platforms stopped supporting Cg long before that.

The confusion is Cg and Direct3D 9 HLSL have nearly identical syntax and built in functions, and the letters “cg” show up so often in Unity’s shaders. I say nearly identical because there are a handful of functions that they do not share. For example, Cg does not have tex2Dgrad, but instead has an overload of tex2D that has the same functionality. However CGPROGRAM and HLSLPROGRAM are identical, as are .cginc and .hlsl files, and are compiled as Direct3D 11 or 12 HLSL (which also happily accepts Direct3D 9 HLSL). They just never had a chance to make a clean break with past versions of Unity, where all those occurrences of “cg” were common, until the SRPs.

Some other confusion comes from Unity’s heavy reliance on macros to bridge the gap between different APIs where the cross compiling tools they use couldn’t quite keep up. The SRPs continue this, with things like texture uniforms and sample functions being wrapped in macros that write out forms supported by the current API. Aiming for Direct3D11? Texture2D, SamplerState, and tex.Sample(sampleState, uv). GLES? sampler2D and tex2D(tex, uv).

Humorously though, they still use the term “fragment shader”, which dates back to Unity’s original OpenGL only origins. Both Cg and HLSL refer to it as a “pixel shader”.

10 Likes

Guys thanks so much for all the informative replies, not only do I feel I have a much better handle on how to start translating my existing shaders, but I also have a better handle on where to look to find existing macros for the new pipeline etc!

Thanks a bunch!

I’d like to add that you can right click on the master nodes of the shader graph to get the generated code, which can be quite useful if you want to get all the boiler plate code, or want to figure out how to access some of the vertex data etc. The problem is that it generates a ridiculously large file that you’ll have to decipher.

1 Like

I really wish there was a converter or even a Unity backed guide for this! That or I just learn more of Shader Graph to recreate my effects… yeah probably that!

Quick update, so far any old school built-in or custom shader that is unlit I had seems to work with Universal RP on Scriptable Render Pipeline when I follow the guide here to enable GPU instancing support. Maybe I just found an unexpected way of converting old shaders but they all work!

Just follow the steps here: Unity - Manual: Single-pass instanced rendering and custom shaders

Let me know if this works for any of you!

Thank you , how do I remove lighting from custom shaders…

What kind of custom shader? You’ve written your own? Did you copy code from a URP shader? Or just trying to make an existing shader you have not receive lighting? If it’s an old shader that’s still working in URP it shouldn’t really be receiving lighting since it would be a basic vert/frag forward shader and the old lighting macros shouldn’t be compatible…
My main advice would be to simply look at the code the URP’s Unlit shader uses and adapt your code to it.

Post the shader you’re trying to modify and we may be able to help you.

2 Likes
Shader "Custom/TilemapShader" {
    Properties {      
        _MainTex ("Map", 2D) = "white" {}
        _Tileset("Tileset", 2D) = "white" {}
        _Size("Map & Tileset Size", Vector) = (16, 16, 20, 13)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _Tileset;

        struct Input {
            float2 uv_MainTex;
        };

        float4 _Size;
        // _Size.xy is the size of the map in tiles.
        // _Size.zw is the size of the tileset in tiles.

        void surf (Input IN, inout SurfaceOutputStandard o) {

            // Scale up the uvs into tile space.
            float2 mapLocation = IN.uv_MainTex * _Size.xy;

            // Get this pixel's position within the tile we're drawing.
            float2 inTileUV = frac(mapLocation);

            // Clamp the texel we read from.
            // ("Point" filtering does this too, but its math is slightly
            // different, which can show up as a shimmer between tiles)
            mapLocation = (mapLocation - inTileUV) / _Size.xy;

            // Slightly inset tiles so they don't bleed at the edges.
            inTileUV = inTileUV * 126.0f / 128.0f + 1.0f / 128.0f;

            // Calculate texture sampling gradients using original UV.
            // Otherwise jumps at tile borders make the hardware apply filtering
            // that lets adjactent tiles bleed in at the edges.
            float4 grad = float4(ddx(IN.uv_MainTex), ddy(IN.uv_MainTex));

            // Read our map texture to determine what tiles go here.
            float4 tileIndex = tex2D(_MainTex, mapLocation);

            // Generate UV offsets into our tileset texture.
            tileIndex = (tileIndex * 255.0f + inTileUV.xyxy )/_Size.zwzw ;

            // Sample two tiles from our tileset: one base tile and one overlay.
            // (Hey, we have room for two indices, might as well use it!)
            fixed4 base = tex2Dgrad(_Tileset, tileIndex.xy, grad.xy, grad.zw);
            fixed4 over = tex2Dgrad(_Tileset, tileIndex.zw, grad.xy, grad.zw);

            // Combine overlaid tile with base using standard alpha blending.
            fixed4 c = lerp(base, over, over.a);

            // Put all this into terms the Unity lighting model understands.
            o.Albedo = c.rgb;         
            o.Metallic = 0.0f;
            o.Smoothness = 0.0f;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Here it is, thank you for your help

This shader was made in 2017, and it doesn’t work with the URP, I think it must be because it uses lighting. So if I can remove the lighting from it out should be ok.
What it does is turns a colour map into a Tileset

Replied you above

Here it is converted to URP, adapted from URP Unlit shader. Let me know if there’s an issue with it.

Shader "Custom/TilemapShader"
{
    Properties
    {
        _MainTex("Map", 2D) = "white" {}
        [NoScaleOffset]_Tileset("Tileset", 2D) = "white" {}
        _Size("Map & Tileset Size", Vector) = (16, 16, 20, 13)
       
        // BlendMode
        [HideInInspector] _Surface("__surface", Float) = 0.0
        [HideInInspector] _Blend("__blend", Float) = 0.0
        [HideInInspector] _AlphaClip("__clip", Float) = 0.0
        [HideInInspector] _SrcBlend("Src", Float) = 1.0
        [HideInInspector] _DstBlend("Dst", Float) = 0.0
        [HideInInspector] _ZWrite("ZWrite", Float) = 1.0
        [HideInInspector] _Cull("__cull", Float) = 2.0

        // Editmode props
        [HideInInspector] _QueueOffset("Queue offset", Float) = 0.0
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "IgnoreProjector" = "True" "RenderPipeline" = "UniversalPipeline" }
        LOD 100

        Blend [_SrcBlend][_DstBlend]
        ZWrite [_ZWrite]
        Cull [_Cull]

        Pass
        {
            Name "Unlit"
            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x

            #pragma vertex vert
            #pragma fragment frag

            // -------------------------------------
            // Unity defined keywords
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"

            TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex);
            TEXTURE2D(_Tileset); SAMPLER(sampler_Tileset);
           
            CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                float4 _Size;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS       : POSITION;
                float2 uv               : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Varyings
            {
                float2 uv        : TEXCOORD0;
                float4 vertex : SV_POSITION;

                UNITY_VERTEX_INPUT_INSTANCE_ID
                UNITY_VERTEX_OUTPUT_STEREO
            };

            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;

                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_TRANSFER_INSTANCE_ID(input, output);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

                VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
                output.vertex = vertexInput.positionCS;
                output.uv = TRANSFORM_TEX(input.uv, _MainTex);

                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_INSTANCE_ID(input);
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                // Scale up the uvs into tile space.
                float2 mapLocation = input.uv * _Size.xy;
                // Get this pixel's position within the tile we're drawing.
                float2 inTileUV = frac(mapLocation);

                // Clamp the texel we read from.
                // ("Point" filtering does this too, but its math is slightly
                // different, which can show up as a shimmer between tiles)
                mapLocation = (mapLocation - inTileUV) / _Size.xy;
    
                // Slightly inset tiles so they don't bleed at the edges.
                inTileUV = inTileUV * 126.0f / 128.0f + 1.0f / 128.0f;
    
                // Calculate texture sampling gradients using original UV.
                // Otherwise jumps at tile borders make the hardware apply filtering
                // that lets adjactent tiles bleed in at the edges.
                float4 grad = float4(ddx(input.uv), ddy(input.uv));
    
                // Read our map texture to determine what tiles go here.
                float4 tileIndex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, mapLocation);
    
                // Generate UV offsets into our tileset texture.
                tileIndex = (tileIndex * 255.0f + inTileUV.xyxy )/_Size.zwzw ;
    
                // Sample two tiles from our tileset: one base tile and one overlay.
                // (Hey, we have room for two indices, might as well use it!)
                half4 base = SAMPLE_TEXTURE2D_GRAD(_Tileset, sampler_Tileset, tileIndex.xy, grad.xy, grad.zw);
                half4 over = SAMPLE_TEXTURE2D_GRAD(_Tileset, sampler_Tileset, tileIndex.zw, grad.xy, grad.zw);
    
                // Combine overlaid tile with base using standard alpha blending.
                return lerp(base, over, over.a);
            }
            ENDHLSL
        }
        Pass
        {
            Tags{"LightMode" = "DepthOnly"}

            ZWrite On
            ColorMask 0

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            #pragma vertex DepthOnlyVertex
            #pragma fragment DepthOnlyFragment

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
            ENDHLSL
        }
    }
    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}

Thank you so much, I am on 2019.2.21 on lightweight render pipeline I thought it is the same as the UDP I get this two errors

Yeah, lightweight was renamed universal in 2019.3, so some of the references are different, could you upgrade?

1 Like

I’d love to upgrade to enjoy the new unity Ui, but when I do I have such an annoying bug on the editor that makes unity editor useless. So I want to stay in 2019.2. 20 for a while if possible as I wasted crazy time trying to solve that bug…
This is the bug https://discussions.unity.com/t/774486