🗂

Sessions 2024にCode Graphics作品投稿の振り返り

2024/12/25に公開

2024年11月16-17日に日本科学未来館で開催されたSESSIONS 2024には現地で参加することはできませんでしたが、作品投稿という形で参加しました。SESSIONSへの投稿は今回が初めてです。本記事では、作品の制作過程や感想などを簡単に記録します。

提出した作品

投稿した部門はCode Graphicsのサブカテゴリ「Classic GLSL Graphics」です。作品の形式は、twiglというサービスで再生可能な、1つのGLSLファイルにまとめられたものに限定されています。

提出した作品は「A Session of Glyph」というタイトルで、このURLからリアルタイムで再生できます。

https://twigl.app/?ol=true&ss=-OAMyiNs1ayNw5Qqmh0G

また、映像として見ることもできます。
https://youtu.be/nDIzFiMMX3E


スクリーンショット

さらに、公式アーカイブには上映時の映像が収録されています。
https://youtu.be/J8515VaCgw8?si=2jeemCKt79dAv5a-&t=2779

すべての26作品が上映された後、参加者と観客による投票が行われ、最終得点は261点で9位となりました。

着想とインスピレーション

最もインスピレーションを受けたのは、橋本麦さんが制作したMONO NO AWAREのMV「かむかもしかもにどもかも!(imai remix)」です。高速に切り替わる似たような文字を見つめていると、字形が本来の意味を超えて、生き物のように見える、不思議で魅力的でした。

私自身も以前からフォントに興味があり、フォント内に含まれるグリフ全体を把握するために、機械学習を用いて潜在空間を3次元に投影し、それをWebGLでナビゲートできる試みをしたことがあります。

https://observablehq.com/@stwind/source-hans-serif-a-glyph-atlas

当時は雑な作業でしたが、今回のきっかけで手法を改めて検討し直そうと思いました。同時に、GLSLファイル1つにどこまでグリフを詰め込めるか挑戦したいとも考えました。

グリフをGLSLファイルに詰め込む

フォントはNoto CJKにする

今回使用したフォントはNoto Serif CJK jp Mediumです。その理由は以下の通りです。

グリフを2次ベジェ曲線に変換する

次に、グリフをGLSLで表現するために、fonttoolsを用いて2次ベジェ曲線に変換しました。


グリフを2次ベジェ曲線に変換する

GLSLでは2次ベジェ曲線を簡単にSDF(Signed Distance Function)で表現できます。ここでは、Inigo QuilezさんとMatt Keeterさんのshadertoyを大いに参考にしました。


2次ベジェ曲線ならグリフをSDFで表現できる

変換後の2次ベジェ曲線は、各グリフあたり主に100~150個程度で、リアルタイム処理に十分対応できると期待しました。


グリフごと2次ベジェ曲線の数の分布

グリフたちを繋げる

続いて考えるのは、グリフたちを滑らかにつなげる(あるいはモーフする)方法です。本質的には、二つグループの2次ベジェ曲線をどうつなげる問題です。

二つの2次ベジェ曲線を繋げる

まずは、2次ベジェ曲線を滑らかに繋げる方法について考えます。例えば、2次ベジェ曲線\overline{p_0 p_1 p_2}\overline{q_0 q_1 q_2}を繋げる1つの方法は、3次ベジェ曲線\overline{p_2 m_0 m_1 q_0}を用いることです。点m_0\overline{p_1 p_2}の延長線上に、点m_1\overline{q_0 q_1}の延長線上に配置します。また、次の関係が成立するようにします

|p_2 m_0|=|m_1 q_0|=|p_2 q_0|

これにより、G1連続が保たれるため、滑らかな動きが可能になります。さらに、この3次ベジェ曲線を2つの2次ベジェ曲線で近似[1]することで、GLSL内での表現が可能になります。以下の図では、赤線が3次ベジェ曲線、青線がそれを近似した2つの2次ベジェ曲線です。


\overline{p_0 p_1 p_2}\overline{q_0 q_1 q_2} 3次ベジェ曲線\overline{p_2 m_0 m_1 q_0}で繋げる

この方法は様々なケースに対応でき、以下のデモで直感的に確認できます。

2次ベジェ曲線のグループを繋げる

次に、複数の2次ベジェ曲線を含むグループ同士を滑らかに繋げる方法を検討します。例えば、曲線グループ\{a, b, c, d\}\{0, 1, 2, 3\}がある場合、繋ぎ方の組み合わせは4!=24通り存在します。

ここで求めたいのは、「全体的に最も短くて滑らかな」繋ぎ方です。この目標を数値的に定義すると、以下の条件が挙げられます。

  • 接続点の角度が直角に近いほど良い。
  • 曲線の長さが短いほど良い。

イメージは下記の図のように、赤線を選べる


赤線を選ぶ

曲線\overline{p_0 p_1 p_2}について、長さsと角度\theta\overrightarrow{p_0 p_1}\overrightarrow{p_2 p_1}のコサイン)を用いて次の式を定義します。

l = s|\theta|

