jquave 发表于 2022-12-25 18:11

[Unity Shader入门]体积光实现

上文说到了SDF(有向向量场)的原理,实现了一个圆体的体积渲染。本文将来实现一个常见的体积渲染案例--体积光。效果如下:


https://www.zhihu.com/video/1589734709482901504
一.实现

1.创建RenderFeature

体积光是一种后处理,所以我们创建一个RenderFeature来实现比较方便。需要用到深度图,因此要把copy depth打开。
public class VolumeLightFeature : ScriptableRendererFeature
{
    ...
    class LightRenderPass : ScriptableRenderPass
    {
         ...
         public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
      {
            CommandBuffer cmd = CommandBufferPool.Get(Cmd_Tag);
            var cameraData = renderingData.cameraData;
            using (new ProfilingScope(cmd, m_ProfilingSampler))
            {
                Camera camera = cameraData.camera;
                cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);
                cmd.SetViewport(camera.pixelRect);
                cmd.DrawMesh(RenderingUtils.fullscreenMesh,Matrix4x4.identity, material);
                cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
            }

            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
      }

         ...
    }

    LightRenderPass m_ScriptablePass;

    /// <inheritdoc/>
    public override void Create()
    {
      m_ScriptablePass = new LightRenderPass(material);

      // Configures where the render pass should be injected.
      m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    }

    // Here you can inject one or multiple render passes in the renderer.
    // This method is called when setting up the renderer once per-camera.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
      renderer.EnqueuePass(m_ScriptablePass);
    }
}
看代码,其实是很基本的renderFeature,只是渲染一次全屏mesh。
2.shader

创建一个volumelight.shader。
首先要根据屏幕的像素点uv坐标来获取到对应的世界空间的坐标。原理是先通过屏幕uv获取到该点的深度,根据uv和深度值反算世界坐标。
#if UNITY_REVERSED_Z
    real depth = tex2D(_CameraDepthTexture,i.uv);
#else
    // Adjust z to match NDC for OpenGL
    real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, tex2D(_CameraDepthTexture,i.uv));
#endif

float3 worldPos = ComputeWorldSpacePosition(i.uv,depth,UNITY_MATRIX_I_VP);我们有 _WorldSpaceCameraPos 内置参数 来获取了相机的世界坐标。
这样结合上文的sdf原理,可以从相机的位置,步进地扫描沿途的点,当点可以被光照射到的时候,叠加一个光量,最后所有光量的平均值就是当前屏幕像素点的体积光色。
为了方便,我们可以直接用urp内置的函数,做一些光的计算。
float4 shadowCoord = TransformWorldToShadowCoord(pos);
float shadowF = MainLightRealtimeShadow(shadowCoord);TransformWorldToShadowCoord 、MainLightRealtimeShadow 这两个方法即可算出该世界坐标pos是否有被光照射到,shadowF为1即被照射到。
因此结合上文的sdf算法,得到以下算法。
half4 Raymarch(float3 ro,float3 rd,float length)
{
    half4 result = half4(0,0,0,0);
    float stepLength = length / SimpleCount;
    for (int i = 0; i < SimpleCount; i++) {
      half3 pos = ro + i*stepLength*rd;
      float4 shadowCoord = TransformWorldToShadowCoord(pos);
      float shadowF = MainLightRealtimeShadow(shadowCoord);
      result += step(0.1,shadowF) * _LightDensity * _LightColor;
    }
    result /= SimpleCount;
    return result;
}由此基本的体积光算法完成。
3.优化

目前实现的光照效果太过僵硬,为了看起来更贴近现实,在渲染出光照效果后,最好再做两次blur,然后再和输出纹理融合,看起来效果更好。
二.源码

附上源码
希望大家喜欢。如果对大家有所帮助,希望能点个赞。如文章有错误,欢迎指出。
页: [1]
查看完整版本: [Unity Shader入门]体积光实现