Custom Shading Model In UE5/自定义光照模型

最近在卷UE5的自定义光照模型,所以就写一个笔记,记录一下/Recently, I’ve been experimenting with custom shading models in Unreal Engine 5 (UE5), so I decided to write some notes to keep track of my progress.

分三个模块,1.拉取源代码,2.C++部分添加custom shading model,3.shader 部分/Divide it into three modules: 1. Fetching the source code, 2. Adding the custom shading model in C++, 3. Working on the shader part.

一. 拉取UE源代码/Fetching the source code

主要步骤还是根据UE官方档案来操作 从源代码构建虚幻引擎 | 虚幻引擎文档 (unrealengine.com)/The main steps to build Unreal Engine from source code should be followed based on the official documentation provided by Unreal Engine.

二.C++部分添加Custom Shading Model/To add a custom shading model in C++

首先我们得先让UE知道我们有一套新的Shading Model,能够在光照模型的下拉菜单中找到我们的custom shading model,所以我们先修改EngineTypes.h跟MaterialShader.cpp这两个文件,本质是添加一个新的枚举/

To make Unreal Engine aware of your custom shading model and have it appear in the dropdown menu of the lighting model, you need to modify the EngineTypes.h and MaterialShader.cpp files. The essence of this modification is to add a new enumeration.

EngineTypes.h的头文件中,在EMaterialShadingModel中添加我们CustonShadingModel
在MaterialShader.cpp文件中,在GetShadingModelString(EMaterialShadingModel ShadingModel)中添加

给shader添加一个宏,让shader走新的分支

HLSLMaterialTranslator.cpp

为了之后的Shader操作,我们需要给我们的shader开放两个CustomData端口,分别在Material.cpp跟MaterialAttributeDefinitionMap.cpp文件

Material.cpp中在两个Customdata中增加我们的ShadingModel的名称
在MaterialAttributeDefinitionMap.cpp增加两个定义

接着我们让新的ShadingModel写入GBuffer,这里我们需要修改两个文件ShaderMaterial.h跟ShaderMaterialDerivedHelpers.cpp

在ShaderMaterial.h中添加我们的光照模型
ShaderMaterialDerivedHelpers.cpp中添加
Dst.WRITES_CUSTOMDATA_TO_GBUFFER = (Dst.USES_GBUFFER && (Mat.MATERIAL_SHADINGMODEL_SUBSURFACE || Mat.MATERIAL_SHADINGMODEL_PREINTEGRATED_SKIN || Mat.MATERIAL_SHADINGMODEL_SUBSURFACE_PROFILE || Mat.MATERIAL_SHADINGMODEL_CLEAR_COAT || Mat.MATERIAL_SHADINGMODEL_TWOSIDED_FOLIAGE || Mat.MATERIAL_SHADINGMODEL_HAIR || Mat.MATERIAL_SHADINGMODEL_CLOTH || Mat.MATERIAL_SHADINGMODEL_EYE || Mat.MATERIAL_SHADINGMODEL_TOON));

接着我们在ShaderGenerationUtil.cpp中添加生成

至此我们的C++部分修改已经完成,总的来说没什么难度,基本就是照葫芦画瓢,我们编译一下看看引擎

At this point, we have completed the modifications in the C++ part. Overall, it wasn’t difficult; it was mostly a matter of following a similar pattern. Let’s compile the engine now to see if our changes are successful.

看到这几个就相当于添加成功了

接着我们开始修改shader部分

Let’s continue with the modification of the shader part.

首先在ShadingCommon.ush中添加custom Shading Model的宏定义,顺序要和之前C++中的保持一致。

我们一开始定义的ShadingModel在NUM之前,所以在这的顺序不变,顺便NUM的ID也从13变为14

接着我们定义一下debug view下的ShadingModel的颜色

依然是在同一个文件中

然后我们在Definitions.usf中定义宏的默认值

现在我们在BasePassCommon.ush输出GBuffer

在最后加上MATERIAL_SHADINGMODEL_TOON

这步之后我们可以在Debug状态下看见我们的ShadingModel,现在我们输出CustomData分别修改DeferredShadingCommon.ush文件与ShadingModelsMaterial.ush

在DeferredShadingCommon.ush文件中增加我们的新建的ShadingModel的ID

下面就是正菜,我们先新建一个ush专门放新加的函数。

这个ush文件放在Engine/Shaders/Private这个路径下

我们下面来到Shadingmodels.ush这个文件先include我们刚才创建的头文件

接着添加着色算法

//Toon Shading model
FDirectLighting ToonBxDF(FGBufferData GBuffer, half3 N, half3 V, half3 L, float Falloff, float NoL, FAreaLight AreaLight, FShadowTerms Shadow)
{
	#if GBUFFER_HAS_TANGENT
		half3 X = GBuffer.WorldTangent;
		half3 Y = normalize(cross(N,X));
	#else	
		half3 X = 0;
		half3 Y = 0;
	#endif
	
	BxDFContext Context;
	Init(Context, N, X, Y, V, L);
	SphereMaxNoH(Context, AreaLight.SphereSinAlpha, true);
	Context.NoV = saturate(abs(Context.NoV) + 1e-5);
	
	float SpecularOffset = 0.5;
	float SpecularRange = GBuffer.CustomData.x;
	
	float3 ShadowColor = 0;
	ShadowColor = GBuffer.DiffuseColor * ShadowColor;
	float offset = GBuffer.CustomData.y;
	float SoftScatterStrength = 0;
	
	offset = offset * 2 - 1;
	half3 H = normalize(V + L);
	float NoH = saturate(dot(N, H));
	NoL = (dot(N, L) + 1) / 2; // overwrite NoL to get more range out of it
	half NoLOffset = saturate(NoL + offset);
	
	FDirectLighting Lighting;
	Lighting.Diffuse = AreaLight.FalloffColor * (smoothstep(0, 1, NoLOffset) * Falloff) * Diffuse_Lambert(GBuffer.DiffuseColor) * 2.2;
	
	float InScatter = pow(saturate(dot(L, -V)), 12) * lerp(3, .1f, 1);
	float NormalContribution = saturate(dot(N, H));
	float BackScatter = GBuffer.GBufferAO * NormalContribution / (PI * 2);
	
	Lighting.Specular = ToonStep(SpecularRange, (saturate(D_GGX(SpecularOffset, NoH)))) * (AreaLight.FalloffColor * GBuffer.SpecularColor * Falloff * 8);

	float3 TransmissionSoft = AreaLight.FalloffColor * (Falloff * lerp(BackScatter, 1, InScatter)) * ShadowColor * SoftScatterStrength;
	float3 ShadowLightener = (saturate(smoothstep(0, 1, saturate(1 - NoLOffset))) * ShadowColor * 0.1);
	
	Lighting.Transmission = (ShadowLightener + TransmissionSoft) * Falloff;
	
	return Lighting;
	
}

			float3 Attenuation = 1;
			BRANCH

			if (GBuffer.ShadingModelID == SHADINGMODELID_TOON)
			{
				float offset = GBuffer.CustomData.y;
				float TerminatorRange = saturate(GBuffer.Roughness - 0.5);
				
				offset = offset * 2 - 1;
				
				BRANCH

				if (offset >= 1)
				{
					Attenuation = 1;
				}
				else
				{
					float NoL = (dot(N, L) + 1) / 2;
					float NoLOffset = saturate(NoL + offset);
					float LightAttenuationOffset = saturate(Shadow.SurfaceShadow + offset);
					float ToonSurfaceShadow = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, LightAttenuationOffset);

					Attenuation = smoothstep(0.5 - TerminatorRange, 0.5 + TerminatorRange, NoLOffset) * ToonSurfaceShadow;
				}
			}
			//Toon Shading Model
在DefeeredLightingCommon.ush中添加以上代码以及修改以下代码

最后我们重新编译一下看一下效果

动态平面阴影

