franciscochonge 发表于 2022-5-28 16:03

unity地形渲染升级:预计算接触阴影

效果展示
因为地形纹理不会绕y轴旋转而当前项目是静态平行光,针对这些自由度的限制 想了这套性能最高的预计算可见角度的地形自阴影渲染方案(1次采样).效果又比ue4实时接触阴影或曲面细分+shadowmap 柔和精确.先放最终效果图.



普通地形效果



本方案效果



普通地形效果



本地方案效果



普通地形效果



本方案效果


地形阴影动态效果
https://www.zhihu.com/video/1511388249272983552
同一个素材 ue4下 阴影更硬,这是因为ue4的接触阴影采用非常有限的步进采样导致 通用性更好,针对性地形不够好.



ue4 下 接触阴影

脑洞过程
最近开始升级地形系统阴影部分,一般成熟的做法是用接触阴影,而且我之前已经在项目中实现了接触阴影.但地形不能直接用,因为地形mesh本身深度变化不能体现地表高度图里像素级的变化,所以一般有2种方案配套,1用曲面细分增加深度图变化精度,2通过高度图做depthoffset,让屏幕空间的深度图产生细节凹凸,这样接触阴影在 查询的时候会按遮挡处理产生细节阴影.做了下效果确实也不错,但曲面细分 或 接触阴影的12次以上采样查询性能都是 十倍与本方案开销.所以写完demo就开始思考性能更好的技术方案.以下是曲面细分+shadowmap效果,实际上shadowmap的精度会更差一些,这里的bias distance等 都完全适配近处地形.而接触阴影效果因为有限的步进导致会更差一些.具体效果可看之前的文章.



普通效果



曲面细分+shadowmap效果

让我最后决定想一种新方案的 根本原因是这个.因为我地形本身有其他图有免费a通道可用,所以性能上0开销.表里可以看到 不开曲面细分效果不行 开了性能不行.因为是3080 所以不要有性能开销不大的错觉,按增量看非常大.


方案思想
这个想法受到sdf启发,另外是考虑到地形贴图不可绕y旋转,且xz倾斜角度也不会超过90度且,大部分处于水平等因素.静态场景可以预先知道 太阳在哪个方位,只是因为地形会倾斜所以相对仰角会变化(其实方位角也会小幅度改变但不易察觉).所以我们给每个像素记录下 这个点在阳光方位上的 遮挡角度即可.用cube 代替 每个像素不同高度图里的高度信息.可以看到,如果与法线记录夹角,分别是90度,45度,60度,50度60度,70度(目测的大概数值).运行时只要判断 平行光相对于地面法线的 夹角大小即可.大于记录的值就为阴影,否则不是阴影.当然可以用smoothstep做上下10度范围的软阴影.下面是每个像素追踪查询阳光方位200个像素的遮挡统计(从3开始,是因为挨着太近,tan函数的分母太接近0 实际效果误差巨大 用atan2反算一样有这问题).因为高度图1的单位具体多少米没法确定 所以和一个像素具体跨度是多少米之间的关系加个缩放值,具体要根据资源测试.一定要用这样的computeshader来算,cpu算一张2048图需要5分钟,这个1秒内完成.



高度图遮挡角度记录



每个像素遮挡角度的计算

渲染的shader 核心部分也很简单,光线转到对象空间,如果是地形 需要自己算每个四边形网格的切线空间.只考虑了一侧的阴影所以 可以用 localL.y 大小算角度大小.


