目の前でゲームを作成する-パート4:パレットのフェード用シェーダー(NESの場合)

今日は、古いNESゲームなどで行われていたように、パレット上でフェードイン/フェードアウトできるシェーダーの実装について説明します。



一番下の行は、限られた色のパレットがある場合、画像を徐々に暗くする(またはその逆に暗くする)ことは不可能だったということです。 パレットに適切な色がなかっただけです。 そして、これは異なる色を使用することで解決されました。色はより暗いと認識されます。 つまり、黄色のオブジェクトをフェードインする必要があり、パレットには濃い黄色の陰影がありません。そのため、最初にオブジェクトを青(暗く見える)にし、次に赤などにする必要があります。



以下に、書かれたシェーダーの最終バージョンがどのように見えるかを示します。







予約するだけです-ゲームで同様のシェーダーを使用するかどうかは、まだ決めていません。 多くの色を持つ現代のピクセルアートのように見えるので、少し議論の余地があります。



したがって、まずシェーダーディスクを作成します。

シェーダー
Shader "Custom/Palette Shader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} } SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" #pragma target 3.0 struct v2f { float4 pos : POSITION; float2 uv : TEXCOORD0; }; uniform sampler2D _MainTex; v2f vert(appdata_img v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord); return o; } half4 frag(v2f i): COLOR { half4 color = tex2D(_MainTex, i.uv); //   ,   half4 rc = color; return rc; } ENDCG } } FallBack "Diffuse" }
      
      



c#
 using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class PaletteShader : MonoBehaviour { public Shader shader; private Material _material; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// protected Material material { get { if (_material == null) { _material = new Material(shader); _material.hideFlags = HideFlags.HideAndDontSave; } return _material; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (shader == null) return; Material mat = material; Graphics.Blit(source, destination, mat); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void OnDisable() { if (_material) DestroyImmediate(_material); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }
      
      







考えてみましょう...



まず、少しの理論。 上で言ったように、色は異なって知覚されます。 青は最も暗いなどとして認識されます。 一般に、TV設定テーブルを使用してb / wテレビで視聴する場合、明から暗の順に注文されます。







このb / wへの色変換は、魔法の公式R*0.21 + G*0.72 + B*0.07



表されます。 このパラメーターを「明るさ」と呼びます。



シェーダーは次のように機能します。元の画像を取得し、その輝度を変更して(下側)、使用可能なパレットから輝度が最も近い色を見つけようとします。 つまり、実際には、シェーダーは2つの部分に分かれています。1)輝度を下げ、2)パレットから色を選択します。



明るさを下げると、すべてが簡単になります-原始的に色に係数を掛けます。 しかし、パレットで最も近い色を見つけることはより困難です。



シェーダーに精通している人は、シェーダーのサイクルが自殺に等しいことを理解しています。 そのため、各ピクセルに適した色を探してパレットを並べ替えることは悪い考えです。 どうする?



ソリューションはシンプルでエレガントです-カラーコンバーターとして機能するテクスチャを作成します。 そして、3Dテクスチャのようなものがあることは非常に幸運です。 つまり、ソースカラーをパレットのカラーインデックスに変換するためのテーブルを取得して事前計算します。 そしてさらに良い-すぐに最終的な色で。 このようなテクスチャでは、R / G / Bコンポーネントの値は3つの軸に配置され、この時点でのピクセルの色は結果の色になります。 すべてがシンプルです! そのようなテクスチャを作成するためだけに残ります。



もちろん、正確な色変換のためには、モンスターのようなテクスチャを作成する必要があります。各軸に沿った寸法は、各コンポーネントのグラデーションの数に対応します。 256x256x256です。 しかし、私たちの場合、精度は私たちにとってまったく重要ではありません。 色深度を下げ、パレット内のすべての色をいくつかの色に減らすようにします。



まず最初に、パレットを作成して、各色の明るさをすぐに思い出しましょう。



 const int depth = 3; // -      const float f_depth = 1.0f / (1.0f * depth - 1.0f); Color[] palette = new Color[depth*depth*depth]; float[] palette_grey = new float[depth*depth*depth]; //     for (int r = 0; r < depth; r++) { for (int g = 0; g < depth; g++) { for (int b = 0; b < depth; b++) { Color c = new Color(r * f_depth / 2, g * f_depth, b * f_depth, 1); int n = r*depth*depth + g*depth + b; palette[n] = c; palette_grey[n] = cr*0.21f + cg*0.72f + cb*0.07f; } } }
      
      





最終的にRコンポーネントを2で割ったという事実に注意する価値があります。 結果のパレットでそれが気に入らなかったので、赤い色が非常に「はみ出している」。



そして今-最も興味深い。 変換のために3Dテクスチャを作成する必要があります。

 const int dim = 16; // -      const float f_dim = 1.0f / (1.0f * dim - 1.0f); Texture3D tex = new Texture3D(dim, dim, dim, TextureFormat.RGB565, false); tex.filterMode = FilterMode.Point; //   ! tex.wrapMode = TextureWrapMode.Clamp; Color[] t = new Color[dim*dim*dim]; //    for (int r = 0; r < dim; r++) { for (int g = 0; g < dim; g++) { for (int b = 0; b < dim; b++) { float grey = (r * 0.21f + g * 0.72f + b * 0.07f) * f_dim; //         int idx = 0; float min_d = grey; for (int i = 1; i < palette_grey.Length; i++) { float d = Mathf.Abs(palette_grey[i] - grey); if (d < min_d) { min_d = d; idx = i; } } t[r * dim * dim + g * dim + b] = palette[idx]; //    } } } tex.SetPixels(t); tex.Apply();
      
      





