WebGL & Three.js

Three.jsで回転・移動・拡大縮小する方法|position・rotation・scale

Three.jsでMeshやGroupを回転・移動・拡大縮小する方法を解説します。position・rotation・scale、ローカル/ワールド座標、Quaternion、pivot、親子階層、Timerを使ったフレームレート非依存アニメーションまで確認できます。

この記事の目次
  1. position・rotation・scaleの早見表
  2. AxesHelperでX・Y・Z軸を表示する
  3. positionでobjectを移動する
  4. x・y・zをまとめて設定する
  5. 方向vectorへ移動する
  6. positionとtranslateX・Y・Zの違い
  7. rotationでobjectを回転する
  8. Euler orderを理解する
  9. rotateX・Y・Zでlocal axis回転する
  10. local axisとworld axisで回転を分ける
  11. Quaternionで姿勢を設定・補間する
  12. lookAtでtarget方向を向かせる
  13. scaleで拡大・縮小する
  14. object.scaleとgeometry.scaleの違い
  15. pivotを変えて回転・scaleの中心を移す
  16. parentとchildのlocal・world座標
  17. world position・scale・quaternionを取得する
  18. localToWorld・worldToLocalで変換する
  19. Timerでframe-rate非依存animationを作る
  20. scaleを往復させる
  21. matrixAutoUpdateと手動matrix更新
  22. 動かない・方向がおかしいときの確認項目
  23. type is not definedになる
  24. 回転角度が大きすぎる
  25. translateXの方向が想定と違う
  26. positionは合っているのにworld位置が違う
  27. lookAtの向きがずれる
  28. geometryを共有する別Meshまで変わる
  29. animation速度が端末で違う
  30. OrbitControlsを動かすとobject transformが戻る
  31. transform実装チェックリスト
  32. まとめ

Three.jsでMeshやGroupを移動・回転・拡大縮小するときは、Object3Dのpositionrotationscaleを変更します。

mesh.position.set(2, 1, -3);
mesh.rotation.set(0, THREE.MathUtils.degToRad(45), 0);
mesh.scale.set(1.5, 1.5, 1.5);

Meshだけでなく、Group、Camera、LightなどObject3Dを継承するobjectで同じ考え方を使えます。ただし、各値は基本的にparentから見たlocal transformです。world座標、rotation axis、親子階層を混同すると、意図しない方向へ動きます。

この記事では、position・Euler・Quaternion・scale、local/world座標、pivot、公転、matrix、現行のTimerを使ったframe-rate非依存animationまで解説します。

position・rotation・scaleの早見表

property default 用途
position Vector3 (0, 0, 0) parent基準の位置
rotation Euler (0, 0, 0, XYZ) radianのEuler角
quaternion Quaternion (0, 0, 0, 1) 内部的な3D rotation
scale Vector3 (1, 1, 1) 各axisの倍率
pivot Vector3 / null null rotation・scaleの中心

Three.jsの基本構成が必要な場合はThree.jsの始め方を先に確認してください。

AxesHelperでX・Y・Z軸を表示する

座標を確認するときはAxesHelperを追加します。X軸は赤、Y軸は緑、Z軸は青です。

const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

特定objectのlocal axisを見たい場合は、そのobjectのchildへ追加します。

const meshAxes = new THREE.AxesHelper(2);
mesh.add(meshAxes);

helperが不要になったらscene graphから外し、GPU resourceも解放します。

mesh.remove(meshAxes);
meshAxes.dispose();

positionでobjectを移動する

x・y・zをまとめて設定する

mesh.position.set(2, 1, -3);

個別にも変更できます。

mesh.position.x = 2;
mesh.position.y += 0.5;
mesh.position.z -= 1;

別のVector3をcopyする場合は、property自体を安易に置換せずcopy()を使います。

const spawnPosition = new THREE.Vector3(4, 0, -2);
mesh.position.copy(spawnPosition);

方向vectorへ移動する

