# How can I calculate vertex positions for a rectangle outline using a unit square, vertex normals, model matrix and outline thickness?

I'm trying to draw a hollow rectangle (i.e. the border of a rectangle without the middle) using a unit square plus normals. The actual positions are being calculated in my vertex shader.

For example, without worrying about the outline for a moment, the unit square positions multiplied by my model matrix gives:

0,0 ___ 1,0           10,10 ___ 60,10
|   |     becomes       |   |
|   |                   |   |
0,1 --- 1,1           10,60 --- 60,60


I think multiplying a normalised position by the scale to get the real size is reasonably well understood. The problem I am having is how to apply this same principle to producing an outline made of 8 triangles.

Assuming that the outline increases the size of the shape, I can find the inside points using the above approach. But the outer points are problematic and I can't quite figure out how to calculate them with the information I have available.

The formula without using a transformation matrix is relatively straightforward:

((vertex_position * scale + vertex_normal * outline_thickness) * rotation) + translation


I've made quite a few attempts at this, and I thought I could simply add the vertex_normal * thickness calculation to the scale components of the model matrix:

let ty = r_pc.info.x;
let thickness = r_pc.info.y;
let is_fill = select(false, true, ty >= 0.5 && ty < 1.5);
let is_outline = select(false, true, ty >= 1.5 && ty < 2.5);

var model = r_pc.model;
var position = vec4(vertex.position, 0.0, 1.0);
if is_outline {
// Add thickness to model's scale components.
var width = vertex.normal * thickness;
model[0][0] += width.x;
model[1][1] += width.y;
}
position = r_camera.view_proj * model * position;

let color = r_pc.color;
let uv = vertex.uv;

return VsOut(position, color, uv);


But that left me missing the top-left half of the outline!

I understand why that would be - it's a multiplication by 0. I think combining the transformations in the way I have done is not quite accurate. I believe I need to scale the point first, then add the translation along the normal, before applying the rotation and translation.

I don't know how to construct such a matrix when the only information I have available is:

• vertex position (normalised)
• vertex normal (or mitre joint)
• model matrix describing the rectangle (not the outline extent)
• outline thickness (not normalised)

Given this information, is it possible to calculate the position of the outer vertices and how? If not, what additional information do I need and how?

I have updated my Unity project which I think does what you want: https://github.com/paulsinnett/RectangleOutlineShader

As long as you stick to integer coordinates, scales, and widths, I think it should be solid.

Here is the new code to generate the geometry:

using UnityEngine;

public class OutlineBox : MonoBehaviour
{
void Start()
{
MeshFilter meshFilter = GetComponent<MeshFilter>();
Mesh mesh = new Mesh
{
name = "OutlineBox"
};
meshFilter.mesh = mesh;

mesh.vertices = new Vector3[]
{
new Vector3(0.0f, 1.0f, 0f),
new Vector3(1.0f, 1.0f, 0f),
new Vector3(1.0f, 0.0f, 0f),
new Vector3(0.0f, 0.0f, 0f),
new Vector3(0.0f, 1.0f, 0f),
new Vector3(1.0f, 1.0f, 0f),
new Vector3(1.0f, 0.0f, 0f),
new Vector3(0.0f, 0.0f, 0f)
};

mesh.uv = new Vector2[]
{
// note: in clip space y is upside down
new Vector2(-1.0f, -1.0f),
new Vector2(1.0f, -1.0f),
new Vector2(1.0f, 1.0f),
new Vector2(-1.0f, 1.0f),
new Vector2(0.0f, 0.0f),
new Vector2(0.0f, 0.0f),
new Vector2(0.0f, 0.0f),
new Vector2(0.0f, 0.0f)
};

mesh.triangles = new int[]
{
0, 1, 5,
0, 5, 4,
1, 2, 6,
1, 6, 5,
2, 3, 7,
2, 7, 6,
3, 0, 4,
3, 4, 7
};
}
}


Notes: I've switched to passing the mitre data into the uv channel because I think the normal data might get corrupted by vertex compression if its not length 1 data. Also the y directions are reversed as in Unity, the clip space is the other way up.

Here is the modified shader code:

Shader "Unlit/OutlineBoxShader"
{
Properties
{
_Width ("Outline Width", Range(0, 10)) = 1
}
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

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

struct v2f
{
float4 vertex : SV_POSITION;
};

float _Width;

v2f vert (appdata v)
{
v2f o;
float4 clip = UnityObjectToClipPos(v.vertex);
// in Unity clip space is -1 to 1 so pixels are twice the size
float2 pixelSize = 2 * (_ScreenParams.zw - float2(1, 1));
clip.xy += v.uv * _Width * pixelSize;
o.vertex = clip;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return fixed4(1, 1, 1, 1);
}
ENDCG
}
}
}


Notes: the $$\\times 2\$$ is needed to calculate the pixel size in Unity because in clip space the screen is -1 to 1, whereas the screen space it is 0 to 1.

• Thanks so much Paul, this is a great help. It does indeed give me an outline like I want, but I've noticed that the bottom right is always 1px thicker than the top left. If I set it to 1px thickness, the top left disappears entirely. Does this happen for you, or do you have any ideas why that might be? Commented Mar 15 at 6:11
• I do, which I think is due to the projection. You could try changing to orthographic projection. Is it a pixel perfect 1 pixel outline that you are trying to achieve?
– Paul
Commented Mar 15 at 10:59
• My projection is already orthographic. I am aiming for a pixel perfect outline - it's for a little 2d framework that I'm tinkering with. If I specify a border width, I'd like it to be that all the way around. Commented Mar 15 at 11:51
• Do you need it to be resolution independent, i.e. a 1 pixel border is one pixel at 1080 and still one pixel at 4K
– Paul
Commented Mar 15 at 14:13
• I suppose a better description would be 1 unit or virtual/logical pixel rather than physical pixel. Commented Mar 15 at 14:24