• 翻译21 平面和线框着色


    使用屏幕空间导数寻找三角形法线
    使用生成的重心坐标创建线框
    线框宽度可配置 .

    made with Unity 2017.1.0.

    image最终效果

    1 平面着色渲染

       网格由三角形组成,定义三角形是平的。使用表面法向量来增加表面弯曲的视觉效果,这使得创建光滑的表面网格成为可能。然而,有时实际上想要显示平坦的三角形,调试网格数据等等。

        平面着色:为了使三角形看起来像平坦的表面,必须使用三角形自带的表面法线,而三角形表面法线又是通过三角形的三个顶点的法向量的平均得到,表面法线使得网格具有多面外观,称为平面着色。这也间接使得三角形之间不可能共享顶点,因为那样它们会共享面法线,而共享面法线会使得多个三角处于同一平面不能达到完美的弯曲视觉效果。 因此,最终会得到非常多的网格数据。 假如可以共享顶点那就太好了,后面会探讨共享面法线。

        线框:显示网格的线框也可能有用,这使得网格的连接视觉效果更加直观明显。理想情况下,我们可以使用自定义材质一次对任何网格进行平面着色和线框渲染。

    Shader "Custom/Flat Wireframe" { … }

    1.1 三角形法线-偏导函数方法

        由于三角形属于二维平面,因此在其平面上的每个点的面法线都相同。 因此,渲染三角形内的每个片段都使用相同的法线向量。 三角形面法线向量怎么计算? 在顶点程序vertex中,我们只能访问存储在网格的顶点数据,除了美术设计初始定义此处存储的向量就是法线向量用来表示三角形的法线,否则全部没用。 在片段程序fragment中,我们只能访问插值后的顶点法线。

    计算角形面法线向量:为了确定表面法线,我们需要知道三角形在世界空间中的方向。这可以通过三角形的三个顶点的位置来确定。 假定它是nondegenerate Triangle非退化三角(不共线三点),则其法线向量等于三角形任意两边的叉积结果的归一化值。 如果它是degenerate Triangle退化三角(),则无论如何都不会渲染。 因此,以逆时针方向给出三角形的顶点 a,b和c,其法线向量为n =(c-a)x(b-a)。 通过归一化,可以得到最终的单位法向矢量ˆn = n / | n |

    image

    三角形法线推导.

        实际上,我们不需要使用三角形的顶点。只要位于三角形平面内的任意三个点就可以。具体来说,我们只需要位于三角形平面内的两个向量,只要这两个向量不平行且大于零即可。
        有一种可以使用上述讨论的算法:渲染片段时使用的世界位置坐标。 例如,当前正在渲染的片段在屏幕空间中的世界坐标,就可以得到该片段在右侧的坐标以及片段在上方的坐标。

    image 
    使用片段的世界位置

       如果我们可以访问相邻片段的世界位置,那么上述算法就可以。虽然无法直接访问相邻片段的数据,但是我们可以访问此数据的screen-space derivatives(屏幕空间导数详细说明) 。 这是通过特殊指令完成的,该指令告诉我们屏幕空间X或Y维度中任何数据片段之间的变化率。

        简单解释屏幕空间导数:例如,我们当前片段的世界位置为P0,屏幕空间X维度的下一个片段的位置是Px。 因此,这两个片段之间的X维度上的世界位置变化率是∂p / ∂x = Px – P0。 这是屏幕空间X维度中世界位置的偏导数。 我们可以通过内置的ddx函数在片段程序fragment中获取此数据,参数是提供顶点的世界坐标。

    void InitializeFragmentNormal(inout Interpolators i) {
    	float3 dpdx = ddx(i.worldPos);
    	…
    }

    我们可以对屏幕空间的Y维度执行相同的操作,通过调用带有世界位置的ddy函数来查∂p / ∂y = Py – P0

    float3 dpdx = ddx(i.worldPos);
    float3 dpdy = ddy(i.worldPos);

        因为这些值表示了片段世界坐标之间的差值,所以它们定义了三角形的两条边。我们实际上不知道那个三角形的确切形状,但它肯定在原来三角形的平面上,这才是最重要的。所以最终的法向量就是这些向量的标准化叉积。用这个向量覆盖原来的法向量。

    float3 dpdx = ddx(i.worldPos);
    float3 dpdy = ddy(i.worldPos);
    i.normal = normalize(cross(dpdy, dpdx));
    ddx和ddy 是咋回事?
        首先,有助于了解GPU着色总是一次评估2x2像素块上的片段/像素。 (即使最终仅需要绘制其中一些像素,而其他像素
    位于多边形之外或被遮挡-不需要的片段也会被mask掉,而不是写入到缓冲区)。
    image
        着色器中变量(或表达式)v的屏幕空间导数是从2x2像素四边形的一侧到另一侧的v值(在代码中的该点)的差。 即ddx
    是右像素中v的值减去左像素中v的值,垂直方向上的ddy同样。
        这回答了“当我们在屏幕上水平(ddx)或垂直(ddy)移动时,v增加或减少的速度有多快?” -即用微积分的术语,它近
    似于变量的偏导数(近似值,因为它在每个片段上使用离散样本,而不是用数学方法评估函数的无穷小行为)
        对于标量,我们也可以把它看成一个梯度向量,∇v = float2(ddx(v), ddy(v)),他们指向屏幕空间中v增长最快的
    方向。这种类型的信息通常在内部用于选择纹理查找的适当的mipmap级别。对于大多数简单的效果,你不需要使用这些导数,
    因为基本的2D纹理采样方法会为你处理它。
    

        创建一个新的材质,使用我们的平面线框着色器。任何使用这种材质的网格都应该使用平面着色来渲染。它们看起来是多面的,虽然当你也使用法线贴图时可能很难看到。在本教程的截图中,我使用了标准的胶囊网格,使用灰色材质

    image  平滑和平面着色对比

    从远处看,胶囊看起来像是由四边形组成的,但这些四边形都是由两个三角形组成的。

    image

    由三角形组成的四边形

    1.2 三角形法线-几何着色阶段处理

        还有另一种方法可以确定三角形的法线。我们可以使用实际的三角形顶点来计算法向量,而不是使用导数指令。这需要用在每个三角形上,而不是每个顶点或片段上。这就是使用几何着色器的地方。

        几何着色器阶段位于顶点和片段程序阶段之间。它得到顶点程序的输出,按片元分组。在得到插值后和用于渲染片段之前,几何程序阶段可以修改这些数据。

    image处理每个三角形的顶点

        额外计算几何着色阶段的意义在于可以给每个元素都提供顶点,所以在我们的例子中每个三角形都有3个顶点。在这里的三角形网格是否共享顶点并不重要,因为几何程序会输出新的顶点数据。这允许我们获得三角形的法向量,并使用它作为所有三个顶点的法向量。

        添加几何着色器的代码文件[flatWireframe.cginc], 并定义一个MyGeometryProgram函数.

    #if !defined(FLAT_WIREFRAME_INCLUDED)
    #define FLAT_WIREFRAME_INCLUDED
    
    #include "My Lighting.cginc"
    
    void MyGeometryProgram () {}
    
    #endif

        注意:当shader model 4.0或更高时,几何体着色器才支持。如果model目标被定义得很低时,Unity会自动增加到这个目标级别,但是低端手机能不能支持该model就得人为调整。同时,要真正使用几何着色器,我们必须添加#pragma geometry指令,就像顶点和片段函数一样。最后,引用flatWireframe.cginc。将这些变化应用到平面着色器的basePass、additivePass和deferredPass中。

    #pragma target 4.0
    …
    #pragma vertex MyVertexProgram
    #pragma fragment MyFragmentProgram
    #pragma geometry MyGeometryProgram
    …
    
    //#include "My Lighting.cginc"
    #include "MyFlatWireframe.cginc"

        定义输出。回到编辑器将会得到着色器编译错误,因为我们还没有正确定义我们的几何函数。我们必须声明它会输出多少顶点。这个数字可以变化,所以我们必须提供一个最大值。因为我们使用的是三角形,所以每次调用总是输出三个顶点。这是通过向函数中添加maxvertexcount属性来指定的,参数为3。

    [maxvertexcount(3)]
    void GeometryProgram () {}

        定义输入。顶点程序的输出数据类型是插值后的顶点。所以在这种情况下

    [maxvertexcount(3)]
    void MyGeometryProgram (InterpolatorsVertex i) {}

        声明语义。但是类型名称在技术上是不正确的,那是我们在命名它时没有考虑到几何着色器。在我们的例子中是三角形。这必须在输入类型之前指定语义。另外,由于三角形每个都有三个顶点,它们可构成一个数组,也要明确地定义它。

    [maxvertexcount(3)]
    void MyGeometryProgram (triangle InterpolatorsVertex i[3]) {}

        因为几何着色器可以输出的顶点数量是不同的,所以没有一个单一的返回类型。相反,几何着色器写入到了片元流。在我们的示例中,它是一个TriangleStream,必须将其指定为inout参数。

    [maxvertexcount(3)]
    void MyGeometryProgram (
    	triangle InterpolatorsVertex i[3],
    	inout TriangleStream stream
    ) {}
    
    
    

    TriangleStream类似于c#中的泛型类型。它需要知道顶点类型,也就是定义的struct InterpolatorsVertex。

    [maxvertexcount(3)]
    void MyGeometryProgram (
    	triangle InterpolatorsVertex i[3],
    	inout TriangleStream<InterpolatorsVertex> stream
    ) {}
    
    
    

        现在函数定义完全正确了,接下来必须将顶点数据放入流中。这是通过对每个顶点调用流的Append函数来完成的,按照接收它们的顺序放入。

    [maxvertexcount(3)]
    void MyGeometryProgram (
    	triangle InterpolatorsVertex i[3],
    	inout TriangleStream<InterpolatorsVertex> stream
    ) {
    	stream.Append(i[0]);
    	stream.Append(i[1]);
    	stream.Append(i[2]);
    }

    一个自定义的几何程序阶段配置完成

    geometry program书写很怪啊!
    Unity的着色器语法混合了CG和HLSL代码。大多数情况下它看起来像CG,但在这种情况下它像HLSL。

    1.3 取出三角形的顶点法线

    基于1.2方法,开始计算每个三角的顶点法线。

    要找到三角形的法向量,首先要提取它的三个顶点的世界位置。

    float3 p0 = i[0].worldPos.xyz;
    float3 p1 = i[1].worldPos.xyz;
    float3 p2 = i[2].worldPos.xyz;
    
    stream.Append(i[0]);
    stream.Append(i[1]);
    stream.Append(i[2]);

    每个三角形做一次标准化的叉乘

    float3 p0 = i[0].worldPos.xyz;
    float3 p1 = i[1].worldPos.xyz;
    float3 p2 = i[2].worldPos.xyz;
    
    float3 triangleNormal = normalize(cross(p1 - p0, p2 - p0));

    将顶点法线替换为三角形法线.

    float3 triangleNormal = normalize(cross(p1 - p0, p2 - p0));
    i[0].normal = triangleNormal;
    i[1].normal = triangleNormal;
    i[2].normal = triangleNormal;

    image

    Flat shading, again.

    上图得到了和以前一样的结果,使用了几何着色阶段舞台而不依赖于屏幕空间的派生指令。

    哪种方法最好?
    如果你所需要的只是平面着色,那么屏幕空间的渐变是实现这种效果最便宜的方法。然后你还可以从网格数据中去除法线——Unity可以自动做到这一点——也可以移除法线插值数据。一般来说,如果你可以不使用自定义几何舞台,那么就这样做。我们将继续使用几何方法,因为我们也需要它来进行线框渲染。

    2 线框渲染

        在处理完平面着色之后,我们继续渲染网格的线框。我们不会创建新的几何程序,也不会使用额外的pass来绘制线框。我们将通过在三角形内部沿其边缘添加线条效果来创建线框视觉效果。尽管定义形状轮廓的线看起来只有内部线的一半粗,但足以创建一个令人信服的线框。

    After taking care of the flat shading, we move on to rendering the mesh's wireframe. We're not going to create new geometry, nor will we use an extra pass to draw lines. We'll create the wireframe visuals by adding a line effect on the inside of triangles, along their edges. This can create a convincing wireframe, although the lines defining a shape's silhouette will appear half as thick as the lines on the inside. This usually isn't very noticeable, so we'll accept this inconsistency.

    image 线框效果预览

    2.1 重心坐标

        要向三角形边缘添加线框效果,我们需要知道片段到最近边缘的距离。这意味着关于三角形的信息需要在片段程序中可用。这可以通过向内插数据中添加三角形的质心坐标来实现。

    image

    重心坐标计算


        因为网格数据不提供重心坐标,所以顶点程序不知道。为了让几何程序输出它们,我们必须定义一个新的结构。它应该包含与内插顶点相同的数据

    struct InterpolatorsGeometry {
    	InterpolatorsVertex data;
    };

      调整流数据类型,使其使用新的结构。

    void MyGeometryProgram (
    	triangle InterpolatorsVertex i[3],
    	inout TriangleStream<InterpolatorsGeometry> stream
    ) {
    	…
    
    	InterpolatorsGeometry g0, g1, g2;
    	g0.data = i[0];
    	g1.data = i[1];
    	g2.data = i[2];
    
    	stream.Append(g0);
    	stream.Append(g1);
    	stream.Append(g2);
    }

    添加额外的数据到插值几何,增加TEXCOORD9类型重心坐标变量.

    struct InterpolatorsGeometry {
    	InterpolatorsVertex data;
    	float3 barycentricCoordinates : TEXCOORD9;
    };

    给每个顶点分配一个质心坐标。

    g0.barycentricCoordinates = float3(1, 0, 0);
    g1.barycentricCoordinates = float3(0, 1, 0);
    g2.barycentricCoordinates = float3(0, 0, 1);
    
    stream.Append(g0);
    stream.Append(g1);
    stream.Append(g2);

    注意,质心坐标的总和总是1。只需要传递其中两个,通过从中减去两个坐标来得到第三个坐标。

    struct InterpolatorsGeometry {
    	InterpolatorsVertex data;
    	float2 barycentricCoordinates : TEXCOORD9;
    };
    
    	[maxvertexcount(3)]
    void MyGeometryProgram (
    	triangle InterpolatorsVertex i[3],
    	inout TriangleStream<InterpolatorsGeometry> stream
    ) {
    	…
    
    	g0.barycentricCoordinates = float2(1, 0);
    	g1.barycentricCoordinates = float2(0, 1);
    	g2.barycentricCoordinates = float2(0, 0);
    
    	…
    }

    2.2 定义额外的插值器

        将重心坐标传递给片段程序,但不能简单地使用这些数据,需在My Lighting.cginc文件定义宏:CUSTOM_GEOMETRY_INTERPOLATORS,来确定是否可使用。

    struct Interpolators {
    	…
    	#if defined (CUSTOM_GEOMETRY_INTERPOLATORS)
    		CUSTOM_GEOMETRY_INTERPOLATORS
    	#endif
    };

        现在我们可以在MyFlatWireframe中定义这个宏。我们必须在引入My Lighting之前做这个。我们也可以在插值几何中使用它,所以我们只需要写一次代码

    #define CUSTOM_GEOMETRY_INTERPOLATORS 
    	float2 barycentricCoordinates : TEXCOORD9;
    
    #include "My Lighting.cginc"
    
    struct InterpolatorsGeometry {
    	InterpolatorsVertex data;
    //	float2 barycentricCoordinates : TEXCOORD9;
    	CUSTOM_GEOMETRY_INTERPOLATORS
    };

    2.3 拆分文件

    我们如何使用重心坐标来可视化线框?也要照明参数不应参与计算。

    创建新文件myLightingInput.cginc

    #if !defined(MY_LIGHTING_INPUT_INCLUDED)
    #define MY_LIGHTING_INPUT_INCLUDED
    
    #include "UnityPBSLighting.cginc"
    #include "AutoLight.cginc"
    
    #if defined(FOG_LINEAR) || defined(FOG_EXP) || defined(FOG_EXP2)
        #if !defined(FOG_DISTANCE)
            #define FOG_DEPTH 1
        #endif
        #define FOG_ON 1
    #endif
    
    …
    
    float3 GetEmission (Interpolators i) {
        #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS)
            #if defined(_EMISSION_MAP)
                return tex2D(_EmissionMap, i.uv.xy) * _Emission;
            #else
                return _Emission;
            #endif
        #else
            return 0;
        #endif
    }
    
    #endif

    在myLighting.cginc删除重复代码

    #if !defined(MY_LIGHTING_INCLUDED)
    #define MY_LIGHTING_INCLUDED
    
    //#include "UnityPBSLighting.cginc"
    // …
    //
    //float3 GetEmission (Interpolators i) {
    //    …
    //}
    
    #include "My Lighting Input.cginc"
    
    void ComputeVertexLightColor (inout InterpolatorsVertex i) {
        #if defined(VERTEXLIGHT_ON)
            i.vertexLightColor = Shade4PointLights(
                unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
                unity_LightColor[0].rgb, unity_LightColor[1].rgb,
                unity_LightColor[2].rgb, unity_LightColor[3].rgb,
                unity_4LightAtten0, i.worldPos.xyz, i.normal
            );
        #endif
    }

    现在就可以在线框shaderMyFlatWireframe.分开计算了

    #include "My Lighting Input.cginc"
    
    #include "My Lighting.cginc"

    2.4 重写线框的Albedo

    定义线框专属宏ALBEDO_FUNCTION.

    #include "My Lighting Input.cginc"
    
    #if !defined(ALBEDO_FUNCTION)
        #define ALBEDO_FUNCTION GetAlbedo
    #endif

    用宏指令ALBEDO_FUNCTION.替换掉GetAlbedo 函数.

    float3 albedo = DiffuseAndSpecularFromMetallic
    (
        ALBEDO_FUNCTION(i), GetMetallic(i), specularTint, oneMinusReflectivity
    );

    在线框shader增加线框函数GetAlbedoWithWireframe , 它首先要计算一次原始的albedo,然后再计算线框!

    #include "My Lighting Input.cginc"
    
    float3 GetAlbedoWithWireframe (Interpolators i) {
        float3 albedo = GetAlbedo(i);
        return albedo;
    }
    
    #define ALBEDO_FUNCTION GetAlbedoWithWireframe
    
    #include "My Lighting.cginc"

    直接使用重心坐标作为反照率.

    float3 GetAlbedoWithWireframe (Interpolators i) {
        float3 albedo = GetAlbedo(i);
        float3 barys;
        barys.xy = i.barycentricCoordinates;
        barys.z = 1 - barys.x - barys.y;
        albedo = barys;
        return albedo;
    }
    image

    重心坐标as albedo

    2.5 Creating Wires

    为了创建线框效果,需要知道片段离最近的三角形边缘有多近。我们可以通过取重心坐标的最小值来得到它,在重心区域内得到了到边缘的最小距离,我们直接用它来表示反照率。

        float3 albedo = GetAlbedo(i);
        float3 barys;
        barys.xy = i.barycentricCoordinates;
        barys.z = 1 - barys.x - barys.y;
    //  albedo = barys;
        float minBary = min(barys.x, min(barys.y, barys.z));
        return albedo * minBary;

    image最小距离.

    统一以最近距离会导致粗细不一致,需要使用smoothstep 过渡

        float minBary = min(barys.x, min(barys.y, barys.z));
        minBary = smoothstep(0, 0.1, minBary);
        return albedo * minBary;
    image

    调整后的过渡

    2.6 固定宽度

    上述线框效果只适用于边长大致相同的三角形,同时有远近视角缘故,线有粗有细。两个方向的屏幕空间导数值可能有负有正,取它们的绝对值

    float minBary = min(barys.x, min(barys.y, barys.z));
    float delta = abs(ddx(minBary)) + abs(ddy(minBary));
    minBary = smoothstep(0, delta, minBary);

    fwidth 函数也可以表示上述两段代码!

    //float minBary = min(barys.x, min(barys.y, barys.z));
    //float delta = abs(ddx(minBary)) + abs(ddy(minBary));
    float delta = fwidth(minBary);
    
    image

    固定宽度线框

    如果觉得产生的线显得有点细,可以通过将过渡从边缘偏移一点来解决粗度,例如用与混合范围相同的值。

        minBary = smoothstep(delta, 2 * delta, minBary);
    image

    增厚的线框(有锯齿).

    产生更粗更清晰的线条,但也会在三角形附近的线条中显示锯齿。这些锯齿影的出现是由于这些区域最近的边缘过渡太突然,不连续的导致的。为了解决这个问题,我们必须先计算重心坐标的导数,再混合,然后在那之后获取最小值.

    /*先取出一次最近距离,先计算偏导取绝对值算出过渡*/
    barys.z = 1 - barys.x - barys.y;
    //float3 deltas = fwidth(barys);
    float minBary = min(barys.x, min(barys.y, barys.z));
    float delta = abs(ddx(minBary)) + abs(ddy(minBary));
    /*用过渡增加重心距离*/
    barys = smoothstep(deltas, 2 * deltas, barys);
    /*再取一次最小距离*/
    float minBary = min(barys.x, min(barys.y, barys.z));
    //float delta = fwidth(minBary);
    //minBary = smoothstep(delta, 2 * delta, minBary);
    return albedo * minBary;
    image

    高级线框.

    2.7 可配置线框

    线框效果有了,但有可能需要使用其他线宽、混合颜色,也许想对每种材料使用不同的设置。 因此,向着色器添加三个属性。

    首先是线框颜色,其次是线框平滑度,控制过渡范围。 从0到10的范围应该足够,默认值为1,代表宽度测量的倍数。 第三是线框厚度,其设置与平滑相同。

    _WireframeColor ("Wireframe Color", Color) = (0, 0, 0)
    _WireframeSmoothing ("Wireframe Smoothing", Range(0, 10)) = 1
    _WireframeThickness ("Wireframe Thickness", Range(0, 10)) = 1
    ...
    float3 _WireframeColor;
    float _WireframeSmoothing;
    float _WireframeThickness;
    
    float3 GetAlbedoWithWireframe (Interpolators i) {
        float3 albedo = GetAlbedo(i);
        float3 barys;
        barys.xy = i.barycentricCoordinates;
        barys.z = 1 - barys.x - barys.y;
        float3 deltas = fwidth(barys);
        float3 smoothing = deltas * _WireframeSmoothing;
        float3 thickness = deltas * _WireframeThickness;
        barys = smoothstep(thickness, thickness + smoothing, barys);
        float minBary = min(barys.x, min(barys.y, barys.z));
    //    return albedo * minBary;
        return lerp(_WireframeColor, albedo, minBary);
    }
    创建新的属性自定义着色器GUI。
        void DoWireframe () {
            GUILayout.Label("Wireframe", EditorStyles.boldLabel);
            EditorGUI.indentLevel += 2;
            editor.ShaderProperty(
                FindProperty("_WireframeColor"),
                MakeLabel("Color")
            );
            editor.ShaderProperty(
                FindProperty("_WireframeSmoothing"),
                MakeLabel("Smoothing", "In screen space.")
            );
            editor.ShaderProperty(
                FindProperty("_WireframeThickness"),
                MakeLabel("Thickness", "In screen space.")
            );
            EditorGUI.indentLevel -= 2;
        }
    public override void OnGUI (
        MaterialEditor editor, MaterialProperty[] properties
    ) {
        this.target = editor.target as Material;
        this.editor = editor;
        this.properties = properties;
        DoRenderingMode();
        if (target.HasProperty("_WireframeColor")) {
            DoWireframe();
        }
        DoMain();
        DoSecondary();
        DoAdvanced();
    }
    image

    可配置线框属性.

  • 相关阅读:
    Jquery复习DAY1(jQuery 选择器 jQuery 样式操作 jQuery 效果)
    git知识
    简单分析vue计算属性和methods方法的区别
    vue生命周期
    elementUI的级联选择器Cascader 高度不匹配问题高度溢出错误
    .Net Core3.1 常用的服务注入
    判断是否为AJAX请求
    JWT权限验证
    CodeSmith介绍
    常见ORM介绍
  • 原文地址:https://www.cnblogs.com/baolong-chen/p/13155566.html
Copyright © 2020-2023  润新知