任意方向へ一定距離進めるには、normalized directionをpositionへ加えます。

const direction = new THREE.Vector3(1, 0, -1).normalize();
mesh.position.addScaledVector(direction, 2);

directionへ2を掛けた移動量が加算されます。

positionとtranslateX・Y・Zの違い

position.xはparentのlocal coordinateにおけるX成分です。一方translateX()は、object自身が現在向いているlocal X axisに沿って移動します。

mesh.position.x += 1; // parent座標のX方向
mesh.translateX(1);   // mesh自身のlocal X方向

objectが回転していなければ同じように見えますが、回転後は方向が異なります。forward方向へ進める場合も、modelの正面axisを確認してtranslateZ()などを選びます。

任意local axisにはtranslateOnAxis()を使えます。axisはnormalizeして渡します。

const localDirection = new THREE.Vector3(1, 1, 0).normalize();
mesh.translateOnAxis(localDirection, 0.5);

rotationでobjectを回転する

rotationはEuler角で、単位はdegreeではなくradianです。

mesh.rotation.set(
  THREE.MathUtils.degToRad(30),
  THREE.MathUtils.degToRad(45),
  0
);

90度はMath.PI / 2、180度はMath.PIです。degreeをそのままrotation.y = 90へ入れないでください。

Euler orderを理解する

Eulerはaxisを適用する順序を持ち、defaultはXYZです。

mesh.rotation.order = "YXZ";
mesh.rotation.set(0.2, 0.8, 0);

同じx・y・z値でもorderが違えば最終姿勢が変わります。cameraのyaw・pitchなど用途に合うorderを選び、途中で安易にreorderしないようにします。Eulerは特定姿勢でgimbal lockが起きるため、複雑なrotation合成や補間にはQuaternionが向きます。

rotateX・Y・Zでlocal axis回転する

mesh.rotateX(THREE.MathUtils.degToRad(10));
mesh.rotateY(THREE.MathUtils.degToRad(20));

rotateX()は現在のlocal X axisに追加回転します。rotation.x = valueでEuler componentを設定する操作と、常に「全く同じ」ではありません。

local axisとworld axisで回転を分ける

normalized axisを使ってlocal回転とworld回転を選べます。

const yAxis = new THREE.Vector3(0, 1, 0);
const angle = THREE.MathUtils.degToRad(15);

mesh.rotateOnAxis(yAxis, angle);      // local axis
mesh.rotateOnWorldAxis(yAxis, angle); // world axis

公式仕様ではrotateOnWorldAxis()は、回転したparentがないことを前提とします。複雑なparent hierarchyではworld quaternionを計算するか、transformを担当するGroupの構造を見直します。

Quaternionで姿勢を設定・補間する

Three.jsはobjectのrotationを内部ではQuaternionとして扱います。任意axisとangleから設定できます。

const axis = new THREE.Vector3(0, 1, 0);
const targetQuaternion = new THREE.Quaternion().setFromAxisAngle(
  axis,
  THREE.MathUtils.degToRad(90)
);

mesh.quaternion.copy(targetQuaternion);

現在姿勢からtargetへ一定角速度で近づける場合はrotateTowards()を使えます。

const radiansPerSecond = THREE.MathUtils.degToRad(90);
mesh.quaternion.rotateTowards(
  targetQuaternion,
  radiansPerSecond * delta
);

割合で滑らかに補間する場合はslerp()があります。毎frame固定tを使うとframe rate依存になるため、時間から補間率を求めます。

lookAtでtarget方向を向かせる

world space上の点へ向けるにはlookAt()を使います。

const target = new THREE.Vector3(0, 1, -5);
mesh.lookAt(target);

modelのfront axisがThree.jsの想定と異なる場合は、import時またはwrapper Groupで向きを補正します。また、non-uniform scaleを持つparentのchildではlookAt()が正しく動かない制約があります。

scaleで拡大・縮小する

全axisを同じ倍率にする場合はsetScalar()、個別ならset()を使います。

