在VR游戏《半条命:Alyx》中,有个酒瓶中液体晃动的交互。
这里在patreon上有个实现了液体晃动的工程,作者忘了叫啥了,记得的话补回来。这里简单分析一下代码的意思,之前也看过,只是不太理解旋转部分的代码,现在重新复习一下。
液体的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轴向上摆动,左右摆动。
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" }