I am trying to implement a fill style on a 2D rectangle object.

Hi,

Our company is busy moving one of our projects from a WPF application to a Unity 2D application.
some of the functionality we a are working on is to implement a fill colour, pattern, and pattern colour on a rectangle object.

We have 7 different patterns which we implement.

  • Crosshatch

  • SquareHatch

  • Forward diagonal

  • Backward Diagonal

  • Vertical line

  • Horizontal Line

  • dotted.

the images provided are from the WPF application which we build the patterns using the .net class StreamGeometry.

194712-crosshatchpattern.png

194713-squarehatch.png

As I am new to unity are there any built-in classes that behave similarly to StreamGeometry class?
what would the best way of doing this in code?

are there any built-in classes that behave similarly to StreamGeometry class?

No, not really as Unity is a game engine. Most of the patterns you talk about come from the GDI brushes. However you can create your own texture, make it tilable and just set your UV coordinates accordingly and you should get the same effect.

Of course another option is to write a shader that produces the desired pattern. For example through a signed distance field (SDF) It’s just a matter of getting fancy with math ^^.

Just as an example, about a week ago I made a quick and dirty (about) to scale diagram of our sun-earth-moon system in order to see the actual umbra and preumbra of the earth shadow. The whole thing is just a single fullscreen “RawImage” with this shader:

Shader "Unlit/SolarSystemShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _SunPos("SunPos", Vector) = (0,0,0,1)
        _SunSize("SunSize", float) = 140
        _EarthPos("EarthPos", Vector) = (15000,0,0,1)
        _EarthSize("EarthSize", float) = 1.2
        _MoonPos("MoonPos", Vector) = (15038.44,0,0,1)
        _MoonSize("MoonSize", float) = 0.3474
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        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;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _SunPos;
            float _SunSize;
            float4 _EarthPos;
            float _EarthSize;
            float4 _MoonPos;
            float _MoonSize;

            float3 _CamPos;
            float _Aspect;
            float _Scale;


            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 drawCircle(float3 uv, float3 pos, float4 col, float size)
            {
                pos = uv - (pos - _CamPos)/_Scale;
                size /= _Scale*2;
                float d = sqrt(pos.x * pos.x + pos.y * pos.y);
                d = smoothstep(1-size, 1-size + 0.003, 1-d);
                return d * col;
            }
            fixed4 drawLine(float3 uv, float3 pos, float3 normal, float4 col, float size)
            {
                pos = uv-(pos - _CamPos) / _Scale;
                float d = abs(dot(pos, normal));
                d = smoothstep(1-size, 1-size + 0.001, 1-d);
                return d * col*0.2;
            }
            fixed4 drawLineRepeat(float3 uv, float3 pos, float3 normal, float4 col, float size)
            {
                pos = uv + _CamPos /_Scale;
                pos.x = frac(pos.x/100 * _Scale)*100/_Scale;
                float d = abs(dot(pos, normal));
                d = smoothstep(1 - size, 1 - size + 0.00001, 1 - d);
                d *= smoothstep(0.001, 0.002, 0.1-abs(pos.y));
                return d * col * 0.2;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                float3 uv = float3(i.uv-0.5,0);
                uv.x *= _Aspect;
                fixed4 col = drawCircle(uv, _SunPos.xyz, float4(1, 1, 0.5, 1), _SunSize);
                col += drawCircle(uv, _EarthPos.xyz, float4(0.5, 0.5, 1, 1), _EarthSize);
                col += drawCircle(uv, _MoonPos.xyz, float4(0.5, 0.5, 0.5, 1), _MoonSize);
                col += drawLine(uv, _SunPos.xyz, float3(1, 0, 0), float4(1, 0.5, 0.5, 0.1), 0.002);
                col += drawLine(uv, _SunPos.xyz, float3(0, 1, 0), float4(1, 0.5, 0.5, 0.1), 0.002);
                col += drawLine(uv, _EarthPos.xyz, float3(1, 0, 0), float4(1, 0.5, 0.5, 0.1), 0.002);
                col += drawLine(uv, _MoonPos.xyz, float3(1, 0, 0), float4(1, 0.5, 0.5, 0.1), 0.002);


                float3 p1 = float3(0, _SunSize*0.5 , 0);
                float3 p2 = float3(_EarthPos.x, _EarthSize * 0.5, 0);
                float3 n = normalize(cross(p1-p2, float3(0,0,1)));
                col += drawLine(uv, p1, n, float4(0, 1, 0, 0.1), 0.002);
                col += drawLine(uv, p1, float3(0, 1, 0), float4(0.5, 0.5, 1, 0.1), 0.002);
                n.y = -n.y;
                p1.y = -p1.y;
                col += drawLine(uv, p1, n, float4(0, 1, 0, 0.1), 0.002);
                col += drawLine(uv, p1, float3(0, 1, 0), float4(0.5, 0.5, 1, 0.1), 0.002);
                n = normalize(cross(p1 - p2, float3(0, 0, 1)));
                col += drawLine(uv, p1, n, float4(1, 1, 0, 0.1), 0.002);
                n.y = -n.y;
                p1.y = -p1.y;
                col += drawLine(uv, p1, n, float4(1, 1, 0, 0.1), 0.002);

                float3 p = 0;
                col += drawLineRepeat(uv, p, float3(1,0,0), float4(1, 1, 0, 0.1), 0.002);
                
                return col;
            }
            ENDCG
        }
    }
}

