Blending和Unity Terrain:如何摆脱交叉点并停止使眼睛受伤

为了在游戏中获得逼真的世界,有必要考虑各种地形相互之间以及与其他模型之间的相互作用。而且,如果3D模型之间的可见相交线破坏了图像的完整性,则值得考虑如何消除它们。这种线条最常见的情况(可能为许多人所熟悉)是具有不透明几何形状的粒子的广告牌的交集。

图片

另一个例子是在“室外”场景中,岩石和植被与景观表面的交汇处令人不安的自然成分。

图片

除了各种平滑方法(SSAA,MSAA,CSAA,FXAA,NFAA,CMAA,DLAA,TAA等),这些方法虽然可以缓解这种相交线的反感,但不能完全纠正这种情况,但还有更有效的技术。我们将考虑它们。

深度融合


Unity有一个内置解决方案,可以消除透明粒子和称为软粒子的不透明几何之间的可见相交。支持此效果的着色器进一步提高了粒子的透明度,具体取决于粒子片段的深度与不透明几何体的深度之间的差异有多小。

图片
软粒子的操作原理

显然,为了正确操作软粒子,需要深度缓冲区。在延迟着色的情况下,深度缓冲区是在渲染全屏缓冲区的阶段形成的,并且考虑到MRT(多个渲染目标,而不是磁共振断层扫描),其存在没有用额外的计算成本来表示。

在前向阴影和使用Unity Legacy Pipeline的情况下,需要额外的遍历才能将不透明的几何体呈现到深度缓冲区[1]通过将适当的值分配给Camera.depthTextureMode属性来激活此过程。该属性在检查器窗口中不可用,但在API [2]中可用

现在,您可以使用正向阴影实现自己的Scriptable Render Pipeline版本,借助MRT,它可以同时渲染深度缓冲区和颜色缓冲区。


消除支持软粒子的着色器中的相交线

通常,使用深度融合方法消除3D模型与景观的可见相交没有技术障碍:

查看代码
// Blending with depth buffer

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;
UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);

struct v2f
{
    // ...

    half4 projPos : TEXCOORD0;
};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);

    // ...

    o.projPos = ComputeScreenPos(o.pos);
    COMPUTE_EYEDEPTH(o.projPos.z);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{     
    fixed4 result = 0;
      
    // ... 

    float depth = SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(i.projPos));
    float sceneZ = LinearEyeDepth(depth);
    float partZ = i.projPos.z;
    float fade = saturate( sceneZ - partZ );
    result.a = smoothstep( BlendStart, BlendEnd, fade );

    // ... 
       
    return result; 
}


但是,这种方法有几个缺点。

第一个缺点与性能有关。深度混合在硬件管道混合阶段即片段着色器的栅格化和计算之后立即起作用。在此阶段,根据对API [6] [7] [8] [9]的调用预定义的公式,将片段着色器的执行结果与记录在输出缓冲区[3] [4] [5]中的结果混合 从某种意义上说,它的工作方式完全像其20年前的工作一样,这是所有硬件管道中进展最少的部分。 GPU从内存读取值,将其与片段着色器的值混合,然后将其写回内存。



对于完全透明或部分透明的3D模型,使用深度融合还是有区别的。透明-例如,粒子广告牌-即使没有深度融合,整个渲染也是透明的。对于不透明的3D模型,在深度融合时,真实,有形,可见的透明度将只具有非常少量的片段,而大多数片段将保持不透明。但是后者完全不意味着混合将不会用于渲染-它将只是闲置工作。

第二个缺点与如何选择用于混合的颜色有关。简而言之,然后混合在特定屏幕像素中的所有片段都位于从相机的世界位置发出并穿过此屏幕像素的世界位置的一条光线上。反过来,这意味着在相机位置或方向发生任何变化时,都将观察到视差:靠近相机的3D模型片段的移动速度将比距离相机较远的景观片段的移动速度更快[10] [11]。从近距离观看时,摄像机的横向位移恒定,这一点尤其明显。


移动相机时的横向视差:与景观片段相比,3D模型的片段移动的距离更大


移动相机时的横向视差:将相机固定在景观片段上时,模型片段的移动速度非常明显

;旋转相机时,会沿屏幕坐标的两个轴立即观察到视差。但是,在动力学中,这不如横向视差明显。


相机移动时的方位角视差:当碎片沿两个

移动时,大脑更难识别视差模式,但最值得注意的是,深度混合的外观取决于观察者观察风景的角度而变化。当视线方向垂直于风景表面的法线时,融合区域几乎变得不可见,但是如果您向下倾斜相机,则该区域的大小会迅速增加。




