izuemon commited on
Commit
033c32a
·
verified ·
1 Parent(s): 2eb5894

Update test/vbhmodel.html

Browse files
Files changed (1) hide show
  1. test/vbhmodel.html +981 -461
test/vbhmodel.html CHANGED
@@ -4,6 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>BVH Foot Orientation Fixer</title>
 
7
  <style>
8
  body {
9
  margin: 0;
@@ -14,6 +15,7 @@
14
  height: 100vh;
15
  overflow: hidden;
16
  }
 
17
  #sidebar {
18
  width: 380px;
19
  padding: 16px;
@@ -24,23 +26,28 @@
24
  flex-direction: column;
25
  gap: 12px;
26
  }
 
27
  #canvas-container {
28
  flex: 1;
29
  position: relative;
30
  background: #1a1a2e;
31
  min-width: 0;
32
  }
 
33
  h1, h2 {
34
  margin: 0;
35
  }
 
36
  h1 {
37
  font-size: 1.3em;
38
  color: #e94560;
39
  }
 
40
  h2 {
41
  font-size: 1.05em;
42
  margin-top: 8px;
43
  }
 
44
  input[type="file"] {
45
  width: 100%;
46
  padding: 8px;
@@ -50,6 +57,7 @@
50
  border-radius: 4px;
51
  box-sizing: border-box;
52
  }
 
53
  .bone-list {
54
  border: 1px solid #0f3460;
55
  border-radius: 6px;
@@ -58,6 +66,7 @@
58
  overflow-y: auto;
59
  background: #0a0f1c;
60
  }
 
61
  .bone-item {
62
  display: flex;
63
  align-items: center;
@@ -65,11 +74,13 @@
65
  padding: 3px 0;
66
  font-size: 0.9em;
67
  }
 
68
  .bone-item.foot-selected {
69
  background: rgba(233, 69, 96, 0.2);
70
  border-radius: 3px;
71
  padding-left: 6px;
72
  }
 
73
  .bone-controls {
74
  border: 1px solid #e94560;
75
  border-radius: 6px;
@@ -77,22 +88,26 @@
77
  margin: 8px 0;
78
  background: #0f3460;
79
  }
 
80
  .bone-controls .legend-like {
81
  font-weight: bold;
82
  color: #e94560;
83
  margin-bottom: 8px;
84
  }
 
85
  .checkbox-group {
86
  display: grid;
87
  grid-template-columns: 1fr 1fr;
88
  gap: 4px;
89
  }
 
90
  .checkbox-item {
91
  display: flex;
92
  align-items: center;
93
  gap: 4px;
94
  font-size: 0.85em;
95
  }
 
96
  button {
97
  padding: 10px;
98
  background: #e94560;
@@ -103,28 +118,35 @@
103
  font-weight: bold;
104
  font-size: 0.9em;
105
  }
 
106
  button:hover {
107
  background: #ff6b7a;
108
  }
 
109
  button:disabled {
110
  background: #555;
111
  cursor: not-allowed;
112
  }
 
113
  .control-buttons {
114
  display: flex;
115
  gap: 8px;
116
  }
 
117
  .control-buttons button {
118
  flex: 1;
119
  }
 
120
  .pause-btn {
121
  background: #0f3460;
122
  border: 1px solid #e94560;
123
  }
 
124
  .info {
125
  font-size: 0.85em;
126
  color: #aaa;
127
  }
 
128
  .status {
129
  font-size: 0.9em;
130
  padding: 6px;
@@ -132,6 +154,7 @@
132
  border-radius: 4px;
133
  min-height: 20px;
134
  }
 
135
  .debug-info {
136
  font-size: 0.8em;
137
  color: #4ecca3;
@@ -144,605 +167,1102 @@
144
  }
145
  </style>
146
  </head>
 
147
  <body>
148
- <div id="sidebar">
149
- <h1>BVH Foot Inverter</h1>
150
- <input type="file" id="fileInput" accept=".bvh" />
151
 
152
- <div id="status" class="status">ファイルをアップロードしてください</div>
153
 
154
- <div class="control-buttons">
155
- <button id="playBtn" disabled>▶ 再生</button>
156
- <button id="pauseBtn" disabled class="pause-btn">⏸ 一時停止</button>
157
- </div>
158
 
159
- <h2>ボーン一覧(高さ250px)</h2>
160
- <div id="boneList" class="bone-list">
161
- <p class="info">ファイルをアップロードしてください</p>
162
- </div>
163
 
164
- <h2>反転設定(足ボーンのみ)</h2>
165
- <div id="footControls">
166
- <p class="info">足ボーンを選択すると反転設定が表示されます</p>
167
- </div>
 
 
 
 
168
 
169
- <button id="exportBtn" disabled>修正BVHをダウンロード</button>
170
- <button id="resetBtn" disabled>すべての反転をリセット</button>
171
 
172
- <div id="debugInfo" class="debug-info"></div>
 
173
  </div>
174
- <div id="canvas-container"></div>
175
 
176
- <script type="importmap">
177
- {
178
- "imports": {
179
- "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
180
- "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
181
- }
182
- }
183
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- <script type="module">
186
- import * as THREE from 'three';
187
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
188
- import { BVHLoader } from 'three/addons/loaders/BVHLoader.js';
189
 
190
- // ---------- グローバル変数 ----------
191
- let parsedBVH = null;
192
- let selectedFootBones = new Set();
193
- const inversionSettings = new Map();
194
 
195
- let mixer = null;
196
- let animationAction = null;
197
- let isPlaying = true;
198
- let clock = new THREE.Clock();
199
 
200
  let currentPreviewRoot = null;
 
201
  let skeletonHelper = null;
202
- let currentScale = 1;
203
- // デバッグ表示用
204
- function debug(msg) {
205
- const debugDiv = document.getElementById('debugInfo');
206
- debugDiv.innerHTML += msg + '<br>';
207
- debugDiv.scrollTop = debugDiv.scrollHeight;
208
- console.log(msg);
209
- }
210
 
211
- function createDefaultSettings() {
212
- return {
213
- offsetX: false, offsetY: false, offsetZ: false,
214
- posX: false, posY: false, posZ: false,
215
- rotX: false, rotY: false, rotZ: false
216
- };
217
- }
218
 
219
- // Three.js シーン初期化
220
- const container = document.getElementById('canvas-container');
 
221
 
222
- const scene = new THREE.Scene();
223
- scene.background = new THREE.Color(0x1a1a2e);
224
 
225
- scene.add(new THREE.AmbientLight(0xffffff, 0.7));
226
- const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
227
- dirLight.position.set(1, 2, 1);
228
- scene.add(dirLight);
229
 
230
- const gridHelper = new THREE.GridHelper(10, 40, 0x333333, 0x222222);
231
- scene.add(gridHelper);
232
 
