Prototype Grid Shader for Unity URP

A URP prototype grid shader for Unity 6: a lit, world-space grid for level blockouts that stays one metre apart however you scale objects, with major lines and shadows.

Also called: grid material, blockout shader, greybox material, triplanar grid, dev texture.

Prototyping URP, Unity 6000.0+ beginner MIT licensed

Use it for: Greyboxing levels, testing jump distances and scale, and any placeholder geometry before real art exists.

Inspector Try the properties
Shader Scripts For Unity/Prototype Grid
PrototypeGrid Drag to rotate

The preview is a WebGL copy of the shader's maths with one fixed light, so it shows no shadows, Bloom or textures.

What it does

  • World-space grid: stretch or scale a cube and the cells stay the same size
  • Projected from all three axes and blended by the surface direction, so walls, floors and slopes all get lines without UVs
  • Major lines every few cells in their own colour for reading distances
  • Anti-aliased lines that stay crisp at a distance
  • Receives and casts shadows, with depth and normals passes

How it works

Each pixel divides its world position by the cell size and measures how far it is from the nearest whole number on each axis, which is its distance to a grid line. Screen-space derivatives widen the edge of each line by one pixel, so lines anti-alias instead of flickering.

Because there are no UVs, the grid is projected along X, Y and Z separately and the three are blended by how much the surface faces each axis. Major lines are the same grid at a larger cell size.

Setup

  1. Save the file as PrototypeGrid.shader under Assets and create a material that uses Scripts For Unity > Prototype Grid.
  2. Put it on your blockout geometry: cubes, ProBuilder shapes or imported greybox meshes.
  3. Leave Cell Size at 1 to read distances in metres, and set Major Line Every to 4 or 5.

Common mistakes

  • The grid slides over a moving object: it is fixed to the world, which is what you want for level geometry. For moving props, use a texture with UVs instead.
  • Lines look blurry on very large surfaces at grazing angles: raise Line Width slightly; very thin lines lose contrast once they are thinner than a pixel.

Questions

Does it need UVs?
No. The grid is projected from world position along X, Y and Z and blended by the surface direction, so it works on any mesh with normals.
Does it work with ProBuilder?
Yes. ProBuilder meshes are ordinary meshes with normals; assign the material like any other.
Can I change the grid size?
Set Cell Size in metres. Major Line Every sets how many cells sit between the stronger lines.

Source

PrototypeGrid.shader · 264 lines · ShaderLab and HLSL for the Universal Render Pipeline 17 (Unity 6)

