※スクリプト変更のため、ツールへのリンクが切れています。専用ページでお試し下さい。
クリックしてカーソルを動かすと、オービット・コントロールのおかげで、カメラ視点でいろんな角度や遠近の絵が得られます。
これだけでもある程度は遊べますが、やはり、キャラそのものを動かしたり回転させたりしてみたくなりますね。そこで今回はトランスフォーム・コントロールを導入しました。
トランスフォーム・コントロール
絵の右上角のボタンを押してフルスクリーンにすると操作がしやすいです。
画面のキャラ以外の部分をクリックすると、オービット・コントロールが効いてカメラ視点で絵が変わります。
キャラをクリックすると、オービットが無効になりキャラを動かすための移動ギズモが現れます。XYZ方向、黄色でハイライトされている方向にキャラを移動できます。
キャラをダブルクリックすると回転ギズモが現れます。黄色でハイライトされた円が表す軸に沿って、キャラが回転します。
いずれも、キャラ以外の場所をクリックすると、トランスフォーム・コントロールが停止し、オービット・コントロールが復活します。
キャラを元の位置に戻し回転をリセットするには、ページを再読込して下さい。
index.htmlから呼び込むmain.js
/* mmNoteツール メインスクリプト */
'use strict';
import * as v3d from "/path-to/v3d.module.js"
import { createPreloader, createCustomPreloader } from "/path-to-sub/preloader.js"
import { prepareFullscreen } from "/path-to-sub/fullscreen.js"
import { camera_fit } from "/path-to-sub/camera_fit.js"
import { gizmo } from "/path-to-sub/gizmo.js"
// ページ読込の後、createApp()を実行
window.addEventListener('load', e => {
const params = v3d.AppUtils.getPageParams()
createApp({
containerId: 'v3d-container',
fsButtonId: 'fullscreen-button',
txButtonId: 'paint-button',
sceneURL: 'mmNote.gltf',
})
})
// createApp()
// ①initOptionsでctxSettings (WebGL属性。canvas.getContext()に渡される)
// ②フルスクリーンとプリローダを組み込む
// ③app実行
async function createApp({ containerId, fsButtonId, txButtonId, sceneURL }) {
let initOptions = {
useFullscreen: true,
useBkgTransp: false,
useCompAssets: false,
}
// フルスクリーンとプリローダのコンストラクタ
const disposeFullscreen = prepareFullscreen(containerId, fsButtonId,
initOptions.useFullscreen)
const preloader = createPreloader(containerId, initOptions)
// Firefoxのスクリーンキャプチャに必要
const ctxSettings = {}
ctxSettings.preserveDrawingBuffer = true
// Appインスタンス作成
const app = new v3d.App(containerId, ctxSettings, preloader)
// フルスクリーンの配置
app.addEventListener('dispose', () => disposeFullscreen?.())
// app実行
app.loadScene(sceneURL, () => {
app.enableControls()
app.renderer.gammaOutput = true // ASUSタブレットで真っ黒に対処? 効かないかも
// シーン内のキャラを拾う
const objList = app.scene.children
let i = 0
let modelList = []
while (i < objList.length) {
let thisObj = objList[i]
if ( thisObj.name.startsWith("Retopo_") ) {
modelList.push(thisObj)
}
i++
}
// カメラ・フィット
camera_fit(app, v3d, modelList[0])
// ギズモ
gizmo(app, v3d, modelList)
app.run()
})
return { app }
}
main.jsから呼び込むサブモジュール camera_fit.js
// https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
'use strict'
function camera_fit(app, v3d, targetObj) {
const obj = targetObj
const camera = app.camera
const renderer = app.renderer
const scene = app.scene
const orbit = app.controls
// カメラの数値を確認
//console.log('position\n', camera.position)
//console.log('lookAt\n', camera.lookAt)
//console.log('fov\n', camera.fov)
//console.log('aspect\n', camera.aspect)
// 表示ウィンドウのサイズを取得
const width = window.innerWidth
const height = window.innerHeight
//console.log(width / height) // = camera.aspect
// レンダラーが描画するキャンバスサイズの設定
renderer.setSize (width, height)
// 視野計算
let vFoV = camera.getEffectiveFOV()
let hFoV = camera.fov * camera.aspect
let FoV = Math.min(vFoV, hFoV) // 縦・横の小さい方
let FoV2 = FoV / 2
let dir = new v3d.Vector3()
camera.getWorldDirection(dir) //方向をワールド座標軸へ?
// バウンディング
// ボックス
let bb = new v3d.Box3()
bb.setFromObject( obj )
// スフェア
let center = new v3d.Vector3()
let bs = bb.getBoundingSphere(new v3d.Sphere(center))
// バウンディング・スフェアのクローンをワールド中心に配置
let bsWorld = bs.center.clone()
// objの座標をワールドへ
obj.localToWorld(bsWorld)
// 計算式
let th = FoV2 * Math.PI / 180.0 // target height?
let sina = Math.sin(th) // サインA
let R = bs.radius // スフェアの半径
let FL = R / sina // フィールド・レングス?
// カメラ方向のベクトル
let cameraDir = new v3d.Vector3()
camera.getWorldDirection(cameraDir)
// カメラ・オフセット
let cameraOffs = cameraDir.clone()
cameraOffs.multiplyScalar(-FL)
let newCameraPos = bsWorld.clone().add(cameraOffs)
// カメラの位置と凝視物のセット
camera.position.copy(newCameraPos)
camera.lookAt(bsWorld)
orbit.targetObj.position.x = bsWorld.x
orbit.targetObj.position.y = bsWorld.y
orbit.targetObj.position.z = bsWorld.z
orbit.update()
}
export { camera_fit }
main.jsから呼び込むサブモジュール gizmo.js
// Transform control gizmo
// キャラをクリックしたら表示、同時にオービットコントロールを止める
// 表示されている状態で他の場所をクリックしたら消える、オービットコントロールが戻る
import { TransformControls } from '/path-to-sub/TransformControls-v3d.js'
function gizmo(app, v3d, modelList) {
const tConVisible = false
const renderer = app.renderer
const scene = app.scene
const camera = app.camera
const tCon = new TransformControls(camera, renderer.domElement)
// 原点周りのギズモを表示させない
tCon._gizmo.children[1].disableChildRendering = true // 回転
tCon._gizmo.children[2].disableChildRendering = true // スケール
tCon._gizmo.children[3].disableChildRendering = true // ギズモの目安
tCon._gizmo.children[4].disableChildRendering = true // 目安の円
tCon._gizmo.children[5].disableChildRendering = true // 目安の十字
//tCon._gizmo.children[6].disableChildRendering = true // ギズモの補助線
//tCon._gizmo.children[7].disableChildRendering = true // 不明
//tCon._gizmo.children[8].disableChildRendering = true // 不明
const raycaster = new v3d.Raycaster()
let mouse = { x : 0, y : 0 }
renderer.domElement.addEventListener( 'click', detectModel, false )
renderer.domElement.addEventListener( 'dblclick', detectModel, false )
/* def detectModel(): */
function detectModel( e ) {
if (e.type == 'dblclick') {
tCon.mode = 'rotate'
tCon._gizmo.children[1].disableChildRendering = false
}
else {
tCon.mode = 'translate'
tCon._gizmo.children[1].disableChildRendering = true
}
mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera )
let intersects = raycaster.intersectObjects( modelList )
if ( intersects.length == 0 ) {
app.controls.enabled = true
scene.remove(tCon)
}
for ( var i = 0; i < modelList.length; i++) {
let bingo = raycaster.intersectObject( modelList[i] )
if ( bingo.length != 0) {
tControl( modelList[i] )
}
}
}
/* def tControl(): */
function tControl( targetModel ) {
// オービット・コントロールを停止
app.controls.enabled = false
tCon.setSpace('local')
tCon.attach( targetModel )
scene.add( tCon )
}
}
export { gizmo }