C for Graphic:ddx/ddy

        最近有被群里好友问ddx/ddy的问题,本着帮助他人且提升自己的初衷,来一篇博客。

        ddx(a):returns approximate partial derivative with respect to window-space X(返回屏幕空间坐标x的近似偏导数)

        ddy(a):returns approximate partial derivative with respect to window-space Y(返回屏幕空间坐标y的近似偏导数)

float  ddx(float a);
float1 ddx(float1 a);
float2 ddx(float2 a);
float3 ddx(float3 a);
float4 ddx(float4 a);

half   ddx(half a);
half1  ddx(half1 a);
half2  ddx(half2 a);
half3  ddx(half3 a);
half4  ddx(half4 a);

fixed  ddx(fixed a);
fixed1 ddx(fixed1 a);
fixed2 ddx(fixed2 a);
fixed3 ddx(fixed3 a);
fixed4 ddx(fixed4 a);

        a:Vector or scalar of which to approximate the partial derivative with respect to window-space XorY(入参a是一个向量或者标量,用来计算屏幕空间x或y的近似偏导数)

        这是字面意思来自nvidia官方(ddx ddy),我相信多数开发者根本无法理解这两个docpage(其实是我当年看这两个page无法理解)。

        今天我们就来好好认识一下这两个函数。首先解释一下导数,微积分中导数用来求“变化率”,我写个例子认识一下:

        为了得到一个函数f(x)=y的图像上某个点的切线,我们可以通过k=(f(a+h)-f(a))/h(h无穷小)得到函数f(x)=y图像上(a,b)点的切线的斜率k,然后得到切线的函数y=kx-ka+b。

        而切线的斜率也就意味着“变化率”(也意味着函数f(x)=y的“走势”),切线斜率为0(切线与x轴平行)说明“无变化”,斜率为1(切线与y轴平行)说明“最大变化”。为了便于理解还是画图。

       

        可以理解吧?在微积分里:函数图像上一个点p1的切线的斜率就是这个点p1变化“无穷小”时得到的新点p2所组成的线段(或射线)的斜率。或者说f(x)=y的增量△y除以x的增量△x就是(x,y)的切线的斜率(即k=(f(x+h)-f(x))/(x+h-x))。

        那么数学上肯定不会用纯文字表示,这么多文字直接对应下面的公式:

       

        这里把f'(a)=f'(x)称为f(x)的导函数(微分函数),求f(x)的f'(x)称为求导运算(微分运算)。

        那么我们就可以理解的出来,求导也就意味着求“变化率”。索性再来个例子,求f(x)=x^2的导函数:

       

        得到f(x)=x^2的导数为2x,即可得到f(x)=x^2图像的离散状态下每个坐标点(a,a^2)的切线斜率为2a(同时可得切线方程y=2ax+a^2)。

        而导数和偏导数的区别就是原方程变量一元和多元的区别,本质则都是求导。

        回到ddx/ddy上来,根据上面介绍可以大致了解ddx/ddy是求”变化率“用的,ddx/ddy分别求屏幕空间x/y轴像素之间的“变化率”,这里的“变化最小值h”为1个像素。至于这个”变化率“的算法我们暂时不知道,这里我google找了一下,如下:

        The specific way the partial derivative is computed is implementation-dependent. Typically fragments are rasterized in 2x2 arrangements of fragments (called quad-fragments) and the partial derivatives of a variable is computed by differencing with the adjacent horizontal fragment in the quad-fragment.

        Derivatives are calculated by taking differences between the pixels in a quad. For instance, ddx will subtract the values in the pixels on the left side of the quad from the values on the right side, and ddy will subtract the bottom pixels from the top ones. The differences can then be returned as the derivative to all four pixels in the quad.

        上面说片段是以2x2排列栅格化,而在这个2x2矩阵中,如图(屏幕空间左下(0,0)右上(width,height)):

       

        ddx函数是右边的像素值-左边的像素值,ddy函数是上面的像素值-下面的像素值(那么片段以2x2为单位栅格化就好理解了,毕竟按照ddx/ddy算法的前置条件就需要横纵像素长度大于2)。结合图像就是ddx(p0UV) = p1RGBA-p0RGBA,ddy(p0UV) = p2RGBA-p0RGBA。但是我们前面看到的ddx/ddx函数的入参和出参分量是一致的(输入uv返回float2,输入color返回float4),所以ddx(p0RGBA)=p1RGBA-p0RGBA,ddy(p0RGBA)=p2RGBA-p0RGBA才对(那么这样看来ddx/ddy的算法比我们想象的简单得多,至少比最简单的一元二次函数求导算法简单)。为了验证我们的想法,代码测试:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DDXYTest : MonoBehaviour
{
    public RawImage srcImage;
    public RawImage destImage;
    public Material ddxyMat;

    void Start()
    {
        Texture2D srctex = new Texture2D(2, 2, TextureFormat.RGB24, false);
        Color srcp0 = new Color(0.5f, 0.5f, 0.5f, 0.5f);
        Color srcp1 = new Color(0.8f, 0.7f, 0.3f, 0.8f);
        Color srcp2 = new Color(0.9f, 0.4f, 1f, 0.9f);
        Color srcp3 = new Color(1f, 1f, 1f, 1.0f);
#if UNITY_EDITOR
        Debug.LogFormat("srcp0 = {0} srcp1 = {1} srcp2 = {2} srcp3 = {3}", srcp0, srcp1, srcp2, srcp3);
#endif
        srctex.SetPixel(0, 0, srcp0);
        srctex.SetPixel(1, 0, srcp1);
        srctex.SetPixel(0, 1, srcp2);
        srctex.SetPixel(1, 1, srcp3);
        srctex.Apply();
        srcImage.texture = srctex;
        RenderTexture destrt = new RenderTexture(2, 2, 0);
        Graphics.Blit(srctex, destrt, ddxyMat);
        Texture2D desttex = RT2Tex2D(destrt);
        destImage.texture = desttex;
        Color destp0 = desttex.GetPixel(0, 0);
        Color destp1 = desttex.GetPixel(1, 0);
        Color destp2 = desttex.GetPixel(0, 1);
        Color destp3 = desttex.GetPixel(1, 1);
#if UNITY_EDITOR
        Debug.LogFormat("destp0 = {0} destp1 = {1} destp2 = {2} destp3 = {3}", destp0, destp1, destp2, destp3);
#endif
    }

    private Texture2D RT2Tex2D(RenderTexture rt)
    {
        Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
        RenderTexture.active = rt;
        tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
        tex.Apply();
        return tex;
    }
}

           cg shader:

