【转载】Liquid Bottle – 液体瓶制作

985 阅读4分钟

原文链接

正文

这个巧妙的做法是从网上学来的,不过这位大佬并没有详细分析实现方式,只是写了一下大概的思路,看他的 twitter 里很多人都在追问详细做法思路。我这儿就厚着脸皮拿来详细分析一下吧。

这玩意儿的实现方式很讨巧,几个功能的实现是依赖同一个算法的,不知道从哪儿讲起比较合适。所以就从实现的流程一步步讲:

准备模型

准备好一个胶囊体来当作药水瓶。

开始写脚本

核心算法:通过旋转 posWorld 获得 XZ 轴偏移,配合脚本输入 Sin 曲线制作出自然晃动的瓶装液体效果,并且可调整液体容量。

首先计算一个物体位移和旋转时的加速度值,用来作为液体晃动的参数。创建一个 C# 脚本,把对象的位移和旋转的 delta 值输入 sin 函数,获得一个在 -1+1 之间的平滑曲线。

我们主要关注物体的 X 轴和 Z 轴的运动影响,所以就把 _WobbleX_WobbleZ 这两个值输入进后面要写的 shader 里。

C# 代码

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

public class Wobble : MonoBehaviour
{
    Renderer rend;
    Vector3 lastPos;
    Vector3 velocity;
    Vector3 lastRot;  
    Vector3 angularVelocity;
    public float MaxWobble = 0.03f;
    public float WobbleSpeed = 1f;
    public float Recovery = 1f;
    float wobbleAmountX;
    float wobbleAmountZ;
    float wobbleAmountToAddX;
    float wobbleAmountToAddZ;
    float pulse;
    float time = 0.5f;

    // Use this for initialization
    void Start()
    {
        rend = GetComponent<Renderer>();
    }
    private void Update()
    {
        time += Time.deltaTime;
        // decrease wobble over time
        wobbleAmountToAddX = Mathf.Lerp(wobbleAmountToAddX, 0, Time.deltaTime * (Recovery));
        wobbleAmountToAddZ = Mathf.Lerp(wobbleAmountToAddZ, 0, Time.deltaTime * (Recovery));

        // make a sine wave of the decreasing wobble
        pulse = 2 * Mathf.PI * WobbleSpeed;
        wobbleAmountX = wobbleAmountToAddX * Mathf.Sin(pulse * time);
        wobbleAmountZ = wobbleAmountToAddZ * Mathf.Sin(pulse * time);

        // send it to the shader
        rend.material.SetFloat("_WobbleX", wobbleAmountX);
        rend.material.SetFloat("_WobbleZ", wobbleAmountZ);

        // velocity
        velocity = (lastPos - transform.position) / Time.deltaTime;
        angularVelocity = transform.rotation.eulerAngles - lastRot;

        // add clamped velocity to wobble
        wobbleAmountToAddX += Mathf.Clamp((velocity.x + (angularVelocity.z * 0.2f)) * MaxWobble, -MaxWobble, MaxWobble);
        wobbleAmountToAddZ += Mathf.Clamp((velocity.z + (angularVelocity.x * 0.2f)) * MaxWobble, -MaxWobble, MaxWobble);

        // keep last position
        lastPos = transform.position;
        lastRot = transform.rotation.eulerAngles;
    }
}

开始写 shader

第一个 Pass —— 水面晃动

vert 阶段,用 RotateAroundYInDegrees 函数,把物体按照屏幕坐标旋转,让原来的 +Y 轴分别转到 +X 轴和 +Z 轴,

@ShaderJoy:视频教程里使用的是对 Object 空间的顶点坐标分别绕 X 轴和 Z 轴旋转

再分别乘上之前的 _WobbleX_WobbleZ。这样就相当于获得了 X 轴和 Z 轴方向的梯度值。如下图:

这个沿着 X 轴和 Z 轴波形曲线的梯度值就是用来制作水面起伏效果的关键。

把梯度值输入到 fragment 里先看看效果:

已经可以看到晃动的感觉了。

step 的方式获得液体的像素位置。如下图:

通过 AlphaToMask On 把黑色的部分当作 alpha clip 剔除了。水面的像素其实是通过 VFace 的方式,把背面的的像素取出来当作水面。

VFace.png

水面高度变化是通过之前在 vert 阶段计算水面晃动的时候,把 worldPos.y 再额外加上一个高度系数 _FillAmount