Shader "fake_shadow"
{
    Properties
    {
        //材质属性面板
        _MainTex ("主贴图",2D) = "white"{}

        _GroundY ("地面Y高度 (外部传入)",float) = 0
        _Shadow_Color("影子颜色",Color) = (1,1,1,1)
        _Shadow_Length("影子长度",float) = 0
        _Shadow_Rotated("影子旋转角度",float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent+5" /*"RenderType"="Transparence"  "RenderPipeline" = "UniversalPipeline" *///注意这里很重要,因为影子是要绘制在地面上,所以地面必须应该先绘制,否则blend混合的时候就是和背后的skybox进行混合了
        }

        pass
        {
            Stencil{

                Ref 1
                //Comp取值依次为  0:Disabled  1:Never  2:Less  3:Equal  4:LessEqual  5:Greater  6:NotEqual  7:GreaterEqual  8:Always
                Comp NotEqual //或者改成NotEqual
                //Pass取值依次为  0:Keep  1:Zero  2:Replace  3:IncrementSaturate  4:DecrementSaturate  5:Invert  6:IncrementWrap  7:DecrementWrap
                Pass Replace
            }

            Blend SrcAlpha oneMinusSrcAlpha   

            //因为和地面重叠所以做个偏移
            //也可以不做偏移,将传入的地面高度抬高一点即可
            Offset -2,-2

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                //这里worldPos一定是float4,因为vert()中实际是手动两次空间变换如果是float3会导致w分量丢失,透视除法会出错
                //如果不参与变换,只是传到frag()中使用的话,比如进行Blinn-Phong光照计算V向量那么float3就够了
                float4 worldPos : TEXCOORD0;
                //做阴影插值和Clip地面以下阴影用
                float cacheWorldY : TEXCOORD1;
                float worldPosY : TEXCOORD2;
};
 //           CBUFFER_START
CBUFFER_START(UnityPerMaterial)
            half _GroundY;
            half4 _Shadow_Color;   
            half _Shadow_Length;     
            half _Shadow_Rotated;
CBUFFER_END
//            CBUFFER_END
            
            v2f vert(appdata v)
            {
                v2f o = (v2f)0;

                //获取世界空间的位置
                o.worldPos = mul(unity_ObjectToWorld,v.vertex);
                //缓存世界空间下的y分量,后续两点作用
                //第一点 : 做插值用做计算xz的偏移量的多少
                //第二点 : 防止在地面以下
                o.cacheWorldY = o.worldPos.y;
    
                o.worldPosY = UnityObjectToWorldNormal(v.vertex).y;

                //设置世界空间下y的值全部都设置为传入的地面高度值
                o.worldPos.y = _GroundY;

                //根据世界空间下模型y值减去传入的地面高度值_GroundY
                //以这个值为传入 lerp(0,_Shadow_Length) 进行线性插值
                //最后获取到模型y值由低到高的插值lerpVal
                //这个max()函数 假设腿部在地面以下则裁切掉腿部阴影,后续使用clip后无需Max
                //half lerpVal = lerp(0,_Shadow_Length,max(0,o.cacheWorldY-_GroundY));
                half lerpVal = lerp(0,_Shadow_Length,o.cacheWorldY-_GroundY);
                

                //常量PI
                //const float PI = 3.14159265;
                //角度转换成弧度
                half radian = _Shadow_Rotated / 180.0 * UNITY_PI;

                //旋转矩阵,对(0,1)向量进行旋转,计算旋转后的向量,该向量就是阴影方向
                //2D旋转矩阵如下
                // [x]        [ cosθ , -sinθ ]
                // [ ]  乘以  
                // [y]        [ sinθ , cosθ  ]
                // x' = xcosθ - ysinθ
                // y' = xsinθ + ycosθ
                half2 ratatedAngle = half2((0*cos(radian)-1*sin(radian)),(0*sin(radian)+1*cos(radian)));
                
                //用以y轴高度为参考计算的插值 lerpVal 去 乘以一个旋转后的方向向量,作为阴影的方向
                //最终得到偏移后的阴影位置
                o.worldPos.xz += lerpVal * ratatedAngle;
                
                //变换到裁剪空间
                o.pos = mul(UNITY_MATRIX_VP,o.worldPos);

                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET
            {
                //剔除低于地面部分的片段
                clip(i.cacheWorldY - _GroundY);
                //用作阴影的Pass直接输出颜色即可
                return _Shadow_Color;
            }

            ENDCG
        }
    }
}
using UnityEngine;

public class SetShadowRotationAndHeight : MonoBehaviour
{

    public int materialIndex = 2; // 设置材质索引

    [SerializeField]
    private Transform rootTransform = null;          //创建模型时设置

    private Renderer rend = null;
    private bool isFindRend = false;
    private float extend = 30f;


    private Transform lightTrans=null;

    void Awake()
    {
        if (GetComponent<MeshFilter>() != null)
        {
            Mesh mesh = GetComponent<MeshFilter>().mesh;
            Bounds bd = mesh.bounds;
            bd.Expand(extend);
            mesh.bounds = bd;
        }
        else if(GetComponent<SkinnedMeshRenderer>() != null)
        {
            //SkinnedMeshRenderer skinnedMesh = GetComponent<SkinnedMeshRenderer>();
            //Bounds bd = skinnedMesh.bounds;
            //bd.Expand(extend);
            //skinnedMesh.bounds = bd;
        }
    }

    public void SetRootTransFrom(Transform root)
    {
        rootTransform = root;

        //GameObject mainLightObj = GameObject.Find("MainLight_Day");
        //if(mainLightObj==null)
        //{
        //    mainLightObj = GameObject.Find("FillLight_Day");
        //}
        //if (mainLightObj != null)
        //    lightTrans = mainLightObj.transform;
    }

    public Renderer GetTransformRenderer()
    {
        if (rend != null)
            return rend;
        if (isFindRend)
            return null;
        rend= GetComponent<Renderer>();
        isFindRend = true;
        return rend;
    }

    bool isDebug = false;

    void Update()
    {
        //if (lightTrans == null)
        //{
        //    if(!isDebug)
        //    {
        //        Debug.Log("找不到光源");
        //        isDebug = true;
        //    }
        //    return;
        //}
            

        if (rootTransform == null)
        {
            rootTransform = transform;
        }
        Renderer tempRend = GetTransformRenderer();

        if (tempRend == null)
            return;

        // 确保指定的材质索引有效
        if (materialIndex < 0 || materialIndex >= tempRend.materials.Length)
        {
            Debug.LogError(transform.name + "Invalid material index!");
            return;
        }

        Material mat = tempRend.materials[materialIndex]; // 获取指定索引的材质

        // 获取父节点的 Y 坐标
        float parentY = rootTransform.position.y + 0.2f;

        // 将父节点的 Y 坐标传递给指定索引的材质的 _GroundY 属性
        mat.SetFloat("_GroundY", parentY);

        // 获取主光源的方向

        if (RenderSettings.sun == null)
            return;

        Vector3 lightDirection = -RenderSettings.sun.transform.forward;
        //Vector3 lightDirection = -lightTrans.forward;

        // 计算主光源的旋转角度的 Y 值(使用 Atan2 函数)
        float shadowRotationY = Mathf.Atan2(lightDirection.x, lightDirection.z) * Mathf.Rad2Deg;

        // 将主光源的旋转角度的 Y 值传递给指定索引的材质的 _Shadow_Rotated 属性
        mat.SetFloat("_Shadow_Rotated", shadowRotationY);
    }
}

Material Blend Tools

写了个基于Height Map的Texture Mix的工具,应该还有提升的空间先看看效果

主要思路是通过splat_control这张贴图的四个通道控制_Splat0~_Splat3这四张贴图的混合,如果splat_control对应通道的值为1,那么这个通道对应的贴图就完全显示,为0则完全不显示,通过修改splat_control贴图就可以实现想要的混合效果了;

核心代码如下

fixed4 splat_control = tex2D (_Control, IN.uv_Control).rgba;	
	fixed3 lay1 = tex2D (_Splat0, IN.uv_Splat0);
	fixed3 lay2 = tex2D (_Splat1, IN.uv_Splat1);
	fixed3 lay3 = tex2D (_Splat2, IN.uv_Splat2);
	fixed3 lay4 = tex2D (_Splat3, IN.uv_Splat3);
	_Alpha = 0.0;
	Albedo.rgb = (lay1 * splat_control.r + lay2 * splat_control.g + lay3 * splat_control.b+ lay4 * splat_control.a);

混合处

float3 blend(float3 lay1, float3 lay2, float4 splat_control)
{
        float b1 = lay1.a * splat_control.r;
        float b2 = lay2.a * splat_control.g;
        float ma = max(b1,b2);
        b1 = max(b1 - (ma – 0.3), 0) * splat_control.r;
        b2 = max(b2 - (ma – 0.3), 0) * splat_control.g;

        return (lay1.rgb * b1 + lay2.rgb * b2)/(b1 + b2);
}

下面是工具

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
[RequireComponent(typeof(MeshCollider))]
public class MeshPainter : MonoBehaviour {

    void Start () {
	
	}
	

	void Update () {
	
	}
}

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections;

[CustomEditor(typeof(MeshPainter))]
[CanEditMultipleObjects]
public class MeshPainterStyle : Editor
{
    string contolTexName = "";

    bool isPaint;

    float brushSize = 16f;
    float brushStronger = 0.5f;

    Texture[] brushTex;
    Texture[] texLayer;

    int selBrush = 0;
    int selTex = 0;


    int brushSizeInPourcent;
    Texture2D MaskTex;
    void OnSceneGUI()
    {
        if (isPaint)
        {
            Painter();
        }

    }
    public override void OnInspectorGUI()
        
    {
        if (Cheak())
        {
            GUIStyle boolBtnOn = new GUIStyle(GUI.skin.GetStyle("Button"));//得到Button样式
            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                isPaint = GUILayout.Toggle(isPaint, EditorGUIUtility.IconContent("ClothInspector.PaintValue"), boolBtnOn, GUILayout.Width(35), GUILayout.Height(25));//编辑模式开关
            GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
            brushSize = (int)EditorGUILayout.Slider("Brush Size", brushSize, 1, 36);//笔刷大小
            brushStronger = EditorGUILayout.Slider("Brush Stronger", brushStronger, 0, 1f);//笔刷强度

            IniBrush();
            layerTex();
            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                    GUILayout.BeginHorizontal("box", GUILayout.Width(340));
                    selTex = GUILayout.SelectionGrid(selTex, texLayer, 4, "gridlist", GUILayout.Width(340), GUILayout.Height(86));
                    GUILayout.EndHorizontal();
                GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();

            GUILayout.BeginHorizontal();
                GUILayout.FlexibleSpace();
                    GUILayout.BeginHorizontal("box", GUILayout.Width(318));
                    selBrush = GUILayout.SelectionGrid(selBrush, brushTex, 9, "gridlist", GUILayout.Width(340), GUILayout.Height(70));
                    GUILayout.EndHorizontal();
                GUILayout.FlexibleSpace();
            GUILayout.EndHorizontal();
        }
        
    }

    //获取材质球中的贴图
    void layerTex()
    {
        Transform Select = Selection.activeTransform;
        texLayer = new Texture[4];
        texLayer[0] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat0")) as Texture;
        texLayer[1] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat1")) as Texture;
        texLayer[2] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat2")) as Texture;
        texLayer[3] = AssetPreview.GetAssetPreview(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Splat3")) as Texture;
    }

    //获取笔刷  
    void IniBrush()
    {
        string MeshPaintEditorFolder = "Assets/MeshPaint/Editor/";
        ArrayList BrushList = new ArrayList();
        Texture BrushesTL;
        int BrushNum = 0;
        do
        {
            BrushesTL = (Texture)AssetDatabase.LoadAssetAtPath(MeshPaintEditorFolder + "Brushes/Brush" + BrushNum + ".png", typeof(Texture));

            if (BrushesTL)
            {
                BrushList.Add(BrushesTL);
            }
            BrushNum++;
        } while (BrushesTL);
        brushTex = BrushList.ToArray(typeof(Texture)) as Texture[];
    }

    //检查
    bool Cheak()
    {
        bool Cheak = false;
        Transform Select = Selection.activeTransform;
        Texture ControlTex = Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Control");
        if(Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.shader == Shader.Find("4Tex_Blend_Normal") || Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.shader == Shader.Find("zcxshaderlibrary/texBlendWithBump"))
        {
            if(ControlTex == null)
            {
                EditorGUILayout.HelpBox("当前模型材质球中未找到Control贴图,绘制功能不可用!", MessageType.Error);
                if (GUILayout.Button("创建Control贴图"))
                {
                    creatContolTex();
                    //Select.gameObject.GetComponent<MeshRenderer>().sharedMaterial.SetTexture("_Control", creatContolTex());
                }
            }
            else
            {
                Cheak = true;
            }
        }
        else 
        {
            EditorGUILayout.HelpBox("当前模型shader错误!请更换!", MessageType.Error);
        }
        return Cheak;
    }

    //创建Contol贴图
    void creatContolTex()
    {

        //创建一个新的Contol贴图
        string ContolTexFolder = "Assets/MeshPaint/Controler/";
        Texture2D newMaskTex = new Texture2D(512, 512, TextureFormat.ARGB32, true);
        Color[] colorBase = new Color[512 * 512];
        for(int t = 0; t< colorBase.Length; t++)
        {
            colorBase[t] = new Color(1, 0, 0, 0);
        }
        newMaskTex.SetPixels(colorBase);

        //判断是否重名
        bool exporNameSuccess = true;
        for(int num = 1; exporNameSuccess; num++)
        {
            string Next = Selection.activeTransform.name +"_"+ num;
            if (!File.Exists(ContolTexFolder + Selection.activeTransform.name + ".png"))
            {
                contolTexName = Selection.activeTransform.name;
                exporNameSuccess = false;
            }
            else if (!File.Exists(ContolTexFolder + Next + ".png"))
            {
                contolTexName = Next;
                exporNameSuccess = false;
            }

        }

        string path = ContolTexFolder + contolTexName + ".png";
        byte[] bytes = newMaskTex.EncodeToPNG();
        File.WriteAllBytes(path, bytes);//保存


        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//导入资源
        //Contol贴图的导入设置
        TextureImporter textureIm = AssetImporter.GetAtPath(path) as TextureImporter;
        TextureImporterPlatformSettings texset = textureIm.GetDefaultPlatformTextureSettings();
        texset.format = TextureImporterFormat.RGBA32;
        //texset.maxTextureSize=512;
        textureIm.SetPlatformTextureSettings(texset);
        textureIm.isReadable = true;
        textureIm.anisoLevel = 9;
        textureIm.mipmapEnabled = false;
        textureIm.wrapMode = TextureWrapMode.Clamp;
        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//刷新


        setContolTex(path);//设置Contol贴图

    }

    //设置Contol贴图
    void setContolTex(string peth)
    {
        Texture2D ControlTex = (Texture2D)AssetDatabase.LoadAssetAtPath(peth, typeof(Texture2D));
        Selection.activeTransform.gameObject.GetComponent<MeshRenderer>().sharedMaterial.SetTexture("_Control", ControlTex);
    }

    void Painter()
    {
        
        
        Transform CurrentSelect = Selection.activeTransform;
        MeshFilter temp = CurrentSelect.GetComponent<MeshFilter>();//获取当前模型的MeshFilter
        float orthographicSize = (brushSize * CurrentSelect.localScale.x) * (temp.sharedMesh.bounds.size.x / 200);//笔刷在模型上的正交大小
        MaskTex = (Texture2D)CurrentSelect.gameObject.GetComponent<MeshRenderer>().sharedMaterial.GetTexture("_Control");//从材质球中获取Control贴图

        brushSizeInPourcent = (int)Mathf.Round((brushSize * MaskTex.width) / 100);//笔刷在模型上的大小
        bool ToggleF = false;
        Event e = Event.current;//检测输入
        HandleUtility.AddDefaultControl(0);
        RaycastHit raycastHit = new RaycastHit();
        Ray terrain = HandleUtility.GUIPointToWorldRay(e.mousePosition);//从鼠标位置发射一条射线
        if (Physics.Raycast(terrain, out raycastHit, Mathf.Infinity, 1 << LayerMask.NameToLayer("ground")))//射线检测名为"ground"的层
        {
            Handles.color = new Color(1f, 1f, 0f, 1f);//颜色
            Handles.DrawWireDisc(raycastHit.point, raycastHit.normal, orthographicSize);//根据笔刷大小在鼠标位置显示一个圆

            //鼠标点击或按下并拖动进行绘制
            if ((e.type == EventType.MouseDrag &amp;&amp; e.alt == false &amp;&amp; e.control == false &amp;&amp; e.shift == false &amp;&amp; e.button == 0) || (e.type == EventType.MouseDown &amp;&amp; e.shift == false &amp;&amp; e.alt == false &amp;&amp; e.control == false &amp;&amp; e.button == 0 &amp;&amp; ToggleF == false))
            {
                //选择绘制的通道
                Color targetColor = new Color(1f, 0f, 0f, 0f);
                switch (selTex)
                {
                    case 0:
                        targetColor = new Color(1f, 0f, 0f, 0f);
                        break;
                    case 1:
                        targetColor = new Color(0f, 1f, 0f, 0f);
                        break;
                    case 2:
                        targetColor = new Color(0f, 0f, 1f, 0f);
                        break;
                    case 3:
                        targetColor = new Color(0f, 0f, 0f, 1f);
                        break;

                }

                //targetColor = new Color(0f, 0f, 0f, 0f);

                Vector2 pixelUV = raycastHit.textureCoord;

                //计算笔刷所覆盖的区域
                int PuX = Mathf.FloorToInt(pixelUV.x * MaskTex.width);
                int PuY = Mathf.FloorToInt(pixelUV.y * MaskTex.height);
                int x = Mathf.Clamp(PuX - brushSizeInPourcent / 2, 0, MaskTex.width - 1);
                int y = Mathf.Clamp(PuY - brushSizeInPourcent / 2, 0, MaskTex.height - 1);
                int width = Mathf.Clamp((PuX + brushSizeInPourcent / 2), 0, MaskTex.width) - x;
                int height = Mathf.Clamp((PuY + brushSizeInPourcent / 2), 0, MaskTex.height) - y;

                Color[] terrainBay = MaskTex.GetPixels(x, y, width, height, 0);//获取Control贴图被笔刷所覆盖的区域的颜色

                Texture2D TBrush = brushTex[selBrush] as Texture2D;//获取笔刷性状贴图
                float[] brushAlpha = new float[brushSizeInPourcent * brushSizeInPourcent];//笔刷透明度

                //根据笔刷贴图计算笔刷的透明度
                for (int i = 0; i < brushSizeInPourcent; i++)
                {
                    for (int j = 0; j < brushSizeInPourcent; j++)
                    {
                        brushAlpha[j * brushSizeInPourcent + i] = TBrush.GetPixelBilinear(((float)i) / brushSizeInPourcent, ((float)j) / brushSizeInPourcent).a;
                    }
                }

                //计算绘制后的颜色
                for (int i = 0; i < height; i++)
                {
                    for (int j = 0; j < width; j++)
                    {
                        int index = (i * width) + j;
                        float Stronger = brushAlpha[Mathf.Clamp((y + i) - (PuY - brushSizeInPourcent / 2), 0, brushSizeInPourcent - 1) * brushSizeInPourcent + Mathf.Clamp((x + j) - (PuX - brushSizeInPourcent / 2), 0, brushSizeInPourcent - 1)] * brushStronger;

                        terrainBay[index] = Color.Lerp(terrainBay[index], targetColor, Stronger);
                    }
                }
                Undo.RegisterCompleteObjectUndo(MaskTex, "meshPaint");//保存历史记录以便撤销

                MaskTex.SetPixels(x, y, width, height, terrainBay, 0);//把绘制后的Control贴图保存起来
                MaskTex.Apply();
                ToggleF = true;
            }

              if(e.type == EventType.MouseUp &amp;&amp; e.alt == false &amp;&amp; e.button == 0 &amp;&amp; ToggleF == true)
            {

                SaveTexture();//绘制结束保存Control贴图
                ToggleF = false;
            }
        }
    }
    public void SaveTexture()
    {
        var path = AssetDatabase.GetAssetPath(MaskTex);
        var bytes = MaskTex.EncodeToPNG();
        File.WriteAllBytes(path, bytes);
        AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);//刷新
    }
}