まあ、実際にはシェーダー自体を書くことは残っていますが、ここではすべてが簡単です:

 half4 color = tex2D(_MainTex, i.uv); half4 rc = tex3D(_PaletteTex, color.rgb * _Br); float d = abs(Luminance(color) - Luminance(rc)); if ((d < 0.15) || (_Br == 1)) rc = color; return rc;
      
      





ここではif



の行に注意を払う価値があります。 2番目の条件は明らかです-「輝度== 1の場合、元の色をそのまま返します」。 ただし、最初の条件は、「パレットの色が結果の色に非常に近い(15%以内)で、元の色も残す場合」という特定の条件です。 これは、不必要な「ガラガラ」の花を減らすために行われます。 必要に応じて、ある種のスナップ。 そして、それはあなたが私たちの画面上のいくつかの要素が最終段階の前にそれらの色になるのを見ることができる理由です。 それ以外の場合、最後まで、それらは色ではなく、パレットからできるだけ近くにありました。 暗い色では何が悪いように見えますか。



実際には、それだけです。



最後のオプション:

シェーダー
 Shader "Custom/Palette Shader" { Properties { _MainTex ("Base (RGB)", 2D) = "white" {} _Br("Brightness", Float) = 0 _PaletteTex ("Pelette texture", 3D) = "white" {} } SubShader { Pass { ZTest Always Cull Off ZWrite Off Fog { Mode off } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" #pragma target 3.0 struct v2f { float4 pos : POSITION; float2 uv : TEXCOORD0; }; uniform sampler2D _MainTex; uniform sampler3D _PaletteTex; uniform float _Br; v2f vert(appdata_img v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord); return o; } half4 frag(v2f i): COLOR { half4 color = tex2D(_MainTex, i.uv); half4 rc = tex3D(_PaletteTex, color.rgb * _Br); float d = abs(Luminance(color) - Luminance(rc)); if ((d < 0.15) || (_Br == 1)) rc = color; return rc; } ENDCG } } FallBack "Diffuse" }
      
      



c#
 using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class PaletteShader : MonoBehaviour { public Shader shader; private Material _material; [Range(0, 1)] public float brightness = 0.0f; [Range(0, 1)] public float random = 1f; private float _r = 0f; private Texture3D _tex; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// protected Material material { get { if (_material == null) { _material = new Material(shader); _material.hideFlags = HideFlags.HideAndDontSave; } return _material; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private Texture3D GeneratePaletteTexture() { const int dim = 16; // -      const int depth = 3; // -      const float f_dim = 1.0f / (1.0f * dim - 1.0f); const float f_depth = 1.0f / (1.0f * depth - 1.0f); Texture3D tex = new Texture3D(dim, dim, dim, TextureFormat.RGB565, false); tex.filterMode = FilterMode.Point; tex.wrapMode = TextureWrapMode.Clamp; Color[] palette = new Color[depth*depth*depth]; float[] palette_grey = new float[depth*depth*depth]; //     for (int r = 0; r < depth; r++) { for (int g = 0; g < depth; g++) { for (int b = 0; b < depth; b++) { Color c = new Color(r * f_depth / 2, g * f_depth, b * f_depth, 1); int n = r*depth*depth + g*depth + b; palette[n] = c; palette_grey[n] = cr*0.21f + cg*0.72f + cb*0.07f; } } } Color[] t = new Color[dim*dim*dim]; //    for (int r = 0; r < dim; r++) { for (int g = 0; g < dim; g++) { for (int b = 0; b < dim; b++) { float grey = (r * 0.21f + g * 0.72f + b * 0.07f) * f_dim; //         int idx = 0; float min_d = grey; for (int i = 1; i < palette_grey.Length; i++) { float d = Mathf.Abs(palette_grey[i] - grey); if (d < min_d) { min_d = d; idx = i; } } t[r * dim * dim + g * dim + b] = palette[idx]; //    } } } tex.SetPixels(t); tex.Apply(); return tex; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (shader == null) return; Material mat = material; mat.SetFloat("_Br", brightness); if (_tex == null) _tex = GeneratePaletteTexture(); if (random != _r) { _r = random; _tex = GeneratePaletteTexture(); } mat.SetTexture("_PaletteTex", _tex); Graphics.Blit(source, destination, mat); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void OnDisable() { if (_material) DestroyImmediate(_material); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }
      
      







また、上記のコードで「ランダム」などのパラメーターを導入したことも注目に値します。 これは、その場でカラーテーブルを再構築する簡単な機会を得るために行われ、パレットパラメータを選択する方が便利でした。 つまり、パレットを生成するコードを変更し、スライダーを「ランダム」に移動すると、ゲームでパレットが再生成されました。



シリーズのすべての記事:

  1. アイデア、ビジョン、設定の選択、プラットフォーム、配信モデルなど。
  2. CRT / LCDの下で写真をスタイリングするためのシェーダー
  3. スクリプト言語をUnity(UniLua)に固定します
  4. パレットによるフェードイン用のシェーダー(la NES)
  5. 小計(プロトタイプ)
  6. PRインディーズゲームについて話しましょう
  7. Unityの2Dアニメーション(「フラッシュのような」)
  8. Unityでのカットシーンのビジュアルスクリプト(uScript)



All Articles