在VR游戲《半條命:Alyx》中,有個酒瓶中液體晃動的交互。

圖1
這里在patreon上有個實現了液體晃動的工程,作者忘了叫啥了,記得的話補回來。這里簡單分析一下代碼的意思,之前也看過,只是不太理解旋轉部分的代碼,現在重新復習一下。

圖2
液體的shader
Shader "Unlit/SpecialFX/Liquid" { Properties { _Tint ("Tint", Color) = (1,1,1,1) _MainTex ("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 _TopColor ("Top Color", Color) = (1,1,1,1) _FoamColor ("Foam Line Color", Color) = (1,1,1,1) _Rim ("Foam Line Width", Range(0,0.1)) = 0.0 _RimColor ("Rim Color", Color) = (1,1,1,1) _RimPower ("Rim Power", Range(0,10)) = 0.0 } SubShader { Tags {"Queue"="Geometry" "DisableBatching" = "True" } Pass { 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 _MainTex; float4 _MainTex_ST; float _FillAmount, _WobbleX, _WobbleZ; float4 _TopColor, _RimColor, _FoamColor, _Tint; float _Rim, _RimPower; 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); //構造一個2x2的旋轉矩陣 return float4(vertex.yz , mul(m, vertex.xz)).xzyw ; //mul(m,vertex.xz))是繞y軸旋轉degrees的角度,這里傳入的是360度,頂點還是保持在原來的位置 //return float4(vertex.yz , vertex.xz).xzyw ; //這里為啥要這么寫呢?其實return得到的值表示成float4(vertex.y,vertex.x,vertex.z,vertex.z),由於只要float3,w分量可以忽略。這里其實意思是頂點的x與y互換了。參考圖3
//原頂點worldPos的值加上這里return的值(worldPosX),頂點就可以在左右的方向擺動,也就是在XY平面旋轉,
//之后的worldPosZ等於float3(vertex.x,vertex.z,vertex.y),就是頂點的z和y互換,在YZ平面旋轉,即液體前后擺動。
//
//通過這種方式,不管你玻璃杯怎么旋轉,左右晃動玻璃杯,液體也會左右擺動;前后晃動的話,液體就會前后擺動,就跟現實中晃動杯子里的水一樣。
} v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); // get world position of the vertex float3 worldPos = mul (unity_ObjectToWorld, v.vertex.xyz); // rotate it around XY float3 worldPosX= RotateAroundYInDegrees(float4(worldPos,0),360); // rotate around ZY float3 worldPosZ = float3 (worldPosX.y, worldPosX.z, worldPosX.x); // combine rotations with worldPos, based on sine wave from script float3 worldPosAdjusted = worldPos + (worldPosX * _WobbleX+worldPosZ * _WobbleZ); //液體原頂點加上在X軸和Z軸上的擺動,這里好奇既然是旋轉,為啥液體不是像平常的3d物體那樣整個旋轉呢。 // how high up the liquid is //其實並沒有改變頂點的位置,而是通過計算存儲一個值,然后拿到片元中根據y軸的值去剔除,得到最終的顏色值。 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(_MainTex, i.uv) * _Tint; // apply fog UNITY_APPLY_FOG(i.fogCoord, col); // rim light //在液體表面有層光暈,菲涅爾。 float dotProduct = 1 - pow(dot(i.normal, i.viewDir), _RimPower); float4 RimResult = smoothstep(0.5, 1.0, dotProduct); RimResult *= _RimColor; // foam edge //在液體水面上的泡沫 float4 foam = ( step(i.fillEdge, 0.5) - step(i.fillEdge, (0.5 - _Rim))) ; float4 foamColored = foam * (_FoamColor * 0.9); // 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 = _TopColor * (foam + result); //VFACE returns positive for front facing, negative for backfacing return facing > 0 ? finalResult: topColor; } ENDCG } } }

圖3 頂點的x和y互換后,變成右邊的圖,即頂點是往右圖的旋轉方向旋轉了,液體在X軸向上擺動,左右擺動。
同理得worldPosZ是頂點的z和y互換,即液體在Z軸方向上擺動.
C#腳本把值傳入液體的shader里
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的wobbleAmountX是控制液體左右擺的幅度,同理,wobbleAmountZ是前后擺動的幅度。這里velocity的值是只有在物體移動時才會不等於0,angularVelocity在只有旋轉時才會不等於0,即只有移動或旋轉時才會晃動液體,移動的增量越大,液體擺動得越大。
最后把玻璃瓶的shader代碼貼上
Shader "Toon/Lit Specular Alpha" { Properties{ _Color("Main Color", Color) = (1,1,1,1) _SColor("Specular Color", Color) = (1,1,1,1) _MainTex("Base (RGB)", 2D) = "white" {} _Ramp("Toon Ramp (RGB)", 2D) = "gray" {} _RampS("Specular Ramp (RGB)", 2D) = "gray" {} // specular ramp, cutoff point _SpecSize("Specular Size", Range(0.65,0.999)) = 0.9 // specular size _SpecOffset("Specular Offset", Range(0.5,1)) = 0.5 // specular offset of the spec Ramp _TColor("Gradient Overlay Top Color", Color) = (1,1,1,1) _BottomColor("Gradient Overlay Bottom Color", Color) = (0.23,0,0.95,1) _Offset("Gradient Offset", Range(-4,4)) = 3.2 [Toggle(RIM)] _RIM("Fresnel Rim?", Float) = 0 _RimColor("Fresnel Rim Color", Color) = (0.49,0.94,0.64,1) [Toggle(FADE)] _FADE("Fade specular to bottom?", Float) = 0 _TopBottomOffset("Specular Fade Offset", Range(-4,4)) = 3.2 } SubShader{ Tags{ "Queue" = "Transparent"} LOD 200 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma surface surf ToonRamp vertex:vert keepalpha #pragma shader_feature FADE // fade toggle #pragma shader_feature RIM // rim fresnel toggle sampler2D _Ramp; // custom lighting function that uses a texture ramp based // on angle between light direction and normal #pragma lighting ToonRamp exclude_path:prepass inline half4 LightingToonRamp(SurfaceOutput s, half3 lightDir, half atten) { #ifndef USING_DIRECTIONAL_LIGHT lightDir = normalize(lightDir); #endif half d = dot(s.Normal, lightDir)*0.5 + 0.5; half3 ramp = tex2D(_Ramp, float2(d,d)).rgb; half4 c; c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2); c.a = s.Alpha; return c; } sampler2D _MainTex; float4 _Color; float4 _SColor; // specular color sampler2D _RampS; // specular ramp float _SpecSize; // specular size float _SpecOffset; // offset specular ramp float4 _TColor; // top gradient color float4 _BottomColor;// bottom gradient color float _TopBottomOffset; // gradient bottom offset float _Offset; // specular fade offset float4 _RimColor; // fresnel rim color struct Input { float2 uv_MainTex : TEXCOORD0; float3 lightDir; float3 worldPos; // world position float3 viewDir; // view direction from camera }; void vert(inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input, o); o.lightDir = WorldSpaceLightDir(v.vertex); // get the worldspace lighting direction } void surf(Input IN, inout SurfaceOutput o) { float3 localPos = (IN.worldPos - mul(unity_ObjectToWorld, float4(0, 0, 0, 1)).xyz);// local position of the object, with an offset half4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; half d = dot(o.Normal, IN.lightDir)*0.5 + _SpecOffset; // basing on normal and light direction half3 rampS = tex2D(_RampS, float2(d, d)).rgb; // specular ramp float rim = 1 - saturate(dot(IN.viewDir, o.Normal)); // calculate fresnel rim #if RIM o.Emission = _RimColor.rgb * pow(rim, 1.5); // fresnel rim #endif float specular= (step(_SpecSize, rampS.r)) * rampS * d * _SColor.a; o.Albedo = specular* _SColor; // specular o.Alpha = c.a + specular; #if FADE float specular2 = (step(_SpecSize, rampS.r)) * rampS * d* saturate(localPos.y + _TopBottomOffset)* _SColor.a; o.Albedo = specular2* _SColor; // fade specular to bottom o.Alpha = c.a + specular2; #endif o.Albedo += c.rgb*lerp(_BottomColor, _TColor, saturate(localPos.y + _Offset)) * 1.1; // multiply color by gradient lerp } ENDCG } Fallback "Diffuse" }