PrototypeGrid.shader
cpp
// Prototype Grid: a lit, world-space grid for level blockouts. Lines stay
// one metre apart however you scale or stretch the object, with stronger
// major lines every few cells. URP 17 (Unity 6). Main directional light only.
// Scripts For Unity, MIT licensed.
Shader "Scripts For Unity/Prototype Grid"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (0.42, 0.44, 0.48, 1)
        _LineColor ("Line Color", Color) = (0.72, 0.74, 0.78, 1)
        _MajorLineColor ("Major Line Color", Color) = (0.95, 0.62, 0.25, 1)
        _CellSize ("Cell Size (m)", Range(0.1, 10)) = 1
        _LineWidth ("Line Width", Range(0.001, 0.25)) = 0.03
        _MajorEvery ("Major Line Every", Range(1, 16)) = 4
        _BlendSharpness ("Side Blend Sharpness", Range(1, 16)) = 6
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Opaque"
            "Queue" = "Geometry"
            "RenderPipeline" = "UniversalPipeline"
        }

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

        CBUFFER_START(UnityPerMaterial)
            half4 _BaseColor;
            half4 _LineColor;
            half4 _MajorLineColor;
            float _CellSize;
            float _LineWidth;
            float _MajorEvery;
            float _BlendSharpness;
        CBUFFER_END
        ENDHLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }
            Cull Back

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #pragma multi_compile_fog
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float fogFactor : TEXCOORD2;
            };

            float4 SfuShadowCoord(float3 positionWS)
            {
            #if defined(_MAIN_LIGHT_SHADOWS_SCREEN)
                return ComputeScreenPos(TransformWorldToHClip(positionWS));
            #else
                return TransformWorldToShadowCoord(positionWS);
            #endif
            }

            // 1 on a grid line, 0 between lines, anti-aliased with screen-space derivatives
            float SfuGridLines(float2 p, float width)
            {
                float2 fw = max(fwidth(p), 0.0001);
                float2 distanceToLine = abs(frac(p + 0.5) - 0.5);
                float2 lines = 1.0 - smoothstep(width * 0.5 - fw, width * 0.5 + fw, distanceToLine);
                return max(lines.x, lines.y);
            }

            // Projects the grid from the three world axes and blends by the surface normal
            float SfuGrid(float3 positionWS, float3 normalWS, float cellSize, float width)
            {
                float3 weights = pow(abs(normalWS), _BlendSharpness);
                weights /= max(weights.x + weights.y + weights.z, 0.0001);
                float3 p = positionWS / cellSize;
                return SfuGridLines(p.yz, width) * weights.x
                     + SfuGridLines(p.xz, width) * weights.y
                     + SfuGridLines(p.xy, width) * weights.z;
            }

            Varyings vert(Attributes input)
            {
                Varyings output;
                VertexPositionInputs positions = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = positions.positionCS;
                output.positionWS = positions.positionWS;
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                output.fogFactor = ComputeFogFactor(positions.positionCS.z);
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                float3 normalWS = normalize(input.normalWS);
                float majorEvery = max(round(_MajorEvery), 1.0);
                float minorLines = SfuGrid(input.positionWS, normalWS, _CellSize, _LineWidth);
                float majorLines = SfuGrid(input.positionWS, normalWS, _CellSize * majorEvery, _LineWidth * 1.5 / majorEvery);

                float3 albedo = lerp(_BaseColor.rgb, _LineColor.rgb, minorLines);
                albedo = lerp(albedo, _MajorLineColor.rgb, majorLines);

                Light mainLight = GetMainLight(SfuShadowCoord(input.positionWS));
                float diffuse = saturate(dot(normalWS, mainLight.direction)) * mainLight.shadowAttenuation;
                float3 color = albedo * (mainLight.color * diffuse + SampleSH(normalWS));

                return half4(MixFog(color, input.fogFactor), 1.0);
            }
            ENDHLSL
        }

        // Casts shadows from the main and additional lights
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }
            ZWrite On
            ZTest LEqual
            ColorMask 0
            Cull Back

            HLSLPROGRAM
            #pragma vertex ShadowVert
            #pragma fragment ShadowFrag
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

            float3 _LightDirection;
            float3 _LightPosition;

            struct ShadowAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct ShadowVaryings
            {
                float4 positionCS : SV_POSITION;
            };

            ShadowVaryings ShadowVert(ShadowAttributes input)
            {
                ShadowVaryings output;
                float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
            #if _CASTING_PUNCTUAL_LIGHT_SHADOW
                float3 lightDirectionWS = normalize(_LightPosition - positionWS);
            #else
                float3 lightDirectionWS = _LightDirection;
            #endif
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
            #if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                output.positionCS = positionCS;
                return output;
            }

            half4 ShadowFrag(ShadowVaryings input) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }

        // Depth prepass, used by depth priming and the depth texture
        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly" }
            ZWrite On
            ColorMask R
            Cull Back

            HLSLPROGRAM
            #pragma vertex DepthVert
            #pragma fragment DepthFrag

            struct DepthAttributes
            {
                float4 positionOS : POSITION;
            };

            struct DepthVaryings
            {
                float4 positionCS : SV_POSITION;
            };

            DepthVaryings DepthVert(DepthAttributes input)
            {
                DepthVaryings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                return output;
            }

            half DepthFrag(DepthVaryings input) : SV_Target
            {
                return input.positionCS.z;
            }
            ENDHLSL
        }

        // Normals for SSAO and other effects that read the normals texture
        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode" = "DepthNormals" }
            ZWrite On
            Cull Back

            HLSLPROGRAM
            #pragma vertex NormalsVert
            #pragma fragment NormalsFrag

            struct NormalsAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct NormalsVaryings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
            };

            NormalsVaryings NormalsVert(NormalsAttributes input)
            {
                NormalsVaryings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                return output;
            }

            half4 NormalsFrag(NormalsVaryings input) : SV_Target
            {
                return half4(NormalizeNormalPerPixel(input.normalWS), 0.0);
            }
            ENDHLSL
        }
    }

    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}

More shaders