完整代码
StaticSelfShadowGPU.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class StaticSelfShadowGPU : MonoBehaviour {

    public Shader renderShader;
        public ComputeShader cs_calculateShadowRotation;
    public Light shadowLight;

    void Start () {
      Vector3 v = Vector3.Cross(new Vector3(1,1,0).normalized, Vector3.up).normalized;
      print(v);
       v= Vector3.Cross( v, new Vector3(1, 1, 0).normalized);
      print(v);
    }
   
    void createSelfShadowForSceneRenders() {
      foreach (var item in FindObjectsOfType<Renderer>())
      {
            if (item.sharedMaterial != null && item.sharedMaterial.shader == renderShader) {
                createSelfShadow(item.sharedMaterial);
            }

      }
      
    }

    void createSelfShadow(Material material) {

      RenderTexture selfShadow = material.GetTexture("_SelfShadow") as RenderTexture;
      var heightTex = material.GetTexture("_HeightTex") ;

      if (selfShadow != null) {
            selfShadow.Release();
      }
                selfShadow = new RenderTexture(heightTex.width, heightTex.height,0, RenderTextureFormat.R8, RenderTextureReadWrite.Linear);
      material.SetTexture("_SelfShadow", selfShadow);
      selfShadow.wrapMode = TextureWrapMode.Repeat;
      selfShadow.enableRandomWrite = true;
      selfShadow.autoGenerateMips = false;
      selfShadow.useMipMap = true;
      selfShadow.Create();
      cs_calculateShadowRotation.SetTexture(0,"Result", selfShadow);
      cs_calculateShadowRotation.SetTexture(0, "HeightMap", heightTex);
      cs_calculateShadowRotation.SetInt("HeightMapWidth", heightTex.width);
      cs_calculateShadowRotation.SetVector("lightDir", shadowLight.transform.forward);
   
      cs_calculateShadowRotation.Dispatch(0, heightTex.width / 8, heightTex.height / 8, 1);
   
      selfShadow.GenerateMips();



    }


}
StaticSelfShadow.compute
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

RWTexture2D<float4> Result;
Texture2D<float4> HeightMap;
int HeightMapWidth;
float4 lightDir;
#define PI3.1415926F

void CSMain (uint3 id : SV_DispatchThreadID)
{
       
    float hBase = HeightMap.r;
    float maxRot = 0;
    float2 dir2D = normalize(lightDir.xz);

    for (int i = 3; i < 200; i++)
    {

      float h = HeightMap[int2(round(id.x + i* dir2D.x+ HeightMapWidth) % HeightMapWidth,
            round(id.y + i * dir2D.y + HeightMapWidth) % HeightMapWidth)].r;
      
      maxRot =max(maxRot, atan2(h - hBase, i / 250.0f));

    }

    Result = float4(maxRot / (PI / 2), 0, 0, 1);

}


   
StaticSelfShadow.shader
// Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld'

Shader "Custom/StaticSelfShadow" {
        Properties {
                _Color ("Color", Color) = (1,1,1,1)
                _MainTex ("Albedo (RGB)", 2D) = "white" {}
                _GlossTex ("_GlossTex", 2D) = "white" {}
                _HeightTex ("Height", 2D) = "black" {}
                _BumpTex ("bump", 2D) = "bump" {}
                _Glossiness ("Smoothness", Range(0,1)) = 0.5
                _AOTex("AO", 2D) = "white" {}
                _UseSelfShadow ("UseSelfShadow",float) = 0.0
                _UseNormal ("UseNormal",float) = 0.0
                _UseAlphaForSmooth ("UseAlphaForSmooth",float) = 0.0
                _InvertGloss ("InvertGloss",float) = 0.0
        }
        SubShader {
                Tags { "RenderType"="Opaque" }
                LOD 200
               
                CGPROGRAM
                // Physically based Standard lighting model, and enable shadows on all light types
                #pragma surface surf Standard2 vertex:vert fullforwardshadows

                // Use shader model 3.0 target, to get nicer looking lighting
                #pragma target 3.0
#include "UnityPBSLighting.cginc"
                sampler2D _MainTex;
                sampler2D_SelfShadow;
                sampler2D _BumpTex;
                sampler2D _AOTex;
                sampler2D _GlossTex;
                float _UseSelfShadow;
                float _UseNormal;
       
                float _UseAlphaForSmooth;
                float _InvertGloss;
                struct Input {
                        float2 uv_MainTex;
                       float3 wNormal;
                };

                half _Glossiness;
                half _Metallic;
                fixed4 _Color;
                struct SurfaceOutputStandard2
                {
                        fixed3 Albedo;      // base (diffuse or specular) color
                        fixed3 Normal;      // tangent space normal, if written
                        fixed3 Emission;
                        half3 faceNormal;
                        half Metallic;      // 0=non-metal, 1=metal
                        // Smoothness is the user facing name, it should be perceptual smoothness but user should not have to deal with it.
                        // Everywhere in the code you meet smoothness it is perceptual smoothness
                        half Smoothness;    // 0=rough, 1=smooth
                        half Occlusion;   // occlusion (default 1)
                        fixed Alpha;      // alpha for transparencies
                        fixed selfShadowRot;      
                };
                inline half4 LightingStandard2(SurfaceOutputStandard2 s, half3 viewDir, UnityGI gi)
                {
                        s.Normal = normalize(s.Normal);
                        float3 faceNormal = s.faceNormal;
               
                        half oneMinusReflectivity;
                        half3 specColor;
               

                        s.Albedo = DiffuseAndSpecularFromMetallic(s.Albedo, s.Metallic, /*out*/ specColor, /*out*/ oneMinusReflectivity);

                        // shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
                        // this is necessary to handle transparency in physically correct way - only diffuse component gets affected by alpha
                        half outputAlpha;
                        if (_UseSelfShadow&& s.selfShadowRot >0.01) {
                                float3 localL = UnityWorldToObjectDir(gi.light.dir);

                                float lrot = atan2(localL.y, sqrt(1- localL.y* localL.y))*180/UNITY_PI;

                          gi.light.color *= 1 - smoothstep(lrot-20, lrot+10, s.selfShadowRot * 90);

                        }
                       
                        s.Albedo = PreMultiplyAlpha(s.Albedo, s.Alpha, oneMinusReflectivity, /*out*/ outputAlpha);
               
                        half4 c = UNITY_BRDF_PBS(s.Albedo, specColor, oneMinusReflectivity, s.Smoothness, s.Normal, viewDir, gi.light, gi.indirect);
                       
                        c.a = outputAlpha;

                        return c;
                }
               

                inline void LightingStandard2_GI(
                        SurfaceOutputStandard2 s,
                        UnityGIInput data,
                        inout UnityGI gi)
                {
               
#if defined(UNITY_PASS_DEFERRED) && UNITY_ENABLE_REFLECTION_BUFFERS
                        gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal);
#else
                        Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.Smoothness, data.worldViewDir, s.Normal, lerp(unity_ColorSpaceDielectricSpec.rgb, s.Albedo, s.Metallic));
                        gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal, g);