在深度倾斜摄影机的同时更改混合区域的宽度如果不是因为3D模型与景观的相交过多,混合可能是消除3D模型与景观的相交线的好选择。此方法更适合于非静态的粒子效果,并且通常不包含高度详细的纹理,因此,在这种情况下不会观察到视差效果。


高度图融合


实施景观融合的另一种方法是使用高度图,Unity提供了通过TerrainData API [12]访问的高度图

知道Terrain对象的位置和TerrainData中指示的地形尺寸,并且手头有一个“高度图”,您可以计算在世界坐标中指定的任何点的地形高度。


采样高度图所需的地形参数

// Setting up a heightmap and uniforms to use with shaders... 

Shader.SetGlobalTexture(Uniforms.TerrainHeightmap, terrain.terrainData.heightmapTexture);
Shader.SetGlobalVector(Uniforms.HeightmapScale, terrain.terrainData.heightmapScale);
Shader.SetGlobalVector(Uniforms.TerrainSize, terrain.terrainData.size);
Shader.SetGlobalVector(Uniforms.TerrainPos, terrain.transform.position);

好了,现在,在计算了景观的高度之后,您还可以在着色器中计算uv坐标,以在世界坐标中采样景观的高度图。

// Computes UV for sampling terrain heightmap... 

float2 TerrainUV(float3 worldPos)
{
    return (worldPos.xz - TerrainPos.xz) / TerrainSize.xz;
}

为了能够在片段着色器和顶点着色器中使用相同的代码,将tex2Dlod函数用于采样。此外,高度图没有Mip级别,因此使用自动计算Mip级别的tex2D函数对其进行采样基本上是没有意义的。

// Returns the height of terrain at a given position in world space... 

float TerrainHeight(float2 terrainUV)
{
    float heightmapSample = tex2Dlod(TerrainHeightmap, float4(terrainUV,0,0));
    return TerrainPos.y + UnpackHeightmap(heightmapSample) * HeightmapScale.y * 2;
}

您可以尝试通过透明度重现消除交集的功能,而无需使用深度缓冲区。这不能解决与该方法相关的其他问题,但是可以使用高度图验证混合的可操作性。

查看代码
// Blending with terrain heightmap

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmap; 
float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;        

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

    // ...

    return o;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ... 

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    result.a = smoothstep( BlendStart, BlendEnd, deltaHeight );

    // ... 
       
    return result; 
}





深度混合和高程混合。相同的着色器参数会导致混合区域的宽度不同,

两种方法的插图使用相同的混合参数。融合区域的宽度在视觉上有所不同,因为与高度图的融合并不取决于观察者的视线与景观法线之间的角度。

与高度图进行混合至少比在深度上进行混合更好:它校正了肉眼可见的混合对相机注视风景的依赖关系。不幸的是,仍然会观察到视差效果。


园林绿化改造


要消除视差,您需要将3D模型的一部分与垂直下方的风景部分混合在一起(在这种情况下,用于混合的颜色选择不取决于相机的位置和方向)。


如何修复视差:选择用于混合的景观片段

当然,我们在这里更多地讨论虚拟景观片段。根据相机的位置,有可能需要将3D模型的片段混合在一起的风景片段甚至都不会落入相机的视野中。在屏幕空间(SSLR)中渲染局部反射时也存在类似的问题。它包含以下事实:不可能渲染不在屏幕上的片段的反射[13]

在景观的情况下,可以使用Unity API提供的辅助纹理高精度地重建虚拟片段的颜色:法线贴图[14],光贴图[15],用于混合图层的加权纹理[16]以及包含在其中的纹理层的组成[17]


重建景观片段

根据与高度图相同的UV 对构成景观的所有纹理进行采样。在图层的情况下,采样坐标通过为特定图层[18] [19]指定的平铺参数进行调整

查看代码
// Blending with reconstructed terrain fragments

#include "UnityCG.cginc"

float BlendStart;
float BlendEnd;

sampler2D_float TerrainHeightmapTexture;
sampler2D_float TerrainNormalTexture;
sampler2D TerrainAlphaMap;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;
Float4 TerrainLightmap_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap0);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap0);
half4 TerrainSplatMap0_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap1);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap1);
half4 TerrainSplatMap1_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap2);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap2);
half4 TerrainSplatMap2_ST;

UNITY_DECLARE_TEX2D(TerrainSplatMap3);
UNITY_DECLARE_TEX2D_NOSAMPLER(TerrainNormalMap3);
half4 TerrainSplatMap3_ST;

