#vpdをパースしたい
vpdは、MMDの1フレーム分のモーションを記録したテキストフォーマットで以下ようなものです。
Vocaloid Pose Data file
miku.osm; // 親ファイル名
14; // 総ポーズボーン数
Bone0{右親指1
-0.000000,0.000000,0.000000; // trans x,y,z
0.071834,0.539167,0.266196,0.795784; // Quatanion x,y,z,w
}
Bone1{右親指2
-0.000000,0.000000,0.000000; // trans x,y,z
-0.131950,0.316493,-0.361478,0.867037; // Quatanion x,y,z,w
}
Bone2{右人指1
-0.000000,0.000000,0.000000; // trans x,y,z
0.000000,-0.000000,0.644218,0.764842; // Quatanion x,y,z,w
}
// 以下省略
楽勝だと思っていたがコメントが曲者だ。
この行コメントをを特別意識せずにパーサを組み立てる方法はないか。
案1: 行ごとに処理してコメントを除去しながら処理する
それはSparacheじゃなくて手作りで解読するやりかただ。
案2: 前処理してコメントを除去してから再突入する
これじゃない感。
で、コメントとSpracheで定跡があるんでないかと探していたら
https://github.com/sprache/Sprache/issues/29
にコメントにマッチさせるパーサはあった。
public static readonly Parser<string> SingleLineComment =
from first in Parse.String("//")
from rest in Parse.Characters.AnyChar.Except(Parse.Char((char)13)).Many().Text()
select rest;
検索していたらANTLRのトークナイザーはコメントを除去できるので簡単のような記述をどこかで目にしたような気がして、ならばToken関数のパワーアップ版を作って空白とともにコメントを無視させればよいと考えた。
https://github.com/sprache/Sprache/blob/master/src/Sprache/Parse.cs
からToken関数をチェック。
/// <summary>
/// Parse the token, embedded in any amount of whitespace characters.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parser"></param>
/// <returns></returns>
public static Parser<T> Token<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException("parser");
return from leading in WhiteSpace.Many()
from item in parser
from trailing in WhiteSpace.Many()
select item;
}
WhiteSpace.Many()で前後から空白を除去している。シンプル。
Sparacheはリファレンスとか無いが、このParse.csを直接見れば個々の関数は小さいのですぐわかる。
行コメントを除去できるToken関数の実験
using Sprache;
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace VpdSample
{
static class Extensions
{
// 行コメントを消化
public static readonly Parser<string> SingleLineComment =
from first in Parse.String("//")
from rest in Parse.AnyChar.Except(Parse.Char((char)13)).Many().Text()
select rest;
// ホワイトスペースに加えて行コメントを消化するToken
public static Parser<T> TokenWithSkipComment<T>(this Parser<T> parser)
{
if (parser == null) throw new ArgumentNullException("parser");
return from leading in SingleLineComment.Or(Parse.WhiteSpace.Return("")).Many()
from item in parser
from trailing in SingleLineComment.Or(Parse.WhiteSpace.Return("")).Many()
select item;
}
}
class Program
{
// ホワイトスペース以外を消化
public static readonly Parser<char> NotWhiteSpace = Parse.Char(x => !char.IsWhiteSpace(x), "whitespace");
static void Main(string[] args)
{
var path = args.First();
var text = File.ReadAllText(path, Encoding.GetEncoding(932));
// コメント除去付トークン切り出しの実験
var parser = NotWhiteSpace.AtLeastOnce().TokenWithSkipComment().Text().Many();
var result=parser.Parse(text);
foreach (var item in result)
{
Console.WriteLine(item);
}
}
}
}
トークンのスキップ対象をComment or whitespaceで先に消化することでいけるか実験。
Vocaloid
Pose
Data
file
miku.osm;
14;
Bone0{右親指1
-0.000000,0.000000,0.000000;
0.071834,0.539167,0.266196,0.795784;
}
Bone1{右親指2
-0.000000,0.000000,0.000000;
-0.131950,0.316493,-0.361478,0.867037;
}
・・・以降省略
うまくいったようだ。
VPDパーサの実装
作った部品を使ってパーサを実装する。
設計
最初にヘッダが来てそこで後続の中括弧で囲われた部分の個数がわかるので、
それを利用してRepeatするという設計に。
// todo
static Parser<Int32> Header;
// todo
static Parser<Node> Node;
public static IEnumerable<Node> Execute(String text)
{
var parser =
from boneCount in Header
from nodes in Node.Token().Repeat(boneCount)
select nodes;
;
return parser.Parse(text);
}
実装
符号付小数に対応するためSignedFloatも定義した。
static class VpdParse
{
/// <summary>
/// ホワイトスペース以外を消化
/// </summary>
static readonly Parser<char> NotWhiteSpace = Parse.Char(x => !char.IsWhiteSpace(x), "whitespace");
/// <summary>
/// 符号付浮動小数
/// </summary>
static readonly Parser<Single> SignedFloat =
from negative in Parse.Char('-').Optional().Select(x => x.IsDefined ? "-" : "")
from num in Parse.Decimal
select Convert.ToSingle(negative + num);
/// <summary>
/// Vocaloid Pose Data file
///
/// miku.osm; // 親ファイル名
/// 14; // 総ポーズボーン数
/// </summary>
static Parser<Int32> Header
{
get
{
return
from _signature in Parse.String("Vocaloid Pose Data file")
from _osm in Parse.String("miku.osm;").TokenWithSkipComment()
from n in (
from number in Parse.Number.Select(x => Convert.ToInt32(x))
from semicolon in Parse.Char(';')
select number
).TokenWithSkipComment()
select n;
}
}
/// <summary>
/// Bone0{右親指1
/// -0.000000,0.000000,0.000000; // trans x,y,z
/// 0.071834,0.539167,0.266196,0.795784; // Quatanion x,y,z,w
/// }
/// </summary>
static Parser<Node> Node
{
get
{
return
from bone in Parse.String("Bone").Text()
from index in Parse.Number
from open in Parse.Char('{')
from name in NotWhiteSpace.AtLeastOnce().Text()
from translation in (
from x in SignedFloat.Select(x => Convert.ToSingle(x))
from _c0 in Parse.Char(',')
from y in SignedFloat.Select(x => Convert.ToSingle(x))
from _c1 in Parse.Char(',')
from z in SignedFloat.Select(x => Convert.ToSingle(x))
from _sc in Parse.Char(';')
select new Vector3(x, y, z)
).TokenWithSkipComment()
from rotation in (
from x in SignedFloat.Select(x => Convert.ToSingle(x))
from _c0 in Parse.Char(',')
from y in SignedFloat.Select(x => Convert.ToSingle(x))
from _c1 in Parse.Char(',')
from z in SignedFloat.Select(x => Convert.ToSingle(x))
from _c2 in Parse.Char(',')
from w in SignedFloat.Select(x => Convert.ToSingle(x))
from _sc in Parse.Char(';')
select new Quaternion(x, y, z, w)
).TokenWithSkipComment()
from close in Parse.Char('}')
select new Node
{
Name=name,
Translation=translation,
Rotation=rotation,
};
}
}