#endif
                }

                // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
                // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
                // #pragma instancing_options assumeuniformscaling
                UNITY_INSTANCING_CBUFFER_START(Props)
                        // put more per-instance properties here
                UNITY_INSTANCING_CBUFFER_END


                        void vert(inout appdata_full v, out Input data)
                {
                        UNITY_INITIALIZE_OUTPUT(Input, data);
                        data.wNormal = mul((float3x3)unity_ObjectToWorld, v.normal);
                }
                void surf (Input IN, inout SurfaceOutputStandard2 o) {
                       
                        fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                        half4data= tex2D (_SelfShadow, IN.uv_MainTex);
                        o.selfShadowRot = data.r;
                        o.faceNormal = IN.wNormal;
                        o.Albedo = c;
                       
                        o.Metallic = _Metallic;
                        o.Smoothness = tex2D(_GlossTex, IN.uv_MainTex)*_Glossiness;
                        if (_UseAlphaForSmooth > 0) {
                                o.Smoothness = c.a;
                        }
                        if (_InvertGloss > 0)        o.Smoothness = 1 - o.Smoothness;
                        if (_UseNormal > 0) {
                                o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_MainTex));
                        }
                        else {
                                o.Normal = half3(0, 0, 1);
                        }
                       
                       o.Occlusion = tex2D(_AOTex, IN.uv_MainTex);

                }
                ENDCG
        }
        FallBack "Diffuse"
}<hr/>2022/05/24 更新
上面是用plane 测试的结果,如果要用到地形shader里,需要做光线到切线空间转换,比较容易出错我就记录下来,还有 computeshader里方向也要取反.





地形shader 落地效果

mypro334 发表于 2022-5-28 16:06

牛牛牛,下次把树也加高度图升级下[酷]

c0d3n4m 发表于 2022-5-28 16:06

以后可以贴图可以专门拿个通道做SDF细节阴影图,把高度图替代掉

JoshWindsor 发表于 2022-5-28 16:15

好的树也不难 因为只绕y轴转

闲鱼技术01 发表于 2022-5-28 16:22

不愧是大佬[赞]

RedZero9 发表于 2022-5-28 16:29

本移动平台用户看到200觉得可以先羡慕一下。[捂脸]

xiangtingsl 发表于 2022-5-28 16:30

我找时间弄个低配的感受一下
页: [1]
查看完整版本: unity地形渲染升级:预计算接触阴影