Shader "Sharpen/TestDDXYShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                col = ddx(col);
                return col;
            }
            ENDCG
        }
    }
}

           上面用一个shader处理srcTex到destTex(srcTex赋值好4个pixel,经过frag函数中ddx处理得到destTex),得到如下数据:

 

            将ddx改为ddy:

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                col = ddy(col);
                return col;
            }

           得到如下数据:

 

            那么结合上面p0、p1、p2、p3像素图,同时心算一下log中的像素值。可以得到如下结论:

            1.ddx(p0RGBA)=float4(clamp01(p1RGB-p0RGB),1.0),且R分量存在0.002的偏移(不知道是什么原因造成,是因为纹理映射缩放导致了数据蠕动吗?)

            2.ddy(p0RGBA)=float4(clamp01(p2RGB-p0RGB),1.0),且B分量存在0.002的偏移

            3.destTex中p0、p1、p2、p3全是ddx/ddy(p0RGBA)的值(是否因为纹理为2x2导致p1、p2、p3无法计算ddx/ddy,则共享p0的ddx/ddy)

           下面修改一下代码将srcTex做成NxN的纹理。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class DDXYNxNTest : MonoBehaviour
{
    public int N;
    public RawImage srcImage;
    public RawImage destImage;
    public Material ddxyMat;

    void Start()
    {
        Texture2D srctex = new Texture2D(N, N, TextureFormat.RGB24, false);
        for (int x = 0; x < N; x++)
        {
            for (int y = 0; y < N; y++)
            {
                Color pixel = new Color((float)(x + 1) / (float)N, (float)(y + 1) / (float)N, (float)(x + 1) / (float)N + (float)(y + 1) / (float)N, 0);
                srctex.SetPixel(x, y, pixel);
            }
        }
#if UNITY_EDITOR
        for (int y = N - 1; y >= 0; y--)
        {
            string log = string.Format("row:{0} ", y);
            for (int x = 0; x < N; x++)
            {
                Color col = srctex.GetPixel(x, y);
                log += string.Format("srcP{0}:{1} ", x + N * y, col);
            }
            Debug.LogFormat(log);
        }
        Debug.LogFormat("===================================================================================");
#endif
        srctex.Apply();
        srcImage.texture = srctex;
        RenderTexture destrt = new RenderTexture(N, N, 0);
        Graphics.Blit(srctex, destrt, ddxyMat);
        Texture2D desttex = RT2Tex2D(destrt);
        destImage.texture = desttex;
#if UNITY_EDITOR
        for (int y = N - 1; y >= 0; y--)
        {
            string log = string.Format("row:{0} ", y);
            for (int x = 0; x < N; x++)
            {
                Color col = desttex.GetPixel(x, y);
                log += string.Format("destpP{0}:{1} ", x + N * y, col);
            }
            Debug.LogFormat(log);
        }
#endif
    }

    private Texture2D RT2Tex2D(RenderTexture rt)
    {
        Texture2D tex = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
        RenderTexture.active = rt;
        tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
        tex.Apply();
        return tex;
    }
}

           设N=3,使用ddx。得到结果如下:

           设N=4,使用ddx。得到结果如下:

          可以看得出来(结合log中红框心算一下):ddx计算确实是以2x2像素矩阵为单位共享Pixel0的ddx值(不满足ddx计算条件的Pixel返回(0,0,0,1)),同理ddy计算也是以2x2像素矩阵为单位共享Pixel0的ddy值(不满足ddy计算条件的Pixel返回(0,0,0,1))。

          设N=3,使用ddy,如图:

 

          设N=4,使用ddy,如图:

 

            那么我们总结:

            1.以2x2像素矩阵共享ddx(p0RGBA)=float4(clamp01(p1RGB-p0RGB),1.0)

            2.以2x2像素矩阵共享ddy(p0RGBA)=float4(clamp01(p2RGB-p0RGB),1.0)

            3.不满足ddx/ddy计算的”边缘“像素返回(0,0,0,1)

            到这里我相信小伙伴们都十分清晰的理解ddx/ddy的含义和计算了吧?那么ddx/ddy有什么应用场景呢?图形学里不可能光是丢两个计算函数在那放着吧?其实ddx/ddy能解决一个很常见的问题,就是边缘查找(以前我们用sobal滤波器查找边缘,现在也可以用ddx/ddy查找),因为偏导数就是为求”变化率“,如果像素矩阵”变化率“过大(大于某个设定值),那么就认为这里就是一个”颜色边缘“。

Shader "DDXY/DDXYEdgeShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _DDPower("DD Power",Range(1,5)) = 1
        _LuminThred("Lumin Threshold",Range(0,1)) = 1
        _LuminPower("Lumin Power",Range(1,2)) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            float _DDPower;
            float _LuminThred;
            float _LuminPower;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 xcol = ddx(col);
                fixed4 ycol = ddy(col);
                fixed4 xycol = (xcol+ycol)*_DDPower;
                //根据yuv编码的y亮度判断边缘
                float lumin = 0.299*xycol.r+0.587*xycol.g+0.114*xycol.b;    
                if(lumin>_LuminThred)
                {
                    col*=_LuminPower;
                }
                return col;
            }
            ENDCG
        }
    }
}

            效果如下:

           可以查找像素变化差异大的”边缘“,当然了ddx/ddy应用场景不止这个,先搞懂原理以后有时间再聊。

猜你喜欢

转载自blog.csdn.net/yinhun2012/article/details/109393911