Shader模块

Shader "zcxshaderlibrary/texBlendWithBump"
{
    Properties
    {
        _SpecularColor("SpecularColor",Color)=(1,1,1,1)
        _Smoothness("Smoothness",range(0,20))=10
        _Cutoff("Cutoff",float)=0.5
        [Space(10)][Header(Layer)]
        [Space(30)][Header(Layer1Map)]
        _Splat0 ("Layer 1(RGBA)",2D) = "white"{}
        _Splat0_Color("_Splat1 Color",Color) = (1,1,1,1)
        _BumpSplat0 ("Layer 1 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat0Scale("Layer 1 Normal Scale",float)=1
        [Space(30)][Header(Layer2Map)]
        _Splat1 ("Layer 2(RGBA)", 2D) = "white" {}
        _Splat1_Color("_Splat2 Color",Color) = (1,1,1,1)
        _BumpSplat1 ("Layer 2 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat1Scale("Layer 2 Normal Scale",float)=1
        [Space(30)][Header(Layer3Map)]
        _Splat2 ("Layer 3(RGBA)", 2D) = "white" {}
        _Splat2_Color("_Splat3 Color",Color) = (1,1,1,1)
        _BumpSplat2 ("Layer 3 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat2Scale("Layer 3 Normal Scale",float)=1
        [Space(30)][Header(Layer4Map)]
        _Splat3 ("Layer 4(RGBA)", 2D) = "white" {}
        _Splat3_Color("_Splat4 Color",Color) = (1,1,1,1)
        _BumpSplat3 ("Layer 4 Normal(Bump)", 2D) = "Bump" {}
        _BumpSplat3Scale("Layer 4 Normal Scale",float)=1

        [Space(30)][Header(Blend Texture)]
        _Control ("Control (RGBA)", 2D) = "white" {}
        _Weight("Blend Weight" , Range(0.001,1)) = 0.2 
        
    }
    SubShader
    {
        Tags
        {
            "RenderPipeline"="UniversalPipeline"
            "Queue"="Geometry"
            "RenderType"="Opaque"
        }

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        CBUFFER_START(UnityPerMaterial)
        
        float4 _BaseColor;
        float4 _Splat0_ST;
        float4 _Splat0_Color;
        float4 _SpecularColor;
        float4 _BumpSplat0_ST;
        float4 _Splat1_ST;
        float4 _Splat1_Color;
        float4 _BumpSplat1_ST;
        float4 _Splat2_ST;
        float4 _Splat2_Color;
        float4 _BumpSplat2_ST;
        float4 _Splat3_ST;
        float4 _Splat3_Color;
        float4 _BumpSplat3_ST;
        float4 _Control_ST;

        //float _Control;
        float _Weight;

        float _Smoothness;
        float _Cutoff;
        
        float _BumpSplat0Scale;
        float _BumpSplat1Scale;
        float _BumpSplat2Scale;
        float _BumpSplat3Scale;


        TEXTURE2D(_Splat0);
        SAMPLER(sampler_Splat0);  
        TEXTURE2D(_Splat1);
        SAMPLER(sampler_Splat1);  
        TEXTURE2D(_Splat2);
        SAMPLER(sampler_Splat2);  
        TEXTURE2D(_Splat3);
        SAMPLER(sampler_Splat3);  
        TEXTURE2D(_Control);
        SAMPLER(sampler_Control); 
        TEXTURE2D(_BumpSplat0);
        SAMPLER(sampler_BumpSplat0);
        TEXTURE2D(_BumpSplat1);
        SAMPLER(sampler_BumpSplat1);
        TEXTURE2D(_BumpSplat2);
        SAMPLER(sampler_BumpSplat2);  
        TEXTURE2D(_BumpSplat3);
        SAMPLER(sampler_BumpSplat3);  





        CBUFFER_END
        float4 _BaseMap_ST;
        
        ENDHLSL
    

        Pass
        {
            Name "URPSimpleLit" 
            Tags{"LightMode"="UniversalForward"}

            HLSLPROGRAM            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            #pragma vertex vert
            #pragma fragment frag

            

            struct Attributes
            {
                float4 positionOS : POSITION;
                float4 normalOS : NORMAL;
                float4 tangentOS  : TANGENT;
                float2 uv0 : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                float2 uv4 : TEXCOORD4;
            };
            struct Varings
            {
                float4 positionCS : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float2 uv3 : TEXCOORD3;
                float2 uv4 : TEXCOORD4;
                float3 positionWS : TEXCOORD5;
                float3 viewDirWS : TEXCOORD6;
                float3 normalWS : NORMAL_WS;
                //float3 normalWS1   : NORMAL_WS;
                float4 tangentWS  : TANGENT_WS;
            };
            
  
            
            inline float4 Blend(float depth1 ,float depth2,float depth3,float depth4 , float4 control) 
            {
                float4 blend ;
                
                blend.r =depth1 * control.r;
                blend.g =depth2 * control.g;
                blend.b =depth3 * control.b;
                blend.a =depth4 * control.a;
                
                float ma = max(blend.r, max(blend.g, max(blend.b, blend.a)));
                blend = max(blend - ma +_Weight , 0) * control;
                return blend/(blend.r + blend.g + blend.b + blend.a);
            }

            Varings vert(Attributes IN)
            {
                Varings OUT;
                VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
                real sign = IN.tangentOS.w * GetOddNegativeScale();
                OUT.positionCS = positionInputs.positionCS;
                OUT.positionWS = positionInputs.positionWS;
                OUT.viewDirWS = GetCameraPositionWS() - positionInputs.positionWS;
                OUT.normalWS = normalInputs.normalWS;
                OUT.tangentWS  = real4(normalInputs.tangentWS, sign);
                OUT.uv0=TRANSFORM_TEX(IN.uv0,_Splat0);
                OUT.uv1=TRANSFORM_TEX(IN.uv1,_Splat1);
                OUT.uv2=TRANSFORM_TEX(IN.uv2,_Splat2);
                OUT.uv3=TRANSFORM_TEX(IN.uv3,_Splat3);
                OUT.uv4=TRANSFORM_TEX(IN.uv4,_Control);
                return OUT;
            }
            
            float4 frag(Varings IN):SV_Target
            {   
                //
                float4 splat_control = SAMPLE_TEXTURE2D(_Control, sampler_Control, IN.uv4);
                //纹理贴图
                float4 lay1 = SAMPLE_TEXTURE2D(_Splat0, sampler_Splat0, IN.uv0);     
                float4 lay2 = SAMPLE_TEXTURE2D(_Splat1, sampler_Splat1, IN.uv1);
                float4 lay3 = SAMPLE_TEXTURE2D(_Splat2, sampler_Splat2, IN.uv2);
                float4 lay4 = SAMPLE_TEXTURE2D(_Splat3, sampler_Splat3, IN.uv3); 

                //Bump贴图
                float3 nor1TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat0, sampler_BumpSplat0,IN.uv0),_BumpSplat0Scale);
                float3 nor2TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat1, sampler_BumpSplat1,IN.uv1),_BumpSplat1Scale);
                float3 nor3TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat2, sampler_BumpSplat2,IN.uv2),_BumpSplat2Scale);
                float3 nor4TS = UnpackNormalScale(SAMPLE_TEXTURE2D (_BumpSplat3, sampler_BumpSplat3,IN.uv3),_BumpSplat3Scale);



                //BaseColor附加到各个图层
                lay1.rgb*=lay1.rgb*_Splat0_Color.rgb;
                lay2.rgb*=lay2.rgb*_Splat1_Color.rgb;
                lay3.rgb*=lay3.rgb*_Splat2_Color.rgb;
                lay4.rgb*=lay4.rgb*_Splat3_Color.rgb;
                
                

                //Normal计算
                real sgn = IN.tangentWS.w;
                real3 bitangent = sgn * cross(IN.normalWS.xyz, IN.tangentWS.xyz);
                real3 nor1WS = mul(nor1TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor2WS = mul(nor2TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor3WS = mul(nor3TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz));
                real3 nor4WS = mul(nor4TS, real3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz)); // 转换至世界空间
                //计算混合
                float4 blend = Blend(lay1.a,lay2.a,lay3.a,lay4.a,splat_control);
                float3 blendNor = nor1WS*blend.r+nor2WS*blend.g+nor3WS*blend.b+nor4WS*blend.a;

                //计算主光
                Light light = GetMainLight();
                float3 diffuse = LightingLambert(light.color, light.direction, IN.normalWS);
                float3 specular = LightingSpecular(light.color, light.direction, normalize(blendNor), normalize(IN.viewDirWS), _SpecularColor, _Smoothness);

                //计算附加光照
                uint pixelLightCount = GetAdditionalLightsCount();
                for (uint lightIndex = 0; lightIndex < pixelLightCount; ++lightIndex)
                {
                    Light light = GetAdditionalLight(lightIndex, IN.positionWS);
                    diffuse += LightingLambert(light.color, light.direction, IN.normalWS);
                    specular += LightingSpecular(light.color, light.direction, normalize(blendNor), normalize(IN.viewDirWS), _SpecularColor, _Smoothness);
                }


                

                float3 basecolor=lay1.rgb *blend.r  + lay2.rgb* blend.g + lay3.rgb * blend.b + lay4.rgb * blend.a;//混合
                
                
                float4 ambient=float4(SampleSH(blendNor), 1.0)*(basecolor,0);

                float3 diff=saturate(dot(light.direction,blendNor))*basecolor;

                real3 viewDirectionWS = SafeNormalize(GetCameraPositionWS() - IN.positionWS); // safe防止分母为0
                real3 h = SafeNormalize(viewDirectionWS + light.direction);
                float3 color=diff*diffuse+specular+ambient.rgb;
                clip(lay1.a*lay2.a*lay3.a*lay4.a-_Cutoff);
                return float4(color,0);
            }
            ENDHLSL            
        }

        
          Pass
        {
            Name "ShadowCaster"
            Tags{"LightMode" = "ShadowCaster"}

            ZWrite On
            ZTest LEqual
            Cull[_Cull]

            HLSLPROGRAM
            // Required to compile gles 2.0 with standard srp library
            #pragma prefer_hlslcc gles
            #pragma exclude_renderers d3d11_9x
            #pragma target 2.0

            // -------------------------------------
            // Material Keywords
            #pragma shader_feature _ALPHATEST_ON
            #pragma shader_feature _GLOSSINESS_FROM_BASE_ALPHA

            //--------------------------------------
            // GPU Instancing
            #pragma multi_compile_instancing

            #pragma vertex ShadowPassVertex
            #pragma fragment ShadowPassFragment


            //由于这段代码中声明了自己的CBUFFER,与我们需要的不一样,所以我们注释掉他
            //#include "Packages/com.unity.render-pipelines.universal/Shaders/SimpleLitInput.hlsl"
            //它还引入了下面2个hlsl文件
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceInput.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
            ENDHLSL
        }

    }
}

RenderFeatureToggler

最近在搞renderFeature,写了个脚本来控制他的开关

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;
 
[System.Serializable]
public struct RenderFeatureToggle
{
    public ScriptableRendererFeature feature;
    public bool isEnabled;
}
 
[ExecuteAlways]
public class RenderFeatureToggler : MonoBehaviour
{
    [SerializeField]
    private List<RenderFeatureToggle> renderFeatures = new List<RenderFeatureToggle>();
    [SerializeField]
    private UniversalRenderPipelineAsset pipelineAsset;
 
    private void Update()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(toggleObj.isEnabled);
        }
    }
}

改一下可以用脚本控制

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering.Universal;

[System.Serializable]
public struct RenderFeatureToggle
{
    public ScriptableRendererFeature feature;
    public bool isEnabled;
}

[ExecuteAlways]
public class RenderFeatureToggler : MonoBehaviour
{
    [SerializeField]
    private List<RenderFeatureToggle> renderFeatures = new List<RenderFeatureToggle>();
    [SerializeField]
    private UniversalRenderPipelineAsset pipelineAsset;

    private void Update()
    {
        //foreach (RenderFeatureToggle toggleObj in renderFeatures)
        //{
        //    toggleObj.feature.SetActive(toggleObj.isEnabled);
        //}
    }

    public void OpenRenderFeatureToggle()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(true);
        }
    }

    public void OffRenderFeatureToggle()
    {
        foreach (RenderFeatureToggle toggleObj in renderFeatures)
        {
            toggleObj.feature.SetActive(false);
        }
    }

}

物体遮挡剔除处理

先看下效果

视频有压缩,效果有点不对,下面是正确的效果

先上两段代码

using System;
using System.Collections.Generic;
using UnityEngine;

public class FadingObject : MonoBehaviour, IEquatable<FadingObject>
{
    public List<Renderer> Renderers = new List<Renderer>();
    public Vector3 Position;
    public List<Material> Materials = new List<Material>();
    [HideInInspector]
    public float InitialAlpha;
    //public float InitialFadeScale;


    private void Awake()
    {
        Position = transform.position;

        if (Renderers.Count == 0)
        {
            Renderers.AddRange(GetComponentsInChildren<Renderer>());
        }
        foreach(Renderer renderer in Renderers)
        {
            Materials.AddRange(renderer.materials);
        }

        InitialAlpha = Materials[0].color.a;
        Materials[0].SetFloat("_DitherThreshold", 1f);
    }

    public bool Equals(FadingObject other)
    {
        return Position.Equals(other.Position);
    }

