Unity Shader - ddx/ddy偏导函数测试,实现:锐化、高度图、Flat shading应用、高度生成法线

141 篇文章 34 订阅


ddx, ddy 说明

这两条指令用于对指定的寄存器,求其值在临近像素上的变化率,因为纹理坐标的梯度可以用来确定纹理当前被缩放的程度,可用该值来计算Mip层,另外它也可以用来计算Texel的跨越Size,由此求得正确的过滤宽度,从而纠正通常的线性过滤在远处由于过滤宽度错误而产生的失真。


DirectX - ddx, ddy

参考:hlsl函数 ddx ddy

下面是以 ddx 的 : derive_rtx 的 dxbc api 文档说明为例(derive_rty 基本上是一样的说明)
在这里插入图片描述

ddx(x), ddy(y)
// 其中x,y都是screen space x,y

OpenGL - dFdx, dFdy

GLSL叫:

dFdx(x), dFdy(y)

参考:An introduction to shader derivative functions - 有讲到 ddx, ddy 的计算原理,下面引用的是原文

Derivatives computation
During triangles rasterization, GPUs run many instances of a fragment shader at a time organizing them in blocks of 2×2 pixels. Derivatives are calculated by taking differences between the pixel values in a block; dFdx subtracts the values of the pixels on the left side of the block from the values on the right side, and dFdy subtracts the values of the bottom pixels from the top ones. See the image below where the grid represents the rendered screen pixels and dFdx, dFdy expressions are provided for the generic value p evaluated by the fragment shader instance at (x, y) screen coordinates and belonging to the 2×2 block highlighted in red.

翻译成中文就是:

偏导计算
在三角形光栅化是,GPU 都是以 2x2 的 block 片段来计算光栅化的。偏导计算于是由这block之间的片段的值来计算的;dFdx 计算并返回的是右边的片段减去左边的片段的值,而 dFdy 是有上减去下的值。查看下图的格式显示的就是 红色高亮的 2x2 block 中对应的 (x, y) 屏幕坐标上的片段。

(虽然这里说的是 2x2 block 的 fragment 来渲染,但,如果我们的几何体比较小、或是离镜头很远,那么光栅化是得出的片段很小的话,小到什么程度,刚好就是 1 pixel,就一个像素(一个片段),那这种算法来处理的话肯定是没法计算 ddx, ddy 的内容的,除非说,使用类似 AA(Anti Aliasing 坑锯齿方式)x2 的方式,实际渲染数据的分辨率大小是原来的 宽高的一赔,那么光栅出来的原本一个 片段,那么 AA x2之后,原本 1 pixel 大小的片段就变成了 2x2 大小的栅格块,那么就可以计算 ddx, ddy 了,如果小于 2x2 的就不用渲染下去了,因为AA x2之后的片段都不足以 2x2 block pixels 的话,那么正常(就是不AA情况下)情况下,可能连一个像素的大小都没有,所以可以直接跳过不处理,这应该是渲染管线中几何应用阶段后,光栅化阶段前的剔除过滤了)

在这里插入图片描述

要注意的是,不论HLSL还是GLSL中,偏导函数都只能在fragment shader阶段处理,因为它是求不同 fragment 的 val 差值


伪代码表示

// jave.lin 2021/11/30 - 后续补充的伪代码说明
// 在 fragment block (2x2) 光栅化后,得到的 fragment(片段)后的 shader 计算过程中的对应 寄存器 的值的差值
// ddx(val) = frag(x+1,y).val - frag(x,y).val; // 从右减左
// ddy(val) = frag(x,y-1).val - frag(x,y).val; // 从上减下

// =================
// jave.lin :
// 下面只是伪代码,只是意思意思的表达,真正的实现不一定是这样的
// 有可能还真的就是标记某个 fragment shader 的方法体对应的指令集
// 打个标记,然后将这个带上标记的数据在:2x2 block 的 frags 中做差值
// =================

// data type
const int INT_T_1 		= 1;
const int FLOAT_T_1		= 2;
const int INT_T_2 		= 3;
const int FLOAT_T_2 	= 4;
const int INT_T_3 		= 5;
const int FLOAT_T_3 	= 6;
const int INT_T_4 		= 7;
const int FLOAT_T_4 	= 8;

// addValT 是 偏导采样数据记录
struct ddValT
{
	int type; // 对应上面的 data type 常量枚举
	char* data;
};

