// 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); }