unity urp 分层调酒思路解析

发布于:2025-04-10 ⋅ 阅读:(37) ⋅ 点赞:(0)

故事从看了这样一篇文章开始,感谢大佬的分享。

https://zhuanlan.zhihu.com/p/694949392

https://zhuanlan.zhihu.com/p/696460056

这是一篇关于星穹铁道调酒效果的复刻文章,来源于星穹铁道的活动--杯中逸事

文章中已经涵盖了大部分原理解释,看完后感觉很有意思便下载观览一番,并对其中一些问题,如深度计算不正确,分层高度计算不正确等改进修正,此外附加易懂的注释

重点在于这两个文件Sh_LiquidBottle_Liquid.shader 用于绘制液体并相应脚本数据

其中大部分解释都在注释中,再配合原作者的文章应该会更容易理解

Shader "LiquidBottle/Liquid"
{
    Properties
    {
        [Space()] [Header(__________________Opacity__________________)]
        [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("SrcBlend", Float) = 5  //  One
        [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("DstBlend", Float) = 10  //  Zero
        
        [Space()] [Header(__________________Mask__________________)]
        _MaskUVScale ("遮罩缩放", Float) = 1
        _MaskInt ("遮罩强度", Float) = 1
        
        [Space()] [Header(__________________Warp__________________)]
        _LerpWarpTex ("分界扰动图", 2D) = "linearGray" {}
        _LerpWarpInt ("扰动强度", Range(0, 1)) = 0
        _LerpWarpMove ("扰动向量(仅xy)", Vector) = (1,1,0,0)
        
        [Space()] [Header(__________________Bubble__________________)]
        [NoScaleOffset] _BubbleTex ("泡沫遮罩", 2D) = "black" {}
        _Bubble_SV ("泡沫缩放(XY)速度(ZW)", Vector) = (1,1,0,1)
        _BubbleInt ("泡沫强度", Float) = 1
        
        [Space()] [Header(__________________Height__________________)]
        [NoScaleOffset] _HeightMap ("液面高度图", 2D) = "black" {}
        _HeightMap_SV ("高度缩放(XY)速度(ZW)", Vector) = (1,1,1,1)
        _HeightWarpInt ("液面高度缩放", Range(0, 0.2)) = 0.05
        _MaxLiquidHeightOS ("最大相对液面高度", Float) = 1
        _LiquidHeight01 ("当前液面高度(归一化)", Range(0, 1)) = 0.5
        
        [Space()] [Header(__________________Rim__________________)]
        _ReflectInt ("边缘光强度", Float) = 0.5
        _FresnelPow ("边缘光次幂", Range(1, 10)) = 2
        _EdgeWidth ("液面高亮宽度", Range(0, 0.1)) = 0.05
    }
    SubShader
    {
        Tags
        {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"  //  透明队列,没看出来有啥用
        }
        Pass
        {
            Stencil
            {
	            Ref 10          // 模板参考值 即参与比较的值
                Comp NotEqual   // 模板值比较条件
	            Pass Replace    // 通过比较条件且通过深度测试时的操作
                ZFail Replace   // 通过比较条件但未通过深度测试时的操作
            }  //  这里即是将所有通过比较条件,即模板值不为10的位置都写入10
            
            //  根据输入的One Zero,根据公式finalValue = sourceFactor * sourceValue operation destinationFactor * destinationValue可知finalValue = sourceValue 
            //而sourceValue代表新绘制的颜色,destinationValue是缓冲区中的颜色,即当序列化输入是Blend One Zero就是完全把当前新绘制的颜色覆盖,这和直接关闭混合的效果是一样的Blend Off
            //  这里这么写是因为外部代码会修改这两个值使得在不同情况下使用不同的混合方式
            Blend [_SrcBlend] [_DstBlend]
            
            //  开启深度写入,在像素着色器会对深度进行处理
            ZWrite On  
            
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Assets/TA/ShaderLib/HlslInclude/CommonHlslInc.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            //  CBUFFER是常量缓冲区 允许在运行时更新 数据打包在一起可减少数据传输次数
            //  UnityPerMaterial是内置的材质常量缓冲区,这么写即是将数据绑定设置为UnityPerMaterial
            //  而UnityPerMaterial是相对于材质来说的,即属性绑定在材质上,即所有使用这个材质的物体
            //  当访问CBuffer中的同名变量时,他们访问的同名变量是同一块地址
            //  这也意味着UnityPerMaterial自动绑定到材质
            
            //  如果需要自定义数据组织,如:CBUFFER_START(MyGlobalBuffer)
            //  这样通过设置全局变量Shader.SetGlobalFloat("Buffer中的变量名", 数值);
            //  随后在任何位置声明即可使用此组织中的数值
            
            //  还有很多unity内置的CBuffer,当引入文件的时候一定要检查使用
            //  如Core.hlsl间接引入的"urp/ShaderLibrary/Input.hlsl"中就有
            //  AdditionalLights/urp_ZBinBuffer/urp_TileBuffer/urp_ReflectionProbeBuffer这几个缓冲区
            //  注意应用这些已配置好的引擎功能
            CBUFFER_START(UnityPerMaterial)
            // 遮罩
            float _MaskUVScale;
            half _MaskInt;
            // 扰动
            float4 _LerpWarpTex_ST;  //  所有的_ST,默认值都为(1,1,0,0)
            half _LerpWarpInt;
            float4 _LerpWarpMove;
            // 视差泡沫
            float4 _Bubble_SV;
            half _BubbleInt;
            // 高度
            float4 _HeightMap_SV;
            half _HeightWarpInt;  //  脚本传递  控制高度图的影响
            float _MaxLiquidUnityHeightRefBottomCenter;  //  脚本传递  液体在unity单位下的最大高度
            float _LiquidHeight01;  //  脚本传递  当前的液面高度相对于_MaxLiquidHeightOS的比例[0,1]
            // 反射光
            half _ReflectInt;
            half _FresnelPow;
            half _EdgeWidth;
            // 下方中心位置
            float3 posWS_Origin;  //  脚本传递
            CBUFFER_END

            //  纹理和采样器不能声明在CBuffer,他们是是资源类型,处理方式不同,当适配SRP Batch的时候也不需要放到CBuffer中
            //  Texture2D是纹理类型,SamplerState是采样器类型(过滤,寻址模式等),而sampler2D同时包含纹理和采样器信息
            //  对于sampler2D仅需要tex2D(sampler2D类型的变量, 采样的uv)即可,方便易用
            //  而纹理类型和采样器类型的配合即是:纹理类型的变量.Sample(采样器类型的变量, 采样的uv) 相对繁琐但胜在灵活

            //  而对于这些变量,究竟是作用于这一个材质还是所有的shader共享,则取决于外部代码的赋值方式是Material.SetXXX()还是Shader.SetGlobalXXX()
            //  当一个同名变量既被Shader.SetGlobal又被Material.Set,那么会根据优先编辑规则判定,优先级规则是: [材质属性 > 全局变量]
            //  而如果有特殊情况,这个变量在面板上,这时取决于这个变量是否是默认值,如果是默认值则全局变量生效,否则是材质变量生效
            //  而如果设置过值,那么变量会一直保持最后一次设置的值,如果设置的就是默认值,那么即便面板上有序列化的值也会获得默认值
            sampler2D _LerpWarpTex, _BubbleTex, _HeightMap;
            Texture2DArray<half> _MaskTex2DArr;  //  脚本传递
            SamplerState sampler_MaskTex2DArr;

            #define MAX_LAYER_NUM 5  //  最大分层个数
            half4 liquidRGBA[MAX_LAYER_NUM];  //  脚本传递  每层的颜色
            half liquidBubbleInt[MAX_LAYER_NUM];  //  脚本传递  每层的气泡值
            half liquidLerpRange[MAX_LAYER_NUM];  //  脚本传递  每层的过渡值

            // 基于包围盒获得mesh的局部坐标系原点  下方中心位置
            // float3 GetRelativeOrigin()
            // {
            //     //  unity_RendererBounds_Min和unity_RendererBounds_Max包含在UnityInput.hlsl文件中,代表渲染盒的左下角和右上角的坐标
            //     //  左下角坐标加右上角坐标取平均即中心点
            //     float3 posWS_Origin = (unity_RendererBounds_Min + unity_RendererBounds_Max) * 0.5;
            //     //  再将计算出的中心点的y坐标设置为左下角坐标的y值,即获得下平面中心点的坐标
            //     posWS_Origin.y = unity_RendererBounds_Min.y;
            //     return posWS_Origin;
            // }
            
            struct a2v
            {
                float3 posOS	: POSITION;
                half3 nDirOS    : NORMAL;
                half4 tDirOS    : TANGENT;
                half color      : COLOR;  //  此处为自定义处理 在建模软件中将背面顶点色设置成了(1,1,1,1),正面顶点色为(0,0,0,1)
                float2 uv       : TEXCOORD0;
            };

            //  nointerpolation是HLSL语言内插修饰符的其中一种,代表不要内插,经典的如像素的法线插值
            //  顶点着色器的输出会经过插值最终传入到像素着色器的输入,用这些修饰符可以控制插值方式
            //  https://learn.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-struct
            struct v2f
            {
                float4 posCS	            : SV_POSITION;
                float3 posWS                : TEXCOORD0;
                half3 nDirWS                : TEXCOORD1;
                float2 uv                   : TEXCOORD2;
                float2 uv_Bubble            : TEXCOORD3;
                nointerpolation bool bPlane : TEXCOORD4;  //  拿出顶点色的R判断是正反面
            };

            v2f vert(a2v i)
            {
                v2f o;
                o.posWS = TransformObjectToWorld(i.posOS);
                o.posCS = TransformWorldToHClip(o.posWS);
                o.nDirWS = TransformObjectToWorldNormal(i.nDirOS);
                o.uv = i.uv;
                
                o.bPlane = i.color.r;  //  拿出顶点色的R判断是正反面
                
                //  计算TBN矩阵 因为这三个向量原本是切线空间的坐标基,将他们在世界空间中的表示组合成的矩阵即是可将切线空间坐标转换到世界空间坐标的矩阵
                half3 tDirWS = TransformObjectToWorldDir(i.tDirOS.xyz);
                half3 bDirWS = normalize(cross(o.nDirWS, tDirWS.xyz) * i.tDirOS.w);
                //  unity中构建矩阵的参数都为列排列
                half3x3 TBN = half3x3(tDirWS, bDirWS, o.nDirWS);  

                //  液体模型的uv是从下到上,让气泡动起来
                o.uv_Bubble = _Bubble_SV.xy *i.uv - _Bubble_SV.zw * _Time.x;
                return o;
            }

            //  depthOUT是深度缓冲区的数据,可以在像素着色器对它的值进行修改
            //  更多语义:https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
            half4 frag(v2f i, out float depthOUT : SV_Depth) : SV_Target
            {
                //------------------------------------预计算start-----------------------------------------------
                float3 posWS = i.posWS;  //  世界空间位置
                //float3 posWS_Origin = posWS - float3(0, 2.375 / 2, 0);  //  下方中心世界空间位置
                float3 posWS_Relative = i.posWS - posWS_Origin; //  相对于下方中心的世界空间位置
                //------------------------------------预计算end-----------------------------------------------
                
                //------------------------------------以xz平面采样高度图start-----------------------------------------------
                //  _HeightMap_SV.xy控制xz面上相对于中心点的xz向量所造成的影响,_HeightMap_SV.zw控制时间所造成的影响
                //  总体上是一个忽视z分量,以中间点为(0,0)向xz面延伸的uv
                float2 uv_Wave = _HeightMap_SV.xy * posWS_Relative.xz + _HeightMap_SV.zw * _Time.x;
                //  使用这个uv采样高度图  将区间映射为[-0.5, 0.5]  _HeightWarpInt用于控制高度图的影响  由脚本传递,在水位变化过程中呈现放大后缩回的效果
                half heightWave = _HeightWarpInt * (tex2D(_HeightMap, uv_Wave).r - 0.5);
                //------------------------------------以xz平面采样高度图start-----------------------------------------------

                //------------------------------------剔除当前液面高度以上的像素以实现涨水start-----------------------------------------------
                // 当前液面相对于posWS_Origin的高度 = 液面的最高unity高度 * 当前液面高度比例
                half liquidHeightRefOrigion = _MaxLiquidUnityHeightRefBottomCenter * _LiquidHeight01;  //  1.125
                //  当前液面高度添加液面震荡
                half liquidHeight = liquidHeightRefOrigion + heightWave;  //  1.125
                //  超出当前液面高度的像素切掉
                half clipH = liquidHeight - posWS_Relative.y;
                clip(clipH);  //  当小于0时被切掉
                //------------------------------------剔除当前液面高度以上的像素以实现涨水start-----------------------------------------------
                
                //------------------------------------计算视线与像素法线的相似程度start-----------------------------------------------
                half3 nDirWS = normalize(i.nDirWS);
                half3 vDirWS = normalize(GetCameraPositionWS() - posWS);
                half NV = saturate(dot(nDirWS, vDirWS));
                //------------------------------------计算视线与像素法线的相似程度end-----------------------------------------------

                //------------------------------------正面深度start-----------------------------------------------
                //  posCS是在顶点着色器中被标记为SV_POSITION的属性,因此在像素着色器中已经过透视除法(即无需再除以w分量),即i.posCS就是NDC坐标
                depthOUT = i.posCS.z;
                //------------------------------------正面深度end-----------------------------------------------

                //------------------------------------背面处理start-----------------------------------------------
                //  [branch]是HLSL的控制流修饰符,可告知GPU如何处理条件分支,[branch]是强制使用动态分支,而[flatten]则是强制使用静态分支
                //  默认情况下,GPU 在处理条件分支时可能会使用[静态分支]或[动态分支]
                //  静态分支:GPU 会预先计算所有可能的路径 -效率较高但会增加指令数量
                //  动态分支:GPU 在运行时根据条件动态选择执行路径 -减少指令数量但可能会导致性能下降
                //  https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-if
                [branch]
                if (i.bPlane)  //  仅对于背面,需要一些额外处理
                {
                    //  修正nv TODO::这里并不是正常水面的法线向量,假设水面平稳使用了float3(0,1,0),这样的光照结果是不正确的
                    NV = saturate(dot(float3(0,1,0), vDirWS));

                    //  消除假平面上的气泡
                    i.uv_Bubble = float2(0, 0);

                    //------------------------------------假平面start-----------------------------------------------
                    // 计算视线和液体顶部假平面的交点
                    float3 FakePlaneRayPosWS = GetPos_PlaneCrossRay(
                        GetCameraPositionWS(), -vDirWS,
                        float3(0, posWS_Origin.y + liquidHeightRefOrigion, 0), float3(0,1,0)
                    );
                    // 为背面的像素点写入假平面的深度,以冒充一个平面
                    float4 FakePlaneRayPosCS = TransformWorldToHClip(FakePlaneRayPosWS);
                    float3 FakePlaneRayPosNDC = FakePlaneRayPosCS / FakePlaneRayPosCS.w;
                    depthOUT = FakePlaneRayPosNDC.z;
                    //------------------------------------假平面end-----------------------------------------------
                }
                //------------------------------------背面处理end-----------------------------------------------

                //------------------------------------菲涅尔反射start-----------------------------------------------
                // Schlick菲涅尔反射 参考Unity Shader入门精要-p216
                float schlickReflect = _ReflectInt + (1 - _ReflectInt) * pow(1 - NV, _FresnelPow);
                //------------------------------------菲涅尔反射end-----------------------------------------------

                //------------------------------------添加水面向下的边缘光到反射start-----------------------------------------------
                //  计算反向的过渡乘数-从水面开始向下到_EdgeWidth逐渐从1开始接近0
                float smoothStepValue = 1 - saturate((clipH - 0) / (_EdgeWidth - 0));
                //  https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-smoothstep
                //  smoothstep的第三个变量是[min, max]之间的值,返回区间的01平滑值
                schlickReflect += smoothstep(0, _EdgeWidth, smoothStepValue * _EdgeWidth);
                //------------------------------------添加水面向下的边缘光到反射end-----------------------------------------------

                /*
                 * texture.GetDimensions(UINT MipLevel, typeX Width, typeX Height, typeX Elements, typeX Depth, typeX NumberOfLevels, typeX NumberOfSamples );
                 * 其中typeX表示可为uint和float其中任意一种
                 * [in] MipLevel:MIPMAP级别。如果未使用此参数,则假定第一个MIP级别
                 * [out] Width:纹理宽度
                 * [out] Width:纹理高度
                 * [out] Elements:数组中的元素数量
                 * [out] Depth:纹理深度
                 * [out] NumberOfLevels:MIPMAP级别的数量
                 * [out] NumberOfSamples:样品数量
                 */
                
                // 液体层混合高度准备,获得数组纹理个数信息
                uint3 size;
	            _MaskTex2DArr.GetDimensions(size.x, size.y, size.z);  //  所以可知这里size的三个分量分别是 x:纹理宽度 y:纹理高度 z:数组中的元素数量
                uint texArrCount = size.z;  //  数组中的元素数量,也是当前液体一共分的层数
                
                // 计算当前位置在总体可允许高度的[0, 1]
                half liquidHeight01_Current = saturate(posWS_Relative.y / _MaxLiquidUnityHeightRefBottomCenter);
                half liquidHeight_MulLayerNum = liquidHeight01_Current * texArrCount;

                //  计算当前位置所在层数
                half layerFloor = floor(liquidHeight_MulLayerNum);
                half layerCeil = layerFloor + 1;
                
                //------------------------------------液体颜色和分界混合start-----------------------------------------------
                //  根据当前所在层数进行0.5错位,以方便处理液面间混合
                uint layerFloorShiftPoint5 = floor(max(0, liquidHeight_MulLayerNum - 0.5)); // 偏移0.5是因为规定混合区域的上下界均不会超过各层的中线,即不会三重及以上混合
                uint layerCeilShiftPoint5 = layerFloorShiftPoint5 + 1;
                
                //  拿到上面一层对这一层的向下层的过渡宽度
                half lerpRange = liquidLerpRange[layerCeilShiftPoint5];
                //  计算每个像素位置的使用颜色,0代表用floor的颜色,1代表用ceil的颜色,中间的值表示混合
                half lerp01 = smoothstep(layerCeilShiftPoint5 - lerpRange, layerCeilShiftPoint5 + lerpRange, liquidHeight_MulLayerNum);

                //------------------------------------采样混合扰动贴图start-----------------------------------------------
                float2 uv_Warp = i.uv * _LerpWarpTex_ST.xy + _LerpWarpTex_ST.zw;  
                //  随时间移动
                uv_Warp += _Time.x * _LerpWarpMove.xy;
                //  采样分界混合扰动
                half lerpRangeWarpMask = _LerpWarpInt * tex2D(_LerpWarpTex, uv_Warp).r;
                //------------------------------------采样混合扰动贴图end-----------------------------------------------
                
                //  应用混合噪声影响 其中abs(lerp01 - 0.5) * 2)把0-1转化为0-1-0的曲线,这样可保证在原本lerp01为0和1也就是混合区间外,混合噪声不起作用
                lerp01 = saturate(lerp01 + (1 - abs(lerp01 - 0.5) * 2) * lerpRangeWarpMask);
                // 采样混合颜色
                half4 currentRGBA = lerp(liquidRGBA[layerFloorShiftPoint5], liquidRGBA[layerCeilShiftPoint5], lerp01);
                //------------------------------------液体颜色和分界混合end-----------------------------------------------
                
                //------------------------------------动态泡沫start-----------------------------------------------
                half4 bubbleRGBA;
                bubbleRGBA.rgb = _BubbleInt;  //  泡沫颜色现在固定白色
                //  采样泡沫透明度
                bubbleRGBA.a = tex2D(_BubbleTex, i.uv_Bubble).r;
                //  应用每种饮料的泡沫透明度
                bubbleRGBA.a *= lerp(liquidBubbleInt[layerFloorShiftPoint5], liquidBubbleInt[layerCeilShiftPoint5], lerp01);
                //------------------------------------动态泡沫end-----------------------------------------------

                //最终颜色混合
                half4 finalRGBA = 0;
                
                finalRGBA.rgb += currentRGBA.rgb;
                finalRGBA.rgb += bubbleRGBA.rgb * bubbleRGBA.a;
                finalRGBA.rgb *= (1 + schlickReflect);
                
                finalRGBA.a = max(currentRGBA.a, bubbleRGBA.a);

                return finalRGBA;
            }
            #undef MAX_LAYER_NUM
            ENDHLSL
        }
    }
}

 LiquidBottleManager.cs 用于控制添加饮料,冰块,混合等操作

using System.Collections.Generic;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Serialization;

public class LiquidBottleManager : MonoBehaviour
{
    [Space()] [Header("__________________Bottle__________________")]
    public GameObject gObj_Bottle;  //  杯子

    [Space()] [Header("__________________Liquid__________________")]
    public Renderer rdr_Liquid;
    public int maxLiquidNum;

    [Space()] [Header("__________________Ice__________________")]
    public GameObject pre_Ice;
    public float iceBirthH = 4;
    public float iceCenterBias = 0;
    
    [Space()] [Header("__________________SRP__________________")]
    //public bool bIceUseRenderFeature = false;
    //public Material mat_RFMerge;
    
    [Space()] [Header("__________________Height__________________")]
    public float maxLiquidUnityHeightRefBottomCenter = 1;
    
    [Space()] [Header("__________________Physic__________________")]
    public float dragScale = 2;
    
    [Space()] [Header("__________________Anime__________________")]
    public LiquidAnimation liquidAnimation = new LiquidAnimation();

    //  最大液体数量
    public const int maxLiquidNumInShader = 5;
    
    // shader参数ID
    private static readonly int id_liquidRGBA = Shader.PropertyToID("liquidRGBA");
    private static readonly int id_liquidBubbleInt = Shader.PropertyToID("liquidBubbleInt");
    private static readonly int id_liquidLerpRange = Shader.PropertyToID("liquidLerpRange");
    private static readonly int id_MaskTex2DArr = Shader.PropertyToID("_MaskTex2DArr");
    private static readonly int id_MaxLiquidUnityHeightRefBottomCenter = Shader.PropertyToID("_MaxLiquidUnityHeightRefBottomCenter");
    private static readonly int id_LiquidHeight01 = Shader.PropertyToID("_LiquidHeight01");
    private static readonly int id_HeightWarpInt = Shader.PropertyToID("_HeightWarpInt");
    private static readonly int id_posWS_Origin = Shader.PropertyToID("posWS_Origin");

    private List<Liquid> liquids;
    private Material mat_Liquid;
    private float _LiquidHeightWS_GLB;  // 全局液面世界高度, 给冰块用
    
    private void Start()
    {
        mat_Liquid = rdr_Liquid.material;
        ResetLayerNum(maxLiquidNum);
        ClearLiquid();
        liquidAnimation.SetManager(this);
    }
    
    private void Update()
    {
        RefreshHeightProp();
        liquidAnimation.UpdateAnim();
        //  物体世界位置
        mat_Liquid.SetVector(id_posWS_Origin, rdr_Liquid.gameObject.transform.position);
    }

    private void FixedUpdate()
    {
        // 冰块浮力
        if (rigid_Ice != null)
        {
            foreach (Rigidbody rigid_Ice_i in rigid_Ice)
            {
                IceFakeBuoyancy(rigid_Ice_i);
            }
        }
    }
    
#region GetSet参数
    /// <summary>
    /// 设置材质的缓存数组
    /// Liquids数组 -> Buffer -> 材质三段同步
    /// 数组参数设置, 首次必须设为可能的最大, 否则后续给shader设置更长的数组会被截断
    /// </summary>
    private Material dstLiquidMat;
    private RenderTexture buffer_Mask2DArr;
    public const int maskSize = 1024;
    private Color[] buffer_LiquidRGBA = new Color[maxLiquidNumInShader];
    private float[] buffer_LiquidBubbleInt = new float[maxLiquidNumInShader];
    private float[] buffer_LiquidLerpRange = new float[maxLiquidNumInShader];
    
    // 按Liquids数组 -> Buffer -> 材质的顺序同步三份数据
    private void UnifyArrayProps()
    {
        // 已有层的buffer
        int liquidNum = liquids.Count;
        for (int i = 0; i < liquidNum; i++)
        {
            Liquid liquid_i = liquids[i];
            buffer_LiquidRGBA[i] = liquid_i.RGBA;
            buffer_LiquidBubbleInt[i] = liquid_i.bubbleIntensity;
            buffer_LiquidLerpRange[i] = liquid_i.lerpRange;
        }

        // 尚未填充层的buffer
        // 为保证材质不取到无效的数组元素, 需对其余层按最后一有效层进行补齐
        Color lastRGBA = Color.clear;
        float lastBubbleInt = 0;
        if (liquidNum > 0)
        {
            Liquid lastLiquid = liquids.Last();
            lastRGBA = lastLiquid.RGBA;
            lastBubbleInt = lastLiquid.bubbleIntensity;
        }
        for (int i = liquidNum; i < maxLiquidNumInShader; i++)
        {
            buffer_LiquidRGBA[i] = lastRGBA;
            buffer_LiquidBubbleInt[i] = lastBubbleInt;
            buffer_LiquidLerpRange[i] = 0;
        }
        
        // 材质设置
        mat_Liquid.SetColorArray(id_liquidRGBA, buffer_LiquidRGBA);
        mat_Liquid.SetFloatArray(id_liquidBubbleInt, buffer_LiquidBubbleInt);
        mat_Liquid.SetFloatArray(id_liquidLerpRange, buffer_LiquidLerpRange);
    }
    
    // 颜色
    public void SetRGBA(int ID, Color RGBA)
    {
        if (ID < liquids.Count)
        {
            liquids[ID].RGBA = RGBA;
        }
        buffer_LiquidRGBA[ID] = RGBA;
        mat_Liquid.SetColorArray(id_liquidRGBA, buffer_LiquidRGBA);
    }
    public Color GetRGBA(int ID) => liquids[ID].RGBA;
    
    // Mask
    public RenderTexture CopyMaskArrayBuffer()
    {
        RenderTexture outArr = new RenderTexture(buffer_Mask2DArr);
        Graphics.CopyTexture(buffer_Mask2DArr, outArr);
        return outArr;
    }
    public RenderTexture GetMaskArray() => buffer_Mask2DArr;
    
    // 泡沫强度
    public void SetBubbleInt(int ID, float bubbleInt)
    {
        if (ID < liquids.Count)
        {
            liquids[ID].bubbleIntensity = bubbleInt;
        }
        buffer_LiquidBubbleInt[ID] = bubbleInt;
        mat_Liquid.SetFloatArray(id_liquidBubbleInt, buffer_LiquidBubbleInt);
    }
    public float GetBubbleInt(int ID) => liquids[ID].bubbleIntensity;

    // 混合范围
    public void SetLerpRange(int ID, float lerpRange)
    {
        if (ID < liquids.Count)
        {
            liquids[ID].lerpRange = lerpRange;
        }
        buffer_LiquidLerpRange[ID] = lerpRange;
        mat_Liquid.SetFloatArray(id_liquidLerpRange, buffer_LiquidLerpRange);
    }
    public float GetLerpRange(int ID) => liquids[ID].lerpRange;
    
    // 归一化高度
    public void SetHeight01(float h01)
    {
        mat_Liquid.SetFloat(id_LiquidHeight01, h01);
    }
    public float GetHeight01()
    {
        return mat_Liquid.GetFloat(id_LiquidHeight01);
    }
    
    // 液面扰动强度
    public void SetHeightWarpInt(float warpInt)
    {
        mat_Liquid.SetFloat(id_HeightWarpInt, warpInt);
    }
    
    // 当前液体层数
    public int GetCurrentLiquidNum() => liquids.Count;
    
    // 标准创建mask2DArr
    private static RenderTexture CreateMask2DArr()
    {
        RenderTexture outRT = new RenderTexture(
            maskSize, maskSize, 0, 
            RenderTextureFormat.R8, RenderTextureReadWrite.Linear
        );
        outRT.dimension = TextureDimension.Tex2DArray;
        outRT.wrapMode = TextureWrapMode.Repeat;
        outRT.useMipMap = false;
        outRT.enableRandomWrite = true;
        return outRT;
    }
    
    // 设置最大液体层数 (重新创建缓存mask的tex2DArray)
    public void ResetLayerNum(int layerNum)
    {
        if (layerNum > 0)
        {
            //  如果发生变化则release
            if (buffer_Mask2DArr != null && layerNum != buffer_Mask2DArr.volumeDepth)
            {
                buffer_Mask2DArr.Release();
            }
            
            buffer_Mask2DArr = CreateMask2DArr();
            buffer_Mask2DArr.volumeDepth = layerNum;  //  设置有几张
            mat_Liquid.SetTexture(id_MaskTex2DArr, buffer_Mask2DArr);
        }
    }
    
    // 更新液面高度参数
    private void RefreshHeightProp()
    {
        // 全局液面世界高度
        float minLiquidHeightWS = rdr_Liquid.bounds.min.y - 0.01f;    // 取包围盒下界作为【杯底】
        _LiquidHeightWS_GLB = Mathf.Lerp(
            minLiquidHeightWS, minLiquidHeightWS + maxLiquidUnityHeightRefBottomCenter,
            GetHeight01()
        );
        //Shader.SetGlobalFloat(id_LiquidHeightWS_GLB, _LiquidHeightWS_GLB);

        // 液面最大高度
        mat_Liquid.SetFloat(id_MaxLiquidUnityHeightRefBottomCenter, maxLiquidUnityHeightRefBottomCenter);
    }
#endregion
#region 液体
    // 添加液体
    public void AddLiquid(Liquid liquidIN)
    {
        int newLiquidID = liquids.Count;
        
        if (newLiquidID < maxLiquidNum)
        {
            // 因为动画动态调参, 且存在同类液体复用的情况, 所以都需要new
            Liquid newLiquid = new Liquid(liquidIN);
            liquids.Add(newLiquid);
            
            // 同步数组数据
            UnifyArrayProps();
            
            // 更新mask2DArr
            Texture2D newMask = newLiquid.maskTex;
            Graphics.Blit(newMask, buffer_Mask2DArr, 0, newLiquidID);
            if (newLiquidID + 1 < maxLiquidNum) // 使得液面能采到当前层的mask而不为空
            {
                Graphics.Blit(newMask, buffer_Mask2DArr, 0, newLiquidID + 1);
            }
            
            // 动画播放预备
            liquidAnimation.StartUpdate_Add();
        }
    }
    
    // 清除(初始化)液体
    public void ClearLiquid()
    {
        liquids = new List<Liquid>(maxLiquidNumInShader);
        mat_Liquid.SetFloat(id_LiquidHeight01, 0);
        UnifyArrayProps();
        for (int i = 0; i < buffer_Mask2DArr.volumeDepth; i++)
        {
            Graphics.Blit(Texture2D.blackTexture, buffer_Mask2DArr, 0, i);
        }
    }
    
    // 混合液体
    public void MixLiquid()
    {
        if (liquids.Count > 1)
        {
            liquidAnimation.StartUpdate_Mix();
        }
    }
#endregion
#region 冰块
    // 重置冰块位置和数量
    private List<Rigidbody> rigid_Ice;
    private float iceSize;
    public void ResetIce(int num)
    {
        // 删除已有的冰块
        if (rigid_Ice != null)
        {
            foreach (Rigidbody rigid_Ice_i in rigid_Ice)
            {
                if (rigid_Ice_i.gameObject)
                {
                    Destroy(rigid_Ice_i.gameObject);
                }
            }
        }
        rigid_Ice = null;
        
        // 在杯子上方生成冰块
        if (num > 0)
        {
            rigid_Ice = new List<Rigidbody>(num);
            Vector3 posWS_Bottle = gObj_Bottle.transform.position;
            iceSize = pre_Ice.transform.lossyScale.x;   // 冰块mesh的边长=单位1
            for (int i = 0; i < num; i++)
            {
                Vector3 pos_i = posWS_Bottle + Vector3.up * (iceBirthH + 2 * i * iceSize);
                GameObject ice = Instantiate(pre_Ice, pos_i, Quaternion.Euler(45, 45 * i, 45));
                Rigidbody rigid_i = ice.GetComponent<Rigidbody>();
                rigid_Ice.Add(rigid_i);
            }
        }
        
    }
    
    // 浮力模拟
    private void IceFakeBuoyancy(Rigidbody rigidbody)
    {
        float currentH = rigidbody.transform.position.y + iceCenterBias;
        float halfSize = 0.5f * iceSize;
        float bottomWS = currentH - halfSize;
        if (bottomWS < _LiquidHeightWS_GLB)
        {
            float V = iceSize * iceSize * Mathf.Min(iceSize, _LiquidHeightWS_GLB - bottomWS);
            Vector3 drag = -rigidbody.velocity * dragScale * V;
            rigidbody.AddForce(-Physics.gravity * V + drag);
        }
    }
#endregion

    // UI
#if UNITY_EDITOR
    public Liquid currentLiquid_UI;
    public int iceNum = 3;
    private Material mat_DrawMask2DArr;
    private RenderTexture rt_DrawMask2DArr;
    public void DrawUI()
    {
        EditorUtility.SetDirty(this);
        
        GUILayout.Space(10);
        GUILayout.Label("__________________杯子__________________");
        gObj_Bottle = EditorGUILayout.ObjectField("杯子", gObj_Bottle, typeof(GameObject), true) as GameObject;
        
        GUILayout.Space(10);
        GUILayout.Label("__________________液体__________________");
        rdr_Liquid = EditorGUILayout.ObjectField("液体Renderer", rdr_Liquid, typeof(Renderer), true) as Renderer;
        GUI.enabled = liquidAnimation.GetUpdateMode() == LiquidAnimation.UpdateMode.None;
        EditorGUI.BeginChangeCheck();
        maxLiquidNum = EditorGUILayout.IntSlider("最大层数", maxLiquidNum, 1, maxLiquidNumInShader);
        if (Application.isPlaying && EditorGUI.EndChangeCheck())
        {
            ClearLiquid();
            ResetLayerNum(maxLiquidNum);
        }
        
        // 目前添加的液体颜色
        GUI.enabled = false;
        if (liquids != null)
        {
            GUILayout.BeginHorizontal();
            for (int i = 0; i < liquids.Count; i++)
            {
                EditorGUILayout.ColorField(liquids[i].RGBA);
            }
            GUILayout.EndHorizontal();
        }
        GUI.enabled = true;
        
        GUILayout.BeginHorizontal();
        currentLiquid_UI = EditorGUILayout.ObjectField("待添加液体", currentLiquid_UI, typeof(Liquid), false) as Liquid;
        GUI.enabled = Application.isPlaying;
        if (currentLiquid_UI)
        {
            GUI.enabled = false;
            EditorGUILayout.ColorField(currentLiquid_UI.RGBA);
            GUI.enabled = 
                Application.isPlaying && 
                liquids != null && liquids.Count < maxLiquidNum && 
                liquidAnimation.GetUpdateMode() == LiquidAnimation.UpdateMode.None;
            if (GUILayout.Button("【添加】"))
            {
                AddLiquid(currentLiquid_UI);
            }
        }
        GUI.enabled = true;
        GUILayout.EndHorizontal();
        GUI.enabled = Application.isPlaying && liquidAnimation.GetUpdateMode() == LiquidAnimation.UpdateMode.None;
        GUILayout.BeginHorizontal();
        if (GUILayout.Button("【清空】"))
        {
            ClearLiquid();
        }
        if (GUILayout.Button("【混合】"))
        {
            MixLiquid();
        }
        GUILayout.EndHorizontal();
        GUI.enabled = true;
        
        GUILayout.Space(10);
        GUILayout.Label("__________________冰块__________________");
        pre_Ice = EditorGUILayout.ObjectField("冰块预制体", pre_Ice, typeof(GameObject), false) as GameObject;
        iceBirthH = EditorGUILayout.FloatField("冰块出生高度", iceBirthH);
        iceCenterBias = EditorGUILayout.Slider("冰块中心偏移", iceCenterBias, -1, 1);
        iceNum = EditorGUILayout.IntField("冰块数量", iceNum);
        GUI.enabled = Application.isPlaying;
        if (GUILayout.Button("【重置】"))
        {
            ResetIce(iceNum);
        }
        GUI.enabled = false;
        GUILayout.Label("冰块尺寸=" + iceSize);
        GUI.enabled = true;
        
        GUILayout.Space(10);
        GUILayout.Label("__________________SRP__________________");
        EditorGUI.BeginChangeCheck();
        // bIceUseRenderFeature = GUILayout.Toggle(bIceUseRenderFeature, "使用SRP");
        // mat_RFMerge = EditorGUILayout.ObjectField("SRP混合材质", mat_RFMerge, typeof(Material), false) as Material;
        // if (EditorGUI.EndChangeCheck() && Application.isPlaying)
        // {
        //     SetRenderMode(bIceUseRenderFeature);
        // }
        
        GUILayout.Space(10);
        GUILayout.Label("__________________高度__________________");
        maxLiquidUnityHeightRefBottomCenter = EditorGUILayout.FloatField("最大相对液面高度", maxLiquidUnityHeightRefBottomCenter);
        
        GUILayout.Space(10);
        GUILayout.Label("__________________物理__________________");
        dragScale = EditorGUILayout.Slider("阻力倍率", dragScale, 0, 10);
        
        GUILayout.Space(10);
        GUILayout.Label("__________________动画__________________");
        GUILayout.Label("【添加液体动画】");
        liquidAnimation.lifeTime_Add = EditorGUILayout.FloatField("动画时间", liquidAnimation.lifeTime_Add);
        liquidAnimation.curve_Up = EditorGUILayout.CurveField("高度变化", liquidAnimation.curve_Up);
        liquidAnimation.curve_Warp_Add = EditorGUILayout.CurveField("液面扰动强度", liquidAnimation.curve_Warp_Add);
        liquidAnimation.curve_LerpRange_Add = EditorGUILayout.CurveField("分层混合范围", liquidAnimation.curve_LerpRange_Add);
        GUILayout.Label("【混合液体动画】");
        liquidAnimation.lifeTime_Mix = EditorGUILayout.FloatField("动画时间", liquidAnimation.lifeTime_Mix);
        liquidAnimation.curve_RGBA_Mix = EditorGUILayout.CurveField("颜色变化", liquidAnimation.curve_RGBA_Mix);
        liquidAnimation.curve_Warp_Mix = EditorGUILayout.CurveField("液面扰动强度", liquidAnimation.curve_Warp_Mix);
        liquidAnimation.curve_LerpRange_Mix = EditorGUILayout.CurveField("分层混合范围", liquidAnimation.curve_LerpRange_Mix);
        
        GUILayout.Space(10);
        GUILayout.Label("__________________其他数据__________________");
        GUI.enabled = false;
        if (mat_Liquid)
        {
            GUILayout.Label("材质颜色");
            {
                GUILayout.BeginHorizontal();
                Color[] mCol = mat_Liquid.GetColorArray(id_liquidRGBA);
                if (mCol != null)
                {
                    foreach (var RGBA in mCol)
                    {
                        EditorGUILayout.ColorField(RGBA);
                    }
                }
                GUILayout.EndHorizontal();
            }
            GUILayout.Label("泡沫强度");
            {
                GUILayout.BeginHorizontal();
                float[] mBubbleInt = mat_Liquid.GetFloatArray(id_liquidBubbleInt);
                if (mBubbleInt != null)
                {
                    foreach (var bubbleInt in mBubbleInt)
                    {
                        EditorGUILayout.FloatField(bubbleInt);
                    }
                }
                GUILayout.EndHorizontal();
            }
            GUILayout.Label("材质混合范围");
            {
                GUILayout.BeginHorizontal();
                float[] mLerpRange = mat_Liquid.GetFloatArray(id_liquidLerpRange);
                if (mLerpRange != null)
                {
                    foreach (var lerpRange in mLerpRange)
                    {
                        EditorGUILayout.FloatField(lerpRange);
                    }
                }
                GUILayout.EndHorizontal();
            }
        }
        GUI.enabled = true;
    }

     const int debugTexsize = 128;
     private void OnGUI()
     {
         return;
         
         // mask纹理数组
         Texture mMask2DArr = mat_Liquid.GetTexture(id_MaskTex2DArr);
         if (mMask2DArr != null)
         {
             if (!mat_DrawMask2DArr)
             {
                 mat_DrawMask2DArr = new Material(Shader.Find("Editor/LiquidBottle_ArrayUI"));
             }
             if (!rt_DrawMask2DArr)
             {
                 rt_DrawMask2DArr = new RenderTexture(256, 256, 0, RenderTextureFormat.ARGB32);
             }
             mat_DrawMask2DArr.SetTexture("_Tex2DArr", mMask2DArr);
             Graphics.Blit(null, rt_DrawMask2DArr, mat_DrawMask2DArr);
             RenderTexture.active = null;    // 没有这行, UI无法有效渲染
             
             GUI.DrawTexture(new Rect(0,0,debugTexsize,debugTexsize), rt_DrawMask2DArr);
         }
         
         // RenderFeature中间纹理
         // 场景
         // GUI.DrawTexture(new Rect(0, debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_SceneColor);
         // GUI.DrawTexture(new Rect(debugTexsize, debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_SceneDepth);
         // // 液体
         // GUI.DrawTexture(new Rect(0, 2*debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_LiquidColor);
         // GUI.DrawTexture(new Rect(debugTexsize, 2*debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_LiquidDepth);
         // // 冰块
         // GUI.DrawTexture(new Rect(0, 3*debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_IceColor);
         // GUI.DrawTexture(new Rect(debugTexsize, 3*debugTexsize, debugTexsize, debugTexsize), 
         //     renderPass.handle_IceDepth);
     }
#endif
}

#if UNITY_EDITOR
[CustomEditor(typeof(LiquidBottleManager))]
class LiquidBottleManagerEditor : Editor
{
    private LiquidBottleManager dst;
    public override void OnInspectorGUI()
    {
        dst = (LiquidBottleManager) target;
        dst.DrawUI();
    }
}
#endif

另外附上我常用的工具方法,其中也有一些计算原理解释

#ifndef CommonHlslFunction
#define CommonHlslFunction

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

//给定轴给定旋转角度构造旋转矩阵
float4x4 CreateRotationMatrix(float3 axis, float angle)
{
    float s = sin(angle);
    float c = cos(angle);
    float t = 1.0f - c;

    float3x3 m = {
        { axis.x * axis.x * t + c, axis.x * axis.y * t - axis.z * s, axis.x * axis.z * t + axis.y * s },
        { axis.y * axis.x * t + axis.z * s, axis.y * axis.y * t + c, axis.y * axis.z * t - axis.x * s },
        { axis.z * axis.x * t - axis.y * s, axis.z * axis.y * t + axis.x * s, axis.z * axis.z * t + c }
    };

    // 将3x3旋转矩阵扩展为4x4矩阵,并设置平移部分为0
    float4x4 rotationMatrix;
    rotationMatrix[0][0] = m[0][0];
    rotationMatrix[0][1] = m[0][1];
    rotationMatrix[0][2] = m[0][2];
    rotationMatrix[0][3] = 0.0f;

    rotationMatrix[1][0] = m[1][0];
    rotationMatrix[1][1] = m[1][1];
    rotationMatrix[1][2] = m[1][2];
    rotationMatrix[1][3] = 0.0f;

    rotationMatrix[2][0] = m[2][0];
    rotationMatrix[2][1] = m[2][1];
    rotationMatrix[2][2] = m[2][2];
    rotationMatrix[2][3] = 0.0f;

    rotationMatrix[3][0] = 0.0f;
    rotationMatrix[3][1] = 0.0f;
    rotationMatrix[3][2] = 0.0f;
    rotationMatrix[3][3] = 1.0f;

    return rotationMatrix;
}

//Gooch光照模型  风格化着色模型  强调冷暖色调  FROM::render4th P146
float3 GoochModel(float3 baseColor, float3 highLightColor, float3 normalWs, float3 lightDirWs, float3 viewDirWs){
    normalWs = normalize(normalWs);
    lightDirWs = normalize(lightDirWs);

    float3 coolColor = float3(0,0,0.55) + 0.25*baseColor;
    float3 warmColor = float3(0.3,0.3,0) + 0.25*baseColor;
                
    float size = clamp(100*(dot(reflect(lightDirWs, normalWs), viewDirWs) - 97), 0, 1);
    float4 halfLambert = dot(normalWs, lightDirWs) * 0.5 + 0.5;
    return  size*highLightColor + (1-size)*(halfLambert*warmColor + (1 - halfLambert)* coolColor);
}

//添加虚拟点光源  _VirtualLightFade越大,衰减越快
float3 CalculatePointVirtualLight(float3 _VirtualLightPos, float3 positionOS, float _VirtualLightFade, float3 _VirtualLightColor)
{
    float virtualLightDis = distance(_VirtualLightPos,positionOS);
    float3 virtualLight = exp(-_VirtualLightFade*virtualLightDis)*_VirtualLightColor;
    //TODO:也许补充方向衰减
    return virtualLight;
}

//切线空间计算视差uv 根据视角方向以深度反追命中点uv
float2 CalculateRealUVAfterDepth(float2 originUV, float3 viewDirTS, float depth)
{
    //计算视角方向和深度方向的cos值
    //一般来讲unity是左手坐标系,但在切线和观察空间较为特殊是右手坐标系,不过这并不影响z轴方向的判断
    float cosTheta = dot(normalize(viewDirTS), float3(0,0,-1));
    //根据深度差算出两点间距离
    float dis = depth / cosTheta;
    //算出应用深度差后对应点位
    float3 originUVPoint = float3(originUV, 0);
    float3 afrerDepthUVPoint = originUVPoint + normalize(viewDirTS) * dis;
    //返回应用深度差后对应UV
    return afrerDepthUVPoint.xy;
}

// https://blog.csdn.net/lafengxiaoyu/article/details/56294678
// n阶贝塞尔曲线
float3 CalculateBezierPoint(float t, float4 CurveControlPoints[50], int ControlPointCount)
{
    int n = ControlPointCount - 1;

    // 对每个分段进行迭代
    for (int r = 1; r <= n; r++)
    {
        for (int i = 0; i <= n - r; i++)
        {
            CurveControlPoints[i] = (1 - t) * CurveControlPoints[i] + t * CurveControlPoints[i + 1];
        }
    }
		        
    return CurveControlPoints[0];
}

//  抖动,用于穿孔或颜色混合 thresholdMatrix是阈值矩阵 _RowAccess是y遮罩 _Transparency是透明度级别 screenPixelPos是屏幕空间像素坐标1920X1080
float4x4 thresholdMatrix =
{
    1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
    13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
    4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
    16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
};
float4x4 _RowAccess =
{
    1,0,0,0,
    0,1,0,0,
    0,0,1,0,
    0,0,0,1
};
float Dither(float2 screenPixelPos, float _Transparency)
{
    float value = _Transparency - thresholdMatrix[fmod(screenPixelPos.x, 4)] * _RowAccess[fmod(screenPixelPos.y, 4)];
    
    if(value > 0)
    {
        return 1.0f;
    }else
    {
        return 0.0f;
    }
}

// 计算直线与平面的交点  pos_Ray射线起点,dir_Ray射线方向,面上一点pos_Plane,面法线方向nDir_Plane
float3 GetPos_PlaneCrossRay(float3 pos_Ray, float3 dir_Ray, float3 pos_Plane, float3 nDir_Plane)
{
    //  参考3D游戏与计算机图形学中的数学方法-直线与平面的交点p60
    //  直线公式为P(t) = pos_Ray + dir_Ray * t;
    //  假设交点为P(x) --- 即 posCross = pos_Ray + dir_Ray * x;
    //  根据平面公式 N·P(x) + D = 0 可知 nDir_Plane · (pos_Ray + dir_Ray * x) + D = 0
    //  可得x = -(nDir_Plane · pos_Ray + D) / (nDir_Plane · dir_Ray) 其中D即为原点到平面的距离
    //  那么可知交点P(x)即可表示为: pos_Ray + dir_Ray * -(nDir_Plane · pos_Ray + D) / (nDir_Plane · dir_Ray)

    //  参考3D游戏与计算机图形学中的数学方法-三维空间中的平面p59
    //  而当已知平面上一点Q时 D = -N · Q = - nDir_Plane · pos_Plane
    //  带入D值到交点P(x)中即可得最终计算式 P(x) = pos_Ray + dir_Ray * dot(nDir_Plane, pos_Plane - pos_Ray) / dot(nDir_Plane, dir_Ray);
    return pos_Ray + dir_Ray * dot(nDir_Plane, pos_Plane - pos_Ray) / dot(nDir_Plane, dir_Ray);
}

//  对应unity的方法TransformWorldToTangent位于core中SpaceTransforms.hlsl 用于把世界空间向量变换到切线空间
//  unity的实现似乎没有考虑不归一化的情况下镜像变换后计算出的方向可能不正确的问题
real3 TransformWorldToTangent_Kerzh(real3 normalWS, real3x3 tangentToWorld, bool doNormalize = true)
{
    //  传入的TBN三个基并不一定两两垂直
    float3 row0 = tangentToWorld[0];  //  T
    float3 row1 = tangentToWorld[1];  //  B
    float3 row2 = tangentToWorld[2];  //  N

    //  这里叉积的结果已经正交,所以涵盖了传入的TBN可能因非均匀拉伸出现的非正交的情况 
    //  叉乘是右手 如col0:四指从B握向N,大拇指的方向
    float3 col0 = cross(row1, row2);  //  B x N 由于原本的TBN并不严格正交 所以这里的结果并不是简单的-T
    float3 col1 = cross(row2, row0);  //  N x T 由于原本的TBN并不严格正交 所以这里的结果并不是简单的-B
    float3 col2 = cross(row0, row1);  //  T x B 由于原本的TBN并不严格正交 所以这里的结果并不是简单的-N
    
    //  叉积计算的是伴随矩的列向量,由于是叉积,即使输入的TBN矩阵不是严格正交的,叉积的结果也会自动正交化
    //  参考3D游戏与计算机图形学中的数学方法-行列式p31
    //  因为矩阵的伴随矩阵就是每个位置换成对应的代数余子式
    //  根据逆矩阵的定义 矩阵的逆 = 矩阵的伴随矩阵 / 矩阵的行列式值
    /*  参考3D游戏与计算机图形学中的数学方法-行列式p32
     *  开始计算逆矩阵:首先看伴随矩阵部分  <注意:伴随矩阵就是逆矩阵的未缩放版本!!!>
     *  注意:伴随矩阵中的每一项为(-1)^(i+j)*det(Mij)
     *      |Tx Bx Nx|              |M11 M21 M31|   |+(ByNz - BzNy) -(BxNz - BzNx) +(BxNy - ByNx)|   |(ByNz - BzNy) (BzNx - BxNz) (BxNy - ByNx)|
     *  M = |Ty By Ny|  M的伴随矩阵 = |M12 M22 M32| = |-(TyNz - TzNy) +(TxNz - TzNx) -(TxNy - TyNx)| = |(TzNy - TyNz) (TxNz - TzNx) (TyNx - TxNy)|
     *      |Tz Bz Nz|              |M13 M23 M33|   |+(TyBz - TzBy) -(TxBz - TzBx) +(TxBy - TyBx)|   |(TyNz - TzNy) (TzBx - TxBz) (TxBy - TyBx)|
     *  计算三个叉乘的结果
     *  col0 = B X N = (ByNz - BzNy, BzNx - BxNz, BxNy - ByNx)
     *  col1 = N X T = (NyTz - NzTy, NzTx - NxTz, NxTy - NyTx)
     *  col2 = T X B = (TyBz - TzBy, TzBx - TxBz, TxBy - TyBx)
     *  观察可见M的伴随矩阵 = 每一行和三个叉乘的结果是一致的
     */
    //  因此使用这三个叉积的向量结果以列构造矩阵,即可获得M的逆转置矩阵(未缩放版本)
    //  构造时是以列的形式构造
    real3x3 matTBN_I_T = real3x3(col0, col1, col2);

    //  即便没有归一化,但已经可以通过矩阵变换达到从世界空间转换到切线空间的目的
    real3 result = mul(matTBN_I_T, normalWS);

    //  dot(row0, col0) = dot(T, B x N) 这是对TBN以行形式组合成的矩阵的行列式值的简便计算
    //  参考3D游戏与计算机图形学中的数学方法-行列式p32
    //  固定一列计算这一列 [每个元素的代数余子式 * 这个元素] 的总和,即是行列式的值
    //  代数余子式本质上就是刨去对应元素所在行列的的行列式值(带符号)
    //  M的行列式值 = Tx(+1)(ByNz - BzNy) + Ty(-1)(BxNz - BzNx) + Tz(+1)(BxNy - ByNx) = Tx(ByNz - BzNy) + Ty(BzNx - BxNz) + Tz(BxNy - ByNx)
    //  dot(row0, col0) = dot(T, B x N) = dot((Tx, Ty, Tz), (ByNz - BzNy, BzNx - BxNz, BxNy - ByNx)) = Tx(ByNz - BzNy) + Ty(BzNx - BxNz) + Tz(BxNy - ByNx)
    //  可见是完全相同的,所以这里点积计算的值即是矩阵M的行列式值
    float determinant = dot(row0, col0);

    //  根据行列式值确定符号,如果行列式为负,表示矩阵包含了镜像变换(即是修改了手性)
    float sgn = determinant < 0.0 ? (-1.0) : 1.0;
    //  处理镜像变换情况 因为一旦变换改变了手性,那么实际的方向会发生翻转
    result = sgn * result;

    //  如果需要归一化
    if (doNormalize)
    {
        return SafeNormalize(result);
    }

    //  消除缩放因子的影响
    return result / determinant;
}

#endif

原作者使用了SRP解决冰块渲染顺序在管线下矛盾的问题,我也还没看完,故相关解释后续向下补充


网站公告

今日签到

点亮在社区的每一天
去签到