mesh.scale.setScalar(1.5);   // uniform scale
mesh.scale.set(2, 1, 0.5);  // non-uniform scale

defaultは1です。0にすると厚みがなくなり、negative scaleは反転・winding・normal・raycastなどへ影響する場合があります。鏡像が必要な場合もmaterial sideやnormalを確認してください。

object.scaleとgeometry.scaleの違い

mesh.scaleはObject3Dのtransformを変更します。geometry.scale()はBufferGeometryのvertex data自体を変更します。

mesh.scale.set(2, 2, 2);      // object transform
geometry.scale(2, 2, 2);      // vertex dataを変更

Geometryを複数Meshで共有している場合、geometry.scale()はすべての利用者へ影響します。日常的な配置・animationにはObject3D transformを使い、model dataの基準寸法を恒久補正するときだけgeometry側を検討します。詳しくはThree.jsのBufferGeometryを参照してください。

pivotを変えて回転・scaleの中心を移す

現在のObject3Dにはrotation・scaleの中心を表すpivotがあります。

mesh.pivot = new THREE.Vector3(1, 0, 0);
mesh.rotation.y = THREE.MathUtils.degToRad(45);

projectで使用中のThree.js revisionがpivot propertyへ対応しているか確認してください。互換性が必要な場合や、objectを別の点の周囲へ公転させる場合はGroupをpivot nodeとして使えます。

const orbit = new THREE.Group();
scene.add(orbit);

mesh.position.set(3, 0, 0);
orbit.add(mesh);

orbit.rotation.y = THREE.MathUtils.degToRad(45);

orbitを回すと、offsetされたmeshがGroupのoriginを中心に公転します。

parentとchildのlocal・world座標

childのposition・rotation・scaleはparentから見たlocal transformです。world transformは親階層のtransformを合成した結果です。

const group = new THREE.Group();
group.position.set(10, 0, 0);
group.rotation.y = THREE.MathUtils.degToRad(90);

mesh.position.set(0, 0, -2);
group.add(mesh);
scene.add(group);

meshのlocal positionは(0, 0, -2)でも、world positionはgroupの移動・回転を反映します。

world position・scale・quaternionを取得する

結果を入れるtarget objectを再利用します。

const worldPosition = new THREE.Vector3();
const worldScale = new THREE.Vector3();
const worldQuaternion = new THREE.Quaternion();

mesh.getWorldPosition(worldPosition);
mesh.getWorldScale(worldScale);
mesh.getWorldQuaternion(worldQuaternion);

render前にtransformを変更し、直後にworld値が必要ならmatrix更新を明示します。

mesh.updateWorldMatrix(true, false);
mesh.getWorldPosition(worldPosition);

localToWorld・worldToLocalで変換する

const localPoint = new THREE.Vector3(0, 1, 0);
const worldPoint = mesh.localToWorld(localPoint.clone());

const backToLocal = mesh.worldToLocal(worldPoint.clone());

methodは渡したVector3自体を書き換えるため、元値を残すならcloneします。

Timerでframe-rate非依存animationを作る

1frameごとにrotation.y += 0.01と書くと、60Hzと120Hzで1秒間の回転量が変わります。秒単位のdelta timeを掛けます。

現行Three.jsではClockがr183からdeprecatedで、代替としてTimerが用意されています。rendererはsetAnimationLoop()が推奨されています。

const timer = new THREE.Timer();
timer.connect(document);

const rotationSpeed = THREE.MathUtils.degToRad(45); // 45度/秒
const moveSpeed = 2; // 2 unit/秒

renderer.setAnimationLoop((timestamp) => {
  timer.update(timestamp);
  const delta = Math.min(timer.getDelta(), 0.1);

  mesh.rotation.y += rotationSpeed * delta;
  mesh.translateZ(moveSpeed * delta);

  renderer.render(scene, camera);
});

