BTSDS: Outlines via Post Processing

This commit is contained in:
Jenny Crowe
2022-03-10 05:45:21 -07:00
parent e7d6796c08
commit 32b647a4d4
23 changed files with 1096 additions and 40 deletions

View File

@ -0,0 +1,146 @@
Shader "Hidden/Roystan/Outline Post Process"
{
SubShader
{
Cull Off ZWrite Off ZTest Always
Pass
{
// Custom post processing effects are written in HLSL blocks,
// with lots of macros to aid with platform differences.
// https://github.com/Unity-Technologies/PostProcessing/wiki/Writing-Custom-Effects#shader
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#include "Packages/com.unity.postprocessing/PostProcessing/Shaders/StdLib.hlsl"
TEXTURE2D_SAMPLER2D(_MainTex, sampler_MainTex);
// _CameraNormalsTexture contains the view space normals transformed
// to be in the 0...1 range.
TEXTURE2D_SAMPLER2D(_CameraNormalsTexture, sampler_CameraNormalsTexture);
TEXTURE2D_SAMPLER2D(_CameraDepthTexture, sampler_CameraDepthTexture);
// Data pertaining to _MainTex's dimensions.
// https://docs.unity3d.com/Manual/SL-PropertiesInPrograms.html
float4 _MainTex_TexelSize;
float _Scale;
float4 _Color;
float _DepthThreshold;
float _DepthNormalThreshold;
float _DepthNormalThresholdScale;
float _NormalThreshold;
// This matrix is populated in PostProcessOutline.cs.
float4x4 _ClipToView;
// Combines the top and bottom colors using normal blending.
// https://en.wikipedia.org/wiki/Blend_modes#Normal_blend_mode
// This performs the same operation as Blend SrcAlpha OneMinusSrcAlpha.
float4 alphaBlend(float4 top, float4 bottom)
{
float3 color = (top.rgb * top.a) + (bottom.rgb * (1 - top.a));
float alpha = top.a + bottom.a * (1 - top.a);
return float4(color, alpha);
}
// Both the Varyings struct and the Vert shader are copied
// from StdLib.hlsl included above, with some modifications.
struct Varyings
{
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 texcoordStereo : TEXCOORD1;
float3 viewSpaceDir : TEXCOORD2;
#if STEREO_INSTANCING_ENABLED
uint stereoTargetEyeIndex : SV_RenderTargetArrayIndex;
#endif
};
Varyings Vert(AttributesDefault v)
{
Varyings o;
o.vertex = float4(v.vertex.xy, 0.0, 1.0);
o.texcoord = TransformTriangleVertexToUV(v.vertex.xy);
// Transform our point first from clip to view space,
// taking the xyz to interpret it as a direction.
o.viewSpaceDir = mul(_ClipToView, o.vertex).xyz;
#if UNITY_UV_STARTS_AT_TOP
o.texcoord = o.texcoord * float2(1.0, -1.0) + float2(0.0, 1.0);
#endif
o.texcoordStereo = TransformStereoScreenSpaceTex(o.texcoord, 1.0);
return o;
}
float4 Frag(Varyings i) : SV_Target
{
float halfScaleFloor = floor(_Scale * 0.5);
float halfScaleCeil = ceil(_Scale * 0.5);
// Sample the pixels in an X shape, roughly centered around i.texcoord.
// As the _CameraDepthTexture and _CameraNormalsTexture default samplers
// use point filtering, we use the above variables to ensure we offset
// exactly one pixel at a time.
float2 bottomLeftUV = i.texcoord - float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleFloor;
float2 topRightUV = i.texcoord + float2(_MainTex_TexelSize.x, _MainTex_TexelSize.y) * halfScaleCeil;
float2 bottomRightUV = i.texcoord + float2(_MainTex_TexelSize.x * halfScaleCeil, -_MainTex_TexelSize.y * halfScaleFloor);
float2 topLeftUV = i.texcoord + float2(-_MainTex_TexelSize.x * halfScaleFloor, _MainTex_TexelSize.y * halfScaleCeil);
float3 normal0 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomLeftUV).rgb;
float3 normal1 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topRightUV).rgb;
float3 normal2 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, bottomRightUV).rgb;
float3 normal3 = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_CameraNormalsTexture, topLeftUV).rgb;
float depth0 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomLeftUV).r;
float depth1 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topRightUV).r;
float depth2 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, bottomRightUV).r;
float depth3 = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, topLeftUV).r;
// Transform the view normal from the 0...1 range to the -1...1 range.
float3 viewNormal = normal0 * 2 - 1;
float NdotV = 1 - dot(viewNormal, -i.viewSpaceDir);
// Return a value in the 0...1 range depending on where NdotV lies
// between _DepthNormalThreshold and 1.
float normalThreshold01 = saturate((NdotV - _DepthNormalThreshold) / (1 - _DepthNormalThreshold));
// Scale the threshold, and add 1 so that it is in the range of 1..._NormalThresholdScale + 1.
float normalThreshold = normalThreshold01 * _DepthNormalThresholdScale + 1;
// Modulate the threshold by the existing depth value;
// pixels further from the screen will require smaller differences
// to draw an edge.
float depthThreshold = _DepthThreshold * depth0 * normalThreshold;
float depthFiniteDifference0 = depth1 - depth0;
float depthFiniteDifference1 = depth3 - depth2;
// edgeDepth is calculated using the Roberts cross operator.
// The same operation is applied to the normal below.
// https://en.wikipedia.org/wiki/Roberts_cross
float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
edgeDepth = edgeDepth > depthThreshold ? 1 : 0;
float3 normalFiniteDifference0 = normal1 - normal0;
float3 normalFiniteDifference1 = normal3 - normal2;
// Dot the finite differences with themselves to transform the
// three-dimensional values to scalars.
float edgeNormal = sqrt(dot(normalFiniteDifference0, normalFiniteDifference0) + dot(normalFiniteDifference1, normalFiniteDifference1));
edgeNormal = edgeNormal > _NormalThreshold ? 1 : 0;
float edge = max(edgeDepth, edgeNormal);
float4 edgeColor = float4(_Color.rgb, _Color.a * edge);
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.texcoord);
return alphaBlend(edgeColor, color);
}
ENDHLSL
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 534727a34958a6b409102bf07fadffab
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
preprocessorOverride: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,84 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-511430771441868654
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 957177531333f744abeb6082b4c44bc0, type: 3}
m_Name: PostProcessOutline
m_EditorClassIdentifier:
active: 1
enabled:
overrideState: 1
value: 1
scale:
overrideState: 1
value: 4
color:
overrideState: 1
value: {r: 0, g: 0, b: 0, a: 1}
depthThreshold:
overrideState: 1
value: 0.6
depthNormalThreshold:
overrideState: 1
value: 0.6
depthNormalThresholdScale:
overrideState: 0
value: 7
normalThreshold:
overrideState: 1
value: 0.4
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8e6292b2c06870d4495f009f912b9600, type: 3}
m_Name: OutlinePostProfile
m_EditorClassIdentifier:
settings:
- {fileID: -511430771441868654}
--- !u!114 &114178475910842986
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7f161f1bfe8d69542a86f8d442c273b8, type: 3}
m_Name: PostProcessOutline
m_EditorClassIdentifier:
active: 1
enabled:
overrideState: 1
value: 1
scale:
overrideState: 0
value: 1
color:
overrideState: 0
value: {r: 1, g: 1, b: 1, a: 1}
depthThreshold:
overrideState: 0
value: 1.5
depthNormalThreshold:
overrideState: 0
value: 0.5
depthNormalThresholdScale:
overrideState: 0
value: 7
normalThreshold:
overrideState: 0
value: 0.4

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 11527536cbbe79e46ae805ff65dfc703
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,42 @@
using System;
using UnityEngine;
using UnityEngine.Rendering.PostProcessing;
[Serializable]
[PostProcess(typeof(PostProcessOutlineRenderer), PostProcessEvent.BeforeStack, "Roystan/Post Process Outline")]
public sealed class PostProcessOutline : PostProcessEffectSettings
{
[Tooltip("Number of pixels between samples that are tested for an edge. When this value is 1, tested samples are adjacent.")]
public IntParameter scale = new IntParameter { value = 1 };
public ColorParameter color = new ColorParameter { value = Color.white };
[Tooltip("Difference between depth values, scaled by the current depth, required to draw an edge.")]
public FloatParameter depthThreshold = new FloatParameter { value = 1.5f };
[Range(0, 1), Tooltip("The value at which the dot product between the surface normal and the view direction will affect " +
"the depth threshold. This ensures that surfaces at right angles to the camera require a larger depth threshold to draw " +
"an edge, avoiding edges being drawn along slopes.")]
public FloatParameter depthNormalThreshold = new FloatParameter { value = 0.5f };
[Tooltip("Scale the strength of how much the depthNormalThreshold affects the depth threshold.")]
public FloatParameter depthNormalThresholdScale = new FloatParameter { value = 7 };
[Range(0, 1), Tooltip("Larger values will require the difference between normals to be greater to draw an edge.")]
public FloatParameter normalThreshold = new FloatParameter { value = 0.4f };
}
public sealed class PostProcessOutlineRenderer : PostProcessEffectRenderer<PostProcessOutline>
{
public override void Render(PostProcessRenderContext context)
{
var sheet = context.propertySheets.Get(Shader.Find("Hidden/Roystan/Outline Post Process"));
sheet.properties.SetFloat("_Scale", settings.scale);
sheet.properties.SetColor("_Color", settings.color);
sheet.properties.SetFloat("_DepthThreshold", settings.depthThreshold);
sheet.properties.SetFloat("_DepthNormalThreshold", settings.depthNormalThreshold);
sheet.properties.SetFloat("_DepthNormalThresholdScale", settings.depthNormalThresholdScale);
sheet.properties.SetFloat("_NormalThreshold", settings.normalThreshold);
sheet.properties.SetColor("_Color", settings.color);
Matrix4x4 clipToView = GL.GetGPUProjectionMatrix(context.camera.projectionMatrix, true).inverse;
sheet.properties.SetMatrix("_ClipToView", clipToView);
context.command.BlitFullscreenTriangle(context.source, context.destination, sheet, 0);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 957177531333f744abeb6082b4c44bc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 91791aadbbc222442ad483fa480daeb6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,61 @@
using UnityEngine;
public class RenderReplacementShaderToTexture : MonoBehaviour
{
[SerializeField]
bool deleteChildren = true;
[SerializeField]
Shader replacementShader;
[SerializeField]
RenderTextureFormat renderTextureFormat = RenderTextureFormat.ARGB32;
[SerializeField]
FilterMode filterMode = FilterMode.Point;
[SerializeField]
int renderTextureDepth = 24;
[SerializeField]
CameraClearFlags cameraClearFlags = CameraClearFlags.Color;
[SerializeField]
Color background = Color.black;
[SerializeField]
string targetTexture = "_RenderTexture";
private RenderTexture renderTexture;
private new Camera camera;
private void Start()
{
if (deleteChildren)
{
foreach (Transform t in transform)
{
DestroyImmediate(t.gameObject);
}
}
Camera thisCamera = GetComponent<Camera>();
// Create a render texture matching the main camera's current dimensions.
renderTexture = new RenderTexture(thisCamera.pixelWidth, thisCamera.pixelHeight, renderTextureDepth, renderTextureFormat);
renderTexture.filterMode = filterMode;
// Surface the render texture as a global variable, available to all shaders.
Shader.SetGlobalTexture(targetTexture, renderTexture);
// Setup a copy of the camera to render the scene using the normals shader.
GameObject copy = new GameObject("Camera" + targetTexture);
camera = copy.AddComponent<Camera>();
camera.CopyFrom(thisCamera);
camera.transform.SetParent(transform);
camera.targetTexture = renderTexture;
camera.SetReplacementShader(replacementShader, "RenderType");
camera.depth = thisCamera.depth - 1;
camera.clearFlags = cameraClearFlags;
camera.backgroundColor = background;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2a7b4608cc97a8940ad8a9229d742532
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5f4f903709ce9854ba9f1fd8341311bb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,52 @@
Shader "Hidden/Roystan/Normals Texture"
{
Properties
{
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 viewNormal : NORMAL;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.viewNormal = COMPUTE_VIEW_NORMAL;
//o.viewNormal = mul((float3x3)UNITY_MATRIX_M, v.normal);
return o;
}
float4 frag (v2f i) : SV_Target
{
return float4(normalize(i.viewNormal) * 0.5 + 0.5, 0);
}
ENDCG
}
}
}

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 95d6144eab187a34b929151454e2969d
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant: