Three.jsでMeshやGroupを移動・回転・拡大縮小するときは、Object3Dのposition・rotation・scaleを変更します。
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に依存しない動きを作れます。