この値lが小さいほど、曲線が短く滑らかであるとみなせます。これに基づき、繋ぎ方を数値的に評価し、Linear Assignmment Problemlapjvライブラリを用いて解決しました[2]


最適な繋ぎ方は17番のやつ

似たようなグリフを探す

ResNetでContrastive Learning

次は似たようなグリフを探す手法についてです。ここでは、resnet18を用いてcontrastive learningを実行しました。データセットとして、すべてのグリフを64\times 64のSDF画像に変換したものを用意しました。


データ拡張したデータセットの一部

実装はJAXを使用し、学習はGoogle Colab上で行いました。全体の処理はおよそ2時間。学習後、得られた潜在空間をTriMapを用いて2次元平面にプロジェクトすると、こんな感じになります

また、ランダムに選んだグリフに対してK近傍法(KNN)を用いて類似するグリフを探索するとこの様子です

KNN近傍探索

グリフを潜在空間に探す

次に、SESSIONSのロゴをSDFに変換し、先ほど学習した潜在空間にマッピングしてみました。この操作により、ロゴが潜在空間内でどの位置に対応するかを特定できます。

最終的に、試行錯誤を重ねて、ロゴを含む9個のグリフを選出しました。

選出されたグリフは、以下の図のように滑らかに繋がることを確認しました。

GLSL

次のステップでは、曲線データをテキスト形式に変換し、GLSL内でvec4の配列として埋め込みました。このデータ部分の記述は以下のようになります。

#define V vec4
const V QUADS[3780] = V[](
V(-.03,-.01,-.03,-.01),V(-.02,-.01,.32,-.32),V(.03,-.69,-.27,-1.05),V(-.27,-.58,-.27,-.54),V(-.27,-.5,-.27,-.43),V(-.32,-.43,-.36,-.43),V(-.36,-.49,-.36,-.52),V(-.36,-.54,-.37,-.65),V(-.49,-.61,-.61,-.56),V(-.51,-.53,-.49,-.53),V(-.46,-.52,-.42,-.51),V(-.42,-.48,-.43,-.46),V(-.48,-.46,-.52,-.46),V(-.56,-.45,-.71,-.44),V(-.72,-.55,-.73,-.65),V(-.58,-.65,-.54,-.65),V(-.5,-.65,.15,-.65),V(.13,-.22,.11,.22),V(-.55,.22,-.61,.22),V(-.67,.22,-.79,.22),V(-.78,.1,-.76,-.02),V(-.65,.05,-.58,.09),V(-.52,.14,-.42,.22),V(-.45,.3,-.49,.38),V(-.6,.3,-.61,.3),V(-.62,.29,-1.14,-.09),V(-.94,-.43,-.74,-.76),V(-.16,-.45,-.12,-.43),V(-.08,-.41,-.01,-.37),V(.04,-.41,.09,-.45),V(.02,-.41,0,-.4),V(-.03,-.37,-.24,-.21),V(-.25,-.14,-.25,-.07),V(.01,-.02,.01,-.02),V(.01,-.02,0,0)
V(-.01,0,-.01,0),V(-.01,0,.37,-.15),V(.16,-.43,-.06,-.71),V(-.35,-.43,-.38,-.39),V(-.42,-.35,-.89,.11),V(-.73,.32,-.57,.54),V(.07,.38,.11,.37),V(.14,.36,.15,.35),V(.14,.36,.14,.38),V(.12,.37,.09,.37),V(.06,.37,.04,.37),V(.04,.37,.03,.36),V(.04,.36,.07,.35),V(.1,.34,.18,.3),V(.2,.36,.22,.42),V(.22,.34,.22,.3),V(.22,.26,.22,.14),V(.19,.13,.16,.12),V(.07,.2,.05,.22),V(.01,.25,0,.26),V(0,.26,-.01,.25),V(0,.24,.03,.18),V(.06,.13,.2,-.08),V(.06,-.18,-.09,-.29),V(-.2,-.07,-.24,.01),V(-.32,.09,-.35,.12),V(-.32,.08,-.28,.03),V(-.25,.07,-.18,.15),V(-.13,.22,-.12,.24),V(-.13,.25,-.14,.26),V(-.15,.24,-.16,.21),V(-.16,.18,-.19,.03),V(-.16,.06,-.14,.09),V(-.05,0,-.05,0),V(-.05,0,0,0),

残る課題は、SDFの実装です。この詳細については本記事では省略しますが、興味のある方はtwiglで公開されているソースコードをご参照ください。

感想

  • 観客がいる上映では、今回の作品は演出や情報量が不足していたと感じました。来年はこの点を改善したい
  • 個人的には、やはりフルプロシージャル生成よりも外部データを工夫して詰め込む方法に興味があります
脚注
  1. Truong, N., Yuksel, C., & Seiler, L. (2020). Quadratic Approximation of Cubic Curves. Proc. ACM Comput. Graph. Interact. Tech., 3(2). ↩︎

  2. lapjvの時間計算量はO(n^3)ですが、上記したように一つグリフの2次ベジェ曲線数最大200ぐらいなので大丈夫です ↩︎

Discussion