Noh | エンジニア向け情報共有コミュニティ
Signup / Login

Three.jsの便利なハック集

javascript
typescript
threejs

投稿日: 2024/08/19

更新日: 2024/08/20

Three.jsを使った個人的に重宝しているコード集です。

ベクトルAからベクトルBの回転、FPSの固定、複数のカメラを同時にレンダリング、TransformControls の大きさをオブジェクトに対して固定する方法など。

ベクトルAからベクトルBの回転

追記

Three.jsにはデフォルトでこの機能がありました。

https://threejs.org/docs/#api/en/math/Quaternion.setFromUnitVectors

以下のように実行できます。

new THREE.Quaternion().setFromUnitVectors( fromVec.clone().normalize(), toVec.clone().normalize(), )

追記終わり

個人的に重宝してる関数。

import * as THREE from "three" export const quaternionAtoB = (a: THREE.Vector3, b: THREE.Vector3) => { const normalizedA = a.clone().normalize() const normalizedB = b.clone().normalize() // ベクトルAとベクトルBの間の軸と角度を計算 const axis = new THREE.Vector3() .crossVectors(normalizedA, normalizedB) .normalize() const angle = Math.acos(normalizedA.dot(normalizedB)) // 回転を表すクォータニオンを作成 const quaternion = new THREE.Quaternion().setFromAxisAngle(axis, angle) if (isNaN(quaternion.x) || isNaN(quaternion.y) || isNaN(quaternion.z)) { return new THREE.Quaternion() } return quaternion }

FPSを制限する

FPSLimitter のオブジェクトを作成して、レンダリングループの中でisRenderingを使ってレンダリングするか分岐させます。

export class FPSLimitter { maxFPS: number lastFrameTime: DOMHighResTimeStamp constructor(maxFPS: number) { this.maxFPS = maxFPS this.lastFrameTime = performance.now() } isRendering() { const now = performance.now() const frameDuration = 1000 / this.maxFPS const delta = now - this.lastFrameTime if (delta > frameDuration) { this.lastFrameTime = now - (delta % frameDuration) return true } return false } }
const fpsLimitter = new FPSLimitter(30) const animate = () => { if (fpsLimitter.isRendering()) { renderer.render(scene, camera) } requestAnimationFrame(animate) } animate()

FPSをログに表示

export class FPSLogger { frameCount: number prevTime: DOMHighResTimeStamp constructor() { this.frameCount = 0 this.prevTime = performance.now() } update() { const currentTime = performance.now() this.frameCount++ if (currentTime >= this.prevTime + 1000) { const fps = this.frameCount this.frameCount = 0 this.prevTime = currentTime console.log("FPS: " + fps) } } }
const fpsLogger = new FPSLogger() const animate = () => { fpsLogger.update() requestAnimationFrame(animate) } animate()

TransformControls の大きさをオブジェクトに対して固定する

ハックな解決方法になってしまう。
この関数を毎フレーム呼ぶ。

transformControlsPositionはGizmoの中心位置(ワールド座標)。transformControls.object.getWorldPosition(new THREE.Vector3())でも多分問題ない。

import * as THREE from "three" import { TransformControls } from "three/addons/controls/TransformControls.js" export const fixWorldSizeOfTransformControls = ( transformControls: TransformControls, transformControlsPosition: THREE.Vector3, camera: THREE.PerspectiveCamera, baseSize: number, ) => { const distance = camera.position.distanceTo(transformControlsPosition) const fov = (camera.fov * Math.PI) / 180 transformControls.setSize(baseSize / (Math.tan(fov / 2) * distance)) }

ダブルクリックでOrbitControlsが壊れる

複数のカメラを使っているときに発生する?
どういった場合に発生するか詳しく検証していないが、以下の方法で解決できた。

/* Disable text selection for all elements */ * { user-select: none; -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* Internet Explorer */ -webkit-user-select: none; /* Chrome, Safari, Opera */ } /* Allow text selection for elements with class "selectable" */ .selectable { user-select: auto; }

参考: https://discourse.threejs.org/t/orbitcontrols-double-click-breaks-with-multiple-scenes/54229/6

Reactでインラインスタイル指定するなら以下のように。

<div style={{ userSelect: "none", MozUserSelect: "none" /* Firefox */, msUserSelect: "none" /* Internet Explorer */, WebkitUserSelect: "none" /* Chrome, Safari, Opera */, }} >

複数のカメラを同時に表示する

複数のCanvasを使用するのはいい方法ではないです。

Canvasの上にDiv要素を重ね、その中にカメラに対応するDiv要素を並べてレンダリングします。

function resizeRendererToDisplaySize(renderer: WebGLRenderer) { const canvas = renderer.domElement const width = canvas.clientWidth const height = canvas.clientHeight const needResize = canvas.width !== width || canvas.height !== height if (needResize) { renderer.setSize(width, height, false) } return needResize } function setScissorForElement( renderer: WebGLRenderer, elem: HTMLDivElement | null, ) { const canvas = renderer.domElement const canvasRect = canvas.getBoundingClientRect() const elemRect = elem.getBoundingClientRect() // compute a canvas relative rectangle const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left const left = Math.max(0, elemRect.left - canvasRect.left) const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top const top = Math.max(0, elemRect.top - canvasRect.top) const width = Math.min(canvasRect.width, right - left) const height = Math.min(canvasRect.height, bottom - top) // setup the scissor to only render to that part of the canvas const positiveYUpBottom = canvasRect.height - bottom renderer.setScissor(left, positiveYUpBottom, width, height) renderer.setViewport(left, positiveYUpBottom, width, height) // return the aspect return width / height }
const renderer = new WebGLRenderer({ canvas: canvasRef.current, antialias: true, }) const scene = new Scene() const animate = () => { resizeRendererToDisplaySize(renderer) renderer.setScissorTest(true) context.main.camera.aspect = setScissorForElement( renderer, mainRef.current, ) context.main.camera.updateProjectionMatrix() renderer.render(scene, context.main.camera) context.sub.camera.aspect = setScissorForElement( renderer, subRef.current, ) context.sub.camera.updateProjectionMatrix() renderer.render(scene, context.sub.camera) requestAnimationFrame(animate) } animate()
<div style={{ position: "relative", width: "100vw", height: "100vh", }} > <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} /> <div ref={splitedRef} style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", display: "flex", }} > <div ref={subRef} style={{ width: "40%", height: "100%", }} ></div> <div ref={mainRef} style={{ width: "60%", height: "100%", }} ></div> </div> </div>

TransformControlsのGizmoObjectを取得

transformControls._gizmoで取得できなくはないが、プライベートな変数にアクセスするのはお行儀がよくない。個人的には以下のような感じで取得している。複数のGizmoをChildとして持たないはずなので問題はないが、少し変更に弱いコードになってしまう。

const transformControls = new TransformControls(camera, elem) const getTransformControlsGizmo = (transformControls: TransformControls) => { // return this._gizmo for (const child of transformControls.children) { if (child instanceof TransformControlsGizmo) { return child as TransformControlsGizmo } } return undefined } getTransformControlsGizmo(transformControls)
yosi

Noh を作ってるエンジニア。