unity曲面细分着色器(下):启发,更高级的曲面细分控制

unity曲面细分着色器(下):启发,更高级的曲面细分控制

目录

参考

Flat and Wireframe Shading Derivatives and Geometry:https://catlikecoding.com/unity/tutorials/advanced-rendering/flat-and-wireframe-shading/

Tessellation Subdividing Triangles:https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/

3.3 曲面细分与几何着色器—大规模草渲染:https://www.yuque.com/sugelameiyoudi-jadcc/okgm7e/xyx5h5#eI5Sb

最好的分数因数tessellation factors是什么?这个问题没有一个完全客观的答案

但是你可以想出一些可以作为启发式方法并产生良好结果的指标。

设置边因素Edge Factors

尽管曲面细分是针对每个边作用,但是这个指标不一定直接基于三角形的边。例如你可以确定每个顶点的因子然后把他们按边平均起来。因子还有可能是存储在一个贴图Texture中。

不管怎么样,对于给定边的两个控制点,有一个单独的函数来确定因子会很方便。

如图,先简单搭建这么一个框架,直接让他返回_TessellationUniform

对于inside factor,我们令他为三条边的平均(实际上和之前的代码没有区别)

在这里插入图片描述

控制细分边长Edge Length

上述的边因素控制了如何切分一个边,我们可以基于边长来确定这个边因素

比方说我们可以指定一个边长,如果这个三角形的边长比指定的边长长,曲面细分的时候我们就把他削减到指定的边长
在这里插入图片描述

这又是一种细分模式,所以在shader中添加一个shader feature和开关用于切换普通模式和指定边长表面细分模式

这里只有两个变量,所以直接用开关就好
在这里插入图片描述

在这里插入图片描述

原文用了custom GUI,用到了枚举,方便阅读(其实他就两个变量,一个开关就够了)

这里我们没写自定义GUI,也可以做一个枚举试试,但是shader feature就要改变了
在这里插入图片描述

在这里插入图片描述

现在要开始改写TessellationEdgeFactor了,前面什么都没做

如果开启了_TESSMODE_EDGE,就确定一条边上两个点的世界空间下的坐标,然后计算他们世界空间下的距离,最后返回的Edge factor就是计算的这个距离除以我们设定的距离

没开启那就还是返回_TessellationUniform
在这里插入图片描述

请添加图片描述

我们可以给不同的边用不同的factor进行切分,在四边形中最为常用,毕竟对角线比其他的边都长。当对四边形使用非均匀比例,将其在一维上拉伸时,这一点也会变得明显。
在这里插入图片描述

当然了,很重要的一点就是共享一条边的patchs,他们对这条边的切分方法应该是一致的,否则这条边上的顶点就会不匹配,就会造成模型断裂等情况

我们对于每条边使用的都是相同的切分逻辑,唯一的不同点可能就在于控制点参数的顺序,由于浮点限制,这在技术上可能会产生不同的因素,但差异非常小,难以察觉。

具体代码

TessellationShader