timer.connect(document)はPage Visibility APIを利用し、backgroundから戻ったときの大きなdeltaを避けます。さらに上限を設けると、debugger停止や重いframe後の極端な移動を抑えられます。

破棄時はloopとTimerを停止します。

renderer.setAnimationLoop(null);
timer.dispose();

scaleを往復させる

毎framescale.x += 0.01とすると無制限に大きくなります。elapsed timeから範囲を作ります。

timer.update(timestamp);

const elapsed = timer.getElapsed();
const scale = 1 + Math.sin(elapsed * 2) * 0.2;
mesh.scale.setScalar(scale);

この例では0.8〜1.2の間を往復します。

matrixAutoUpdateと手動matrix更新

Three.jsはdefaultで、position・quaternion・scaleからlocal matrixを自動計算し、親階層からworld matrixを更新します。

console.log(mesh.matrixAutoUpdate);      // true
console.log(mesh.matrixWorldAutoUpdate); // true

通常はこのままで構いません。大量の静的objectで明確な効果を計測できた場合だけ、自動更新を止めて手動管理します。

mesh.position.set(2, 0, 0);
mesh.matrixAutoUpdate = false;
mesh.updateMatrix();

matrixを直接変更する場合はmatrixAutoUpdate = falseが必要です。その後updateMatrix()を呼ぶとposition・quaternion・scaleから再構成され、直接変更したmatrixを上書きするため、2つの管理方法を混ぜないでください。

動かない・方向がおかしいときの確認項目

type is not definedになる

animation内のswitchで未定義変数を参照しています。操作modeをstateとして明示するか、最初はswitchを外して1つのtransformだけ動かします。

回転角度が大きすぎる

degreeをradianとして代入しています。THREE.MathUtils.degToRad()で変換します。

translateXの方向が想定と違う

translateXはobjectのlocal X axisです。AxesHelperをchildへ追加して向きを確認します。worldまたはparent軸で動かすならpositionやworld direction変換を使います。

positionは合っているのにworld位置が違う

parentのposition・rotation・scaleが合成されています。getWorldPosition()で確認し、scene graphを見直します。

lookAtの向きがずれる

modelのfront axisが異なる、up vectorが異なる、non-uniform scaleのparentがある可能性があります。wrapper Groupでmodel向きを補正すると分離しやすくなります。

geometryを共有する別Meshまで変わる

geometry.translate()geometry.scale()は共有vertex dataを変更します。object transformを使うか、Geometryをcloneしてから変更します。Object3Dとresource複製はThree.jsでobjectをcloneする方法を参照してください。

animation速度が端末で違う

frameごとの固定加算ではなく、Timerのdeltaを速度へ掛けます。物理simulationではfixed timestepも検討します。

OrbitControlsを動かすとobject transformが戻る

OrbitControlsはcamera transformとtargetを管理します。cameraを手動変更する処理と同時に使う場合は所有者を決めます。詳しくはThree.js OrbitControlsの使い方で確認できます。

transform実装チェックリスト

  • position・rotation・scaleはObject3Dのlocal transformと理解した
  • rotationへradianを渡した
  • Euler orderとQuaternionの役割を分けた
  • translateXとposition.xのaxisの違いを確認した
  • parent transformを含むworld値を確認した
  • AxesHelperでlocal axisを可視化した
  • geometry transformとobject transformを分けた
  • pivotまたはGroupで回転中心を設計した
  • animation速度へTimerのdeltaを掛けた
  • scaleへ終了範囲を設けた
  • matrix自動更新と手動更新を混在させていない

まとめ

Three.jsでobjectを配置するときは、position・rotationまたはquaternion・scaleを変更します。これらはparent基準のlocal transformで、world transformはscene graph全体の合成結果です。

degreeとradian、local axisとworld axis、object transformとgeometry vertex変更を分けてください。animationでは現行のTimerとrenderer.setAnimationLoop()を使い、delta timeを速度へ掛けることで、displayのrefresh rateに依存しない動きを作れます。