107 lines
6.8 KiB
HLSL
Raw Permalink 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
// 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 cameras 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);
}