youkantoe

FSpyCamera開発記録1

2020 / 4 / 25

はじめに

FSpyCameraはfSpyというカメラキャリブレーションソフトのデータをthree.jsのカメラにコンバートするスクリプトです。

一枚の画像から、その画像の空間のカメラを3DCG上で作成することができる、このソフトはもともとBlenderのblamというアドオンが元になっています(同じ作者の方のものです)。

僕はこのblamがすごく好きで、それで後発のfSpyも大好きだったので、前にいくつかfSpyのカメラデータをthree.js上にコンバートできないかという実験をしていました。

僕はこのblamもfSpyも日頃の趣味の作品の作成で本当によく使わせてもらっています。

ありがたいことです。

僕がこのライブラリを作ろうと思った理由は、他の人にfSpyをもっと使ってほしいというのもありますし、自分の作品のなかでももっとインタラクティブで、いつもと違ってものを作りたいというのもあります。

また、ベースのソースコードは半年以上前に書いていたものですが、そのコードが陳腐化しないようにしたいということでもありました。

ベースとなる方法

基本的に、fSpyからカメラデータを持ってくるには、

  1. Blenderに読み込んでからgltfなどに変換してカメラデータを持ってくる
  2. fSpyから吐き出されるjson or projectデータから作成する

という方法になると思います。

個人的には1の方法が楽なのですが、Blenderを介さないといけないため、人によってはかなり面倒になってしまいます。

そのため、エンジニアにもできるだけ楽な方法にするために、jsonデータからの読み込みにしました。

また、fSpyのパラメータからthree.js上のカメラを作成したとしても、webブラウザにはリサイズによるサイズの変更などの面倒な対応が必要になってきます。

fSpyのデータだけでは、そういったリサイズ対応はできないため、レスポンシブ対応ができなくなってしまいます。

そのため、このライブラリではそういったことも肩代わりするようにしています。

悩んだこと

自分はこういったライブラリっぽいものは個人でちょこちょこと作ってはいたのですが、1年以上前のことになり、そのことは1ファイル2000行とかそういうスクリプトになっていました(一応モジュール化はしていた)。

またprivateリポジトリでTypeScriptを使ってAfeterEffects用のアドオンも作成していました。

そのため、こういった現代的な作り方というのに、めちゃ詳しいかと言われるとそうでもないため、そこに悩みました。

ただ、TypeScriptやESLintなどはできるだけ積極的に採用していきたいですよね。

ライブラリの責任範囲について

もともとの機能の母体がthree.jsのライブラリに依存しているため、そのthree.js上の中で、このライブラリの立ち位置をしっかりと把握する必要がありました。

つまり、ライブラリには、chart.jsなどのように一つでドン!という感じで俺が一括で世話してやるよみたいなライブラリもあれば、lodashみたいにちょこちょことおすそ分けしますねみたいなライブラリもあると思うのですが、このライブラリはどちらかといえば、後者のライブラリとなるように思います。

とはいえ、3DCGはどうしても登場人物が多くなりがちです。

例えば、画面を描画するRenderer、3DCG上の位置や回転などを決めるCamera、背景(3DCG上に置いてもいいし、CSSでbackground-imageやobject-fitにさせてもいい)、3DCG上のシーンを定義するSceenなどなど。

また、最終的には、どこかでrequestAnimationFrameなどで定期的にレンダリングを実行する必要があります。

また、モデルデータなどは非同期で取得してきますので、ロードが終わった時点でコールバックが発生することが多いです。

つまり、こちらもコールバックでカメラや画像などを取得した場合、Promiseasync awaitを使用しない場合、コードが複雑になる可能性があります。

いわゆるコールバック地獄というやつですね。

モデルデータは、別途テクスチャも非同期で取ってくることもあり、コールバックが発生しやすいように思います。

そういったことを加味しつつライブラリの作成を始めました。

カメラに関しては、ライブラリの名前自体がCameraと書いてありますし、こちらの操作環境下に置いてもいいでしょう。

しかし、悩ましいのは、画面を描画するRendererです。

カメラのアスペクトやサイズを設定したとしても、three.js上ではrendererもパラメータを変更する必要があります。

そこで、じゃあrendererも引数でもらってこっちで色々とやってしまいますねとやれば、たしかに楽なのですが、そうすることでこのライブラリがrendererと密結合になりすぎてしまうリスクを抱えます。

基本的にライブラリの責任範囲は、その機能に限局されたもののみを行い、それ以外に関してはユーザーの自由にまかせるのが大事と思います。

もちろん、全部まかなうぜというライブラリもありますが。

