找回密码
 立即注册
查看: 358|回复: 0

Unity实现Impostor踩坑日记(二)

[复制链接]
发表于 2022-3-17 06:51 | 显示全部楼层 |阅读模式
概述

本期针对上一期实现的 Impostor 进行调优。
重建世界坐标

在片元着色器阶段,通过插值,我们天然可以取得面片上对应像素的两个坐标分量(对应着在 frame上采样的UV)。如果在烘焙阶段,我们记录了各点的深度信息,就有机会在渲染这个 Billboarding 面片的片元着色器中重建三维坐标。



重建世界坐标的原理

烘焙深度信息

烘焙深度信息可以考虑在裁剪空间下进行,因为烘焙相机采用正交投影,因此可以将NDC坐标的Z分量进行线性变换并存储。不过这里需要注意平台差异,DirectX-Like平台对应的近平面Z分量为0,而OpenGL-Like平台对应的近平面Z分量为-1。
// packing depth in clipPos into [0, 1]
// In URP,
// UNITY_NEAR_CLIP_VALUE == 1 in DirectX-like
// UNITY_NEAR_CLIP_VALUE == -1 in OpenGL-like
// see Packages\com.unity.render-pipelines.core\API\D3D11.hlsl
half linear01Depth = input.positionCS.z / (1.5 - 0.5 * UNITY_NEAR_CLIP_VALUE) + (0.25 - 0.25 * UNITY_NEAR_CLIP_VALUE);

outGBuffer0 = half4(surfaceData.albedo.rgb, 1);
outGBuffer1 = half4(remappedNormalWS.xyz, linear01Depth);注意, UNITY_NEAR_CLIP_VALUE 这个值在 URP 下和 Built-In 下的值可能不同,最好输出出来看一下。总之,最终输出到 GBuffer 上的数值需要在 之间。最终输出的深度结果类似于下图,越靠近屏幕的点深度越大。



法线与深度贴图上记录的深度信息

计算世界坐标

个人认为,重建世界坐标的工作在相机坐标系下是最合适的,因为在相机坐标系下,深度就是简单的对z分量的加减。所以可以先在顶点着色器中,将对应的四边形面片各顶点的相机坐标传到片元着色器,然后在片元着色器中重建世界坐标。
half4 normalDepth = tex2D(_NormalDepth, input.frameUV);
half3 normalOS = normalize(normalDepth.xyz * 2 - 1);
half linear01Depth = normalDepth.w;

// Fragment position in view space
float4 viewPos = float4(input.viewPos.xy, input.viewPos.z + (linear01Depth - 0.5) * _DepthFactor, 1);其中,_DepthFactor 参数指的是在烘焙阶段 Frustum 的 Far 与 Near 之间的距离,可以在烘焙结束之后、生成材质的阶段随着 vFrames,hFrames 等变量一起写入。linear01Depth 减去 0.5 的原因是要将深度转换到面片上,获取相对于面片的深度。获得了相机坐标系下的坐标,就很容易能获得裁剪空间下的坐标与世界坐标。
float3 worldPos = mul(UNITY_MATRIX_I_V, viewPos).xyz;
input.positionCS = mul(UNITY_MATRIX_P, viewPos);
input.positionCS.xyz /= input.positionCS.w; // 需要显式地做齐次除法!此外,当我们开启了 Z-Write,我们可能需要将重建出来的 z 值进行写入。于是我们这么做
half4 frag (Varyings input, out float depth : SV_DEPTH) : SV_Target
{
    // ........
    input.positionCS.xyz /= input.positionCS.w;
    depth = input.positionCS.z;
    // ........
}写入Z值前后,可以看到有更优的显示效果。



左边是不自定义深度写入的,右边是自定义写入深度的,更立体

Billboarding 的连续旋转问题

Billboarding 目前的设定是连续地朝向相机的方向旋转,然而除了在拍摄角度以外的面片朝向都是“不完全正确”的。面片上的像素对应的是面朝拍摄方向的,所以我心里犯嘀咕,会不会强制面片朝向拍摄角度会有更好的效果呢?动手尝试一下。这里的关键就是通过重新计算 frameIdx,然后构建出俯仰角与方位角,再求出 Billboard 的朝向。
// yaw in [0,1]
float yaw01 = INV_TWO_PI * atan2(-objectViewDirection.z, -objectViewDirection.x);
// pitch in [0,1]
float pitch01 = INV_PI * acos(-objectViewDirection.y);
   
// get frame bias by view direction
// select nearest frame !
float2 frameIdx = float2(round(yaw01 * _hFrames), min(round(pitch01 * (_vFrames - 1)), _vFrames));
float latitudeFraction = -(PI / (_vFrames - 1));
float longitudeFraction = TWO_PI / _hFrames;
// resolve integer billboarding from frameIdx, to avoid billboarding continously
// See also: ImpostorBaker.GetCameraRotation(frameIdx)
// artifact when view direction near to poles
float nearestPitch = PI / 2 + latitudeFraction * frameIdx.y;
float nearestYaw = longitudeFraction * frameIdx.x;
float cosPitch = cos(nearestPitch);
float sinPitch = sin(nearestPitch);
float cosYaw = cos(nearestYaw);
float sinYaw = sin(nearestYaw);
float3 billboardNormal = -float3(cosYaw * cosPitch, sinPitch, sinYaw * cosPitch); // get unit vector by yaw and pitch实验结果表明,效果反而更差了,所以这段代码没啥用。

