SVG+CSS AnimationでLive2Dを動かす

f:id:spring_raining:20200125150243p:plain

年末年始の自由研究として、CSSだけでLive2Dを動かすことができないかを試していました。紆余曲折あったものの、なんとか動きそうということが分かったのでひとまず情報共有。

Live2Dとは

Live2Dは、2Dのイラストをモーフィング技術を使ってアニメーションさせるソフトウェアです。あのアプリゲームのキャラクターや、あのVTuberを動かすために使われています。

f:id:spring_raining:20200125170034g:plain:w300

かわいい!!!

モデルデータを表示させる

Source: CSSLive2D/src/01_parse at master · spring-raining/CSSLive2D · GitHub

まず、Live2D公式サイトにあるサンプルデータのキャラクターを画面に表示させることを目標としてみます。各キャラクターはそれぞれモデルデータ (Haru.moc3)、テクスチャ画像 (*.png)、表情データ (*.exp3.json)、モーションデータ (*.motion3.json) などが用意されているようです。

f:id:spring_raining:20200125171758p:plain

モデルデータはバイナリかつ仕様が非公開になっていますが…

f:id:spring_raining:20200125172813p:plain

公式のLive2D Cubism Core SDKを使うとある程度の情報を得ることができます。

f:id:spring_raining:20200125172629p:plain

この情報を読み取ってみると、各パーツの形状に関する情報は drawables に含まれていることが分かってきます。

Live2Dのモデルはパーツの集合で出来ていて、1つのパーツにはメッシュと呼ばれる三角形の集合体が割り当てられています。メッシュの頂点はその位置のテクスチャと紐付いており、頂点を動かすことでキャラクターをアニメーションさせているようです。

f:id:spring_raining:20200125173520p:plain

それぞれのパーツに1枚1枚独立したテクスチャ画像は用意されておらず、複数のテクスチャがUVマップのような形でまとめられています (このモデルの場合2048x2048が2枚)。

f:id:spring_raining:20200125180547p:plain

そして、この頂点の位置情報と、各座標とテクスチャ上に対応する位置情報が 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);

作成したパーツのcanvasdrawables の情報を元に配置させると…

f:id:spring_raining:20200125181049p:plain

これは失敗例 パーツは重ね順の指定もあるので、z-index を使って制御します。

f:id:spring_raining:20200125180657p:plain

今度は成功しました! この段階では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/> に変換すると、ちゃんと各パーツのテクスチャが表示されました。ポリゴン同士で微妙に隙間が空いているのが残念…

f:id:spring_raining:20200125184957p:plain

デフォーマ

Live2Dではパーツの変形を制御するものとして デフォーマ を提供しています。デフォーマは制御点を使ってパーツや他のデフォーマの変形を定義し、デフォーマ同士に親子関係を作ることで親デフォーマの変形が小デフォーマに影響する複雑なアニメーションを表現しています。

f:id:spring_raining:20200125185637g:plain

しかし、本来ならmoc3データに入っているはずのデフォーマに関する情報が、Live2DのSDKではJSから見ることが出来ない… 意図的に秘匿しているっぽい感じもあるので、諦めて自分でデフォーマの情報を書くことにします。

デフォーマには 回転デフォーマワープデフォーマ の2種類があり、それぞれ実現可能な変形が異なります。回転デフォーマはその名の通りパーツを回転させるもので、中心点と変形可能な角度の範囲を指定します。ワープデフォーマは、ある四角形の領域を縦と横方向に一定数分割し、田の字になったデフォーマの各頂点を動かすことで、その領域にあるポリゴン頂点の移動を補完するものです。*1 今回用意したデフォーマの定義は、このあたりからなんとなく読み取れると思います。

ところで、SVGで用意した各ポリゴンはまだ本来の位置に配置できていません。ポリゴンをシステマティックに配置するため、ここでCSS transformの matrix() 関数を使います。

CSSのmatrix関数は6つのパラメータを使って2次元変換行列を指定し、その値に従って要素を変形させます。ポリゴン3点のそれぞれの移動元と移動先座標が分かればこのパラメータが決定するので、下記の式に従って全てのポリゴンのmatrix変形をCSSに追加します。