    public override int GetHashCode()
    {
        return Position.GetHashCode();
    }
}


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FadeObjectBlockingObject : MonoBehaviour
{
    [SerializeField]
    private LayerMask LayerMask;
    [SerializeField]
    private Transform Target;
    [SerializeField]
    private Camera Camera;
    [SerializeField]
    [Range(0, 1f)]
    private float FadedAlpha = 0.33f;
    [SerializeField]
    private bool RetainShadows = true;
    [SerializeField]
    private Vector3 TargetPositionOffset = Vector3.up;
    [SerializeField]
    private float FadeSpeed = 1;

    [Header("Read Only Data")]
    [SerializeField]
    private List<FadingObject> ObjectsBlockingView = new List<FadingObject>();
    private Dictionary<FadingObject, Coroutine> RunningCoroutines = new Dictionary<FadingObject, Coroutine>();

    private RaycastHit[] Hits = new RaycastHit[10];

    private void OnEnable()
    {
        StartCoroutine(CheckForObjects());
    }

    private IEnumerator CheckForObjects()
    {
        while (true)
        {
            int hits = Physics.RaycastNonAlloc(
                Camera.transform.position,
                (Target.transform.position + TargetPositionOffset - Camera.transform.position).normalized,
                Hits,
                Vector3.Distance(Camera.transform.position, Target.transform.position + TargetPositionOffset),
                LayerMask
            );

            if (hits > 0)
            {
                for (int i = 0; i < hits; i++)
                {
                    FadingObject fadingObject = GetFadingObjectFromHit(Hits[i]);

                    if (fadingObject != null &amp;&amp; !ObjectsBlockingView.Contains(fadingObject))
                    {
                        if (RunningCoroutines.ContainsKey(fadingObject))
                        {
                            if (RunningCoroutines[fadingObject] != null)
                            {
                                StopCoroutine(RunningCoroutines[fadingObject]);
                            }

                            RunningCoroutines.Remove(fadingObject);
                        }

                        RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectOut(fadingObject)));
                        ObjectsBlockingView.Add(fadingObject);
                    }
                }
            }

            FadeObjectsNoLongerBeingHit();

            ClearHits();

            yield return null;
        }
    }

    private void FadeObjectsNoLongerBeingHit()
    {
        List<FadingObject> objectsToRemove = new List<FadingObject>(ObjectsBlockingView.Count);

        foreach (FadingObject fadingObject in ObjectsBlockingView)
        {
            bool objectIsBeingHit = false;
            for (int i = 0; i < Hits.Length; i++)
            {
                FadingObject hitFadingObject = GetFadingObjectFromHit(Hits[i]);
                if (hitFadingObject != null &amp;&amp; fadingObject == hitFadingObject)
                {
                    objectIsBeingHit = true;
                    break;
                }
            }

            if (!objectIsBeingHit)
            {
                if (RunningCoroutines.ContainsKey(fadingObject))
                {
                    if (RunningCoroutines[fadingObject] != null)
                    {
                        StopCoroutine(RunningCoroutines[fadingObject]);
                    }
                    RunningCoroutines.Remove(fadingObject);
                }

                RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectIn(fadingObject)));
                 objectsToRemove.Add(fadingObject);
            }
        }

        foreach(FadingObject removeObject in objectsToRemove)
        {
            ObjectsBlockingView.Remove(removeObject);
        }
    }

    private IEnumerator FadeObjectOut(FadingObject FadingObject)
    {
        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
            material.SetInt("_ZWrite", 0);
            material.SetInt("_Surface", 1);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;

            material.SetShaderPassEnabled("DepthOnly", false);
            material.SetShaderPassEnabled("SHADOWCASTER", RetainShadows);

            material.SetOverrideTag("RenderType", "Transparent");

            material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        float time = 0;

        while (FadingObject.Materials[0].color.a > FadedAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadingObject.InitialAlpha, FadedAlpha, time * FadeSpeed);
                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                        
                    );

                    //print("_1");
                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);
                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private IEnumerator FadeObjectIn(FadingObject FadingObject)
    {
        float time = 0;

        while (FadingObject.Materials[0].color.a < FadingObject.InitialAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadedAlpha, FadingObject.InitialAlpha, time * FadeSpeed);

                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                    );

                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);

                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
            material.SetInt("_ZWrite", 1);
            material.SetInt("_Surface", 0);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;

            material.SetShaderPassEnabled("DepthOnly", true);
            material.SetShaderPassEnabled("SHADOWCASTER", true);

            material.SetOverrideTag("RenderType", "Opaque");

            material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private void ClearHits()
    {
        System.Array.Clear(Hits, 0, Hits.Length);
    }

    private FadingObject GetFadingObjectFromHit(RaycastHit Hit)
    {
        return Hit.collider != null ? Hit.collider.GetComponent<FadingObject>() : null;
    }

}


放一个自动抓取场景中的物体然后再实现遮挡处理的脚本,这个主要是用在main camera需要从全局调用的场景

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FadeObjectBlockingObject : MonoBehaviour
{
    [SerializeField]
    private LayerMask LayerMask;
    [SerializeField]
    private Transform Target;
    [SerializeField]
    private Camera Camera;
    [SerializeField]
    [Range(0, 1f)]
    private float FadedAlpha = 0.33f;
    [SerializeField]
    private bool RetainShadows = true;
    [SerializeField]
    private Vector3 TargetPositionOffset = Vector3.up;
    [SerializeField]
    private float FadeSpeed = 1;

    [Header("Read Only Data")]
    [SerializeField]
    private List<FadingObject> ObjectsBlockingView = new List<FadingObject>();
    private Dictionary<FadingObject, Coroutine> RunningCoroutines = new Dictionary<FadingObject, Coroutine>();

    private RaycastHit[] Hits = new RaycastHit[10];
    void Start()
    {
        this.Camera = Camera.main;
        this.Target = this.transform;
        if (this.transform.parent.name == "ePlayer")
        {
            StartCoroutine(CheckForObjects());    
        }
    }

    private void OnEnable()
    {
        
    }

    private IEnumerator CheckForObjects()
    {
        while (true)
        {
            int hits = Physics.RaycastNonAlloc(
                Camera.transform.position,
                (Target.transform.position + TargetPositionOffset - Camera.transform.position).normalized,
                Hits,
                Vector3.Distance(Camera.transform.position, Target.transform.position + TargetPositionOffset),
                LayerMask
            );

            // Gizmos.DrawLine(Camera.transform.position, Target.transform.position);
            // Debug.DrawLine(Camera.transform.position, Target.transform.position , Color.red, 0.5f ); 
            if (hits > 0)
            {
                for (int i = 0; i < hits; i++)
                {
                    FadingObject fadingObject = GetFadingObjectFromHit(Hits[i]);

                    if (fadingObject != null &amp;&amp; !ObjectsBlockingView.Contains(fadingObject))
                    {
                        if (RunningCoroutines.ContainsKey(fadingObject))
                        {
                            if (RunningCoroutines[fadingObject] != null)
                            {
                                StopCoroutine(RunningCoroutines[fadingObject]);
                            }

                            RunningCoroutines.Remove(fadingObject);
                        }

                        RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectOut(fadingObject)));
                        ObjectsBlockingView.Add(fadingObject);
                    }
                }
            }

            FadeObjectsNoLongerBeingHit();

            ClearHits();

            yield return null;
        }
    }

    private void FadeObjectsNoLongerBeingHit()
    {
        List<FadingObject> objectsToRemove = new List<FadingObject>(ObjectsBlockingView.Count);

        foreach (FadingObject fadingObject in ObjectsBlockingView)
        {
            bool objectIsBeingHit = false;
            for (int i = 0; i < Hits.Length; i++)
            {
                FadingObject hitFadingObject = GetFadingObjectFromHit(Hits[i]);
                if (hitFadingObject != null &amp;&amp; fadingObject == hitFadingObject)
                {
                    objectIsBeingHit = true;
                    break;
                }
            }

            if (!objectIsBeingHit)
            {
                if (RunningCoroutines.ContainsKey(fadingObject))
                {
                    if (RunningCoroutines[fadingObject] != null)
                    {
                        StopCoroutine(RunningCoroutines[fadingObject]);
                    }
                    RunningCoroutines.Remove(fadingObject);
                }

                RunningCoroutines.Add(fadingObject, StartCoroutine(FadeObjectIn(fadingObject)));
                 objectsToRemove.Add(fadingObject);
            }
        }

        foreach(FadingObject removeObject in objectsToRemove)
        {
            ObjectsBlockingView.Remove(removeObject);
        }
    }

    private IEnumerator FadeObjectOut(FadingObject FadingObject)
    {
        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
            material.SetInt("_ZWrite", 0);
            material.SetInt("_Surface", 1);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Transparent;

            material.SetShaderPassEnabled("DepthOnly", false);
            material.SetShaderPassEnabled("SHADOWCASTER", RetainShadows);

            material.SetOverrideTag("RenderType", "Transparent");

            material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.EnableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        float time = 0;

        while (FadingObject.Materials[0].color.a > FadedAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadingObject.InitialAlpha, FadedAlpha, time * FadeSpeed);
                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                        
                    );

                    //print("_1");
                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);
                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private IEnumerator FadeObjectIn(FadingObject FadingObject)
    {
        float time = 0;

        while (FadingObject.Materials[0].color.a < FadingObject.InitialAlpha)
        {
            foreach (Material material in FadingObject.Materials)
            {
                if (material.HasProperty("_BaseColor"))
                {
                    float a = Mathf.Lerp(FadedAlpha, FadingObject.InitialAlpha, time * FadeSpeed);

                    material.color = new Color(
                        material.color.r,
                        material.color.g,
                        material.color.b,
                        a
                    );

                    //Setting fade value
                    material.SetFloat("_DitherThreshold", a);

                }
            }

            time += Time.deltaTime;
            yield return null;
        }

        foreach (Material material in FadingObject.Materials)
        {
            material.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.One);
            material.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.Zero);
            material.SetInt("_ZWrite", 1);
            material.SetInt("_Surface", 0);

            material.renderQueue = (int)UnityEngine.Rendering.RenderQueue.Geometry;

            material.SetShaderPassEnabled("DepthOnly", true);
            material.SetShaderPassEnabled("SHADOWCASTER", true);

            material.SetOverrideTag("RenderType", "Opaque");

            material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
        }

        if (RunningCoroutines.ContainsKey(FadingObject))
        {
            StopCoroutine(RunningCoroutines[FadingObject]);
            RunningCoroutines.Remove(FadingObject);
        }
    }

    private void ClearHits()
    {
        System.Array.Clear(Hits, 0, Hits.Length);
    }

    private FadingObject GetFadingObjectFromHit(RaycastHit Hit)
    {
        return Hit.collider != null ? Hit.collider.GetComponent<FadingObject>() : null;
    }

}

