I've been having fun working with shaders.
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. Wasn't a problem for early prototype levels, but some of the later levels would have taken years with this method. I didn't want to waist time making outlines by hand so I worked on other game aspects.
About a week ago I decided it was finally time to take on this challenge so here are my solutions. Hopefully someone else can find them helpful. I'm in Unity btw.
For starters I determined it'd be much easier for the shader to make the outlines as the result of three scaled faces verse what I had been doing.
This looks exactly the same as the other method 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.
Code:
Shader "Custom/Outlines" {
Properties {
_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 {}
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _BColor;
//structs
struct vertexInput {
float4 vertex : POSITION;
};
struct vertexOutput {
float4 pos : SV_POSITION;
};
//vertex function
vertexOutput vert(vertexInput v){
vertexOutput o;
o.pos= mul(UNITY_MATRIX_MVP, v.vertex);
return o;
}
//fragment function
float4 frag(vertexOutput i) : COLOR
{
return _BColor;
}
ENDCG
}
Pass{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//user 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;
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
}
}
}
The "-float4" value after the second and third vert positions is to combat z buffering. I'm doing it this way instead of the regular way in unity because it offers me more control down the road.
If you were to copy this shader in to your game and apply it to a cube, this is the result.
Kinda cool, but not correct. This is because the tangent values for the cube are not pointing towards the center of each face. Instead they're calculated by Unity to line up with the UV maps. To fix this, I created a custom script that processes assets after they have been imported by Unity. It all works behind the scenes. In the Assets folder, it needs a folder called "Editor." In there you can put scripts like this. The basics of my script are
Code:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
public class GOutline : AssetPostprocessor {
Mesh myMesh;
Vector3[] vertices;
Vector4[] tans;
Vector3[] norms;
int[] faces;
void OnPostprocessModel (GameObject myModel) {
//This makes the script work for multiobject files as well
Transform[] allChildren = myModel.GetComponentsInChildren<Transform>();
foreach (Transform child in allChildren) {
if(child.GetComponent<MeshFilter>()){
myMesh=child.GetComponent<MeshFilter>().sharedMesh;
vertices=myMesh.vertices;
tans=myMesh.tangents;
norms=myMesh.normals;
faces=myMesh.triangles;
for(int i = 0; i < vertices.Length; i++){
//do your caclulations for each vertex, then assign them back to the array
tans[i]=mynewtans;
norms[i]=mynewnorms;
//etc
}
//Assign the arrays back to the mesh
myMesh.tangents=tans;
myMesh.normals=norms;
}
}
}
}
The result is a perfect looking outline!
Hopefully this isn't just interesting but also gives someone some good references if they're needing something similar.
Oh and also I'm back! Been busy and got caught up with some things. All your games are looking fantastic! Congrats on finishing psyscrolr Ashodin.
Might as well post a new screenshot of Vizioneck too!!!
And this image ties back in to shaders!
The blue outlines act like windows, so stacking three planes to make them doesn't naturally work when the top and bottom planes are transparent. Working around that was actually pretty simple. Here's the final shader.
Code:
Shader "Custom/GOBlue" {
Properties {
_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", float)=.3
_OutlineW ("Outline Width", float)=.1
}
SubShader {
Tags { "Queue" = "Transparent" "RenderType"="Transparent" }
Pass{
Blend SrcAlpha OneMinusSrcAlpha
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 _BColor;
//structs
struct vertexInput {
float4 vertex : POSITION;
};
struct vertexOutput {
float4 pos : SV_POSITION;
};
//vertex function
vertexOutput vert(vertexInput v){
vertexOutput o;
o.pos= mul(UNITY_MATRIX_MVP, v.vertex);
}
//fragment function
float4 frag(vertexOutput i) : COLOR
{
return _BColor;
}
ENDCG
}
Pass{
Blend SrcAlpha OneMinusSrcAlpha
Cull off
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);
}
//fragment function
float4 frag(vertexOutput i) : COLOR
{
return _BColor;
}
ENDCG
}
//This is the non transparent layer.
Pass{
Cull off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
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;
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
}
}
}
This shader also handles hiding blue outlines when viewed through blue surfaces, as you can see in the picture.