f:id:spring_raining:20200125195804p:plain:w300

このとき、 {} $$\left( \begin{array}{cc} a & b \\ c & d \\ tx & ty \end{array} \right) = \left( \begin{array}{ccc} x_1 & y_1 & 1 \\ x_3 & y_3 & 1 \\ x_5 & y_5 & 1 \end{array} \right) ^{-1} \left( \begin{array}{cc} x_2 & y_2 \\ x_4 & y_4 \\ x_6 & y_6 \end{array} \right) $$

さらに、ここから先程用意したデフォーマ情報をもとに変形させる処理を加えます。各デフォーマの階層だけ <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);

数多のバグを乗り越え、実装が完成するとこのように動かすことができます。

f:id:spring_raining:20200125205922g:plain

首が左右に動く! 首の揺れに応じて髪がなびく!! 右のもみあげがバグで千切れてるしすごいカクカクしてますが見なかったことにします。

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;
}

ついに完成です。最後にアニメーション結果を表示させてみましょう…!

f:id:spring_raining:20200125211634g:plain

え!?!?!?

なんで…………?

どうやら、ブラウザは一定数のDOMの読み込みを終えてしまうと、その時点で画面へ表示されアニメーションが開始してしまうようです。これでは、全てのポリゴンを同時にアニメーションさせることが出来ません。不本意ですが、以下のスニペットで解決させます。

<style>
  .loading * {
    animation-play-state: paused !important;
  }
<style>
<body class="loading" onload="this.document.body.classList.remove('loading')">
...

というわけで、こちらが最終成果です。JSでアニメーションさせるよりもかなりパフォーマンスが上がっていますが、No JSでアニメーションさせるという野望は叶いませんでした。

f:id:spring_raining:20200125213034g:plain

まとめ

色々厳しすぎるので、大人しくWebGLで動く公式のSDKを使ったほうが良いと思います。

Live demo

spring-raining.github.io

Repository

github.com

*1:本来のワープデフォーマは制御点の分割数と補完領域の分割数が独立していたり、補完の方法も線形ではなかったりともっと複雑ですが、今回は単純な線形補間としています

CSS組版で作った技術同人誌8作の変遷

この記事は 技術同人誌 Advent Calendar 2019 の11日目に公開されるはずだった16日目に公開された記事です。正直すまんかった。

これまでに8冊の技術同人誌を作ってきた間で、技術同人誌というジャンルがめちゃめちゃな勢いで普及し、本を書く手段もいろいろな選択肢が用意されるようになりました。その中でも、一貫してCSS組版 + Vivliostyleというニッチな方法で作り続けた体験談は意外と参考になることもあるのでは、ということで、今までにどのような方法で執筆してきたかを振り返ります。

C90

はじめる React Native
はじめる React Native

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上で原稿の入力とプレビューが同時にできる簡易的なプレビューアーを用意しました。ページ数などの目安が即座に分かるなど色々なメリットがあったので、できることなら今後もぜひ用意したいツールです。

spring-raining.hatenablog.com

また、この本では唯一本文だけでなく表紙もHTML・CSSで作りましたが、目的のレイアウトを実現するまでの時間対効果が恐ろしく悪かったため、以降では普通にIllustratorで作っています。

C92/技術書典3

CSSではじめる同人誌制作
CSSではじめる同人誌制作

やっていく合同誌
やっていく合同誌

Source: GitHub - pentapod/c92-previewer

Source: GitHub - pentapod/tbf03-previewer

この頃は原稿を書くのに必死過ぎて、特に環境の変更は試さずとにかく前回のものをそのまま使っていました。このタイミングで何故かCSSリセットをnormalize.cssからressに変えてます。なんでだろう 多分何か理由があったはずですが思い出せない… あと自分が描いたわけではないですが、「やっていく合同誌」の表紙がお気に入りです。

技術書典4ごろからMarked.jsから拡張性の高いRemarkへの乗り換えを視野に入れ、Dewritefulと名付けたRemark用のオレオレMarkdown拡張ライブラリをちょくちょく作ったりしていました。放置状態なので時間ができたらなんとかしたいです。

