なんとなく

誰得感満載な記事が多いかも。Mono関係とLinuxのサーバ関係、レビューとか。

Xamarin.AndroidでテクスチャにETC2を使ってみた。

はじめに

OpenGL ES 3.0 から圧縮テクスチャでETC2も使えるようになっていて、Androidでももう数年前からサポートされているのに、結構前から時々検索していたのだけど、やってみたという日本語のブログを見つけられないでいた*1

「じゃあ」と思い、自分でやってみることにしたが、環境はXamarin.Androidで、OpenTKでBindingされたものを使っているので、C#でのソースになる。ただ、簡単に読み替えることはできるので、誰かの参考にはなる気がしたのでブログを書いてみた。

今回はKTX形式のファイルに格納されたETC2(RGB8A1)を使用し、圧縮テクスチャを貼り付けるということについて見ていく。

準備

まず、描画する前に使用する圧縮テクスチャを準備する必要がある。今回はKTX形式のファイルに格納されたETC2を使用するので、それを準備する。準備として下の2工程を行う必要がある。

  • テクスチャにする画像(PNG)の準備
  • PNGからKTXのETC2へのコンバート

テクスチャにする画像は対象としているETC2(RGB8A1)はETC1に比べてAlphaをサポートしているので、Alphaがあるもの。

今回は、画像のサイズもテクスチャであるからべき乗であるべきで、512x512のものにした。また、文字の黒らしいところをAlphaとして設定した。

etc2compのビルド

PNGからKTX形式のETC2へのコンバートするために、etc2compというツールを使った。Googleから提供されているもので、ソースからビルドする必要があった。

github.com

よりソースを準備し、Visual Studioでビルドし、etc2compを使い変換した。手順は以下。

  • Visual StudioでCMakeが使えるようになったので、CMakeLists.txtを読み込みプロジェクトを展開

  • Args.txtを編集し、デバッグ時に変換

Args.txtは以下

C:\Users\takeshich\Pictures\bloh\susumu.png
-format RGB8A1
-errormetric rgba
-output C:\Users\takeshich\Documents\dev\etc2comp\EncodedImages\susumu_etc2_512_mipmap6RGB8A1.ktx
-mipmaps 6
-verbose
-effort 0

mipmapについて上記以外に0個にしたファイルも別途用意した。mipmapについて後述するが、KTXのファイル形式上複数格納される場合もあるので、その場合を検証するため。

描画

今回は、準備したKTXを使用し、描画を簡単に進めるためにOpenGL ESを取り扱っているサンプルである*2

github.com

を使用することにした。以下3点については修正する必要があった。

  • ファイルを開き、フォーマットに従ってKTXを読み込む
  • 読み込んだテクスチャをモデルに貼り付ける
  • Alphaのためのシェーダーの修正、アルファブレンディングの設定追加

KTXにあるETC2のロード

wlog.flatlib.jp

を参考にした。

KTXのファイルの詳細については仕様書を見てほしい。

KTX File Format

見ていくとヘッダにあるデータはそのままテクスチャのロードに時に使用できる。 KTXの読み込みについては、ヘッダとメタデータの構造体を持ち、 ファイルを読み込んだStreamから、ヘッダを構造体に格納し、データ部を返す関数のあるクラスを作成した。

なお、データ部分については、mipmapのある場合もあるのでテクスチャロード時に展開するようにした。

class KTX
{
    public struct KTXHeader
    {
        public Byte[] identifier;
        public Byte[] endianness;
        public UInt32 glType;
        public UInt32 glTypeSize;
        public UInt32 glFormat;
        public UInt32 glInternalFormat;
        public UInt32 glBaseInternalFormat;
        public UInt32 pixelWidth;
        public UInt32 pixelHeight;
        public UInt32 pixelDepth;
        public UInt32 numberOfArrayElements;
        public UInt32 numberOfFaces;
        public UInt32 numberOfMipmapLevels;
        public UInt32 bytesOfKeyValueData;
    }

