zoukankan      html  css  js  c++  java
  • 翻译22 Unity中的曲面

    hull and domain渲染阶段
    三角细分
    控制细化

    Unity2017.1.0

    image最终效果.

    1 Hulls and Domains渲染阶段

    曲面细分是将表面切成更小更精细的技术。在我们的例子中,我们将三角形细分,最后得到覆盖相同空间的更多更小的三角形。
    GPU能够拆分渲染提供给它的三角形。 这样做有多种原因,当三角形的一部分最终被剪切时。 我们无法控制,但是还有一个细分阶段可以配置。 此阶段位于顶点vertex和片段fragment着色器阶段之间, 添加一个Hulls and Domains程序。

    image流水程序

    1.1 创建Tessellation Shader

    创建Tessellation.shader MyTessellation.cginc

    Shader "Custom/Tessellation" { … }
    #if !defined(TESSELLATION_INCLUDED)
    #define TESSELLATION_INCLUDED
    
    #endif

    当使用Tessellation,最小的着色器目标等级是4.6。如果我们不手动设置,Unity将会发出警告并自动使用该级别。我们要把镶嵌阶段添加到base_pass和additive_pass,deferred_pass,阴影通道没必要。

    #pragma target 4.6
    …
    #include "MyFlatWireframe.cginc"
    #include "MyTessellation.cginc"

    创建一个quad、材质.

    image

    它由两个等腰直角三角形组成。短边长度为1,长对角线长度为√2.

    1.2 Hull Shaders程序-整数模式

    类似geometry着色阶段,Hull程序可以处理三角形,四边形,或等值线,必须告诉它应该在什么表面上工作,并提供必要的数据。这是赫尔项目的工作。void函数开始

    void MyHullProgram () {}

    hull program在一个面片上运行,它作为参数传递给hull,添加一个InputPatch参数

    void MyHullProgram (InputPatch patch) {}

    面片上可能有许多网格顶点,必须指定顶点的数据格式。现在我们将使用VertexData结构体

    void MyHullProgram (InputPatch<VertexData> patch) {}

    当我们处理三角形时,每个面片将包含三个顶点。这个也必须指定为InputPatch的第二个模板参数

    void MyHullProgram (InputPatch<VertexData, 3> patch) {}

    hull程序的工作是传递所需的顶点数据到Tessellation阶段。虽然给它提供了一整个面片,但该函数每次只能处理面片中一个顶点,附加一个参数指定hull应该与哪个(顶点)一起工作。该参数是一个无符号整数,具有SV_OutputControlPointID语义

    void MyHullProgram (
        InputPatch<VertexData, 3> patch,
        uint id : SV_OutputControlPointID
    ) {}

    先只需将面片作为数组,根据索引id然后返回所需的顶点数据

    VertexData MyHullProgram (
        InputPatch<VertexData, 3> patch,
        uint id : SV_OutputControlPointID
    ) {
        return patch[id];
    }

    这看起来像一个函数程序

    #pragma vertex MyVertexProgram
    #pragma fragment MyFragmentProgram
    #pragma hull MyHullProgram
    #pragma geometry MyGeometryProgram

    这将产生一些编译错误,提示没有正确配置hull着色器。与几何函数一样,它需要属性来配置。首先,我们必须明确地告诉它它适用于三角形。这是通过使用tri作为参数的UNITY_domain属性完成的

    [UNITY_domain("tri")]
    VertexData MyHullProgram …

    还必须明确地指定每个三角形一个patch输出三个控制点,

    [UNITY_domain("tri")]
    [UNITY_outputcontrolpoints(3)]
    VertexData MyHullProgram …

    当GPU创建新的三角形时,它需要知道我们想要将它们定义为顺时针还是逆时针。像所有其他三角形一样,它们应该是顺时针的。这是通过UNITY_outputtopology属性来控制的。它的参数应该是triangle_cw

    [UNITY_domain("tri")]
    [UNITY_outputcontrolpoints(3)]
    [UNITY_outputtopology("triangle_cw")]
    VertexData MyHullProgram …

    GPU还需要通过UNITY_partitioning属性被告知如何分割-整数模式

    [UNITY_domain("tri")]
    [UNITY_outputcontrolpoints(3)]
    [UNITY_outputtopology("triangle_cw")]
    [UNITY_partitioning("integer")]
    VertexData MyHullProgram …

    除了划分方法,GPU还需要知道patch应该被分割成多少部分。这不是一个常数值,它可以在每个patch中变化。我们必须提供一个函数来计算它,叫做patch常数函数。

    [UNITY_domain("tri")]
    [UNITY_outputcontrolpoints(3)]
    [UNITY_outputtopology("triangle_cw")]
    [UNITY_partitioning("integer")]
    [UNITY_patchconstantfunc("MyPatchConstantFunction")]
    VertexData MyHullProgram …

    1.3 Patch常数函数

    每个patch仅被调用一次patch常数函数,而不是每个顶点都调用一次。实际上,此功能是与MyHullProgram并行运行。

    image

    为了确定如何细分三角形,GPU使用了四个细分因子。三角形patch的三个边都有一个因子,必须作为具有SV_TessFactor语义的float数组传递。 三角形的内部也有一个因子,使用SV_InsideTessFactor语义。

    struct TessellationFactors 
    {
        float edge[3] : SV_TessFactor;
        float inside : SV_InsideTessFactor;
    };

    patch常量函数以patch作为输入参数并输出因子。所有因子设置为1,这将导致tessellation阶段不细分patch。

    TessellationFactors MyPatchConstantFunction (InputPatch<VertexData, 3> patch) {
        TessellationFactors f;
        f.edge[0] = 1;
        f.edge[1] = 1;
        f.edge[2] = 1;
        f.inside = 1;
        return f;
    }

    1.4 Domain Shaders程序

    一个完整的曲面细分阶段需要Hull确定面片,也要Domain生成顶点。

    void MyDomainProgram () {}

    二者都作用在同一个阶段,再次使用UNITY_domain属性

    [UNITY_domain("tri")]
    void MyDomainProgram () {}

    Domain参数需要细分因子以及patch面片.

    [UNITY_domain("tri")]
    void MyDomainProgram (
        TessellationFactors factors,
        OutputPatch<VertexData, 3> patch
    ) {}

    曲面细分阶段确定细分时,它不会产生任何新的顶点。 相反,它会为这些顶点提供重心坐标。 使用这些坐标来导出最终顶点取决于Domain着色器。 为其提供重心坐标它们具有SV_DomainLocation语义。

    [UNITY_domain("tri")]
    void MyDomainProgram (
        TessellationFactors factors,
        OutputPatch<VertexData, 3> patch,
        float3 barycentricCoordinates : SV_DomainLocation
    ) {}

    在函数内部,我们必须生成最终的顶点数据。

    [UNITY_domain("tri")]
    void MyDomainProgram (
        TessellationFactors factors,
        OutputPatch<VertexData, 3> patch,
        float3 barycentricCoordinates : SV_DomainLocation
    ) {
        VertexData data;
    }

    为了找到这个顶点的位置,使用重心坐标。X、Y和Z坐标决定了第一个、第二个和第三个控制点的权重。

    VertexData data;
    data.vertex =
        patch[0].vertex * barycentricCoordinates.x +
        patch[1].vertex * barycentricCoordinates.y +
        patch[2].vertex * barycentricCoordinates.z;

    但是这就必须用同样的方法插值所有数据。让我们为它定义一个方便的宏,它可以用于所有的向量大小

    //    data.vertex =
    //        patch[0].vertex * barycentricCoordinates.x +
    //        patch[1].vertex * barycentricCoordinates.y +
    //        patch[2].vertex * barycentricCoordinates.z;    
    #define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = 
        patch[0].fieldName * barycentricCoordinates.x + 
        patch[1].fieldName * barycentricCoordinates.y + 
        patch[2].fieldName * barycentricCoordinates.z;
    
    MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)

    除了顶点位置,还有法线,切线,和所有UV坐标.

        MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
        MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
        MY_DOMAIN_PROGRAM_INTERPOLATE(tangent)
        MY_DOMAIN_PROGRAM_INTERPOLATE(uv)
        MY_DOMAIN_PROGRAM_INTERPOLATE(uv1)
        MY_DOMAIN_PROGRAM_INTERPOLATE(uv2)

    我们唯一没有插入的是实例id。因为Unity不同时支持GPU实例化和镶嵌,所以复制这个ID是没有意义的。为了防止编译错误,从三次着色器传递中删除多编译指令。这也将从着色器的GUI中移除实例化选项。

    //#pragma multi_compile_instancing
    //#pragma instancing_options lodfade force_same_maxcount_for_gl

    现在我们有了一个新的顶点,它将在这一阶段之后被发送到几何程序或插值器。但是这些程序期望插值顶点数据,而不是顶点数据。为了解决这个问题,让Domain着色器接管了原始顶点程序的职责。这是通过在其中调用MyVertexProgram来完成的——就像其他任何函数一样——并返回结果。

    [UNITY_domain("tri")]
    InterpolatorsVertex MyDomainProgram (
        TessellationFactors factors,
        OutputPatch<VertexData, 3> patch,
        float3 barycentricCoordinates : SV_DomainLocation
    ) {
        …
        return MyVertexProgram(data);
    }

    Now we can add the domain shader to our three shader passes, but we'll still get errors.

    #pragma hull MyHullProgram
    #pragma domain MyDomainProgram

    1.5 控制顶点

    MyVertexProgram只需要调用一次,只是我们改变了它调用的位置。但是我们仍然需要指定一个顶点程序在顶点着色器阶段被调用,它位于Hull着色器之前。在这一点上,我们不需要做任何事情,所以我们可以使用一个函数,它可以直接通过顶点数据,而不需要修改。

    VertexData MyTessellationVertexProgram (VertexData v) {
        return v;
    }

    使用这个函数给三个着色器的顶点程序传递数据。

    #pragma vertex MyTessellationVertexProgram

    这将产生另一个编译器错误,提示重用了位置语义。我们必须为我们的顶点程序使用另一个输出结构,使用INTERNALTESSPOS语义来表示顶点位置。结构体的其余部分与VertexData相同,只是它从来没有实例ID。将其命名为TessellationControlPoint。

    struct TessellationControlPoint {
        float4 vertex : INTERNALTESSPOS;
        float3 normal : NORMAL;
        float4 tangent : TANGENT;
        float2 uv : TEXCOORD0;
        float2 uv1 : TEXCOORD1;
        float2 uv2 : TEXCOORD2;
    };

    改变MyTessellationVertexProgram它会把顶点数据放入一个控制点结构中并返回那个值

    TessellationControlPoint MyTessellationVertexProgram (VertexData v) {
        TessellationControlPoint p;
        p.vertex = v.vertex;
        p.normal = v.normal;
        p.tangent = v.tangent;
        p.uv = v.uv;
        p.uv1 = v.uv1;
        p.uv2 = v.uv2;
        return p;
    }

    接下来,MyHullProgram也必须进行更改,以便使用TessellationControlPoint而不是VertexData。只有它的参数类型需要更改.

    TessellationControlPoint MyHullProgram (
        InputPatch<TessellationControlPoint, 3> patch,
        uint id : SV_OutputControlPointID
    ) {
        return patch[id];
    }

    patch常数函数也是如此。

    TessellationFactors MyPatchConstantFunction (
        InputPatch<TessellationControlPoint, 3> patch
    ) {
        …
    }

    Domain程序的参数类型也必须改变。

    InterpolatorsVertex MyDomainProgram (
        TessellationFactors factors,
        OutputPatch<TessellationControlPoint, 3> patch,
        float3 barycentricCoordinates : SV_DomainLocation
    ) {
        …
    }

    在这一点上,我们终于有了一个正确的Tessellation着色器。

    2 细分三角形

    整个Tessellation设置的要点是,我们可以细分patch。这允许我们用一组较小的三角形替换单个三角形。我们现在就来做。

    2.1 Tessellation Factors

    三角形的细分是由它的Tessellation因子控制的。我们在mypatchconstant函数中确定这些因素。目前,我们将它们都设置为1,这不会产生视觉变化。Hull、Domain通过原始的顶点数据并没有产生任何新的东西,将所有因子设置为2,效果立显。

    TessellationFactors MyPatchConstantFunction (
        InputPatch<TessellationControlPoint, 3> patch
    ) {
        TessellationFactors f;
        f.edge[0] = 2;
        f.edge[1] = 2;
        f.edge[2] = 2;
        f.inside = 2;
        return f;
    }

    image image

    Tessellation因子为2 vs 3.

    镶嵌因子是偶数时,中心有一个顶点。当它们是奇数时,中心三角形会有代替。如果我们使用较大的Tessellation因子,我们会得到多个三角形。每向中心移动一步,三角形被细分的数量就会减少2,直到最后得到1或0个子边。
    image

    2.1 不同的边和因子组合

    三角形如何细分是由因子控制。边缘因子可用于覆盖它们各自的边缘被细分的数量。这只会影响到原始的patch边缘,而不会影响到生成的内部三角形。为了清楚地看到这一点,将内因子设置为7,同时保持边因子为1。

    f.edge[0] = 1;
    f.edge[1] = 1;
    f.edge[2] = 1;
    f.inside = 7;
    image

    内7,边缘1.

    边缘因子也有可能大于内部因子。例如,将边缘因子设置为7,而将内部因子设置为1。

    f.edge[0] = 7;
    f.edge[1] = 7;
    f.edge[2] = 7;
    f.inside = 1;
    image

    内1 边缘7.

    在本例中,内部因子被强制执行为2,因为否则不会生成新的三角形。

    2.3 配置因子属性

    硬编码不好,增加可配置属性.

    float _TessellationUniform;
    …
    TessellationFactors MyPatchConstantFunction (
        InputPatch<TessellationControlPoint, 3> patch
    ) {
        TessellationFactors f;
        f.edge[0] = _TessellationUniform;
        f.edge[1] = _TessellationUniform;
        f.edge[2] = _TessellationUniform;
        f.inside = _TessellationUniform;
        return f;
    }

    增加1–64范围限制.

    _TessellationUniform ("Tessellation Uniform", Range(1, 64)) = 1

    GUI扩展.

    void DoTessellation () {
        GUILayout.Label("Tessellation", EditorStyles.boldLabel);
        EditorGUI.indentLevel += 2;
        editor.ShaderProperty
        (
            FindProperty("_TessellationUniform"),
            MakeLabel("Uniform")
        );
        EditorGUI.indentLevel -= 2;
    }

    在渲染模式和线框部分之间调用OnGUI中的这个方法。仅当所需属性存在时才执行此操作.

    public override void OnGUI (
        MaterialEditor editor, MaterialProperty[] properties
    ) {
        …
        DoRenderingMode();
        if (target.HasProperty("_TessellationUniform")) {
            DoTessellation();
        }
        if (target.HasProperty("_WireframeColor")) {
            DoWireframe();
        }
        …
    }
    image

    可配置.

    tesselation_integer

    2.4 增加分数模式

    尽管我们使用了一个浮点数来设置因子,但我们总是会以每条边的等效细分结束。这是因为我们使用的是整数模式。虽然这是一个很好的模式,看看镶嵌如何工作,它阻止我们平滑过渡之间的细分级别。幸运的是,还有分式划分模式。我们把模式改为fractional_odd。

    [UNITY_domain("tri")]
    [UNITY_outputcontrolpoints(3)]
    [UNITY_outputtopology("triangle_cw")]
    [UNITY_partitioning("fractional_odd")]
    [UNITY_patchconstantfunc("MyPatchConstantFunction")]
    TessellationControlPoint MyHullProgram …

    tesselation_odd1

    分数奇数分配

    当使用整个奇数因子时,fractional_odd分区模式产生的结果与整数模式相同。但在奇因子之间转换时,额外的边细分会被分离和增长,或者收缩和合并。这意味着边不再总是被分割成等长的段。这种方法的优点是细分层之间的过渡现在是平滑的。

    tesselation_odd2

    分数偶数分配

    3 Tessellation Heuristics

    什么因子是最完美的?这是个问题,但这个问题没有一个单一的客观答案。

    3.1 边缘优先

    细分因子必须提供,可以确定每个顶点的因子,然后对每条边进行平均。这些因子被储存在一个纹理中。在任何情况下,给定一条边的两个顶点,用一个单独的函数来确定系数是很方便的。

    float TessellationEdgeFactor (
        TessellationControlPoint cp0, TessellationControlPoint cp1
    ) {
        return _TessellationUniform;
    }

    使用这个函数来处理mypatchconstant函数内的边缘因子。

    TessellationFactors MyPatchConstantFunction (
        InputPatch<TessellationControlPoint, 3> patch
    ) {
        TessellationFactors f;
        f.edge[0] = TessellationEdgeFactor(patch[1], patch[2]);
        f.edge[1] = TessellationEdgeFactor(patch[2], patch[0]);
        f.edge[2] = TessellationEdgeFactor(patch[0], patch[1]);
        f.inside = _TessellationUniform;
        return f;
    }

    对于内因子,我们只需要用边因子的平均值.

    f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) * (1 / 3.0);

    3.2 基于边缘长度

    由于边因子控制细分多少原始三角形的边,以这些边的长度为基础的因子,这是有意义的。例如,我们可以指定所需的三角形边长。如果我们得到的三角形边比这个长,我们应该把它们再除以所需的长度。为此添加一个变量。

    float _TessellationUniform;
    float _TessellationEdgeLength;
    

    还可以添加属性。我们使用范围从0.1到1,默认值为0.5。这是世界空间单位。

    _TessellationUniform ("Tessellation Uniform", Range(1, 64)) = 1
    _TessellationEdgeLength ("Tessellation Edge Length", Range(0.1, 1)) = 0.5

    我们需要一个着色器功能,使它有可能在基于全部或基于边缘之间切换。使用_TESSELLATION_EDGE关键字,在我们的三次传递中添加必需的指令.

    #pragma shader_feature _TESSELLATION_EDGE

    接下来,在MyLightingShaderGUI中添加一个enum类型来表示镶嵌模式。

        enum TessellationMode {
            Uniform, Edge
        }

    GUI扩展

    void DoTessellation () {
            GUILayout.Label("Tessellation", EditorStyles.boldLabel);
            EditorGUI.indentLevel += 2;
    
            TessellationMode mode = TessellationMode.Uniform;
            if (IsKeywordEnabled("_TESSELLATION_EDGE")) {
                mode = TessellationMode.Edge;
            }
            EditorGUI.BeginChangeCheck();
            mode = (TessellationMode)EditorGUILayout.EnumPopup(
                MakeLabel("Mode"), mode
            );
            if (EditorGUI.EndChangeCheck()) {
                RecordAction("Tessellation Mode");
                SetKeyword("_TESSELLATION_EDGE", mode == TessellationMode.Edge);
            }
    
            if (mode == TessellationMode.Uniform) {
                editor.ShaderProperty(
                    FindProperty("_TessellationUniform"),
                    MakeLabel("Uniform")
                );
            }
            else {
                editor.ShaderProperty(
                    FindProperty("_TessellationEdgeLength"),
                    MakeLabel("Edge Length")
                );
            }
            EditorGUI.indentLevel -= 2;
        }
    image

    边缘模式

    现在我们必须调整TessellationEdgeFactor。当定义_TESSELLATION_UNIFORM时,确定两个点的世界位置,然后计算它们之间的距离。这是世界空间中的边长。边因子等于这个长度除以期望的长度.

    float TessellationEdgeFactor (
        TessellationControlPoint cp0, TessellationControlPoint cp1
    ) {
        #if defined(_TESSELLATION_EDGE)
            float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
            float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
            float edgeLength = distance(p0, p1);
            return edgeLength / _TessellationEdgeLength;
        #else
            return _TessellationUniform;
        #endif
    }
    image

    不同的四边形尺度,相同的期望边长.

    因为我们现在用边的长度来确定边的镶嵌因子,我们可以得到每条边的不同因子。你可以看到这种情况发生在四边形上,因为对角线比其他边长。当对四边形使用非均匀尺度,在一维中拉伸它时,这一点也变得很明显。

     image拉伸quad.

    要做到这一点,很重要的一点是,共享一条边的patch最终都要为那条边使用相同的镶嵌因子。否则,生成的顶点将不能沿着那条边匹配,这会在网格中产生可见的缺口。在我们的例子中,我们对所有的边使用相同的逻辑。唯一的区别是控制点参数的顺序。由于浮点数的限制,这在技术上可能会产生不同的因素,但差异将是非常小的,不会被注意到.

    3.3 基于屏幕空间边缘长度

    虽然我们现在可以控制世界空间中的三角形边长,但这与它们在屏幕空间中的显示方式并不一致。tessellation的要点是增加更多的三角形时。所以我们不想再细分那些看起来很小的三角形。所以让我们用屏幕空间的边长来代替。

    首先,改变边长属性的范围。我们将使用像素代替世界单位,所以5-100这样的范围更有意义。

    _TessellationEdgeLength ("Tessellation Edge Length", Range(5, 100)) = 50

    将世界空间计算替换为屏幕空间计算。要做到这一点,这些点必须转换到剪辑空间而不是世界空间。然后它们的距离在2D中确定,使用它们的X和Y坐标,除以它们的W坐标,将它们投影到屏幕上。

    //float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
    //float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
    //float edgeLength = distance(p0, p1);
    
    float4 p0 = UnityObjectToClipPos(cp0.vertex);
    float4 p1 = UnityObjectToClipPos(cp1.vertex);
    float edgeLength = distance(p0.xy / p0.w, p1.xy / p1.w);
    return edgeLength / _TessellationEdgeLength;

    现在我们在裁剪空间中得到了结果,它是一个大小为2的统一立方体,适合显示。为了转换为像素,我们必须按像素的显示大小进行缩放。实际上,由于显示器很少是方形的,为了得到最精确的结果,在确定距离之前,我们应该分别缩放X和Y坐标。但是让我们通过简单的缩放屏幕高度来看看它是什么样子.

    return edgeLength * _ScreenParams.y / _TessellationEdgeLength;
    image

    相同的世界尺寸,不同的屏幕显示尺寸.

    们的三角形边缘现在被细分根据他们是多大渲染。位置、旋转和缩放都会影响到这一点。结果,Tessellation的数量改变时,事情在运动。.

    3.4 基于视野距离

    纯粹依赖于边的视觉长度的一个缺点是,在世界空间中很长的边在屏幕空间中可能会非常小。这可能导致这些边根本没有被细分,而其他边被细分很多,当曲面细分被用来增加近距离的细节或生成复杂的轮廓。

    另一种方法是使用世界空间的边缘长度,但是根据视图距离调整因素。越远的东西,它应该在视觉上显得越小,因此它需要的镶嵌就越少。用边长除以边长到摄像机的距离。我们可以用边的中点来确定这个距离。

    //float4 p0 = UnityObjectToClipPos(cp0.vertex);
    //float4 p1 = UnityObjectToClipPos(cp1.vertex);
    //float edgeLength = distance(p0.xy / p0.w, p1.xy / p1.w);
    //return edgeLength * _ScreenParams.y / _TessellationEdgeLength;
    
    float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
    float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
    float edgeLength = distance(p0, p1);
    
    float3 edgeCenter = (p0 + p1) * 0.5;
    float viewDistance = distance(edgeCenter, _WorldSpaceCameraPos);
    return edgeLength / (_TessellationEdgeLength * viewDistance);

    我们仍然可以保持Tessellation依赖于显示大小,通过简单地分解屏幕高度,并保持我们的5-100滑块范围。注意,这些值不再直接对应于显示像素。当你改变相机的视场时,这一点是非常明显的,这根本不会影响Tessellation。所以这种简单的方法并不适用于使用可变视场的游戏,例如放大和缩小。

    return edgeLength * _ScreenParams.y / (_TessellationEdgeLength * viewDistance);
    image

    基于边缘长度和视图距离.

    3.5 基于内部因子

    虽然Tessellation可能看起来在这一点上工作得很好。当使用一个统一的四边形时,它不是很明显,但是当使用一个变形的立方体时,它就变得很明显。 image内部因素不正确的立方体.

    在立方体的情况下,组成一个面的两个三角形各有一个非常不同的内部因子。四边形和立方体面之间的唯一区别是定义三角形顶点的顺序。Unity的默认立方体不使用对称的三角形布局,而quad使用对称的三角形布局。这表明,边缘的顺序明显影响内部因子。我们只是取边缘因子的平均值,所以它们的顺序不重要。一定是别的什么地方出了问题。

    在计算内部因子时再次显式调用TessellationEdgeFactors函数。从逻辑上讲,这不应该有什么区别,因为我们只是执行了两次完全相同的计算。着色器编译器肯定会优化它.

    //f.inside = (f.edge[0] + f.edge[1] + f.edge[2]) * (1 / 3.0);
    f.inside =
        (TessellationEdgeFactor(patch[1], patch[2]) +
        TessellationEdgeFactor(patch[2], patch[0]) +
        TessellationEdgeFactor(patch[0], patch[1])) * (1 / 3.0);
    image

    内部因子正确的立方体.

    显然,这确实有效果,因为两面三角形现在最终都使用几乎相同的内部因子。这里发生了什么?

    patch常量函数与Hull着色器的其余部分并行调用。但实际上它可以变得更复杂。着色器编译器也能够并行化边缘因子的计算。MyPatchConstantFunction内部的代码被分解和部分复制,并被一个并行计算三个边缘因子的分支进程替换。一旦这三个过程都完成了,他们的结果被结合起来并用来计算内部因素。

    它不影响我们的着色器的结果,只会影响它的性能。不幸的是,在OpenGL Core生成的代码中有一个bug。在计算内部因子时,不使用三个边因子,而只使用第三个边因子。它只是访问了索引2三次,而不是索引0,1和2。所以我们总是得到一个内因子等于第三条边因子。

    对于patch常数函数,着色器编译器将并行化设置为优先级。它会尽快拆分,然后无法再优化TessellationEdgeFactor的重复调用。我们以三个程序结束,每个程序计算两个点的世界位置、距离、最终因子。然后还有一个计算内部因子的程序,现在它还计算三个点的世界位置,以及所涉及的所有距离和因素。由于我们现在正在对内部因子进行所有工作,因此对边缘因子也单独完成部分工作是没有意义的。

    事实证明,如果我们首先计算这些点的世界位置,然后分别对边缘和内部因子计算TessellationEdgeFactor,则着色器编译器将决定不为每个边缘因子分开单独的程序。我们最终得到了一个可以全部计算的流程。在这种情况下,着色器编译器确实优化了TessellationEdgeFactor的重复调用。

    float TessellationEdgeFactor (float3 p0, float3 p1) {
        #if defined(_TESSELLATION_EDGE)
    //        float3 p0 = mul(unity_ObjectToWorld, cp0.vertex).xyz;
    //        float3 p1 = mul(unity_ObjectToWorld, cp1.vertex).xyz;
            …
        #else
            return _TessellationUniform;
        #endif
    }
    
    TessellationFactors MyPatchConstantFunction (
        InputPatch<TessellationControlPoint, 3> patch
    ) {
        float3 p0 = mul(unity_ObjectToWorld, patch[0].vertex).xyz;
        float3 p1 = mul(unity_ObjectToWorld, patch[1].vertex).xyz;
        float3 p2 = mul(unity_ObjectToWorld, patch[2].vertex).xyz;
        TessellationFactors f;
        f.edge[0] = TessellationEdgeFactor(p1, p2);
        f.edge[1] = TessellationEdgeFactor(p2, p0);
        f.edge[2] = TessellationEdgeFactor(p0, p1);
        f.inside =
            (TessellationEdgeFactor(p1, p2) +
            TessellationEdgeFactor(p2, p0) +
            TessellationEdgeFactor(p0, p1)) * (1 / 3.0);
        return f;
    }
  • 相关阅读:
    洛谷 P4160 [SCOI2009]生日快乐 题解
    洛谷 P1041 传染病控制 题解
    洛谷 P3154 [CQOI2009]循环赛 题解
    洛谷 P1144 最短路计数 题解
    洛谷 P2296 寻找道路 题解
    洛谷 P1514 引水入城 题解
    洛谷 P2661 信息传递 题解
    洛谷 P3958 奶酪 题解
    洛谷 P3501 [POI2010]ANT-Antisymmetry 题解
    【LGR-069】洛谷 2 月月赛 II & EE Round 2 Div.2 A-C题解
  • 原文地址:https://www.cnblogs.com/baolong-chen/p/13172655.html
Copyright © 2011-2022 走看看