Besides that there is this simply script for the interaction. All it does is setting the “vitual” camera position and scale in the shader so you can pan around and zoom in and out. Though the whole diagram is just a single fullscreen UI.RawImage that does not move in screenspace.

Here’s the script that does the panning and zooming:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExecuteInEditMode]
public class SolarSystem : MonoBehaviour
{
    public Material mat;
    public Vector3 camPos;
    public float z = 1f;

    float aspect;

    void Update()
    {
        if (mat == null)
            return;
        aspect = Screen.width / (float)Screen.height;
        mat.SetVector("_CamPos", camPos);
        mat.SetFloat("_Aspect", aspect);
        mat.SetFloat("_Scale", z);

    }
    private void OnGUI()
    {
        var screenSizeInv = new Vector3(1f / Screen.height, -1f / Screen.height, 1);
        Event e = Event.current;
        var mPos = Vector3.Scale(e.mousePosition, screenSizeInv) - new Vector3(0.5f*aspect, -0.5f, 0);
        GUILayout.BeginVertical("box");
        float dist = (camPos.x + mPos.x * z)*10000;
        GUILayout.Label("dist from sun: " + (dist.ToString("000,000,000km",System.Globalization.CultureInfo.InvariantCulture)));
        GUILayout.Label("blue lines: horizontal to sun

green lines: tangents of sun and earth
yellow lines: crossing tangents");
GUILayout.EndVertical();
if (e.isMouse && Input.GetMouseButton(0))
{
var d = new Vector3(e.delta.x * screenSizeInv.x, e.delta.y * screenSizeInv.y);
camPos -= d*z;
}
else if (e.type == EventType.ScrollWheel)
{
var delta = (camPos + mPos * z);
if (e.delta.y <0)
z /= 1.1f;
else
z = 1.1f;
z = Mathf.Clamp(z, 0.00001f, 20000f);
camPos = (delta - mPos
z);
}
camPos.y = Mathf.Clamp(camPos.y, -150, 150);
camPos.x = Mathf.Clamp(camPos.x, -150, 15100);
}
}

Note I just posted this as an example. In your case what you’re most likely interested in is the “drawLineRepeat” approach which draws the repeated yellow markers every 1 million kilometers. The “scale” of one unit (when we talk about the camera position and the position of the earth and moon) is given in one unit == 10000km. In your case it probably makes more sense to write a surface shader and use the screenspace position of a fragment, though it depends on your exact needs. The shader examples page has some great examples which may help in case you want to go down this route.

Though using an actual texture and let it tile across a quad may be enough for your usecase. Though we don’t know enough about your exact usecase so it’s quite difficult to recommend a particular method.

I have managed to do the easier ones of the patterns, horizontal, vertical, and squarehatch patterns. using the UILineRenderer from the unity-ui-extensions.
by creating a number of vector pairs representing the lines and using a line list.

by doing the following for the horizontal.

private Dictionary<int, Vector2[]> BuildHorizontalHatchPattern(Vector3 startPoint, Vector3 endPoint)
        {
            var returnValue = new Dictionary<int, Vector2[]>();
            var size = new Vector2(Mathf.Abs(startPoint.x - endPoint.x),
                Mathf.Abs(startPoint.y - endPoint.y));
            var lineStartPoint = new Vector2(0, LineSpacing);
            var lineEndPoint = new Vector2(size.x - LineLengthOffset, LineSpacing);
            var verticalLineCount = size.y / LineSpacing;

            for (var i = 0; i < verticalLineCount; i++)
            {
                if (!returnValue.ContainsKey(i))
                {
                    returnValue.Add(i, new []{lineStartPoint, lineEndPoint});
                }

                lineStartPoint += new Vector2(0, LineSpacing);
                lineEndPoint += new Vector2(0, LineSpacing);
            }
            return returnValue;
        }

and this for the Vertical.