Shader "Unlit/TessellationShader"
{
    Properties {
        _MainTex("MainTex",2D)="white"{}
        _DiffuseIntensity("DiffuseIntensity",float)=1
        _SpecularIntensity("SpecularIntensity",float)=1
        _Shineness("Shineness",float)=0.5

        [Header(WireFrame)]
        [Toggle]_WireFrameEnable("WireFrameEnable",float)=1
        [Toggle]_FlatShading("FlatShading",float)=1
        _WireFrameColor("WireFrameColor",Color)=(0,0,0,1)
        _WireframeSmoothing ("Wireframe Smoothing", Range(0, 10)) = 1
    _WireframeThickness ("Wireframe Thickness", Range(0, 10)) = 1

        [Header(Tessellation)]
        [KeywordEnum(Uniform,Edge)]_TessMode("TessMode",int)=0
        _TessellationUniform("TessellationUniform",Range(1,64))=1
        _TessellationEdgeLength("TessellationEdgeLength",Range(0.1,1))=0.5
    }
    SubShader
    {
        pass
        {
            Tags{"LightMode"="ForwardBase"}
            CGPROGRAM
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            #include "FlatWireFrame.cginc"
            #include "MyTessellation.cginc"
            #include "vertexProcess.cginc"
            #pragma shader_feature _WIREFRAMEENABLE_ON
            #pragma shader_feature _ _TESSMODE_UNIFORM _TESSMODE_EDGE
            #pragma vertex MyTessellationVertexProgram
            #pragma fragment frag
            #pragma hull MyHullProgram
            #pragma domain MyDomainProgram
            #pragma geometry MyGeometryProgram
            #pragma multi_compile_fwdbase
            #pragma target 4.6

            void InitializeFragmentNormal(inout VertexOutput i)
            {
                float3 dpdx = ddx(i.vertexWS);
                float3 dpdy = ddy(i.vertexWS);
                i.normalWS = normalize(cross(dpdy, dpdx));
            }

            sampler2D _MainTex;
            float _DiffuseIntensity,_SpecularIntensity,_Shineness;
            float4 _WireFrameColor;
            float _WireframeThickness,_WireframeSmoothing,_WireFrameEnable;
            

            float4 frag(VertexOutput i) : SV_TARGET {
                
                // InitializeFragmentNormal(i);
                // 阴影
                UNITY_LIGHT_ATTENUATION(atten,i,i.vertexWS);

                float4 ambient=tex2D(_MainTex,i.uv)*atten*unity_AmbientSky;
                
                // Lambert:ambient+kd*dot(N,L)
                float3 lightDir=_WorldSpaceLightPos0;
                float3 normalDir=normalize(i.normalWS);
                float4 Lambert=ambient+_DiffuseIntensity*max(0,dot(normalDir,lightDir))*_LightColor0;

                // Blinn-Phong:ks*dot(N,H)
                float3 viewDir=normalize(_WorldSpaceCameraPos-i.vertexWS);
                float3 H=normalize(lightDir+viewDir);
                float4 BlPhong=_SpecularIntensity*pow(max(0,dot(normalDir,H)),_Shineness);

                float4 result=Lambert+BlPhong;
                #if _WIREFRAMEENABLE_ON
                    float3 barys=getBarysWithWireframe(i);
                    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));
                    
                    float4 finalBary=lerp(_WireFrameColor, (Lambert+BlPhong), minBary);
                    result=finalBary;
                #endif
                
                return result;
            }

            ENDCG
        }
        pass
        {
            Tags{"LightMode"="ShadowCaster"}
            CGPROGRAM
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_shadowcaster
            struct VertexInput
            {
               float4 vertex:POSITION;
               float3 normal:NORMAL;
            };

            struct v2f
            {
                V2F_SHADOW_CASTER;
            };

            v2f vert(VertexInput v)
            {
                v2f o;
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                return o;
            }

            fixed4 frag(v2f i):SV_TARGET
            {
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
        }
    }
}

MyTessellation.cginc

#if !defined(MYTESSELLATION_INCLUDED)
#define MYTESSELLATION_INCLUDED
#include "vertexProcess.cginc"
struct TessellationFactors
{
    float edge[3]:SV_TessFactor;
    float inside:SV_InsideTessFactor;
};

float _TessellationUniform;

float _TessellationEdgeLength;

float TessellationEdgeFactor(
    TessellationControlPoint cp0,
    TessellationControlPoint cp1
){
    // 确定一条边上两个点的世界空间下的坐标,然后计算他们世界空间下的距离,最后返回的Edge factor就是计算的这个距离除以我们设定的距离
    #if _TESSMODE_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
}

TessellationFactors MyPatchConstantFuncition(
    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 = (f.edge[0] + f.edge[1] + f.edge[2]) * (1 / 3.0);
    return f;
}

[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("fractional_odd")]
[UNITY_patchconstantfunc("MyPatchConstantFuncition")]
TessellationControlPoint MyHullProgram(InputPatch<TessellationControlPoint,3> patch,
    uint id: SV_OutputControlPointID)
{
    return patch[id];
}

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

[UNITY_domain("tri")]
VertexOutput MyDomainProgram(
    TessellationFactors factors,
    OutputPatch<TessellationControlPoint,3> patch,
    float3 barycentricCoordinates:SV_DomainLocation
)
{
    VertexInput data;
    MY_DOMAIN_POGRAM_INTERPOLATE(vertex)
    MY_DOMAIN_POGRAM_INTERPOLATE(normal)
    MY_DOMAIN_POGRAM_INTERPOLATE(tangent)
    MY_DOMAIN_POGRAM_INTERPOLATE(uv)

    return vert(data);
}
#endif

vertexProcess.cginc没有任何改变,参照前文

屏幕空间下的细分边长控制

目前为止都是基于世界空间中的边长做细分,这和屏幕空间中的显示不符合

曲面细分的要点就在于:在需要的时候添加更多的三角形

如果给在屏幕中很小一块的图形添加很多三角形,那会很不划算

所以我们要使用基于屏幕空间的细分边长控制

首先,修改EdgeLength的范围,现在是以像素为单位,而不是世界空间下的长度,所以范围修改为5~100
在这里插入图片描述

接着把edge factor里面的计算从世界空间切换到相机空间,这样两点之间的距离就是2d屏幕上的距离

首先把他们转换到裁剪空间,然后使用xy坐标除以他们的w坐标来把它投影到屏幕上(但是这里其实得到的是NDC归一化设备坐标,而且范围是-1~1)
在这里插入图片描述

