DeepGlyphで感じた課題とAI JIMOJI
先月の1月23日に、自分らしいフォントを自由に楽しむ世界というコンセプトのサービス『MOJICITY (もじシティ) 』が公開されました。こちらのサービスのコンテンツ『AI JIMOJI』では手書きの文字がOpenTypeフォントになるというとても面白い体験ができます。今回このフォント生成システム実装の一部に関わらせていただきました。
私は以前個人のプロジェクトとして似たようなコンセプトの『DeepGlyph』を開発していたこともあり、AI JIMOJIとの関連を説明すべきかと思いこちらでブログの形で公開します。DeepGlyphのBeta版公開以降更新が止まっている件も (すみません…) 合わせて書こうと思います。
最初にまとめると、
- DeepGlyphは公開以降サービスの方向性と技術的な課題のミスマッチが解消できず行き詰まっている
- AI JIMOJIとDeepGlyphは (一見) 目的は似ているがアプローチは全く異なる
- AI JIMOJIは多くのメンバーによるR&Dの成果によるもので、私のアイディアは一部のみ
DeepGlyphのリリース以降開発が止まっていたのはなぜ?
いくつかの面で、DeepGlyphが実現したい方向性とDeepGlyphができることとのミスマッチを認識したためです。
DeepGlyphが実現していることは「ゴシック体や明朝体などのフォントを少ないサンプルから生成すること」でした。一方でBeta版のリリース以降、DeepGlyphの目的が「自分の書いた文字をフォントとして生成する」ものとして認識されているようでした。一見同じような目的に見えますが、この2つは特徴として大きく違っています。
前者のゴシック体や明朝体などの一般的に使われるフォントの文字 (紛らわしいので以後グリフと表現) は、一見フォントごとに多くのバリエーションを持っているように見えますが、文字全体を見ると統一感を持ってデザインされているものです。フォントは利用目的上、それぞれのグリフはストロークの幅や余白の持ち方が一つのフォントの中で一貫しています。それは「グリフの生成」が深層学習を用いた生成のタスクとして適していると私が考えた理由の一つでもあります。
一方で、手書きの文字はそのようなグリフとは異なるルールで作られるものです。同一人物が書いた文字という意味では一貫性はありますが、その特徴はグリフの持つ特徴とは全く異なるもので、本来DeepGlyphが目的としていないものでした。もしDeepGlyphのタスクを手書き文字の生成にする場合は、データセットやそもそもの生成方法から考え直さないといけないはずです。DeepGlyphのインターフェースが手書き文字を受け付けそうな見た目になっているのも勘違いの一因ですが、この部分の認識がずれたままだと (手書き文字として) 期待はずれの生成結果になった、という失望をユーザーに与え続けてしまいそうでした。
それではグリフの生成に特化したアプリケーションに改善していくか? と考えたときも、主にクオリティの面で壁にぶつかります。Beta版を使ってみた方は理解してもらえるかと思いますが、生成されるフォントは全体として一貫性はあるもののなんとなくふにゃふにゃしたグリフになりがちで、サンプルとして入力したグリフともそれほど似ていないという問題があります。
前提として (手書き文字ではなく) 自分のデザインしたグリフのフォントを作りたい! というDeepGlyphのユースケースを考えたときに、比較的フォントに対する品質への目が厳しそうなユーザーとの相性は元々よくありませんでした*1。 残念ながら現状の生成品質では、こういったユーザーの方を満足させられるようはプロダクトにはなっていません。いや、実は品質を改善するアイディアは無いことはないですが、具体的に学習に落とし込む方法が見いだせていないという状態です。
このようなことを延々考えた結果、アプリケーションとしてのDeepGlyphを発展させる方向を決めきれないまま今に至っています。ただ、多彩なグリフを自動生成する方法としてDeepGlyphが使えないということはなく、タスク次第で提示できるものもまだまだあると思っています。SIGGRAPH 2021で発表された『Goshuin 2.0 *2 』は御朱印を自動生成するというシステムで、自動生成ができることとタスクとの相性の良さを感じました。
そしてなぜ今になってフォント生成アプリケーションがAI JIMOJIという形で公開されたのか、という話はまた長くなってしまうのですが、結果的には後者の「自分の書いた文字をフォントとして生成する」という目的を達成するものになりました。
AI JIMOJI について
ここで明らかにしておきますが、AI JIMOJIは明確にフォントワークス社の提供するサービスであり、DeepGlyphとは直接的な関係性はありません。フォントの生成アイディアは私以外の複数人とのR&Dの結果で、その生成手法はDeepGlyphと全く違うものになっています。
ではなぜDeepGlyphをほったらかしにして宣伝しているかというと、このサービスが提供するフォント生成の先に私自身が興味と期待を持っているからです。それは、AI JIMOJIが日本語における最大の手書き文字データセットとなる可能性、また最も品質の良い手書き文字データセットになる可能性です。
MNISTに代表されるように、手書き文字のデータセットはビットマップ (幅×高さの2次元配列) 形式が大半です。この形式は学習データとして取り扱いやすいものですが、ビットマップ形式に落とし込む際にどうしてもデータとしての情報量が失われてしまいます。
AI JIMOJIが目指す目的も、フォントの生成だけが目的とは限らず、むしろ『Quick, Draw!』のようなコンセプトで発展していくサービスだと思っています。2022年1月に一旦はWebサービスのコンテンツとして登場しましたが、今後いろいろな形での展開を検討しているとのことで、今後どのように発展していくのか楽しみです。
一開発者として、ぜひAI JIMOJIを使ってフォント生成を楽しみながら品質向上に役立ててもらいたいです。スマートフォンと指で書いても良いですが、できればお手元のiPad+Apple Pencilで試してみてください!
*1:もし「手書き文字ではなくグリフを生成する」かつ「フォントの品質に対する要求が厳しくない」ユースケースを思いついた方はこっそり私に教えて下さい🥺
*2:Shimizu, Shuma, et al. "Goshuin 2.0: Construction of the World’s Largest Goshuin Dataset and Automatic Generation System of Goshuin with Neural Style Transfer." ACM SIGGRAPH 2021 Posters. 2021. 1-2. https://dl.acm.org/doi/10.1145/3450618.3469160
SVG+CSS AnimationでLive2Dを動かす
年末年始の自由研究として、CSSだけでLive2Dを動かすことができないかを試していました。紆余曲折あったものの、なんとか動きそうということが分かったのでひとまず情報共有。
Live2Dとは
Live2Dは、2Dのイラストをモーフィング技術を使ってアニメーションさせるソフトウェアです。あのアプリゲームのキャラクターや、あのVTuberを動かすために使われています。
かわいい!!!
モデルデータを表示させる
Source: CSSLive2D/src/01_parse at master · spring-raining/CSSLive2D · GitHub
まず、Live2D公式サイトにあるサンプルデータのキャラクターを画面に表示させることを目標としてみます。各キャラクターはそれぞれモデルデータ (Haru.moc3
)、テクスチャ画像 (*.png
)、表情データ (*.exp3.json
)、モーションデータ (*.motion3.json
) などが用意されているようです。
モデルデータはバイナリかつ仕様が非公開になっていますが…
公式のLive2D Cubism Core SDKを使うとある程度の情報を得ることができます。
この情報を読み取ってみると、各パーツの形状に関する情報は drawables
に含まれていることが分かってきます。
Live2Dのモデルはパーツの集合で出来ていて、1つのパーツにはメッシュと呼ばれる三角形の集合体が割り当てられています。メッシュの頂点はその位置のテクスチャと紐付いており、頂点を動かすことでキャラクターをアニメーションさせているようです。
それぞれのパーツに1枚1枚独立したテクスチャ画像は用意されておらず、複数のテクスチャがUVマップのような形でまとめられています (このモデルの場合2048x2048が2枚)。
そして、この頂点の位置情報と、各座標とテクスチャ上に対応する位置情報が drawables
に記述されています。というわけで、この情報通りにテクスチャからパーツを取り出してcanvasに貼り付けてみます。
// テクスチャ画像をcanvasで用意 const texture = document.createElement("canvas"); ... // パーツ描画用のcanvas const offCanvas = document.createElement("canvas"); const ctx = offCanvas.getContext("2d"); // Path2Dにメッシュの三角形の数だけパスを追加 const path = new Path2D(); for (let i = 0; i < indexCount; i += 3) { const [p, q, r] = indexArray.slice(i, i + 3); const [px, py] = uvArray.slice(p * 2, (p + 1) * 2); const [qx, qy] = uvArray.slice(q * 2, (q + 1) * 2); const [rx, ry] = uvArray.slice(r * 2, (r + 1) * 2); // Live2Dは左下が原点の座標系なのでy方向に反転 path.moveTo(px * w, (1 - py) * h); path.lineTo(qx * w, (1 - qy) * h); path.lineTo(rx * w, (1 - ry) * h); path.closePath(); } // Path2Dの形にくり抜いてテクスチャをコピー ctx.clip(path, "nonzero"); ctx.drawImage(texture, 0, 0);
作成したパーツのcanvasを drawables
の情報を元に配置させると…
これは失敗例 パーツは重ね順の指定もあるので、z-index
を使って制御します。
今度は成功しました! この段階ではJavaScriptを使っていますが、パーツ画像をData URLか何かで書き出してやればJavaScript無しで表示させることも出来るでしょう。ただし、これだけではまだキャラクターは動かせません。
モデルを動かす
Source: CSSLive2D/src/02_morph at master · spring-raining/CSSLive2D · GitHub
次にモデルのアニメーションに挑戦します。これまでは各パーツを画像として切り出したので、次はメッシュの三角形単位で画像化して…といきたいところだったのですが、ポリゴン(メッシュ中の三角形)の数が14088もあるモデルをすべて変換すると、重すぎて使い物にならないことが発覚します。そこで、方針を変換してSVGの <ClipPath />
と <use />
を使ってレンダリングを試みます。
まず、SVG内でテクスチャをSymbolとして定義します。これをSVG内の各ポリゴンで参照することで、ポリゴン毎にテクスチャを持つことによるコストを削減します。
<defs> <symbol id="texture_00" width="2048" height="2048"> <image xlink:href="resources/Haru/Haru.2048/texture_00.png"></image> </symbol> <symbol id="texture_01" width="2048" height="2048"> <image xlink:href="resources/Haru/Haru.2048/texture_01.png"></image> </symbol> </defs>
テクスチャからポリゴンを切り出すための <ClipPath />
を各ポリゴン用に用意します。idを適当に割り振っておき、後で参照できるようにします。
<defs> <clipPath id="clip_15e573e1d9f758efbe2ad2cab2d4ed2f"> <path d="M 1740.155029296875 1379.649169921875 L 1705.1416015625 1358.64111328125 L 1666.2769775390625 1385.0565185546875 Z"></path> </clipPath> <clipPath id="clip_b67855933fef072af8a663c7f97e77"> <path d="M 1600.101318359375 1393.654541015625 L 1666.2769775390625 1385.0565185546875 L 1635.11474609375 1363.3095703125 Z"></path> </clipPath> ... </def>
<use/>
から定義したテクスチャを参照し、clip-path
で切り抜きたい形を指定することで、ポリゴン1つを表現できます。
<use xlink:href="#texture_00" clip-path="url(#clip_15e573e1d9f758efbe2ad2cab2d4ed2f)"></use> <use xlink:href="#texture_00" clip-path="url(#clip_b67855933fef072af8a663c7f97e77)"></use> ...
すべてのポリゴンを <use/>
に変換すると、ちゃんと各パーツのテクスチャが表示されました。ポリゴン同士で微妙に隙間が空いているのが残念…
デフォーマ
Live2Dではパーツの変形を制御するものとして デフォーマ を提供しています。デフォーマは制御点を使ってパーツや他のデフォーマの変形を定義し、デフォーマ同士に親子関係を作ることで親デフォーマの変形が小デフォーマに影響する複雑なアニメーションを表現しています。
しかし、本来ならmoc3データに入っているはずのデフォーマに関する情報が、Live2DのSDKではJSから見ることが出来ない… 意図的に秘匿しているっぽい感じもあるので、諦めて自分でデフォーマの情報を書くことにします。
デフォーマには 回転デフォーマ と ワープデフォーマ の2種類があり、それぞれ実現可能な変形が異なります。回転デフォーマはその名の通りパーツを回転させるもので、中心点と変形可能な角度の範囲を指定します。ワープデフォーマは、ある四角形の領域を縦と横方向に一定数分割し、田の字になったデフォーマの各頂点を動かすことで、その領域にあるポリゴン頂点の移動を補完するものです。*1 今回用意したデフォーマの定義は、このあたりからなんとなく読み取れると思います。
ところで、SVGで用意した各ポリゴンはまだ本来の位置に配置できていません。ポリゴンをシステマティックに配置するため、ここでCSS transformの matrix()
関数を使います。
CSSのmatrix関数は6つのパラメータを使って2次元変換行列を指定し、その値に従って要素を変形させます。ポリゴン3点のそれぞれの移動元と移動先座標が分かればこのパラメータが決定するので、下記の式に従って全てのポリゴンのmatrix変形をCSSに追加します。
このとき、
さらに、ここから先程用意したデフォーマ情報をもとに変形させる処理を加えます。各デフォーマの階層だけ <g>
タグを入れ子にし、そのデフォーマを変形させる場合は <g>
タグ全体に対し変形させるようにします。とても都合の良いことに、SVGは複数のtransformを子の定義から順に適用してくれるため、これで上手くいきます。
<!-- 親の回転デフォーマ --> <g style="transform: rotate(-0.9597992411402052deg); transform-origin: 0px 1003.52px;"> <!-- ポリゴンの変形 --> <g style="transform: matrix(0.934, 0.058, -0.039, 0.919, -1878.59, -910.749);"> <!-- 実際のテクスチャ --> <use ... /> </g> ... </g>
そして、アニメーションのために window.requestAnimationFrame
内でCSSを書き換える処理を書きます。
const cb = (time) => { const angle = Math.sin(time / 1000); ... element.style.transform = getMatrix(...); window.requestAnimationFrame(cb); }; window.requestAnimationFrame(cb);
数多のバグを乗り越え、実装が完成するとこのように動かすことができます。
首が左右に動く! 首の揺れに応じて髪がなびく!! 右のもみあげがバグで千切れてるしすごいカクカクしてますが見なかったことにします。
CSS Animationで動かす
Source: CSSLive2D/src/03_css_animation at master · spring-raining/CSSLive2D · GitHub
JSからCSSのパラメータを操作してアニメーションさせるところまで出来たので、あとはこれをCSS Animationで表現するように修正します。といっても、アニメーションのキーフレームとなる変形は全て判明しているので、あとは各ポリゴンに以下のような@keyframesを用意して置き換えるだけです。なお、ここで使用している cubic-bezier(0.34, 0, 0.64, 0.43)
と cubic-bezier(0.36, 0.57, 0.66, 1)
は正弦波を再現したものです。
@keyframes anim_poly_a13b3d4032780bcccd23a9bd5d4a4b { 0% { /* アニメーション開始時の変形 */ transform: matrix(0.958,0,0.037,1,-946.294,-225.005); animation-timing-function: cubic-bezier(0.34, 0, 0.64, 0.43); } 50% { /* アニメーション中間点の変形 */ transform: matrix(1,0,0,1,-932,-225.005); animation-timing-function: cubic-bezier(0.36, 0.57, 0.66, 1); } 100% { /* アニメーション終了時の変形 */ transform: matrix(0.958,0,-0.063,1,-811.153,-225.005); } } [data-poly="poly_a13b3d4032780bcccd23a9bd5d4a4b"] { animation: 2s linear 0s infinite alternate anim_poly_a13b3d4032780bcccd23a9bd5d4a4b; }
ついに完成です。最後にアニメーション結果を表示させてみましょう…!
…
え!?!?!?
なんで…………?
…
どうやら、ブラウザは一定数のDOMの読み込みを終えてしまうと、その時点で画面へ表示されアニメーションが開始してしまうようです。これでは、全てのポリゴンを同時にアニメーションさせることが出来ません。不本意ですが、以下のスニペットで解決させます。
<style> .loading * { animation-play-state: paused !important; } <style> <body class="loading" onload="this.document.body.classList.remove('loading')"> ...
というわけで、こちらが最終成果です。JSでアニメーションさせるよりもかなりパフォーマンスが上がっていますが、No JSでアニメーションさせるという野望は叶いませんでした。
まとめ
色々厳しすぎるので、大人しくWebGLで動く公式のSDKを使ったほうが良いと思います。
Live demo
Repository
*1:本来のワープデフォーマは制御点の分割数と補完領域の分割数が独立していたり、補完の方法も線形ではなかったりともっと複雑ですが、今回は単純な線形補間としています
CSS組版で作った技術同人誌8作の変遷
この記事は 技術同人誌 Advent Calendar 2019 の11日目に公開されるはずだった16日目に公開された記事です。正直すまんかった。
これまでに8冊の技術同人誌を作ってきた間で、技術同人誌というジャンルがめちゃめちゃな勢いで普及し、本を書く手段もいろいろな選択肢が用意されるようになりました。その中でも、一貫してCSS組版 + Vivliostyleというニッチな方法で作り続けた体験談は意外と参考になることもあるのでは、ということで、今までにどのような方法で執筆してきたかを振り返ります。
C90
Source: GitHub - pentapod/C90-builder
記念すべき1冊目。この段階で、原稿をPug + Markdown、レイアウトをStylusで書き、Gulpでビルド、だいたいできてきたらVivliostyle viewerでPDF出力するという流れを作っています。この頃は同じくCSS組版で作られた「CSSシークレット」のサンプルコードが公開されており(2019年12月時点で非公開)、紙面レイアウトを作る上で大いに参考にした記憶があります。
技術書典2
Source: GitHub - pentapod/tbf02-builder
2回めの即売会となる技術書典2では、初の合同誌を制作。それぞれの原稿はMarkdownで書きますが、この回ではWeb上で原稿の入力とプレビューが同時にできる簡易的なプレビューアーを用意しました。ページ数などの目安が即座に分かるなど色々なメリットがあったので、できることなら今後もぜひ用意したいツールです。
また、この本では唯一本文だけでなく表紙もHTML・CSSで作りましたが、目的のレイアウトを実現するまでの時間対効果が恐ろしく悪かったため、以降では普通にIllustratorで作っています。
C92/技術書典3
Source: GitHub - pentapod/c92-previewer
Source: GitHub - pentapod/tbf03-previewer
この頃は原稿を書くのに必死過ぎて、特に環境の変更は試さずとにかく前回のものをそのまま使っていました。このタイミングで何故かCSSリセットをnormalize.cssからressに変えてます。なんでだろう 多分何か理由があったはずですが思い出せない… あと自分が描いたわけではないですが、「やっていく合同誌」の表紙がお気に入りです。
技術書典4ごろからMarked.jsから拡張性の高いRemarkへの乗り換えを視野に入れ、Dewritefulと名付けたRemark用のオレオレMarkdown拡張ライブラリをちょくちょく作ったりしていました。放置状態なので時間ができたらなんとかしたいです。
C94/技術書典5
Source: GitHub - pentapod/c94-draft
Source: GitHub - pentapod/tbf05-draft
C94はC92で出した「CSSではじめる同人誌制作」の増訂版ということで、原稿執筆はほどほどに執筆環境の改善に力を入れていました。文章校正ライブラリのTextlintを導入し、誤字や日本語のミスを防いでいます。また、巻末に索引を入れるためのPugフィルターなどを用意しています(このあたり)。 PDFの出力周りでは、CLIツールのvivliostyle-cli(旧viola-savepdf)によりトライ&エラーが劇的にやりやすくなりました。
レイアウト上で良かった変更点は、画面表示やPDF出力用のCSSと入稿時に用いるCSSを分けた点です。やはりカラーディスプレイと黒のインクで印刷される紙面は見栄えが全然違うため、2つのメディアに合ったスタイルが必要でした。紙面ではWebデザインで避けられがちな #000000 を積極的に使い、図表の線は細くくっきりを心がけるようにします。
技術書典6
Source: GitHub - spring-raining/tbf06-draft: Vivliostyleで本を作ろう Vol.1
「Vivliostyleで本を作ろう Vol.1」で、ついにMarkdownトランスパイラをMarked.jsからRemarkに乗り換えました。これで、今まではHTMLタグ直書きで凌いでいた図表の相互参照などを、書きやすい記述ルールで実現できるようになりました。RemarkのプラグインにはDewritefulだけでなく、Paperistのプラグインなどを使っています。
技術書典7
Source: GitHub - spring-raining/tbf07-draft: Vivliostyleで本を作ろう Vol.2
この回では今まで引き継ぎ使っていたソースコードを一新し、akabekobeko/env-create-bookをベースとした構成に移行しました。これにより、今までだましだまし使い限界を迎えつつあったGulpのタスクランナーをnpm-scriptsに載せ替えることができました。また、このタイミングでStylusからSCSSを使うようにしています。Stylus確かにすごく良い仕様なんですけどね…
まとめ
同人誌の制作は締切がすべて、締切前に出せば完成! というスタイルは、完成後のメンテナンスを考える必要のあるWeb開発の考えとは異なり、普段使わないライブラリを執筆時に試せる良い機会です。これからも原稿の内容自体はもちろん、原稿を執筆する環境も積極的に新しい技術を取り入れたいですね!
P.S.
過去作の無料公開というエントリで約束してた公開時期からなんと1年半も放置してしまいました… どれだけ待たせるんだ ほんとすみません 約束通り無料公開に切り替えています。どうぞ見ていってください :pray:
【予告】過去作の無料公開 &「CSSではじめる同人誌制作」増訂版出します
CSS組版に対応したオンラインエディタViolaとviola-savepdfのご紹介
先日,印刷・出版のためのオンラインエディタ「Viola」をリリースしました.
使い方
ViolaはAdobe製エディタBracketsをベースにしたオンラインエディタです1.プレビューワにVivliostyle.jsを組み込むことで,最新のCSS組版技術を使ったレイアウトが実現できます.
右側領域はプレビューになっており,上部のスイッチでモバイルプレビュー,デスクトッププレビュー,印刷プレビューに切り替えることができます.印刷プレビューは反映に若干タイムラグが生じるので,執筆時はデスクトッププレビューがおすすめです.
HTMLを直接書くのは正直だるい作業ですが,Violaに備わるMarkdownトランスパイラがある程度この作業を緩和してくれます.Markdownファイルを作成すると,自動でHTMLファイルが作成され,Markdownファイルの内容が反映されます.基本的なMarkdown記法はカバーされていますが,脚注など微妙に未対応の機能があり,今後修正したい箇所です.
Violaで作成したデータはIndexedDBを使ってブラウザが確保する領域に保存されます.このデータは,能動的に消さない限りブラウザを閉じてもそのままですが,シークレットモードでアクセスした場合,タブを閉じるとデータが消えてしまうので注意してください.また,アプリケーションのアセットファイルはService Workerでまるっとキャッシュされているので,キャッシュが残る限りオフラインでの執筆が可能です.将来的には,作成したデータはオンライン時に同期させるようにする予定です.
ところで,Violaはオンラインエディタという形式を取っていますが,実際にはVEDAのように各種エディタのプラグインという形で開発する方法も考えられます.
これは想定する利用環境の違いが理由で,Violaは執筆に慣れた人よりもむしろ,まだ使い慣れたエディタをインストールしていない初心者を対象にしているためです.このような方でもすぐに執筆を始めるためには,オンラインエディタのほうが適しています.執筆が初めての人……そう! それはこの記事を見ているあなた! 今すぐ技術書典あたりに応募して書き始めましょう!
さらに,複数人による共著の際にも簡単に同じ環境を用意できるメリットもあります.「○○というエディタをインストールしてプラグインを入れて」よりも「viola.pubにアクセスして」と頼むほうがずっと簡単です.
viola-savepdf
とはいえ,使い慣れたエディタやワークフローを既に構築しているかもしれません.そんな時はViolaのpdf出力をコマンドラインで実行するviola-savepdfを試してみてください.
npmでviola-savepdfをインストールします.
npm install -g viola-savepdf
viola-savepdfの使い方は超簡単.Chrome(Chrome Canary)をインストールした状態でsavepdf
コマンドを実行すると,Headless Chromeを介してhtmlページをPDF形式で保存します.
savepdf -s JIS-B5 -o output.pdf target.html
--preview
オプションで出力プレビューも確認できます.
Violaとviola-savepdf,どちらもぜひぜひご利用ください.
今後の実装予定
- スタイルテンプレートの用意
- 数式表示機能(MathJax?)
- Markdownによる執筆機能の強化
- 表記ルールを拡張して,ルビや参照などに対応できると良さそう
- ePub出力機能
- 実は仕様よく知らない…
上記の機能はあくまでも予定ということで……