第28章 AssetPostprocessor - エディター拡張入門

第28章 AssetPostprocessor

AssetPostprocessor は、アセットをインポートするときのフックとして機能します。インポート設定を変更したり、インポートしたアセットを更に加工するために使用することができます。

28.1 Preprocess と Postprocess

AssetPostprocessor には、大きく分けて2種類のフックがあります。

PreprocessXX 系
アセットをインポートするためのインポート設定を加工するために使用します。このフックが呼び出される時点ではアセットはインポートされていません。
PostprocessXX 系
インポートするアセットを更に加工したい場合に使用します。このフックは、アセットがインポートされた後に呼び出されます。ですが、引数として渡されるインポートされたオブジェクトは、まだアセットとして保存されていません。また、引数として渡されたオブジェクトはアセットが作成された後は破棄されます。つまり、PostprocessXX 系のメソッド内で参照を持たせることはできません。

図28.1 は、アセットのインポートの流れを図にしたものです。

アセットのインポートの流れ

図28.1: アセットのインポートの流れ

28.2 AssetImporter

AssetImporter は、インポート時に使用する ImportSettings に加え、AssetBundleName や ユーザーが自由にデータを格納できる userdata を備えています。これらのデータは、シリアル化され .meta ファイル(の一部*1)として保存されます。

イメージ的には「AssetImporter ≒ meta ファイル」と思ってもらってよい

図28.2: イメージ的には「AssetImporter ≒ meta ファイル」と思ってもらってよい

[*1] .meta ファイルには licenseType や fileFormatVersion など AssetImporter で扱わない情報もある

28.3 Version

AssetPostprocessor には、バージョンの概念が存在します。アセットのインポート時に、各アセット(正確には AssetImporter)に対して AssetPostprocessor のバージョンが割り当てられます。このバージョンをユーザーが管理することで、アセットのグルーピングが可能になり、バージョンの変更時にまとめて再インポートが可能になります。

例として考えられる簡潔なストーリとしては、以下になります。

 1: AssetPostprocessor のエディター拡張を実装。
 2: 仕様変更があったため、AssetPostprocessor のバージョンを変更。
 3: 実装した AssetPostprocessor によってインポートされたアセットがすべて再インポートされ仕様変更点が適用。

バージョンを管理するクラスを作る

public class AssetImporterUtility
{
  public const int VERSION = 2015;
}

GetVersion をオーバーライド

using UnityEngine;
using UnityEditor;

public class NewBehaviourScript :  AssetPostprocessor
{
  public override uint GetVersion ()
  {
    return AssetImporterUtility.VERSION;
  }

  void OnPreprocessTexture ()
  {
    //何か処理
  }

  void OnPostprocessTexture (Texture2D texture)
  {
    //何か処理
  }
}

28.4 テクスチャの命名規則からスプライトを作成する

AssetPostprocessor を使った例としてテクスチャの命名規則からスプライトを作成してみます。

下図の 図28.3 のようにファイル名が「UnityChan_64x64.png」のテクスチャを用意します。命名規則はファイル名からほしい情報を取得できるように「{テクスチャ名}_{スプライトの width}x{スプライトの height}.png」としました。

unitychan2d のテクスチャ「UnityChan_64x64.png」

図28.3: unitychan2d のテクスチャ「UnityChan_64x64.png」

まず、OnPreprocessTexture でテクスチャに関するインポーター設定を調整します。OnPreprocessTexture ではテクスチャ情報がまだ取得できないのでスプライトの設定を行うことができません。

void OnPreprocessTexture ()
{
  //Sprites フォルダ配下であれば実行
  if (assetPath.StartsWith ("Assets/Sprites/") == false)
    return;

  TextureImporter importer = (TextureImporter)assetImporter;
  importer.textureType = TextureImporterType.Sprite;
  importer.spriteImportMode = SpriteImportMode.Multiple;
  importer.filterMode = FilterMode.Point;
}

