拡張を実装する

2021-03-15 17:10:43 +0900 +0900
最終更新 2021/03/15: updated gltf list order (ae91cf6)

UniVRM-0.63.2 から UniGLTF の構成が変わって、 extensions / extras の実装方法が変わりました。

GLTF 拡張とは

https://github.com/KhronosGroup/glTF/tree/master/extensions#extensions-vs-extras

{
  "asset": {
    "version": 2.0,
    "extras": {
      "guid": "9abb92a3-39cf-4986-a758-c43d4bb4ab58",
    }
  }
}

名前(JsonPath)が asset.extras.guid で値が "9abb92a3-39cf-4986-a758-c43d4bb4ab58" です。 extensions (extras 。複数形に注意) の

  • JsonPath。例 extensions.VRM, asset.extras.guid
  • 型、内容。例 object(VRMに関する諸々), string(guid文字列)

の取り決めが GTTF拡張 です。

extensions はオフィシャルに仕様を策定して JsonSchema として公開する。

extrasJsonSchema を作るほどでもないちょっとした追加データを手軽に追加という気持ちの違いです。仕組みは同じです。

This enables glTF models to contain application-specific properties without creating a full glTF extension

extensions は、{ベンダー名}_{拡張名} という命名規則です。 ベンダー名は、 https://github.com/KhronosGroup/glTF に申し込んで登録できます。

UniGLTF の extensions

v0.63.0 以前は、GLTF 型extensions フィールドに、GLTFExtensions 型を定義して、VRM フィールドを定義するという方法をとっていました。

class VRM
{

}

class GLTFExtensions
{
    public VRM VRM;
}

// すべての拡張の型を事前に知っている必要があり、拡張を分離できない
class GLTF
{
    public GLTFExtensions extensions;
}
// 個々の extensions に対して別個の型を定義する必要がある
class GLTFMaterialExtensions
{
    public KHR_materials_unlit KHR_materials_unlit;
}

class GLTFMaterial
{
    public GLTFMaterialExtensions extensions;
}

この設計だと GLTF と拡張を別ライブラリとして分離することができませんでした。

v0.63.1 から設計を変更して、すべての extensions/extras に同じ型の入れ物を使うように変更しました。 UniGLTF は import/export の具体的な内容を知らずに中間データの入れ物として扱います。

// extensions / extras の入れ物として使う型
// 実行時は、 glTFExtensionImport / glTFExtensionExport を使う
public abstract class glTFExtension
{

}

class GLTF
{
    // UniGLTFは具体的な型を知らない。利用側が処理(serialize/deserialize)する
    public glTFExtension extensions;
}

UniGLTF の拡張の書き方

拡張は、以下の部品要素から作れます。

  • 名前(JsonPath)。例: extensions.VRM, materials[*].extensions.KHR_materials_unlit
  • 拡張の型。T型
  • デシリアライザー(import)。 jsonバイト列 => T型
  • シリアライザーexport)。T型 => jsonバイト列

JSONPATH と 型を決める

// 型
class GoodMaterial
{
    // `materials[*].extensions.CUSTOM_materials_good`
    public const string EXTENSION_NAME = "CUSTOM_materials_good";

    public int GoodValue;
}

import

GoodMaterial DeserializeGoodMaterial(ListTreeNode<JsonValue> json)
{
    // デシリアライズ。手で書くかコード生成する(後述)
}

// ユーティリティ関数例
bool TryGetExtension<T>(UniGLTF.glTFExtension extension, string key, Func<ListTreeNode<JsonValue>, T> deserializer, out T value)
{
    if(material.extensions is UniGLTF.glTFExtensionsImport import)
    {
        // null check 完了
        foreach(var kv in import.ObjectItems())
        {
            if(kv.key.GetString()==key)
            {
                value = Deserialize(kv.Value);
                return true;
            }
        }
    }

    value = default;
    return false;
}

void ImportMaterial(UniGLTF.glTFMaterial material)
{
    // material の処理に割り込んで
    if(TryGetExtension(material.extension, GoodMaterial.EXTENSION_NAME, DeserializeGoodMaterial, out GoodMaterial good))
    {
        // good material 独自の処理
    }
}

