I'm trying to draw a grid around the cursor that represents buildable area.

this cursor grid, aligns to the global grid. all building placements must be on the grid and so this visual queue must aid in understanding that.

I also will have deforming terrain.

here's what I got so far, this is a grid drawing method that uses GL.Lines :

(by the way I'm using the Cartesian Coordinate system so that's why I iterated over x and y instead of x and z don't let that throw you off, y is not 3D height in the following)

private void drawSize50Grid(int entryX, int entryY)
    if (!mat)
        Debug.LogError("Please Assign a material on the inspector");
    int amountOfGridToDraw = 50;


    float currentX = entryX;
    float currentY = entryY;

    for (int x = 0; x < amountOfGridToDraw; x++)
        for (int y = 0; y < amountOfGridToDraw; y++)
            //divisionFactor is float which makes the grid the size I want (smaller than the unity visual queue when you have nothing in a scene)
            float nextX = divisionFactor/100f + currentX;
            float nextY = divisionFactor/100f + currentY;

                           // fixed height of 0.0001f, this is temporary, I'll pass height when I get there. 
            var point = new Vector3(currentX, 0.0001f, currentY);
            var drawPoint2 = new Vector3(nextX, 0.0001f, currentY);
            var drawPoint3 = new Vector3(currentX, 0.0001f, nextY);

            if (currentY != amountOfGridToDraw)
                currentY += divisionFactor/100f;

        currentY = 0f;
        currentX += divisionFactor/100f;

here's my point conversion methods :

public Vector3 GetNearestPointOn3DGrid(Vector3 position)
    position -= transform.position;

    Vector3 result = new Vector3(
        // width
        // length

    result += transform.position;

    return result;

private float magicFormula (float entryPoint)
    float decimalPart = entryPoint % 1;
    float intergerPart = entryPoint - decimalPart;
    return Mathf.RoundToInt(decimalPart / (1 / divisionFactor)) * (1 / divisionFactor) + intergerPart;

If I call my grid in this way :

void OnRenderObject()
     drawSize50Grid(100f, 100f);

Then, when I run the game, my grid shows up starting at (LeftHand coordinate system) point 100f,0.0001f, 100f.

But OnRenderObject() is called only once.

This lifecycle will not allow me to have a grid that follows the cursor.

Also it seems that this is a GL.Line hard limit : they cannot be redrawn at runtime.

I realize drawing and redrawing tons of segments as the mouse zooms around is bound to be taxing to the CPU & GPU but I've tampered my expectations :

// I guess I would call drawGridAroundCursor() inside Update()?
// right now doing this does nothing because GL.Lines refuse to draw after OnRenderObject()
public void drawGridAroundCursor()
    if (camera.GetCurrentDistanceToGround() < 60f && isShiftCurrentlyHeldDown) 
        gridAdjustedCursor = GetNearestPointOn3DGrid(cursorRayHit);
        drawSize50Grid(gridAdjustedCursor.x, gridAdjustedCursor.z)
  • I'm only doing 50x50 grid (that's 4950 lines) (actually I'm willing to go down further than that 15x15 might cut it)
  • I'm only rendering the grid if the camera is close to the ground (where it shows up)
  • and only showing it when holding down shift.

that should limit the load.

But if there's even more things I can do to optimize I'm open to ideas, so long as it doesn't involve sacrificing correct placement of the lines.

here's what my above code WOULD look like if it rendered at real time : (camera viewing straight down at the terrain, also not at all to scale, the cursor is GIGANTIC in this mock-up) grid following cursor

And in the end here's what I'm going to do (draw from -25 -25 relative to grid-adjusted to cursorRayHit and chose a more and more transparent color the further the point are to the center of the grid) : circle with fading edges

so obviously when the cursor moves the faded out borders follow it around but the grid points will stay in place (and go transparent when the cursor is too far away)

This effect was inspired by this : windows border effect

And after I get to that step I want to add height variation to match the terrain and then add color input per cell of the grid depending on whether or not the terrain is constructible or not.

So the final solution would look something like this :

with terrain deformation and colors.

  • 1
    \$\begingroup\$ Can you show us an image of what you want this to look like? We might be able to achieve that appearance, with correct snapping, without an object per line. \$\endgroup\$ – DMGregory May 14 at 16:09
  • 1
    \$\begingroup\$ Can you record a gif of the Windows 10 effect you want to imitate, and embed it in your question? \$\endgroup\$ – DMGregory May 14 at 16:14
  • \$\begingroup\$ @DMGregory I added a gif now. does that help? \$\endgroup\$ – tatsu May 14 at 17:32

When this question was just about drawing a plain grid of lines, I said...

I'd be tempted to draw this with a shader on a single quad, using the world coordinates of the vertices to snap the lines to your desired intervals, no matter if the parent quad is moving in sub-grid increments.

Animated example moving a grid around the scene view

Here's a quick shader that does this, with parameters to control the exact density of the grid lines, their offset relative to the world origin, their thickness and colour. You can control the size of the fade-out just by scaling the quad that you draw it on.

Shader "Unlit/WorldGrid"
        _ScaleShift ("Lines per Unit & Shift", Vector) = (10, 10, 0, 0)
        _Color ("Colour", Color) = (1, 0, 0, 1)
        _Thickness ("Thickness", float) = 0.5
        Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
        LOD 100

        Lighting Off
        Blend SrcAlpha OneMinusSrcAlpha

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;

            struct v2f
                float2 uv : TEXCOORD0;
                float2 worldPos : TEXCOORD1;
                float4 vertex : SV_POSITION;

            float4 _ScaleShift;
            fixed4 _Color;
            float _Thickness;

            v2f vert (appdata v)
                v2f o;

                o.vertex = UnityObjectToClipPos(v.vertex);

                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.uv = 2.0f * (v.uv - 0.5f);
                o.worldPos = worldPos.xz * _ScaleShift.xy + _ScaleShift.zw;
                return o;

            fixed4 frag (v2f i) : SV_Target
                float2 distanceFromLine = abs(frac(i.worldPos + 0.5f) - 0.5f);
                float2 speedPerPixel = fwidth(i.worldPos);

                float2 pixelsFromLine = abs(distanceFromLine / speedPerPixel);

                float opacity = 1.0f - saturate(min(pixelsFromLine.x, pixelsFromLine.y) - 0.5f * (_Thickness - 0.5f));

                float radiusSquared = dot(i.uv, i.uv);
                float falloff = max(1.0f - radiusSquared, 0.0f);

                fixed4 col = _Color;
                col.a *= opacity * falloff * falloff;

                return col;

But now that I have your full requirements:

  • Deforms to fit terrain
  • LWRP/URP Shader Graph
  • Colour-coded cells based on buildable/non-buildable sites

...well, it's doable:

Animated grid example conforming to terrain, with red cells for obstructions

But we need something more complicated:

A graph of terrible awe

So let's unpack that, starting with a bit of prep work:

  • In your camera's settings, set "Depth Texture" to "On" - that will force the camera to render the scene's depth, which we'll need for the terrain-hugging decal effect.

  • Create a cube that we'll use for drawing the effect. Make it as wide in each direction as the amount of grid you want to show, and tall enough to span changes in terrain height.

  • Create a new Unlit shader graph, and a material to put it on.

  • Set your Unlit Master node to use Additive blending if you want glowy lines like in the gif above.

  • Set up shader parameters for...

    • Grid Density (Vector 2) - how many grid cells per world unit (here I used 5, 5)

    • Grid Offset (Vector 2) - how much to shift the grid relative to Unity's grid (I used -0.5, -0.5)

    • Thickness (Vector 1) - how thick to draw the lines (0 for hairline, 1 for a bit bolder)

    • Buildable Mask (Texture 2D) - a texture as large as your building grid, containing 0/black for obstructed cells, and 1/white for open cells. You can update this in your scripts with SetPixel/SetPixels or blit quads into it as a RenderTexture to keep it up to date as your buildings change.

    • Buildable Colour / Blocked Colour (Colour) - the colours to draw the lines.

Now let's attack the graph in pieces:

Finding our place in the world

World Decal Position & Map to Grid Coordinates groups

We start by taking a ray from the camera, through our screen pixel, and extending it as far as the stored depth in the camera's depth texture. That tells us the world-space coordinates of the terrain surface behind this part of the cube.

Then we take the xz components of this world position, and scale and shift them by our grid density/offset to convert into our building coordinate space.

Fading Away

Grid Lines & Radial Fade groups

The top section does the same math from the text fragment shader above, taking the building-space point, working out how far this pixel is from the nearest grid edge, and turning that into a brightness/opacity value for our line according to our thickness parameter.

The bottom section takes the worldspace point (before conversion to building space) and transforms it by the inverse of our model matrix to get it into the cube's local coordinates. Then we fade it out radially so you don't see the edges of the cube.

We multiply these two effects to get the resulting brightness/opacity of the line.

A place to stand, a place to grow...

Closest Cell, Closest Neighbour Cell, Buildable Mask Texture Space, and Buildable/Non-Buildable Color groups

The top section takes our building-space position and rounds it to an integer, so all pixels in the square sample the same building mask tile (technically point filtering should do this too, but I have sometimes found discrepancies between the hardware filter's rounding and what I get in the shader, so I like to err on the side of caution)

We then divide this by the size of our building mask texture to get a texture coordinate inside the building mask, and sample from that to find out whether we should draw this tile as green/buildable, or red/blocked.

If we just left it there though, we'd get these ugly/speckly/half-and-half lines where a buildable cell borders a blocked cell. To fix that, the group on the bottom figures out which edge we're closest to, then steps across that edge to sample the neighbouring cell on that side. We'll draw this edge red if either this cell or our closest neighbour is non-buildable.

Note that the lines headed to that Vector2 node on the bottom cross-over. This means we're doing step(pixelsFromEdge.xy, pixelsFromEdge.yx) - so we get (1, 0) if we're closest to an edge between cells with an x separation, and (0, 1) if we're closest to an edge between cells with a y (z) separation.

By default, this will want to draw over all opaque content in your scene, but you can use Custom Render Queue setting to wedge it between your terrain and your buildings. I found setting the grid to render in queue 2501 (AlphaTest+51), and my buildings to render in 2502+ did the trick. If you're using a built-in shader, you may need to create your own material that references it, then put the Inspector in Debug mode to see the Custom Render Queue field:

Debug inspector setting custom render queue

| improve this answer | |
  • \$\begingroup\$ omg! you're nuts! thank you so much! I was in the process of trying to at least do the color part but I was having trouble figuring out the right format of the object or array to pass. big problems. You're an absolute hero! OMG!! \$\endgroup\$ – tatsu May 17 at 20:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.