OnPostprocessTexture で、スプライトの設定をしていきます。スプライトの Rect の生成は、Internal で非推奨扱いですが InternalSpriteUtility.GenerateGridSpriteRectangles を使用すると楽に生成ができます。OnPostprocessXX 系の中で Importer の設定を変更していますが、スプライトのインポート処理は未だ行われていないので問題ありません。

void OnPostprocessTexture (Texture2D texture)
{
  int width, height;
  if (TryGetSpriteSize (out width, out height) == false)
    return;

  var filename = Path.GetFileNameWithoutExtension (assetPath);

  //スプライト生成に関する各種設定
  var offset = Vector2.zero;
  var size = new Vector2 (width, height);
  var padding = Vector2.zero;

  var rects = InternalSpriteUtility
    .GenerateGridSpriteRectangles (texture, offset, size, padding);

  //生成したスプライトの Rect を元に SpriteMetaData を生成
  var spriteMetadata = new List<SpriteMetaData> ();

  for (int i = 0; i < rects.Length; i++) {
    var rect = rects [i];
    spriteMetadata.Add (new SpriteMetaData {
      name = filename + " " + i,
      rect = rect
    });
  }

  TextureImporter importer = (TextureImporter)assetImporter;
  //最後にスプライト情報を適用
  importer.spritesheet = spriteMetadata.ToArray ();
}

//ファイル名からスプライトのサイズを取得する
bool TryGetSpriteSize (out int width, out int height)
{
  width = 0;
  height = 0;

  var filename = Path.GetFileNameWithoutExtension (assetPath);
  var pattern = @"(?<name>.*?)_(?<width>\d+)x(?<height>\d+)";
  var regex = new Regex (pattern);

  if (regex.IsMatch (filename) == false)
    return false;

  var groups = regex.Match (filename).Groups;
  width = int.Parse (groups ["width"].Value);
  height = int.Parse (groups ["height"].Value);
  return true;
}
インポートと同時にスプライトが生成される

図28.4: インポートと同時にスプライトが生成される

下記が完全なコードです。

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class SpriteAssetPostprocessor :  AssetPostprocessor
{

  void OnPreprocessTexture ()
  {
    if (assetPath.StartsWith ("Assets/Sprites/") == false)
      return;

    TextureImporter importer = (TextureImporter)assetImporter;
    importer.textureType = TextureImporterType.Sprite;
    importer.spriteImportMode = SpriteImportMode.Multiple;
    importer.filterMode = FilterMode.Point;
  }


  void OnPostprocessTexture (Texture2D texture)
  {
    int width, height;
    if (TryGetSpriteSize (out width, out height) == false)
      return;

    var filename = Path.GetFileNameWithoutExtension (assetPath);

    var offset = Vector2.zero;
    var size = new Vector2 (width, height);
    var padding = Vector2.zero;

    var rects = InternalSpriteUtility
      .GenerateGridSpriteRectangles (texture, offset, size, padding);

    var spriteMetadata = new List<SpriteMetaData> ();

    for (int i = 0; i < rects.Length; i++) {
      var rect = rects [i];
      spriteMetadata.Add (new SpriteMetaData {
        name = filename + " " + i,
        rect = rect
      });
    }

    TextureImporter importer = (TextureImporter)assetImporter;
    importer.spritesheet = spriteMetadata.ToArray ();
  }

  bool TryGetSpriteSize (out int width, out int height)
  {
    width = 0;
    height = 0;

    var filename = Path.GetFileNameWithoutExtension (assetPath);
    var pattern = @"(?<name>.*?)_(?<width>\d+)x(?<height>\d+)";
    var regex = new Regex (pattern);

    if (regex.IsMatch (filename) == false)
      return false;

    var groups = regex.Match (filename).Groups;
    width = int.Parse (groups ["width"].Value);
    height = int.Parse (groups ["height"].Value);
    return true;
  }

}
第27章 HideFlags