スケルトン・ヘルパーを入れてみた

漫画漫文ノート・メモ

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


7月5日の記事で、オブジェクトにトランスフォーム・コントロールを付けたことを書きましたが、当初はオブジェクトが移動・回転できればいいと思っていました。でも、いじっているうちに「やっぱり身体の各部を動かしてポーズをつけたいよね」と欲が出て、スケルトン・ヘルパーという機能も実装したくなりました。

どうにか動いたので、htmlとjsを掲載します。

08月08日の更新でjsを書き換えましたので、ダルマさんのスケルトンは動かなくなりました。この記事をどうしようか迷いましたが、そのときどきの記録として文もコードもそのまま掲載しておきます。


スケルトン・ヘルパー

絵の左上角にカーソルをあてると、ポーズ・ボタンが現れます。画面内のモデルがクリックされてトランスフォーム・コントロールが表示されている状態であれば、ボタンを押すとボーン選択のメニューが出ます。 (ただし、クリックしたモデルがスケルトンを持っている場合です。画面では、2頭身の姫だるまには骨が仕込んでありますが、おにぎり頭は骨がなく、この場合はメニューが出ません)

メニュー内の操作したいボーン名をクリックすると、緑から青へのグラデーションの色がついたスケルトン・ヘルパーが表示され、選択されたボーンが 回転ギズモで操作できるようになります。

メニューとスケルトン・ヘルパーを消すには"Done"をクリックして下さい。

ページ再読込でリセットされます。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
<title>漫画漫文ノート by Moonlight Workshop</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  <link rel="stylesheet" type="text/css" href="/css/mystyle.css">
</head>
<body>
  <div id="v3d-container">
    <div id="fullscreen-button" class="fullscreen-open" title="Toggle fullscreen mode"></div>
    <div id="pose-button" class="pose-button"></div>
    <!-- div id="paint-button" class="paint-button"></div -->
  </div>
  <script type="module" src="main.js"></script>
</body>
</html>

index.htmlから呼び出すmain.js

/* mmNoteツール メインスクリプト */

'use strict';
import * as v3d from "/path-to-js/v3d.module.js"
import { createPreloader, createCustomPreloader } from "/path-to-js/mlw/preloader.js"
import { prepareFullscreen } from "/path-to-js/mlw/fullscreen.js"
import { camera_fit } from "/path-to-js/mlw/camera_fit.js"
import { gizmo } from "/path-to-js/mlw/gizmo.js"
import { bono } from "/path-to-js/mlw/bono.js"

// ページ読込の後、createApp()を実行
window.addEventListener('load', e => {
    const params = v3d.AppUtils.getPageParams()
    createApp({
        containerId:    'v3d-container',
        fsButtonId:     'fullscreen-button',
        txButtonId:     'paint-button',
        poButtonId:     'pose-button',
        sceneURL:       'mmNote.gltf',
        })
    })

// createApp()
// ①initOptionsでctxSettings (WebGL属性。canvas.getContext()に渡される)
// ②フルスクリーンとプリローダを組み込む
// ③app実行
async function createApp({ containerId, fsButtonId, txButtonId, poButtonId, 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()

        // グリッド
        const grid = new v3d.GridHelper(50, 50, 0x000000, 0x000000)
        grid.material.opacity = 0.2
        grid.material.transparent = true
        app.scene.add(grid)

        // シーン内のキャラを拾う
        const objList = app.scene.children
        let i = 0
        let modelList = []
        while (i < objList.length) {
            let thisObj = objList[i]
            // Retpo_メッシュの場合もある(骨がないキャラまたは物体)
            if (thisObj.name.startsWith("Armature") || thisObj.name.startsWith("Retopo_")) {
                modelList.push(thisObj)
                }
            i++
            }

        // カメラ・フィット:最初のモデルにフォーカス
        camera_fit(app, v3d, modelList[0])

        // ギズモ
        gizmo(app, v3d, modelList)

        // 漫画ツール呼び出し
        bono(app, v3d, poButtonId, modelList)
        //paint(app, v3d, txButtonId)

        // 実行
        app.run()
        })
    return { app }
    }

main.jsから呼び出すボーン操作スクリプト

// Pose control
// ボーンを表示してキャラのポーズを操作する
// スクリプト名は「ザ・ストレイン」のフェットがクインランを呼ぶ時の愛称^^

import { SkeletonHelper } from '/path-to-js/v3d.module.js'
import { TransformControls } from '/path-to-js/mlw/TransformControls-v3d.js'
import { GUI } from '/path-to-js/jsm/libs/lil-gui.module.min.js'

function bono(app, v3d, poButtonId, modelList) {
    const bttn = document.getElementById(poButtonId)
    bttn.addEventListener( 'click', chkObj, false )

    const renderer = app.renderer
    const scene = app.scene
    const camera = app.camera
    let Armtr

    function chkObj() {
        if ( !app.controls.enabled ) {
            for  ( var i = 0; i < modelList.length; i++ ) {
                if ( modelList[i].tConAttached ) {
                    if ( modelList[i].type === 'Bone' ) {
                        Armtr = modelList[i]
                        doPose(Armtr)
                        }
                    }
                }
            }
        }

    function doPose(Armtr) {
        // ボーン回転ギズモの作成
        const bRot = new TransformControls(camera, renderer.domElement)
        bRot.setSpace('local')
        bRot.setMode('rotate')
        // 原点周りのギズモを表示させない
        bRot._gizmo.children[0].disableChildRendering = true    // 移動
        //bRot._gizmo.children[1].disableChildRendering = true    // 回転
        bRot._gizmo.children[2].disableChildRendering = true    // スケール
        bRot._gizmo.children[3].disableChildRendering = true    // ギズモの目安
        bRot._gizmo.children[4].disableChildRendering = true    // 目安の円(黄色)
        bRot._gizmo.children[5].disableChildRendering = true    // 目安の十字(黄色)
        bRot._gizmo.children[6].disableChildRendering = true  // ギズモの補助線
        bRot._gizmo.children[7].disableChildRendering = true  // 不明
        bRot._gizmo.children[8].disableChildRendering = true  // 不明

        const helper = new SkeletonHelper( Armtr )
        helper.material.linewidth = 20      // Firefoxでは太くならない ;_;
        scene.add( helper )
        // ヘルパー内のボーンを拾う
        const boneList = helper.bones
        const targetBones = []
        // 全体オブジェクトとダミーは対象にしない
        for ( var j = 0; j < boneList.length; j++ ) {
            if ( j > 0 && !boneList[j].name.includes('Dummy') ) {
                targetBones.push(boneList[j])
                }
            }
        // ボーンを選択して回転ヘルパーを出す
        // (ボーンの選択はレイキャストでは出来ない。周りのメッシュを拾ってしまう)
        // (GUIメニューはフルスクリーンでは表示されないが、実用上問題ないと思う)
        const gui = new GUI( { width: 200 } )
        const menuDict = {}
        for ( var k = 0; k < targetBones.length; k++ ) {
            let val = k
            menuDict[targetBones[k].name] = () => { bRot.attach(targetBones[val]); scene.add( bRot ) }
            gui.add(menuDict, targetBones[k].name)
            }
        const quitButton = { 'Done': function() {
            scene.remove( helper ), scene.remove( bRot ), bRot.detach(targetBones), gui.destroy()
            } }
        gui.add(quitButton, 'Done')
        }           // doPoseの終り
    }               // bonoの終り

export { bono }


関連記事