struct v2f
{
   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;
#if defined(LIGHTMAP_ON)
   float2 modelLightMapUV : TEXCOORD2;
   float2 terrainLightMapUV : TEXCOORD3;
#endif

   // ...
};


v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);

#if defined(LIGHTMAP_ON)
    o.modelLightMapUV = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    o.terrainLightMapUV = o.heightMapUV * TerrainLightmap_ST.xy + TerrainLightmap_ST.zw;
#endif

    // ...

    return o;
}
half3 TerrainNormal(float2 terrainUV)
{
    return tex2Dlod( TerrainNormalTexture, float4(terrainUV,0,0) ).xyz * 2.0 - 1.0;
}

half4 TerrainSplatMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 splat0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap0, TerrainSplatMap0, uv0);
    half4 splat1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap1, TerrainSplatMap1, uv1);
    half4 splat2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap2, TerrainSplatMap2, uv2);
    half4 splat3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainSplatMap3, TerrainSplatMap3, uv3);         
    half4 result = splat0 * control.r + 
                   splat1 * control.g + 
                   splat2 * control.b + 
                   splat3 * control.a;
    return result;
}

half3 TerrainNormalMap(float2 uv0, float2 uv1, float2 uv2, float2 uv3, half4 control)
{
    half4 n0 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap0, TerrainSplatMap0, uv0);
    half4 n1 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap1, TerrainSplatMap1, uv1);
    half4 n2 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap2, TerrainSplatMap2, uv2);
    half4 n3 = UNITY_SAMPLE_TEX2D_SAMPLER(TerrainNormalMap3, TerrainSplatMap3, uv3);
    half3 result = UnpackNormalWithScale(n0, 1.0) * control.r +
                   UnpackNormalWithScale(n1, 1.0) * control.g +
                   UnpackNormalWithScale(n2, 1.0) * control.b +
                   UnpackNormalWithScale(n3, 1.0) * control.a;
    result.z += 1e-5;
    return result;
}

half3 TerrainLightmap(float2 uv, half3 normal)
{
#if defined(LIGHTMAP_ON)
#if defined(DIRLIGHTMAP_COMBINED)
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half4 lmd = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
    result = DecodeDirectionalLightmap(result, lmd, normal);
#else
    half4 lm = UNITY_SAMPLE_TEX2D(unity_Lightmap, uv);
    half3 result = DecodeLightmapRGBM(lm, unity_Lightmap_HDR);
#endif                
#else
    half3 result = UNITY_LIGHTMODEL_AMBIENT.rgb;
#endif
    return result;
}

fixed4 frag(v2f i) : COLOR
{
    fixed4 result = 0;

    // ...

    // compute model color and put it to the result

    // ... 

    // reconstruction of terrain fragment

    float2 splatUV0 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap0);
    float2 splatUV1 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap1);
    float2 splatUV2 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap2);
    float2 splatUV3 = TRANSFORM_TEX(i.heightMapUV, TerrainSplatMap3);

    half4 control = tex2D(_TerrainAlphaMap, i.heightMapUV);
    half4 terrainColor = TerrainSplatMap(splatUV0, splatUV1, splatUV2, splatUV3, control);

    half3 terrainSurfaceNormal = TerrainNormal(i.heightMapUV);
    half3 terrainSurfaceTangent = cross(terrainSurfaceNormal, float3(0,0,1));
    half3 terrainSurfaceBitangent = cross(terrainSurfaceTangent, terrainSurfaceNormal);

    half3 terrainNormal = TerrainNormalMap(splatUV0, splatUV1, splatUV2, splatUV3, control);
    terrainNormal = terrainNormal.x * terrainSurfaceTangent + 
                    terrainNormal.y * terrainSurfaceBitangent + 
                    terrainNormal.z * terrainSurfaceNormal;
    
    half3 terrainLightmapColor = TerrainLightmap(i.heightMapUV, terrainNormal);
    terrainColor *= terrainLightmapColor;

    // blend model color & terrain color

    half height = TerrainHeight(i.heightMapUV);
    half deltaHeight = i.worldPos.y - height;
    half blendingWeight = smoothstep(BlendStart, BlendEnd, deltaHeight);

    result.rgb = lerp(result.rgb, terrainColor, blendingFactor);
       
    return result; 
}


因此,与景观碎片的重建融合解决了深度融合和与高度图(包括视差)融合的所有典型问题。



园林绿化改造


地形碎片重建性能


现在,该问问题了,这种妥协的价值是什么?乍一看,重建景观碎片的资源强度远远超过了alpha混合的资源强度。为了进行重建,有必要从内存中执行许多其他读取操作。对于Alpha混合,您只需要从内存进行一次读取操作,而对内存进行一次写入操作。