// 单个片段的数据
struct FragInfo
{
	vector<ddValT*> ddValDatas;
	short sx, sy; // screen x, y
} frags;

// 所有片段的二维数据
FragInfo[][] frags;

// 当前 片段的数据
FragInfo* curFrag;

// FragShader
struct FragShader
{
	// 当前 上下文记录的 ddx, ddy 的索引
	int _curDDX_IDX;
	int _curDDY_IDY;
	void* __Func_Address__;
	void Reset()
	{
		_curDDX_IDX = 0;
		_curDDY_IDY = 0;
	}
	void Run(FragInfo* frag)
	{
		__Func_Address(frag);
	}
};


void DrawCall()
{
	...
	// 当前使用的 fragment shader
	auto& fragShader = ...;
	for (auto& frag : frags)
	{
		// 在上下文执行之前,先清除 ddx, ddy 的数据
		
		frag.ddValDatas.clear();
		fragShader.Reset();
		fragShader.Run(frag);
	}
	...
}

// RetT : 返回的数据类型
// ArgT : 参数的数据类型
// curFrag : 当前上下文片段信息类型
// frags: 当前上下文中所有片段的二维集合
RetT ddx(ArgT* val)
{
	FragShader& __context_cur_frag = ...;
	int dd_idx = __context_cur_frag._curDDX_IDX++;
	rightFrag.ddValDatas[dd_idx] = val;

	barrier(); // 先等所有的 ddx, ddy 都同步添加到 ddValDatas
	
	int rx = curFrag.x + 1, ry = curFrag.y;
	FragInfo* rightFrag = frags[rx][ry];
	ddValT* dd_data = rightFrag.ddValDatas[dd_idx];
	switch (dd_data->type)
	{
	case INT_T_1:
	{
		int* rval = (int*)dd_data->data;
		return *rval - *(int*)(val);
	}
	case FLOAT_T_1:
	{
		float* rval = (float*)dd_data->data;
		return *rval - *(float*)(val);
	}
	case INT_T_2:
	{
		int2* rval = (int2*)dd_data->data;
		return *rval - *(int2*)(val);
	}
	case FLOAT_T_2:
	{
		float2* rval = (float2*)dd_data->data;
		return *rval - *(float2*)(val);
	}
	case INT_T_3:
	{
		int3* rval = (int3*)dd_data->data;
		return *rval - *(int3*)(val);
	}
	case FLOAT_T_3:
	{
		float3* rval = (float3*)dd_data->data;
		return *rval - *(float3*)(val);
	}
	case INT_T_4:
	{
		int4* rval = (int4*)dd_data->data;
		return *rval - *(int4*)(val);
	}
	case FLOAT_T_4:
	{
		float4* rval = (float4*)dd_data->data;
		return *rval - *(float4*)(val);
	}
	default:
		throw Error("xxx");
	}
}

// ddy 类似上面,就不写了



可用它来做什么


简单的边缘突出应用

在下面Project提供的源Unity工程的 Sharpen.unity 场景


Shader

// jave.lin 2019.07.02
Shader "Test/TestDDX&Tex"
{
    Properties
    {
        [KeywordEnum(IncreaseEdgeAdj, BrightEdgeAdj)] _EADJ("Edge Adj type", Float) = 0
        _Tex("Tex", 2D) = "white" {}
        _Intensity("Intensity", Range(0, 20)) = 2
    }
    SubShader
    {
        Pass
        {
            Tags { "RenderType"="Opaque" }
            Cull off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _EADJ_INCREASEEDGEADJ _EADJ_BRIGHTEDGEADJ
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };
            sampler2D _Tex;
            float4 _Tex_ST;
            float _Intensity;
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _Tex);
                return o;
            }
            fixed4 frag (v2f i, float f : VFACE) : SV_Target
            {
                fixed a = 1;
                if (f < 0) a = 0.5;
                fixed3 c = tex2D(_Tex, i.uv).rgb;
                #if _EADJ_INCREASEEDGEADJ // 边缘调整:增加边缘差异调整
                // 类似两个3x3的卷积核处理
                /*
                one:
                | 0| 0| 0|
                | 0|-1| 1|
                | 0| 0| 0|

                two:
                | 0| 0| 0|
                | 0|-1| 0|
                | 0| 1| 0|
                */
                //使用(ddx(c) + ddy(c)),没有绝对值,会然边缘的像素亮度差异变大,即:加强边缘突出
                c += (ddx(c) + ddy(c)) * _Intensity;
                #else //_EADJ_BRIGHTEDGEADJ // 边缘调整:增加边缘亮度调整
                //c += abs(ddx(c)) + abs(ddy(c)) *_Intensity;
                c += fwidth(c) * _Intensity; // fwidth(c) ==> abs(ddx(c)) + abs(ddy(c))
                //使用fwidth函数,可以看出,会是边缘变亮,突出边缘
                // fwidth func in HLSL: https://docs.microsoft.com/zh-cn/windows/desktop/direct3dhlsl/dx-graphics-hlsl-fwidth
                #endif // end _EADJ_INCREASEEDGEADJ
                return fixed4(c, a);
            }
            ENDCG
        }
    }
}

