プログマのプログラマ日記

技術メモや自社サービスに関する記事を書いていく予定です。

CesiumのClippingPlaneCollectionでmodelMatrixを指定したときに断面がゆらいでしまう問題

ClippingPlaneCollectionにmodelMatrixを設定したらカメラの移動に伴って切断面がゆらぐ現象が発生したため、modelMatrixを使わないで何とかしたというお話です。

ClippingPlaneCollectionの座標系とmodelMatrix

CesiumではClippingPlaneCollectionを使ってtileset等のオブジェクトをクリップする(指定平面より外側の描画を無効にすることでオブジェクトを切断したような見栄えにする)ことができます。

クリップ平面は対象となるオブジェクトのローカル座標系で指定する必要があります。つまり、平面の法線を(0, 0, -1)とかにしてdistanceを0にするとオブジェクト原点から上半分がすっぱり切れる感じです。

Cesium sandcatsleの例

これはこれで分かりやすくて良いのですが、複数オブジェクトに同一座標のクリップ平面を適用したり任意の地理座標でクリップするなど、実際にはグローバル座標系で平面を設定できたほうがうれしい場面が多いと思います。

そのような状況に対応する場合、ClippingPlaneCollectionのmodelMatrixというpropertyを使うことができます。

modelMatrixにグローバル座標系からローカル座標系に変換するための行列を設定することで、グローバル座標系で指定した平面をオブジェクトのローカル座標系に変換してくれるようです。

ここまでは、公式のドキュメントに載っている情報で、modelMatrixの具体的な設定方法もネットでわりと見つかります。

実際にmodelMatrixを設定すると?...

ここからが本題です。
とある業務でmodelMatrixを設定したClippingPlaneCollectionを使ってみたところ、遠目にはうまく切れているように見えるのですが断面に接近するとカメラの動きに合わせてクリップ断面が前後にゆらぐ動きになってしまいました。

問題が再現するコード

→sandcatsleへのリンク

クリッピングプレーンを設定している個所の抜粋。

// tilesetにクリッピングプレーンを設定する
const setClippingPlane = (tileset, normalGlobal, distance) => {
  // クリッピングプレーンは、tilesetのlocal座標系で設定する必要がある。
  // clippingPlanesの生成時にglobalからlocal座標系に変換するための行列を設定することで
  // global座標系のクリッピングプレーンを設定できる。
  const clippingPlaneGlobal = new Cesium.ClippingPlane(normalGlobal, distance);
  // tile local => global座標系に変換するための行列
  const clippingPlanesOriginMatrix = tileset.clippingPlanesOriginMatrix;
  // 上記の逆行列(global => local変換)
  const modelMatrixInv = Cesium.Matrix4.inverse(
    clippingPlanesOriginMatrix, new Cesium.Matrix4());
  const clippingPlanes = new Cesium.ClippingPlaneCollection({
    // planesにはglobal座標系で定義した平面を設定する
    planes: [clippingPlaneGlobal],
    unionClippingRegions: true,
    edgeColor: Cesium.Color.WHITE,
    edgeWidth: 2.0,
    // modelMatrixにtile local => global座標系に変換するための行列を設定する
    modelMatrix: modelMatrixInv
  });
  tileset.clippingPlanes = clippingPlanes;
};

画面のキャプチャ

左下の箱状の設備の部分の断面が、カメラの動きに合わせて移動してしまっているのがわかるでしょうか。

modelMatrixを使わないでなんとかしてみる

modelMatrixを設定しない場合は上記現象は起きないようなので、ClippingPlaneCollectionにmodelMatrixを設定するのではなくmodelMatrixを事前に適用してローカル座標系に変換した平面を設定してみることにしました。

グローバル座標系で作成した平面をローカル座標系に変換するコードはこんな感じ。

// グローバル座標系のクリッピングプレーンをローカル座標系に変換
const toLocalClippingPlane = (tileset, normalGlobal, distance) => {
  // tile local => global座標系に変換するための行列
  const clippingPlanesOriginMatrix = tileset.clippingPlanesOriginMatrix;
  // 上記の逆行列(global => local変換)
  const modelMatrixInv = Cesium.Matrix4.inverse(
    clippingPlanesOriginMatrix, new Cesium.Matrix4());
  // 法線ベクトルをローカル座標系に変換
  const normalLocal = Cesium.Matrix4.multiplyByPointAsVector(
    modelMatrixInv, normalGlobal, new Cesium.Cartesian3());

  // ローカル座標系でのdistanceを計算。
  // (タイル原点からglobalPlaneへの距離として計算できる)
  // タイル原点のglobal座標を取得
  const tileOriginGlobal = Cesium.Matrix4.getTranslation(
    clippingPlanesOriginMatrix, new Cesium.Cartesian3());
  
  // 平面を定義
  const planeGlobal = new Cesium.Plane(normalGlobal, distance);

  // p0Globalから平面までの距離を計算
  const distanceLocal = Cesium.Plane.getPointDistance(planeGlobal, tileOriginGlobal);

  return new Cesium.ClippingPlane(normalLocal, distanceLocal);
};

ClippingPlaneCollectionを生成するときは、上のメソッドを呼び出してクリッピングプレーンをtilesetローカル座標系に変換します。

// tilesetにクリッピングプレーンを設定する
const setClippingPlane = (tileset, normalGlobal, distance) => {
  // グローバル座標系で定義したクリッピングプレーンをtilesetローカル座標系に変換する
  const clippingPlaneLocal = toLocalClippingPlane(tileset, normalGlobal, distance);
  const clippingPlanes = new Cesium.ClippingPlaneCollection({
    // planesにはlocal座標系に変換した平面を設定する
    planes: [clippingPlaneLocal],
    unionClippingRegions: true,
    edgeColor: Cesium.Color.WHITE,
    edgeWidth: 2.0,
    // modelMatrixは設定する必要がない
    // modelMatrix: modelMatrixInv
  });
  tileset.clippingPlanes = clippingPlanes;
};

コードの全体は以下の通り。

→sandcatsleへのリンク

ゆらぎが無くなりました!

ちなみに...

クリッピングプレーンはグローバル座標系で指定できたほうが良いのではという提案は以下のissueで上がっていますが、現時点(2024年4月)では3年以上放置状態のようです。

github.com