连续 Billboarding 效果
https://www.zhihu.com/video/1484618819989176320

离散 Billboarding 效果
https://www.zhihu.com/video/1484618929770917888
PBR

这个步骤相对简单一些,只要把 URP Lit 这个着色器的相应内容抄过来,然后将相应的变量进行替换即可。某些材质的属性比如 Metallic Map 可能需要烘焙到一张新的 Impostor 贴图上,而一些数值参数可以自行转写到 Impostor 的着色器中。实现的效果如图,虽然还是能看出来哪个是 Impostor,但是离远了效果还可以。






投动态阴影

对于在 Unity 中广泛使用的 Shadow Map 技术而言,生成阴影的过程实际上是向一张深度图写当前物体深度的过程。这个深度是相对于光源的。所以在这里,我们需要将Billboard 朝向渲染阴影的光源。
我们知道,在之前做 Billboading 的时候,可以访问 _WorldSpaceCameraPos 来获取当前相机的世界坐标,然后再求视线方向。
float3 objectSpaceCameraPos = mul(GetWorldToObjectMatrix(), float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
float3 objectViewDirection = normalize(objectSpaceCameraPos);然而这个方式在正交投影当中其实是不适用的。正交投影当中,视线方向和相机位置无关。因此直接通过逆相机变换矩阵取得视线方向。
// get GetObjectSpaceViewDirection in different projections
// https://forum.unity.com/threads/how-to-get-current-shadowcaster-light-direction.1108139/
// https://forum.unity.com/threads/unity_matrix_v-and-camera-position.519881/
float3 GetObjectSpaceViewDirection()
{
    if (GetViewToHClipMatrix()[3][3] == 1)
    {
        return normalize(UNITY_MATRIX_I_V._m02_m12_m22);
    }
    else
    {
        return normalize(mul(GetWorldToObjectMatrix(), float4(UNITY_MATRIX_I_V._m03_m13_m23, 1)).xyz);
    }
}之后来实现 ShadowCaster 。注意,由于我们在之前重建了模型的世界坐标,那么 ShadowCaster 不能仅对坐标变换后的四边形面片上的片元做 Alpha Test 就得到阴影。因为用这种方式填充 Shadow Map,这样得到的深度数据是面片的深度,会给自身的凹陷处投不正确的影子。



仅使用Alpha Test剔除深度带来的不正确的自投影



仅使用 Alpha Test,能往别的物体上投出阴影

和之前一样,我们在 Shadow Caster 中也实现世界坐标的重建,重建后对坐标系套用相应的 Bias 与 Normal Bias即可。(这里建议参考 URP Lit 里实现的Shadow Caster)。
worldPos = ApplyShadowBias(worldPos, input.normalWS, normalize(UNITY_MATRIX_I_V._m02_m12_m22));

input.positionCS = TransformWorldToHClip(worldPos);
input.positionCS.xyz /= input.positionCS.w;
#if UNITY_REVERSED_Z
input.positionCS.z = min(input.positionCS.z, input.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
input.positionCS.z = max(input.positionCS.z, input.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif

depth = input.positionCS.z;可是,这样还不够,仍然会有不正确的自阴影问题,如下图。


这个问题我想了好久,最后找到原因了。我们在填充 Shadow Map 时,这个模型通过 Billboard 的方式面朝光源,这使得获取的深度实际上是有误差的。这样带着误差的数据用来渲染阴影就很难不出错了。有一个比较简单的解决这个问题的方案是,给写入的深度值加一定的固定偏移,就像 Shadow Bias 做的那样,能一定程度上避免这种现象发生。



偏移 = 0.25



偏移 = 0.15

当然,这样会带来一些问题,比如平面和 Impostor 相交时会导致 Peter Panning 的发生。



Peter Panning

所以这个参数是需要根据 Impostor 相关资产、视线方向、光照方向等场景来进行一定的调整,感觉还是不太好用。
做到这里,稍微概括一下 Impostor,特别是球形投影 Impostor 有哪些缺点:

  • 由视点变化或者是环境导致的视觉误差较大;
  • 纹理分辨率要求较高,否则单个 Frame 的分辨率太小;
  • 对直线较多的物体支持不好(例如上边的水塔);
  • 对于顶点较少的物体不一定会节省内存与包体空间。
个人感觉可能在真实感渲染中比较难用起来。
后续可能会做的工作:

  • ★相邻帧混合,使得随视角的变化更平滑。
下期再见。个人学习日记,转载请注明。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 16:51 , Processed in 0.065965 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表