※スクリプト変更のため、ツールへのリンクが切れています。専用ページでお試し下さい。
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 }