読者です 読者をやめる 読者になる 読者になる

UnityでOIT(Order Independent Tranceparency)

 通常半透明オブジェクトをレンダリングする際には、ソート(奥から手前に向かって)が必要になります。しかし、Unityではオブジェクト単位でのソートはしますが、ポリゴン単位でのソートは行いません。(Unityに限った話ではありませんが)

 従って、正しく半透明オブジェクトを描画するためには、細かくオブジェクトを分割するか(ポリゴン単位まで)あるいは描画順に関係ない描画方法を用いるしかありません。

 ここでは後者の、描画順に関係のない半透明オブジェクトの描画方法(OIT:Order Independent Tranceparency)を紹介します。参考にしたのはここ
Weighted Blended Order-Independent Transparencyについて | shikihuiku
です。

 上述の通り、通常半透明オブジェクトを正しく描画するためには、奥のオブジェクトから描画する必要があります。ある半透明オブジェクトのピクセルの描画色を決めるとき、その奥にあるピクセルの色情報が必要となるためです。色 = これから描画する色 * α + 背景色 * (1-α)という計算式を用いるからです(surface shaderの#pragmaにalphaを指定している場合)。

 オブジェクト単位のソートで正しくなる場合は良いのですが、一つのオブジェクト(メッシュ)内に複数のポリゴンがある場合でもポリゴン単位でのソートは行われないため、見る方向によって結果が変わったりしてしまいます。またポリゴンが交差している場合はポリゴン単位のソートでも十分ではなく、ピクセル単位のソートが必要になります。

 そこで、ピクセルの描画順に関わらず正しく半透明オブジェクトを描画出来る方法が考案されてたみたいです。参考のリンク(の先の論文)では、ピクセルの描画順に関わらず(つまり描画順に独立=OrderIndenpendent)正しく(といっても、上記ソートして後ろから描画する場合の近似)半透明オブジェクトを描画する方法が紹介されています。Unityでそのアルゴリズムを実装する方法を示します。

 論文にもある通り,上記の後ろから前に描画する(sorted OVER)手法を"近似"で求めます。
(考え方としては奥に数回描画される半透明ピクセル全体の合計値をとり平均化したうえで、それを不透明オブジェクトのみのレンダリング結果に合成する、というものです。加算なので描画順は関係が無くなるわけです)

 必要なパラメータは

1.半透明オブジェクトの合計値(Σc * w)
2.半透明オブジェクトのαの合計値(Σα * w)
3.半透明オブジェクトのαのΠ(1-α)。

cは半透明ピクセルの色(ただしあらかじめαで乗算済み)
wは奥行き方向の重みです。奥にあるピクセルよりも、手前にあるピクセルの方が優先的に表示されるべき、ってことのようです。

Π(1-α)は背景が透けて見える割合です。例えばそれぞれ0.3,0.4のα値をもつポリゴンを重ねてレンダリングする場合、 (1-0.3)*(1-0.4) = 0.42になります。またこれに0.0のα値をもつポリゴンを重ねても、当然 0.42 * (1-0.0) = 0.42になります。完全に透明なポリゴンを重ねても結果は変わらない、という当たり前の話なのですが、この論文の前の手法では値が変化していました。


さて

 半透明なポリゴンを加算合成したり、乗算合成する必要があるわけですが、不透明オブジェクトの後ろにある半透明オブジェクトは描画されない(遮蔽される。当然)必要があります。MainCameraの不透明オブジェクトのZバッファを使いまわすことが出来ればよいのですが、出来ない(やり方が分からない)ようですので、非効率ですが別カメラで両方ともレンダリングします。

 別カメラでレンダリングするとしても、1と2は同一バッファ(RGBA)に入りますが、3は別のバッファに描画する必要があります(チャネル数が一つ足りないので)。MRTを用いて1&2,3を二つのバッファに同時にレンダリングすることは出来ますが、ここではシンプルに、通常のレンダーターゲット(MainCameraの)のαチャネルをΠ(1-α)の格納に用います。(普通α値はグローエフェクトなどに用いられていますが、それは使わないことが前提です。)

以下はΠ(1-α)を格納するための、α値だけを出力するシェーダです。
(色を出力しないため、エディタ上では見えなくなります。欠点です)

Shader "Custom/TestTransp" {
Properties {
	_Color ("Main Color", Color) = (1,1,1,1)
	_MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
}

SubShader {
	Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
	LOD 200
	Blend Zero OneMinusSrcAlpha
	ZWrite Off
	ColorMask A

CGPROGRAM
#pragma surface surf Lambert
#pragma debug

sampler2D _MainTex;
fixed4 _Color;

struct Input {
	float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutput o) {
	fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
	o.Albedo = c.rgb;
	o.Alpha = c.a;
}
ENDCG
}

Fallback "Transparent/VertexLit"
}

Blend Zero OneMinusSrcAlphaにより、destination(背景)に半透明オブジェクトの(1-α)を掛けたものがα値に格納されます。ColorMask Aにより、RGB値はマスクされ変化せず、α値のみ出力します。(カラーは最終的に合成して描画します)

 すべての半透明オブジェクトのピクセルを描画するため、ZWriteはオフにして、半透明オブジェクトによって他の半透明オブジェクトが遮蔽されるのを防ぎます。(不透明オブジェクトによっては普通に遮蔽される必要がありますので、ZTestは行います)
 ここで得られるα値(Π(1-α)は遮蔽されない(=背景が見える割合)率になります。

次に、別カメラでの描画について解説します。

using UnityEngine;
using System.Collections;

public class OITTest : MonoBehaviour {
    public Shader RepShader;
    public RenderTexture RT0;

	// Use this for initialization
	void Start () 
    {
        RT0 = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.ARGBHalf);
        camera.targetTexture = RT0;
	}
	
	// Update is called once per frame
	void Update () 
    {
        camera.RenderWithShader(RepShader, "RenderType");
        Shader.SetGlobalTexture("_ABuffer", RT0);
	}
}

 RenderTargetを作成し、ReplacementShaderを使ってレンダリングを行い、シェーダのグローバル変数としてテクスチャを設定しています。(グローバル変数に設定するとすべてのシェーダからアクセス可能になります。ここではシンプルにするためそうしているだけで、実際には特定のマテリアルにテクスチャを渡すべき…かも)
 RenderWithShaderをUpdate関数内で呼んでいますが、これはMainCameraの結果とずれてしまうのでもう少し適切な場所に配置すべきだと思います。適当です。

 次にReplacementShaderの中身です

Shader "Custom/AlphaRep" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Tags {"RenderType"="Opaque"}
		LOD 200
		
		CGPROGRAM
		#pragma surface surf Lambert

		sampler2D _MainTex;

		struct Input {
			float2 uv_MainTex;
		};

		void surf (Input IN, inout SurfaceOutput o) {
			o.Albedo = 0;
			o.Alpha = 1;
		}
		ENDCG
	}

	SubShader {
	Tags {"RenderType"="Transparent"}
	LOD 200
	Blend One One
	ZWrite Off

	Pass {
		Name "FORWARD"
		Tags { "LightMode" = "ForwardBase" }

	CGPROGRAM
	#pragma vertex vert_surf
	#pragma fragment frag_surf
	#pragma multi_compile_fwdbase
	#include "HLSLSupport.cginc"
	#include "UnityShaderVariables.cginc"
	#define UNITY_PASS_FORWARDBASE
	#include "UnityCG.cginc"
	#include "Lighting.cginc"
	#include "AutoLight.cginc"

	#define INTERNAL_DATA
	#define WorldReflectionVector(data,normal) data.worldRefl
	#define WorldNormalVector(data,normal) normal
	#line 1
	#line 15

	//#pragma surface surf Lambert
	#pragma debug

	sampler2D _MainTex;
	fixed4 _Color;

	struct Input {
		float2 uv_MainTex;
	};

	void surf (Input IN, inout SurfaceOutput o) {
		fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
		o.Albedo = c.rgb;
		o.Alpha = c.a;
	}
	#ifdef LIGHTMAP_OFF
	struct v2f_surf {
	  float4 pos : SV_POSITION;
	  float2 pack0 : TEXCOORD0;
	  fixed3 normal : TEXCOORD1;
	  fixed3 vlight : TEXCOORD2;
	  LIGHTING_COORDS(3,4)
	  float3 wpos : TEXCOORD5;
	};
	#endif
	#ifndef LIGHTMAP_OFF
	struct v2f_surf {
	  float4 pos : SV_POSITION;
	  float2 pack0 : TEXCOORD0;
	  float2 lmap : TEXCOORD1;
	  LIGHTING_COORDS(2,3)
	  float3 wpos : TEXCOORD4;
	};
	#endif
	#ifndef LIGHTMAP_OFF
	float4 unity_LightmapST;
	#endif
	float4 _MainTex_ST;
	v2f_surf vert_surf (appdata_full v) {
	  v2f_surf o;
	  o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
	  o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
	  #ifndef LIGHTMAP_OFF
	  o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
	  #endif
	  float3 worldN = mul((float3x3)_Object2World, SCALED_NORMAL);
	  #ifdef LIGHTMAP_OFF
	  o.normal = worldN;
	  #endif
	  #ifdef LIGHTMAP_OFF
	  float3 shlight = ShadeSH9 (float4(worldN,1.0));
	  o.vlight = shlight;
	  #ifdef VERTEXLIGHT_ON
	  float3 worldPos = mul(_Object2World, v.vertex).xyz;
	  o.vlight += Shade4PointLights (
		unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
		unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
		unity_4LightAtten0, worldPos, worldN );
	  #endif // VERTEXLIGHT_ON
	  #endif // LIGHTMAP_OFF
	  TRANSFER_VERTEX_TO_FRAGMENT(o);

	  o.wpos = mul(_Object2World,v.vertex).xyz;

	  return o;
	}


	#ifndef LIGHTMAP_OFF
	sampler2D unity_Lightmap;
	#ifndef DIRLIGHTMAP_OFF
	sampler2D unity_LightmapInd;
	#endif
	#endif
	fixed4 frag_surf (v2f_surf IN) : COLOR {
	  #ifdef UNITY_COMPILER_HLSL
	  Input surfIN = (Input)0;
	  #else
	  Input surfIN;
	  #endif
	  surfIN.uv_MainTex = IN.pack0.xy;
	  #ifdef UNITY_COMPILER_HLSL
	  SurfaceOutput o = (SurfaceOutput)0;
	  #else
	  SurfaceOutput o;
	  #endif
	  o.Albedo = 0.0;
	  o.Emission = 0.0;
	  o.Specular = 0.0;
	  o.Alpha = 0.0;
	  o.Gloss = 0.0;
	  #ifdef LIGHTMAP_OFF
	  o.Normal = IN.normal;
	  #endif
	  surf (surfIN, o);
	  fixed atten = LIGHT_ATTENUATION(IN);
	  fixed4 c = 0;
	  #ifdef LIGHTMAP_OFF
	  c = LightingLambert (o, _WorldSpaceLightPos0.xyz, atten);
	  #endif // LIGHTMAP_OFF || DIRLIGHTMAP_OFF
	  #ifdef LIGHTMAP_OFF
	  c.rgb += o.Albedo * IN.vlight;
	  #endif // LIGHTMAP_OFF
	  #ifndef LIGHTMAP_OFF
	  #ifndef DIRLIGHTMAP_OFF
	  fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
	  fixed4 lmIndTex = tex2D(unity_LightmapInd, IN.lmap.xy);
	  half3 lm = LightingLambert_DirLightmap(o, lmtex, lmIndTex, 0).rgb;
	  #else // !DIRLIGHTMAP_OFF
	  fixed4 lmtex = tex2D(unity_Lightmap, IN.lmap.xy);
	  fixed3 lm = DecodeLightmap (lmtex);
	  #endif // !DIRLIGHTMAP_OFF
	  #ifdef SHADOWS_SCREEN
	  #if (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3)) && defined(SHADER_API_MOBILE)
	  c.rgb += o.Albedo * min(lm, atten*2);
	  #else
	  c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
	  #endif
	  #else // SHADOWS_SCREEN
	  c.rgb += o.Albedo * lm;
	  #endif // SHADOWS_SCREEN
	  c.a = o.Alpha;
	#endif // LIGHTMAP_OFF

	  c.rgb *= c.a;

	  float z = length(IN.wpos - _WorldSpaceCameraPos);
	  z = abs(z) / 200;
	  z = z * z * z * z;

	  float v = 0.3 / ( 0.00001 + z);

	  float w = pow((c.a + 0.01),4);
	  w += max(0.01,min(3000,v));

	  c *= w;

	  return c;
	}

	ENDCG
	}
	}
}


 二つのサブシェーダ(不透明用、半透明用)があります。実際にはCutOut用など色々作る必要アリです。不透明用のシェーダに関しては特に色を求める必要はない(深度のみ)ので、SurfaceShaderを使うのは無駄ですが、面倒なのでそのままです。
 半透明用のシェーダはライティングされた最終的な色が必要になるのでSurfaceShader…といきたいところですが、最終結果に重み付けを行うために#pragma debugで展開した結果を貼り付けています。finalcolorモディファイアではfixed4しか出力できず、精度的に問題があるのでは、というのが展開した理由です。


 さて半透明の方のサブシェーダですが、
 Blend One Oneで加算合成を行っています。また不透明ピクセルより手前にある全ピクセルを描画する必要があるため、ZWriteはオフです。
 ライティング結果のc.rgbにα値を乗算したり、また奥行きに関する重み付け変数wを計算したりしています。詳しくはオリジナルの論文を参照してください。wに関しては(9)の式をそのまま使用しています。(Z値に関してはわざわざ距離を求めなくても良いような気がしますが…)

 これで半透明オブジェクトのカラーの(重み付けされた後の)合算値、α値(重み付けされた後の)の合算値、MainCameraのレンダーターゲットのα値に格納されている背景が見える割合Π(1-α)、が揃いました。あとは合成するのみです。イメージエフェクトで実装します。

 using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class AlphaCompositeImageEffect : MonoBehaviour
{
    public Shader m_AlphaCompositeShader;
    private Material m_Material;

    private static Material CreateMaterial(Shader shader)
    {
        if (!shader)
            return null;
        Material m = new Material(shader);
        m.hideFlags = HideFlags.HideAndDontSave;
        return m;
    }
    private static void DestroyMaterial(Material mat)
    {
        if (mat)
        {
            DestroyImmediate(mat);
            mat = null;
        }
    }
    private bool m_Supported;


    void OnDisable()
    {
        DestroyMaterial(m_Material);
    }

    void Start()
    {
        if (!SystemInfo.supportsImageEffects)
        {
            m_Supported = false;
            enabled = false;
            return;
        }

        CreateMaterials();
        if (!m_Material)
        {
            m_Supported = false;
            enabled = false;
            return;
        }

        m_Supported = true;
    }

    private void CreateMaterials()
    {
        if (!m_Material && m_AlphaCompositeShader.isSupported)
        {
            m_Material = CreateMaterial(m_AlphaCompositeShader);
        }
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (!m_Supported || !m_AlphaCompositeShader.isSupported)
        {
            enabled = false;
            return;
        }
        CreateMaterials();

        Graphics.Blit(source, destination, m_Material);
    }
}

 上記はSSAOイメージエフェクトの必要な部分のみコピーして作成したものです。
 Graphics.Blitは元のレンダリング画像(テクスチャ)とマテリアルを使用して、destinationに加工後の画像を出力します。

 合成用のシェーダは次のようになります。