也就是在一个长为2的立方体空间内,所以还必须根据像素的显示尺寸进行缩放。
在这里插入图片描述

一般来说,显示屏很少是正方形的,所以确认两点距离之前我们分别要对xy进行缩放,但是先只对y轴缩放看看什么样子
在这里插入图片描述

如图,现在他的细分取决于他在屏幕空间中显示的大小,他的位置旋转和缩放都会对此产生影响
请添加图片描述

代码

float TessellationEdgeFactor(
    TessellationControlPoint cp0,
    TessellationControlPoint cp1
){
    // 确定一条边上两个点的世界空间下的坐标,然后计算他们世界空间下的距离,最后返回的Edge factor就是计算的这个距离除以我们设定的距离
    #if _TESSMODE_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);

        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;
        
    #else
        return _TessellationUniform;
    #endif
}

是否应该使用一半的屏幕高度?

裁剪空间的这个空间立方体范围为-1~1,xy分别对应了显示器的宽和高,这意味着我们最终会得到两倍的边长尺寸,结果就是他实际细分的边长只有我们目标边长的一半

但是这不重要,使用屏幕高度的主要目的是用显示分辨率控制曲面细分,边长是否和我们设定的这个_TessellationEdgeLength匹配不重要
在这里插入图片描述

使用片元到相机的距离来控制曲面细分

完全依靠视觉长度来控制曲面细分的一个缺点就在于:

世界空间下很长的一条线可能会在屏幕上只显示很小一块区域,而这会导致他根本没有做曲面细分,但是其他的边又被细分太多次了

这并不是我们想要的

所以另一种不同的方法就是还是使用世界空间下的边长,但是依据view distance来调整这个factor控制细分,近大远小,越远的地方他的曲面细分等级越低

我们可以用这个边的中点到相机的距离来确定整条边到相机的距离

只要返回的时候依然考虑屏幕中的像素高度(*_ScreenParams.y),我们仍然可以实现基于屏幕空间大小的曲面细分控制,滑动条范围还是5~100

但是他现在代表的不再是屏幕空间像素,你在切换摄像机fov的时候就能注意到切换fov并不会影响曲面细分参数

所以这种方法对于使用可变视野(例如放大和缩小)的游戏并不适用。
在这里插入图片描述
请添加图片描述

代码

float TessellationEdgeFactor(
    TessellationControlPoint cp0,
    TessellationControlPoint cp1
){
    // 确定一条边上两个点的世界空间下的坐标,然后计算他们世界空间下的距离,最后返回的Edge factor就是计算的这个距离除以我们设定的距离
    #if _TESSMODE_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);

        float3 edgeCenter=(p0+p1)*0.5;
        float viewDistance=distance(edgeCenter,_WorldSpaceCameraPos);

        // 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;
        return edgeLength*_ScreenParams.y/(_TessellationEdgeLength*viewDistance);
    #else
        return _TessellationUniform;
    #endif
}

使用正确的inside参数

尽管目前的效果看起来还行,但是曲面细分的inside factor可能会出现一些奇怪的情况

在使用Opengl内核的时候,变形立方体的这一点就尤为明显

在这里插入图片描述

如图,一个面的两个三角形,使用了不同的inside factor导致了错误

quad和cube的唯一区别就在于三角形顶点的定义顺序,unity默认的cube不使用堆成三角形布局,但是quad使用对称三角形布局

但是我们这里的inside factor只是简单的对三条边的factor做了个平均,所以其实顺序无所谓,如果是其他求inside方法,可能会出错

如图,这么写,逻辑上和上面的其实没有任何区别,实际上就是做了两次同样的运算,编译器肯定会对他进行优化
在这里插入图片描述

但是出现了不同,他的效果正确了(我的效果本来就是对的所以演示不出来,用的原文的图)
在这里插入图片描述

为什么会这样?

由前文得知,patch constant functionhull shader是并行运行的,但实际情况可能比这更加复杂

shader编译器可能也会让edge factors 的计算也并行运行:用于计算edge factorsMyPatchConstantFuncition会被切分并部分复制,然后用3个并行运行的子进程替代,三个子进程处理完毕之后他们的结果就会被整合在一起然后去计算inside factor

不管编译器是否用子进程计算,他都不应该影响结果,只会影响性能。但是不幸的是,在生成OpenGL内核相应代码的时候有bug,他不会使用全部3个edge factor去计算inside factor,而是只会使用第三个edge factor,这就得到了一个错误的结果,导致inside factor= third edge factor

编译器会尽快拆分进程,所以他就没法优化上述的重复调用

但是如果我们一开始计算世界空间坐标,然后再调用TessellationEdgeFactor,编译器就不会对每个edge factor使用多线程计算,只用一个线程就把所有数据都计算了,OpenGL内核下他的效果就对了(这时候他就会优化重复调用了)
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值