outline by pushing vertex away by a normal.


I want to achieve a simple outline. you know: scale original object a bit and render it before the actual object. Standard stuff. But I want to do it by adding a normal (multiplied by a user defined factor ) to the mesh vertex position (pushing the vertex out in the normal direction), which should guarantee that the outline width will not depend on object size. unlike when you just scale vertex by a factor like 1.2 to make it a 20% bigger so the bigger objects have bigger outlines and in that case you have assume that the mesh origin is in the actual center. so my approach seems to be better, but it gives me the same frailty. the thick objects have thicker outline which i completely don’t understand. where’s the fault in my thinking?

                v2f o;
                float3 ver = v.vertex.xyz + v.normal.xyz * _OutlineWidth;
                o.vertex = UnityObjectToClipPos(ver);

Try to normalize push direction
float3 ver = v.vertex.xyz + normalize(v.normal.xyz) * _OutlineWidth;

it’s normalized by default, but i tried it just to be sure.

I suppose you use scaled objects? Try push in world space then. Instead of use UnityObjectToClipPos macros multiply vertex position with object to world matrix(you get positions in world space), transform normals to word space, push positions, then multiply by vp matrix.

1 Like

The vertex position you pass into the UnityObjectToClipPos() function is assumed to be in object space (hence the name of the function). Since it’s in object space, it’ll be affected by the object’s scale. So if you move the object space vertex by 1 unit along the object space normal, if the object is scaled 200% it’s now going to be 2 units when rendered.

What you need to do instead is make sure you’re moving the vertex position in world space. The UnityObjectToClipPos() function looks like this:

inline float4 UnityObjectToClipPos(in float3 pos)
{
    return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
}

There’s actually a little more in that function if you look at the source files, but that’s for special VR rendering that 99% of people will never use, so I removed it. The main two things is the two mul() calls and the matrices they’re using. One is transforming the object space position into world space, and the second is transforming the world space into clip space. If you want to know what clip space is, search online for homogeneous clip space and enjoy that rabbit hole, or search for some of my posts on this forum on the topic. TLDR, you can think of it as screen space for simplicity (but it’s totally not).

So for a world space outline, we can skip using the built in function and instead call each mul() separately and modify the world space before transforming to clip space.

float4 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0));
float3 worldNormal = UnityObjectToWorldNormal(v.normal);
worldPos.xyz += worldNormal * _OutlineWidth;
o.vertex = mul(UNITY_MATRIX_VP, worldPos);

Basically, what @mouurusai said.

2 Likes

Yeah! the scale of the objects was the issue!
Thank you both!

Hey! I need I dig into a topic from long ago…
But I try to do the same thing, I’m a newbie in shader coding… but until now it worked good enough ha ha -

I tried to implement the world normal solution to my shader… and met with a strange result!
8586814--1150831--upload_2022-11-15_9-22-1.png
Like it’s not totally bugged shader ? it have the outlines I expected, but …
Also I got the error :
UnityObjectToWorldNormal: no matching 1 parameter function
And also
invalid subscript 'normal'
Here is the outline area just for reference…

 Pass
            {
                Cull Front
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
                fixed4 _StrokeColor;
                float _StrokeWidth;
                struct appdata
                {
                    float4 vertex:POSITION;
                };
                struct v2f
                {
                    float4 clipPos:SV_POSITION;
                };
                v2f vert (appdata v)
                {
                    v2f o;
                   // o.clipPos=UnityObjectToClipPos(v.vertex*_StrokeWidth);
                   float4 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0));
                   float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                   worldPos.xyz += worldNormal * _StrokeWidth;
                   o.vertex = mul(UNITY_MATRIX_VP, worldPos);

                    ///return o;
                }
                fixed4 frag (v2f i) : SV_Target
                {
                    return _StrokeColor;
                }
                ENDCG
            }

If you could help, it would be super useful!
Thank you :smile:

Hi, that’s because there’s no variable called “normal” in “v” (struct appdata).

The information stored in a mesh’s vertices will be passed to the “appdata” structure. (select a mesh and see its properties in the inspector panel)

You need to add the normal to appdata (see how standard shader does this):

struct appdata
{
    float4 vertex : POSITION;
    half3 normal : NORMAL;
};

Hey again! thank you for your answer, however, now it says
invalid subscript 'vertex'
But it already declared in App data, no ? what’s going on ?

 Pass
            {
                Cull Front
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #include "UnityCG.cginc"
                fixed4 _StrokeColor;
                float _StrokeWidth;
                struct appdata
                {
                    float4 vertex:POSITION;
                    half3 normal : NORMAL;
                };
                struct v2f
                {
                    float4 clipPos:SV_POSITION;
                };
                v2f vert (appdata v)
                {
                   v2f o;
                   // o.clipPos=UnityObjectToClipPos(v.vertex*_StrokeWidth);
                   float4 worldPos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1.0));
                   float3 worldNormal = UnityObjectToWorldNormal(v.normal);
                   worldPos.xyz += worldNormal * _StrokeWidth;
                   o.vertex = mul(UNITY_MATRIX_VP, worldPos);
                    return o;
                }
                fixed4 frag (v2f i) : SV_Target
                {
                    return _StrokeColor;
                }
                ENDCG
            }

and if I switch to :
v.vertex = mul(UNITY_MATRIX_VP, worldPos);
I got

Output variable vert contains a system-interpreted value (SV_POSITION) which must be written in every execution path of the shader. Unconditional initialization may help.```
But I don't know where I put a condition for the initialisation of app data's vertex infos ..

Hi, so sorry for the late reply.

This error should be related to line 26.

“o” is a “v2f” structure, which does not contain any variable called “vertex”, so compiler throws an error.

What you need to do is changing from “o.vertex” to “o.clipPos”.

And I found that there’re some strange lines.

“worldPos” should be a float3 (instead of float4) containing coordinates of xyz-axes.

You can refer to the format in Shader Graph’s space transformation functions:
8588263--1151164--TransformObjectToWorld.jpg

I saw that the shader multiplies “float3/4 worldPos” with “float4x4 UNITY_MATRIX_VP” (World Space to Clip Space matrix).

(refer to Shader Graph’s functions)
For 3-component directions & 4x4 matrices, you need to:

newDirection = mul((float3x3)matrix, direction);

For 3-component positions & 4x4 matrices, you need to:

newPosition = mul(matrix, float4(position, 1.0));

This error told you that there’s no value in SV_POSITION (clipPos).

SV_POSITION is a must needed variable for fragment (pixel) shaders.

The x and y component (range from -1 to 1) represents the screen coordinate of this fragment (pixel).

The z component is the depth value of the current pixel.

The w component is related to projection.

Without this variable, the fragment shader won’t be able to render pixels.

1 Like

Thank you! Now it works, and I think I start to better understand how shader coding works :smile: !

1 Like