Skip to content

Latest commit

 

History

History
235 lines (172 loc) · 8.87 KB

内置函数.md

File metadata and controls

235 lines (172 loc) · 8.87 KB

数学函数

saturate(x)

截取到0~1,相当于clamp(x, 0, 1)

clamp(v, min, max)

将v夹取到$[min, max]$区间

smoothstep(float t1, float t2, float x)

小于t1则返回0,大于t2则返回1,否则在0~1之间插值(接近0或1的位置的斜率不是固定的)

float smoothstep(float t1, float t2, float x) {
  // Scale, bias and saturate x to 0..1 range
  x = clamp((x - t1) / (t2 - t1), 0.0, 1.0); 
  // Evaluate polynomial
  return x * x * (3 - 2 * x);
}

step (a, x)

step (a, x)
{
  if (x < a) 
  {
    return 0;
  }
  else
  {
    return 1;
  }
}

常用于优化if else if a ? b : c等结构

return a < b ? c : d;
// 等价于
return a - b < 0 ? c : d;
// 等价于
half k = step(a, b);
return k * c + (1-k) * d;

dot(vector1, vector2)

float dot(float3 vector1, float3 vector2)
{
    return vector1.x * vector2.x + vector1.y * vector2.y + vector1.z * vector2.z;
}

ddx(propertyName),ddy(propertyName),fwidth(propertyName)

ddx和ddy分别表示x、y方向上的在该像素上的微分

fwidth = abs(ddx(p)) + abs(ddy(p))

propertyName参数可传入PositionNormalColorUV等,分别用于计算该像素与相邻两个像素的这些属性的差值

用途:

参考:An introduction to shader derivative functions | A Clockwork Berry

函数

ComputeScreenPos(float4 positionCS)

URP:Core/Core.hlsl

// TODO: A similar function should be already available in SRP lib on master. Use that instead
float4 ComputeScreenPos(float4 positionCS)
{
    float4 o = positionCS * 0.5f;
    o.xy = float2(o.x, o.y * _ProjectionParams.x) + o.w;
    o.zw = positionCS.zw;
    return o;
}

built-in:UnityCG.cginc

