トランスフォーム・コントロールをつけてみた

漫画漫文ノート・メモ

※スクリプト変更のため、ツールへのリンクが切れています。専用ページでお試し下さい。


クリックしてカーソルを動かすと、オービット・コントロールのおかげで、カメラ視点でいろんな角度や遠近の絵が得られます。

これだけでもある程度は遊べますが、やはり、キャラそのものを動かしたり回転させたりしてみたくなりますね。そこで今回はトランスフォーム・コントロールを導入しました。


トランスフォーム・コントロール

絵の右上角のボタンを押してフルスクリーンにすると操作がしやすいです。

画面のキャラ以外の部分をクリックすると、オービット・コントロールが効いてカメラ視点で絵が変わります。

キャラをクリックすると、オービットが無効になりキャラを動かすための移動ギズモが現れます。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 }


関連記事