2025-10-31 03:28:00 +09:00

387 lines
20 KiB
HLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SPDX-License-Identifier: (Not available for this version, you are only allowed to use this software if you have express permission from the copyright holder and agreed to the latest NiloToonURP EULA)
// Copyright (c) 2021 Kuroneko ShaderLab Limited
// For more information, visit -> https://github.com/ColinLeung-NiloCat/UnityURPToonLitShaderExample
// #pragma once is a safe guard best practice in almost every .hlsl,
// doing this can make sure your .hlsl's user can include this .hlsl anywhere anytime without producing any multi include conflict
#pragma once
// [Screen Space Outline]
// TODO: result is not consistance when screen resolution / renderscale is changing, or when camera distance is changing
float IsScreenSpaceOutline(
float2 SV_POSITIONxy,
float OutlineThickness,
float DepthSensitivity,
float NormalsSensitivity,
float selfLinearEyeDepth,
float depthSensitivityDistanceFadeoutStrength,
float cameraFOV,
float3 viewDirectionWS)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// We start from a screen space outline method by "alexander ameye", and develop base on it
// *Code: https://gist.github.com/alexanderameye/d956574a67adf885f4e008d68b1c3238
// *Tutorial: https://alexanderameye.github.io/notes/edge-detection-outlines/
//
// currently this method is using both depth and normals texture's screen space difference
// TODO: add color's delta
// TODO: add user defined color RT's difference (blue protocal material ID difference as outline method)?
// TODO: add depth fix (https://twitter.com/bgolus/status/1532830971573129216)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// [Should we use Raw depth (Raw depth texture sample value) or View space depth for depth delta/compare in screen space?]
// = must use Raw depth! Why?
// - Raw depth is linear in screen space(great for postprocess edge detection!), but non-linear in view space (bad for soft particle)
// - view space depth is non-linear in screen space(bad for postprocess edge detection!), but linear in view space (great for soft particle)
// https://www.humus.name/index.php?ID=255
//
//These days the depth buffer is increasingly being used for other purposes than just hidden surface removal.
//Being linear in screen space turns out to be a very desirable property for post-processing.
//Assume for instance that you want to do edge detection on the depth buffer, perhaps for antialiasing by blurring edges.
//This is easily done by comparing a pixel's depth with its neighbors' depths. With Z values you have constant pixel-to-pixel deltas, except for across edges of course.
//This is easy to detect by comparing the delta to the left and to the right, and if they don't match (with some epsilon) you crossed an edge.
//And then of course the same with up-down and diagonally as well. This way you can also reject pixels that don't belong to the same surface if you implement say a blur filter but don't want to blur across edges, for instance for smoothing out artifacts in screen space effects, such as SSAO with relatively sparse sampling.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
float2 texelSize = GetScaledScreenTexelSize(); // (x = 1/texture width, y = 1/texture height)
float2 UV = SV_POSITIONxy * texelSize.xy; // UV is screen space [0,1] uv
// far away pixels auto reduce depth sensitivity in shader, to avoid far object always = outline
DepthSensitivity /= (1 + selfLinearEyeDepth * depthSensitivityDistanceFadeoutStrength * cameraFOV * 0.01); // 0.01 is a magic number that prevent most of the outline artifact
// make screen space outline result not affected by render scale / game resolution
OutlineThickness *= GetScaledScreenWidthHeight().y / 1080; // TODO: currently hardcode using height only, make it better for all aspect
float halfScaleFloor = floor(OutlineThickness * 0.5);
float halfScaleCeil = ceil(OutlineThickness * 0.5);
float2 uvSamples[4];
float depthSamples[4];
float3 normalSamples[4];
uvSamples[0] = UV - float2(texelSize.x, texelSize.y) * halfScaleFloor;
uvSamples[1] = UV + float2(texelSize.x, texelSize.y) * halfScaleCeil;
uvSamples[2] = UV + float2(texelSize.x * halfScaleCeil, -texelSize.y * halfScaleFloor);
uvSamples[3] = UV + float2(-texelSize.x * halfScaleFloor, texelSize.y * halfScaleCeil);
for(int i = 0; i < 4 ; i++)
{
depthSamples[i] = SampleSceneDepth(uvSamples[i]);
normalSamples[i] = SampleSceneNormals(uvSamples[i]);
}
// Depth
float depthFiniteDifference0 = depthSamples[1] - depthSamples[0];
float depthFiniteDifference1 = depthSamples[3] - depthSamples[2];
float edgeDepth = sqrt(pow(depthFiniteDifference0, 2) + pow(depthFiniteDifference1, 2)) * 100;
float depthThreshold = (1/DepthSensitivity) * depthSamples[0];
edgeDepth = edgeDepth > depthThreshold ? 1 : 0;
// Normals
float3 normalFiniteDifference0 = normalSamples[1] - normalSamples[0];
float3 normalFiniteDifference1 = normalSamples[3] - normalSamples[2];
float edgeNormal = sqrt(dot(normalFiniteDifference0, normalFiniteDifference0) + dot(normalFiniteDifference1, normalFiniteDifference1));
edgeNormal = edgeNormal > (1/NormalsSensitivity) ? 1 : 0;
float edge = max(edgeDepth, edgeNormal);
return edge;
}
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
#define HQ_AA 0
float3 positionWSToFlatNormalWSUnitVector(float3 positionWS)
{
// Calculate the rate of change of the position in the screen space x and y directions
float3 ddxPosition = ddx(positionWS);
float3 ddyPosition = ddy(positionWS);
// The cross product of these two vectors gives the surface normal
float3 normal = cross(ddyPosition,ddxPosition);
// Normalize the result to ensure it's a unit vector
normal = normalize(normal);
return normal;
}
// _NiloToonPrepassBufferTex is now defined in NiloPrepassBufferTextureUtil.hlsl
// Unity Core defined common inline sampler already in URP14
// see GlobalSamplers.hlsl
#if UNITY_VERSION < 202220
SAMPLER(sampler_PointClamp);
#endif
float SampleNiloCharacterMaterialID(float2 uv)
{
return SAMPLE_TEXTURE2D_X(_NiloToonPrepassBufferTex, sampler_PointClamp, uv).r * 255.0;
}
float IsScreenSpaceOutlineV2(
float2 SV_POSITIONxy,
float OutlineThickness,
float GeometryEdgeThreshold,
float NormalAngleCosThetaThresholdMin,
float NormalAngleCosThetaThresholdMax,
bool DrawGeometryEdge,
bool DrawNormalAngleEdge,
bool DrawMaterialIDBoundary,
bool DrawCustomIDBoundary,
bool DrawWireframe,
float3 positionWS)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// We start from a screen space outline method by "alexander ameye", and develop base on it
// *Code: https://gist.github.com/alexanderameye/d956574a67adf885f4e008d68b1c3238
// *Tutorial: https://alexanderameye.github.io/notes/edge-detection-outlines/
// also by "Robin Seibold"
// https://youtu.be/LMqio9NsqmM?si=Lsgwp573KvCKV1rP
//
// Outline Shader
// https://roystan.net/articles/outline-shader/
// https://github.com/IronWarrior/UnityOutlineShader
// https://github.com/Robinseibold/Unity-URP-Outlines
//
// Edge Detection Outlines
// https://ameye.dev/notes/edge-detection-outlines/
//
// (SIGGRAPH 2020) That's a wrap: a Manifold Garden Rendering Retrospective
// https://youtu.be/5VozHerlOQw?si=PsNHPRHkqwhSc6QA
//
// @bgolus's twitter about screen space outline
// https://twitter.com/bgolus/status/1532830971573129216
//
// currently this method is using both depth and normals texture's screen space difference
// TODO: add color's delta
// TODO: add user defined color RT's difference (blue protocal material ID difference as outline method)?
// TODO: add depth fix (https://twitter.com/bgolus/status/1532830971573129216)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
float2 texelSize = GetScaledScreenTexelSize(); // (x = 1/RT width, y = 1/RT height)
float2 UV = SV_POSITIONxy * texelSize; // UV is screen space [0,1] uv, do not + 0.5 to SV_POSITIONxy
// make screen space outline result not affected by render scale / game resolution
float2 screenWidthHeight = GetScaledScreenWidthHeight();
OutlineThickness *= min(screenWidthHeight.x,screenWidthHeight.y) / 1080;
float halfScaleFloor = max(floor(OutlineThickness * 0.5),1); // max(x,1) to prevent 0 pixel offset
float halfScaleCeil = ceil(OutlineThickness * 0.5);
float resultOutline = 0;
#if HQ_AA
const uint SAMPLE_COUNT = 8;
#else
const uint SAMPLE_COUNT = 3;
#endif
float2 uvSamples[SAMPLE_COUNT];
float depthSamples[SAMPLE_COUNT];
float3 normalSamples[SAMPLE_COUNT];
float3 positionWSSamples[SAMPLE_COUNT];
float objectIDSamples[SAMPLE_COUNT];
float sdf[SAMPLE_COUNT];
float closestEyeDepthOfAllSamples = FLT_MAX;
// 2x2
uvSamples[0] = UV + float2(texelSize.x, 0);
uvSamples[1] = UV + float2(texelSize.x, texelSize.y);
uvSamples[2] = UV + float2(0, texelSize.y);
#if HQ_AA
uvSamples[3] = UV + float2(-texelSize.x, texelSize.y);
uvSamples[4] = UV + float2(-texelSize.x, 0);
uvSamples[5] = UV + float2(-texelSize.x, -texelSize.y);
uvSamples[6] = UV + float2(0, -texelSize.y);
uvSamples[7] = UV + float2(texelSize.x, -texelSize.y);
#endif
// sample all data of adjacent pixels around center pixel
for(uint i = 0; i < SAMPLE_COUNT ; i++)
{
depthSamples[i] = SampleSceneDepth(uvSamples[i]);
normalSamples[i] = SampleSceneNormals(uvSamples[i]);
objectIDSamples[i] = SampleNiloCharacterMaterialID(uvSamples[i]);
// Reconstruct the world space positions of pixels from the depth texture
// https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.0/manual/writing-shaders-urp-reconstruct-world-position.html
//---------------------------------------------------------------------------------------
float depth = depthSamples[i];
// Adjust z to match NDC for OpenGL
#if !UNITY_REVERSED_Z
depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, depth);
#endif
positionWSSamples[i] = ComputeWorldSpacePosition(uvSamples[i],depth,UNITY_MATRIX_I_VP);
//---------------------------------------------------------------------------------------
sdf[i] = distance(UV / texelSize, uvSamples[i] / texelSize);
closestEyeDepthOfAllSamples = min(closestEyeDepthOfAllSamples, LinearEyeDepth(depthSamples[i],_ZBufferParams));
}
// sample self pixel's depth+normal
float selfDepthSample = SampleSceneDepth(UV);
float3 selfNormalSample = SampleSceneNormals(UV); // sample N from camera normal texture is required, don't use fragment's normalWS since result is not correct
//----------------------------------------------------------------------------------------------------------------------------------------------------------
// find Normals difference
//----------------------------------------------------------------------------------------------------------------------------------------------------------
float minDistanceToAnyEdge_normalMethod = FLT_MAX;
for(uint j = 0; j < SAMPLE_COUNT; j++)
{
bool isOutline = dot(selfNormalSample, normalSamples[j]) < NormalAngleCosThetaThresholdMin;
if(isOutline)
{
minDistanceToAnyEdge_normalMethod = min(minDistanceToAnyEdge_normalMethod, sdf[j]);
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------
// Manifold Garden's "is adjacent positionWS on plane?" edge detection, it will prevent generating edge within a flat surface(e.g. big flat ground)
// ----------------------------------------------------------------------------------------------------------------------------------------------------------
// do not use this as input -> ComputeWorldSpacePosition(UV,selfDepthSample,UNITY_MATRIX_I_VP);
float3 selfFlatN = positionWSToFlatNormalWSUnitVector(positionWS); // since we are fitting a plane to center pixel, flatN is a must, do not use fragment's smoothed normal
float flatNSmoothedNDiff = dot(selfNormalSample,selfFlatN);
float minDistanceToAnyEdge_flatPolyDepthMethod = FLT_MAX;
// only run when flat normal and smoothed normal are not having a big difference
if(flatNSmoothedNDiff > 0.99)
{
for(uint i = 0; i < SAMPLE_COUNT; i++)
{
float3 adjacentPixelPositionWSRelativeToOriginalPixel = positionWSSamples[i] - positionWS;
// for a plane passing origin with normal unit vector N,
// and for a random position P,
// dot(P, N) is the signed projected shortest distance from P to plane
// (dot(P,N) is |P||N|cos(theta) = |P|cos(theta))
float unsignedProjectedDistanceWSOfPointToPlane = abs(dot(selfFlatN,adjacentPixelPositionWSRelativeToOriginalPixel));
// default 0.003 = 0.3cm is good
bool isOutline = unsignedProjectedDistanceWSOfPointToPlane > GeometryEdgeThreshold;
if(isOutline)
{
minDistanceToAnyEdge_flatPolyDepthMethod = min(minDistanceToAnyEdge_flatPolyDepthMethod, sdf[i]);
}
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------
// Material border edge
//----------------------------------------------------------------------------------------------------------------------------------------------------------
float selfObjectID = SampleNiloCharacterMaterialID(UV);
float minDistanceToAnyEdge_objectIDMethod = FLT_MAX;
for(uint k = 0; k < SAMPLE_COUNT; k++)
{
bool isOutline = selfObjectID != objectIDSamples[k];
if(isOutline)
{
minDistanceToAnyEdge_objectIDMethod = min(minDistanceToAnyEdge_objectIDMethod, sdf[k]);
}
}
//----------------------------------------------------------------------------------------------------------------------------------------------------------
// return result
//----------------------------------------------------------------------------------------------------------------------------------------------------------
float finalMinDistanceToAnyEdge = FLT_MAX;
if(DrawGeometryEdge)
{
finalMinDistanceToAnyEdge = min(finalMinDistanceToAnyEdge,minDistanceToAnyEdge_flatPolyDepthMethod);
}
if(DrawNormalAngleEdge)
{
finalMinDistanceToAnyEdge = min(finalMinDistanceToAnyEdge,minDistanceToAnyEdge_normalMethod);
}
if(DrawMaterialIDBoundary)
{
finalMinDistanceToAnyEdge = min(finalMinDistanceToAnyEdge,minDistanceToAnyEdge_objectIDMethod);
}
//saturate(1-smoothstep(sqrt(2),sqrt(2)*3,finalMinDistanceToAnyEdge));
if(abs(finalMinDistanceToAnyEdge-sqrt(2)) < 0.1) resultOutline = 0.25;
if(abs(finalMinDistanceToAnyEdge-1.0) < 0.1) resultOutline = 1;
//resultOutline = finalMinDistanceToAnyEdge < 100;// test
// aa using fwidth
/*
if(resultOutline < 10)
{
float gradient = fwidth(resultOutline);
resultOutline = smoothstep(0.5 * gradient, 1 - 0.5 * gradient, resultOutline);
}
*/
// apply distance fadeout
float linearEyeDepth = LinearEyeDepth(closestEyeDepthOfAllSamples,_ZBufferParams);
float fadeoutStart = 50;
float fadeoutExtendRange = 100;
float fadeoutFactor = smoothstep(fadeoutStart,fadeoutStart+fadeoutExtendRange,abs(linearEyeDepth));
resultOutline = lerp(resultOutline,0,fadeoutFactor);
return resultOutline;
}
// [Notes of Manifold gargen's edge detection]
// (SIGGRAPH 2020) That's a wrap: a Manifold Garden Rendering Retrospective
// https://youtu.be/5VozHerlOQw?si=dSAwmKkJItHDJPhe
// MG_Siggraph: That's a wrap: A Manifold Garden Rendering Retrospective
// https://docs.google.com/presentation/d/166oOh7IS1uV_4mKzer_kVdBysAch6fqH/edit#slide=id.p1
// https://docs.google.com/document/d/1EcPQ2v7fAHhrHjnwMjJnL4Ijx5tQO4Sqv6kcq2fIdBg/edit
/*
Edges: Manifold Gardens aesthetic relies heavily on its painterly shaded edges.
We render these as a post processing effect, reading from an extra buffer containing normal/depth metadata.
First, consider a naive edge implementation: Check if any 4-neighbor pixel (up, down, left, right) has a different normal or depth; if so, this pixel is an outline!
Unfortunately this aliases terribly. It effectively renders outlines at a quarter resolution: if A->B is an edge so is B->A!
Additionally, the edges have no defined width meaning the theoretical nyquist frequency is infinitely large!
To fix this, first lets create a good definition of when pixels are considered different:
We propose to use a threshold on the angle between the world-space normals, and a threshold on the worldspace distance of a neighbouring pixel,
to a plane fit to the normal and position of the central pixel.
This definition is much easier to tweak than a naive straight threshold approach, and solves cases of false edges at high incidence angles.
Secondly, lets consider a better definition of how to shade an edge:
If two pixels differ, there is an edge somewhere between them, meaning every pixel has some distance to its nearest edge - this is effectively a signed distance field (SDF)!
We can now render anti-aliased iso-lines of this SDF using standard techniques. Most importantly, this definition extends to arbitrary sample positions.
Manifold Gardens render loop works with MSAA - and we can calculate a distance for each subsample by testing for an edge against every other subsample,
effectively sampling the SDF at a much higher resolution.
The performance of this is not great however. Two observations made this approach feasible:
Most pixels are not edges. We can do a simpler broad check first, to see if a pixel can be an edge, and if not, skip the slow path.
Edges are symmetric, and we can prove the coarse pass only needs to be done at a ¼ resolution!
*/
/*
Achieving Rollerdrome's iconic comic book aesthetic | Unity
https://youtu.be/G1NY0LKDqJo?si=hPsLpmOkxpwMiKxh
https://unity.com/case-study/rollerdrome
outline produced from 2 RTs:
[1]
edge buffer = solid color ID RT, then draw edge on any color diff
- material ID
- vertex color (in modeling program, manual select faces and flood with target color)
- shadow map shadow result (like a screen space shadow map result)
[2 (not sure about this)]
another texture mask buffer =
- manual paint edge textures and mask textures
*/