在shader中,可以看到有两种方法,对应材质Inspector中的两个选项
在这里插入图片描述
IncreaseEdgeAdj=边缘突出-锐化-增加差值;BrightEdgeAdj=边缘突出-增加亮度

边缘突出-锐化-增加差值

在这里插入图片描述
使用(ddx(c ) + ddy(c )),没有绝对值,会然边缘的像素亮度差异变大,即:加强边缘突出

边缘突出-增加亮度

在这里插入图片描述
fwidth(c ) ==> abs(ddx(c )) + abs(ddy(c ))
使用fwidth函数,可以看出,会是边缘变亮,突出边缘


高度生成法线应用

高度图,法线图,都是属于凹凸图的其一

在下面Project提供的源Unity工程的 HeightMap.unity 场景


准备一张高度图

高度图博文

用PS或是GIMP随便画一个黑白的高度图就好了,注意我们的画笔需要设置成软笔刷,这样才会有渐变过渡,不然笔刷太硬,没啥过渡的灰度,那么shader渲染出来的法线角度太陡,就不太容易观察法线对光影的影响。

图片导出jpg就好了,不需要alpha,因为我们shader只要一个通道的值就好R通道。

如下图,我们用GIMP制图
在这里插入图片描述
导出到Unity中,再设置一下不需要alpha source,如下图
在这里插入图片描述
在导出来的jpg我们可以看到只有黑白

黑色表示没有越是接近黑色,说明高度越低,全黑,说明完全没有高度值影响
反之,白色说明就是有高度。

我们调整表面法线就就是用这些相邻像素的高度差异作为影响当前法线的水平、垂直(法线:xy)的因数,即可调整法线。

如果调整法线,如下图:
在这里插入图片描述


Shader

