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

[笔记] unity树的阴影技巧

[复制链接]
发表于 2021-5-26 12:29 | 显示全部楼层 |阅读模式
需求与痛点

unity的树有个痛点不论引擎默认还是speedtree shader都是强制 不支持烘焙的。原因也很直观大量的树实例需要那么多lightmap 和uv2计算 又慢又大。所以对于实时阴影距离外的 树渲染一直是unity项目需要解决的肉眼可见的画质短板。
这里不涉及更为复杂的 需要大量投入的大场景阴影方案。仅仅用2个小技巧帮助大部分小团队提升这方面的画质表现。
1简单方案 ,适合手游 0采样

大部分都是用speedtree来做资源,而speedtree最后有个billboard特殊旋转方式,为此 speedtree的树常常具备 旋转对称性的 特点而且地形树的碰撞体早版本引擎默认是不支持旋转的 所以也需要他具有旋转对称性。那么对于喜欢数学的人眼里看什么都是数学规律,旋转对称性这个规律 就等于可以推算 背光的一面了。看下图,根据 vertex相对于树实例0点的世界坐标方向 oa,与平行光方向的dot规律就能得到是否在 背光面/阴影里。dot为0 刚好是在分界线上。如此一来代码就很简单了让他们在xz平面上投影 求dot就可以了。如果你使用frag/vert 很好设置到shadowmask上或简单的修改miancolor,如果用surface 需要看他编译后代码进行修改,稍微复杂些,需要改UnityShadowLibrary 的计算shadowmask函数。这个需要的人不多不详细说了。
这个方式优点是非常简单高效 但效果却不错。看以下效果测试对比。地板上的模糊阴影可以看到已经是出了shadowmap到了shadowmask范围了。
普通无烘焙树在 实时阴影外光影很平
用了该简单方案 光影提升明显
2进阶方案 ,基于prefab烘焙阴影



这个方案其实是我当年转TA后第一个创造的轮子,但这种思路属于人人都可轻松独立发明出来的。他的思想非常朴素,同时喜欢数学与计算机的人都会发现一个很底层规律。设备有限 数学无限,用离散资源代替连续描述。我们不知道360度下的阴影情况,但是可以烘焙4个方向的,然后根据当前朝向,对这4个方向进行插值即可。我选4个方向是因为 rgba刚好存储。具体多少个更合适可根据自己项目调整。
4方向烘焙图与合并后结果
为了让一个树的shadowmask可以单独完整铺满一个图 可以在max烘焙,也可以在unity烘焙。但是在unity烘焙他按场景烘焙常常不能铺满,需要设置独立的 bake tag才行。首先新建一个烘焙参数配置文件
然后随便取一个不是-1的tag 就可以保持独立分配贴图了




插值角度计算
这是比较难写对的地方,常规的插值是判断逻辑是这样的
    求出当前灯光相对于旋转后树 在xz平面上的角度。找到这个角度所在象限的2个确定方向的轴,取出这2个轴对应在rgba里2个通道的float值 a和b求当前角度在2个轴之间的权重,更靠近哪个。然后用lerp(a,b,该权重)得到当前角度float值 做完灯光强度叠加
这个计算对数学不好的人太晕,所以我又想了一种更直观的坐标轴投影法。就是我们高中力学常用的, 力在 某方向上大小与方向=力在x轴投影 +力在y轴投影 矢量合的 大小与方向。光照也一样,这些都是数学的基础矢量定义。投影xy的大小 用数学表示就是 cos(a),和sin(a),就是cos(a),cos(90-a) ,图形里用dot(x轴,v),dot(y轴,v), 表达。
但是 这样需要判断方向性 x与-x,z与-z 需要采样的颜色不同。 为了不做判断 其实可以 看成4个轴的 dot ,但是其中2个<0 所以max(0,dot())即可得到2个需要的合成。我们实际关注下 灯光方向与这4个轴的关系。


可以看出 r通道时 对应树的局部坐标系时 +y,g时时-x,其他2个方向求反就可以。为什么不是xz这是与模型建模的坐标系有关,自己测下就可以调整。为什么光照方向与坐标系相反 因为shader里用的不是光照朝向 而是_WorldSpaceLightPos0
所以对应的代码就是这样
计算局部角度
根据角度插值采样的代码
看下最终效果很不错


因为我们已经考虑了各种方向插值 所以不管树怎么旋转,平行光不同场景y轴角度的不同 观察角度的不同 都比较正常,限制就是 平行光不要出现 太陡和太平,否则按45度烘焙会对应不上,然后树只能绕y旋转 不能其他角度旋转 这一点几乎所有游戏都可以遵循。
结束语:这一篇没什么技术 都是技巧的分享, 希望我自己独自孤单摸索出来的这些边边角角对改善他人项目有帮助。
相关代码

