ディゾルブ、というのは「溶ける」とかの意味があるようです。
なので表面が溶けているような、燃え落ちているようなそんな演出をディゾルブと呼びます。
今回は簡単のため、ライティングやシャドウについては考えていません。
表面をとりあえず上記イメージになるように実装してみました。
今回のデモはGithubで公開しています。
考え方
実はディゾルブ自体の仕組みは全然むずかしくありません。
まず、以下のようなPhotoshopなどでよく見る雲模様のテクスチャを用意します。
白と黒が入り混じった雲のような模様です。
このテクスチャを用いて、各ピクセルの輝度を求めます。(HLSLでは Luminance
関数を利用)
そしてその輝度値が、閾値を超えていた場合に discard
する、ただそれだけです。
つまり、この閾値を時間経過によって変えていってやれば最初のイメージのように、じわーっと消えていく演出ができあがる、というわけです。
コード断片
まずは輝度値を計算しているところ。
fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
if (_CutOff > a) {
discard;
}
こんな感じです。
ここで出てくる _CutOff
が閾値を表します。
これをスクリプトなどから操作してやれば目的の処理が完成します。
スクリプト部分はこんな感じ(あくまでデモ用です)
using UnityEngine;
using System.Collections;
[RequireComponent(typeof(Renderer))]
public class Dissolver : MonoBehaviour {
Material m_Material;
YieldInstruction m_Instruction = new WaitForEndOfFrame();
void Start () {
m_Material = GetComponent<Renderer>().material;
StartCoroutine("Animate");
}
IEnumerator Animate() {
float time = 0;
float duration = 5f;
int dir = 1;
while (true) {
yield return m_Instruction;
time += Time.deltaTime * dir;
var t = time / duration;
if (t > 1f) {
dir = -1;
}
else if (t < 0) {
dir = 1;
}
m_Material.SetFloat("_CutOff", t);
}
}
}
おまけ
さて、上記だけでもそれなりにじわーっと消えていく演出ができるんですが、ちょっとしたおまけとして、消えていく縁が少しにじむような感じのものを追加してみます。
(最初のアニメーションGifはそれを適用したもの)
まずコード断片だけを示すと以下になります。
fixed4 frag (v2f i) : SV_Target {
fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
fixed b = smoothstep(_CutOff - _Width, _CutOff, a) - smoothstep(_CutOff, _CutOff + _Width, a);
return fixed4(0.0, 0.0, b, 1.0);
}
ポイントは以下です。
fixed b = smoothstep(_CutOff - _Width, _CutOff, a) - smoothstep(_CutOff, _CutOff + _Width, a);
smoothstepを用いることで、対象の値をスムーズにブレンドすることができます。
定義
ret smoothstep(min, max, x)
上記サイトから説明を引用すると、
x < min の場合は 0、x > max の場合は 1、それ以外の場合で、x が [min, max] の範囲内であれば 0 と 1 の間の値を返します。
それを踏まえて、最初の計算を見てみると、
smoothstep(_CutOff - _Width, _CutOff, a)
となっています。
つまり、 min = _CutOff - _Width
、 max = _CutOff
となります。
そして評価される値が先ほど計算した輝度値( = a
)です。
日本語で書くと、 閾値から幅を引いた値を最小とし、そこから閾値までの間で輝度値を元に補間係数を算出する となります。
具体例を出してみると、閾値を 0.5
、幅を 0.1
とした場合。
仮に輝度値が 0.45
だったら 0
となります。( 0.45 < 0.5
)
今度は 0.55
とした場合は 1
となります。( 0.55 > 0.5
)
そしてもうひとつの式を見てみると、 _Width
分だけmaxがずれているのが分かります。
最終的にその結果を減算しています。つまりなにをしているかというと、閾値を超えた場合の 1
を減算することになり、結果として 輝度が閾値内にとどまっている場合に限り値が0以上になるようにしている というわけです。
なので縁に近い(閾値に近い)場所以外は常に 0
となり、縁に近い場合だけ色が出現する、つまりは縁に色がつく、というわけなのです。
あとは縁の色が 0
以外の場合に、最初にレンダリングしたものに色を加算合成してやれば冒頭のイメージができあがります。
ということで、最終的なシェーダコードを載せておきます。
(最終的なサンプルでは、幅の違う縁の処理を2回実行して合成しています)
シェーダコード全文
Shader "Custom/Dissolve" {
Properties {
_Color ("Main Color", Color) = (.5,.5,.5,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_DissolveTex ("Desolve (RGB)", 2D) = "white" {}
_CutOff("Cut off", Range(0.0, 1.0)) = 0.0
_Width("Width", Float) = 0.01
}
SubShader {
Tags { "RenderType"="Opaque" "Queue"="Transparent" }
Pass {
Name "BASE"
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DissolveTex;
float4 _DissolveTex_ST;
float4 _Color;
float _CutOff;
float _Width;
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float2 texcoord : TEXCOORD0;
float2 dissolvecoord : TEXCOORD1;
};
v2f vert (appdata v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = _Color * tex2D(_MainTex, i.texcoord);
fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
if (_CutOff > a) {
discard;
}
return col;
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" }
Name "Add"
Cull Off
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DissolveTex;
float4 _DissolveTex_ST;
float4 _Color;
float _CutOff;
float _Width;
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float2 dissolvecoord : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
fixed b = smoothstep(_CutOff - _Width, _CutOff, a) - smoothstep(_CutOff, _CutOff + _Width, a);
return fixed4(0.0, 0.0, b, 1.0);
}
ENDCG
}
Pass {
Tags { "LightMode" = "ForwardBase" }
Name "Add"
Cull Off
Blend One One
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _DissolveTex;
float4 _DissolveTex_ST;
float4 _Color;
float _CutOff;
float _Width;
struct appdata {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 pos : SV_POSITION;
float2 dissolvecoord : TEXCOORD0;
};
v2f vert (appdata v)
{
v2f o;
o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
fixed w = _Width * 1.5;
fixed b = smoothstep(_CutOff - w, _CutOff, a) - smoothstep(_CutOff, _CutOff + w, a);
return fixed4(0.0, b, 0.0, 1.0);
}
ENDCG
}
}
Fallback "VertexLit"
}