// jave.lin 2019.07.02
Shader "Test/TestDDX&HeightMap"
{
    Properties
    {
        [KeywordEnum(LMRTMB,CMRCML,NAVDDXPOSDDY)] _S ("Sample Type", Float) = 0
        _Color("Main Color", Color) = (1,1,1,1)
        _MainTex("Main Tex", 2D) = "white" {}
        _HightMap("Hight Map", 2D) = "white" {}
        _Intensity("Intensity", Range(0, 20)) = 5
        _SpecuarlIntensity("Specular Intensity", Range(0, 100)) = 80
        _SpecuarlStrengthen("Specular Strengthen", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "Queue"="Transparent" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _S_LMRTMB _S_CMRCML _S_NAVDDXPOSDDY

            #include "UnityCG.cginc"
			#include "Lighting.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
            };
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
                float3 normal : TEXCOORD3;
            };
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _HightMap;
            float4 _HightMap_TexelSize; // 1/w, 1/h, w, h
            float _Intensity;
            float _SpecuarlIntensity;
            float _SpecuarlStrengthen;
			inline float3x3 getTBN (inout float3 normal, float4 tangent) {
				float3 wNormal = UnityObjectToWorldNormal(normal);		    // 将法线从对象空间转换到世界空间
				float3 wTangent = UnityObjectToWorldDir(tangent.xyz);		// 将切线从对象空间转换到世界空间
				float3 wBitangent = normalize(cross(wNormal, wTangent));	// 根据世界空间下的法线,切线,叉乘算出世界空间下的副切线
                normal = wNormal;
				return float3x3(wTangent, wBitangent, wNormal);			    // 根据世界空间下的法线,切线,副切线,组合成TBN,可将切线空间下的法线转换到世界空间下
			}
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                float3x3 tbn = getTBN(v.normal, v.tangent);
                // w2t or t2w可以参考我之前写的:Unity Shader - 切线空间的法线贴图应用(T2W & W2T)
                // https://blog.csdn.net/linjf520/article/details/94165872
                o.lightDir = mul(tbn, normalize(_WorldSpaceLightPos0.xyz)); // w2t : world to tangent space
                o.viewDir = mul(tbn, normalize(_WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, v.vertex))); // w2t : world to tangent space
                o.normal = mul(tbn, v.normal); // w2t : world to tangent space
                return o;
            }
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
                // 三种采样方式:本质方法是一样的,类似两个3x3的卷积核处理
                #if _S_LMRTMB
                /*
                one:
                | 0| 0| 0|
                |-1| 0| 1|
                | 0| 0| 0|

                two:
                | 0|-1| 0|
                | 0| 0| 0|
                | 0| 1| 0|
                */
                // 这种方式是参考:Unity Shader-法线贴图(Normal)及其原理
                // https://blog.csdn.net/puppet_master/article/details/53591167
                float offsetU = tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(-1, 0)).r - tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(1, 0)).r;
                float offsetV = tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(0, 1)).r - tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(0, -1)).r;
                #elif _S_CMRCML
                /*
                one:
                | 0| 0| 0|
                | 0| 1|-1|
                | 0| 0| 0|

                two:
                | 0|-1| 0|
                | 0| 1| 0|
                | 0| 0| 0|
                */
                fixed cr = tex2D(_HightMap, i.uv).r;
                float offsetU = (cr - tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(1, 0)).r) * _Intensity;
                float offsetV = (cr - tex2D(_HightMap, i.uv + _HightMap_TexelSize * float2(0, -1)).r) * _Intensity;
                #else // _S_NAVDDXPOSDDY
                /*
                one:
                | 0| 0| 0|
                | 0|-1| 1|
                | 0| 0| 0|

                two:
                | 0| 0| 0|
                | 0|-1| 0|
                | 0| 1| 0|
                */
                fixed h = tex2D(_HightMap, i.uv).r;
                float offsetU = -ddx(h); // 右边像素采样 - 当前像素采样 = U的斜率,这里我们取反向,因为我们需要的是当前-右边的值,而ddx是固定的right-cur,所以我们只能取反
                float offsetV = ddy(h); // 下边像素采样 - 当前像素采样 = V的斜率,这里我们不用取反向,斜率方向刚刚好是我们需要的
                #endif // end _S_LMRTMB

                // 调整tangent space normal
                float3 n = normalize(i.normal.xyz + float3(offsetU, offsetV, 0) * _Intensity);
                // 为了测试法线,添加了diffuse与specular的光照因数
                // diffuse
                float ldn = dot(i.lightDir, n) * 0.5 + 0.5;
                fixed3 diffuse = _LightColor0.rgb * _Color * ldn * c.rgb * tex2D(_MainTex, i.uv);
                // specular
                float3 halfAngle = normalize(i.lightDir + i.viewDir);
                float3 hdn = max(0, dot(halfAngle, n));
                fixed3 specular = _LightColor0.rgb * _Color * pow(hdn, 100 - _SpecuarlIntensity) * _SpecuarlStrengthen;
                fixed3 combined = diffuse + specular;
                return fixed4(combined, 1);
            }
            ENDCG
        }
    }
}

其中有三种算法方式,前两种基本一样,支持采样坐标不太一样,最后一种就是使用偏导函数DDX,DDY来处理的


整体运行效果

在这里插入图片描述

还有三种不同算法的使用
在这里插入图片描述

采样质量从高到底(对应选项从上到下),最后一种就是DDX,DDY

无论用的是那种方式,我们都是采用高度图中相邻像素的灰度值(这里我们用R通道当灰度值,因为只有黑白,无所谓)相减,得到的差值我们当做是:当前像素就临近像素的高度斜率,然后用这个斜率调整对应水平、垂直的,法线:xy值。


Flat Shading 应用

在下面Project提供的源Unity工程的 ShowTBN.unity 场景


Shader

