|
@@ -0,0 +1,286 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+ <head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <title>Three.js CCDIKSolver Browser</title>
|
|
|
+ <link rel="shortcut icon" href="../../files/favicon.ico" />
|
|
|
+ <link rel="stylesheet" type="text/css" href="../../files/main.css">
|
|
|
+ <style>
|
|
|
+ canvas {
|
|
|
+ display: block;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ #newWindow {
|
|
|
+ display: block;
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0.3em;
|
|
|
+ left: 0.5em;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+
|
|
|
+ <!-- Import maps polyfill -->
|
|
|
+ <!-- Remove this when import maps will be widely supported -->
|
|
|
+ <script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
|
|
|
+
|
|
|
+ <script type="importmap">
|
|
|
+ {
|
|
|
+ "imports": {
|
|
|
+ "three": "../../build/three.module.js"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+
|
|
|
+ <a id='newWindow' href='./ccdiksolver-browser.html' target='_blank'>Open in New Window</a>
|
|
|
+
|
|
|
+ <script type="module">
|
|
|
+ //
|
|
|
+ // Forked from /docs/api/en/objects/SkinnedMesh example
|
|
|
+ //
|
|
|
+
|
|
|
+ import {
|
|
|
+ Bone,
|
|
|
+ Color,
|
|
|
+ CylinderGeometry,
|
|
|
+ DoubleSide,
|
|
|
+ Float32BufferAttribute,
|
|
|
+ MeshPhongMaterial,
|
|
|
+ PerspectiveCamera,
|
|
|
+ PointLight,
|
|
|
+ Scene,
|
|
|
+ SkinnedMesh,
|
|
|
+ Skeleton,
|
|
|
+ SkeletonHelper,
|
|
|
+ Vector3,
|
|
|
+ Uint16BufferAttribute,
|
|
|
+ WebGLRenderer
|
|
|
+ } from 'three';
|
|
|
+ import { CCDIKSolver, CCDIKHelper } from "../../examples/jsm/animation/CCDIKSolver.js";
|
|
|
+
|
|
|
+ import { GUI } from '../../examples/jsm/libs/lil-gui.module.min.js';
|
|
|
+ import { OrbitControls } from '../../examples/jsm/controls/OrbitControls.js';
|
|
|
+
|
|
|
+ let gui, scene, camera, renderer, orbit, mesh, bones, skeletonHelper, ikSolver;
|
|
|
+
|
|
|
+ const state = {
|
|
|
+ ikSolverAutoUpdate: true
|
|
|
+ };
|
|
|
+
|
|
|
+ function initScene() {
|
|
|
+
|
|
|
+ gui = new GUI();
|
|
|
+
|
|
|
+ scene = new Scene();
|
|
|
+ scene.background = new Color( 0x444444 );
|
|
|
+
|
|
|
+ camera = new PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 200 );
|
|
|
+ camera.position.z = 30;
|
|
|
+ camera.position.y = 30;
|
|
|
+
|
|
|
+ renderer = new WebGLRenderer( { antialias: true } );
|
|
|
+ renderer.setPixelRatio( window.devicePixelRatio );
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+ document.body.appendChild( renderer.domElement );
|
|
|
+
|
|
|
+ orbit = new OrbitControls( camera, renderer.domElement );
|
|
|
+ orbit.enableZoom = false;
|
|
|
+
|
|
|
+ window.addEventListener( 'resize', function () {
|
|
|
+
|
|
|
+ camera.aspect = window.innerWidth / window.innerHeight;
|
|
|
+ camera.updateProjectionMatrix();
|
|
|
+
|
|
|
+ renderer.setSize( window.innerWidth, window.innerHeight );
|
|
|
+
|
|
|
+ }, false );
|
|
|
+
|
|
|
+ initBones();
|
|
|
+ setupDatGui();
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function createGeometry( sizing ) {
|
|
|
+
|
|
|
+ const geometry = new CylinderGeometry(
|
|
|
+ 5, // radiusTop
|
|
|
+ 5, // radiusBottom
|
|
|
+ sizing.height, // height
|
|
|
+ 8, // radiusSegments
|
|
|
+ sizing.segmentCount * 1, // heightSegments
|
|
|
+ true // openEnded
|
|
|
+ );
|
|
|
+
|
|
|
+ const position = geometry.attributes.position;
|
|
|
+
|
|
|
+ const vertex = new Vector3();
|
|
|
+
|
|
|
+ const skinIndices = [];
|
|
|
+ const skinWeights = [];
|
|
|
+
|
|
|
+ for ( let i = 0; i < position.count; i ++ ) {
|
|
|
+
|
|
|
+ vertex.fromBufferAttribute( position, i );
|
|
|
+
|
|
|
+ const y = ( vertex.y + sizing.halfHeight );
|
|
|
+
|
|
|
+ const skinIndex = Math.floor( y / sizing.segmentHeight );
|
|
|
+ const skinWeight = ( y % sizing.segmentHeight ) / sizing.segmentHeight;
|
|
|
+
|
|
|
+ skinIndices.push( skinIndex, skinIndex + 1, 0, 0 );
|
|
|
+ skinWeights.push( 1 - skinWeight, skinWeight, 0, 0 );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ geometry.setAttribute( 'skinIndex', new Uint16BufferAttribute( skinIndices, 4 ) );
|
|
|
+ geometry.setAttribute( 'skinWeight', new Float32BufferAttribute( skinWeights, 4 ) );
|
|
|
+
|
|
|
+ return geometry;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function createBones( sizing ) {
|
|
|
+
|
|
|
+ bones = [];
|
|
|
+
|
|
|
+ // "root bone"
|
|
|
+ let rootBone = new Bone();
|
|
|
+ rootBone.name = "root";
|
|
|
+ rootBone.position.y = -sizing.halfHeight;
|
|
|
+ bones.push( rootBone );
|
|
|
+
|
|
|
+ //
|
|
|
+ // "bone0", "bone1", "bone2", "bone3"
|
|
|
+ //
|
|
|
+
|
|
|
+ // "bone0"
|
|
|
+ let prevBone = new Bone();
|
|
|
+ prevBone.position.y = 0;
|
|
|
+ rootBone.add(prevBone);
|
|
|
+ bones.push( prevBone );
|
|
|
+
|
|
|
+ // "bone1", "bone2", "bone3"
|
|
|
+ for ( let i = 1; i <= sizing.segmentCount; i ++ ) {
|
|
|
+
|
|
|
+ const bone = new Bone();
|
|
|
+ bone.position.y = sizing.segmentHeight;
|
|
|
+ bones.push( bone );
|
|
|
+ bone.name = `bone${i}`;
|
|
|
+ prevBone.add( bone );
|
|
|
+ prevBone = bone;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // "target"
|
|
|
+ const targetBone = new Bone();
|
|
|
+ targetBone.name = "target";
|
|
|
+ targetBone.position.y = sizing.height + sizing.segmentHeight; // relative to parent: rootBone
|
|
|
+ rootBone.add( targetBone );
|
|
|
+ bones.push( targetBone );
|
|
|
+
|
|
|
+ return bones;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function createMesh( geometry, bones ) {
|
|
|
+
|
|
|
+ const material = new MeshPhongMaterial( {
|
|
|
+ color: 0x156289,
|
|
|
+ emissive: 0x072534,
|
|
|
+ side: DoubleSide,
|
|
|
+ flatShading: true,
|
|
|
+ wireframe: true
|
|
|
+ } );
|
|
|
+
|
|
|
+ const mesh = new SkinnedMesh( geometry, material );
|
|
|
+ const skeleton = new Skeleton( bones );
|
|
|
+
|
|
|
+ mesh.add( bones[ 0 ] );
|
|
|
+
|
|
|
+ mesh.bind( skeleton );
|
|
|
+
|
|
|
+ skeletonHelper = new SkeletonHelper( mesh );
|
|
|
+ skeletonHelper.material.linewidth = 2;
|
|
|
+ scene.add( skeletonHelper );
|
|
|
+
|
|
|
+ return mesh;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function setupDatGui() {
|
|
|
+
|
|
|
+ gui.add( mesh, 'pose' ).name( 'mesh.pose()' );
|
|
|
+
|
|
|
+ mesh.skeleton.bones
|
|
|
+ .filter((bone) => bone.name === "target")
|
|
|
+ .forEach(function (bone) {
|
|
|
+ const folder = gui.addFolder( bone.name );
|
|
|
+
|
|
|
+ const delta = 20;
|
|
|
+ folder.add( bone.position, 'x', - delta + bone.position.x, delta + bone.position.x );
|
|
|
+ folder.add( bone.position, 'y', - bone.position.y, bone.position.y );
|
|
|
+ folder.add( bone.position, 'z', - delta + bone.position.z, delta + bone.position.z );
|
|
|
+ });
|
|
|
+
|
|
|
+ gui.add( ikSolver, 'update' ).name( 'ikSolver.update()' );
|
|
|
+ gui.add( state, 'ikSolverAutoUpdate' )
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function initBones() {
|
|
|
+
|
|
|
+ const segmentHeight = 8;
|
|
|
+ const segmentCount = 3;
|
|
|
+ const height = segmentHeight * segmentCount;
|
|
|
+ const halfHeight = height * 0.5;
|
|
|
+
|
|
|
+ const sizing = {
|
|
|
+ segmentHeight,
|
|
|
+ segmentCount,
|
|
|
+ height,
|
|
|
+ halfHeight
|
|
|
+ };
|
|
|
+
|
|
|
+ const geometry = createGeometry( sizing );
|
|
|
+ const bones = createBones( sizing );
|
|
|
+ mesh = createMesh( geometry, bones );
|
|
|
+
|
|
|
+ scene.add( mesh );
|
|
|
+
|
|
|
+ //
|
|
|
+ // ikSolver
|
|
|
+ //
|
|
|
+
|
|
|
+ const iks = [
|
|
|
+ {
|
|
|
+ target: 5,
|
|
|
+ effector: 4,
|
|
|
+ links: [{ index: 3 }, { index: 2 }, { index: 1 }]
|
|
|
+ }
|
|
|
+ ];
|
|
|
+ ikSolver = new CCDIKSolver( mesh, iks );
|
|
|
+ scene.add( new CCDIKHelper( mesh, iks ) );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function render() {
|
|
|
+
|
|
|
+ requestAnimationFrame( render );
|
|
|
+
|
|
|
+ if ( state.ikSolverAutoUpdate ) {
|
|
|
+ ikSolver?.update();
|
|
|
+ }
|
|
|
+
|
|
|
+ renderer.render( scene, camera );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ initScene();
|
|
|
+ render();
|
|
|
+
|
|
|
+ </script>
|
|
|
+ </body>
|
|
|
+</html>
|