export

void SerializeGoodMaterial(UniJSON.JsonFormatter f, GoodMaterial value)
{
    // シリアライズ。手で書くかコード生成する(後述)
}

// ユーティリティ関数例
public ArraySegment<byte> SerializeExtension<T>(T value, Func<T, ArraySegment<byte>> serialize)
{
    var f = new UniJSON.JsonFormatter();
    serialize(f, value);
    return f.GetStoreBytes();
}

void ExportGoodMaterial(UniGLTF.glTFMaterial material, GoodMaterial good)
{
    // material の処理に割り込んで
    if(!(material.extensions is UniGLTF.glTFExtensionsExport export))
    {
        // 無かった。新規作成
        export = new UniGLTF.glTFExtensionsExport();
        material.extensions = export;
    }

    var bytes = SerializeExtension(good, SerializeGoodMaterial);
    export.Add(GoodMaterial.EXTENSION_NAME, bytes);
}

実装例

GLTF: GLTF全体

C#の型からコード生成

  • Assets\UniGLTF\Runtime\UniGLTF\Format\GltfSerializer.g.cs
  • Assets\UniGLTF\Runtime\UniGLTF\Format\GltfDeserializer.g.cs

ジェネレーターの呼び出しコード

  • Assets\UniGLTF\Editor\UniGLTF\Serialization\SerializerGenerator.cs
  • Assets\UniGLTF\Editor\UniGLTF\Serialization\DeserializerGenerator.cs

生成コードの呼び出し

GLTF: meshes[*].extras.targetNames

コード生成せずに手書き

  • Assets\UniGLTF\Runtime\UniGLTF\Format\ExtensionsAndExtras\gltf_mesh_extras_targetNames.cs

生成コードの呼び出し

GLTF: materials[*].extensions.KHR_materials_unlit

コード生成せずに手書き

  • Assets\UniGLTF\Runtime\UniGLTF\Format\ExtensionsAndExtras\KHR_materials_unlit.cs

生成コードの呼び出し

GLTF: materials[*].extensions.KHR_texture_transform

コード生成せずに手書き

  • Assets\UniGLTF\Runtime\UniGLTF\Format\ExtensionsAndExtras\KHR_texture_transform.cs

生成コードの呼び出し

VRM0: extensions.VRM

C#の型からコード生成

  • Assets\VRM\Runtime\Format\VRMSerializer.g.cs
  • Assets\VRM\Runtime\Format\VRMDeserializer.g.cs

ジェネレーターの呼び出しコード

  • Assets\VRM\Editor\VRMSerializerGenerator.cs
  • Assets\VRM\Editor\VRMDeserializerGenerator.cs

生成コードの呼び出し

VRM1: extensions.VRMC_vrm など

JsonSchemaからコード生成

5つの Extensions に分かれたので個別に作成。 ささる場所(JsonPath)が違うのに注意。

extensions.VRMC_vrm

  • Assets\VRM10\Runtime\Format\VRM

materials[*].extensions.VRMC_materials_mtoon

  • Assets\VRM10\Runtime\Format\MaterialsMToon

nodes[*].extensions.VRMC_node_collider

  • Assets\VRM10\Runtime\Format\NodeCollider

extensions.VRMC_springBone

  • Assets\VRM10\Runtime\Format\SpringBone

extensions.VRMC_vrm_constraints

  • Assets\VRM10\Runtime\Format\Constraints

ジェネレーターの呼び出しコード

  • Assets\VRM10\Editor\GeneratorMenu.cs

生成コードの呼び出し

コード生成

JSON と C# の型との シリアライズ/デシリアライズは定型コードになるので、ジェネレーターがあります。 C# の型から生成するものと、JsonSchema から C# の型とともに生成するものがあります。

C# の型から生成

シリアライザー

ジェネレーターを呼び出すコードを作成します。

  • 元になる型
  • 出力先

の2つを決めます。static関数を生成するので、namespace と static class で囲ってあげます。

  • Assets\UniGLTF\Editor\UniGLTF\Serialization\SerializerGenerator.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using UniJSON;