 private Dictionary<int, Vector2[]> BuildVerticalHatchPattern(Vector3 startPoint, Vector3 endPoint)
        {
            var returnValue = new Dictionary<int, Vector2[]>();
            var size = new Vector2(Mathf.Abs(startPoint.x - endPoint.x),
                Mathf.Abs(startPoint.y - endPoint.y));
            var lineStartPoint = new Vector2(LineSpacing, 0);
            var lineEndPoint = new Vector2(LineSpacing, size.y - LineLengthOffset);
            var horizontalLineCount = size.x / LineSpacing;

            for (var i = 0; i < horizontalLineCount; i++)
            {
                if (!returnValue.ContainsKey(i))
                {
                    returnValue.Add(i, new []{lineStartPoint, lineEndPoint});
                }

                lineStartPoint += new Vector2(LineSpacing, 0);
                lineEndPoint += new Vector2(LineSpacing, 0);
            }
            return returnValue;
        }

and this for the squarehatch.

private Dictionary<int, Vector2[]> BuildSquareHatchPattern(Vector3 startPoint, Vector3 endPoint)
        {
            var returnValue = new Dictionary<int, Vector2[]>();
            var size = new Vector2(Mathf.Abs(startPoint.x - endPoint.x),
                Mathf.Abs(startPoint.y - endPoint.y));
            var lineStartPoint = new Vector2(LineSpacing, 0);
            var lineEndPoint = new Vector2(LineSpacing, size.y - LineLengthOffset);
            var horizontalLineCount = size.x / LineSpacing;

            for (var i = 0; i < horizontalLineCount; i++)
            {
                if (!returnValue.ContainsKey(i))
                {
                    returnValue.Add(i, new []{lineStartPoint, lineEndPoint});
                }

                lineStartPoint += new Vector2(LineSpacing, 0);
                lineEndPoint += new Vector2(LineSpacing, 0);
            }
            
            lineStartPoint = new Vector2(0, LineSpacing);
            lineEndPoint = new Vector2(size.x - LineLengthOffset, LineSpacing);
            var verticalLineCount = size.y / LineSpacing;

            for (var i = returnValue.Count; i < horizontalLineCount + verticalLineCount; i++)
            {
                if (!returnValue.ContainsKey(i))
                {
                    returnValue.Add(i, new[] { lineStartPoint, lineEndPoint });
                }

                lineStartPoint += new Vector2(0, LineSpacing);
                lineEndPoint += new Vector2(0, LineSpacing);
            }

            return returnValue;
        }

and update the UILineRenderer to do the following.

private void GetFillStyleCoordinates(FillStyles fillPatternType, Vector3 startPoint, Vector3 endPoint)
    {
        var pointDictionary = _fillsStyleProvider.GetFillStyle(fillPatternType, startPoint, endPoint);
        if (pointDictionary == null) return;
        
        var lineSegments = new List<Vector2[]>();
        var linePoints = new List<Vector2>();
        for (var i = 0; i < pointDictionary.Count; i++)
        {
            var points = pointDictionary*;*

if (points == null)
{
continue;
}

linePoints.AddRange(points);
lineSegments.Add(points);
}

fillPatternLineRenderer.Points = linePoints.ToArray();
fillPatternLineRenderer.Segments = lineSegments;
}
Please note: this code is still a work in progress, and nowhere near complete.
However, I don’t really understand how to create a dotted pattern and a forward or backward diagonal line or the crosshatch which is the forward and backward diagonal patterns combined.
Could anybody help me with this? Please