在Unity下实现Normal Smooth以解决描边断裂的问题

啥也不说先上代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using Unity.Jobs;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;

public class OutLineBaker : AssetPostprocessor
{
    static bool bExecuting = false;
    [MenuItem("Tools/BakeNormal", false, 100)]
    
    public static void BakeNormal()
        {
        Object obj = Selection.activeObject;
        string path = AssetDatabase.GetAssetPath(obj);
        string ext = Path.GetExtension(path);

        if(ext == ".fbx")
        {
            bExecuting = true;
            string newPath = Path.GetDirectoryName(path) + "/copy_@@@" + Path.GetFileName(path);
            Debug.Log($"new Path= {newPath}");
            AssetDatabase.CopyAsset(path, newPath);
            //AssetDatabase.ImportAsset(newPath);


        }
        else
        {
            Debug.Log($"必须对.fbx文件进行此操作");
        }
        //Debug.Log($"EXT name = {ext}");

        }
    static void OnPostOver(string path,bool isCopy)
    {
        if (isCopy)
        {
            string srcPath = path.Replace("copy_@@@", "");
            Debug.Log($"复制体后处理后,本体的path={srcPath}");
            AssetDatabase.ImportAsset(srcPath);
        }
        else
        {
            Debug.Log($"处理完毕,关闭开关");
            bExecuting = false;
        }

    }
    void OnPreprocessModel()
    {
        if (!bExecuting) return;
        //Input Settings

        ModelImporter importer = assetImporter as ModelImporter;

        if (!assetImporter.assetPath.Contains("copy_@@@")) return;

        importer.importNormals = ModelImporterNormals.Calculate;
        importer.normalCalculationMode = ModelImporterNormalCalculationMode.AngleWeighted;
        importer.normalSmoothingAngle = 180f;


        importer.importAnimation = false;//no animation imported
        importer.materialImportMode = ModelImporterMaterialImportMode.None;//no material imported
        Debug.Log($"1");
    }

    void OnPostprocessModel(GameObject go)
    {
        if (!bExecuting) return;
        //AfterInputing Settings

        if(go.name.Contains("copy_@@@"))
        {
            //Debug.Log($"Enter the copy obj postprocess");
            OnPostOver(assetPath, true);
        }
        else
        {
           // Debug.Log($"Self");
            string copyPath = Path.GetDirectoryName(assetPath) + "/copy_@@@" + Path.GetFileName(assetPath);
            Debug.Log($"复制体的Path = {copyPath}");
            GameObject cGo = AssetDatabase.LoadAssetAtPath<GameObject>(copyPath);

            Dictionary<string, Mesh> dic_name2mesh_src = GetMesh(go);//原本模型的各个子Mesh,通过节点名索引
            Dictionary<string, Mesh> dic_name2mesh_copy = GetMesh(cGo);//平滑模型各个子Mesh,通过节点名索引

            foreach (var item in dic_name2mesh_src) 
            {
                item.Value.colors = GetColor(item.Value,dic_name2mesh_copy[item.Key]);
            }

            OnPostOver("", false);


        }

      
    }
    Dictionary<string,Mesh>GetMesh(GameObject go)
    {
        Dictionary<string, Mesh> dic = new();
        foreach (var item in go.GetComponentsInChildren<MeshFilter>())
        {
            string n = item.name.Replace("copy_@@@", "");
            dic.Add(n, item.sharedMesh);
        }

        if (dic.Count == 0)
        {
            foreach (var item in go.GetComponentsInChildren<SkinnedMeshRenderer>())
            {
                string n = item.name.Replace("copy_@@@", "");
                dic.Add(n, item.sharedMesh);
            }
        }

        return dic;
    }

    Color[] GetColor(Mesh srcMesh,Mesh smthMesh)
    {
        //按照顶点位置索引,烘焙信息
        int lenSrc = srcMesh.vertices.Length;
        int lenSmth = smthMesh.vertices.Length;
        int maxOverlap = 10;

        NativeArray<Vector3> arr_vert_smth = new(smthMesh.vertices,Allocator.Persistent);//Allocator为资源分配的方式,一般选择Persistent
        NativeArray<Vector3> arr_normal_smth = new(smthMesh.normals, Allocator.Persistent);

        NativeArray<UnsafeHashMap<Vector3, Vector3>> arr_vert2normal = new(maxOverlap, Allocator.Persistent);
        NativeArray<UnsafeHashMap<Vector3, Vector3>.ParallelWriter> arr_vert2normal_writer = new(maxOverlap, Allocator.Persistent);

        for (int i = 0; i < maxOverlap; i++)            
        {
            arr_vert2normal[i] = new UnsafeHashMap<Vector3, Vector3>(lenSmth, Allocator.Persistent);
            arr_vert2normal_writer[i] = arr_vert2normal[i].AsParallelWriter();
        }


        CollectNormalJob collectJob = new()
        {
            verts = arr_vert_smth,
            normals = arr_normal_smth,
            arr_vert2normal= arr_vert2normal_writer
        };

        JobHandle collectHandle = collectJob.Schedule(lenSmth, 128);
        collectHandle.Complete();




        NativeArray<Vector3> arr_vert_src = new(srcMesh.vertices, Allocator.Persistent);
        NativeArray<Vector3> arr_nor_src = new(srcMesh.normals, Allocator.Persistent);
        NativeArray<Vector4> arr_tgt_src = new(srcMesh.tangents, Allocator.Persistent);

        NativeArray<Color> arr_color = new(lenSrc, Allocator.Persistent);

        BakeNormalJob bakeJob = new()
        {
            normals = arr_nor_src,
            verts = arr_vert_src,
            tangents = arr_tgt_src,
            bakedNormals = arr_vert2normal,
            colors = arr_color

        };

        JobHandle bakeHandel = bakeJob.Schedule(lenSrc,128);
        bakeHandel.Complete();//烘焙完成

        Color[] cols = new Color[lenSrc];
        arr_color.CopyTo(cols);

        foreach (var item in arr_vert2normal)
        {
            item.Dispose();
        }
        arr_vert_smth.Dispose();
        arr_normal_smth.Dispose();
        arr_vert2normal.Dispose();
        arr_vert2normal_writer.Dispose();
        arr_vert_src.Dispose();
        arr_nor_src.Dispose();
        arr_tgt_src.Dispose();
        arr_color.Dispose();


        return cols;









    }

    struct CollectNormalJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<Vector3> verts;

        [ReadOnly] public NativeArray<Vector3> normals;


        //顶点-法线的映射
        [NativeDisableContainerSafetyRestriction]//解除安全锁定


        //顶点-法线
        public NativeArray<UnsafeHashMap<Vector3, Vector3>.ParallelWriter> arr_vert2normal;//NativeArray是一个平行字典,有一个安全封装,一般是无法写入的,所以需要一个.ParallelWriter的写入器
        