233
- const camera = new THREE.PerspectiveCamera(
234
- 50,
235
- Math.max(container.clientWidth, 1) / Math.max(container.clientHeight, 1),
236
- 0.01,
237
- 100
238
- );
239
- camera.position.set(0, 1.4, 3);
240
- camera.lookAt(0, 0.8, 0);
241
-
242
- const renderer = new THREE.WebGLRenderer({ antialias: true });
243
- renderer.setSize(container.clientWidth, container.clientHeight);
244
- renderer.setPixelRatio(window.devicePixelRatio);
245
- renderer.outputColorSpace = THREE.SRGBColorSpace;
246
- container.appendChild(renderer.domElement);
247
-
248
- const controls = new OrbitControls(camera, renderer.domElement);
249
- controls.target.set(0, 0.8, 0);
250
- controls.update();
251
-
252
- window.addEventListener('resize', () => {
253
- const w = Math.max(container.clientWidth, 1);
254
- const h = Math.max(container.clientHeight, 1);
255
- camera.aspect = w / h;
256
- camera.updateProjectionMatrix();
257
- renderer.setSize(w, h);
258
- });
259
 
260
- function animate() {
261
- requestAnimationFrame(animate);
262
 
263
- const delta = clock.getDelta();
264
- if (mixer && isPlaying) {
265
- mixer.update(delta);
266
- }
267
 
268
- if (currentPreviewRoot) {
269
- currentPreviewRoot.updateMatrixWorld(true);
270
- }
271
- if (skeletonHelper) {
272
- skeletonHelper.update();
 
 
 
 
 
 
 
 
 
 
273
  }
274
 
275
- controls.update();
276
- renderer.render(scene, camera);
277
- }
278
- animate();
279
 
280
- class ColoredSkeletonHelper extends THREE.LineSegments {
281
- constructor(rootBone, footBoneNames) {
 
282
 
283
- const geometry = new THREE.BufferGeometry();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
 
285
- const material = new THREE.LineBasicMaterial({
286
- vertexColors: true,
287
- toneMapped: false
288
- });
289
 
290
- super(geometry, material);
 
 
 
 
 
 
 
 
291
 
292
  this.rootBone = rootBone;
 
293
  this.footBoneNames = footBoneNames;
294
 
295
- this.positions = [];
296
- this.colors = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
- this.positionAttr = null;
299
- this.colorAttr = null;
 
 
 
 
 
 
 
300
 
301
- this.tempVecA = new THREE.Vector3();
302
- this.tempVecB = new THREE.Vector3();
 
 
 
 
 
 
 
303
 
304
  this.update();
305
  }
306
 
307
  update() {
308
 
309
- this.positions.length = 0;
310
- this.colors.length = 0;
 
 
311
 
312
- this.rootBone.updateWorldMatrix(true, true);
313
 
314
- this.rootBone.traverse((bone) => {
315
 
316
- if (!bone.isBone) return;
317
 
318
- const settings = inversionSettings.get(bone.name);
319
 
320
- if (!settings) return;
321
- if (!selectedFootBones.has(bone.name)) return;
322
 
323
- if (settings.offsetX) bone.position.x *= -1;
324
- if (settings.offsetY) bone.position.y *= -1;
325
- if (settings.offsetZ) bone.position.z *= -1;
326
- });
327
 
328
- this.geometry.setAttribute(
329
- 'position',
330
- new THREE.Float32BufferAttribute(this.positions, 3)
331
- );
332
 
333
- this.geometry.setAttribute(
334
- 'color',
335
- new THREE.Float32BufferAttribute(this.colors, 3)
336
- );
 
 
 
 
 
 
337
 
338
- this.geometry.computeBoundingSphere();
 
 
 
 
 
339
  }
340
  }
341
 
