A look behind the scenes with VizionEck's outlines

Started by Legend, Nov 26, 2014, 09:43 PM

previous topic - next topic

0 Members and 1 Guest are viewing this topic.

Legend

Hi everybody,

Warning: this gets a bit technical

Originally the outlines in VizionEck were a separate mesh made by hand. I'd finish my level maps, and then edge by edge I'd extrude a rectangle until the full thing was covered.


(The large space between them is just illustrative)

The result looked great but as you'd imagine it took a lot of time. To fix that, I switched to a different method that calculates the outlines in real time.

The solution is a custom shader. Shaders for those of you that don't know, are responsible for drawing polygons to the screen. They handle everything from basic textures to complex bump maps. I determined it'd be much easier for my shader to make the outlines as the result of three scaled faces verse what I had been doing. Rendering the same geometry three times with slight variations is much quicker than generating completely new geometry and rendering it just once.



This looks exactly the same as outlines once all three layers are packed together.



So all the shader needs to do is render each face three times, each slightly smaller than before. To do this the shader moves each vertex along its tangent. If these tangents point towards the center of the face, then it scales the face. Pretty simple.

"//" means a comment. These lines are ignored by the system and are only there to make reading the code more user friendly.

Code: [Select]

Shader "Custom/Outlines" {
    Properties {
                //These four properties set up variables for materials using the shader. Colors are shown as four numbers representing red, blue, green, and alpha. Alpha is the transparency of the color. "float" is a non integer number, being shorthand for "floating point."
    _Color ("Outline Color", Color) = (1.0,1.0,1.0,1.0)
    _BColor("Background Color", Color) = (0.0,0.0,0.0,1.0)
    _OutlineD ("Outline Distance From Edge", float)=.3
    _OutlineW ("Outline Width", float)=.1
    }
    SubShader {
    Tags {}
               //A pass is what renders the polygon. Since I need to render it three times, I have three passes.
    Pass{
   
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
   
                //Here the color variable is defined for this pass
    fixed4 _BColor;
   
    //structs
    struct vertexInput {
    float4 vertex : POSITION;
    };
    struct vertexOutput {
    float4 pos : SV_POSITION;
    };
   
    //vertex function
    vertexOutput vert(vertexInput v){
    vertexOutput o;
   
                        //this converts the vertex positions from world space to screen space
    o.pos= mul(UNITY_MATRIX_MVP, v.vertex);
    return o;
    }
   
    //fragment function
    float4 frag(vertexOutput i) : COLOR
    {
                        //sets the pixel to the color
    return _BColor;
    }
   
    ENDCG
    }
                //second pass
    Pass{
   
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
   
   

    //material defined variables
    float _OutlineD;
    float _OutlineW;
    fixed4 _Color;
   
   
    //structs
    struct vertexInput {
    float4 vertex : POSITION;
    float3 tangent : TANGENT;
    };
    struct vertexOutput {
    float4 pos : SV_POSITION;
    float3 tangentDir : TEXCOORD0;
    };
   
    //vertex function
    vertexOutput vert(vertexInput v){
    vertexOutput o;
                        //offsets the vertices to make each polygon smaller, then converts to screen space, then decreases world space depth. This makes this pass render above the previous pass
    o.pos= mul(UNITY_MATRIX_MVP, v.vertex+(_OutlineD-_OutlineW)*float4(v.tangent, 0))-float4(0,0,.0001,0);
    return o;
    }
   
    //fragment function
    float4 frag(vertexOutput i) : COLOR
    {
    return _Color;
    }
   
    ENDCG
    }
    Pass{
   
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
   
   
    float _OutlineD;
    float _OutlineW;
    fixed4 _BColor;
   
   
   
    //structs
    struct vertexInput {
    float4 vertex : POSITION;
    float3 tangent : TANGENT;
    };
    struct vertexOutput {
    float4 pos : SV_POSITION;
    float3 tangentDir : TEXCOORD0;
    };
   
    //vertex function
    vertexOutput vert(vertexInput v){
    vertexOutput o;

    o.pos= mul(UNITY_MATRIX_MVP, v.vertex+(_OutlineD+_OutlineW)*float4(v.tangent, 0))-float4(0,0,.0002,0);
    return o;
    }
   
    //fragment function
    float4 frag(vertexOutput i) : COLOR
    {

    return _BColor;
    }
   
    ENDCG
    }
    }
    }



Although the shader is finished, this is the result it makes.




Kinda cool, but not correct. This is because the tangent values for the cube are not pointing towards the center of each face. To change this I have a custom import function that calculates tangents the way I need.

See, now it's perfect.




Let me know if you have any questions or need extra clarification.

-Michael Armbrust

Legend


Shinobi-san

I'd imagine this must have saved you a lot of time.
Vizioneck STILL not banned at my workplace \O/

Legend


I'd imagine this must have saved you a lot of time.


Yeah. It's really fun being able to just drag and drop the material on to level geometry and have the outlines generated within milliseconds.


Plus it has some other advantages, but I'll save those for the next tech talk.

Shinobi-san


Yeah. It's really fun being able to just drag and drop the material on to level geometry and have the outlines generated within milliseconds.


Plus it has some other advantages, but I'll save those for the next tech talk.



randomness? Or something along those lines...
Vizioneck STILL not banned at my workplace \O/