实际上,一切都将取决于硬件平台的功能。纹理压缩,mip映射,GPU核心处理能力和特定的硬件管道优化(早期深度抑制)支持片段重建。与alpha混合相反,上面已经提到的事实将发挥作用,它是所有GPU中最不先进的部分。

但是,总会有优化的空间。例如,在重建风景的颜色的情况下,仅对于位于不高于风景表面上方一定高度的3D模型的狭窄片段条带进行此重建的需要。

着色器中的动态分支可能会导致可预测的性能结果,但是应注意两点:

  1. 如果在大多数情况下不满足此条件,则应按条件跳过不必要的计算。
  2. . , ( , ), GPU. ― (branch granularity), , , , , . , , . , GPU , , . , GPU, , 1 (PowerVR SGX).


可视化不同程度的连贯性

在重建片段的情况下,要考虑以下两个方面:在大多数情况下,分支条件将允许中断资源密集型操作以重建景观的颜色,并且该条件是一致的,除了极少数的片段(在这些示例中,这些片段是在“红色”和“绿色”区域之间的边界上)。


景观碎片重建的连贯性

关于这种混合方法仍然需要添加一些注释:

  1. 仅当景观启用了绘制实例化”模式时,Unity才会提供所有必要的纹理[20],否则法线贴图将不可用,从而使您无法正确地重构景观照明以进行混合。
  2. Unity API , (base map) . - .
  3. , API (, Metal 16 ).
  4. 3D- , Terrain, SRP.
  5. 3D- , 3D- .
  6. , «» , «» . , «» , . «» .






在设计3D模型时,不可能考虑到应该使用这些模型的各种地形浮雕。通常,3D模型必须在景观中深深“下沉”或旋转以隐藏突出的零件,反之亦然-以便显示应该可见的隐藏零件。 “预热”模型限制了它们的适用性,如果3D模型的绘制早于景观,则还会导致透支效果。反过来,转弯也远非适用于所有3D模型(例如,不适用于房屋和树木)。


要隐藏3D模型的突出元素,必须将其“淹没”在景观中

捕捉是图形编辑器用户熟悉的一个术语。此功能允许控制点“粘贴”到空间网格的节点,并在3D编辑器中粘贴到其他对象的面和表面。在顶点着色器中捕捉到景观高度的地图可以大大简化场景的设计。


没有捕捉的3D模型。具有顶点捕捉功能的3D模型。具有顶点捕捉和融合功能的3D模型。具有顶点捕捉,混合和静态照明的3D模型

实施捕捉的主要困难是,您需要确定3D模型的哪些顶点需要捕捉到高度图,哪些顶点不值得。顶点仅包含有关表面局部性质的信息(这还不够),不包含有关其拓扑结构的任何信息(这是必需的)。

与在其他应用程序中一样,通过在顶点中直接实现必要的参数,最容易在建模阶段解决此问题。作为这样的参数,您应该选择一个直观的属性-例如,捕捉的权重因子(而不是我们想要的灵活性,而不是到开放曲面边界的距离)。


捕捉的加权编码

查看代码
// Per-vertex snapping with terrain heightmap

#include "UnityCG.cginc"

sampler2D_float TerrainHeightmapTexture;

float4 HeightmapScale;
float4 TerrainSize;
float4 TerrainPos;

struct v2f
{

   // ...

   float3 worldPos : TEXCOORD0;
   float2 heightMapUV : TEXCOORD1;

   // ...

};

v2f vert(appdata v)
{
    v2f o;
    UNITY_INITIALIZE_OUTPUT(v2f,o);
   
    // ...
    
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.heightMapUV = TerrainUV(o.worldPos);
    float snappingWeight = v.color.r;                
    half height = TerrainHeight( o.heightMapUV );                
    o.worldPos.y = lerp( o.worldPos.y, height, snappingWeight );
    o.pos = UnityWorldToClipPos( half4( o.worldPos, 1 ) );

    // ...

    return o;
}


顶点捕捉的适用性受到地形和3D模型表面之间的一般对应关系的限制。为了弥补它们之间的显着差异,有必要使用其他更占用资源的方法-例如,使用带有蒙皮的3D模型。


结论


本文的主要思想是:任何足够复杂且具有潜在可伸缩性的着色器都需要基本数据。开发人员的任务是了解图形系统的操作方式:它提供什么数据,如何将它们彼此组合以及如何在着色器中使用它。

在一般情况下,我们可以得出结论,克服限制图形效果可能性的框架的唯一选择是组合各种着色器的结果。


参考文献



All Articles