注意: 这个系数是调整 Y 轴的高度变化的,要根据每个物体的实际尺寸来调整水面最高点和最低点。

因为是根据 Y 轴高度算出来的水面,所以不管物体是什么形状,如何旋转角度,都不会影响水面高度,所以看起来很自然。

到此为止 核心功能 都做完了,之后可以给水面和水体加上颜色,然后增加一个 Pass 做玻璃瓶子。

第二个 Pass —— 玻璃瓶子

玻璃瓶子的做法就是 Normal 往外挤出一点距离,渲染模式 Alpha blend,加上 RimSpecular

LiquidBottle04.gif

完整 shader 代码

Shader "WalkingFat/LiquidBottle/SparkleLiquid01"
{
    Properties
    {
        _LiquidColor ("LiquidColor", Color) = (1,1,1,1)
        _NoiseTex ("Noise Texture", 2D) = "white" {}
        _FillAmount ("Fill Amount", Range(-10,10)) = 0.0
        [HideInInspector] _WobbleX ("WobbleX", Range(-1,1)) = 0.0
        [HideInInspector] _WobbleZ ("WobbleZ", Range(-1,1)) = 0.0
        _LiquidTopColor ("Liquid Top Color", Color) = (1,1,1,1)
        _LiquidFoamColor ("Liquid Foam Color", Color) = (1,1,1,1)
        _FoamLineWidth ("Liquid Foam Line Width", Range(0,0.1)) = 0.0    
        _LiquidRimColor ("Liquid Rim Color", Color) = (1,1,1,1)
        _LiquidRimPower ("Liquid Rim Power", Range(0,10)) = 0.0
        _LiquidRimIntensity ("Liquid Rim Intensity", Range(0.0,3.0)) = 1.0
        
        _BottleColor ("Bottle Color", Color) = (0.5,0.5,0.5,1)
        _BottleThickness ("Bottle Thickness", Range(0,1)) = 0.1
          
        _BottleRimColor ("Bottle Rim Color", Color) = (1,1,1,1)
        _BottleRimPower ("Bottle Rim Power", Range(0,10)) = 0.0
        _BottleRimIntensity ("Bottle Rim Intensity", Range(0.0,3.0)) = 1.0
        
        _BottleSpecular ("BottleSpecular", Range(0,1)) = 0.5
        _BottleGloss ("BottleGloss", Range(0,1) ) = 0.5
    }
 
    SubShader
    {
        Tags
        { 
            "DisableBatching" = "True" 
            "Queue"="Transparent"
            "RenderType"="Transparent"
        }
        
        //1st pass draw liquid
        Pass
        {
            Tags {"RenderType" = "Opaque" "Queue" = "Geometry"}
            
            Zwrite On
            Cull Off // we want the front and back faces
            AlphaToMask On // transparency

            CGPROGRAM


            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL; 
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float3 viewDir : COLOR;
                float3 normal : COLOR2;    
                float fillEdge : TEXCOORD2;
            };

            sampler2D _NoiseTex;
            float4 _NoiseTex_ST;
            float _FillAmount, _WobbleX, _WobbleZ;
            float4 _LiquidTopColor, _LiquidRimColor, _LiquidFoamColor, _LiquidColor;
            float _FoamLineWidth, _LiquidRimPower, _LiquidRimIntensity;

            float4 RotateAroundYInDegrees (float4 vertex, float degrees)
            {
                float alpha = degrees * UNITY_PI / 180;
                float sina, cosa;
                sincos(alpha, sina, cosa);
                float2x2 m = float2x2(cosa, sina, -sina, cosa);
                
                ///@ShaderJoy (vertex.y, vertex.z, vertex.rx, vertex.rz)
                ///            x         y         z          w
                ///            xyzw -> xzyw
                ///           (vertex.y, vertex.rx, vertex.z, vertex.rz)
                ///            x         y         z          w
                return float4(vertex.yz , mul(m, vertex.xz)).xzyw;            
            }

            v2f vert (appdata v)
            {
                v2f o;

                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _NoiseTex);
                UNITY_TRANSFER_FOG(o,o.vertex);  
                
                // get world position of the vertex
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex.xyz);  
                
                // rotate it around XY
                /// @ShaderJoy
                /// vertex.y, vertex.rx, vertex.z
                float3 worldPosX = RotateAroundYInDegrees(float4(worldPos, 0), 360).xyz;
                // rotate around XZ
                /// @ShaderJoy
                /// vertex.rx, vertex.z, vertex.y
                //float3 worldPosZ = float3 (worldPosX.y, worldPosX.z, worldPosX.x);   
                float3 worldPosZ = worldPosX.yzx; ///< @ShaderJoy
                
                // combine rotations with worldPos, based on sine wave from script
                float3 worldPosAdjusted = worldPos + (worldPosX * _WobbleX) + (worldPosZ * _WobbleZ);
                // how high up the liquid is
                o.fillEdge =  worldPosAdjusted.y + _FillAmount;

                o.viewDir = normalize(ObjSpaceViewDir(v.vertex));
                o.normal = v.normal;
                return o;
            }

            fixed4 frag (v2f i, fixed facing : VFACE) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_NoiseTex, i.uv) * _LiquidColor;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);

                // rim light
                float dotProduct = 1 - pow(dot(i.normal, i.viewDir), _LiquidRimPower);
                float4 RimResult = _LiquidRimColor * smoothstep(0.5, 1.0, dotProduct) * _LiquidRimIntensity;

                // foam edge 
                /// @ShaderJoy (0.5 - _FoamLineWidth) < fillEdge < 0.5
                float4 foam = step(i.fillEdge, 0.5) - step(i.fillEdge, (0.5 - _FoamLineWidth));
                float4 foamColored = foam * _LiquidFoamColor;
                
                // rest of the liquid
                float4 result = step(i.fillEdge, 0.5) - foam;
                float4 resultColored = result * col;
                
                // both together, with the texture
                float4 finalResult = resultColored + foamColored;               
                finalResult.rgb += RimResult;

                // color of backfaces/ top
                float4 topColor = _LiquidTopColor * (foam + result);
                
                //VFACE returns positive for front facing, negative for backfacing
                return facing > 0 ? finalResult : topColor;
                   
            }
            ENDCG
        }
        
        // 2nd pass draw glass bottle
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"
 
            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL; 
            };
     
            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 viewDir : COLOR;
                float3 normal : COLOR2;
                float2 uv : TEXCOORD0;
                float3 lightDir : TEXCOORD1;
                float3 normalDir : TEXCOORD2;
                float3 viewDirWorld : TEXCOORD3;
                UNITY_FOG_COORDS(3)
            };
     
            float4 _BottleColor, _BottleRimColor;
            float _BottleThickness, _BottleRim, _BottleRimPower, _BottleRimIntensity;
            float _BottleSpecular, _BottleGloss;
     
            v2f vert (appdata v)
            {
                v2f o;
                v.vertex.xyz += _BottleThickness * v.normal;
                o.vertex = UnityObjectToClipPos(v.vertex);

                o.viewDir = normalize(ObjSpaceViewDir(v.vertex));
                o.normal = v.normal;
                
                float4 posWorld = mul (unity_ObjectToWorld, v.vertex);
                o.viewDirWorld = normalize(_WorldSpaceCameraPos.xyz - posWorld.xyz);
                o.normalDir = UnityObjectToWorldNormal (v.normal);
                o.lightDir = normalize(_WorldSpaceLightPos0.xyz);

                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }
               
            fixed4 frag (v2f i, fixed facing : VFACE) : SV_Target
            {
                // specular
                i.normalDir = normalize(i.normalDir);
                float specularPow = exp2 ((1 - _BottleGloss) * 10.0 + 1.0);
                fixed4 specularColor = fixed4 (_BottleSpecular, _BottleSpecular, _BottleSpecular, _BottleSpecular);
               
                float3 halfVector = normalize (i.lightDir + i.viewDirWorld);
                fixed4 specularCol = pow (max (0,dot (halfVector, i.normalDir)), specularPow) * specularColor;
                
                // rim light
                float dotProduct = 1 - pow(dot(i.normal, i.viewDir), _BottleRimPower);
                fixed4 RimCol = _BottleRimColor * smoothstep(0.5, 1.0, dotProduct) * _BottleRimIntensity;
                
                fixed4 finalCol = RimCol + _BottleColor + specularCol;
                
                UNITY_APPLY_FOG(i.fogCoord, col);

                return finalCol;
            }
            ENDCG
        }
    }
    Fallback "VertexLit"
}