    public struct KTXMetadata
    {
        public UInt32? keyAndValueByteSize;
        public Byte[] keyAndValue;
        public Byte[] valuePadding;
    }

    public static byte[] Load(Stream rawdata, out KTXHeader ktxheader,out KTXMetadata ktxmetadata)
    {
        byte[] imagedata;

        using (var ms = new MemoryStream())
        {
            rawdata.CopyTo(ms);
            int Length = ms.ToArray().Length;
            ms.Seek(0, SeekOrigin.Begin);

            //ヘッダの読み込み
            //イメージデータの取得
            using (var br = new BinaryReader(ms))
            {
                ktxheader.identifier = br.ReadBytes(12);
                ktxheader.endianness = br.ReadBytes(4);
                ktxheader.glType = br.ReadUInt32();
                ktxheader.glTypeSize = br.ReadUInt32();
                ktxheader.glFormat = br.ReadUInt32();
                ktxheader.glInternalFormat = br.ReadUInt32();
                ktxheader.glBaseInternalFormat = br.ReadUInt32();
                ktxheader.pixelWidth = br.ReadUInt32();
                ktxheader.pixelHeight = br.ReadUInt32();
                ktxheader.pixelDepth = br.ReadUInt32();
                ktxheader.numberOfArrayElements = br.ReadUInt32();
                ktxheader.numberOfFaces = br.ReadUInt32();
                ktxheader.numberOfMipmapLevels = br.ReadUInt32();
                ktxheader.bytesOfKeyValueData = br.ReadUInt32();
                if (ktxheader.bytesOfKeyValueData != 0)
                {
                    ktxmetadata.keyAndValueByteSize = br.ReadUInt32();
                    ktxmetadata.keyAndValue = br.ReadBytes((int)ktxmetadata.keyAndValueByteSize);
                    ktxmetadata.valuePadding = br.ReadBytes(3 - (((int)ktxmetadata.keyAndValueByteSize + 3) % 4));
                }
                else
                {
                    ktxmetadata.keyAndValueByteSize = null;
                    ktxmetadata.keyAndValue = null;
                    ktxmetadata.valuePadding = null;
                }

                //headersize(64) +bytesOfKeyValueData
                int datasize = Length - 64 + (int)ktxheader.bytesOfKeyValueData;
                imagedata = new byte[datasize];
                imagedata = br.ReadBytes(datasize);
            }
        }

        return imagedata;

    }
}

Androidの問題なのか、StreamのLengthが取れない問題が発生するためSeekしている。

描画とmipmapの処理

サンプルを使ったので、texcoords(UV)が正しくなく反転されて表示している面もあるが、あくまでも描画出来るかというところを目的にしているので、表示の正しさはお許してほしい。

f:id:takeshich:20181026172808p:plain:w300

圧縮テクスチャであるから、GL.CompressedTexImage2Dを使用する。 PixelInternalFormatについては、KTXファイルのヘッダにある値をそのまま使用することができる。 mipmapがある場合はそのレベルを入れる。データ部にはイメージデータのサイズと実データという順で格納されているので、mipmapの個数だけ取得しながらテクスチャとして適用する。。mipmapがない場合は、mipmapを生成するようにしている。

GL.CompressedTexImage2D(TextureTarget.Texture2D, (int)mip, (PixelInternalFormat)hd.glInternalFormat,
                            width, height, 0,
                            imageSize, imagedatab);

サンプルのPaintingView.csのLoadTextureを以下のように変更した。

GL.BindTexture(TextureTarget.Texture2D, tex_id);

//mipmapのありなしのテクスチャを用意(0個と6個)
//string filename = "susumu_etc2_512_mipmap0RGB8A1.ktx";
string filename = "susumu_etc2_512_mipmap6RGB8A1.ktx";

byte[] imagedata;
KTX.KTXHeader hd;
KTX.KTXMetadata mt;
uint miplevel;