shader 代码就几行上面有截图了,发一个工具代码。顺便吐槽一下 图程小弟一直没招到,需要自己写这些简单小工具,算不算上班摸鱼呢?
工具长这样
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Artplugins
{
    public class TreeShadowmaskBakerEditor : EditorWindow {

        private Light bakeLight;
        private Renderer bakeRenderer;
        public string info;
        [MenuItem("地形/烘焙树阴影")]
        public static void OpenWindow()
        {
            var win = GetWindow<TreeShadowmaskBakerEditor>("烘焙树阴影");

            win.showInit();
        }
        private void showInit() {
            bakeLight = null;
            foreach (var item in FindObjectsOfType<Light>())
            {
                if (item.type == LightType.Directional) {
                    bakeLight = item;
                    break;
                }

            }
           

            Show();

        }
        private void OnGUI()
        {
            bakeLight = EditorGUILayout.ObjectField("烘焙阴影的灯光", bakeLight, typeof(Light), true) as Light;
            bakeRenderer = EditorGUILayout.ObjectField("烘焙阴影的树", bakeRenderer, typeof(Renderer), true) as Renderer;
            if (GUILayout.Button("烘焙阴影")) {

                bakeShadowMask();

            }
            EditorGUILayout.LabelField(info);
        }

        private void bakeShadowMask()
        {
            if (bakeRenderer == null || bakeLight == null)
            {
                info = "需要设置 灯光 和 烘焙对象 参数";
                return;
            }
            Quaternion initRot = bakeLight.transform.rotation;
            Vector3 rot = bakeLight.transform.forward;
            float xzLen = Mathf.Sqrt(1 - rot.y * rot.y);
            Color32[][] colors = new Color32[4][];
            GameObjectUtility.SetStaticEditorFlags(bakeRenderer.gameObject, StaticEditorFlags.LightmapStatic | StaticEditorFlags.ReflectionProbeStatic);
            for (int i = 0; i < 4; i++)
            {
                rot.z = Mathf.Cos(i * Mathf.PI / 2) * xzLen;
                rot.x = Mathf.Sin(i * Mathf.PI / 2) * xzLen;
                bakeLight.transform.rotation = Quaternion.LookRotation(rot.normalized, Vector3.up);
                Lightmapping.Bake();
                if ((uint)bakeRenderer.lightmapIndex > LightmapSettings.lightmaps.Length)
                {
                    //throw new System.Exception("bakeRenderer lightmap index error");
                    info = "bakeRenderer lightmap index error";
                     return;
                }
                var maskT = LightmapSettings.lightmaps[bakeRenderer.lightmapIndex].shadowMask;
                UnityEditor.AssetDatabase.RenameAsset(UnityEditor.AssetDatabase.GetAssetPath(maskT), "tempTreeMask");
                colors = maskT.GetPixels32();

            }
            var mask0 = LightmapSettings.lightmaps[0].shadowMask;
          var   finalMask = new Texture2D(mask0.width, mask0.height, TextureFormat.ARGB32, false, true);
            var finalColors = finalMask.GetPixels32();

            for (int i = 0; i < finalColors.Length; i++)
            {
                finalColors.r = colors[0].r;
                finalColors.g = colors[1].r;
                finalColors.b = colors[2].r;
                finalColors.a = colors[3].r;
            }
            finalMask.SetPixels32(finalColors);
            finalMask.Apply();
            string path = Path.GetDirectoryName(UnityEditor.AssetDatabase.GetAssetPath(bakeRenderer.sharedMaterial.mainTexture));

            File.WriteAllBytes(path + "/TreeShadowMask.png", finalMask.EncodeToPNG());
             AssetDatabase.Refresh();
            info = "烘焙成功 >>>>>>>>>>>>>  "+ path + " / TreeShadowMask.png";
        }
    }
    public class TreeShadowmaskBakerImporter : AssetPostprocessor
    {
      
   
        private void OnPreprocessTexture()
        {
         
     
            string fileName = Path.GetFileName(assetPath);
            TextureImporter importer = TextureImporter.GetAtPath(assetPath) as TextureImporter;

            if (fileName.StartsWith("TreeShadowMask")) {
                importer.sRGBTexture = false;
                importer.maxTextureSize = 256;
            }
            if (fileName.StartsWith("tempTreeMask"))
            {
                importer.isReadable = true;
                importer.textureCompression = TextureImporterCompression.Uncompressed;
            }

        }
     
      
    }
}

本帖子中包含更多资源

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

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

本版积分规则

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

GMT+8, 2024-6-2 08:49 , Processed in 0.091865 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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