ライブラリが権限を超越するようなことを行うと、コード全体がブラックボックス化しやすくなると感じます。

つまり、どこで誰が何をやっているかわからないというわけですね。

そのため、直感的かつわかりやすく機能を提示し、それ以外のことをする場合は、オプションで明示的に設定することが望ましいと感じます。

そのため、リサイズ時のカメラパラメータの変更なども本当はユーザーに任せるのがだいじなのかもしれません。

しかし、あまりに自由にさせすぎると、今度はそのライブラリの勉強コストやすぐにつかえなくなってしまうため、利便性が下がります。

つまり、ライブラリはすぐに使えること、機能として拡張性が高いことを同時に達成したいというジレンマを抱えるように思います。

ライブラリ内部で行っていること

fSpyから取得したjsonデータはこちらのようなデータ構造になっています。

{
  "principalPoint": {
    "x": 0,
    "y": 0
  },
  "viewTransform": {
    "rows": [
      [
        -0.47586821560263803,
        -0.8786045834558818,
        -0.04004281845072309,
        1.2734352909781597
      ],
      [
        -0.3505159087369854,
        0.1476958801447456,
        0.9248375666626839,
        1.2964133421079407
      ],
      [
        -0.8066523657074629,
        0.45413644746770265,
        -0.3782486589268601,
        -10
      ],
      [
        0,
        0,
        0,
        1
      ]
    ]
  },
  "cameraTransform": {
    "rows": [
      [
        -0.4758682156026382,
        -0.35051590873698535,
        -0.8066523657074628,
        -7.006122776763709
      ],
      [
        -0.8786045834558818,
        0.14769588014474555,
        0.4541364474677025,
        5.468735648470887
      ],
      [
        -0.04004281845072309,
        0.9248375666626839,
        -0.3782486589268601,
        -4.930466411807364
      ],
      [
        0,
        0,
        0,
        1
      ]
    ]
  },
  "horizontalFieldOfView": 1.1113867595995295,
  "verticalFieldOfView": 0.8717067129524652,
  "vanishingPoints": [
    {
      "x": -0.950018999947836,
      "y": -0.6997667887156778
    },
    {
      "x": 3.115581460883984,
      "y": -0.5237379302278802
    },
    {
      "x": -0.17048228038024912,
      "y": 3.937495497226361
    }
  ],
  "vanishingPointAxes": [
    "xPositive",
    "yNegative",
    "zNegative"
  ],
  "relativeFocalLength": 1.6103934842642844,
  "imageWidth": 4032,
  "imageHeight": 3024
}

ここで大事なのは、

cameraTransformverticalFieldOfViewimageWidthimageHeightだと思います。

cameraTransformはthree.js上では、THREE.Matrix4で表現できます。

ここでcameraTransformの値を、セットしてから、PerspectiveCameraは、three.jsの基底クラスであるObject3Dを継承しているため、setRotationFromMatrix関数で、行列データから回転を取得します。

これでカメラの回転は取得できました。

あとは、カメラのFOV(視野角)とposition(位置)とアスペクトを取得する必要があります。

カメラのFOVはthree.jsのPerspectiveCameraのdocsのfovの箇所にもあるようにverticalでかつ角度にて管理されていることがわかります。

fSpyからもらったvertical fovの値はラジアンになっているのでthree.jsのTHREE.Math.RadToDeg関数でradianをdegreeに変換します。

カメラのポジションは、fSpyからもらったjsonのcameraTransformの各列(配列)の、3つめの値を使用します。

カメラのアスペクトは、基本的にはcanvasのサイズに依存するので、レンダリング範囲に応じて変化させます。

jsonデータのimageWidthimageHeightから、オリジナルの画像のアスペクトと、canvasのアスペクトから、three.jsのPerspectiveCameraのzoomを変えています。

リサイズでは、CSSのbackground-size: coverのような挙動を基本に想定しています。

下のスクリプト(一部読みやすいように変更しました)は、一時的にレンダリング範囲がwindowの幅と高さということにしています。

  onResize() {

    let width = window.innerWidth;
    let height = window.innerHeight;
    const ratio = width / height;
    if( ratio <= this.fSpyImageRatio  ){
        this.camera.aspect = ratio;
        this.camera.zoom = 1;
    }else{
        this.camera.aspect = ratio
        this.camera.zoom = ratio / this.fSpyImageRatio
    }
    this.camera.updateProjectionMatrix();
  }

上下方向に縮む際と、横方向に縮む際の2つのパターンがあるため、if文で分岐させています。

cameraをupdateProjectionMatrixで更新するのも大事です。