using (var st = Android.App.Application.Context.Assets.Open(filename))
{
    imagedata = KTX.Load(st, out hd, out mt);

    miplevel = hd.numberOfMipmapLevels > 0 ? hd.numberOfMipmapLevels : 1;
    int width = (int)hd.pixelWidth;
    int height = (int)hd.pixelHeight;

    using (var ms = new MemoryStream(imagedata))
    {
        using (var br = new BinaryReader(ms))
        {
            for (uint mip = 0; mip < miplevel; mip++)
            {

                int imageSize = br.ReadInt32();
                byte[] imagedatab = new byte[imageSize];
                imagedatab = br.ReadBytes(imageSize);

                GL.CompressedTexImage2D(TextureTarget.Texture2D, (int)mip, (PixelInternalFormat)hd.glInternalFormat,
                            width, height, 0,
                            imageSize, imagedatab);

                width = width / 2;
                height = height / 2;

            }
        }
    }
}

//ベースになるミップマップレベルを指定
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0);

GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)All.NearestMipmapLinear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);

if (miplevel > 1)
{
    //最大のミップマップレベルを指定
    GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, (int)(hd.numberOfMipmapLevels -1));
}
else
{
    GL.GenerateMipmap(TextureTarget.Texture2D);
}

fragment shaderでのAlpha対応

Alpha対応についてはfragment shaderで以下のようにした。

lowp vec4 color;
color = texture (text, vec2(textureCoordinate.x,1.0-textureCoordinate.y));

if(color.a < 0.5)
{
    discard;
}
else
{
    fragColor = color * (amb + diff);
}

問題とハマり

今回の本筋とはまったくちがうところで、2点あった。

  • エミュレータでmipmapが存在しGL.GenerateMipmapしない場合において、TextureParameterName.TextureMinFilterでMipmapがある場合に適用できるものを選択すると処理されず、読み込まれたはずのテクスチャではなく黒表示(0,0,0,1)になる問題

  • 実機でテクスチャが表示されない問題

解決はしたので、わかる範囲でなぜ問題だったかを記述する。

まずは、エミュレータでの問題については、Mipmapの設定(TextureMaxLevel)で、最大ミップマップのレベルを指定していたが、誤った値を指定していたためだった。TextureMaxLevelについては、個数ではなく0から始まるレベルで個数で考えた場合-1して置かなければならないものであった。

//最大のミップマップレベルを指定
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, (int)(hd.numberOfMipmapLevels -1));

これに気づくのにかなりの時間がかかり、TouchEventでのカメラの実装を入れたりしたので、かなり改修した。それは無駄であったが、いろいろ勉強になった。スクリーンショットFPSとか表示させているのはそのせい。

f:id:takeshich:20181026174046p:plain:w300

次に、実機でテクスチャが表示できない問題は、サンプルでは、モデルが表示できなくなることから、シェーダーの気がしていた。エミュレータでかなり改修していてその変更に伴って解決してしまっていたので、どこが問題であったか不明であるが、サンプルに問題があることは確かなようだ。今回はその点については追求しないでおく。

以下は実機でのスクリーンショットFPSが下がっているは、透過の処理をしているためか??? f:id:takeshich:20181026174124j:plain:w300

この点、気になって精査してみたら、indexにglDrawElementsInstancedでプリムカウントに24が設定されていた。glVertexAttribDivisorが使われていないので、ちょっと意味がわからなかった。そのため、glDrawElementsを使用してみたところ、以下のようなFPSになった。

f:id:takeshich:20181027160243j:plain:w300

*1:英語のものは見つけることができるが、NDK側でやっているのが多い。

*2:個人的には取りあえず動く程度だけど、DrawCallとかが微妙。glDrawElementsInstancedが使われているが、分割されたりしていないので、glDrawElementsで良いと思われる。分割した使い方は別に作成したほうがサンプルとしては良いと思う。