glTFファイルの読み込み(WPF3Dで表示)

3Dデータ用のフォーマットは3dsCOLLADA等たくさん存在しますが、2015年にKhronos GroupよりglTFというフォーマットが公開されました。
GitHub - KhronosGroup/glTF: glTF – Runtime 3D Asset Delivery

JSONベースで記述されていますが、頂点データなどは別ファイルにバイナリとして保存されているため、高速に読み込むことが可能です。

今回はglTFを読み込んで表示してみたいと思います。

読み込むデータは下記リンクにあるDuckを利用して実装していきたいと思います。
https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/Duck/glTF
f:id:any-programming:20170814212650p:plain

glTFパース

現状すでにたくさんのパーサが存在していますが、今回はJSONライブラリを利用して自前でパースしてみたいと思います。

JSONパーサは下記のNewtonsoft.Jsonを利用しました。
NuGet Gallery | Json.NET 10.0.3

今回は必要最低限のデータをパースするよう、下記の様にGLTFクラスを準備しました。

public enum ComponentType
{
    Int8   = 5120,
    UInt8  = 5121,
    Int16  = 5122,
    UInt16 = 5123,
    Float  = 5126
}
public enum DataType
{
    VEC3,
    VEC2,
    SCALAR
}
public class Attributes
{
    public int Normal { get; set; }
    public int Position { get; set; }
    public int Texcoord_0 { get; set; }
}
public class Primitive
{
    public Attributes Attributes { get; set; }
    public int Indices { get; set; }
}
public class Mesh
{
    public Primitive[] Primitives { get; set; }
}
public class Accessor
{
    public int BufferView { get; set; }
    public int ByteOffset { get; set; }
    public ComponentType ComponentType { get; set; }
    public int Count { get; set; }
    public DataType Type { get; set; }
}
public class Image
{
    public string Uri { get; set; }
}
public class BufferView
{
    public int Buffer { get; set; }
    public int ByteOffset { get; set; }
    public int ByteLength { get; set; }
}
public class Buffer
{
    public string Uri { get; set; }
}
public class GLTF
{
    public Mesh[] Meshes { get; set; }
    public Accessor[] Accessors { get; set; }
    public Image[] Images { get; set; }
    public BufferView[] BufferViews { get; set; }
    public Buffer[] Buffers { get; set; }
}

gltfをテキストとして読み込み、GLTFとしてDeserializeObjectを呼ぶことで、各々のプロパティにデータがセットされます。

Jsonテキストとデシリアライズ先のクラスにおいて、階層とプロパティ名が同じ(デフォルトでは大文字小文字関係なし)場合、そこにデータがデシリアライズされるようになっており、Jsonテキスト側かクラス側のどちらかにしか存在しないプロパティは無視されるようになっています。

そのため上記の様にクラスを定義することで、読み込みたいデータのみをパース出来るようになっています。

string str = File.ReadAllText(path);
GLTF gltf = JsonConvert.DeserializeObject<GLTF>(str);

詳しいフォーマットはGitHubのglTFサイトを参照してください。

パースしたglTFからメッシュデータに変換

glTFでは最終的にAccessorで定義された条件でバッファを参照することとなります。

GLTFクラスからバッファデータを取得する拡張メソッドを下記の様に定義しました。

バイナリデータは別のファイルに格納されているため、ファイルの存在するディレクトリパスを渡しています。(通常はgltfファイルと同じ場所にあると思います。)

public static int Size(this ComponentType type) =>
    type == ComponentType.Float  ? 4 :
    type == ComponentType.Int16  ? 2 :
    type == ComponentType.UInt16 ? 2 :
    1;
public static int Size(this DataType type) =>
    type == DataType.SCALAR ? 1 :
    type == DataType.VEC2   ? 2 :
    3;