inline float4 ComputeNonStereoScreenPos(float4 pos) {
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

inline float4 ComputeScreenPos(float4 pos) {
    float4 o = ComputeNonStereoScreenPos(pos);
    #if defined(UNITY_SINGLE_PASS_STEREO)
    o.xy = TransformStereoScreenSpaceTex(o.xy, pos.w);
    #endif
    return o;
}

这里其实就是相当于将xy分量加w,然后除以2,将范围为$[-w,w]$的xy分量映射到$[0,w]$,因此仍需要手动进行透视除法除以$w$将其变换到$[-1,1]$。zw分量不变

ComputeScreenPos为什么常见于在顶点着色器中使用?

ComputeScreenPos既可以在顶点着色器,也可以在片元着色器中调用。

官方之所以在顶点着色器中调用,是因为这些变换都是线性的,最终结果不变。(一般情况下)片元数量比顶点数量多得多,因此顶点着色器下计算可以减少计算量(片元的值来自硬件对顶点的值进行线性插值,这基本上是免费的)。

  • 先在顶点着色器中将$[-w,w]$映射到$[0,w]$(线性),再经过硬件光栅化插值(线性),最后在片元着色器中进行透视除法映射到$[0,1]$(非线性)
struct Varyings
{
    // 此结构中的位置必须具有 SV_POSITION 语义。
    float4 positionHCS                            : SV_POSITION;
    float4 screenPosition                         : TEXCOORD1;
};

// vertex shader
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.screenPosition = ComputeScreenPos(OUT.positionHCS);

// fragment shader
float4 screenPosition = (OUT.screenPosition / OUT.screenPosition.w);
  • 先在顶点着色器中什么也不做,再经过硬件光栅化插值(线性),最后在片元着色器中,先将$[-w,w]$映射到$[0,w]$(线性),再进行透视除法映射到$[0,1]$(非线性)
struct Varyings
{
    // 此结构中的位置必须具有 SV_POSITION 语义。
    float4 positionHCS                            : SV_POSITION;
    float4 positionHCSWithoutPerspectiveDivision  : TEXCOORD1;
};
    
// vertex shader
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
OUT.positionHCSWithoutPerspectiveDivision = TransformObjectToHClip(IN.positionOS.xyz);

// fragment shader
float4 screenPosition = ComputeScreenPos(OUT.positionHCSWithoutPerspectiveDivision);
screenPosition = (screenPosition / screenPosition.w);

为什么不直接顺便进行透视除法除以$w$?

原因是该函数需要尽可能复用,可能在顶点着色器中被调用,如果投影矩阵是非线性变换(透视投影相机)的,那么该函数的返回值通过一个插值器传递到片元着色器中(被线性插值后)的结果,与插值到片元着色器后再调用该函数的结果是不一样的。 举例:x1为-1,x2为2,两个顶点的w都为3,在顶点着色器中调用ComputeScreenPos并进行透视除法后,x1变为-1/3,x2变为2/3,再插值到片元着色器中,x1和x2的中点的值为1/6;如果是先插值到片元着色器,x1仍为-1,x2仍为2,x1和x2的中点的值为1,再做透视除法,结果为1/3。

_ProjectionParams.x:1表示投影已上下翻转(以匹配类似 OpenGL 的投影坐标),-1表示它尚未翻转

该函数一般用于采样屏幕空间贴图(比如深度纹理)

float4 screenPosition = ComputeScreenPos(positionCS);  // 范围为[0,w]
tex2Dproj(_CameraDepthTexture, screenPosition);  // tex2Dproj会先将uv先除以w再进行采样,即使用范围为[0,1]的uv进行采样

也等同于以下操作

float4 screenPosition = ComputeScreenPos(positionCS);
tex2D(_CameraDepthTexture, float2(screenPosition.xy / screenPosition.w))

虽然的函数名字似乎意味着会直接得到屏幕空间中的位置,但并不是这样的,我们仍需在片元着色器中除以它的w分量来得到真正的视口空间中的位置。那么,为什么Unity不直接在ComputeScreenPos中为我们进行除以w分量的这个步骤呢?为什么还需要我们来进行这个除法?这是因为,如果Unity在顶点着色器中这么做的话,就会破坏插值的结果。

我们知道,从顶点着色器到片元着色器的过程实际会有一个插值的过程(如果你忘了的话,可以回顾2.3.6小节)。如果不在顶点着色器中进行这个除法,保留x、y和w分量,那么它们在插值后再进行这个除法,得到的和就是正确的(我们可以认为是除法抵消了插值的影响)。但如果我们直接在顶点着色器中进行这个除法,那么就需要对和直接进行插值,这样得到的插值结果就会不准确。原因是,我们不可以在投影空间中进行插值,因为这并不是一个线性空间,而插值往往是线性的

顶点着色器中将顶点位置变换到齐次裁剪坐标空间下,进行输出(用SV_POSITION关键字标记的变量表示顶点着色器的输出)后,再由硬件做透视除法

参考自《Unity shader入门精要》2.3.2章节

参考:

Unity Shader中的ComputeScreenPos函数 - 知乎 (zhihu.com)

LinearEyeDepth

将深度缓冲或深度纹理中的(非线性)深度值(取值范围$[0,1]$)转换到观察空间下的(线性)深度值(即positionVS.z),结果取值范围为$[Near,Far]$。

为什么不是$[-Near,-Far]$?因为该函数还对Near和Far进行了取反:Near *= -1Far *= -1

// Z buffer to linear depth.
// Does NOT correctly handle oblique view frustums.
// Does NOT work with orthographic projection.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float LinearEyeDepth(float depth, float4 zBufferParam)
{
    return 1.0 / (zBufferParam.z * depth + zBufferParam.w);
}

Linear01Depth

等同于LinearEyeDepth()/Far,结果取值范围为$[0,1]$,0表示刚好在摄像机位置上(而不是近平面),1表示在远平面上。

// Z buffer to linear 0..1 depth (0 at camera position, 1 at far plane).
// Does NOT work with orthographic projections.
// Does NOT correctly handle oblique view frustums.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float Linear01Depth(float depth, float4 zBufferParam)
{
    return 1.0 / (zBufferParam.x * depth + zBufferParam.y);
}

Linear01DepthFromNear

从com.unity.render-pipelines.core@10.4.0开始增加的函数,0表示刚好在近平面上,1表示在远平面上。

// Z buffer to linear 0..1 depth (0 at near plane, 1 at far plane).
// Does NOT correctly handle oblique view frustums.
// Does NOT work with orthographic projection.
// zBufferParam = { (f-n)/n, 1, (f-n)/n*f, 1/f }
float Linear01DepthFromNear(float depth, float4 zBufferParam)
{
    return 1.0 / (zBufferParam.x + zBufferParam.y / depth);
}