C94/技術書典5

CSSではじめる同人誌制作 増訂版
CSSではじめる同人誌制作 増訂版

Dive Into OpenType
Dive Into OpenType

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

Vivliostyleで本を作ろう Vol.1
Vivliostyleで本を作ろう Vol.1

Source: GitHub - spring-raining/tbf06-draft: Vivliostyleで本を作ろう Vol.1

「Vivliostyleで本を作ろう Vol.1」で、ついにMarkdownトランスパイラをMarked.jsからRemarkに乗り換えました。これで、今まではHTMLタグ直書きで凌いでいた図表の相互参照などを、書きやすい記述ルールで実現できるようになりました。RemarkのプラグインにはDewritefulだけでなく、Paperistプラグインなどを使っています。

技術書典7

Vivliostyleで本を作ろう Vol.2
Vivliostyleで本を作ろう Vol.2

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ではじめる同人誌制作」増訂版出します

f:id:spring_raining:20180625150851p:plain

サークルpentapodが頒布した過去作「はじめる React Native」と「チートシートチートシート」について、現在電子版を300円で販売していますが、今度の夏コミのタイミングで無料公開することにしました。この機会にぜひチェックしてください!

以下のフォームからメールアドレスを登録すると、無料公開時にリマインダーを受信できます。

docs.google.com

また、過去作「CSSではじめる同人誌制作」についても、夏コミで増訂版を発刊する予定です。お近くまで寄られる際は、8月12日(コミックマーケット94 3日目)プ-30a までぜひお越しください〜〜

CSS組版に対応したオンラインエディタViolaとviola-savepdfのご紹介

先日,印刷・出版のためのオンラインエディタ「Viola」をリリースしました.

viola.pub

使い方

ViolaはAdobe製エディタBracketsをベースにしたオンラインエディタです1.プレビューワにVivliostyle.jsを組み込むことで,最新のCSS組版技術を使ったレイアウトが実現できます.

f:id:spring_raining:20180111173214p:plain

右側領域はプレビューになっており,上部のスイッチでモバイルプレビュー,デスクトッププレビュー,印刷プレビューに切り替えることができます.印刷プレビューは反映に若干タイムラグが生じるので,執筆時はデスクトッププレビューがおすすめです.

f:id:spring_raining:20180113161744p:plain:w300

HTMLを直接書くのは正直だるい作業ですが,Violaに備わるMarkdownトランスパイラがある程度この作業を緩和してくれます.Markdownファイルを作成すると,自動でHTMLファイルが作成され,Markdownファイルの内容が反映されます.基本的なMarkdown記法はカバーされていますが,脚注など微妙に未対応の機能があり,今後修正したい箇所です.

f:id:spring_raining:20180113170233p:plain

Violaで作成したデータはIndexedDBを使ってブラウザが確保する領域に保存されます.このデータは,能動的に消さない限りブラウザを閉じてもそのままですが,シークレットモードでアクセスした場合,タブを閉じるとデータが消えてしまうので注意してください.また,アプリケーションのアセットファイルはService Workerでまるっとキャッシュされているので,キャッシュが残る限りオフラインでの執筆が可能です.将来的には,作成したデータはオンライン時に同期させるようにする予定です.

f:id:spring_raining:20180113161054p:plain

ところで,Violaはオンラインエディタという形式を取っていますが,実際にはVEDAのように各種エディタのプラグインという形で開発する方法も考えられます.

これは想定する利用環境の違いが理由で,Violaは執筆に慣れた人よりもむしろ,まだ使い慣れたエディタをインストールしていない初心者を対象にしているためです.このような方でもすぐに執筆を始めるためには,オンラインエディタのほうが適しています.執筆が初めての人……そう! それはこの記事を見ているあなた! 今すぐ技術書典あたりに応募して書き始めましょう!

techbookfest.org

さらに,複数人による共著の際にも簡単に同じ環境を用意できるメリットもあります.「○○というエディタをインストールしてプラグインを入れて」よりも「viola.pubにアクセスして」と頼むほうがずっと簡単です.

