107 lines
6.8 KiB
HLSL
107 lines
6.8 KiB
HLSL
// 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
|
||
|
||
// Push an imaginary vertex from original position towards camera in view space (linear, view space unit),
|
||
// complete imaginary vertex's view space to clip space transformation,
|
||
// then only overwrite original positionCS.z using imaginary vertex's w division corrected result positionCS.z value.
|
||
// Will only affect ZTest ZWrite's depth value when doing rasterization
|
||
|
||
// Useful for:
|
||
// -Hide ugly outline on face/eye/hair
|
||
// -Make eyebrow/eye render on top of hair
|
||
// -Solve ZFighting issue without moving geometry
|
||
|
||
// note:
|
||
// Why not just positionCS.z += _ConstantOffset?
|
||
// because depth buffer is non-linear if perspective camera, doing this will make ZOffset not stable across all camera near/far settings or vertex to camera distance.
|
||
// This function use view space unit ZOffset which is stable across all camera near/far settings or camera distance.
|
||
|
||
// This method will work in DirectX, Vulkan and OpenGL/OpenGLES,
|
||
// because it is doing full projection matrix mul completely without knowing the projection matrix z-row.zw value
|
||
|
||
// In Unity, view space look into -Z direction, so
|
||
// positive viewSpaceZOffsetAmount means bring vertex depth closer to camera
|
||
// negative viewSpaceZOffsetAmount means push vertex depth away from camera
|
||
// *if you just want to use this function, you don't have to understand the math inside in order to use it
|
||
float4 NiloGetNewClipPosWithZOffsetVSPerspectiveCamera(float4 originalPositionCS, float viewSpaceZOffsetAmount)
|
||
{
|
||
// push an imaginary vertex in view space,
|
||
// then use
|
||
// x = max(cam near plane + eps + vertex eyeDepth*0.001,x)
|
||
// to prevent pushing the vertex over camera's near plane
|
||
// the "+ vertex eyeDepth*0.001" means keeping the vertex z sorting even when the vertex was pushed over camera's near plane,
|
||
// without this "+ vertex eyeDepth*0.001" all vertex will be pushed to a flat "cam near plane + eps" plane completely when zoffset(pushing vertex to camera) is too large
|
||
// which makes the vertices look all broken with zfighting since they all have the same depth
|
||
// (_ProjectionParams.y is the camera’s near plane)
|
||
float float_Eps = 5.960464478e-8; // 2^-24, machine epsilon: 1 + EPS = 1 (half of the ULP for 1.0f)
|
||
float modifiedPositionVS_Z = -max(_ProjectionParams.y + float_Eps + originalPositionCS.w * 0.001, originalPositionCS.w - viewSpaceZOffsetAmount);
|
||
// TODO: do we need to prevent zoffset push over farplane? is it really needed by user?
|
||
|
||
// we only care mul(UNITY_MATRIX_P, modifiedPositionVS).z, and
|
||
// UNITY_MATRIX_P's Z row's xy is always 0, and
|
||
// positionVS's w is always 1
|
||
// so this is the only math that remains after removing all the useless math that won't affect calculating mul(UNITY_MATRIX_P, modifiedPositionVS).z
|
||
float modifiedPositionCS_Z = modifiedPositionVS_Z * UNITY_MATRIX_P[2].z + UNITY_MATRIX_P[2].w;
|
||
|
||
// when this function received an originalPositionCS.xyzw and we want to apply viewspace ZOffset,
|
||
// we can't edit it's xy because it will affect vertex position on screen
|
||
// we can't edit it's w because positionCS.w will be used by w division later in hardware, which also affect ndc's xy vertex position
|
||
// so we can only edit originalPositionCS.z
|
||
|
||
// But in order to do a correct view space ZOffset, we need to edit both originalPositionCS's zw
|
||
// So we first "cancel" the hardware w division by * original CLIPw to our new modified CLIPz first
|
||
// then we do the correct w division manually in vertex shader to simulate hardware's w division
|
||
// original NDCz = original CLIPz / original CLIPw
|
||
|
||
// [here are the steps to find out the correct positionCS.z to output]
|
||
// our desired NDCz = modified CLIPz / modified CLIPw
|
||
// our desired NDCz = modified CLIPz / modified CLIPw * original CLIPw / original CLIPw
|
||
// our desired NDCz = modified CLIPz * original CLIPw / modified CLIPw / original CLIPw
|
||
// our desired NDCz = (modified CLIPz * original CLIPw / modified CLIPw) / (original CLIPw)
|
||
// our desired NDCz = (modified CLIPz * original CLIPw / -modified VIEWz) / (original CLIPw)
|
||
// so (modified CLIPz * original CLIPw / -modified VIEWz) is our output positionCS.z
|
||
originalPositionCS.z = modifiedPositionCS_Z * originalPositionCS.w / (-modifiedPositionVS_Z); // overwrite positionCS.z
|
||
|
||
return originalPositionCS;
|
||
}
|
||
float4 NiloGetNewClipPosWithZOffsetVSOrthographicCamera(float4 originalPositionCS, float viewSpaceZOffsetAmount)
|
||
{
|
||
// since depth buffer is linear when using Orthographic camera
|
||
// just push imaginary vertex linearly and overwrite originalPositionCS.z
|
||
float zoffsetCS = viewSpaceZOffsetAmount / (_ProjectionParams.z-_ProjectionParams.y); // if near plane is really small, use * _ProjectionParams.w ?
|
||
zoffsetCS *= (UNITY_NEAR_CLIP_VALUE > 0 ? 1 : -2); // DirectX ndcZ is [1,0], OpenGL ndcZ is [-1,1]
|
||
originalPositionCS.z = originalPositionCS.z + zoffsetCS;
|
||
return originalPositionCS;
|
||
}
|
||
|
||
// this global float can be optionally controlled if user call to NiloToonPlanarReflectionHelper.cs
|
||
// originally added for supporting planar reflection (CalculateObliqueMatrix will make our ZOffset method fail)
|
||
float _GlobalShouldDisableNiloToonZOffset; // default 0 in GPU, so even no one assign this float, the code will still function correctly by default
|
||
|
||
// support both Orthographic and Perspective camera projection,
|
||
// always slower than above functions but easier for user if they need to support both cameras
|
||
float4 NiloGetNewClipPosWithZOffsetVS(float4 originalPositionCS, float viewSpaceZOffsetAmount)
|
||
{
|
||
// [early exit to avoid 0 ZOffset change]
|
||
// this if() check is added to make planar reflection renders correctly by default, when viewSpaceZOffsetAmount is 0
|
||
if(viewSpaceZOffsetAmount == 0)
|
||
return originalPositionCS;
|
||
|
||
// high level function contain global disable logic, to reduce code complexity of this .hlsl's user code
|
||
if(_GlobalShouldDisableNiloToonZOffset)
|
||
return originalPositionCS;
|
||
|
||
// since instruction count is not high and it is pure ALU, maybe not worth a static uniform branching here,
|
||
// so we use a?b:c (movc: conditional move) here
|
||
return IsPerspectiveProjection() ?
|
||
NiloGetNewClipPosWithZOffsetVSPerspectiveCamera(originalPositionCS,viewSpaceZOffsetAmount) :
|
||
NiloGetNewClipPosWithZOffsetVSOrthographicCamera(originalPositionCS,viewSpaceZOffsetAmount);
|
||
}
|
||
|