Hi,
some background info first: my target platform is WebGL and I want outlines around objects in a small scene of about 50 static objects with a camera that can be moved around freely.
The “inverted hull” outlines approach (in which vertices are moved out along their normals, normals are inverted and front faces culled) doesn’t produce nice enough outlines and the mostly sharp-edged meshes I’m using would require too much manual work to get to a point where I could bake interpolated normals to vertex colors to combat this.
Since I’m targeting WebGL and want to support older devices as well, I’m hesitant to use depth and normal data and a Sobel filter for edge detection. That seems like an awful lot of extra (temporary) textures and texture lookups. Plus I could live with not having “inner” edges detected (i.e. edges are only detected between one object and another or the skybox).
I thus came up with the following idea:
- prepare all objects to be outlined by assigning different vertex colors per object (all vertices of one object have the same color but no two objects do)
- use a ScriptableRenderPass to draw the scene to a temporary RenderTexture using an override material with an unlit vertex color shader
- Blit* that RenderTexture to the camera’s render target using an outline shader that detects edges in it based solely on color differences and composites the resulting edges onto the
_CameraOpaqueTexture
- I say Blit, but through trial and error I found that it (too) behaves strangely on WebGL. I could never get it to produce the expected result, only white or grey. So instead I use
cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, blitMaterial);
All this works great in the editor, and I am getting outlines in WebGL builds as well, but only between the skybox and all opaque geometry instead of between differently vertex-colored objects as expected (and as in the editor). To track down why, I changed the shader used for the “Blit” step to directly output the vertex colors and all the objects appear completely white (while they should be different shades of grey as set in Blender and shown in the editor).
The vertex colors shader doesn’t seem to be the problem, when I assign it to the materials used in the scene and get rid of the ScriptableRenderPass I get the colors I expect both in the editor and in WebGL builds. I also modified it to return a fixed color to see if it always produces white on WebGL, but the fixed color persists.
The ScriptableRenderPass (set to execute “After Rendering”):
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class CustomOutlineFeature : ScriptableRendererFeature
{
class RenderPass : ScriptableRenderPass
{
private Material grabMaterial;
private Material blitMaterial;
private RenderTargetIdentifier sourceID;
private RenderTargetHandle tempTextureHandle;
private FilteringSettings _filteringSettings;
private List<ShaderTagId> _shaderTagIdList = new List<ShaderTagId>() { new ShaderTagId("UniversalForward"), new ShaderTagId("SRPDefaultUnlit") };
public RenderPass(Material grabMaterial, Material blitMaterial) : base()
{
this.grabMaterial = grabMaterial;
this.blitMaterial = blitMaterial;
tempTextureHandle.Init("_TempBlitMaterialTexture");
}
public void SetSource(RenderTargetIdentifier source)
{
this.sourceID = source;
}
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
_filteringSettings = new FilteringSettings(RenderQueueRange.opaque);
cmd.GetTemporaryRT(tempTextureHandle.id, cameraTextureDescriptor);
ConfigureTarget(tempTextureHandle.Identifier());
//ConfigureTarget(sourceID);
ConfigureClear(ClearFlag.All, Color.black);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get();
SortingCriteria sortingCriteria = renderingData.cameraData.defaultOpaqueSortFlags;
DrawingSettings drawingSettings = CreateDrawingSettings(_shaderTagIdList, ref renderingData, sortingCriteria);
drawingSettings.overrideMaterial = grabMaterial;
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings);// drawn into tempTexture
cmd.SetGlobalTexture("_MainTex", tempTextureHandle.Identifier());
cmd.SetRenderTarget(sourceID);
cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, blitMaterial);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override void FrameCleanup(CommandBuffer cmd)
{
cmd.ReleaseTemporaryRT(tempTextureHandle.id);
}
}
[System.Serializable]
public class Settings {
public Material grabMaterial;
public Material blitMaterial;
public int materialPassIndex = -1;
public RenderPassEvent renderEvent = RenderPassEvent.AfterRenderingOpaques;
}
[SerializeField]
private Settings settings = new Settings();
[SerializeField]
private Settings secondSettings = new Settings();
RenderPass renderPass;
private GrabRenderPass grabPass;
public Material GrabMaterial => settings.grabMaterial;
public Material BlitMaterial => settings.blitMaterial;
public override void Create()
{
this.renderPass = new RenderPass(settings.grabMaterial, settings.blitMaterial);
renderPass.renderPassEvent = settings.renderEvent;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (renderingData.cameraData.cameraType == CameraType.Game)
{
renderPass.ConfigureInput(ScriptableRenderPassInput.Color);
renderPass.SetSource(renderer.cameraColorTarget);
renderer.EnqueuePass(renderPass);
}
}
}
The unlit vertex colors shader:
Shader "Unlit/UnlitVertexColors"
{
Properties
{
}
SubShader
{
Tags { "Queue"="Geometry" "RenderType"="Opaque" }
Cull off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float4 vertexColors : COLOR;
};
struct v2f
{
float4 vertex : SV_POSITION;
float4 vertexColors : COLOR;
};
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.vertexColors = v.vertexColors;
return o;
}
fixed4 frag (v2f i) : COLOR//SV_Target
{
fixed4 col = i.vertexColors;
// fixed color works, vertex colors don't
return col;
}
ENDCG
}
}
}
The edge detection shader:
Shader "Unlit/OutlineBlitClipSpaceShader"
{
SubShader
{
Cull Off
ZWrite Off
ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#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 _MainTex_TexelSize;
sampler2D _CameraOpaqueTexture;
float4 _CameraOpaqueTexture_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = float4(v.vertex.xyz, 1.0);
o.uv = v.uv;
#if UNITY_UV_STARTS_AT_TOP
o.vertex.y *= -1;
#endif
return o;
}
fixed4 SamplePixel(in float2 uv)
{
return tex2D(_MainTex, uv);
}
fixed4 frag (v2f i) : SV_Target
{
float2 offsets[4] = {
float2(-1, -1),
float2(-1, 1),
float2(1, -1),
float2(1, 1)
};
// sample the texture
const fixed4 originalFragment = tex2D(_CameraOpaqueTexture, i.uv);
const fixed4 currentPixelColor = SamplePixel(i.uv);
const float2 edgeWeight = float2(0.5, 0.5);
fixed4 sampledValue = fixed4(0, 0, 0, 0);
for (int j = 0; j < 4; j++) {
sampledValue += SamplePixel(i.uv + offsets[j] * _MainTex_TexelSize.xy * edgeWeight);
}
sampledValue /= 4;
return lerp(originalFragment, /*_EdgeColor*/float4(0.99, 0.4, 0.4, 1), step(/*_Threshold*/0.0001, length(currentPixelColor - sampledValue)));
}
ENDCG
}
}
}