5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Three.jsでworldToLocalを使うときはattachしよう

Posted at

ワールド座標をローカル座標に変換するためにworldToLocal1を利用したところ、意図しない結果になってハマりました。Three.jsのバージョンはr119です。

解決法

オブジェクトのシーンへの追加をattachにより行います。グループを利用する場合については後述します。

const scene = new THREE.Scene();

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshStandardMaterial({
        color: 0xFFD046
    })
);
cube.position.set(2, 3, 2);
scene.attach(cube); // これが重要!

// ワールド座標からローカル座標へ変換
const dir = cube.worldToLocal(
    new THREE.Vector3(-4, 0, 2)
);

attachとは

Three.jsのドキュメント(Object3D#attach)によれば

.attach ( object : Object3D ) : this
Adds object as a child of this, while maintaining the object's world transform.

とあり、ワールドでの変換を保持したまま子オブジェクトとして追加するものです。

オブジェクトのシーンへの追加はaddが広く用いられているようですが、attachでは内部的にaddを呼び出している2ためaddをする必要はありません。

attachの代わりにaddするとどうなるか

worldToLocalはオブジェクトのシーンへの追加方法によっては次の画像のように全く意図しない結果になります。

attachした場合(意図した結果) :smile: addした場合 :disappointed_relieved:

画像は黄色の箱(cube)から青色の箱(cube2)に向かう矢印を黄色の箱のローカル座標に描画したものです。意図した結果である左の画像に対して、右の画像では全く別の方向を指しています。コードは次に示す通りです。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js test</title>
    <style>
        html, body{ margin: 0; padding: 0; }
        canvas{ display: block; }
    </style>
</head>
<body>
    <script src="./main.js"></script>
</body>
</html>
main.ts
import * as THREE from "three";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";

// コンテナ
const container = document.createElement("div");
document.body.appendChild(container);

// レンダラー
const renderer = new THREE.WebGLRenderer({
    antialias: true
});
renderer.setSize( window.innerWidth, window.innerHeight );
container.appendChild(renderer.domElement);

// ワールド
const scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(5));
scene.add(new THREE.GridHelper(10, 10));

// カメラ
const camera = new THREE.PerspectiveCamera(
    50, window.innerWidth / window.innerHeight
);
camera.position.set(-8, 6, 8);

// コントロール
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// ライト
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(0, 5, 0);
scene.add(ambientLight, pointLight);

// オブジェクト
function generateCube(colorHex: THREE.Color){
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshStandardMaterial({
        color: colorHex
    });
    const cube = new THREE.Mesh(geometry, material);
    cube.add(new THREE.AxesHelper(2));
    return cube;
}
const cube = generateCube(new THREE.Color(0xFFD046));
cube.position.set(2, 3, 2);
cube.rotateX(THREE.MathUtils.degToRad(60));
cube.rotateY(THREE.MathUtils.degToRad(90));
scene.attach(cube); // これが重要!

const cube2 = generateCube(new THREE.Color(0x003F91));
cube2.position.set(-4, 0, -2);
scene.add(cube2);

// 座標変換
const dir = cube.worldToLocal(
    cube2.position.clone()
);
const length = dir.length();
dir.normalize();
const arrowLocal = new THREE.ArrowHelper(
    dir,
    new THREE.Vector3(0, 0, 0), // origin
    length,
    0xff0000
);
cube.add(arrowLocal);

function animate(){
    controls.update();
    renderer.render(scene, camera);
    requestAnimationFrame(() => animate());
}

animate();

いろいろ試したところ、cubeを回転させず移動のみの場合はaddでも矢印は青い箱に刺さるように表示されました。

グループを利用する場合

黄色い箱をグループに入れるような場合はグループのattachも必要になります。画像は先の例と同様に黄色の箱から青色の箱に向かう矢印を描画したものです。グループにはAxesHelperと黄色の箱を追加しています。

attachした場合(意図した結果) :smile: addした場合 :disappointed_relieved:
good_group.jpg bad_group.jpg

先の例では黄色い箱はシーンにattachしていましたが、今回はグループが親になるため、グループへのattachを行います。座標変換はグローバル→グループ→オブジェクトの順に行います。

main.ts(改変した部分)
...
// オブジェクト
function generateCube(colorHex: THREE.Color){
    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshStandardMaterial({
        color: colorHex
    });
    const cube = new THREE.Mesh(geometry, material);
    cube.add(new THREE.AxesHelper(2));
    return cube;
}
const cube = generateCube(new THREE.Color(0xFFD046));
cube.position.set(2, 3, 2);
cube.rotateX(THREE.MathUtils.degToRad(60));
cube.rotateY(THREE.MathUtils.degToRad(90));
// scene.attach(cube); シーンにはattachしません

// グループ
const group = new THREE.Group();
group.position.set(1, 1, 1);
group.rotateX(THREE.MathUtils.degToRad(60));
group.rotateY(THREE.MathUtils.degToRad(45));

group.add(new THREE.AxesHelper(2));
group.attach(cube);  // グループ <- cube
scene.attach(group); // シーン <- グループ

const cube2 = ...
...
// 座標変換
const dir = cube.worldToLocal(
    group.worldToLocal(cube2.position.clone())
);
...

参考

  1. https://threejs.org/docs/#api/en/core/Object3D.worldToLocal

  2. https://github.com/mrdoob/three.js/blob/r119/src/core/Object3D.js#L393

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?