        public void Execute(int index)
        {
            //每次执行
            for (int i = 0; i < arr_vert2normal.Length; i++)
            {
                if (i==arr_vert2normal.Length)
                {
                    Debug.Log($"超出顶点数量");
                    break;
                }
                if (arr_vert2normal[i].TryAdd(verts[index], normals[index]))
                {
                    Debug.Log($"当前添加:顶点:{verts[index]},法线:{normals[index]}");
                    break;
                } 
            }
        }
    }

    struct BakeNormalJob : IJobParallelFor
    {
        //切线空间 切线 法线 顶点位置 烘焙好的法线 输出-颜色数组
        [ReadOnly] public NativeArray<Vector3> normals;
        [ReadOnly] public NativeArray<Vector3> verts;
        [ReadOnly] public NativeArray<Vector4> tangents;
        [ReadOnly][NativeDisableContainerSafetyRestriction] public NativeArray<UnsafeHashMap<Vector3, Vector3>> bakedNormals;
        public NativeArray<Color> colors;

        public void Execute(int index)
        {
            Vector3 newNormal = Vector3.zero;

            for (int i = 0; i < bakedNormals.Length; i++)
            {
                if (bakedNormals[i][verts[index]]!= Vector3.zero){
                    newNormal += bakedNormals[i][verts[index]];
                }
                else
                {
                    break;
                }
            }
            newNormal = newNormal.normalized;

            //tangent作为VEC4数值,里面的W存储的是+1或者-1,因为opengl于DX坐标一个左手一个右手,所以拿Z区分
            Vector3 bitangent = (Vector3.Cross(normals[index], tangents[index]) * tangents[index].w).normalized;

            Matrix4x4 tbn = new(
                tangents[index],
                bitangent,
                normals[index],
                Vector4.zero
                );

            tbn = tbn.transpose;

            Vector3 finalNormal = tbn.MultiplyVector(newNormal).normalized;

            Color col = new(

                finalNormal.x*0.5f+0.5f,
                finalNormal.y*0.5f+0.5f,
                finalNormal.z*0.5f+0.5f,
                1



                );

            colors[index] = col;
        }
    }
}

使用之前,现在unity中载入Collection,如果Unity版本大于2022,需要将UnsafeHashMap替代成UnsafeParallelHashMap

1.C++编程简介

基本基础具备:

·学过某种procedure language(C 语言最佳)

·变量

·类型:int,float,char.struct …

·作用域

·循环

·流程控制: if-else,switch-case

·知道一个程序需要编译、链接才能被执行

·知道如何编译和链接


C++ Class

·class without pointer members

-Complex

·class with poniter members

-String

Classes之间的关系

-继承(inheritance)

-复合(composition)

-委托(delegation)

单一Class的设计被称为Object Based(基于对象)

符合Class相互之间有某种关联被称为Object Oriented(面向对象)


C++历史。。略过吧,基本就是发源于C,之前还有B语言,这些在C的时候已经有点了解了

面向对象不止C++还有JAVA跟我们最熟悉的C#


C++

C++语言

C++标准库


捏脸、换装系统总结

最近工作上遇到了与捏脸换装相关的工作内容,由于涉及到了以前未接触过的领域,在这做个工作总结。

捏脸这部分市面上一般有四种解决方案:

1.基于骨骼调整:调整骨骼的Scale、Rotation、Position

2.基于MESH差值:制作两个最大和最小值的Mesh,然后根据差值计算中间的Mesh,运行的时候生产最终的mesh

3.基于材质:简单的来说通过shader与贴图来控制肤色、瞳孔颜色、唇色等。

4.替换Mesh:这个属于是最简单的,但对于美术来说会产生巨量的工作量,一般是用作特殊配件,比如发型、时装、额外的饰品等。

捏脸的系统要保证运行时效率的同时,预览的速度也要做保证,有时候这两者会有一些冲突,可以通过多方案支持、内存换效率之类的方式来进行优化。如果是手游,最好关注下极限情况下的渲染效率和内存消耗,毕竟如果有材质参数的修改是无法合批的,基于骨骼或者mesh的修改的话对于内存也有一定的额外消耗。

基于骨骼调整

其实基于骨骼调整捏脸比较有参考价值的是《Honey Select》中的捏脸系统

对模型资源的规格进行分析, 发现存在大量的morph动画. 也就是说, HS中的的头部骨骼, 全部是用于捏脸的, 表情动画使用MorphTargets(BlendShape)驱动.

对于头部来说这种方式非常便捷,因为HS作为一款端游卖点就是高精度自定义,如果是手机端的话可能可以做相应的取舍。

下面是身体部分

查看其蒙皮信息可以发现, 所有影响顶点的骨骼名字全部带有”_s_”字样, 其父骨骼都是不带”_s_”的同名骨骼. 也就是说, HS的身体骨架中, 父骨骼负责动画, 子骨骼负责蒙皮.

基础的美术工作到这就差不多,但当我们进行调节的时候应该有一个更加平白的表达方式,比如说调节鼻子骨骼的信息,我们的确只需要调节NoseBase的Y值就好了,我们需要做的就是根据滑杆在最大值和最小值之间进行线性插值.

对于”眉毛角度Z轴”的调节, 这时只调节一根骨骼就不对了, 需要左右对称着来. 也就是说, 有一些调节项需要同时调节左右对称的两根骨骼.

对于”眉毛左右位置”, 如果在直线上两个端点之间进行插值, 很容易就跟面部三角形穿插了. 所以这里的插值路径只有最大值和最小值已经满足不了需求了, 而是需要按照曲线进行位置插值, 并且配合旋转插值贴合面部的法线方向. 也就是说, 一个调节项的插值可能是基于曲线(或多个关键帧), 而且可以同时影响骨骼Transform的多个分量.

眼睛的大小调节是最复杂的, 一共影响6根骨骼. 也就是说, 一个调节项是可以对应多根骨骼的.

我们总结一下, 脸型(或体型)调整原理就是:

  • 本质上修改的是骨骼的Local Transform(Translation, Rotation, Scale)
  • 一次只修改Local Transform的某个分量(或多个):Tx/Ty/Tz/Rx/Ry/Rz/Sx/Sy/Sz
  • 使用滑杆在预设的调节范围之间进行插值
  • 插值不一定是线性的, 可能是有多个关键帧
  • 每个调节项可能对应不只一根骨骼

对衣服的资源进行分析可以发现两点值得学习的地方:

  • 每件衣服都配有一个剔除掉被遮住的三角形的裸模, 一方面可以提升绘制性能, 一方面能避免衣服和皮肤两层三角形的穿插
  • 裙摆/披风/长衫等都是共用同样的8条物理骨骼, 算是比较传统的布料模拟做法

基于MESH差值

捏脸主要区域集中在正面,如果美术有能力统一脸部拓扑,使用Mesh差值通过顶点动画实现预设头更好的解决办法。取得新的顶点位置后,要重新计算Mesh法线。

实际脸部形变区域

即使头发使用mesh,头皮依然需要相应颜色的贴图映衬。对于短发寸头,通过对贴图改变发际线的形状也可以取得更多头发的多样性。

捏脸到底使用Mesh差值还是使用骨骼调整,具体还是要看引擎的要求。有的引擎对于Morph Targets优化更好,有的对于骨骼动画优化更好。同时这也取决于你的脸部动画是通过骨骼驱动的还是Morph驱动的。

如果没有性能要求,谁的效果好? 答案肯定是基于Mesh差值的结果。

关于楚留香传的捏脸系统,在额头附近同时调节两个控制器会造成异常的凸起。 这是因为两个控制器同时对相同一部分顶点造成位移而形成的的Double Transformation。可以在美术上normalize两个控制器的影响,或者在程序上限值两个控制器的调整范围。

捏脸是个系统工程,需要美术,程序,设计师多方面的讨论。

说一个不容易发现的细节:有的预设头如果眼睛的距离本身很近,那么控制器是在默认状态就会锁住你的范围,避免你捏出一个眼距过大过小的怪物。

Custom Stylized Lit(STILL WORKING)

该shader主要是为了解决项目中的通用材质的自定义问题,主要是用来实现较为写实的风格,基于URP Lit Shader制作而成,可以控制光的亮度、控制高光与菲涅尔等,并且支持预制笔刷。

1,该shader使用了3种颜色,用阈值与光滑度来remapping表面光照。

2,该shader可以控制Specular的方向与Intensity

3,通过Fresnel调节暗部细节