HTML Agility Packを使ってUnityで使える3Dマークアップ言語を実装してみる

# 目的

3Dオブジェクトを記述する3Dマークアップ言語をUnityで実装してみる。

# マークアップ言語？

ざっくりHTMLみたいな、[A-Frame](https://aframe.io/)に近いものを作ってみます。

# 道具立て

HTMLライクなマークアップ言語のパースに、[HTML Agility Pack](https://html-agility-pack.net/)を使います。
NuGetで入れることもできますし、サイトからnupkgをダウンロードして7zipなどで展開して、DLLだけ使うということもできます。
.Net 4.5用だったり、.Net Standard 2.0用だったり、いくつか種類があるので、自分のプロジェクトに合ったものを使います。
UnityのDLLのImportセッティングのところで、適切なプラットフォームに対して、適切なDLLが使われるように設定しましょう。

# 実際のプログラム

以下のプログラムがパーサーのエントリー部分になります。

```HomlParser.cs
using System.Collections;
using System.Collections.Generic;
using System.Runtime;
using UnityEngine;
using HtmlAgilityPack;
using Homl.DOM;
using System.Reflection;

namespace Homl.Parser
{
    public class HomlParser
    {
        public static Dictionary<string, ParseNode> nodeTemplates;

        public HomlParser()
        {
            if(nodeTemplates == null)
            {
                nodeTemplates = new Dictionary<string, ParseNode>();

                nodeTemplates.Add("a-scene", new Body());
                nodeTemplates.Add("a-box", new Cube());
                nodeTemplates.Add("a-cylinder", new Cylinder());
                nodeTemplates.Add("a-sphere", new Sphere());
                nodeTemplates.Add("a-link", new ATag());
                nodeTemplates.Add("a-text", new Text());
                nodeTemplates.Add("homl", new Homl());
            }
        }

        public Document Parse(string homl)
        {
            var htmlDoc = new HtmlAgilityPack.HtmlDocument();
            htmlDoc.LoadHtml(homl);

            GameObject documentGO = new GameObject("Document");
            Document document = documentGO.AddComponent<Document>();
            BoxCollider bc = documentGO.AddComponent<BoxCollider>();

            walkinChild(htmlDoc.DocumentNode.ChildNodes, documentGO);

            bc.size = new Vector3(0.3f, 0.3f, 0.3f);
            return document;
        }

        private void walkinChild(HtmlAgilityPack.HtmlNodeCollection nodes, GameObject parent)
        {
            foreach (HtmlAgilityPack.HtmlNode node in nodes)
            {
                GameObject nodeObject = null;
                Debug.Log(node.Name);

                if (nodeTemplates.ContainsKey(node.Name))
                {
                    nodeObject = nodeTemplates[node.Name].parse(node, parent);
                } else
                {
                    nodeObject = parent;
                }

                if (node.HasChildNodes)
                {
                    walkinChild(node.ChildNodes, nodeObject);
                }

            }
        }
    }
}
```

再帰的にマークアップの構造をたどり、タグ名の辞書から該当タグをパースするオブジェクトを探しながらUnityのGameObjectを作成していきます。

各タグのパーサーは、例えばCubeに対するパーサーが以下のようになっています。

```Cube.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HtmlAgilityPack;

namespace Homl.Parser
{
    public class Cube : ParseNode
    {
        public override GameObject parse(HtmlNode homlNode, GameObject parent)
        {
            PrimitiveProducer pp = GameObject.Find("PrimitiveProducer").GetComponent<PrimitiveProducer>();
            GameObject cube = GameObject.Instantiate(pp.getCube());
            float x = float.Parse(homlNode.GetAttributeValue("x", "0.0f"));
            float y = float.Parse(homlNode.GetAttributeValue("y", "0.0f"));
            float z = float.Parse(homlNode.GetAttributeValue("z", "0.0f"));
            float s = float.Parse(homlNode.GetAttributeValue("size", "0.1f"));
            string color = homlNode.GetAttributeValue("color", "white");

            cube.transform.parent = parent.transform;
            cube.transform.Translate(new Vector3(x, y, z));
            cube.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);

            Material mat = cube.GetComponent<Renderer>().material;
            mat.color = ColorManager.Instance.getColor(color);
            mat.SetColor("_BaseColor", ColorManager.Instance.getColor(color));

            Vector3 scale = new Vector3(s, s, s);
            Vector3[] verts = cube.GetComponent<MeshFilter>().mesh.vertices;
            Vector3[] newverts = new Vector3[verts.Length];
            int i = 0;
            foreach (Vector3 v in verts)
            {
                v.Scale(scale);
                newverts[i] = v;
                i++;
            }
            cube.GetComponent<MeshFilter>().mesh.vertices = newverts;
            BoxCollider bc = cube.GetComponent<BoxCollider>();
            bc.size = scale;

            DOM.Cube cb = cube.AddComponent<DOM.Cube>();
            return cube;
        }
    }
}
```

PrimitiveProducerは、プリミティブの3DオブジェクトPrefabの参照を管理しているオブジェクトです。
Attributeの指定の仕方などは、本家のA-Frameと異なるところなので、コンポーネント志向の構造と合わせて抜本的な見直しは必要ですが、
これに加えて、DOMのクラス群を合わせて動かすことで、3DオブジェクトをマークアップからUnity内に生成して、リンクなどの仕組みを作ることもできるようになりました。

# 表示結果

![20191223_homl.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30257/2f4679ea-eda1-9ba8-c820-fbe9b1bdba8f.png)

## もとになったマークアップ

天気予報の雪だるまのつもり

```
<homl>
<head>
<title>Weather</title>
</head>
<body>
<a-scene wx=0.2 wy=0.001 wz=0.2>
<a-cylinder height=0.02 r=0.02 x=0 y=0.18 z=0 color=red />
<a-sphere r=0.1 x=0 y=0.05 z=0 color=white />
<a-sphere r=0.08 x=0 y=0.14 z=0 color=white />
</a-scene>
</body>
</homl>
```

機能テスト用

```
<homl>
<head>
<title>TestHoloML</title>
</head>
<body>
<a-scene wx=0.2 wy=0.2 wz=0.2>
<a-box size=0.1 x=0 y=0 z=0 color=blue>
 <a-cylinder height=0.05 r=0.02 x=0.1 y=0.1 z=0.1 color=red />
 <a-sphere r=0.05 x=-0.1 y=-0.1 z=-0.1 color=green />
 <a-text size=0.1 x=0 y=0 z=0 color=blue>ABCDEF</text>
</a-box>
<a-link href=”test.homl”>
 <a-box size=0.1 x=0.1 y=-0.1 z=0.1 color=white />
</a-link>
</a-scene>
</body>
</homl>
```

3Dグラフ

```
<homl>
<head>
<title>3D Graph</title>
</head>
<body>
<a-scene wx=0.5 wy=0.001 wz=0.2>
<a-cylinder height=0.2 r=0.05 x=-0.2 y=0.2 z=0 color=red />
<a-cylinder height=0.12 r=0.05 x=-0.1 y=0.12 z=0 color=red />
<a-cylinder height=0.14 r=0.05 x=0 y=0.14 z=0 color=red />
<a-cylinder height=0.08 r=0.05 x=0.1 y=0.08 z=0 color=red />
<a-cylinder height=0.05 r=0.05 x=0.2 y=0.05 z=0 color=red />
</a-scene>
</body>
</homl>
```

現状ではプリミティブオブジェクトの組み合わせだけなのですが、このように表示が可能になりました。