状態異常「凍結」みたいなものが必要になったので、実装してみました。
ぶっちゃけ、表現としては大きめの半透明の氷のオブジェクトの中に入れてしまえば簡単だと思います。
しかしこの表現は凍らせたいオブジェクトの大きさや形状が様々な場合、無理な引き伸ばしが必要だったり、体の一部がはみ出たりすることがあります。
また、凍らせる対象が画面にたくさんいる時は表示するオブジェクトを増やすコストが気になりますし、むやみに半透明オブジェクトを表示させるのも考えものです。
そういう懸念への対策として作ってみたシェーダーでした。
シェーダー全文
----------------------------------------
Freeze.shader(折りたたみ)
------------------------------------------
Shader "Custom/Freeze" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_ShadeColor ("Shade Color", Color) = (0.5,0.5,0.5)
_SpecularPower ("Specular Power", Range(0, 30)) = 1
_SpecularColor ("Specular Color", Color) = (0.5,0.5,0.5)
_FreezeRate ("Freeze Rate", Range(0.0, 1.0)) = 0
_FreezeRateMax ("Freeze Rate Max", Range(0.0, 1.0)) = 0.7
_FreezeBorder ("Freeze Border", Range(0.0, 1.0)) = 0.05
_FreezeBorderColor ("Freeze Border Color", Color) = (0.5,0.5,0.5)
_IceTex ("Ice Texture", 2D) = "black" {}
_DisolveTex ("DisolveTex", 2D) = "white" {}
[Normal] _IceNormalMap ("Ice Normal Map", 2D) = "bump" {}
_IceNormalMapScale ("Ice Normal Map Scale", Range(0, 3)) = 0.5
_IceSpecularPower ("Ice Specular Power", Range(0, 30)) = 1
_IceSpecularColor ("Ice Specular Color", Color) = (0.5,0.5,0.5)
_RimLightBorder ("RimLight Border", Range(0.0, 1.0)) = 0.5
_RimLightColor ("Rim Light Color", Color) = (0.5,0.5,0.5)
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 lightDir : TEXCOORD2;
float3 viewDir : TEXCOORD3;
float2 uvIce : TEXCOORD4;
half4 tangent : TEXCOORD5;
half3 binormal : TEXCOORD6;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float3 _ShadeColor;
float _SpecularPower;
float3 _SpecularColor;
float _FreezeRate;
float _FreezeRateMax;
float _FreezeBorder;
float3 _FreezeBorderColor;
sampler2D _IceTex;
float4 _IceTex_ST;
sampler2D _DisolveTex;
sampler2D _IceNormalMap;
float _IceNormalMapScale;
float _IceSpecularPower;
float3 _IceSpecularColor;
float _RimLightBorder;
float3 _RimLightColor;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.uvIce = TRANSFORM_TEX(v.uv, _IceTex);
o.normal = UnityObjectToWorldNormal(v.normal);
o.tangent = mul(unity_ObjectToWorld, v.tangent.xyz);
// binormalはtangentのwとunity_WorldTransformParams.wを掛ける(Unityの決まり)
o.binormal = normalize(cross(v.normal.xyz, v.tangent.xyz) * v.tangent.w * unity_WorldTransformParams.w);
o.binormal = mul(unity_ObjectToWorld, o.binormal);
o.lightDir = normalize(WorldSpaceLightDir(v.vertex));
o.viewDir = normalize(WorldSpaceViewDir(v.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv);
half3 halfDir = normalize(i.lightDir + i.viewDir);
// 拡散反射
float diffuse = max(dot(i.normal, i.lightDir), 0);
float3 shade = lerp(_ShadeColor, 1, diffuse);
col.rgb *= shade;
// 鏡面反射
float specularRate = pow(max(0, dot(i.normal, halfDir)), _SpecularPower);
float3 specular = specularRate * _SpecularColor;
col.rgb += specular;
fixed4 disolve = tex2D(_DisolveTex, i.uvIce);
float freezeRate = lerp(0, _FreezeRateMax, step(disolve.r, _FreezeRate));
float freezeBorderRate = 1 - smoothstep(0, _FreezeBorder, disolve.r - _FreezeRate);
freezeBorderRate *= step(freezeRate, 0);
col.rgb += _FreezeBorderColor * freezeBorderRate;
// 氷のテクスチャをブレンド
col = lerp(col, tex2D(_IceTex, i.uvIce), freezeRate);
// 氷のノーマルマップを反映した法線を計算
half3 localNormal = UnpackNormalWithScale(tex2D(_IceNormalMap, i.uvIce), _IceNormalMapScale);
// 接空間のベクトルをワールド空間に変換
float3 normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;
i.normal = lerp(i.normal, normalize(normal), freezeRate);
// 氷の鏡面反射
specularRate = pow(max(0, dot(i.normal, halfDir)), _IceSpecularPower);
specular = specularRate * _IceSpecularColor;
col.rgb += lerp(0, specular, freezeRate);
// リムライト
float rimLightRate = 1 - smoothstep(0, _RimLightBorder, dot(i.normal, i.viewDir));
rimLightRate = lerp(0, rimLightRate, freezeRate);
col.rgb += lerp(0, _RimLightColor, rimLightRate);
// 氷の拡散反射
float iceShade = lerp(shade, 1, 0.5);
col.rgb *= lerp(1, iceShade, freezeRate);
return col;
}
ENDCG
}
}
FallBack "Diffuse"
}
表現を分解
一つ一つはよくあるシェーダー表現を使っています。
氷のテクスチャをブレンド
// 氷のテクスチャをブレンド
col = lerp(col, tex2D(_IceTex, i.uvIce), freezeRate);
メインテクスチャに対して氷の色や模様を持った氷テクスチャをブレンドしています。
freezeRate
は凍り具合を表す変数。
今回使ったテクスチャは下記のものです。
この色味や模様のディティールの多さの適切なバランスを取るのが意外と難しかったです。
青すぎても白すぎても氷っぽく見えないし、ディティールも少なすぎるとただ青白いだけ、多すぎると野暮ったい。
ちなみに、スクショのものではブレンド率(freezeRate
)の上限を0.7
としていて、元の色がちょっと透けてる感じにしました。
モデルのディティールがテクスチャではなくポリゴンでそれなりに表現されていれば、完全に氷のテクスチャに置き換えてみてもいいですね。
鏡面反射
// 氷の鏡面反射
specularRate = pow(max(0, dot(i.normal, halfDir)), _IceSpecularPower);
specular = specularRate * _IceSpecularColor;
col.rgb += lerp(0, specular, freezeRate);
凍る前の表示とは別に鏡面反射の強度と色を用意して、加算しています。
強度強め、色は白めだといい感じです。
氷のノーマルマップを反映
// 氷のノーマルマップを反映した法線を計算
half3 localNormal = UnpackNormalWithScale(tex2D(_IceNormalMap, i.uvIce), _IceNormalMapScale);
// 接空間のベクトルをワールド空間に変換
float3 normal = i.tangent * localNormal.x + i.binormal * localNormal.y + i.normal * localNormal.z;
i.normal = lerp(i.normal, normalize(normal), freezeRate);
氷の鏡面反射計算の前に氷の質感としてノーマルマップを反映する処理を追加しています。
鏡面反射の反映角度が程よく分散して氷っぽさがここでグッと上がりました。
今回使ったノーマルマップは下記のものです。
リムライト
// リムライト
float rimLightRate = 1 - smoothstep(0, _RimLightBorder, dot(i.normal, i.viewDir));
rimLightRate = lerp(0, rimLightRate, freezeRate);
col.rgb += lerp(0, _RimLightColor, rimLightRate);
_RimLightBorder
をsmoothstep
に使ってリムライトの影響幅を調整しています。
フチが明るくなることで氷の厚みが感じられるようになりました。
氷の表現としてはこれで完成です。
ディゾルブ
fixed4 disolve = tex2D(_DisolveTex, i.uvIce);
float freezeRate = lerp(0, _FreezeRateMax, step(disolve.r, _FreezeRate));
この処理は 氷のテクスチャのブレンド の工程の前に追加しているものです。
冒頭の動画のように徐々に凍る演出を作ったり、逆に解凍される演出を作ったりすることを想定して、凍り具合_FreezeRate
の反映範囲をディゾルブでまばらにしています。
今回使ったノイズテクスチャは下記のものです。
ディゾルブの境界
float freezeBorderRate = 1 - smoothstep(0, _FreezeBorder, disolve.r - _FreezeRate);
freezeBorderRate *= step(freezeRate, 0);
col.rgb += _FreezeBorderColor * freezeBorderRate;
凍っていない箇所からディゾルブの境界に近づくほど、反映度が強くなる加算カラーを当てています。
_FreezeBorder
をsmoothstep
に使って、加算カラーの影響範囲を調整。
step
関数は既に凍っている箇所に加算カラーが反映されないよう切り分けるための計算です。
これで自然と凍結の影響が進行して見えるようになりました。
所感
今回のネタはAssetStoreで見かけた上記の動画43秒あたりの「Ice Soldier」を3D落とし込んだものでした。
まだまだシェーダーを使った表現の引き出しが少ないので、ゲームに汎用的に使える演出の引き出しを増やすために、こういう感じで模写するのは地力が付きそうだなと思いました。
次は燃える表現をやってみたいです。
頂点シェーダーを使った表現の引き出しも増やしたいけど何か良いネタないかな・・・。