// jave.lin 2019.07.02
Shader "Test/TestDDX&TBN"
{
    Properties
    {
        _HightMap("Hight Map", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            struct v2f
            {
                float4 positionCS : SV_POSITION;
                float3 positionWS : TEXCOORD0;
            };
            sampler2D _HightMap;
            float4 _HightMap_TexelSize; // 1/w, 1/h, w, h
            v2f vert (float4 positionOS : POSITION)
            {
                v2f o = (v2f)0;
                o.positionCS = UnityObjectToClipPos(positionOS);
                o.positionWS = mul(unity_ObjectToWorld, positionOS);
                return o;
            }
            float4 frag (v2f i) : SV_Target
            {
            	// jave.lin 2021/11/30 修改了代码,优化了写法,可能会与 Project 中的代码不带一样
            	// 但是主要理解意思就够了
                float3 T = ddy(i.positionWS); // jave.lin : world space tangent = ddy(val) = 上片元.val - 下片元.val
                float3 B = ddx(i.positionWS); // jave.lin : world space bitangent = ddx(val) = 右片元.val - 左片元.val
                float3 N = normalize(cross(T, B)); // jave.lin : world space normal = 求法线
                //类似flat shader
                //平坦着色,没有插值,因为偏导函数只能在同一三角面内处理,没有插值的
                //return fixed4(N, 1);
                float3  L = normalize(UnityWorldSpaceLightDir(_WorldSpaceLightPos0.xyz)); // jave.lin : world space light dir
                float halfLambert = dot(L, N) * 0.5 + 0.5;
                return float4(halfLambert.xxx, 1);
            }
            ENDCG
        }
    }
}

主要看我们的法线如果求得

float3 T = ddy(i.positionWS); // jave.lin : world space tangent = ddy(val) = 上片元.val - 下片元.val
float3 B = ddx(i.positionWS); // jave.lin : world space bitangent = ddx(val) = 右片元.val - 左片元.val
float3 N = normalize(cross(T, B)); // jave.lin : world space normal = 求法线

就算在app to shader阶段没有 NORMAL 传入,我们也可以通过偏导函数来求得N 世界空间的法线

关于上面的 cross 叉乘,可以查看我之前写的一篇:Unity Shader - Billboard 广告板/广告牌 - BB树,BB投影 - 查看 向量叉乘的顺序 部分的内容,之前我写了一个 -ddx(val) 的写法是为了构建左手坐标的

然后添加了 diffuse 光照

但要注意,我们在像素阶段用ddx求出来的tbn是相对整个三角面的,所以没有插值,效果就如同Flat shader一样,没有对法线插值。

关于叉乘可以参考这篇:Basis Orientations in 3ds Max, Unity 3D and Unreal Engine

下面引用博主的两张图:
Basis orientations in 3ds Max, Unity 3D and Unreal Engine (right and left hand rules are shown)
Basis orientations in 3ds Max, Unity 3D and Unreal Engine (right and left hand rules are shown) - 在3ds Max, Unity 3D 和 Unreal 引擎 的基本朝向(左右手坐标的规则显示)

  • 3ds Max – right handed, z-up - 右手坐标,z 向上
  • Unity 3D – left handed, y-up - 左手坐标,y 向上
  • Unreal Engine – left handed, z-up - 左手坐标,y 向上

Right hand rules
右手坐标的 叉乘规则


运行效果

在这里插入图片描述


用于优化

如下,我在优化一篇高度雾的时候,使用的优化技巧:

#ifdef _HIGH_QUALITY_ON
				// jave.lin : method1:
				float2 xz1 = noise_wp.xz + fixed2(_DerivativeSampleGaps, 0);
				float2 xz2 = noise_wp.xz + fixed2(0, _DerivativeSampleGaps);
				float h = GetNoise(noise_wp.xz);
				//return h;
				float h1 = GetNoise(xz1);
				//return h1;
				float h2 = GetNoise(xz2);
				//return h2;
				float3 pos = float3(noise_wp.x, h, noise_wp.z);
				float3 posU = float3(xz1.x, h1, xz1.y);
				float3 posV = float3(xz2.x, h2, xz2.y);
				float3 N = normalize(cross(posV - pos, posU - pos));
#else
				// jave.lin : optimize method2:
				float h = GetNoise(noise_wp.xz);
				//return h2;
				float3 pos = float3(noise_wp.x, h, noise_wp.z);
				float3 N = normalize(cross(ddy(pos), ddx(pos)));
#endif

Project

TestDerivativeFunc 提取码: 7jur


References

  • 20
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值