public static byte[][] AccessorBytes(this GLTF gltf, string directory)
{
    var buffers =
        gltf.Buffers
        .Select(x => File.ReadAllBytes($"{directory}\\{x.Uri}"))
        .ToArray();

    var bufferViews =
        gltf.BufferViews
        .Select(x => buffers[x.Buffer]
                     .Skip(x.ByteOffset)
                     .Take(x.ByteLength))
        .ToArray();

    var accessors =
        gltf.Accessors
        .Select(x => bufferViews[x.BufferView]
                     .Skip(x.ByteOffset)
                     .Take(x.Count * x.ComponentType.Size() * x.Type.Size())
                     .ToArray())
        .ToArray();

    return accessors;
}

さらにバッファデータから実際にDuckメッシュを作成する、Duckクラスを定義します。

Primitive.IndicesやPrimitive.Attributes.Positionなどにバッファのインデクスが格納されているので、そのバッファからPoint3D[]などを読み込みます。(WPF3Dで表示するため)

本来、テクスチャはMaterial→Texture→Imageの階層で参照するのですが、簡単のためImageから直接読み込んでいます。

class Duck
{
    public Point3D[] Positions { get; }
    public Vector3D[] Normals { get; }
    public Point[] Texcoords { get; }
    public int[] Indices { get; }
    public ImageSource Texture { get; }
    public Duck(string directory, GLTF gltf)
    {
        var accessorBytes = gltf.AccessorBytes(directory);

        var primitive = gltf.Meshes[0].Primitives[0];

        Indices = ConvertTo(accessorBytes[primitive.Indices], 2, (bytes, start) => 
            (int)BitConverter.ToUInt16(bytes, start)
        );
        Positions = ConvertTo(accessorBytes[primitive.Attributes.Position], 12, (bytes, start) =>
            new Point3D(BitConverter.ToSingle(bytes, start), 
                        BitConverter.ToSingle(bytes, start + 4), 
                        BitConverter.ToSingle(bytes, start + 8))
        );
        Normals = ConvertTo(accessorBytes[primitive.Attributes.Normal], 12, (bytes, start) =>
            new Vector3D(BitConverter.ToSingle(bytes, start), 
                         BitConverter.ToSingle(bytes, start + 4), 
                         BitConverter.ToSingle(bytes, start + 8))
        );
        Texcoords = ConvertTo(accessorBytes[primitive.Attributes.Texcoord_0], 8, (bytes, start) =>
            new Point(BitConverter.ToSingle(bytes, start), 
                      BitConverter.ToSingle(bytes, start + 4))
        );
        Texture = gltf.Images.Select(x => new BitmapImage(new Uri($"{directory}\\{x.Uri}"))).Single();
    }
    T[] ConvertTo<T>(byte[] bytes, int stepBytes, Func<byte[], int, T> convert)
    {
        T[] dst = new T[bytes.Length / stepBytes];
        for (int i = 0; i < dst.Length; ++i)
            dst[i] = convert(bytes, stepBytes * i);
        return dst;
    }
}


WPF3Dで表示

MeshGeometry3Dにデータをセットし、Viewport3Dを作成します。

詳しくは下記記事を参照してください。
WPFのクラスで3Dプログラミング - 何でもプログラミング

var model = new GeometryModel3D()
{
    Geometry = new MeshGeometry3D()
    {
        Positions = new Point3DCollection(mesh.Positions),
        Normals = new Vector3DCollection(mesh.Normals),
        TextureCoordinates = new PointCollection(mesh.Texcoords),
        TriangleIndices = new Int32Collection(mesh.Indices)
    },
    Material = new DiffuseMaterial(new ImageBrush(mesh.Texture)),
};

var light = new DirectionalLight(Colors.White, new Vector3D(0, 0, -1));

var camera = new PerspectiveCamera(new Point3D(0, 100, 300), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45);

Viewport3D viewport3D = new Viewport3D();
viewport3D.Camera = camera;
viewport3D.Children.Add(new ModelVisual3D() { Content = model });
viewport3D.Children.Add(new ModelVisual3D() { Content = light });

Content = viewport3D;

f:id:any-programming:20170814220540p:plain