using UnityEditor;
using UnityEngine;

namespace UniGLTF
{
    public static class SerializerGenerator
    {
        const BindingFlags FIELD_FLAGS = BindingFlags.Instance | BindingFlags.Public;

        const string Begin = @"// Don't edit manually. This is generaged. 
using System;
using System.Collections.Generic;
using UniJSON;

namespace UniGLTF {

    static public class GltfSerializer
    {

";

        const string End = @"
    } // class
} // namespace
";

        static string OutPath
        {
            get
            {
                return Path.Combine(UnityEngine.Application.dataPath,
                "UniGLTF/UniGLTF/Scripts/IO/GltfSerializer.g.cs");
            }
        }

        [MenuItem(UniGLTFVersion.MENU + "/GLTF: Generate Serializer")]
        static void GenerateSerializer()
        {
            var info = new ObjectSerialization(typeof(glTF), "gltf", "Serialize_");
            Debug.Log(info);

            using (var s = File.Open(OutPath, FileMode.Create))
            using (var w = new StreamWriter(s, new UTF8Encoding(false)))
            {
                w.Write(Begin);
                info.GenerateSerializer(w, "Serialize");
                w.Write(End);
            }

            Debug.LogFormat("write: {0}", OutPath);
            UnityPath.FromFullpath(OutPath).ImportAsset();
        }
    }
}

デシリアライザー

ジェネレーターを呼び出すコードを作成します。

  • 元になる型
  • 出力先

の2つを決めます。static関数を生成するので、namespace と static class で囲ってあげます。

  • Assets\UniGLTF\Editor\UniGLTF\Serialization\DeserializerGenerator.cs
using System.IO;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;

namespace UniGLTF
{
    /// <summary>
    /// Generate deserializer from ListTreeNode<JsonValue> to glTF using type reflection
    /// </summary>
    public static class DeserializerGenerator
    {
        public const BindingFlags FIELD_FLAGS = BindingFlags.Instance | BindingFlags.Public;

        const string Begin = @"// Don't edit manually. This is generaged. 
using UniJSON;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace UniGLTF {

public static class GltfDeserializer
{

";

        const string End = @"
} // GltfDeserializer
} // UniGLTF 
";

        static string OutPath
        {
            get
            {
                return Path.Combine(UnityEngine.Application.dataPath,
                "UniGLTF/UniGLTF/Scripts/IO/GltfDeserializer.g.cs");
            }
        }

        [MenuItem(UniGLTFVersion.MENU + "/GLTF: Generate Deserializer")]
        static void GenerateSerializer()
        {
            var info = new ObjectSerialization(typeof(glTF), "gltf", "Deserialize_");
            Debug.Log(info);

            using (var s = File.Open(OutPath, FileMode.Create))
            using (var w = new StreamWriter(s, new UTF8Encoding(false)))
            {
                w.Write(Begin);
                info.GenerateDeserializer(w, "Deserialize");
                w.Write(End);
            }

            Debug.LogFormat("write: {0}", OutPath);
            UnityPath.FromFullpath(OutPath).ImportAsset();
        }
    }
}

キー出力の抑制

index に無効な値として -1 を入れる場合に、JSONではキーを出力しないとしたいことがあります。

TODO: int? にするべきだった

[JsonSchema(Minimum = 0)]
int index = -1;

のようにすることで、キーの出力を抑制できます。

    // 生成コードのキー出力例
    if(value.index>=0){

何も付けないと

    // 出力制御無し
    if(true){

enum のエンコーディング

enumの値の名前を文字列で使う、enumの値の数値を使うの2種類がありえます。 enumの場合はデフォルト値が無いので必須です。

[JsonSchema(EnumSerializationType = EnumSerializationType.AsInt)]
public glBufferTarget target;

[JsonSchema(EnumSerializationType = EnumSerializationType.AsLowerString)]
public ProjectionType type;

JsonSchemaから生成

VRM-1.0 の実装

TODO:


VRM - humanoid 3d avatar format for VR