viola-savepdf

とはいえ,使い慣れたエディタやワークフローを既に構築しているかもしれません.そんな時はViolaのpdf出力をコマンドラインで実行するviola-savepdfを試してみてください.

npmでviola-savepdfをインストールします.

npm install -g viola-savepdf

viola-savepdfの使い方は超簡単.ChromeChrome Canary)をインストールした状態でsavepdfコマンドを実行すると,Headless Chromeを介してhtmlページをPDF形式で保存します.

savepdf -s JIS-B5 -o output.pdf target.html

--previewオプションで出力プレビューも確認できます.

f:id:spring_raining:20180111180018p:plain

Violaとviola-savepdf,どちらもぜひぜひご利用ください.

今後の実装予定

  • スタイルテンプレートの用意
  • 数式表示機能(MathJax?)
  • Markdownによる執筆機能の強化
    • 表記ルールを拡張して,ルビや参照などに対応できると良さそう
  • ePub出力機能
    • 実は仕様よく知らない…

上記の機能はあくまでも予定ということで……


  1. 正確には,BracketsMozillaがフォークした「Bramble」をベースにしています.Mozilla自身も,Brambleを使ったオンラインエディタThimbleを公開しています.

ドワンゴ サマーインターンに参加しました

だいぶ前の話ですが,9月4日〜29日までの間ドワンゴのサマーインターンに参加していました.ので,感想です.

f:id:spring_raining:20171029224828j:plain

おもいで

ドワンゴのオフィスは東銀座のでっかいビルにあります.1階が歌舞伎座になっててすごい.

有名な話ですが,ドワンゴではSlackをとてもとても活用しています1.ン千個あるPublic channelsではハッキングから今日のおかずまで雑談,もとい業務連絡が飛び交っています.業務時間中なのに反応速度が爆速ですごい

また,社内の一部エンジニアの中では1.2kgのチャーハンを平らげるしきたりがあります.これは500g.

f:id:spring_raining:20171029224852j:plain

社内ではボードゲーム部をはじめ各種部活動がさかんです.これは縁あり料理研究部の活動にお邪魔したときの様子.このあと滅茶苦茶鯖を焼いた.

f:id:spring_raining:20171029224923j:plain

そしてこれが新機能リリース祝いの寿司.

f:id:spring_raining:20171029224949j:plain

以上の通り,インターン中の写真は飯しかありませんでした.なんかこうもっとキラキラした写真は無かったのか.

やったこと

サマーインターンでは他のインターン生とチームを組み,friends.nicoというSNSサービス(Mastodonインスタンス)の新機能を企画・開発しました.ここでいう「企画」は,既に用意された案から選んで実装してみましたというのではなく,文字通りアイディアを出すところからやります.多分実務の1/3くらいは新機能を考えていた時間だと思います.

そして最終的に用意した機能は「ユーザープロフィール絵文字機能」.以下の記事で詳しく紹介されています.色々なトラブルはありましたが,何とか期間中に開発した機能をリリースすることができました.

ch.nicovideo.jp

思い返せば,私にとってWebサービスの開発への興味はどのように作ったかということで,なにを作ったかということに対しての興味はそれ程強くありませんでした.今回のドワンゴでのインターンは,何を作ればユーザーが喜んでくれるのか,どうすれば気持ちよく利用してもらえるか,ということに本気で考えさせられるという点で,これまでに無い経験になりました.たまたまMastodonという題材があったというタイミング的な面もありますが,SNSという下手をすれば一発で崩壊するセンシティブな題材で,一介の学生にここまで自由にさせてもらえる場所はなかなか無いと思います.

インターンを終えて,個人でWebサービスを開発する意欲がまた湧いてきました.Mastodonみたいな一大ムーブメントでなくても,使った人が笑えるような,ユーザー同士が交流し合えるような,良いものを作っていきたいですね.

メンターの方には本当にお世話になりました.ありがとうございます.

あわせて読みたい

随時追加的なアレです.

medium.com

kurome-stdio.hatenablog.com