目的
3Dオブジェクトを記述する3Dマークアップ言語をUnityで実装してみる。
マークアップ言語?
ざっくりHTMLみたいな、A-Frameに近いものを作ってみます。
道具立て
HTMLライクなマークアップ言語のパースに、HTML Agility Packを使います。
NuGetで入れることもできますし、サイトからnupkgをダウンロードして7zipなどで展開して、DLLだけ使うということもできます。
.Net 4.5用だったり、.Net Standard 2.0用だったり、いくつか種類があるので、自分のプロジェクトに合ったものを使います。
UnityのDLLのImportセッティングのところで、適切なプラットフォームに対して、適切なDLLが使われるように設定しましょう。
実際のプログラム
以下のプログラムがパーサーのエントリー部分になります。
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に対するパーサーが以下のようになっています。
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内に生成して、リンクなどの仕組みを作ることもできるようになりました。
表示結果
もとになったマークアップ
天気予報の雪だるまのつもり
<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>
現状ではプリミティブオブジェクトの組み合わせだけなのですが、このように表示が可能になりました。