342
- // ---------- プレビュー更新 ----------
343
- function disposePreview() {
344
- if (skeletonHelper) {
345
- scene.remove(skeletonHelper);
346
- skeletonHelper.geometry?.dispose?.();
347
- skeletonHelper.material?.dispose?.();
348
- skeletonHelper = null;
349
- }
350
 
351
- if (currentPreviewRoot) {
352
- scene.remove(currentPreviewRoot);
353
- currentPreviewRoot.traverse((obj) => {
354
- if (obj.isMesh) {
355
- obj.geometry?.dispose?.();
356
- if (Array.isArray(obj.material)) {
357
- obj.material.forEach((m) => m.dispose?.());
358
- } else {
359
- obj.material?.dispose?.();
360
- }
361
- }
362
- });
363
- currentPreviewRoot = null;
 
364
  }
365
 
366
- if (mixer) {
367
- mixer.stopAllAction();
368
- mixer = null;
369
- animationAction = null;
 
 
 
 
 
 
 
 
370
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
 
373
- function updatePreview() {
374
- debug('updatePreview 開始');
 
 
375
 
376
- disposePreview();
 
 
377
 
378
- if (!parsedBVH) {
379
- debug('parsedBVH が null です');
380
- return;
381
- }
382
 
383
- try {
384
- const origSkeleton = parsedBVH.skeleton;
385
- const origClip = parsedBVH.clip;
386
 
387
- debug(`元のスケルトン: ${origSkeleton.bones.length} ボーン, クリップ長: ${origClip.duration.toFixed(2)}秒`);
388
 
389
- // ルートボーンを複製
390
- const rootBone = origSkeleton.bones[0].clone();
391
 
392
- // ルートボーン自体のオフセット反転
393
- const rootSettings = inversionSettings.get(origSkeleton.bones[0].name);
394
- if (rootSettings && selectedFootBones.has(origSkeleton.bones[0].name)) {
395
- if (rootSettings.offsetX) rootBone.position.x *= -1;
396
- if (rootSettings.offsetY) rootBone.position.y *= -1;
397
- if (rootSettings.offsetZ) rootBone.position.z *= -1;
 
 
 
 
 
 
398
  }
399
 
400
- // アニメーショントラックを修正
401
- const modifiedTracks = origClip.tracks.map((track) => {
402
- const parts = track.name.split('.');
403
- const boneName = parts[0];
404
- const property = parts[1];
405
- const settings = inversionSettings.get(boneName);
406
 
407
- if (!settings || !selectedFootBones.has(boneName)) {
408
- return track;
409
- }
 
 
410
 
411
- if (property === 'position' && track instanceof THREE.VectorKeyframeTrack) {
412
- const values = [...track.values];
413
- for (let i = 0; i < values.length; i += 3) {
414
- if (settings.posX) values[i] *= -1;
415
- if (settings.posY) values[i + 1] *= -1;
416
- if (settings.posZ) values[i + 2] *= -1;
417
- }
418
- return new THREE.VectorKeyframeTrack(track.name, [...track.times], values);
 
 
 
 
 
 
 
 
 
419
  }
420
 
421
- if (property === 'quaternion' && track instanceof THREE.QuaternionKeyframeTrack) {
422
- const values = [...track.values];
423
- for (let i = 0; i < values.length; i += 4) {
424
- const quat = new THREE.Quaternion(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  values[i],
426
  values[i + 1],
427
  values[i + 2],
428
  values[i + 3]
429
  );
430
 
431
- const euler = new THREE.Euler().setFromQuaternion(quat, 'YXZ');
432
- if (settings.rotX) euler.x *= -1;
433
- if (settings.rotY) euler.y *= -1;
434
- if (settings.rotZ) euler.z *= -1;
 
 
 
 
 
435
 
436
- quat.setFromEuler(euler);
 
437
 
438
- values[i] = quat.x;
439
- values[i + 1] = quat.y;
440
- values[i + 2] = quat.z;
441
- values[i + 3] = quat.w;
442
- }
443
- return new THREE.QuaternionKeyframeTrack(track.name, [...track.times], values);
 
 
 
444
  }
445
 
446
- return track;
447
- });
 
 
 
 
448
 
449
- const modifiedClip = new THREE.AnimationClip(
450
- origClip.name,
451
- origClip.duration,
452
- modifiedTracks
453
- );
454
- // シーンに追加
455
- currentPreviewRoot = new THREE.Group();
456
- currentPreviewRoot.name = 'BVH_PreviewRoot';
457
- currentPreviewRoot.add(rootBone);
458
-
459
- // -------------------------
460
- // 自動スケール補正
461
- // -------------------------
462
- rootBone.updateMatrixWorld(true);
463
-
464
- const box = new THREE.Box3().setFromObject(rootBone);
465
- const size = new THREE.Vector3();
466
- box.getSize(size);
467
-
468
- const maxSize = Math.max(size.x, size.y, size.z);
469
-
470
- // 人型が大体 1.8 くらいになるよう補正
471
- if (maxSize > 0) {
472
- currentScale = 1.8 / maxSize;
473
- } else {
474
- currentScale = 1;
475
- }
476
 
477
- currentPreviewRoot.scale.setScalar(currentScale);
 
 
 
 
 
478
 
479
- // 地面位置補正
480
- rootBone.updateMatrixWorld(true);
 
481
 
482
- const fixedBox = new THREE.Box3().setFromObject(currentPreviewRoot);
 
483
 
484
- currentPreviewRoot.position.y = -fixedBox.min.y;
485
 
486
- scene.add(currentPreviewRoot);
487
 
488
- // カスタムスケルトンヘルパー
489
- skeletonHelper = new ColoredSkeletonHelper(rootBone, selectedFootBones);
490
- scene.add(skeletonHelper);
491
 
492
- // アニメーション開始
493
- mixer = new THREE.AnimationMixer(rootBone);
494
- animationAction = mixer.clipAction(modifiedClip);
495
- animationAction.play();
496
- isPlaying = true;
497
 
498
- document.getElementById('playBtn').disabled = true;
499
- document.getElementById('pauseBtn').disabled = false;
500
 
501
- debug('プレビュー更新成功');
502
- } catch (error) {
503
- debug('エラー: ' + error.message);
504
- console.error('Update preview error:', error);
505
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  }
507
 
508
- // ---------- ファイル処理 ----------
509
- async function handleFile(file) {
510
- document.getElementById('status').textContent = '読み込み中...';
511
- document.getElementById('debugInfo').innerHTML = '';
512
- debug('ファイル読み込み: ' + file.name);
513
-
514
- const text = await file.text();
515
- const loader = new BVHLoader();
516
-
517
- try {
518
- const result = loader.parse(text);
519
- parsedBVH = result;
520
- debug('BVH解析成功');
521
-
522
- // 初期設定
523
- selectedFootBones.clear();
524
- inversionSettings.clear();
525
-
526
- // foot を含むボーンを自動選択
527
- result.skeleton.bones.forEach((bone) => {
528
- if (bone.name && bone.name.toLowerCase().includes('foot')) {
529
- selectedFootBones.add(bone.name);
530
- inversionSettings.set(bone.name, createDefaultSettings());
531
- debug(`足ボーン自動選択: ${bone.name}`);
532
- }
533
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
 
535
- buildBoneList();
536
- buildFootControls();
537
- updatePreview();
538
-
539
- document.getElementById('exportBtn').disabled = false;
540
- document.getElementById('resetBtn').disabled = false;
541
- document.getElementById('status').textContent =
542
- `読み込み完了: ${result.skeleton.bones.length} ボーン, ${result.clip.duration.toFixed(2)}秒`;
543
- } catch (error) {
544
- debug('解析エラー: ' + error.message);
545
- document.getElementById('status').textContent = 'エラー: ' + error.message;
546
- console.error(error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  }
548
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
 
550
- // ---------- UI構築 ----------
551
- function buildBoneList() {
552
- const boneListDiv = document.getElementById('boneList');
553
- boneListDiv.innerHTML = '';
554
 
555
- if (!parsedBVH) return;
556
 
557
- const bones = parsedBVH.skeleton.bones;
558
- debug(`ボーン一覧構築: ${bones.length} ボーン`);
559
 
560
- bones.forEach((bone, index) => {
561
- const isFoot = selectedFootBones.has(bone.name);
562
- const item = document.createElement('div');
563
- item.className = 'bone-item' + (isFoot ? ' foot-selected' : '');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
 
565
- const checkbox = document.createElement('input');
566
- checkbox.type = 'checkbox';
567
- checkbox.className = 'bone-checkbox';
568
- checkbox.value = bone.name;
569
- checkbox.id = `bone_${index}`;
570
- checkbox.checked = isFoot;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
- checkbox.addEventListener('change', (e) => {
573
  if (e.target.checked) {
574
- selectedFootBones.add(bone.name);
575
- if (!inversionSettings.has(bone.name)) {
576
- inversionSettings.set(bone.name, createDefaultSettings());
 
 
 
 
 
 
 
 
 
 
 
 
577
  }
 
578
  } else {
579
- selectedFootBones.delete(bone.name);
 
 
 
580
  }
581
 
582
- document.querySelectorAll('.bone-item').forEach((el) => {
583
- const cb = el.querySelector('.bone-checkbox');
584
- if (cb && selectedFootBones.has(cb.value)) {
585
- el.classList.add('foot-selected');
586
- } else {
587
- el.classList.remove('foot-selected');
588
- }
589
- });
590
 
591
  buildFootControls();
 
592
  updatePreview();
593
- });
 
594
 
595
- const label = document.createElement('label');
596
- label.htmlFor = checkbox.id;
597
- label.textContent = bone.name;
598
 
599
- item.appendChild(checkbox);
600
- item.appendChild(label);
601
- boneListDiv.appendChild(item);
602
- });
 
 
 
 
603
  }
 
 
604
 
605
- function buildFootControls() {
606
- const footControlsDiv = document.getElementById('footControls');
607
- footControlsDiv.innerHTML = '';
608
 
609
- if (selectedFootBones.size === 0) {
610
- footControlsDiv.innerHTML = '<p class="info">足ボーンが選択されていません</p>';
611
- return;
612
- }
 
 
 
 
 
 
613
 
614
- debug(`反転設定構築: ${selectedFootBones.size} ボーン`);
615
 
616
- let index = 0;
617
- for (const boneName of selectedFootBones) {
618
- const settings = inversionSettings.get(boneName);
619
- if (!settings) continue;
620
 
621
- const div = document.createElement('div');
622
- div.className = 'bone-controls';
623
 
624
- const legend = document.createElement('div');
625
- legend.className = 'legend-like';
626
- legend.textContent = boneName;
627
- div.appendChild(legend);
628
 
629
- const groupDiv = document.createElement('div');
630
- groupDiv.className = 'checkbox-group';
631
 
632
- const labels = [
633
- ['offset X', 'offsetX'], ['offset Y', 'offsetY'], ['offset Z', 'offsetZ'],
634
- ['pos X', 'posX'], ['pos Y', 'posY'], ['pos Z', 'posZ'],
635
- ['rot X', 'rotX'], ['rot Y', 'rotY'], ['rot Z', 'rotZ']
636
- ];
637
 
638
- for (const [label, key] of labels) {
639
- const item = document.createElement('div');
640
- item.className = 'checkbox-item';
 
641
 
642
- const cb = document.createElement('input');
643
- cb.type = 'checkbox';
644
- cb.id = `foot_${index}_${key}`;
645
- cb.checked = settings[key];
646
 
647
- cb.addEventListener('change', (e) => {
648
- settings[key] = e.target.checked;
649
- debug(`${boneName}.${key} = ${e.target.checked}`);
650
- updatePreview();
651
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
- const lbl = document.createElement('label');
654
- lbl.htmlFor = cb.id;
655
- lbl.textContent = label;
656
 
657
- item.appendChild(cb);
658
- item.appendChild(lbl);
659
- groupDiv.appendChild(item);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  }
 
661
 
662
- div.appendChild(groupDiv);
663
- footControlsDiv.appendChild(div);
664
- index++;
665
- }
 
 
 
 
 
 
666
  }
667
 
668
- // ---------- イベントハンドラ ----------
669
- document.getElementById('fileInput').addEventListener('change', (e) => {
670
- const file = e.target.files[0];
671
- if (file) handleFile(file);
672
- });
673
 
674
- document.getElementById('playBtn').addEventListener('click', () => {
675
- if (animationAction) {
676
- animationAction.paused = false;
677
- animationAction.play();
678
- isPlaying = true;
679
- document.getElementById('playBtn').disabled = true;
680
- document.getElementById('pauseBtn').disabled = false;
681
- debug('アニメーション再生');
682
- }
683
- });
684
 
685
- document.getElementById('pauseBtn').addEventListener('click', () => {
686
- if (animationAction) {
687
- animationAction.paused = true;
688
- isPlaying = false;
689
- document.getElementById('playBtn').disabled = false;
690
- document.getElementById('pauseBtn').disabled = true;
691
- debug('アニメーション一時停止');
692
- }
693
- });
694
 
695
- document.getElementById('resetBtn').addEventListener('click', () => {
696
- for (const settings of inversionSettings.values()) {
697
- for (const key in settings) {
698
- settings[key] = false;
699
- }
700
- }
701
 
702
- document.querySelectorAll('#footControls input[type="checkbox"]').forEach((cb) => {
703
- cb.checked = false;
704
- });
705
 
706
- debug('反転設定をリセット');
707
- updatePreview();
708
- });
709
 
710
- // ドラッグ&ドロップ
711
- container.addEventListener('dragover', (e) => e.preventDefault());
712
- container.addEventListener('drop', async (e) => {
713
- e.preventDefault();
714
- const file = e.dataTransfer.files[0];
715
- if (file?.name?.toLowerCase().endsWith('.bvh')) {
716
- await handleFile(file);
717
- }
718
- });
719
 
720
- // エクスポート
721
- document.getElementById('exportBtn').addEventListener('click', () => {
722
- const file = document.getElementById('fileInput').files[0];
723
- if (!file) {
724
- alert('ファイルを選択してください');
725
- return;
726
- }
727
 
728
- // ここは元ファイルの保存です。
729
- // BVHを書き出し自体を完全に反映したい場合は、別途BVH再生成処理が必要です。
730
- const reader = new FileReader();
731
- reader.onload = function (e) {
732
- const blob = new Blob([e.target.result], { type: 'text/plain' });
733
- const url = URL.createObjectURL(blob);
734
- const a = document.createElement('a');
735
- a.href = url;
736
- a.download = 'modified_' + file.name;
737
- a.click();
738
- URL.revokeObjectURL(url);
739
- debug('ファイルをダウンロード: ' + a.download);
740
- };
741
- reader.readAsText(file);
742
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
- debug('BVH Foot Inverter 初期化完了');
745
- debug('Three.js バージョン: ' + THREE.REVISION);
746
- </script>
747
  </body>
748
  </html>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>BVH Foot Orientation Fixer</title>
7
+
8
  <style>
9
  body {
10
  margin: 0;
 
15
  height: 100vh;
16
  overflow: hidden;
17
  }
18
+
19
  #sidebar {
20
  width: 380px;
21
  padding: 16px;
 
26
  flex-direction: column;
27
  gap: 12px;
28
  }
29
+
30
  #canvas-container {
31
  flex: 1;
32
  position: relative;
33
  background: #1a1a2e;
34
  min-width: 0;
35
  }
36
+
37
  h1, h2 {
38
  margin: 0;
39
  }
40
+
41
  h1 {
42
  font-size: 1.3em;
43
  color: #e94560;
44
  }
45
+
46
  h2 {
47
  font-size: 1.05em;
48
  margin-top: 8px;
49
  }
50
+
51
  input[type="file"] {
52
  width: 100%;
53
  padding: 8px;
 
57
  border-radius: 4px;
58
  box-sizing: border-box;
59
  }
60
+
61
  .bone-list {
62
  border: 1px solid #0f3460;
63
  border-radius: 6px;
 
66
  overflow-y: auto;
67
  background: #0a0f1c;
68
  }
69
+
70
  .bone-item {
71
  display: flex;
72
  align-items: center;
 
74
  padding: 3px 0;
75
  font-size: 0.9em;
76
  }
77
+
78
  .bone-item.foot-selected {
79
  background: rgba(233, 69, 96, 0.2);
80
  border-radius: 3px;
81
  padding-left: 6px;
82
  }
83
+
84
  .bone-controls {
85
  border: 1px solid #e94560;
86
  border-radius: 6px;
 
88
  margin: 8px 0;
89
  background: #0f3460;
90
  }
91
+
92
  .bone-controls .legend-like {
93
  font-weight: bold;
94
  color: #e94560;
95
  margin-bottom: 8px;
96
  }
97
+
98
  .checkbox-group {
99
  display: grid;
100
  grid-template-columns: 1fr 1fr;
101
  gap: 4px;
102
  }
103
+
104
  .checkbox-item {
105
  display: flex;
106
  align-items: center;
107
  gap: 4px;
108
  font-size: 0.85em;
109
  }
110
+
111
  button {
112
  padding: 10px;
113
  background: #e94560;
 
118
  font-weight: bold;
119
  font-size: 0.9em;
120
  }
121
+
122
  button:hover {
123
  background: #ff6b7a;
124
  }
125
+
126
  button:disabled {
127
  background: #555;
128
  cursor: not-allowed;
129
  }
130
+
131
  .control-buttons {
132
  display: flex;
133
  gap: 8px;
134
  }
135
+
136
  .control-buttons button {
137
  flex: 1;
138
  }
139
+
140
  .pause-btn {
141
  background: #0f3460;
142
  border: 1px solid #e94560;
143
  }
144
+
145
  .info {
146
  font-size: 0.85em;
147
  color: #aaa;
148
  }
149
+
150
  .status {
151
  font-size: 0.9em;
152
  padding: 6px;
 
154
  border-radius: 4px;
155
  min-height: 20px;
156
  }
157
+
158
  .debug-info {
159
  font-size: 0.8em;
160
  color: #4ecca3;
 
167
  }
168
  </style>
169
  </head>
170
+
171
  <body>
 
 
 
172
 
173
+ <div id="sidebar">
174
 
175
+ <h1>BVH Foot Inverter</h1>
 
 
 
176
 
177
+ <input type="file" id="fileInput" accept=".bvh" />
 
 
 
178
 
179
+ <div id="status" class="status">
180
+ ファイルをアップロードしてください
181
+ </div>
182
+
183
+ <div class="control-buttons">
184
+ <button id="playBtn" disabled>▶ 再生</button>
185
+ <button id="pauseBtn" disabled class="pause-btn">⏸ 一時停止</button>
186
+ </div>
187
 
188
+ <h2>ボーン一覧</h2>
 
189
 
190
+ <div id="boneList" class="bone-list">
191
+ <p class="info">ファイルをアップロードしてください</p>
192
  </div>
 
193
 
194
+ <h2>反転設定(足ボーンのみ)</h2>
195
+
196
+ <div id="footControls">
197
+ <p class="info">
198
+ 足ボーンを選択すると反転設定が表示されます
199
+ </p>
200
+ </div>
201
+
202
+ <button id="exportBtn" disabled>
203
+ 修正BVHをダウンロード
204
+ </button>
205
+
206
+ <button id="resetBtn" disabled>
207
+ すべての反転をリセット
208
+ </button>
209
+
210
+ <div id="debugInfo" class="debug-info"></div>
211
+
212
+ </div>
213
+
214
+ <div id="canvas-container"></div>
215
+
216
+ <script type="importmap">
217
+ {
218
+ "imports": {
219
+ "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
220
+ "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
221
+ }
222
+ }
223
+ </script>
224
+
225
+ <script type="module">
226
+
227
+ import * as THREE from 'three';
228
+
229
+ import { OrbitControls }
230
+ from 'three/addons/controls/OrbitControls.js';
231
+
232
+ import { BVHLoader }
233
+ from 'three/addons/loaders/BVHLoader.js';
234
+
235
+
236
+ // =====================================================
237
+ // グローバル
238
+ // =====================================================
239
+
240
+ let parsedBVH = null;
241
+
242
+ let selectedFootBones = new Set();
243
+
244
+ const inversionSettings = new Map();
245
 
246
+ let mixer = null;
247
+ let animationAction = null;
 
 
248
 
249
+ let isPlaying = true;
 
 
 
250
 
251
+ let clock = new THREE.Clock();
 
 
 
252
 
253
  let currentPreviewRoot = null;
254
+
255
  let skeletonHelper = null;
 
 
 
 
 
 
 
 
256
 
 
 
 
 
 
 
 
257
 
258
+ // =====================================================
259
+ // debug
260
+ // =====================================================
261
 
262
+ function debug(msg) {
 
263
 
264
+ const debugDiv = document.getElementById('debugInfo');
 
 
 
265
 
266
+ debugDiv.innerHTML += msg + '<br>';
 
267
 
268
+ debugDiv.scrollTop = debugDiv.scrollHeight;
269
+
270
+ console.log(msg);
271
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
 
 
273
 
274
+ // =====================================================
275
+ // default settings
276
+ // =====================================================
 
277
 
278
+ function createDefaultSettings() {
279
+
280
+ return {
281
+ offsetX: false,
282
+ offsetY: false,
283
+ offsetZ: false,
284
+
285
+ posX: false,
286
+ posY: false,
287
+ posZ: false,
288
+
289
+ rotX: false,
290
+ rotY: false,
291
+ rotZ: false
292
+ };
293
  }
294
 
 
 
 
 
295
 
296
+ // =====================================================
297
+ // Three.js
298
+ // =====================================================
299
 
300
+ const container = document.getElementById('canvas-container');
301
+
302
+ const scene = new THREE.Scene();
303
+
304
+ scene.background = new THREE.Color(0x1a1a2e);
305
+
306
+ scene.add(
307
+ new THREE.AmbientLight(0xffffff, 0.8)
308
+ );
309
+
310
+ const dirLight = new THREE.DirectionalLight(
311
+ 0xffffff,
312
+ 0.7
313
+ );
314
+
315
+ dirLight.position.set(1, 2, 1);
316
+
317
+ scene.add(dirLight);
318
+
319
+ const gridHelper = new THREE.GridHelper(
320
+ 3,
321
+ 30,
322
+ 0x333333,
323
+ 0x222222
324
+ );
325
+
326
+ scene.add(gridHelper);
327
+
328
+ const camera = new THREE.PerspectiveCamera(
329
+ 50,
330
+ container.clientWidth / container.clientHeight,
331
+ 0.01,
332
+ 100
333
+ );
334
+
335
+ camera.position.set(1.5, 2, 3);
336
+
337
+ camera.lookAt(0, 0.8, 0);
338
+
339
+ const renderer = new THREE.WebGLRenderer({
340
+ antialias: true
341
+ });
342
+
343
+ renderer.setSize(
344
+ container.clientWidth,
345
+ container.clientHeight
346
+ );
347
+
348
+ renderer.setPixelRatio(window.devicePixelRatio);
349
+
350
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
351
+
352
+ container.appendChild(renderer.domElement);
353
+
354
+ const controls = new OrbitControls(
355
+ camera,
356
+ renderer.domElement
357
+ );
358
+
359
+ controls.target.set(0, 0.8, 0);
360
+
361
+ controls.update();
362
+
363
+ window.addEventListener('resize', () => {
364
+
365
+ const w = container.clientWidth;
366
+ const h = container.clientHeight;
367
+
368
+ camera.aspect = w / h;
369
+
370
+ camera.updateProjectionMatrix();
371
+
372
+ renderer.setSize(w, h);
373
+ });
374
+
375
+
376
+ // =====================================================
377
+ // animate
378
+ // =====================================================
379
+
380
+ function animate() {
381
+
382
+ requestAnimationFrame(animate);
383
+
384
+ const delta = clock.getDelta();
385
+
386
+ if (mixer && isPlaying) {
387
+ mixer.update(delta);
388
+ }
389
+
390
+ if (skeletonHelper) {
391
+ skeletonHelper.update();
392
+ }
393
+
394
+ controls.update();
395
+
396
+ renderer.render(scene, camera);
397
+ }
398
+
399
+ animate();
400
 
 
 
 
 
401
 
402
+ // =====================================================
403
+ // 球ジョイントヘルパー
404
+ // =====================================================
405
+
406
+ class ColoredSkeletonHelper extends THREE.Group {
407
+
408
+ constructor(rootBone, footBoneNames) {
409
+
410
+ super();
411
 
412
  this.rootBone = rootBone;
413
+
414
  this.footBoneNames = footBoneNames;
415
 
416
+ this.items = [];
417
+
418
+ const sphereGeo =
419
+ new THREE.SphereGeometry(0.035, 16, 16);
420
+
421
+ rootBone.traverse((bone) => {
422
+
423
+ if (!bone.isBone) return;
424
+
425
+ const isFoot =
426
+ footBoneNames.has(bone.name);
427
+
428
+ const sphereMat =
429
+ new THREE.MeshBasicMaterial({
430
+ color: isFoot
431
+ ? 0xe94560
432
+ : 0x4ecca3
433
+ });
434
+
435
+ const sphere =
436
+ new THREE.Mesh(
437
+ sphereGeo,
438
+ sphereMat
439
+ );
440
+
441
+ sphere.scale.setScalar(
442
+ isFoot ? 1.5 : 1.0
443
+ );
444
+
445
+ this.add(sphere);
446
+
447
+ let line = null;
448
+
449
+ if (
450
+ bone.parent &&
451
+ bone.parent.isBone
452
+ ) {
453
+
454
+ const lineGeo =
455
+ new THREE.BufferGeometry();
456
 
457
+ const lineMat =
458
+ new THREE.LineBasicMaterial({
459
+ color: 0x444444
460
+ });
461
+
462
+ line = new THREE.Line(
463
+ lineGeo,
464
+ lineMat
465
+ );
466
 
467
+ this.add(line);
468
+ }
469
+
470
+ this.items.push({
471
+ bone,
472
+ sphere,
473
+ line
474
+ });
475
+ });
476
 
477
  this.update();
478
  }
479
 
480
  update() {
481
 
482
+ this.rootBone.updateWorldMatrix(
483
+ true,
484
+ true
485
+ );
486
 
487
+ for (const item of this.items) {
488
 
489
+ const bone = item.bone;
490
 
491
+ const pos = new THREE.Vector3();
492
 
493
+ bone.getWorldPosition(pos);
494
 
495
+ item.sphere.position.copy(pos);
 
496
 
497
+ if (item.line) {
 
 
 
498
 
499
+ const parentPos =
500
+ new THREE.Vector3();
 
 
501
 
502
+ const childPos =
503
+ new THREE.Vector3();
504
+
505
+ bone.parent.getWorldPosition(
506
+ parentPos
507
+ );
508
+
509
+ bone.getWorldPosition(
510
+ childPos
511
+ );
512
 
513
+ item.line.geometry.setFromPoints([
514
+ parentPos,
515
+ childPos
516
+ ]);
517
+ }
518
+ }
519
  }
520
  }
521
 
 
 
 
 
 
 
 
 
522
 
523
+ // =====================================================
524
+ // dispose preview
525
+ // =====================================================
526
+
527
+ function disposePreview() {
528
+
529
+ if (skeletonHelper) {
530
+
531
+ scene.remove(skeletonHelper);
532
+
533
+ skeletonHelper.traverse((obj) => {
534
+
535
+ if (obj.geometry) {
536
+ obj.geometry.dispose();
537
  }
538
 
539
+ if (obj.material) {
540
+
541
+ if (Array.isArray(obj.material)) {
542
+
543
+ obj.material.forEach(m => {
544
+ m.dispose();
545
+ });
546
+
547
+ } else {
548
+
549
+ obj.material.dispose();
550
+ }
551
  }
552
+ });
553
+
554
+ skeletonHelper = null;
555
+ }
556
+
557
+ if (currentPreviewRoot) {
558
+
559
+ scene.remove(currentPreviewRoot);
560
+
561
+ currentPreviewRoot = null;
562
+ }
563
+
564
+ if (mixer) {
565
+
566
+ mixer.stopAllAction();
567
+
568
+ mixer = null;
569
+
570
+ animationAction = null;
571
+ }
572
+ }
573
+
574
+
575
+ // =====================================================
576
+ // update preview
577
+ // =====================================================
578
+
579
+ function updatePreview() {
580
+
581
+ debug('updatePreview');
582
+
583
+ disposePreview();
584
+
585
+ if (!parsedBVH) return;
586
+
587
+ try {
588
+
589
+ const origSkeleton =
590
+ parsedBVH.skeleton;
591
+
592
+ const origClip =
593
+ parsedBVH.clip;
594
+
595
+ // -----------------------------
596
+ // clone root
597
+ // -----------------------------
598
+
599
+ const rootBone =
600
+ origSkeleton.bones[0].clone();
601
+
602
+ function cloneChildren(source, target) {
603
+
604
+ source.children.forEach((childBone) => {
605
+
606
+ if (!childBone.isBone) return;
607
+
608
+ const newChild =
609
+ childBone.clone();
610
+
611
+ const settings =
612
+ inversionSettings.get(
613
+ childBone.name
614
+ );
615
+
616
+ if (
617
+ settings &&
618
+ selectedFootBones.has(
619
+ childBone.name
620
+ )
621
+ ) {
622
+
623
+ if (settings.offsetX)
624
+ newChild.position.x *= -1;
625
+
626
+ if (settings.offsetY)
627
+ newChild.position.y *= -1;
628
+
629
+ if (settings.offsetZ)
630
+ newChild.position.z *= -1;
631
+ }
632
+
633
+ target.add(newChild);
634
+
635
+ cloneChildren(
636
+ childBone,
637
+ newChild
638
+ );
639
+ });
640
  }
641
 
642
+ cloneChildren(
643
+ origSkeleton.bones[0],
644
+ rootBone
645
+ );
646
 
647
+ // -----------------------------
648
+ // tracks
649
+ // -----------------------------
650
 
651
+ const modifiedTracks =
652
+ origClip.tracks.map((track) => {
 
 
653
 
654
+ const parts =
655
+ track.name.split('.');
 
656
 
657
+ const boneName = parts[0];
658
 
659
+ const property = parts[1];
 
660
 
661
+ const settings =
662
+ inversionSettings.get(
663
+ boneName
664
+ );
665
+
666
+ if (
667
+ !settings ||
668
+ !selectedFootBones.has(
669
+ boneName
670
+ )
671
+ ) {
672
+ return track;
673
  }
674
 
675
+ // position
 
 
 
 
 
676
 
677
+ if (
678
+ property === 'position' &&
679
+ track instanceof
680
+ THREE.VectorKeyframeTrack
681
+ ) {
682
 
683
+ const values =
684
+ [...track.values];
685
+
686
+ for (
687
+ let i = 0;
688
+ i < values.length;
689
+ i += 3
690
+ ) {
691
+
692
+ if (settings.posX)
693
+ values[i] *= -1;
694
+
695
+ if (settings.posY)
696
+ values[i + 1] *= -1;
697
+
698
+ if (settings.posZ)
699
+ values[i + 2] *= -1;
700
  }
701
 
702
+ return new THREE.VectorKeyframeTrack(
703
+ track.name,
704
+ [...track.times],
705
+ values
706
+ );
707
+ }
708
+
709
+ // rotation
710
+
711
+ if (
712
+ property === 'quaternion' &&
713
+ track instanceof
714
+ THREE.QuaternionKeyframeTrack
715
+ ) {
716
+
717
+ const values =
718
+ [...track.values];
719
+
720
+ for (
721
+ let i = 0;
722
+ i < values.length;
723
+ i += 4
724
+ ) {
725
+
726
+ const quat =
727
+ new THREE.Quaternion(
728
  values[i],
729
  values[i + 1],
730
  values[i + 2],
731
  values[i + 3]
732
  );
733
 
734
+ const euler =
735
+ new THREE.Euler()
736
+ .setFromQuaternion(
737
+ quat,
738
+ 'YXZ'
739
+ );
740
+
741
+ if (settings.rotX)
742
+ euler.x *= -1;
743
 
744
+ if (settings.rotY)
745
+ euler.y *= -1;
746
 
747
+ if (settings.rotZ)
748
+ euler.z *= -1;
749
+
750
+ quat.setFromEuler(euler);
751
+
752
+ values[i] = quat.x;
753
+ values[i + 1] = quat.y;
754
+ values[i + 2] = quat.z;
755
+ values[i + 3] = quat.w;
756
  }
757
 
758
+ return new THREE.QuaternionKeyframeTrack(
759
+ track.name,
760
+ [...track.times],
761
+ values
762
+ );
763
+ }
764
 
765
+ return track;
766
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
 
768
+ const modifiedClip =
769
+ new THREE.AnimationClip(
770
+ origClip.name,
771
+ origClip.duration,
772
+ modifiedTracks
773
+ );
774
 
775
+ // -----------------------------
776
+ // preview root
777
+ // -----------------------------
778
 
779
+ currentPreviewRoot =
780
+ new THREE.Group();
781
 
782
+ currentPreviewRoot.add(rootBone);
783
 
784
+ scene.add(currentPreviewRoot);
785
 
786
+ // -----------------------------
787
+ // auto scale
788
+ // -----------------------------
789
 
790
+ const box =
791
+ new THREE.Box3()
792
+ .setFromObject(currentPreviewRoot);
 
 
793
 
794
+ const size =
795
+ new THREE.Vector3();
796
 
797
+ box.getSize(size);
798
+
799
+ const maxSize =
800
+ Math.max(
801
+ size.x,
802
+ size.y,
803
+ size.z
804
+ );
805
+
806
+ const targetSize = 2.0;
807
+
808
+ if (maxSize > 0) {
809
+
810
+ const scale =
811
+ targetSize / maxSize;
812
+
813
+ currentPreviewRoot.scale.setScalar(
814
+ scale
815
+ );
816
  }
817
 
818
+ // -----------------------------
819
+ // helper
820
+ // -----------------------------
821
+
822
+ skeletonHelper =
823
+ new ColoredSkeletonHelper(
824
+ rootBone,
825
+ selectedFootBones
826
+ );
827
+
828
+ currentPreviewRoot.add(
829
+ skeletonHelper
830
+ );
831
+
832
+ // -----------------------------
833
+ // animation
834
+ // -----------------------------
835
+
836
+ mixer =
837
+ new THREE.AnimationMixer(
838
+ rootBone
839
+ );
840
+
841
+ animationAction =
842
+ mixer.clipAction(
843
+ modifiedClip
844
+ );
845
+
846
+ animationAction.play();
847
+
848
+ isPlaying = true;
849
+
850
+ document.getElementById(
851
+ 'playBtn'
852
+ ).disabled = true;
853
+
854
+ document.getElementById(
855
+ 'pauseBtn'
856
+ ).disabled = false;
857
+
858
+ } catch (error) {
859
+
860
+ console.error(error);
861
+
862
+ debug(error.message);
863
+ }
864
+ }
865
 
866
+
867
+ // =====================================================
868
+ // handle file
869
+ // =====================================================
870
+
871
+ async function handleFile(file) {
872
+
873
+ document.getElementById(
874
+ 'status'
875
+ ).textContent = '読み込み中...';
876
+
877
+ document.getElementById(
878
+ 'debugInfo'
879
+ ).innerHTML = '';
880
+
881
+ const text = await file.text();
882
+
883
+ const loader = new BVHLoader();
884
+
885
+ try {
886
+
887
+ const result =
888
+ loader.parse(text);
889
+
890
+ parsedBVH = result;
891
+
892
+ selectedFootBones.clear();
893
+
894
+ inversionSettings.clear();
895
+
896
+ result.skeleton.bones.forEach((bone) => {
897
+
898
+ if (
899
+ bone.name &&
900
+ bone.name
901
+ .toLowerCase()
902
+ .includes('foot')
903
+ ) {
904
+
905
+ selectedFootBones.add(
906
+ bone.name
907
+ );
908
+
909
+ inversionSettings.set(
910
+ bone.name,
911
+ createDefaultSettings()
912
+ );
913
  }
914
+ });
915
+
916
+ buildBoneList();
917
+
918
+ buildFootControls();
919
+
920
+ updatePreview();
921
+
922
+ document.getElementById(
923
+ 'exportBtn'
924
+ ).disabled = false;
925
+
926
+ document.getElementById(
927
+ 'resetBtn'
928
+ ).disabled = false;
929
 
930
+ document.getElementById(
931
+ 'status'
932
+ ).textContent =
933
+ `読み込み完了: ${result.skeleton.bones.length} bones`;
934
 
935
+ } catch (e) {
936
 
937
+ console.error(e);
 
938
 
939
+ document.getElementById(
940
+ 'status'
941
+ ).textContent =
942
+ '読み込み失敗';
943
+ }
944
+ }
945
+
946
+
947
+ // =====================================================
948
+ // UI
949
+ // =====================================================
950
+
951
+ function buildBoneList() {
952
+
953
+ const boneListDiv =
954
+ document.getElementById(
955
+ 'boneList'
956
+ );
957
+
958
+ boneListDiv.innerHTML = '';
959
+
960
+ if (!parsedBVH) return;
961
+
962
+ parsedBVH.skeleton.bones.forEach(
963
+ (bone, index) => {
964
+
965
+ const isFoot =
966
+ selectedFootBones.has(
967
+ bone.name
968
+ );
969
 
970
+ const item =
971
+ document.createElement('div');
972
+
973
+ item.className =
974
+ 'bone-item' +
975
+ (isFoot
976
+ ? ' foot-selected'
977
+ : '');
978
+
979
+ const checkbox =
980
+ document.createElement('input');
981
+
982
+ checkbox.type = 'checkbox';
983
+
984
+ checkbox.checked = isFoot;
985
+
986
+ checkbox.className =
987
+ 'bone-checkbox';
988
+
989
+ checkbox.value = bone.name;
990
+
991
+ checkbox.id =
992
+ 'bone_' + index;
993
+
994
+ checkbox.addEventListener(
995
+ 'change',
996
+ (e) => {
997
 
 
998
  if (e.target.checked) {
999
+
1000
+ selectedFootBones.add(
1001
+ bone.name
1002
+ );
1003
+
1004
+ if (
1005
+ !inversionSettings.has(
1006
+ bone.name
1007
+ )
1008
+ ) {
1009
+
1010
+ inversionSettings.set(
1011
+ bone.name,
1012
+ createDefaultSettings()
1013
+ );
1014
  }
1015
+
1016
  } else {
1017
+
1018
+ selectedFootBones.delete(
1019
+ bone.name
1020
+ );
1021
  }
1022
 
1023
+ buildBoneList();
 
 
 
 
 
 
 
1024
 
1025
  buildFootControls();
1026
+
1027
  updatePreview();
1028
+ }
1029
+ );
1030
 
1031
+ const label =
1032
+ document.createElement('label');
 
1033
 
1034
+ label.textContent =
1035
+ bone.name;
1036
+
1037
+ item.appendChild(checkbox);
1038
+
1039
+ item.appendChild(label);
1040
+
1041
+ boneListDiv.appendChild(item);
1042
  }
1043
+ );
1044
+ }
1045
 
 
 
 
1046
 
1047
+ // =====================================================
1048
+ // controls
1049
+ // =====================================================
1050
+
1051
+ function buildFootControls() {
1052
+
1053
+ const footControlsDiv =
1054
+ document.getElementById(
1055
+ 'footControls'
1056
+ );
1057
 
1058
+ footControlsDiv.innerHTML = '';
1059
 
1060
+ if (
1061
+ selectedFootBones.size === 0
1062
+ ) {
 
1063
 
1064
+ footControlsDiv.innerHTML =
1065
+ '<p class="info">未選択</p>';
1066
 
1067
+ return;
1068
+ }
 
 
1069
 
1070
+ let index = 0;
 
1071
 
1072
+ for (const boneName of selectedFootBones) {
 
 
 
 
1073
 
1074
+ const settings =
1075
+ inversionSettings.get(
1076
+ boneName
1077
+ );
1078
 
1079
+ const div =
1080
+ document.createElement('div');
 
 
1081
 
1082
+ div.className =
1083
+ 'bone-controls';
1084
+
1085
+ const legend =
1086
+ document.createElement('div');
1087
+
1088
+ legend.className =
1089
+ 'legend-like';
1090
+
1091
+ legend.textContent =
1092
+ boneName;
1093
+
1094
+ div.appendChild(legend);
1095
+
1096
+ const groupDiv =
1097
+ document.createElement('div');
1098
+
1099
+ groupDiv.className =
1100
+ 'checkbox-group';
1101
 
1102
+ const labels = [
 
 
1103
 
1104
+ ['offset X', 'offsetX'],
1105
+ ['offset Y', 'offsetY'],
1106
+ ['offset Z', 'offsetZ'],
1107
+
1108
+ ['pos X', 'posX'],
1109
+ ['pos Y', 'posY'],
1110
+ ['pos Z', 'posZ'],
1111
+
1112
+ ['rot X', 'rotX'],
1113
+ ['rot Y', 'rotY'],
1114
+ ['rot Z', 'rotZ']
1115
+ ];
1116
+
1117
+ for (const [label, key] of labels) {
1118
+
1119
+ const item =
1120
+ document.createElement('div');
1121
+
1122
+ item.className =
1123
+ 'checkbox-item';
1124
+
1125
+ const cb =
1126
+ document.createElement('input');
1127
+
1128
+ cb.type = 'checkbox';
1129
+
1130
+ cb.checked = settings[key];
1131
+
1132
+ cb.addEventListener(
1133
+ 'change',
1134
+ (e) => {
1135
+
1136
+ settings[key] =
1137
+ e.target.checked;
1138
+
1139
+ updatePreview();
1140
  }
1141
+ );
1142
 
1143
+ const lbl =
1144
+ document.createElement('label');
1145
+
1146
+ lbl.textContent = label;
1147
+
1148
+ item.appendChild(cb);
1149
+
1150
+ item.appendChild(lbl);
1151
+
1152
+ groupDiv.appendChild(item);
1153
  }
1154
 
1155
+ div.appendChild(groupDiv);
 
 
 
 
1156
 
1157
+ footControlsDiv.appendChild(div);
 
 
 
 
 
 
 
 
 
1158
 
1159
+ index++;
1160
+ }
1161
+ }
 
 
 
 
 
 
1162
 
 
 
 
 
 
 
1163
 
1164
+ // =====================================================
1165
+ // events
1166
+ // =====================================================
1167
 
1168
+ document.getElementById('fileInput')
1169
+ .addEventListener('change', (e) => {
 
1170
 
1171
+ const file = e.target.files[0];
 
 
 
 
 
 
 
 
1172
 
1173
+ if (file) {
1174
+ handleFile(file);
1175
+ }
1176
+ });
 
 
 
1177
 
1178
+ document.getElementById('playBtn')
1179
+ .addEventListener('click', () => {
1180
+
1181
+ if (animationAction) {
1182
+
1183
+ animationAction.paused = false;
1184
+
1185
+ animationAction.play();
1186
+
1187
+ isPlaying = true;
1188
+
1189
+ document.getElementById(
1190
+ 'playBtn'
1191
+ ).disabled = true;
1192
+
1193
+ document.getElementById(
1194
+ 'pauseBtn'
1195
+ ).disabled = false;
1196
+ }
1197
+ });
1198
+
1199
+ document.getElementById('pauseBtn')
1200
+ .addEventListener('click', () => {
1201
+
1202
+ if (animationAction) {
1203
+
1204
+ animationAction.paused = true;
1205
+
1206
+ isPlaying = false;
1207
+
1208
+ document.getElementById(
1209
+ 'playBtn'
1210
+ ).disabled = false;
1211
+
1212
+ document.getElementById(
1213
+ 'pauseBtn'
1214
+ ).disabled = true;
1215
+ }
1216
+ });
1217
+
1218
+ document.getElementById('resetBtn')
1219
+ .addEventListener('click', () => {
1220
+
1221
+ for (const settings of inversionSettings.values()) {
1222
+
1223
+ for (const key in settings) {
1224
+
1225
+ settings[key] = false;
1226
+ }
1227
+ }
1228
+
1229
+ buildFootControls();
1230
+
1231
+ updatePreview();
1232
+ });
1233
+
1234
+
1235
+ // drag and drop
1236
+
1237
+ container.addEventListener(
1238
+ 'dragover',
1239
+ (e) => e.preventDefault()
1240
+ );
1241
+
1242
+ container.addEventListener(
1243
+ 'drop',
1244
+ async (e) => {
1245
+
1246
+ e.preventDefault();
1247
+
1248
+ const file =
1249
+ e.dataTransfer.files[0];
1250
+
1251
+ if (
1252
+ file &&
1253
+ file.name
1254
+ .toLowerCase()
1255
+ .endsWith('.bvh')
1256
+ ) {
1257
+
1258
+ await handleFile(file);
1259
+ }
1260
+ }
1261
+ );
1262
+
1263
+ debug('初期化完了');
1264
+
1265
+ </script>
1266
 
 
 
 
1267
  </body>
1268
  </html>