Shader "Custom/AlphaCompositeShader" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}

    SubShader { 
		ZTest Always
		Cull Off
		
		Fog { Mode off }
		Pass {
		
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma fragmentoption ARB_precision_hint_fastest
	
			#include "UnityCG.cginc"
	
			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD;
			};
	
			struct v2f {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD;
			};
			
			float4 _MainTex_ST;
			
			v2f vert (appdata_t v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}
	
			sampler2D _MainTex;
			sampler2D _ABuffer;
			
			half4 frag (v2f i) : COLOR
			{
				float4 c = tex2D(_MainTex, i.texcoord);
				i.texcoord.y = 1 - i.texcoord.y;
				float4 ab = tex2D(_ABuffer,i.texcoord);
				

				float oa = c.a;
				

				c.rgb *= oa;
				ab.rgb = ab.rgb / clamp(ab.a,1e-4,5e4);
				ab.rgb *= (1-oa);
				c.rgb += ab.rgb;

				return c;
			}
			ENDCG 
		} 
		}
	FallBack "Diffuse"
}

 これも論文の計算式をそのまま用いているだけですので、詳しくはそっち参照です。

 描画順に依存しない半透明オブジェクトのレンダリングが可能になりましたが、エディタ画面で半透明オブジェクトが表示出来なかったり、半透明オブジェクトのシェーダをすべて標準のシェーダから置き換える必要があったり、不透明オブジェクトのα値をすべて1にしたり、背景(SkyBox,SolidColorなど)のα値を1にする必要があるなど、わりと面倒だったりします。2回描くのもアレだし。