├── .gitignore ├── LICENSE ├── README.md ├── assets └── logo.svg ├── build ├── fik.js ├── fik.min.js └── fik.module.js ├── demos ├── 2d │ ├── demo_0.js │ ├── demo_1.js │ ├── demo_10.js │ ├── demo_2.js │ ├── demo_3.js │ ├── demo_4.js │ ├── demo_5.js │ ├── demo_6.js │ ├── demo_7.js │ ├── demo_8.js │ └── demo_9.js └── 3d │ ├── demo_0 - Copie.js │ ├── demo_0.js │ ├── demo_1.js │ ├── demo_10.js │ ├── demo_11.js │ ├── demo_12.js │ ├── demo_13.js │ ├── demo_2.js │ ├── demo_3.js │ ├── demo_4.js │ ├── demo_5.js │ ├── demo_6.js │ ├── demo_7.js │ ├── demo_8.js │ └── demo_9.js ├── favicon.ico ├── index.html ├── jsm ├── controls │ ├── OrbitControls.js │ └── TransformControls.js └── libs │ ├── three.core.js │ ├── three.module.js │ ├── tween.esm.js │ └── uil.module.js ├── package.json ├── rollup.config.js └── src ├── FIK.js ├── FIK_dev.js ├── constants.js ├── core ├── Bone2D.js ├── Bone3D.js ├── Chain2D.js ├── Chain3D.js ├── Joint2D.js ├── Joint3D.js ├── Structure2D.js ├── Structure3D.js └── Tools.js ├── math ├── M3.js ├── Math - Copie.js ├── Math.js ├── Q.js ├── V2.js └── V3.js └── solver ├── HISolver.js └── IKSolver.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | desktop.ini 3 | dev_3d.html 4 | dev_2d.html 5 | package-lock.json 6 | prepa 7 | *.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright © 2010-2018 three.js authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

javascript fast iterative solver for Inverse Kinematics on three.js

4 | is a conversion from java to Caliko libs, Caliko library is an implementation
5 | of the FABRIK inverse kinematics algorithm by Dr. Andreas Aristidou

6 | 7 |

LAUNCH DEMO

8 | 9 |

10 | Note: Fullik is now FIK
11 | new Fullik.Bone() -> new FIK.Bone3D()
12 | new Fullik.Bone() -> new FIK.Bone3D()
13 | new Fullik.Chain() -> new FIK.Chain3D()
14 | new Fullik.Structure() -> new FIK.Structure3D()
15 |

16 | 17 | ## Usage 18 | 19 | ```js 20 | const { Structure3D } = require('FIK'); 21 | const scene = new THREE.Scene(); 22 | 23 | const solver = Structure3D(scene); 24 | 25 | const n = { 26 | mesh : new THREE.Mesh( new THREE.SphereGeometry( 0.1, 6, 4 ), new THREE.MeshStandardMaterial({color:0xFFFF00, wireframe:true }) ), 27 | control : new THREE.TransformControls( camera, renderer.domElement ), 28 | } 29 | 30 | function updateSolver() { 31 | solver.update(); 32 | } 33 | 34 | scene.add(n.mesh); 35 | scene.add(n.control); 36 | 37 | n.control.addEventListener('change', updateSolver); 38 | 39 | n.position = n.mesh.position; // publishes x, y, z to the engine... 40 | 41 | ``` 42 | 43 | From [Demo 2](https://github.com/lo-th/fullik/blob/gh-pages/demos/3d/demo_2.js) 44 | 45 | ```js 46 | var numChains = 3; 47 | var rotStep = 360 / numChains; 48 | var constraintAngleDegs = 45; 49 | var color = 0xFF0000; 50 | 51 | for (var i = 0; i < numChains; i++ ){ 52 | 53 | switch (i){ 54 | case 0: color = 0x550000; break; 55 | case 1: color = 0x005500; break; 56 | case 2: color = 0x000055; break; 57 | } 58 | 59 | var chain = new FIK.Chain3D( color ) 60 | 61 | var startLoc = new FIK.V3(0, 0, -40); 62 | startLoc = FIK._Math.rotateYDegs( startLoc, rotStep * i ); 63 | var endLoc = startLoc.clone(); 64 | endLoc.z -= defaultBoneLength; 65 | 66 | var basebone = new FIK.Bone3D( startLoc, endLoc ); 67 | chain.addBone( basebone ); 68 | 69 | for (var j = 0; j < 7; j++) { 70 | 71 | chain.addConsecutiveRotorConstrainedBone( defaultBoneDirection, defaultBoneLength, constraintAngleDegs ); 72 | 73 | }; 74 | 75 | solver.add( chain, target, true ); 76 | 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /demos/2d/demo_0.js: -------------------------------------------------------------------------------- 1 | tell("Demo 0 - stress test."); 2 | 3 | var n = 60; 4 | var pi = Math.PI; 5 | var numBones = 20; 6 | var x, y, r = 100, r2 = 95; 7 | var v1, v2, uv 8 | 9 | for( var j=0; j < n; j++ ){ 10 | 11 | x = Math.cos( 2 * j * pi / n ); 12 | y = Math.sin( 2 * j * pi / n ); 13 | 14 | v1 = new FIK.V2( x*r, y*r ); 15 | v2 = new FIK.V2( x*r2, y*r2 ); 16 | 17 | 18 | var chain = new FIK.Chain2D( FIK.math.rand(0x555555, 0xFFFFFF) ); 19 | var boneLength = 5; 20 | var basebone = new FIK.Bone2D( v1, v2 ); 21 | chain.addBone( basebone ); 22 | 23 | uv = v2.minus( v1 ).normalize(); 24 | 25 | for(var i=0; i 2 | 3 | 4 | 5 | fiK 6 | 7 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 33 | 452 | 453 | 454 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullik", 3 | "version": "1.3.3", 4 | "description": "JavaScript 3D library", 5 | "main": "build/fik.js", 6 | "repository": "lo-th/Fullik", 7 | "jsnext:main": "build/fik.module.js", 8 | "module": "build/fik.module.js", 9 | "files": [ 10 | "package.json", 11 | "LICENSE", 12 | "README.md", 13 | "build/fik.js", 14 | "build/fik.min.js", 15 | "build/fik.module.js", 16 | "src" 17 | ], 18 | "directories": { 19 | "example": "examples" 20 | }, 21 | "eslintConfig": { 22 | "extends": "mdcs" 23 | }, 24 | "scripts": { 25 | "build": "rollup -c", 26 | "dev": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/rollup.config.js -w -m inline\" \"servez -p 8111 --index\"", 27 | "start": "concurrently --names \"ROLLUP,HTTP\" -c \"bgBlue.bold,bgGreen.bold\" \"rollup -c utils/rollup.config.js -w -m inline\" \"http-server -c-1 -p 8111 -o index.html\"" 28 | }, 29 | "keywords": [ 30 | "fullik", 31 | "fik", 32 | "fik.js", 33 | "ui" 34 | ], 35 | "author": "lo-th", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/lo-th/Fullik/issues" 39 | }, 40 | "homepage": "http://lo-th.github.io/Fullik/", 41 | "devDependencies": { 42 | "@babel/core": "^7.12.16", 43 | "@babel/plugin-proposal-class-properties": "^7.12.13", 44 | "@babel/preset-env": "^7.12.16", 45 | "@rollup/plugin-babel": "^5.3.0", 46 | "@rollup/plugin-node-resolve": "^11.2.0", 47 | "concurrently": "^5.3.0", 48 | "regenerator-runtime": "^0.13.7", 49 | "rollup": "^2.39.0", 50 | "rollup-plugin-terser": "^7.0.2", 51 | "servez": "^1.11.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | function babelCleanup() { 5 | 6 | const doubleSpaces = / {2}/g; 7 | 8 | return { 9 | 10 | transform( code ) { 11 | 12 | code = code.replace( doubleSpaces, '\t' ); 13 | 14 | return { 15 | code: code, 16 | map: null 17 | }; 18 | 19 | } 20 | 21 | }; 22 | 23 | } 24 | 25 | 26 | function header() { 27 | 28 | return { 29 | 30 | renderChunk( code ) { 31 | 32 | return `/** 33 | * @license 34 | * Copyright 2010-2022 fik.js Authors 35 | * SPDX-License-Identifier: MIT 36 | */ 37 | ${ code }`; 38 | 39 | } 40 | 41 | }; 42 | 43 | } 44 | 45 | 46 | 47 | const babelrc = { 48 | presets: [ 49 | [ 50 | '@babel/preset-env', 51 | { 52 | modules: false, 53 | // the supported browsers of the three.js browser bundle 54 | // https://browsersl.ist/?q=%3E0.3%25%2C+not+dead 55 | targets: '>1%', 56 | loose: true, 57 | bugfixes: true, 58 | } 59 | ] 60 | ], 61 | plugins: [ 62 | [ 63 | "@babel/plugin-proposal-class-properties", 64 | { 65 | "loose": true 66 | } 67 | ] 68 | ] 69 | }; 70 | 71 | export default [ 72 | { 73 | input: 'src/FIK.js', 74 | plugins: [ 75 | header() 76 | ], 77 | output: [ 78 | { 79 | format: 'esm', 80 | file: 'build/fik.module.js' 81 | } 82 | ] 83 | }, 84 | { 85 | input: 'src/FIK.js', 86 | plugins: [ 87 | babel( { 88 | babelHelpers: 'bundled', 89 | compact: false, 90 | babelrc: false, 91 | ...babelrc 92 | } ), 93 | babelCleanup(), 94 | header() 95 | ], 96 | output: [ 97 | { 98 | format: 'umd', 99 | name: 'FIK', 100 | file: 'build/fik.js', 101 | indent: '\t' 102 | } 103 | ] 104 | }, 105 | { 106 | input: 'src/FIK.js', 107 | plugins: [ 108 | babel( { 109 | babelHelpers: 'bundled', 110 | babelrc: false, 111 | ...babelrc 112 | } ), 113 | babelCleanup(), 114 | terser(), 115 | header() 116 | ], 117 | output: [ 118 | { 119 | format: 'umd', 120 | name: 'FIK', 121 | file: 'build/fik.min.js' 122 | } 123 | ] 124 | } 125 | 126 | ]; 127 | 128 | -------------------------------------------------------------------------------- /src/FIK.js: -------------------------------------------------------------------------------- 1 | export { math } from './math/Math.js'; 2 | export { V2 } from './math/V2.js'; 3 | export { V3 } from './math/V3.js'; 4 | export { M3 } from './math/M3.js'; 5 | 6 | export { Joint3D } from './core/Joint3D.js'; 7 | export { Bone3D } from './core/Bone3D.js'; 8 | export { Chain3D } from './core/Chain3D.js'; 9 | export { Structure3D } from './core/Structure3D.js'; 10 | 11 | export { Joint2D } from './core/Joint2D.js'; 12 | export { Bone2D } from './core/Bone2D.js'; 13 | export { Chain2D } from './core/Chain2D.js'; 14 | export { Structure2D } from './core/Structure2D.js'; 15 | 16 | export { IKSolver } from './solver/IKSolver.js'; 17 | export { HISolver } from './solver/HISolver.js'; 18 | 19 | export * from './constants.js'; -------------------------------------------------------------------------------- /src/FIK_dev.js: -------------------------------------------------------------------------------- 1 | //import './polyfills.js'; 2 | 3 | import { math } from './math/Math.js'; 4 | import { V2 } from './math/V2.js'; 5 | import { V3 } from './math/V3.js'; 6 | import { M3 } from './math/M3.js'; 7 | 8 | import { Joint3D } from './core/Joint3D.js'; 9 | import { Bone3D } from './core/Bone3D.js'; 10 | import { Chain3D } from './core/Chain3D.js'; 11 | import { Structure3D } from './core/Structure3D.js'; 12 | 13 | import { Joint2D } from './core/Joint2D.js'; 14 | import { Bone2D } from './core/Bone2D.js'; 15 | import { Chain2D } from './core/Chain2D.js'; 16 | import { Structure2D } from './core/Structure2D.js'; 17 | 18 | import { IKSolver } from './solver/IKSolver.js'; 19 | import { HISolver } from './solver/HISolver.js'; 20 | 21 | import { REVISION, X_AXE, Y_AXE, Z_AXE, X_NEG, Y_NEG, Z_NEG, UP, DOWN, LEFT, RIGHT } from './constants.js'; 22 | 23 | export const FIK = { 24 | 25 | REVISION : REVISION, 26 | 27 | X_AXE : X_AXE, 28 | Y_AXE : Y_AXE, 29 | Z_AXE : Z_AXE, 30 | X_NEG : X_NEG, 31 | Y_NEG : Y_NEG, 32 | Z_NEG : Z_NEG, 33 | UP : UP, 34 | DOWN : DOWN, 35 | LEFT : LEFT, 36 | RIGHT : RIGHT, 37 | 38 | math:math, 39 | V2:V2, 40 | V3:V3, 41 | M3:M3, 42 | 43 | Bone3D:Bone3D, 44 | Chain3D:Chain3D, 45 | Joint3D:Joint3D, 46 | Structure3D:Structure3D, 47 | 48 | Bone2D:Bone2D, 49 | Chain2D:Chain2D, 50 | Joint2D:Joint2D, 51 | Structure2D:Structure2D, 52 | 53 | IKSolver:IKSolver, 54 | HISolver:HISolver, 55 | 56 | } -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A list of constants built-in for 3 | * the Fik engine. 4 | */ 5 | import { V3 } from './math/V3.js'; 6 | import { V2 } from './math/V2.js'; 7 | import { M3 } from './math/M3.js'; 8 | 9 | export const REVISION = '1.4.0'; 10 | 11 | export const PRECISION = 0.001; 12 | export const PRECISION_DEG = 0.01; 13 | export const MAX_VALUE = Infinity; 14 | 15 | // chain Basebone Constraint Type 16 | 17 | export const NONE = 1; // No constraint 18 | // 3D 19 | export const GLOBAL_ROTOR = 2;// World-space rotor constraint 20 | export const GLOBAL_HINGE = 3;// World-space hinge constraint 21 | export const LOCAL_ROTOR = 4;// Rotor constraint in the coordinate space of (i.e. relative to) the direction of the connected bone 22 | export const LOCAL_HINGE = 5;// Hinge constraint in the coordinate space of (i.e. relative to) the direction of the connected bone 23 | 24 | // 2D 25 | export const GLOBAL_ABSOLUTE = 6; // Constrained about a world-space direction 26 | export const LOCAL_RELATIVE = 7; // Constrained about the direction of the connected bone 27 | export const LOCAL_ABSOLUTE = 8; // Constrained about a direction with relative to the direction of the connected bone 28 | 29 | // joint Type 30 | export const J_BALL = 10; 31 | export const J_LOCAL = 11; 32 | export const J_GLOBAL = 12; 33 | 34 | export const START = 20; 35 | export const END = 21; 36 | 37 | // Define world-space axis 38 | 39 | export const X_AXE = new V3( 1, 0, 0 ); 40 | export const Y_AXE = new V3( 0, 1, 0 ); 41 | export const Z_AXE = new V3( 0, 0, 1 ); 42 | 43 | export const X_NEG = new V3( -1, 0, 0 ); 44 | export const Y_NEG = new V3( 0, -1, 0 ); 45 | export const Z_NEG = new V3( 0, 0, -1 ); 46 | 47 | // Define world-space 2D cardinal axes 48 | 49 | export const UP = new V2( 0, 1 ); 50 | export const DOWN = new V2( 0, -1 ); 51 | export const LEFT = new V2( -1, 0 ); 52 | export const RIGHT = new V2( 1, 0 ); 53 | -------------------------------------------------------------------------------- /src/core/Bone2D.js: -------------------------------------------------------------------------------- 1 | import { END, START } from '../constants.js'; 2 | import { Joint2D } from './Joint2D.js'; 3 | import { V2 } from '../math/V2.js'; 4 | 5 | 6 | export class Bone2D { 7 | 8 | constructor( Start, End, directionUV, length, clockwiseDegs, anticlockwiseDegs, color ) { 9 | 10 | this.isBone2D = true 11 | 12 | this.start = new V2(); 13 | this.end = new V2(); 14 | this.length = length || 0; 15 | 16 | this.joint = new Joint2D( clockwiseDegs, anticlockwiseDegs ); 17 | 18 | this.globalConstraintUV = new V2(1, 0); 19 | this.boneConnectionPoint = END; 20 | 21 | this.color = color || null; 22 | this.name = ''; 23 | 24 | // init 25 | 26 | this.setStartLocation( Start ); 27 | 28 | if( End ){ 29 | 30 | this.setEndLocation( End ); 31 | if( this.length === 0 ) this.length = this.getLength(); 32 | 33 | } else if ( directionUV ) { 34 | 35 | this.setEndLocation( this.start.plus( directionUV.normalised().multiplyScalar( this.length ) ) ); 36 | 37 | } 38 | 39 | } 40 | 41 | clone() { 42 | 43 | let b = new Bone2D( this.start, this.end ); 44 | b.length = this.length; 45 | b.globalConstraintUV = this.globalConstraintUV; 46 | b.boneConnectionPoint = this.boneConnectionPoint; 47 | b.joint = this.joint.clone(); 48 | b.color = this.color; 49 | b.name = this.name; 50 | return b; 51 | 52 | } 53 | 54 | // SET 55 | 56 | setName( name ) { 57 | 58 | this.name = name; 59 | 60 | } 61 | 62 | setColor( c ) { 63 | 64 | this.color = c; 65 | 66 | } 67 | 68 | setBoneConnectionPoint( bcp ) { 69 | 70 | this.boneConnectionPoint = bcp; 71 | 72 | } 73 | 74 | setStartLocation( v ) { 75 | 76 | this.start.copy( v ); 77 | 78 | } 79 | 80 | setEndLocation( v ) { 81 | 82 | this.end.copy( v ); 83 | 84 | } 85 | 86 | setLengt( length ) { 87 | 88 | if ( length > 0 ) this.length = length; 89 | 90 | } 91 | 92 | setGlobalConstraintUV( v ) { 93 | 94 | this.globalConstraintUV = v; 95 | 96 | } 97 | 98 | // SET JOINT 99 | 100 | setJoint( joint ) { 101 | 102 | this.joint = joint; 103 | 104 | } 105 | 106 | setClockwiseConstraintDegs( angleDegs ) { 107 | 108 | this.joint.setClockwiseConstraintDegs( angleDegs ); 109 | 110 | } 111 | 112 | setAnticlockwiseConstraintDegs( angleDegs ) { 113 | 114 | this.joint.setAnticlockwiseConstraintDegs( angleDegs ); 115 | 116 | } 117 | 118 | setJointConstraintCoordinateSystem ( coordSystem ) { 119 | 120 | this.joint.setConstraintCoordinateSystem( coordSystem ); 121 | 122 | } 123 | 124 | 125 | // GET 126 | 127 | getGlobalConstraintUV() { 128 | 129 | return this.globalConstraintUV; 130 | 131 | } 132 | 133 | getBoneConnectionPoint() { 134 | 135 | return this.boneConnectionPoint; 136 | 137 | } 138 | 139 | getDirectionUV() { 140 | 141 | return this.end.minus( this.start ).normalize(); 142 | 143 | } 144 | 145 | getLength() { 146 | 147 | return this.start.distanceTo( this.end ); 148 | 149 | } 150 | 151 | 152 | } -------------------------------------------------------------------------------- /src/core/Bone3D.js: -------------------------------------------------------------------------------- 1 | import { END, START } from '../constants.js'; 2 | import { Joint3D } from './Joint3D.js'; 3 | import { V3 } from '../math/V3.js'; 4 | 5 | 6 | export class Bone3D { 7 | 8 | constructor( startLocation, endLocation, directionUV, length, color ) { 9 | 10 | this.isBone3D = true 11 | 12 | this.joint = new Joint3D(); 13 | this.start = new V3(); 14 | this.end = new V3(); 15 | 16 | this.dir = new V3(); 17 | 18 | this.boneConnectionPoint = END; 19 | this.length = 0; 20 | 21 | this.color = color || 0xFFFFFF; 22 | this.name = ''; 23 | 24 | this.init( startLocation, endLocation, directionUV, length ); 25 | this.dir = this.getDirection() 26 | 27 | } 28 | 29 | init( startLocation, endLocation, directionUV, length ){ 30 | 31 | this.setStartLocation( startLocation ); 32 | if( endLocation ){ 33 | this.setEndLocation( endLocation ); 34 | this.length = this.getLength(); 35 | 36 | } else { 37 | this.setLength( length ); 38 | this.setEndLocation( this.start.plus( directionUV.normalised().multiplyScalar( length ) ) ); 39 | } 40 | 41 | 42 | 43 | } 44 | 45 | clone() { 46 | 47 | let b = new this.constructor( this.start, this.end ); 48 | b.joint = this.joint.clone(); 49 | return b; 50 | 51 | } 52 | 53 | // SET 54 | 55 | setColor( c ) { 56 | 57 | this.color = c; 58 | 59 | } 60 | 61 | setBoneConnectionPoint( bcp ) { 62 | 63 | this.boneConnectionPoint = bcp; 64 | 65 | } 66 | 67 | setHingeClockwise( angle ) { 68 | 69 | 70 | this.joint.setHingeClockwise( angle ); 71 | 72 | } 73 | 74 | setHingeAnticlockwise( angle ) { 75 | 76 | this.joint.setHingeAnticlockwise( angle ); 77 | 78 | } 79 | 80 | setBallJointConstraintDegs( angle ) { 81 | 82 | this.joint.setBallJointConstraintDegs( angle ); 83 | 84 | } 85 | 86 | setStartLocation( location ) { 87 | 88 | this.start.copy ( location ); 89 | 90 | } 91 | 92 | setEndLocation( location ) { 93 | 94 | this.end.copy ( location ); 95 | 96 | } 97 | 98 | getDirection() { 99 | 100 | return this.end.clone().min( this.start ).normalize(); 101 | 102 | } 103 | 104 | setLength( lng ) { 105 | 106 | if ( lng > 0 ) this.length = lng; 107 | 108 | } 109 | 110 | setJoint( joint ) { 111 | 112 | this.joint = joint; 113 | 114 | } 115 | 116 | 117 | // GET 118 | 119 | getBoneConnectionPoint() { 120 | 121 | return this.boneConnectionPoint; 122 | 123 | } 124 | 125 | getDirectionUV () { 126 | 127 | return this.end.minus( this.start ).normalize(); 128 | 129 | } 130 | 131 | getLength(){ 132 | 133 | return this.start.distanceTo( this.end ); 134 | 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /src/core/Chain2D.js: -------------------------------------------------------------------------------- 1 | import { NONE, GLOBAL_ABSOLUTE, LOCAL_RELATIVE, LOCAL_ABSOLUTE, END, START, J_LOCAL, J_GLOBAL, MAX_VALUE } from '../constants.js'; 2 | import { math } from '../math/Math.js'; 3 | import { V2 } from '../math/V2.js'; 4 | import { Bone2D } from './Bone2D.js'; 5 | import { Tools } from './Tools.js'; 6 | 7 | 8 | export class Chain2D { 9 | 10 | constructor( color ) { 11 | 12 | this.isChain2D = true; 13 | this.tmpTarget = new V2(); 14 | 15 | this.bones = []; 16 | this.name = ''; 17 | 18 | this.solveDistanceThreshold = 1.0; 19 | this.minIterationChange = 0.01; 20 | this.maxIteration = 15; 21 | this.precision = 0.001; 22 | 23 | this.chainLength = 0; 24 | this.numBones = 0; 25 | 26 | this.baseLocation = new V2(); 27 | this.fixedBaseMode = true; 28 | 29 | this.baseboneConstraintType = NONE; 30 | 31 | this.baseboneConstraintUV = new V2(); 32 | this.baseboneRelativeConstraintUV = new V2(); 33 | 34 | this.lastTargetLocation = new V2( MAX_VALUE, MAX_VALUE ); 35 | this.lastBaseLocation = new V2( MAX_VALUE, MAX_VALUE ); 36 | 37 | this.boneConnectionPoint = END; 38 | 39 | this.currentSolveDistance = MAX_VALUE; 40 | this.connectedChainNumber = -1; 41 | this.connectedBoneNumber = -1; 42 | 43 | this.color = color || 0xFFFFFF; 44 | 45 | this.embeddedTarget = new V2(); 46 | this.useEmbeddedTarget = false; 47 | 48 | } 49 | 50 | clone() { 51 | 52 | let c = new this.constructor() 53 | 54 | c.solveDistanceThreshold = this.solveDistanceThreshold; 55 | c.minIterationChange = this.minIterationChange; 56 | c.maxIteration = this.maxIteration; 57 | c.precision = this.precision; 58 | 59 | c.bones = this.cloneBones(); 60 | c.baseLocation.copy( this.baseLocation ); 61 | c.lastTargetLocation.copy( this.lastTargetLocation ); 62 | c.lastBaseLocation.copy( this.lastBaseLocation ); 63 | 64 | // Copy the basebone constraint UV if there is one to copy 65 | if ( !(this.baseboneConstraintType === NONE) ){ 66 | c.baseboneConstraintUV.copy( this.baseboneConstraintUV ); 67 | c.baseboneRelativeConstraintUV.copy( this.baseboneRelativeConstraintUV ); 68 | } 69 | 70 | // Native copy by value for primitive members 71 | c.fixedBaseMode = this.fixedBaseMode; 72 | 73 | c.chainLength = this.chainLength; 74 | c.numBones = this.numBones; 75 | c.currentSolveDistance = this.currentSolveDistance; 76 | 77 | c.boneConnectionPoint = this.boneConnectionPoint; 78 | c.connectedChainNumber = this.connectedChainNumber; 79 | c.connectedBoneNumber = this.connectedBoneNumber; 80 | c.baseboneConstraintType = this.baseboneConstraintType; 81 | 82 | c.color = this.color; 83 | 84 | c.embeddedTarget = this.embeddedTarget.clone(); 85 | c.useEmbeddedTarget = this.useEmbeddedTarget; 86 | 87 | return c; 88 | 89 | } 90 | 91 | clear() { 92 | 93 | let i = this.numBones; 94 | while(i--){ 95 | this.removeBone(i); 96 | } 97 | 98 | } 99 | 100 | addBone( bone ) { 101 | 102 | if( bone.color === null ) bone.setColor( this.color ); 103 | 104 | // Add the new bone to the end of the ArrayList of bones 105 | this.bones.push( bone ); 106 | 107 | 108 | // If this is the basebone... 109 | if ( this.numBones === 0 ){ 110 | // ...then keep a copy of the fixed start location... 111 | this.baseLocation.copy( bone.start ); 112 | 113 | // ...and set the basebone constraint UV to be around the initial bone direction 114 | this.baseboneConstraintUV.copy( bone.getDirectionUV() ); 115 | 116 | } 117 | 118 | // Increment the number of bones in the chain and update the chain length 119 | this.numBones ++; 120 | 121 | // Increment the number of bones in the chain and update the chain length 122 | this.updateChainLength(); 123 | 124 | } 125 | 126 | removeBone( id ) { 127 | 128 | if ( id < this.numBones ){ 129 | // ...then remove the bone, decrease the bone count and update the chain length. 130 | this.bones.splice(id, 1) 131 | this.numBones --; 132 | this.updateChainLength(); 133 | } 134 | 135 | } 136 | 137 | addConsecutiveBone( directionUV, length, clockwiseDegs, anticlockwiseDegs, color ) { 138 | 139 | if ( this.numBones === 0 ){ Tools.error('Chain is empty ! need first bone'); return }; 140 | 141 | if( directionUV.isBone2D ) { // first argument is bone 142 | 143 | let bone = directionUV; 144 | 145 | // Validate the direction unit vector - throws an IllegalArgumentException if it has a magnitude of zero 146 | let dir = bone.getDirectionUV(); 147 | math.validateDirectionUV( dir ); 148 | 149 | // Validate the length of the bone - throws an IllegalArgumentException if it is not a positive value 150 | let len = bone.length; 151 | math.validateLength( len ); 152 | 153 | let prevBoneEnd = this.bones[ this.numBones-1 ].end; 154 | 155 | bone.setStartLocation( prevBoneEnd ); 156 | bone.setEndLocation( prevBoneEnd.plus( dir.multiplyScalar( len ) ) ); 157 | 158 | // Add a bone to the end of this IK chain 159 | this.addBone( bone ); 160 | 161 | } else if( directionUV.isVector2 ) { 162 | 163 | color = color || this.color; 164 | 165 | // Validate the direction unit vector - throws an IllegalArgumentException if it has a magnitude of zero 166 | math.validateDirectionUV( directionUV ); 167 | 168 | // Validate the length of the bone - throws an IllegalArgumentException if it is not a positive value 169 | math.validateLength( length ); 170 | 171 | // Get the end location of the last bone, which will be used as the start location of the new bone 172 | let prevBoneEnd = this.bones[ this.numBones-1 ].end; 173 | 174 | // Add a bone to the end of this IK chain 175 | this.addBone( new Bone2D( prevBoneEnd, null, directionUV.normalised(), length, clockwiseDegs, anticlockwiseDegs, color ) ); 176 | 177 | 178 | } 179 | 180 | } 181 | 182 | 183 | // ------------------------------- 184 | // GET 185 | // ------------------------------- 186 | 187 | getBoneConnectionPoint() { 188 | 189 | return this.boneConnectionPoint; 190 | 191 | } 192 | 193 | getConnectedBoneNumber() { 194 | 195 | return this.connectedBoneNumber; 196 | 197 | } 198 | 199 | getConnectedChainNumber(){ 200 | 201 | return this.connectedChainNumber; 202 | 203 | } 204 | 205 | getEmbeddedTarget() { 206 | 207 | return this.embeddedTarget; 208 | 209 | } 210 | 211 | getBaseboneConstraintType() { 212 | 213 | return this.baseboneConstraintType; 214 | 215 | } 216 | 217 | getBaseboneConstraintUV() { 218 | 219 | if ( !(this.baseboneConstraintType === NONE) ) return this.baseboneConstraintUV; 220 | 221 | } 222 | 223 | getBaseLocation() { 224 | 225 | return this.bones[0].start; 226 | 227 | } 228 | 229 | getEffectorLocation() { 230 | 231 | return this.bones[this.numBones-1].end; 232 | 233 | } 234 | 235 | getLastTargetLocation() { 236 | 237 | return this.lastTargetLocation; 238 | 239 | } 240 | 241 | getLiveChainLength() { 242 | 243 | let lng = 0; 244 | let i = this.numBones; 245 | while( i-- ) lng += this.bones[i].getLength(); 246 | return lng; 247 | 248 | } 249 | 250 | 251 | // ------------------------------- 252 | // SET 253 | // ------------------------------- 254 | 255 | setColor( color ) { 256 | 257 | this.color = color; 258 | let i = this.numBones; 259 | while( i-- ) this.bones[i].setColor( this.color ); 260 | 261 | } 262 | 263 | setBaseboneRelativeConstraintUV( constraintUV ) { 264 | 265 | this.baseboneRelativeConstraintUV = constraintUV; 266 | 267 | } 268 | 269 | setConnectedBoneNumber( boneNumber ) { 270 | 271 | this.connectedBoneNumber = boneNumber; 272 | 273 | } 274 | 275 | setConnectedChainNumber( chainNumber ) { 276 | 277 | this.connectedChainNumber = chainNumber; 278 | 279 | } 280 | 281 | setBoneConnectionPoint( point ) { 282 | 283 | this.boneConnectionPoint = point; 284 | 285 | } 286 | 287 | setBaseboneConstraintUV( constraintUV ) { 288 | 289 | math.validateDirectionUV( constraintUV ); 290 | this.baseboneConstraintUV.copy( constraintUV.normalised() ); 291 | 292 | } 293 | 294 | setBaseLocation( baseLocation ){ 295 | 296 | this.baseLocation.copy( baseLocation ); 297 | 298 | } 299 | 300 | setBaseboneConstraintType( value ) { 301 | 302 | this.baseboneConstraintType = value; 303 | 304 | } 305 | 306 | setFixedBaseMode( value ) { 307 | 308 | // Enforce that a chain connected to another chain stays in fixed base mode (i.e. it moves with the chain it's connected to instead of independently) 309 | if ( !value && this.connectedChainNumber !== -1) return; 310 | if ( this.baseboneConstraintType === GLOBAL_ABSOLUTE && !value ) return; 311 | // Above conditions met? Set the fixedBaseMode 312 | this.fixedBaseMode = value; 313 | 314 | } 315 | 316 | setMaxIterationAttempts( maxIteration ) { 317 | 318 | if ( maxIteration < 1 ) return; 319 | this.maxIteration = maxIteration; 320 | 321 | } 322 | 323 | setMinIterationChange( minIterationChange ) { 324 | 325 | if (minIterationChange < 0) return; 326 | this.minIterationChange = minIterationChange; 327 | 328 | } 329 | 330 | setSolveDistanceThreshold( solveDistance ) { 331 | 332 | if ( solveDistance < 0 ) return; 333 | this.solveDistanceThreshold = solveDistance; 334 | 335 | } 336 | 337 | // ------------------------------- 338 | // 339 | // UPDATE TARGET 340 | // 341 | // ------------------------------- 342 | 343 | solveForEmbeddedTarget() { 344 | 345 | if ( this.useEmbeddedTarget ) return this.solveForTarget( this.embeddedTarget ); 346 | 347 | } 348 | 349 | resetTarget(){ 350 | 351 | this.lastBaseLocation = new V2( MAX_VALUE, MAX_VALUE ); 352 | this.currentSolveDistance = MAX_VALUE; 353 | 354 | } 355 | 356 | 357 | // Solve the IK chain for this target to the best of our ability. 358 | // The end result of running this method is that the IK chain configuration is updated. 359 | 360 | // To minimuse CPU usage, this method dynamically aborts if: 361 | // - The solve distance (i.e. distance between the end effector and the target) is below the solveDistanceThreshold, 362 | // - A solution incrementally improves on the previous solution by less than the minIterationChange, or 363 | // - The number of attempts to solve the IK chain exceeds the maxIteration. 364 | 365 | solveForTarget( t ) { 366 | 367 | this.tmpTarget.set( t.x, t.y ); 368 | let p = this.precision; 369 | 370 | let isSameBaseLocation = this.lastBaseLocation.approximatelyEquals( this.baseLocation, p ); 371 | 372 | // If we have both the same target and base location as the last run then do not solve 373 | if ( this.lastTargetLocation.approximatelyEquals( this.tmpTarget, p ) && isSameBaseLocation ) return this.currentSolveDistance; 374 | 375 | // Keep starting solutions and distance 376 | let startingDistance; 377 | let startingSolution = null; 378 | 379 | // If the base location of a chain hasn't moved then we may opt to keep the current solution if our 380 | // best new solution is worse... 381 | if ( isSameBaseLocation ) { 382 | startingDistance = this.bones[ this.numBones-1 ].end.distanceTo( this.tmpTarget ); 383 | startingSolution = this.cloneBones(); 384 | } else { 385 | // Base has changed? Then we have little choice but to recalc the solution and take that new solution. 386 | startingDistance = MAX_VALUE; 387 | } 388 | 389 | // Not the same target? Then we must solve the chain for the new target. 390 | // We'll start by creating a list of bones to store our best solution 391 | let bestSolution = []; 392 | 393 | // We'll keep track of our best solve distance, starting it at a huge value which will be beaten on first attempt 394 | let bestSolveDistance = MAX_VALUE; 395 | let lastPassSolveDistance = MAX_VALUE; 396 | 397 | // Allow up to our iteration limit attempts at solving the chain 398 | let solveDistance; 399 | 400 | let i = this.maxIteration; 401 | 402 | while( i-- ){ 403 | 404 | // Solve the chain for this target 405 | solveDistance = this.solveIK( this.tmpTarget ); 406 | 407 | // Did we solve it for distance? If so, update our best distance and best solution, and also 408 | // update our last pass solve distance. Note: We will ALWAYS beat our last solve distance on the first run 409 | 410 | if ( solveDistance < bestSolveDistance ) { 411 | 412 | bestSolveDistance = solveDistance; 413 | bestSolution = this.cloneBones(); 414 | 415 | // If we are happy that this solution meets our distance requirements then we can exit the loop now 416 | if ( solveDistance <= this.solveDistanceThreshold ) break; 417 | 418 | } else { 419 | 420 | // Did not solve to our satisfaction? Okay... 421 | // Did we grind to a halt? If so break out of loop to set the best distance and solution that we have 422 | if ( Math.abs( solveDistance - lastPassSolveDistance ) < this.minIterationChange ) break; 423 | 424 | } 425 | 426 | // Update the last pass solve distance 427 | lastPassSolveDistance = solveDistance; 428 | 429 | } 430 | 431 | // Did we get a solution that's better than the starting solution's to the new target location? 432 | if ( bestSolveDistance < startingDistance ){ 433 | // If so, set the newly found solve distance and solution as the best found. 434 | this.currentSolveDistance = bestSolveDistance; 435 | this.bones = bestSolution; 436 | } else { 437 | // Did we make things worse? Then we keep our starting distance and solution! 438 | this.currentSolveDistance = startingDistance; 439 | this.bones = startingSolution; 440 | } 441 | 442 | // Update our last base and target locations so we know whether we need to solve for this start/end configuration next time 443 | this.lastBaseLocation.copy( this.baseLocation ); 444 | this.lastTargetLocation.copy( this.tmpTarget ); 445 | 446 | return this.currentSolveDistance; 447 | 448 | } 449 | 450 | // ------------------------------- 451 | // 452 | // SOLVE IK 453 | // 454 | // ------------------------------- 455 | 456 | // Solve the IK chain for the given target using the FABRIK algorithm. 457 | // retun the best solve distance found between the end-effector of this chain and the provided target. 458 | 459 | solveIK( target ) { 460 | 461 | if ( this.numBones === 0 ) return; 462 | 463 | let bone, boneLength, angle, nextBone, startPosition, endPosition, directionUV, baselineUV; 464 | 465 | // ---------- Forward pass from end effector to base ----------- 466 | 467 | // Loop over all bones in the chain, from the end effector (numBones-1) back to the basebone (0) 468 | let i = this.numBones; 469 | 470 | while( i-- ){ 471 | 472 | // Get the length of the bone we're working on 473 | bone = this.bones[i]; 474 | boneLength = bone.length; 475 | 476 | 477 | // If we are NOT working on the end effector bone 478 | if ( i !== this.numBones - 1 ) { 479 | 480 | nextBone = this.bones[i+1]; 481 | 482 | // Get the outer-to-inner unit vector of this bone 483 | directionUV = bone.getDirectionUV().negate(); 484 | 485 | // Get the outer-to-inner unit vector of the bone further out 486 | baselineUV = bone.joint.coordinateSystem === J_LOCAL ? nextBone.getDirectionUV().negate() : bone.getGlobalConstraintUV().negated(); 487 | directionUV.constrainedUV( baselineUV, nextBone.joint.min, nextBone.joint.max ); 488 | 489 | // At this stage we have a outer-to-inner unit vector for this bone which is within our constraints, 490 | // so we can set the new inner joint location to be the end joint location of this bone plus the 491 | // outer-to-inner direction unit vector multiplied by the length of the bone. 492 | startPosition = bone.end.plus( directionUV.multiplyScalar( boneLength ) ); 493 | 494 | // Set the new start joint location for this bone 495 | bone.setStartLocation( startPosition ); 496 | 497 | // If we are not working on the basebone, then we also set the end joint location of 498 | // the previous bone in the chain (i.e. the bone closer to the base) to be the new 499 | // start joint location of this bone. 500 | if ( i > 0 ) this.bones[i-1].setEndLocation( startPosition ); 501 | 502 | } else { // If we ARE working on the end effector bone... 503 | 504 | // Snap the end effector's end location to the target 505 | bone.setEndLocation( target ); 506 | 507 | // update directionUV 508 | directionUV = bone.getDirectionUV().negate(); 509 | 510 | if ( i > 0 ){ 511 | 512 | // The end-effector bone is NOT the basebone as well 513 | // Get the outer-to-inner unit vector of the bone further in 514 | baselineUV = bone.joint.coordinateSystem === J_LOCAL ? this.bones[i-1].getDirectionUV().negate() : bone.getGlobalConstraintUV().negated(); 515 | directionUV.constrainedUV( baselineUV, bone.joint.min, bone.joint.max ); 516 | 517 | } else { 518 | 519 | if(bone.joint.coordinateSystem !== J_LOCAL){ 520 | 521 | // Can constrain if constraining against global coordinate system 522 | baselineUV = bone.getGlobalConstraintUV().negated(); 523 | directionUV.constrainedUV( baselineUV, bone.joint.min, bone.joint.max ); 524 | 525 | } 526 | 527 | } 528 | 529 | // Calculate the new start joint location as the end joint location plus the outer-to-inner direction UV 530 | // multiplied by the length of the bone. 531 | startPosition = bone.end.plus( directionUV.multiplyScalar( boneLength ) ); 532 | 533 | // Set the new start joint location for this bone to be new start location... 534 | bone.setStartLocation( startPosition ); 535 | 536 | // ...and set the end joint location of the bone further in to also be at the new start location. 537 | if ( i > 0 ) this.bones[i-1].setEndLocation( startPosition ); 538 | 539 | } 540 | 541 | } // End of forward pass loop over all bones 542 | 543 | // ---------- Step 2 of 2 - Backward pass from base to end effector ----------- 544 | 545 | for ( i = 0; i < this.numBones; i++ ){ 546 | 547 | bone = this.bones[i]; 548 | boneLength = bone.length; 549 | 550 | // If we are not working on the basebone 551 | if ( i !== 0 ){ 552 | 553 | // Get the inner-to-outer direction of this bone as well as the previous bone to use as a baseline 554 | directionUV = bone.getDirectionUV(); 555 | // Constrain the angle between this bone and the inner bone. 556 | baselineUV = bone.joint.coordinateSystem === J_LOCAL ? this.bones[i-1].getDirectionUV() : bone.getGlobalConstraintUV(); 557 | directionUV.constrainedUV( baselineUV, bone.joint.min, bone.joint.max ); 558 | 559 | // At this stage we have an inner-to-outer unit vector for this bone which is within our constraints, 560 | // so we can set the new end location to be the start location of this bone plus the constrained 561 | // inner-to-outer direction unit vector multiplied by the length of this bone. 562 | endPosition = bone.start.plus( directionUV.multiplyScalar(boneLength) ); 563 | 564 | // Set the new end joint location for this bone 565 | bone.setEndLocation( endPosition ); 566 | 567 | // If we are not working on the end bone, then we set the start joint location of 568 | // the next bone in the chain (i.e. the bone closer to the end effector) to be the 569 | // new end joint location of this bone also. 570 | if ( i < this.numBones-1 ) this.bones[i+1].setStartLocation( endPosition ); 571 | 572 | } else {// If we ARE working on the base bone... 573 | 574 | // If the base location is fixed then snap the start location of the base bone back to the fixed base 575 | if ( this.fixedBaseMode ){ 576 | 577 | bone.setStartLocation( this.baseLocation ); 578 | 579 | } else {// If the base location is not fixed... 580 | 581 | // ...then set the new base bone start location to be its the end location minus the 582 | // bone direction multiplied by the length of the bone (i.e. projected backwards). 583 | startPosition = bone.end.minus( bone.getDirectionUV().multiplyScalar( boneLength ) ); 584 | bone.setStartLocation( startPosition ); 585 | 586 | } 587 | 588 | // update directionUV 589 | directionUV = bone.getDirectionUV(); 590 | 591 | // If the base bone is unconstrained then process it as usual... 592 | if ( this.baseboneConstraintType === NONE ){ 593 | 594 | // Calculate the new end location as the start location plus the direction multiplyScalar by the length of the bone 595 | endPosition = bone.start.plus( directionUV.multiplyScalar( boneLength ) ); 596 | 597 | // Set the new end joint location 598 | bone.setEndLocation( endPosition ); 599 | 600 | // Also, set the start location of the next bone to be the end location of this bone 601 | if ( this.numBones > 1 ) this.bones[1].setStartLocation( endPosition ); 602 | 603 | } else { 604 | 605 | // ...otherwise we must constrain it to the basebone constraint unit vector 606 | 607 | // LOCAL_ABSOLUTE? (i.e. local-space directional constraint) - then we must constraint about the relative basebone constraint UV... 608 | baselineUV = this.baseboneConstraintType === LOCAL_ABSOLUTE ? this.baseboneRelativeConstraintUV : this.baseboneConstraintUV; 609 | directionUV.constrainedUV( baselineUV, bone.joint.min, bone.joint.max ); 610 | 611 | //directionUV = bone.getDirectionUV(); 612 | 613 | // At this stage we have an inner-to-outer unit vector for this bone which is within our constraints, 614 | // so we can set the new end location to be the start location of this bone plus the constrained 615 | // inner-to-outer direction unit vector multiplied by the length of the bone. 616 | endPosition = bone.start.plus( directionUV.multiplyScalar( boneLength ) ); 617 | 618 | // Set the new end joint location for this bone 619 | bone.setEndLocation( endPosition ); 620 | 621 | // If we are not working on the end bone, then we set the start joint location of 622 | // the next bone in the chain (i.e. the bone closer to the end effector) to be the 623 | // new end joint location of this bone. 624 | if ( i < (this.numBones - 1) ) { this.bones[i+1].setStartLocation( endPosition ); } 625 | 626 | 627 | } // End of basebone constraint enforcement section 628 | 629 | } // End of base bone handling section 630 | 631 | } // End of backward-pass loop over all bones 632 | 633 | // Update our last target location 634 | this.lastTargetLocation.copy( target ); 635 | 636 | // ...and calculate and return the distance between the current effector location and the target. 637 | return this.bones[this.numBones-1].end.distanceTo( target ); 638 | 639 | } 640 | 641 | updateChainLength() { 642 | 643 | // Loop over all the bones in the chain, adding the length of each bone to the mChainLength property 644 | this.chainLength = 0; 645 | let i = this.numBones; 646 | while(i--) this.chainLength += this.bones[i].length; 647 | 648 | } 649 | 650 | cloneBones() { 651 | 652 | // Use clone to create a new Bone with the values from the source Bone. 653 | let chain = []; 654 | for ( let i = 0, n = this.bones.length; i < n; i++ ) chain.push( this.bones[i].clone() ); 655 | return chain; 656 | 657 | } 658 | 659 | } -------------------------------------------------------------------------------- /src/core/Chain3D.js: -------------------------------------------------------------------------------- 1 | import { NONE, GLOBAL_ROTOR, GLOBAL_HINGE, LOCAL_ROTOR, LOCAL_HINGE, J_BALL, J_GLOBAL, J_LOCAL, END, START, MAX_VALUE, PRECISION, PRECISION_DEG } from '../constants.js'; 2 | import { math } from '../math/Math.js'; 3 | import { V3 } from '../math/V3.js'; 4 | import { M3 } from '../math/M3.js'; 5 | import { Bone3D } from './Bone3D.js'; 6 | import { Tools } from './Tools.js'; 7 | 8 | 9 | export class Chain3D { 10 | 11 | constructor( color ) { 12 | 13 | this.isChain3D = true 14 | 15 | this.tmpTarget = new V3(); 16 | this.tmpMtx = new M3(); 17 | 18 | this.bones = []; 19 | this.name = ''; 20 | this.color = color || 0xFFFFFF; 21 | 22 | this.solveDistanceThreshold = 1.0; 23 | this.minIterationChange = 0.01; 24 | this.maxIteration = 20; 25 | this.precision = 0.001; 26 | 27 | this.chainLength = 0; 28 | this.numBones = 0; 29 | 30 | this.baseLocation = new V3(); 31 | this.fixedBaseMode = true; 32 | 33 | this.baseboneConstraintType = NONE; 34 | 35 | this.baseboneConstraintUV = new V3(); 36 | this.baseboneRelativeConstraintUV = new V3(); 37 | this.baseboneRelativeReferenceConstraintUV = new V3(); 38 | this.lastTargetLocation = new V3( MAX_VALUE, MAX_VALUE, MAX_VALUE ); 39 | 40 | this.lastBaseLocation = new V3( MAX_VALUE, MAX_VALUE, MAX_VALUE ); 41 | this.currentSolveDistance = MAX_VALUE; 42 | 43 | this.connectedChainNumber = -1; 44 | this.connectedBoneNumber = -1; 45 | this.boneConnectionPoint = END; 46 | 47 | // test full restrict angle 48 | this.isFullForward = false; 49 | 50 | 51 | 52 | this.embeddedTarget = new V3(); 53 | this.useEmbeddedTarget = false; 54 | 55 | } 56 | 57 | clone() { 58 | 59 | let c = new this.constructor(); 60 | 61 | c.solveDistanceThreshold = this.solveDistanceThreshold; 62 | c.minIterationChange = this.minIterationChange; 63 | c.maxIteration = this.maxIteration; 64 | c.precision = this.precision 65 | 66 | c.bones = this.cloneBones(); 67 | c.baseLocation.copy( this.baseLocation ); 68 | c.lastTargetLocation.copy( this.lastTargetLocation ); 69 | c.lastBaseLocation.copy( this.lastBaseLocation ); 70 | 71 | // Copy the basebone constraint UV if there is one to copy 72 | if ( !(this.baseboneConstraintType === NONE) ){ 73 | c.baseboneConstraintUV.copy( this.baseboneConstraintUV ); 74 | c.baseboneRelativeConstraintUV.copy( this.baseboneRelativeConstraintUV ); 75 | } 76 | 77 | // Native copy by value for primitive members 78 | c.fixedBaseMode = this.fixedBaseMode; 79 | 80 | c.chainLength = this.chainLength; 81 | c.numBones = this.numBones; 82 | c.currentSolveDistance = this.currentSolveDistance; 83 | 84 | c.boneConnectionPoint = this.boneConnectionPoint; 85 | c.connectedChainNumber = this.connectedChainNumber; 86 | c.connectedBoneNumber = this.connectedBoneNumber; 87 | c.baseboneConstraintType = this.baseboneConstraintType; 88 | 89 | c.color = this.color; 90 | 91 | c.embeddedTarget = this.embeddedTarget.clone(); 92 | c.useEmbeddedTarget = this.useEmbeddedTarget; 93 | 94 | return c; 95 | 96 | } 97 | 98 | clear() { 99 | 100 | let i = this.numBones; 101 | while(i--) this.removeBone(i); 102 | this.numBones = 0; 103 | 104 | } 105 | 106 | addBone( bone ) { 107 | 108 | bone.setColor( this.color ); 109 | 110 | // Add the new bone to the end of the ArrayList of bones 111 | this.bones.push( bone ); 112 | // Increment the number of bones in the chain and update the chain length 113 | this.numBones ++; 114 | 115 | // If this is the basebone... 116 | if ( this.numBones === 1 ){ 117 | // ...then keep a copy of the fixed start location... 118 | this.baseLocation.copy( bone.start ); 119 | 120 | // ...and set the basebone constraint UV to be around the initial bone direction 121 | this.baseboneConstraintUV.copy( bone.getDirectionUV() ); 122 | } 123 | 124 | // Increment the number of bones in the chain and update the chain length 125 | this.updateChainLength(); 126 | 127 | } 128 | 129 | removeBone( id ) { 130 | 131 | if ( id < this.numBones ){ 132 | // ...then remove the bone, decrease the bone count and update the chain length. 133 | this.bones.splice(id, 1) 134 | this.numBones --; 135 | this.updateChainLength(); 136 | 137 | } 138 | 139 | } 140 | 141 | addConsecutiveBone( directionUV, length ) { 142 | 143 | if (this.numBones > 0) { 144 | // Get the end location of the last bone, which will be used as the start location of the new bone 145 | // Add a bone to the end of this IK chain 146 | // Note: We use a normalised version of the bone direction 147 | this.addBone( new Bone3D( this.bones[ this.numBones-1 ].end, undefined, directionUV.normalised(), length ) ); 148 | } 149 | 150 | } 151 | 152 | addConsecutiveFreelyRotatingHingedBone( directionUV, length, type, hingeRotationAxis ) { 153 | 154 | this.addConsecutiveHingedBone( directionUV, length, type, hingeRotationAxis, 180, 180, math.genPerpendicularVectorQuick( hingeRotationAxis ) ); 155 | 156 | } 157 | 158 | addConsecutiveHingedBone( DirectionUV, length, type, HingeRotationAxis, clockwiseDegs, anticlockwiseDegs, hingeReferenceAxis ) { 159 | 160 | // Cannot add a consectuive bone of any kind if the there is no basebone 161 | if ( this.numBones === 0 ) return; 162 | 163 | // Normalise the direction and hinge rotation axis 164 | let directionUV = DirectionUV.normalised(); 165 | let hingeRotationAxis = HingeRotationAxis.normalised(); 166 | 167 | // Create a bone, get the end location of the last bone, which will be used as the start location of the new bone 168 | let bone = new Bone3D( this.bones[ this.numBones-1 ].end, undefined, directionUV, length, this.color ); 169 | 170 | type = type || 'global'; 171 | 172 | // ...set up a joint which we'll apply to that bone. 173 | bone.joint.setHinge( type === 'global' ? J_GLOBAL : J_LOCAL, hingeRotationAxis, clockwiseDegs, anticlockwiseDegs, hingeReferenceAxis ); 174 | 175 | // Finally, add the bone to this chain 176 | this.addBone( bone ); 177 | 178 | } 179 | 180 | addConsecutiveRotorConstrainedBone( boneDirectionUV, length, constraintAngleDegs ) { 181 | 182 | if (this.numBones === 0) return; 183 | 184 | // Create the bone starting at the end of the previous bone, set its direction, constraint angle and colour 185 | // then add it to the chain. Note: The default joint type of a new Bone is J_BALL. 186 | boneDirectionUV = boneDirectionUV.normalised(); 187 | let bone = new Bone3D( this.bones[ this.numBones-1 ].end, undefined , boneDirectionUV, length ); 188 | bone.joint.setAsBallJoint( constraintAngleDegs ); 189 | this.addBone( bone ); 190 | 191 | } 192 | 193 | // ------------------------------- 194 | // GET 195 | // ------------------------------- 196 | 197 | getBoneConnectionPoint() { 198 | 199 | return this.boneConnectionPoint; 200 | 201 | } 202 | 203 | getConnectedBoneNumber(){ 204 | 205 | return this.connectedBoneNumber; 206 | 207 | } 208 | 209 | getConnectedChainNumber(){ 210 | 211 | return this.connectedChainNumber; 212 | 213 | } 214 | 215 | getBaseboneConstraintType(){ 216 | 217 | return this.baseboneConstraintType; 218 | 219 | } 220 | 221 | getBaseboneConstraintUV(){ 222 | 223 | if ( !(this.baseboneConstraintType === NONE) ) return this.baseboneConstraintUV; 224 | 225 | } 226 | 227 | getBaseLocation() { 228 | 229 | return this.bones[0].start; 230 | 231 | } 232 | 233 | getEffectorLocation() { 234 | 235 | return this.bones[this.numBones-1].end; 236 | 237 | } 238 | 239 | getLastTargetLocation() { 240 | 241 | return this.lastTargetLocation; 242 | 243 | } 244 | 245 | getLiveChainLength() { 246 | 247 | let lng = 0; 248 | let i = this.numBones; 249 | while( i-- ) lng += this.bones[i].getLength(); 250 | return lng; 251 | 252 | } 253 | 254 | 255 | getBaseboneRelativeReferenceConstraintUV() { 256 | 257 | return this.baseboneRelativeReferenceConstraintUV; 258 | 259 | } 260 | 261 | // ------------------------------- 262 | // SET 263 | // ------------------------------- 264 | 265 | setConnectedBoneNumber( boneNumber ) { 266 | 267 | this.connectedBoneNumber = boneNumber; 268 | 269 | } 270 | 271 | setConnectedChainNumber( chainNumber ) { 272 | 273 | this.connectedChainNumber = chainNumber; 274 | 275 | } 276 | 277 | setBoneConnectionPoint( point ) { 278 | 279 | this.boneConnectionPoint = point; 280 | 281 | } 282 | 283 | setColor( c ) { 284 | 285 | this.color = c; 286 | let i = this.numBones; 287 | while( i-- ) this.bones[i].setColor( this.color ); 288 | 289 | } 290 | 291 | setBaseboneRelativeConstraintUV( uv ) { 292 | 293 | this.baseboneRelativeConstraintUV = uv.normalised(); 294 | 295 | } 296 | 297 | setBaseboneRelativeReferenceConstraintUV( uv ) { 298 | 299 | this.baseboneRelativeReferenceConstraintUV = uv.normalised(); 300 | 301 | } 302 | 303 | setBaseboneConstraintUV( uv ) { 304 | 305 | this.baseboneConstraintUV = uv.normalised(); 306 | 307 | } 308 | 309 | setRotorBaseboneConstraint( type, constraintAxis, angleDegs ) { 310 | 311 | // Sanity checking 312 | if (this.numBones === 0){ Tools.error("Chain must contain a basebone before we can specify the basebone constraint type."); return; } 313 | if ( !(constraintAxis.length() > 0) ){ Tools.error("Constraint axis cannot be zero."); return;} 314 | 315 | type = type || 'global'; 316 | // Set the constraint type, axis and angle 317 | this.baseboneConstraintType = type === 'global' ? GLOBAL_ROTOR : LOCAL_ROTOR; 318 | this.baseboneConstraintUV = constraintAxis.normalised(); 319 | this.baseboneRelativeConstraintUV.copy( this.baseboneConstraintUV ); 320 | this.bones[0].joint.setAsBallJoint( angleDegs ); 321 | 322 | } 323 | 324 | setHingeBaseboneConstraint( type, hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ) { 325 | 326 | // Sanity checking 327 | if ( this.numBones === 0){ Tools.error("Chain must contain a basebone before we can specify the basebone constraint type."); return; } 328 | if ( hingeRotationAxis.length() <= 0 ){ Tools.error("Hinge rotation axis cannot be zero."); return; } 329 | if ( hingeReferenceAxis.length() <= 0 ){ Tools.error("Hinge reference axis cannot be zero."); return; } 330 | if ( !( math.perpendicular( hingeRotationAxis, hingeReferenceAxis ) ) ){ Tools.error("The hinge reference axis must be in the plane of the hinge rotation axis, that is, they must be perpendicular."); return; } 331 | 332 | type = type || 'global'; 333 | 334 | // Set the constraint type, axis and angle 335 | this.baseboneConstraintType = type === 'global' ? GLOBAL_HINGE : LOCAL_HINGE; 336 | this.baseboneConstraintUV = hingeRotationAxis.normalised(); 337 | this.bones[0].joint.setHinge( type === 'global' ? J_GLOBAL : J_LOCAL, hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ); 338 | 339 | } 340 | 341 | setFreelyRotatingGlobalHingedBasebone( hingeRotationAxis ) { 342 | 343 | this.setHingeBaseboneConstraint( 'global', hingeRotationAxis, 180, 180, math.genPerpendicularVectorQuick( hingeRotationAxis ) ); 344 | } 345 | 346 | setGlobalHingedBasebone( hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ) { 347 | 348 | this.setHingeBaseboneConstraint( 'global', hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ); 349 | 350 | } 351 | 352 | setFreelyRotatingLocalHingedBasebone( hingeRotationAxis ) { 353 | 354 | this.setHingeBaseboneConstraint( 'local', hingeRotationAxis, 180, 180, math.genPerpendicularVectorQuick( hingeRotationAxis ) ); 355 | 356 | } 357 | 358 | setLocalHingedBasebone( hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ) { 359 | 360 | this.setHingeBaseboneConstraint( 'local', hingeRotationAxis, cwDegs, acwDegs, hingeReferenceAxis ); 361 | 362 | } 363 | 364 | setBaseLocation( baseLocation ) { 365 | 366 | this.baseLocation.copy( baseLocation ); 367 | 368 | } 369 | 370 | setFixedBaseMode( value ){ 371 | 372 | // Enforce that a chain connected to another chain stays in fixed base mode (i.e. it moves with the chain it's connected to instead of independently) 373 | if ( !value && this.connectedChainNumber !== -1) return; 374 | if ( this.baseboneConstraintType === GLOBAL_ROTOR && !value ) return; 375 | // Above conditions met? Set the fixedBaseMode 376 | this.fixedBaseMode = value; 377 | 378 | } 379 | 380 | setMaxIterationAttempts( maxIterations ) { 381 | 382 | if (maxIterations < 1) return; 383 | this.maxIteration = maxIterations; 384 | 385 | } 386 | 387 | setMinIterationChange( minIterationChange ) { 388 | 389 | if (minIterationChange < 0) return; 390 | this.minIterationChange = minIterationChange; 391 | 392 | } 393 | 394 | setSolveDistanceThreshold( solveDistance ) { 395 | 396 | if (solveDistance < 0) return; 397 | this.solveDistanceThreshold = solveDistance; 398 | 399 | } 400 | 401 | // ------------------------------- 402 | // 403 | // UPDATE TARGET 404 | // 405 | // ------------------------------- 406 | 407 | solveForEmbeddedTarget() { 408 | 409 | if ( this.useEmbeddedTarget ) return this.solveForTarget( this.embeddedTarget ); 410 | 411 | } 412 | 413 | resetTarget() { 414 | 415 | this.lastBaseLocation = new V3( MAX_VALUE, MAX_VALUE, MAX_VALUE ); 416 | this.currentSolveDistance = MAX_VALUE; 417 | 418 | } 419 | 420 | 421 | // Method to solve this IK chain for the given target location. 422 | // The end result of running this method is that the IK chain configuration is updated. 423 | 424 | // To minimuse CPU usage, this method dynamically aborts if: 425 | // - The solve distance (i.e. distance between the end effector and the target) is below the solveDistanceThreshold, 426 | // - A solution incrementally improves on the previous solution by less than the minIterationChange, or 427 | // - The number of attempts to solve the IK chain exceeds the maxIteration. 428 | 429 | solveForTarget( t ) { 430 | 431 | this.tmpTarget.set( t.x, t.y, t.z ); 432 | let p = this.precision; 433 | 434 | let isSameBaseLocation = this.lastBaseLocation.approximatelyEquals( this.baseLocation, p ); 435 | 436 | // If we have both the same target and base location as the last run then do not solve 437 | if ( this.lastTargetLocation.approximatelyEquals( this.tmpTarget, p ) && isSameBaseLocation ) return this.currentSolveDistance; 438 | 439 | // Keep starting solutions and distance 440 | let startingDistance; 441 | let startingSolution = null; 442 | 443 | // If the base location of a chain hasn't moved then we may opt to keep the current solution if our 444 | // best new solution is worse... 445 | if ( isSameBaseLocation ) { 446 | startingDistance = this.bones[ this.numBones-1 ].end.distanceTo( this.tmpTarget ); 447 | startingSolution = this.cloneBones(); 448 | } else { 449 | // Base has changed? Then we have little choice but to recalc the solution and take that new solution. 450 | startingDistance = MAX_VALUE; 451 | } 452 | 453 | /* 454 | * NOTE: We must allow the best solution of THIS run to be used for a new target or base location - we cannot 455 | * just use the last solution (even if it's better) - because that solution was for a different target / base 456 | * location combination and NOT for the current setup. 457 | */ 458 | 459 | // Not the same target? Then we must solve the chain for the new target. 460 | // We'll start by creating a list of bones to store our best solution 461 | let bestSolution = []; 462 | 463 | // We'll keep track of our best solve distance, starting it at a huge value which will be beaten on first attempt 464 | let bestSolveDistance = MAX_VALUE; 465 | let lastPassSolveDistance = MAX_VALUE; 466 | 467 | // Allow up to our iteration limit attempts at solving the chain 468 | let solveDistance; 469 | 470 | let i = this.maxIteration; 471 | 472 | while( i-- ){ 473 | 474 | // Solve the chain for this target 475 | solveDistance = this.solveIK( this.tmpTarget ); 476 | 477 | // Did we solve it for distance? If so, update our best distance and best solution, and also 478 | // update our last pass solve distance. Note: We will ALWAYS beat our last solve distance on the first run. 479 | if ( solveDistance < bestSolveDistance ) { 480 | 481 | bestSolveDistance = solveDistance; 482 | bestSolution = this.cloneBones(); 483 | 484 | // If we are happy that this solution meets our distance requirements then we can exit the loop now 485 | if ( solveDistance <= this.solveDistanceThreshold ) break; 486 | 487 | } else {// Did not solve to our satisfaction? Okay... 488 | 489 | // Did we grind to a halt? If so break out of loop to set the best distance and solution that we have 490 | if ( Math.abs( solveDistance - lastPassSolveDistance ) < this.minIterationChange ) break; 491 | 492 | } 493 | 494 | // Update the last pass solve distance 495 | lastPassSolveDistance = solveDistance; 496 | 497 | } // End of loop 498 | 499 | // Did we get a solution that's better than the starting solution's to the new target location? 500 | if ( bestSolveDistance < startingDistance ){ 501 | // If so, set the newly found solve distance and solution as the best found. 502 | this.currentSolveDistance = bestSolveDistance; 503 | this.bones = bestSolution; 504 | } else { 505 | // Did we make things worse? Then we keep our starting distance and solution! 506 | this.currentSolveDistance = startingDistance; 507 | this.bones = startingSolution; 508 | } 509 | 510 | // Update our base and target locations 511 | this.lastBaseLocation.copy( this.baseLocation ); 512 | this.lastTargetLocation.copy( this.tmpTarget ); 513 | 514 | return this.currentSolveDistance; 515 | 516 | } 517 | 518 | // ------------------------------- 519 | // 520 | // SOLVE IK 521 | // 522 | // ------------------------------- 523 | 524 | // Solve the IK chain for the given target using the FABRIK algorithm. 525 | // retun the best solve distance found between the end-effector of this chain and the provided target. 526 | 527 | solveIK( target ) { 528 | 529 | if ( this.numBones === 0 ) return; 530 | 531 | let bone, boneLength, joint, jointType, nextBone; 532 | let hingeRotationAxis, hingeReferenceAxis; 533 | let tmpMtx = this.tmpMtx; 534 | 535 | // ---------- Forward pass from end effector to base ----------- 536 | 537 | // Loop over all bones in the chain, from the end effector (numBones-1) back to the basebone (0) 538 | let i = this.numBones; 539 | 540 | while( i-- ){ 541 | 542 | 543 | // Get the length of the bone we're working on 544 | bone = this.bones[i]; 545 | boneLength = bone.length; 546 | joint = bone.joint; 547 | jointType = joint.type; 548 | 549 | // If we are NOT working on the end effector bone 550 | if ( i !== this.numBones - 1 ) { 551 | 552 | nextBone = this.bones[i+1]; 553 | 554 | // Get the outer-to-inner unit vector of the bone further out 555 | let outerBoneOuterToInnerUV = nextBone.getDirectionUV().negate(); 556 | 557 | // Get the outer-to-inner unit vector of this bone 558 | let boneOuterToInnerUV = bone.getDirectionUV().negate(); 559 | 560 | // Get the joint type for this bone and handle constraints on boneInnerToOuterUV 561 | 562 | /*if( this.isFullForward ){ 563 | 564 | switch ( jointType ) { 565 | case J_BALL: 566 | // Constrain to relative angle between this bone and the next bone if required 567 | boneOuterToInnerUV.limitAngle( outerBoneOuterToInnerUV, tmpMtx, nextBone.joint.rotor ); 568 | break; 569 | case J_GLOBAL: 570 | 571 | hingeRotationAxis = nextBone.joint.getHingeRotationAxis().negated(); 572 | hingeReferenceAxis = nextBone.joint.getHingeReferenceAxis().negated(); 573 | 574 | // Project this bone outer-to-inner direction onto the hinge rotation axis 575 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 576 | 577 | // NOTE: Constraining about the hinge reference axis on this forward pass leads to poor solutions... so we won't. 578 | if( !nextBone.joint.freeHinge ) boneOuterToInnerUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, nextBone.joint.min, nextBone.joint.max ); 579 | 580 | 581 | break; 582 | case J_LOCAL: 583 | 584 | 585 | if ( i > 0 ) {// Not a basebone? Then construct a rotation matrix based on the previous bones inner-to-to-inner direction... 586 | // ...and transform the hinge rotation axis into the previous bones frame of reference. 587 | 588 | tmpMtx.createRotationMatrix( outerBoneOuterToInnerUV ); 589 | hingeRotationAxis = nextBone.joint.getHingeRotationAxis().clone().negate().applyM3( tmpMtx ); 590 | hingeReferenceAxis = nextBone.joint.getHingeReferenceAxis().clone().negate().applyM3( tmpMtx ); 591 | 592 | 593 | 594 | } else {// ...basebone? Need to construct matrix from the relative constraint UV. 595 | 596 | hingeRotationAxis = this.baseboneRelativeConstraintUV.negated(); 597 | hingeReferenceAxis = this.baseboneRelativeReferenceConstraintUV.negated(); 598 | 599 | } 600 | 601 | // Project this bone's outer-to-inner direction onto the plane described by the relative hinge rotation axis 602 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 603 | 604 | // NOTE: Constraining about the hinge reference axis on this forward pass leads to poor solutions... so we won't. 605 | if( !nextBone.joint.freeHinge ){ 606 | 607 | boneOuterToInnerUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, nextBone.joint.min, nextBone.joint.max ); 608 | 609 | } 610 | break; 611 | } 612 | } else {*/ 613 | 614 | switch ( jointType ) { 615 | case J_BALL: 616 | // Constrain to relative angle between this bone and the next bone if required 617 | boneOuterToInnerUV.limitAngle( outerBoneOuterToInnerUV, tmpMtx, joint.rotor ); 618 | break; 619 | case J_GLOBAL: 620 | 621 | hingeRotationAxis = joint.getHingeRotationAxis(); 622 | 623 | // Project this bone outer-to-inner direction onto the hinge rotation axis 624 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 625 | 626 | // NOTE: Constraining about the hinge reference axis on this forward pass leads to poor solutions... so we won't. 627 | break; 628 | case J_LOCAL: 629 | 630 | 631 | if ( i > 0 ) {// Not a basebone? Then construct a rotation matrix based on the previous bones inner-to-to-inner direction... 632 | // ...and transform the hinge rotation axis into the previous bones frame of reference. 633 | 634 | tmpMtx.createRotationMatrix( this.bones[i-1].getDirectionUV() ); 635 | hingeRotationAxis = joint.getHingeRotationAxis().clone().applyM3( tmpMtx ); 636 | 637 | } else {// ...basebone? Need to construct matrix from the relative constraint UV. 638 | 639 | hingeRotationAxis = this.baseboneRelativeConstraintUV; 640 | 641 | } 642 | 643 | // Project this bone's outer-to-inner direction onto the plane described by the relative hinge rotation axis 644 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 645 | 646 | // NOTE: Constraining about the hinge reference axis on this forward pass leads to poor solutions... so we won't. 647 | break; 648 | } 649 | //} 650 | 651 | // At this stage we have a outer-to-inner unit vector for this bone which is within our constraints, 652 | // so we can set the new inner joint location to be the end joint location of this bone plus the 653 | // outer-to-inner direction unit vector multiplied by the length of the bone. 654 | let newStartLocation = bone.end.plus( boneOuterToInnerUV.multiplyScalar( boneLength ) ); 655 | 656 | // Set the new start joint location for this bone 657 | bone.setStartLocation( newStartLocation ); 658 | 659 | // If we are not working on the basebone, then we also set the end joint location of 660 | // the previous bone in the chain (i.e. the bone closer to the base) to be the new 661 | // start joint location of this bone. 662 | if (i > 0) this.bones[i-1].setEndLocation( newStartLocation ); 663 | 664 | } else { // If we ARE working on the end effector bone... 665 | 666 | // Snap the end effector's end location to the target 667 | bone.setEndLocation( target ); 668 | 669 | // Get the UV between the target / end-location (which are now the same) and the start location of this bone 670 | let boneOuterToInnerUV = bone.getDirectionUV().negated(); 671 | 672 | // If the end effector is global hinged then we have to snap to it, then keep that 673 | // resulting outer-to-inner UV in the plane of the hinge rotation axis 674 | switch ( jointType ) { 675 | case J_BALL: 676 | // Ball joints do not get constrained on this forward pass 677 | break; 678 | case J_GLOBAL: 679 | hingeRotationAxis = joint.getHingeRotationAxis(); 680 | // Global hinges get constrained to the hinge rotation axis, but not the reference axis within the hinge plane 681 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 682 | break; 683 | case J_LOCAL: 684 | // Local hinges get constrained to the hinge rotation axis, but not the reference axis within the hinge plane 685 | 686 | // Construct a rotation matrix based on the previous bones inner-to-to-inner direction... 687 | tmpMtx.createRotationMatrix( this.bones[i-1].getDirectionUV() ); 688 | 689 | // ...and transform the hinge rotation axis into the previous bones frame of reference. 690 | hingeRotationAxis = joint.getHingeRotationAxis().clone().applyM3( tmpMtx ); 691 | 692 | // Project this bone's outer-to-inner direction onto the plane described by the relative hinge rotation axis 693 | boneOuterToInnerUV.projectOnPlane( hingeRotationAxis ); 694 | break; 695 | } 696 | 697 | // Calculate the new start joint location as the end joint location plus the outer-to-inner direction UV 698 | // multiplied by the length of the bone. 699 | let newStartLocation = target.plus( boneOuterToInnerUV.multiplyScalar( boneLength ) ); 700 | 701 | // Set the new start joint location for this bone to be new start location... 702 | bone.setStartLocation( newStartLocation ); 703 | 704 | // ...and set the end joint location of the bone further in to also be at the new start location (if there IS a bone 705 | // further in - this may be a single bone chain) 706 | if (i > 0) this.bones[i-1].setEndLocation( newStartLocation ); 707 | 708 | } 709 | 710 | } // End of forward pass 711 | 712 | // ---------- Backward pass from base to end effector ----------- 713 | 714 | for ( i = 0; i < this.numBones; i++ ){ 715 | 716 | bone = this.bones[i]; 717 | boneLength = bone.length; 718 | joint = bone.joint; 719 | jointType = joint.type; 720 | 721 | // If we are not working on the basebone 722 | if ( i !== 0 ){ 723 | 724 | // Get the inner-to-outer direction of this bone as well as the previous bone to use as a baseline 725 | let boneInnerToOuterUV = bone.getDirectionUV(); 726 | 727 | let prevBoneInnerToOuterUV = this.bones[i-1].getDirectionUV(); 728 | 729 | //let hingeRotationAxis, hingeReferenceAxis; 730 | 731 | switch ( jointType ) { 732 | 733 | case J_BALL: 734 | 735 | // Keep this bone direction constrained within the rotor about the previous bone direction 736 | boneInnerToOuterUV.limitAngle( prevBoneInnerToOuterUV, tmpMtx, joint.rotor ); 737 | 738 | break; 739 | case J_GLOBAL: 740 | 741 | // Get the hinge rotation axis and project our inner-to-outer UV onto it 742 | hingeRotationAxis = joint.getHingeRotationAxis(); 743 | hingeReferenceAxis = joint.getHingeReferenceAxis(); 744 | 745 | boneInnerToOuterUV.projectOnPlane( hingeRotationAxis ); 746 | 747 | // Constrain rotation about reference axis if required 748 | if( !joint.freeHinge ) boneInnerToOuterUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, joint.min, joint.max ); 749 | 750 | break; 751 | case J_LOCAL: 752 | 753 | // Transform the hinge rotation axis to be relative to the previous bone in the chain 754 | // Construct a rotation matrix based on the previous bone's direction 755 | tmpMtx.createRotationMatrix( prevBoneInnerToOuterUV ); 756 | 757 | // Transform the hinge rotation axis into the previous bone's frame of reference 758 | hingeRotationAxis = joint.getHingeRotationAxis().clone().applyM3( tmpMtx ); 759 | hingeReferenceAxis = joint.getHingeReferenceAxis().clone().applyM3( tmpMtx ); 760 | 761 | 762 | // Project this bone direction onto the plane described by the hinge rotation axis 763 | // Note: The returned vector is normalised. 764 | boneInnerToOuterUV.projectOnPlane( hingeRotationAxis ); 765 | 766 | // Constrain rotation about reference axis if required 767 | if( !joint.freeHinge ) boneInnerToOuterUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, joint.min, joint.max ); 768 | 769 | break; 770 | 771 | } 772 | 773 | // At this stage we have a outer-to-inner unit vector for this bone which is within our constraints, 774 | // so we can set the new inner joint location to be the end joint location of this bone plus the 775 | // outer-to-inner direction unit vector multiplied by the length of the bone. 776 | let newEndLocation = bone.start.plus( boneInnerToOuterUV.multiplyScalar( boneLength ) ); 777 | 778 | // Set the new start joint location for this bone 779 | bone.setEndLocation( newEndLocation ); 780 | 781 | // If we are not working on the end effector bone, then we set the start joint location of the next bone in 782 | // the chain (i.e. the bone closer to the target) to be the new end joint location of this bone. 783 | if (i < (this.numBones - 1)) { this.bones[i+1].setStartLocation( newEndLocation ); } 784 | 785 | } else { // If we ARE working on the basebone... 786 | 787 | // If the base location is fixed then snap the start location of the basebone back to the fixed base... 788 | if ( this.fixedBaseMode ){ 789 | 790 | bone.setStartLocation( this.baseLocation ); 791 | 792 | } else { // ...otherwise project it backwards from the end to the start by its length. 793 | 794 | bone.setStartLocation( bone.end.minus( bone.getDirectionUV().multiplyScalar( boneLength ) ) ); 795 | 796 | } 797 | 798 | // Get the inner-to-outer direction of this bone 799 | let boneInnerToOuterUV = bone.getDirectionUV(); 800 | 801 | let hingeRotationAxis, hingeReferenceAxis; 802 | 803 | switch ( this.baseboneConstraintType ){ 804 | 805 | case NONE: // Nothing to do because there's no basebone constraint 806 | break; 807 | case GLOBAL_ROTOR: 808 | 809 | boneInnerToOuterUV.limitAngle( this.baseboneConstraintUV, tmpMtx, joint.rotor ); 810 | 811 | break; 812 | case LOCAL_ROTOR: 813 | 814 | boneInnerToOuterUV.limitAngle( this.baseboneRelativeConstraintUV, tmpMtx, joint.rotor ); 815 | 816 | break; 817 | case GLOBAL_HINGE: 818 | 819 | hingeRotationAxis = joint.getHingeRotationAxis(); 820 | hingeReferenceAxis = joint.getHingeReferenceAxis(); 821 | 822 | // Get the inner-to-outer direction of this bone and project it onto the global hinge rotation axis 823 | boneInnerToOuterUV.projectOnPlane( hingeRotationAxis ); 824 | 825 | // Constrain as necessary 826 | if( !joint.freeHinge ) boneInnerToOuterUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, joint.min, joint.max ); 827 | 828 | break; 829 | case LOCAL_HINGE: 830 | 831 | hingeRotationAxis = this.baseboneRelativeConstraintUV; 832 | hingeReferenceAxis = this.baseboneRelativeReferenceConstraintUV; 833 | 834 | // Get the inner-to-outer direction of this bone and project it onto the global hinge rotation axis 835 | //let boneInnerToOuterUV = bone.getDirectionUV(); 836 | boneInnerToOuterUV.projectOnPlane( hingeRotationAxis ); 837 | 838 | // Constrain as necessary 839 | if( !joint.freeHinge ) boneInnerToOuterUV.constrainedUV( hingeReferenceAxis, hingeRotationAxis, tmpMtx, joint.min, joint.max ); 840 | 841 | break; 842 | 843 | } 844 | 845 | 846 | // Set the new end location of this bone, and if there are more bones, 847 | // then set the start location of the next bone to be the end location of this bone 848 | let newEndLocation = bone.start.plus( boneInnerToOuterUV.multiplyScalar( boneLength ) ); 849 | bone.setEndLocation( newEndLocation ); 850 | 851 | if ( this.numBones > 1 ) { this.bones[1].setStartLocation( newEndLocation ); } 852 | 853 | } // End of basebone handling section 854 | 855 | } // End of backward-pass i over all bones 856 | 857 | // Update our last target location 858 | this.lastTargetLocation.copy( target ); 859 | 860 | // DEBUG - check the live chain length and the originally calculated chain length are the same 861 | // if (Math.abs( this.getLiveChainLength() - chainLength) > 0.01) Tools.error(""Chain length off by > 0.01"); 862 | 863 | // Finally, calculate and return the distance between the current effector location and the target. 864 | //return math.distanceBetween( this.bones[this.numBones-1].end, target ); 865 | return this.bones[this.numBones-1].end.distanceTo( target ); 866 | 867 | } 868 | 869 | updateChainLength() { 870 | 871 | // Loop over all the bones in the chain, adding the length of each bone to the chainLength property 872 | this.chainLength = 0; 873 | let i = this.numBones; 874 | while(i--) this.chainLength += this.bones[i].length; 875 | 876 | } 877 | 878 | cloneBones() { 879 | 880 | // Use clone to create a new Bone with the values from the source Bone. 881 | let chain = []; 882 | for ( let i = 0, n = this.bones.length; i < n; i++ ) chain.push( this.bones[i].clone() ); 883 | return chain; 884 | 885 | } 886 | 887 | } -------------------------------------------------------------------------------- /src/core/Joint2D.js: -------------------------------------------------------------------------------- 1 | import { math } from '../math/Math.js'; 2 | import { J_LOCAL, J_GLOBAL } from '../constants.js'; 3 | 4 | export class Joint2D { 5 | 6 | constructor( clockwise, antiClockwise, coordSystem ) { 7 | 8 | this.isJoint2D = true; 9 | 10 | this.coordinateSystem = coordSystem || J_LOCAL; 11 | 12 | if( clockwise < 0 ) clockwise *= -1; 13 | 14 | this.min = clockwise !== undefined ? -clockwise * math.toRad : -math.PI; 15 | this.max = antiClockwise !== undefined ? antiClockwise * math.toRad : math.PI; 16 | 17 | } 18 | 19 | clone() { 20 | 21 | let j = new this.constructor(); 22 | j.coordinateSystem = this.coordinateSystem; 23 | j.max = this.max; 24 | j.min = this.min; 25 | return j; 26 | 27 | } 28 | 29 | validateAngle( a ) { 30 | 31 | a = a < 0 ? 0 : a; 32 | a = a > 180 ? 180 : a; 33 | return a; 34 | 35 | } 36 | 37 | // SET 38 | 39 | set( joint ) { 40 | 41 | this.max = joint.max; 42 | this.min = joint.min; 43 | this.coordinateSystem = joint.coordinateSystem; 44 | 45 | } 46 | 47 | setClockwiseConstraintDegs( angle ) { 48 | 49 | // 0 to -180 degrees represents clockwise rotation 50 | if( angle < 0 ) angle *= -1; 51 | this.min = - (this.validateAngle( angle ) * math.toRad); 52 | 53 | } 54 | 55 | setAnticlockwiseConstraintDegs( angle ) { 56 | 57 | // 0 to 180 degrees represents anti-clockwise rotation 58 | this.max = this.validateAngle( angle ) * math.toRad; 59 | 60 | } 61 | 62 | setConstraintCoordinateSystem( coordSystem ) { 63 | 64 | this.coordinateSystem = coordSystem; 65 | 66 | } 67 | 68 | 69 | // GET 70 | 71 | getConstraintCoordinateSystem() { 72 | 73 | return this.coordinateSystem; 74 | 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/core/Joint3D.js: -------------------------------------------------------------------------------- 1 | import { V3 } from '../math/V3.js'; 2 | import { math } from '../math/Math.js'; 3 | import { J_BALL, J_GLOBAL, J_LOCAL } from '../constants.js'; 4 | 5 | 6 | export class Joint3D { 7 | 8 | constructor() { 9 | 10 | this.isJoint3D = true; 11 | 12 | this.rotor = math.PI; 13 | this.min = -math.PI; 14 | this.max = math.PI; 15 | 16 | this.freeHinge = true; 17 | 18 | this.rotationAxisUV = new V3(); 19 | this.referenceAxisUV = new V3(); 20 | this.type = J_BALL; 21 | 22 | } 23 | 24 | clone() { 25 | 26 | let j = new this.constructor(); 27 | j.type = this.type; 28 | j.rotor = this.rotor; 29 | j.max = this.max; 30 | j.min = this.min; 31 | j.freeHinge = this.freeHinge; 32 | j.rotationAxisUV.copy( this.rotationAxisUV ); 33 | j.referenceAxisUV.copy( this.referenceAxisUV ); 34 | 35 | return j 36 | 37 | } 38 | 39 | testAngle() { 40 | 41 | if( this.max === math.PI && this.min === -math.PI ) this.freeHinge = true; 42 | else this.freeHinge = false; 43 | 44 | } 45 | 46 | validateAngle( a ) { 47 | 48 | a = a < 0 ? 0 : a; 49 | a = a > 180 ? 180 : a; 50 | return a; 51 | 52 | } 53 | 54 | setAsBallJoint( angle ) { 55 | 56 | this.rotor = this.validateAngle( angle ) * math.toRad; 57 | this.type = J_BALL; 58 | 59 | } 60 | 61 | // Specify this joint to be a hinge with the provided settings 62 | 63 | setHinge( type, rotationAxis, clockwise, anticlockwise, referenceAxis ) { 64 | 65 | this.type = type; 66 | if( clockwise < 0 ) clockwise *= -1; 67 | this.min = -this.validateAngle( clockwise ) * math.toRad; 68 | this.max = this.validateAngle( anticlockwise ) * math.toRad; 69 | 70 | this.testAngle(); 71 | 72 | this.rotationAxisUV.copy( rotationAxis ).normalize(); 73 | this.referenceAxisUV.copy( referenceAxis ).normalize(); 74 | 75 | } 76 | 77 | // GET 78 | 79 | getHingeReferenceAxis() { 80 | 81 | return this.referenceAxisUV; 82 | 83 | } 84 | 85 | getHingeRotationAxis() { 86 | 87 | return this.rotationAxisUV; 88 | 89 | } 90 | 91 | // SET 92 | 93 | setBallJointConstraintDegs( angle ) { 94 | 95 | this.rotor = this.validateAngle( angle ) * math.toRad; 96 | 97 | } 98 | 99 | setHingeClockwise( angle ) { 100 | 101 | if( angle < 0 ) angle *= -1; 102 | this.min = -this.validateAngle( angle ) * math.toRad; 103 | this.testAngle(); 104 | 105 | } 106 | 107 | setHingeAnticlockwise( angle ) { 108 | 109 | this.max = this.validateAngle( angle ) * math.toRad; 110 | this.testAngle(); 111 | 112 | } 113 | 114 | /*setHingeRotationAxis: function ( axis ) { 115 | 116 | this.rotationAxisUV.copy( axis ).normalize(); 117 | 118 | }, 119 | 120 | setHingeReferenceAxis: function ( referenceAxis ) { 121 | 122 | this.referenceAxisUV.copy( referenceAxis ).normalize(); 123 | 124 | },*/ 125 | 126 | 127 | 128 | } -------------------------------------------------------------------------------- /src/core/Structure2D.js: -------------------------------------------------------------------------------- 1 | import { UP, NONE, GLOBAL_ABSOLUTE, LOCAL_RELATIVE, LOCAL_ABSOLUTE, END, START } from '../constants.js'; 2 | import { math } from '../math/Math.js'; 3 | import { V2 } from '../math/V2.js'; 4 | import { Tools } from './Tools.js'; 5 | 6 | export class Structure2D { 7 | 8 | constructor( scene, THREE ) { 9 | 10 | this.THREE = THREE 11 | 12 | this.isStructure2D = true; 13 | 14 | this.fixedBaseMode = true; 15 | 16 | this.chains = []; 17 | this.meshChains = []; 18 | this.angleChains= []; 19 | this.targets = []; 20 | this.numChains = 0; 21 | 22 | this.scene = scene || null; 23 | 24 | this.isWithMesh = false; 25 | 26 | } 27 | 28 | update() { 29 | 30 | //console.log('up') 31 | 32 | let chain, mesh, bone, target, tmp = new this.THREE.Vector3(); 33 | let hostChainNumber; 34 | let hostChain, hostBone, constraintType; 35 | let a, angle; 36 | 37 | for( let i = 0; i < this.numChains; i++ ){ 38 | 39 | chain = this.chains[i]; 40 | 41 | target = this.targets[i] || null; 42 | 43 | hostChainNumber = chain.getConnectedChainNumber(); 44 | 45 | // Get the basebone constraint type of the chain we're working on 46 | constraintType = chain.getBaseboneConstraintType(); 47 | 48 | // If this chain is not connected to another chain and the basebone constraint type of this chain is not global absolute 49 | // then we must update the basebone constraint UV for LOCAL_RELATIVE and the basebone relative constraint UV for LOCAL_ABSOLUTE connection types. 50 | // Note: For NONE or GLOBAL_ABSOLUTE we don't need to update anything before calling solveForTarget(). 51 | if ( hostChainNumber !== -1 && constraintType !== GLOBAL_ABSOLUTE ) { 52 | // Get the bone which this chain is connected to in the 'host' chain 53 | let hostBone = this.chains[ hostChainNumber ].bones[ chain.getConnectedBoneNumber() ]; 54 | 55 | chain.setBaseLocation( chain.getBoneConnectionPoint() === START ? hostBone.start : hostBone.end ); 56 | 57 | 58 | // If the basebone is constrained to the direction of the bone it's connected to... 59 | let hostBoneUV = hostBone.getDirectionUV(); 60 | 61 | if ( constraintType === LOCAL_RELATIVE ){ 62 | 63 | // ...then set the basebone constraint UV to be the direction of the bone we're connected to. 64 | chain.setBaseboneConstraintUV( hostBoneUV ); 65 | 66 | } else if ( constraintType === LOCAL_ABSOLUTE ) { 67 | 68 | // Note: LOCAL_ABSOLUTE directions are directions which are in the local coordinate system of the host bone. 69 | // For example, if the baseboneConstraintUV is Vec2f(-1.0f, 0.0f) [i.e. left], then the baseboneConnectionConstraintUV 70 | // will be updated to be left with regard to the host bone. 71 | 72 | // Get the angle between UP and the hostbone direction 73 | angle = UP.getSignedAngle( hostBoneUV ); 74 | 75 | // ...then apply that same rotation to this chain's basebone constraint UV to get the relative constraint UV... 76 | let relativeConstraintUV = chain.getBaseboneConstraintUV().clone().rotate( angle ); 77 | 78 | // ...which we then update. 79 | chain.setBaseboneRelativeConstraintUV( relativeConstraintUV ); 80 | 81 | } 82 | 83 | // NOTE: If the basebone constraint type is NONE then we don't do anything with the basebone constraint of the connected chain. 84 | 85 | } // End of if chain is connected to another chain section 86 | 87 | // Finally, update the target and solve the chain 88 | 89 | if ( !chain.useEmbeddedTarget ) chain.solveForTarget( target ); 90 | else console.log('embed', chain.solveForEmbeddedTarget()); 91 | 92 | 93 | // update 3d mesh 94 | 95 | if( this.isWithMesh ){ 96 | 97 | mesh = this.meshChains[i]; 98 | angle = this.angleChains[i]; 99 | 100 | for ( let j = 0; j < chain.numBones; j++ ) { 101 | 102 | bone = chain.bones[j]; 103 | mesh[j].position.set( bone.start.x, bone.start.y, 0 ); 104 | a = math.unwrapRad( -Math.atan2( bone.start.x - bone.end.x, bone.start.y - bone.end.y )); 105 | a = math.unwrapRad( -Math.atan2( bone.end.x - bone.start.x, bone.end.y - bone.start.y )); 106 | mesh[j].rotation.z = a 107 | angle[j] = a * math.toDeg; 108 | 109 | //mesh[j].lookAt( tmp.set( bone.end.x, bone.end.y, 0 ) ); 110 | } 111 | 112 | } 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | setFixedBaseMode( value ) { 120 | 121 | // Update our flag and set the fixed base mode on the first (i.e. 0th) chain in this structure. 122 | this.fixedBaseMode = value; 123 | let i = this.numChains, host; 124 | while(i--){ 125 | host = this.chains[i].getConnectedChainNumber(); 126 | if(host===-1) this.chains[i].setFixedBaseMode( this.fixedBaseMode ); 127 | } 128 | //this.chains[0].setFixedBaseMode( this.fixedBaseMode ); 129 | 130 | } 131 | 132 | clear() { 133 | 134 | this.clearAllBoneMesh(); 135 | 136 | let i, j; 137 | 138 | i = this.numChains; 139 | while(i--){ 140 | this.remove(i); 141 | } 142 | 143 | this.chains = []; 144 | this.meshChains = []; 145 | this.angleChains = []; 146 | this.targets = []; 147 | 148 | } 149 | 150 | add( chain, target, meshBone ) { 151 | 152 | this.chains.push( chain ); 153 | this.numChains ++; 154 | 155 | //if( target.isVector3 ) target = new V2(target.x, target.y); 156 | 157 | if(target) this.targets.push( target ); 158 | 159 | 160 | if( meshBone ) this.addChainMeshs( chain ); 161 | 162 | } 163 | 164 | remove( id ){ 165 | 166 | this.chains[id].clear(); 167 | this.chains.splice(id, 1); 168 | this.meshChains.splice(id, 1); 169 | this.angleChains.splice(id, 1); 170 | this.targets.splice(id, 1); 171 | this.numChains --; 172 | 173 | } 174 | 175 | /*setFixedBaseMode:function( fixedBaseMode ){ 176 | for ( let i = 0; i < this.numChains; i++) { 177 | this.chains[i].setFixedBaseMode( fixedBaseMode ); 178 | } 179 | },*/ 180 | 181 | getNumChains() { 182 | 183 | return this.numChains; 184 | 185 | } 186 | 187 | getChain( id ) { 188 | 189 | return this.chains[id]; 190 | 191 | } 192 | 193 | connectChain( Chain, chainNumber, boneNumber, point, target, meshBone, color ) { 194 | 195 | let c = chainNumber; 196 | let n = boneNumber; 197 | 198 | point = point || 'end'; 199 | 200 | if ( c > this.numChains ){ Tools.error('Chain not existe !'); return }; 201 | if ( n > this.chains[ c ].numBones ){ Tools.error('Bone not existe !'); return }; 202 | 203 | // Make a copy of the provided chain so any changes made to the original do not affect this chain 204 | let chain = Chain.clone(); 205 | 206 | chain.setBoneConnectionPoint( point === 'end' ? END : START ); 207 | chain.setConnectedChainNumber( c ); 208 | chain.setConnectedBoneNumber( n ); 209 | 210 | // The chain as we were provided should be centred on the origin, so we must now make it 211 | // relative to the start or end position of the given bone in the given chain. 212 | 213 | let position = point === 'end' ? this.chains[ c ].bones[ n ].end : this.chains[ c ].bones[ n ].start; 214 | 215 | //if ( connectionPoint === 'start' ) connectionLocation = this.chains[chainNumber].getBone(boneNumber).start; 216 | //else connectionLocation = this.chains[chainNumber].getBone(boneNumber).end; 217 | 218 | 219 | chain.setBaseLocation( position ); 220 | 221 | // When we have a chain connected to a another 'host' chain, the chain is which is connecting in 222 | // MUST have a fixed base, even though that means the base location is 'fixed' to the connection 223 | // point on the host chain, rather than a static location. 224 | chain.setFixedBaseMode( true ); 225 | 226 | // Translate the chain we're connecting to the connection point 227 | for ( let i = 0; i < chain.numBones; i++ ){ 228 | 229 | chain.bones[i].start.add( position ); 230 | chain.bones[i].end.add( position ); 231 | 232 | } 233 | 234 | this.add( chain, target, meshBone ); 235 | 236 | } 237 | 238 | // 3D THREE 239 | 240 | addChainMeshs( chain, id ) { 241 | 242 | this.isWithMesh = true; 243 | 244 | let meshBone = []; 245 | let angle = []; 246 | 247 | let lng = chain.bones.length; 248 | for(let i = 0; i this.numChains ) return; 187 | if ( boneNumber > this.chains[chainNumber].numBones ) return; 188 | 189 | // Make a copy of the provided chain so any changes made to the original do not affect this chain 190 | let chain = Chain.clone();//new Fullik.Chain( newChain ); 191 | if( color !== undefined ) chain.setColor( color ); 192 | 193 | // Connect the copy of the provided chain to the specified chain and bone in this structure 194 | //chain.connectToStructure( this, chainNumber, boneNumber ); 195 | 196 | chain.setBoneConnectionPoint( point === 'end' ? END : START ); 197 | chain.setConnectedChainNumber( c ); 198 | chain.setConnectedBoneNumber( n ); 199 | 200 | // The chain as we were provided should be centred on the origin, so we must now make it 201 | // relative to the start location of the given bone in the given chain. 202 | 203 | let position = point === 'end' ? this.chains[ c ].bones[ n ].end : this.chains[ c ].bones[ n ].start; 204 | 205 | 206 | chain.setBaseLocation( position ); 207 | // When we have a chain connected to a another 'host' chain, the chain is which is connecting in 208 | // MUST have a fixed base, even though that means the base location is 'fixed' to the connection 209 | // point on the host chain, rather than a static location. 210 | chain.setFixedBaseMode( true ); 211 | 212 | // Translate the chain we're connecting to the connection point 213 | for ( let i = 0; i < chain.numBones; i++ ){ 214 | 215 | chain.bones[i].start.add( position ); 216 | chain.bones[i].end.add( position ); 217 | 218 | } 219 | 220 | this.add( chain, target, meshBone ); 221 | 222 | } 223 | 224 | 225 | // 3D THREE 226 | 227 | addChainMeshs( chain, id ) { 228 | 229 | this.isWithMesh = true; 230 | 231 | let meshBone = []; 232 | let lng = chain.bones.length; 233 | for(let i = 0; i < lng; i++ ){ 234 | meshBone.push( this.addBoneMesh( chain.bones[i], i-1, meshBone, chain )); 235 | } 236 | 237 | this.meshChains.push( meshBone ); 238 | 239 | } 240 | 241 | addBoneMesh( bone, prev, ar, chain ) { 242 | 243 | let s = 2, r = 2, a1, a2, axe; 244 | let size = bone.length; 245 | let color = bone.color; 246 | let g = new this.THREE.CylinderGeometry ( 1, 0.5, size, 4 ); 247 | g.rotateX( -Math.PI * 0.5 ); 248 | g.translate( 0, 0, size*0.5 ); 249 | //g.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) ) 250 | //g.applyMatrix4( new this.THREE.Matrix4().makeTranslation( 0, 0, size*0.5 ) ); 251 | let m = new this.THREE.MeshStandardMaterial({ color:color, wireframe:false, shadowSide:this.THREE.DoubleSide }); 252 | 253 | let m2 = new this.THREE.MeshBasicMaterial({ wireframe : true }); 254 | //let m4 = new this.THREE.MeshBasicMaterial({ wireframe : true, color:color, transparent:true, opacity:0.3 }); 255 | 256 | let extraMesh = null; 257 | let extraGeo; 258 | 259 | let type = bone.joint.type; 260 | switch(type){ 261 | case J_BALL : 262 | m2.color.setHex(0xFF6600); 263 | let angle = bone.joint.rotor; 264 | 265 | if(angle === Math.PI) break; 266 | s = 2//size/4; 267 | r = 2;// 268 | extraGeo = new this.THREE.CylinderGeometry ( 0, r, s, 6,1, true ); 269 | extraGeo.rotateX( -Math.PI * 0.5 ); 270 | extraGeo.translate( 0, 0, s*0.5 ); 271 | //extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) ) 272 | //extraGeo.applyMatrix4( new this.THREE.Matrix4().makeTranslation( 0, 0, s*0.5 ) ); 273 | extraMesh = new this.THREE.Mesh( extraGeo, m2 ); 274 | break; 275 | case J_GLOBAL : 276 | axe = bone.joint.getHingeRotationAxis(); 277 | a1 = bone.joint.min; 278 | a2 = bone.joint.max; 279 | r = 2; 280 | //console.log('global', a1, a2) 281 | m2.color.setHex(0xFFFF00); 282 | extraGeo = new this.THREE.CircleGeometry( r, 12, a1, -a1+a2 ); 283 | 284 | 285 | extraGeo.rotateX( -Math.PI * 0.5 ); 286 | //extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) ); 287 | if( axe.z === 1 ) extraGeo.rotateX( -Math.PI * 0.5 );//extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) ); 288 | if( axe.y === 1 ) {extraGeo.rotateY( -Math.PI * 0.5 ); extraGeo.rotateX( -Math.PI * 0.5 ) }//{extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationY( -Math.PI*0.5 ) );extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) );} 289 | if( axe.x === 1 ) {extraGeo.rotateY( Math.PI * 0.5 );/*extraGeo.applyMatrix4(new this.THREE.Matrix4().makeRotationY( Math.PI*0.5 ));*/} 290 | 291 | extraMesh = new this.THREE.Mesh( extraGeo, m2 ); 292 | break; 293 | case J_LOCAL : 294 | 295 | axe = bone.joint.getHingeRotationAxis(); 296 | a1 = bone.joint.min; 297 | a2 = bone.joint.max; 298 | r = 2; 299 | 300 | m2.color.setHex(0x00FFFF); 301 | extraGeo = new this.THREE.CircleGeometry( r, 12, a1, -a1+a2 ); 302 | extraGeo.rotateX( -Math.PI * 0.5 ) 303 | 304 | // extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( -Math.PI*0.5 ) ); 305 | if( axe.z === 1 ) { extraGeo.rotateY( -Math.PI * 0.5 ); extraGeo.rotateX( Math.PI * 0.5 ); } 306 | if( axe.x === 1 ) extraGeo.rotateZ( -Math.PI * 0.5 ) 307 | if( axe.y === 1 ) { extraGeo.rotateX( Math.PI * 0.5 ); extraGeo.rotateY( Math.PI * 0.5 ) } 308 | 309 | /*if( axe.z === 1 ) { extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationY( -Math.PI*0.5 ) ); extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( Math.PI*0.5 ) );} 310 | if( axe.x === 1 ) extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationZ( -Math.PI*0.5 ) ); 311 | if( axe.y === 1 ) { extraGeo.applyMatrix4( new this.THREE.Matrix4().makeRotationX( Math.PI*0.5 ) ); extraGeo.applyMatrix4(new this.THREE.Matrix4().makeRotationY( Math.PI*0.5 ));}*/ 312 | 313 | extraMesh = new this.THREE.Mesh( extraGeo, m2 ); 314 | break; 315 | } 316 | 317 | const axis = new this.THREE.AxesHelper(2); 318 | //let bw = new this.THREE.Mesh( g, m4 ); 319 | 320 | let b = new this.THREE.Mesh( g, m ); 321 | 322 | b.add(axis); 323 | //b.add(bw); 324 | this.scene.add( b ); 325 | 326 | b.castShadow = true; 327 | b.receiveShadow = true; 328 | 329 | if( prev !== -1 ){ 330 | if( extraMesh !== null ){ 331 | if(type!==J_GLOBAL){ 332 | extraMesh.position.z = chain.bones[prev].length; 333 | ar[prev].add( extraMesh ); 334 | } else { 335 | b.add( extraMesh ); 336 | b.userData.extra = extraMesh; 337 | b.userData.axe = axe; 338 | //extraMesh.position=(b.position) 339 | //this.scene.add( extraMesh ) 340 | } 341 | 342 | } 343 | } else { 344 | if( extraMesh !== null ){ 345 | 346 | b.add( extraMesh ); 347 | b.userData.extra = extraMesh; 348 | b.userData.axe =new this.THREE.Vector3(0,0,1); 349 | //extraMesh.position=(b.position) 350 | //this.scene.add( extraMesh ) 351 | } 352 | } 353 | 354 | return b; 355 | 356 | } 357 | 358 | clearAllBoneMesh() { 359 | 360 | if(!this.isWithMesh) return; 361 | 362 | let i, j, b; 363 | 364 | i = this.meshChains.length; 365 | while(i--){ 366 | j = this.meshChains[i].length; 367 | while(j--){ 368 | b = this.meshChains[i][j]; 369 | this.scene.remove( b ); 370 | b.geometry.dispose(); 371 | b.material.dispose(); 372 | } 373 | this.meshChains[i] = []; 374 | } 375 | this.meshChains = []; 376 | 377 | } 378 | 379 | } -------------------------------------------------------------------------------- /src/core/Tools.js: -------------------------------------------------------------------------------- 1 | export const Tools = { 2 | 3 | error: function (str) { 4 | 5 | console.error( str ) 6 | 7 | }, 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/math/M3.js: -------------------------------------------------------------------------------- 1 | import { V3 } from './V3.js'; 2 | import { math } from './Math.js'; 3 | 4 | export class M3 { 5 | 6 | constructor(n11, n12, n13, n21, n22, n23, n31, n32, n33) { 7 | 8 | M3.prototype.isMatrix3 = true; 9 | 10 | this.elements = [ 11 | 12 | 1, 0, 0, 13 | 0, 1, 0, 14 | 0, 0, 1 15 | 16 | ]; 17 | 18 | if ( arguments.length > 0 ) { 19 | 20 | console.error( 'M3: the constructor no longer reads arguments. use .set() instead.' ); 21 | 22 | } 23 | if ( n11 !== undefined ) { 24 | 25 | this.set( n11, n12, n13, n21, n22, n23, n31, n32, n33 ); 26 | 27 | } 28 | 29 | } 30 | 31 | get XBasis(){ return new V3(this.elements[0], this.elements[1], this.elements[2]); } 32 | set XBasis(v){this.elements[0] = v.x; this.elements[1] = v.y; this.elements[2] = v.z;} 33 | 34 | get YBasis(){ return new V3(this.elements[3], this.elements[4], this.elements[5]); } 35 | set YBasis(v){this.elements[3] = v.x; this.elements[4] = v.y; this.elements[5] = v.z;} 36 | 37 | get ZBasis(){ return new V3(this.elements[6], this.elements[7], this.elements[8]); } 38 | set ZBasis(v){this.elements[6] = v.x; this.elements[7] = v.y; this.elements[8] = v.z;} 39 | 40 | 41 | set( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) { 42 | 43 | let te = this.elements; 44 | 45 | te[ 0 ] = n11; te[ 1 ] = n21; te[ 2 ] = n31; 46 | te[ 3 ] = n12; te[ 4 ] = n22; te[ 5 ] = n32; 47 | te[ 6 ] = n13; te[ 7 ] = n23; te[ 8 ] = n33; 48 | 49 | return this; 50 | 51 | } 52 | 53 | identity() { 54 | 55 | this.set( 56 | 57 | 1, 0, 0, 58 | 0, 1, 0, 59 | 0, 0, 1 60 | 61 | ); 62 | 63 | return this; 64 | 65 | } 66 | 67 | setV3( xAxis, yAxis, zAxis ) { 68 | 69 | const te = this.elements; 70 | 71 | te[ 0 ] = xAxis.x; 72 | te[ 3 ] = xAxis.y; 73 | te[ 6 ] = xAxis.z; 74 | 75 | te[ 1 ] = yAxis.x; 76 | te[ 4 ] = yAxis.y; 77 | te[ 7 ] = yAxis.z; 78 | 79 | te[ 2 ] = zAxis.x; 80 | te[ 5 ] = zAxis.y; 81 | te[ 8 ] = zAxis.z; 82 | 83 | return this; 84 | 85 | } 86 | 87 | transpose() { 88 | 89 | let tmp, m = this.elements; 90 | 91 | tmp = m[ 1 ]; m[ 1 ] = m[ 3 ]; m[ 3 ] = tmp; 92 | tmp = m[ 2 ]; m[ 2 ] = m[ 6 ]; m[ 6 ] = tmp; 93 | tmp = m[ 5 ]; m[ 5 ] = m[ 7 ]; m[ 7 ] = tmp; 94 | 95 | return this; 96 | 97 | } 98 | 99 | createRotationMatrix( referenceDirection ) { 100 | 101 | // NEW VERSION - 1.3.8 102 | 103 | let zAxis = referenceDirection; 104 | let xAxis = new V3(1, 0, 0); 105 | let yAxis = new V3(0, 1, 0); 106 | 107 | // Singularity fix 108 | if ( Math.abs( referenceDirection.y ) > 0.9999 ){ 109 | 110 | yAxis.copy( xAxis ).cross( zAxis ).normalize(); 111 | 112 | } else { 113 | 114 | xAxis.copy( zAxis ).cross( yAxis ).normalize(); 115 | yAxis.copy( xAxis ).cross( zAxis ).normalize(); 116 | 117 | } 118 | 119 | return this.setV3( xAxis, yAxis, zAxis ); 120 | 121 | } 122 | 123 | rotateDegs( axis, angleDegs ){ 124 | return this.rotateRads(axis, angleDegs * math.toRad); 125 | } 126 | 127 | rotateRads( axis, angle ){ 128 | 129 | const dest = new this.constructor(); 130 | let sin = Math.sin( angle ); 131 | let cos = Math.cos( angle ); 132 | let oneMinusCos = 1.0 - cos; 133 | 134 | let xy = axis.x * axis.y; 135 | let yz = axis.y * axis.z; 136 | let xz = axis.x * axis.z; 137 | let xs = axis.x * sin; 138 | let ys = axis.y * sin; 139 | let zs = axis.z * sin; 140 | 141 | let f00 = axis.x * axis.x * oneMinusCos + cos; 142 | let f01 = xy * oneMinusCos + zs; 143 | let f02 = xz * oneMinusCos - ys; 144 | 145 | let f10 = xy * oneMinusCos - zs; 146 | let f11 = axis.y * axis.y * oneMinusCos + cos; 147 | let f12 = yz * oneMinusCos + xs; 148 | 149 | let f20 = xz * oneMinusCos + ys; 150 | let f21 = yz * oneMinusCos - xs; 151 | let f22 = axis.z * axis.z * oneMinusCos + cos; 152 | 153 | let m = this.elements 154 | 155 | let t00 = m[0] * f00 + m[3] * f01 + m[6] * f02; 156 | let t01 = m[1] * f00 + m[4] * f01 + m[7] * f02; 157 | let t02 = m[2] * f00 + m[5] * f01 + m[8] * f02; 158 | 159 | let t10 = m[0] * f10 + m[3] * f11 + m[6] * f12; 160 | let t11 = m[1] * f10 + m[4] * f11 + m[7] * f12; 161 | let t12 = m[2] * f10 + m[5] * f11 + m[8] * f12; 162 | 163 | let k = //dest.elements 164 | // Construct and return rotation matrix 165 | k[6] = m[0] * f20 + m[3] * f21 + m[6] * f22; 166 | k[7] = m[1] * f20 + m[4] * f21 + m[7] * f22; 167 | k[8] = m[2] * f20 + m[5] * f21 + m[8] * f22; 168 | 169 | k[0] = t00; 170 | k[1] = t01; 171 | k[2] = t02; 172 | 173 | k[3] = t10; 174 | k[4] = t11; 175 | k[5] = t12; 176 | 177 | return dest; 178 | 179 | } 180 | 181 | rotateAboutAxis( v, angle, axis ){ 182 | 183 | let sin = Math.sin( angle ); 184 | let cos = Math.cos( angle ); 185 | let oneMinuscos = 1.0 - cos; 186 | 187 | // It's quicker to pre-calc these and reuse than calculate x * y, then y * x later (same thing). 188 | let xyOne = axis.x * axis.y * oneMinuscos; 189 | let xzOne = axis.x * axis.z * oneMinuscos; 190 | let yzOne = axis.y * axis.z * oneMinuscos; 191 | 192 | let te = this.elements; 193 | 194 | // Calculate rotated x-axis 195 | te[ 0 ] = axis.x * axis.x * oneMinuscos + cos; 196 | te[ 3 ] = xyOne + axis.z * sin; 197 | te[ 6 ] = xzOne - axis.y * sin; 198 | 199 | // Calculate rotated y-axis 200 | te[ 1 ] = xyOne - axis.z * sin; 201 | te[ 4 ] = axis.y * axis.y * oneMinuscos + cos; 202 | te[ 7 ] = yzOne + axis.x * sin; 203 | 204 | // Calculate rotated z-axis 205 | te[ 2 ] = xzOne + axis.y * sin; 206 | te[ 5 ] = yzOne - axis.x * sin; 207 | te[ 8 ] = axis.z * axis.z * oneMinuscos + cos; 208 | 209 | // this.identity() 210 | const mm = this//.rotateRads(axis, angle); 211 | 212 | // Multiply the source by the rotation matrix we just created to perform the rotation 213 | return v.clone().applyM3( mm ); 214 | 215 | } 216 | 217 | 218 | /////// 219 | 220 | determinant() { 221 | 222 | const te = this.elements; 223 | 224 | const a = te[ 0 ], b = te[ 1 ], c = te[ 2 ], 225 | d = te[ 3 ], e = te[ 4 ], f = te[ 5 ], 226 | g = te[ 6 ], h = te[ 7 ], i = te[ 8 ]; 227 | 228 | return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g; 229 | 230 | } 231 | 232 | invert() { 233 | 234 | const te = this.elements, 235 | 236 | n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], 237 | n12 = te[ 3 ], n22 = te[ 4 ], n32 = te[ 5 ], 238 | n13 = te[ 6 ], n23 = te[ 7 ], n33 = te[ 8 ], 239 | 240 | t11 = n33 * n22 - n32 * n23, 241 | t12 = n32 * n13 - n33 * n12, 242 | t13 = n23 * n12 - n22 * n13, 243 | 244 | det = n11 * t11 + n21 * t12 + n31 * t13; 245 | 246 | if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0 ); 247 | 248 | const detInv = 1 / det; 249 | 250 | te[ 0 ] = t11 * detInv; 251 | te[ 1 ] = ( n31 * n23 - n33 * n21 ) * detInv; 252 | te[ 2 ] = ( n32 * n21 - n31 * n22 ) * detInv; 253 | 254 | te[ 3 ] = t12 * detInv; 255 | te[ 4 ] = ( n33 * n11 - n31 * n13 ) * detInv; 256 | te[ 5 ] = ( n31 * n12 - n32 * n11 ) * detInv; 257 | 258 | te[ 6 ] = t13 * detInv; 259 | te[ 7 ] = ( n21 * n13 - n23 * n11 ) * detInv; 260 | te[ 8 ] = ( n22 * n11 - n21 * n12 ) * detInv; 261 | 262 | return this; 263 | 264 | } 265 | 266 | multiplyMatrices( a, b ) { 267 | 268 | const ae = a.elements; 269 | const be = b.elements; 270 | const te = this.elements; 271 | 272 | const a11 = ae[ 0 ], a12 = ae[ 3 ], a13 = ae[ 6 ]; 273 | const a21 = ae[ 1 ], a22 = ae[ 4 ], a23 = ae[ 7 ]; 274 | const a31 = ae[ 2 ], a32 = ae[ 5 ], a33 = ae[ 8 ]; 275 | 276 | const b11 = be[ 0 ], b12 = be[ 3 ], b13 = be[ 6 ]; 277 | const b21 = be[ 1 ], b22 = be[ 4 ], b23 = be[ 7 ]; 278 | const b31 = be[ 2 ], b32 = be[ 5 ], b33 = be[ 8 ]; 279 | 280 | te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31; 281 | te[ 3 ] = a11 * b12 + a12 * b22 + a13 * b32; 282 | te[ 6 ] = a11 * b13 + a12 * b23 + a13 * b33; 283 | 284 | te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31; 285 | te[ 4 ] = a21 * b12 + a22 * b22 + a23 * b32; 286 | te[ 7 ] = a21 * b13 + a22 * b23 + a23 * b33; 287 | 288 | te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31; 289 | te[ 5 ] = a31 * b12 + a32 * b22 + a33 * b32; 290 | te[ 8 ] = a31 * b13 + a32 * b23 + a33 * b33; 291 | 292 | return this; 293 | 294 | } 295 | 296 | premultiply( m ) { 297 | 298 | return this.multiplyMatrices( m, this ); 299 | 300 | } 301 | 302 | multiply( m ) { 303 | 304 | return this.multiplyMatrices( this, m ); 305 | 306 | } 307 | 308 | setFromMatrix4( m ) { 309 | 310 | const me = m.elements; 311 | 312 | this.set( 313 | 314 | me[ 0 ], me[ 4 ], me[ 8 ], 315 | me[ 1 ], me[ 5 ], me[ 9 ], 316 | me[ 2 ], me[ 6 ], me[ 10 ] 317 | 318 | ); 319 | 320 | return this; 321 | 322 | } 323 | 324 | copy( m ) { 325 | 326 | const te = this.elements; 327 | const me = m.elements; 328 | 329 | te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ]; 330 | te[ 3 ] = me[ 3 ]; te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ]; 331 | te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ]; te[ 8 ] = me[ 8 ]; 332 | 333 | return this; 334 | 335 | } 336 | 337 | fromArray( array, offset = 0 ) { 338 | 339 | for ( let i = 0; i < 9; i ++ ) { 340 | 341 | this.elements[ i ] = array[ i + offset ]; 342 | 343 | } 344 | 345 | return this; 346 | 347 | } 348 | 349 | toArray( array = [], offset = 0 ) { 350 | 351 | const te = this.elements; 352 | 353 | array[ offset ] = te[ 0 ]; 354 | array[ offset + 1 ] = te[ 1 ]; 355 | array[ offset + 2 ] = te[ 2 ]; 356 | 357 | array[ offset + 3 ] = te[ 3 ]; 358 | array[ offset + 4 ] = te[ 4 ]; 359 | array[ offset + 5 ] = te[ 5 ]; 360 | 361 | array[ offset + 6 ] = te[ 6 ]; 362 | array[ offset + 7 ] = te[ 7 ]; 363 | array[ offset + 8 ] = te[ 8 ]; 364 | 365 | return array; 366 | 367 | } 368 | 369 | clone() { 370 | 371 | return new this.constructor().fromArray( this.elements ); 372 | 373 | } 374 | 375 | isOrthogonal(){ 376 | 377 | const xCrossYDot = this.XBasis.dot(this.YBasis); 378 | const xCrossZDot = this.XBasis.dot(this.ZBasis); 379 | const yCrossZDot = this.YBasis.dot(this.ZBasis); 380 | if ( math.nearEquals(xCrossYDot, 0.0, 0.01) && math.nearEquals(xCrossZDot, 0.0, 0.01) && math.nearEquals(yCrossZDot, 0.0, 0.01) ) return true; 381 | return false; 382 | 383 | } 384 | 385 | } -------------------------------------------------------------------------------- /src/math/Math - Copie.js: -------------------------------------------------------------------------------- 1 | import { Tools } from '../core/Tools.js'; 2 | 3 | export const _Math = { 4 | 5 | toRad: Math.PI / 180, 6 | toDeg: 180 / Math.PI, 7 | pi90: Math.PI * 0.5, 8 | twoPI: Math.PI * 2, 9 | 10 | // Center point is p1; angle returned in Radians 11 | //findAngle: function ( p0, p1, p2 ) { 12 | findAngle: function ( b0, b1 ) { 13 | 14 | /*let a = p1.minus(p2).lengthSq(), 15 | b = p1.minus(p0).lengthSq(), 16 | c = p2.minus(p0).lengthSq(),*/ 17 | let a = b0.end.minus(b1.end).lengthSq(), 18 | b = b0.end.minus(b0.start).lengthSq(), 19 | c = b1.end.minus(b0.start).lengthSq(), 20 | angle, r, sign; 21 | 22 | r = ( a + b - c ) / Math.sqrt( 4 * a * b ); 23 | if( r <= -1 ) angle = Math.PI; 24 | else if( r >= 1 ) angle = 0; 25 | else angle = Math.acos( r ); 26 | // sign 27 | sign = b0.end.x * b1.end.y - b0.end.y * b1.end.x; 28 | return sign >= 0 ? angle : -angle; 29 | 30 | }, 31 | 32 | clamp: function ( v, min, max ) { 33 | 34 | v = v < min ? min : v; 35 | v = v > max ? max : v; 36 | return v; 37 | 38 | }, 39 | 40 | /*clamp: function ( value, min, max ) { 41 | 42 | return Math.max( min, Math.min( max, value ) ); 43 | 44 | },*/ 45 | 46 | lerp: function ( x, y, t ) { 47 | 48 | return ( 1 - t ) * x + t * y; 49 | 50 | }, 51 | 52 | rand: function ( low, high ) { 53 | 54 | return low + Math.random() * ( high - low ); 55 | 56 | }, 57 | 58 | randInt: function ( low, high ) { 59 | 60 | return low + Math.floor( Math.random() * ( high - low + 1 ) ); 61 | 62 | }, 63 | 64 | nearEquals: function (a, b, t) { 65 | 66 | return Math.abs(a - b) <= t ? true : false; 67 | 68 | }, 69 | 70 | perpendicular: function ( a, b ) { 71 | 72 | return _Math.nearEquals( a.dot(b), 0.0, 0.01 ) ? true : false; 73 | 74 | //return _Math.nearEquals( _Math.dotProduct(a, b), 0.0, 0.01 ) ? true : false; 75 | 76 | }, 77 | 78 | genPerpendicularVectorQuick: function ( v ) { 79 | 80 | //return _Math.genPerpendicularVectorFrisvad( v ); 81 | 82 | let p = v.clone(); 83 | // cross(v, UP) : cross(v, RIGHT) 84 | return Math.abs( v.y ) < 0.99 ? p.set( -v.z, 0, v.x ).normalize() : p.set( 0, v.z, -v.y ).normalize(); 85 | 86 | }, 87 | 88 | /*genPerpendicularVectorHM: function ( v ) { 89 | 90 | let a = v.abs(); 91 | let b = v.clone(); 92 | if (a.x <= a.y && a.x <= a.z) return b.set(0, -v.z, v.y).normalize(); 93 | else if (a.y <= a.x && a.y <= a.z) return b.set(-v.z, 0, v.x).normalize(); 94 | else return b.set(-v.y, v.x, 0).normalize(); 95 | 96 | },*/ 97 | 98 | genPerpendicularVectorFrisvad: function ( v ) { 99 | 100 | let nv = v.clone(); 101 | if ( v.z < -0.9999999 ) return nv.set(0, -1, 0);// Handle the singularity 102 | let a = 1/(1 + v.z); 103 | return nv.set( 1 - v.x * v.x * a, - v.x * v.y * a, -v.x ).normalize(); 104 | 105 | }, 106 | 107 | // rotation 108 | 109 | rotateXDegs: function ( v, angle ) { return v.clone().rotate( angle * _Math.toRad, 'X' ); }, 110 | rotateYDegs: function ( v, angle ) { return v.clone().rotate( angle * _Math.toRad, 'Y' ) }, 111 | rotateZDegs: function ( v, angle ) { return v.clone().rotate( angle * _Math.toRad, 'Z' ) }, 112 | 113 | // distance 114 | 115 | withinManhattanDistance: function ( v1, v2, distance ) { 116 | 117 | if (Math.abs(v2.x - v1.x) > distance) return false; // Too far in x direction 118 | if (Math.abs(v2.y - v1.y) > distance) return false; // Too far in y direction 119 | if (Math.abs(v2.z - v1.z) > distance) return false; // Too far in z direction 120 | return true; 121 | 122 | }, 123 | 124 | manhattanDistanceBetween: function ( v1, v2 ) { 125 | 126 | return Math.abs(v2.x - v1.x) + Math.abs(v2.x - v1.x) + Math.abs(v2.x - v1.x); 127 | 128 | }, 129 | 130 | distanceBetween: function ( v1, v2 ) { 131 | 132 | let dx = v2.x - v1.x; 133 | let dy = v2.y - v1.y; 134 | let dz = v1.z !== undefined ? v2.z - v1.z : 0; 135 | return Math.sqrt( dx * dx + dy * dy + dz * dz ); 136 | 137 | }, 138 | 139 | unwrapDeg: ( r ) => (r - (Math.floor((r + 180)/360))*360), 140 | unwrapRad: ( r ) => (r - (Math.floor((r + Math.PI)/(2*Math.PI)))*2*Math.PI), 141 | 142 | 143 | /*unwrapDeg: function ( r ) { 144 | 145 | r = r % 360; 146 | if (r > 180) r -= 360; 147 | if (r < -180) r += 360; 148 | return r; 149 | 150 | }, 151 | 152 | unwrapRad: function( r ){ 153 | 154 | r = r % _Math.twoPI; 155 | if (r > Math.Pi ) r -= _Math.twoPI; 156 | if (r < - Math.Pi ) r += _Math.twoPI; 157 | return r; 158 | 159 | },*/ 160 | 161 | 162 | // ______________________________ 2D _____________________________ 163 | 164 | rotateDegs: function( v, angle ) { 165 | 166 | return v.clone().rotate( angle * _Math.toRad ) 167 | 168 | }, 169 | 170 | 171 | validateDirectionUV: function( directionUV ) { 172 | 173 | if( directionUV.length() < 0 ) Tools.error("vector direction unit vector cannot be zero."); 174 | 175 | }, 176 | 177 | validateLength: function( length ) { 178 | 179 | if( length < 0 ) Tools.error("Length must be a greater than or equal to zero."); 180 | 181 | }, 182 | 183 | 184 | 185 | } -------------------------------------------------------------------------------- /src/math/Math.js: -------------------------------------------------------------------------------- 1 | import { Tools } from '../core/Tools.js'; 2 | 3 | export const math = { 4 | 5 | PI:Math.PI, 6 | toRad: Math.PI / 180, 7 | toDeg: 180 / Math.PI, 8 | pi90: Math.PI * 0.5, 9 | twoPI: Math.PI * 2, 10 | 11 | // Center point is p1; angle returned in Radians 12 | //findAngle: function ( p0, p1, p2 ) { 13 | findAngle: function ( b0, b1 ) { 14 | 15 | //return Math.atan2( b1.end - b0.end, b1.start - b0.start ) 16 | 17 | /*let a = p1.minus(p2).lengthSq(), 18 | b = p1.minus(p0).lengthSq(), 19 | c = p2.minus(p0).lengthSq(),*/ 20 | let a = b0.end.minus(b1.end).lengthSq(), 21 | b = b0.end.minus(b0.start).lengthSq(), 22 | c = b1.end.minus(b0.start).lengthSq(), 23 | angle, r, sign; 24 | 25 | r = ( a + b - c ) / Math.sqrt( 4 * a * b ); 26 | if( r <= -1 ) angle = Math.PI; 27 | else if( r >= 1 ) angle = 0; 28 | else angle = Math.acos( r ); 29 | // sign 30 | sign = b0.end.x * b1.end.y - b0.end.y * b1.end.x; 31 | return sign >= 0 ? angle : -angle; 32 | 33 | }, 34 | 35 | clamp: function ( v, min, max ) { 36 | 37 | v = v < min ? min : v; 38 | v = v > max ? max : v; 39 | return v; 40 | 41 | }, 42 | 43 | /*clamp: function ( value, min, max ) { 44 | 45 | return Math.max( min, Math.min( max, value ) ); 46 | 47 | },*/ 48 | 49 | lerp: function ( x, y, t ) { 50 | 51 | return ( 1 - t ) * x + t * y; 52 | 53 | }, 54 | 55 | rand: function ( low, high ) { 56 | 57 | return low + Math.random() * ( high - low ); 58 | 59 | }, 60 | 61 | randInt: function ( low, high ) { 62 | 63 | return low + Math.floor( Math.random() * ( high - low + 1 ) ); 64 | 65 | }, 66 | 67 | nearEquals: function (a, b, t) { 68 | 69 | return Math.abs(a - b) <= t ? true : false; 70 | 71 | }, 72 | 73 | perpendicular: function ( a, b ) { 74 | 75 | return math.nearEquals( a.dot(b), 0.0, 0.01 ) ? true : false; 76 | 77 | //return math.nearEquals( math.dotProduct(a, b), 0.0, 0.01 ) ? true : false; 78 | 79 | }, 80 | 81 | genPerpendicularVectorQuick: function ( v ) { 82 | 83 | //return math.genPerpendicularVectorFrisvad( v ); 84 | 85 | let p = v.clone(); 86 | // cross(v, UP) : cross(v, RIGHT) 87 | return Math.abs( v.y ) < 0.99 ? p.set( -v.z, 0, v.x ).normalize() : p.set( 0, v.z, -v.y ).normalize(); 88 | 89 | }, 90 | 91 | /*genPerpendicularVectorHM: function ( v ) { 92 | 93 | let a = v.abs(); 94 | let b = v.clone(); 95 | if (a.x <= a.y && a.x <= a.z) return b.set(0, -v.z, v.y).normalize(); 96 | else if (a.y <= a.x && a.y <= a.z) return b.set(-v.z, 0, v.x).normalize(); 97 | else return b.set(-v.y, v.x, 0).normalize(); 98 | 99 | },*/ 100 | 101 | genPerpendicularVectorFrisvad: function ( v ) { 102 | 103 | let nv = v.clone(); 104 | if ( v.z < -0.9999999 ) return nv.set(0, -1, 0);// Handle the singularity 105 | let a = 1/(1 + v.z); 106 | return nv.set( 1 - v.x * v.x * a, - v.x * v.y * a, -v.x ).normalize(); 107 | 108 | }, 109 | 110 | nearEquals(a,b,tolerance){ 111 | return (Math.abs(a - b) <= tolerance) ? true : false; 112 | }, 113 | 114 | // rotation 115 | 116 | rotateXDegs: function ( v, angle ) { return v.clone().rotate( angle * math.toRad, 'X' ); }, 117 | rotateYDegs: function ( v, angle ) { return v.clone().rotate( angle * math.toRad, 'Y' ) }, 118 | rotateZDegs: function ( v, angle ) { return v.clone().rotate( angle * math.toRad, 'Z' ) }, 119 | 120 | // distance 121 | 122 | withinManhattanDistance: function ( v1, v2, distance ) { 123 | 124 | if (Math.abs(v2.x - v1.x) > distance) return false; // Too far in x direction 125 | if (Math.abs(v2.y - v1.y) > distance) return false; // Too far in y direction 126 | if (Math.abs(v2.z - v1.z) > distance) return false; // Too far in z direction 127 | return true; 128 | 129 | }, 130 | 131 | manhattanDistanceBetween: function ( v1, v2 ) { 132 | 133 | return Math.abs(v2.x - v1.x) + Math.abs(v2.y - v1.y) + Math.abs(v2.z - v1.z); 134 | 135 | }, 136 | 137 | distanceBetween: function ( v1, v2 ) { 138 | 139 | let dx = v2.x - v1.x; 140 | let dy = v2.y - v1.y; 141 | let dz = v1.z !== undefined ? v2.z - v1.z : 0; 142 | return Math.sqrt( dx * dx + dy * dy + dz * dz ); 143 | 144 | }, 145 | 146 | unwrapDeg: ( r ) => (r - (Math.floor((r + 180)/360))*360), 147 | unwrapRad: ( r ) => (r - (Math.floor((r + Math.PI)/(2*Math.PI)))*2*Math.PI), 148 | 149 | 150 | /*unwrapDeg: function ( r ) { 151 | 152 | r = r % 360; 153 | if (r > 180) r -= 360; 154 | if (r < -180) r += 360; 155 | return r; 156 | 157 | }, 158 | 159 | unwrapRad: function( r ){ 160 | 161 | r = r % math.twoPI; 162 | if (r > Math.Pi ) r -= math.twoPI; 163 | if (r < - Math.Pi ) r += math.twoPI; 164 | return r; 165 | 166 | },*/ 167 | 168 | 169 | // ______________________________ 2D _____________________________ 170 | 171 | rotateDegs: function( v, angle ) { 172 | 173 | return v.clone().rotate( angle * math.toRad ) 174 | 175 | }, 176 | 177 | 178 | validateDirectionUV: function( directionUV ) { 179 | 180 | if( directionUV.length() < 0 ) Tools.error("vector direction unit vector cannot be zero."); 181 | 182 | }, 183 | 184 | validateLength: function( length ) { 185 | 186 | if( length < 0 ) Tools.error("Length must be a greater than or equal to zero."); 187 | 188 | }, 189 | 190 | 191 | 192 | } -------------------------------------------------------------------------------- /src/math/Q.js: -------------------------------------------------------------------------------- 1 | 2 | import { math } from './Math.js'; 3 | //import { MathUtils } from './MathUtils.js'; 4 | 5 | export class Q { 6 | 7 | constructor( x = 0, y = 0, z = 0, w = 1 ) { 8 | 9 | this.isQuaternion = true; 10 | 11 | this._x = x; 12 | this._y = y; 13 | this._z = z; 14 | this._w = w; 15 | 16 | } 17 | 18 | static slerp( qa, qb, qm, t ) { 19 | 20 | return qm.copy( qa ).slerp( qb, t ); 21 | 22 | } 23 | 24 | static slerpFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) { 25 | 26 | // fuzz-free, array-based Quaternion SLERP operation 27 | 28 | let x0 = src0[ srcOffset0 + 0 ], 29 | y0 = src0[ srcOffset0 + 1 ], 30 | z0 = src0[ srcOffset0 + 2 ], 31 | w0 = src0[ srcOffset0 + 3 ]; 32 | 33 | const x1 = src1[ srcOffset1 + 0 ], 34 | y1 = src1[ srcOffset1 + 1 ], 35 | z1 = src1[ srcOffset1 + 2 ], 36 | w1 = src1[ srcOffset1 + 3 ]; 37 | 38 | if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) { 39 | 40 | let s = 1 - t; 41 | const cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1, 42 | dir = ( cos >= 0 ? 1 : - 1 ), 43 | sqrSin = 1 - cos * cos; 44 | 45 | // Skip the Slerp for tiny steps to avoid numeric problems: 46 | if ( sqrSin > Number.EPSILON ) { 47 | 48 | const sin = Math.sqrt( sqrSin ), 49 | len = Math.atan2( sin, cos * dir ); 50 | 51 | s = Math.sin( s * len ) / sin; 52 | t = Math.sin( t * len ) / sin; 53 | 54 | } 55 | 56 | const tDir = t * dir; 57 | 58 | x0 = x0 * s + x1 * tDir; 59 | y0 = y0 * s + y1 * tDir; 60 | z0 = z0 * s + z1 * tDir; 61 | w0 = w0 * s + w1 * tDir; 62 | 63 | // Normalize in case we just did a lerp: 64 | if ( s === 1 - t ) { 65 | 66 | const f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 ); 67 | 68 | x0 *= f; 69 | y0 *= f; 70 | z0 *= f; 71 | w0 *= f; 72 | 73 | } 74 | 75 | } 76 | 77 | dst[ dstOffset ] = x0; 78 | dst[ dstOffset + 1 ] = y0; 79 | dst[ dstOffset + 2 ] = z0; 80 | dst[ dstOffset + 3 ] = w0; 81 | 82 | } 83 | 84 | static multiplyQuaternionsFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) { 85 | 86 | const x0 = src0[ srcOffset0 ]; 87 | const y0 = src0[ srcOffset0 + 1 ]; 88 | const z0 = src0[ srcOffset0 + 2 ]; 89 | const w0 = src0[ srcOffset0 + 3 ]; 90 | 91 | const x1 = src1[ srcOffset1 ]; 92 | const y1 = src1[ srcOffset1 + 1 ]; 93 | const z1 = src1[ srcOffset1 + 2 ]; 94 | const w1 = src1[ srcOffset1 + 3 ]; 95 | 96 | dst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1; 97 | dst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1; 98 | dst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1; 99 | dst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1; 100 | 101 | return dst; 102 | 103 | } 104 | 105 | get x() { 106 | 107 | return this._x; 108 | 109 | } 110 | 111 | set x( value ) { 112 | 113 | this._x = value; 114 | this._onChangeCallback(); 115 | 116 | } 117 | 118 | get y() { 119 | 120 | return this._y; 121 | 122 | } 123 | 124 | set y( value ) { 125 | 126 | this._y = value; 127 | this._onChangeCallback(); 128 | 129 | } 130 | 131 | get z() { 132 | 133 | return this._z; 134 | 135 | } 136 | 137 | set z( value ) { 138 | 139 | this._z = value; 140 | this._onChangeCallback(); 141 | 142 | } 143 | 144 | get w() { 145 | 146 | return this._w; 147 | 148 | } 149 | 150 | set w( value ) { 151 | 152 | this._w = value; 153 | this._onChangeCallback(); 154 | 155 | } 156 | 157 | set( x, y, z, w ) { 158 | 159 | this._x = x; 160 | this._y = y; 161 | this._z = z; 162 | this._w = w; 163 | 164 | this._onChangeCallback(); 165 | 166 | return this; 167 | 168 | } 169 | 170 | clone() { 171 | 172 | return new this.constructor( this._x, this._y, this._z, this._w ); 173 | 174 | } 175 | 176 | copy( quaternion ) { 177 | 178 | this._x = quaternion.x; 179 | this._y = quaternion.y; 180 | this._z = quaternion.z; 181 | this._w = quaternion.w; 182 | 183 | this._onChangeCallback(); 184 | 185 | return this; 186 | 187 | } 188 | 189 | setFromEuler( euler, update ) { 190 | 191 | if ( ! ( euler && euler.isEuler ) ) { 192 | 193 | throw new Error( 'THREE.Quaternion: .setFromEuler() now expects an Euler rotation rather than a Vector3 and order.' ); 194 | 195 | } 196 | 197 | const x = euler._x, y = euler._y, z = euler._z, order = euler._order; 198 | 199 | // http://www.mathworks.com/matlabcentral/fileexchange/ 200 | // 20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/ 201 | // content/SpinCalc.m 202 | 203 | const cos = Math.cos; 204 | const sin = Math.sin; 205 | 206 | const c1 = cos( x / 2 ); 207 | const c2 = cos( y / 2 ); 208 | const c3 = cos( z / 2 ); 209 | 210 | const s1 = sin( x / 2 ); 211 | const s2 = sin( y / 2 ); 212 | const s3 = sin( z / 2 ); 213 | 214 | switch ( order ) { 215 | 216 | case 'XYZ': 217 | this._x = s1 * c2 * c3 + c1 * s2 * s3; 218 | this._y = c1 * s2 * c3 - s1 * c2 * s3; 219 | this._z = c1 * c2 * s3 + s1 * s2 * c3; 220 | this._w = c1 * c2 * c3 - s1 * s2 * s3; 221 | break; 222 | 223 | case 'YXZ': 224 | this._x = s1 * c2 * c3 + c1 * s2 * s3; 225 | this._y = c1 * s2 * c3 - s1 * c2 * s3; 226 | this._z = c1 * c2 * s3 - s1 * s2 * c3; 227 | this._w = c1 * c2 * c3 + s1 * s2 * s3; 228 | break; 229 | 230 | case 'ZXY': 231 | this._x = s1 * c2 * c3 - c1 * s2 * s3; 232 | this._y = c1 * s2 * c3 + s1 * c2 * s3; 233 | this._z = c1 * c2 * s3 + s1 * s2 * c3; 234 | this._w = c1 * c2 * c3 - s1 * s2 * s3; 235 | break; 236 | 237 | case 'ZYX': 238 | this._x = s1 * c2 * c3 - c1 * s2 * s3; 239 | this._y = c1 * s2 * c3 + s1 * c2 * s3; 240 | this._z = c1 * c2 * s3 - s1 * s2 * c3; 241 | this._w = c1 * c2 * c3 + s1 * s2 * s3; 242 | break; 243 | 244 | case 'YZX': 245 | this._x = s1 * c2 * c3 + c1 * s2 * s3; 246 | this._y = c1 * s2 * c3 + s1 * c2 * s3; 247 | this._z = c1 * c2 * s3 - s1 * s2 * c3; 248 | this._w = c1 * c2 * c3 - s1 * s2 * s3; 249 | break; 250 | 251 | case 'XZY': 252 | this._x = s1 * c2 * c3 - c1 * s2 * s3; 253 | this._y = c1 * s2 * c3 - s1 * c2 * s3; 254 | this._z = c1 * c2 * s3 + s1 * s2 * c3; 255 | this._w = c1 * c2 * c3 + s1 * s2 * s3; 256 | break; 257 | 258 | default: 259 | console.warn( 'THREE.Quaternion: .setFromEuler() encountered an unknown order: ' + order ); 260 | 261 | } 262 | 263 | if ( update !== false ) this._onChangeCallback(); 264 | 265 | return this; 266 | 267 | } 268 | 269 | setFromAxisAngle( axis, angle ) { 270 | 271 | // http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm 272 | 273 | // assumes axis is normalized 274 | 275 | const halfAngle = angle / 2, s = Math.sin( halfAngle ); 276 | 277 | this._x = axis.x * s; 278 | this._y = axis.y * s; 279 | this._z = axis.z * s; 280 | this._w = Math.cos( halfAngle ); 281 | 282 | this._onChangeCallback(); 283 | 284 | return this; 285 | 286 | } 287 | 288 | setFromRotationMatrix( m ) { 289 | 290 | // http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm 291 | 292 | // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) 293 | 294 | const te = m.elements, 295 | 296 | m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ], 297 | m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ], 298 | m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ], 299 | 300 | trace = m11 + m22 + m33; 301 | 302 | if ( trace > 0 ) { 303 | 304 | const s = 0.5 / Math.sqrt( trace + 1.0 ); 305 | 306 | this._w = 0.25 / s; 307 | this._x = ( m32 - m23 ) * s; 308 | this._y = ( m13 - m31 ) * s; 309 | this._z = ( m21 - m12 ) * s; 310 | 311 | } else if ( m11 > m22 && m11 > m33 ) { 312 | 313 | const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 ); 314 | 315 | this._w = ( m32 - m23 ) / s; 316 | this._x = 0.25 * s; 317 | this._y = ( m12 + m21 ) / s; 318 | this._z = ( m13 + m31 ) / s; 319 | 320 | } else if ( m22 > m33 ) { 321 | 322 | const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 ); 323 | 324 | this._w = ( m13 - m31 ) / s; 325 | this._x = ( m12 + m21 ) / s; 326 | this._y = 0.25 * s; 327 | this._z = ( m23 + m32 ) / s; 328 | 329 | } else { 330 | 331 | const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 ); 332 | 333 | this._w = ( m21 - m12 ) / s; 334 | this._x = ( m13 + m31 ) / s; 335 | this._y = ( m23 + m32 ) / s; 336 | this._z = 0.25 * s; 337 | 338 | } 339 | 340 | this._onChangeCallback(); 341 | 342 | return this; 343 | 344 | } 345 | 346 | setFromUnitVectors( vFrom, vTo ) { 347 | 348 | // assumes direction vectors vFrom and vTo are normalized 349 | 350 | const EPS = 0.000001; 351 | 352 | let r = vFrom.dot( vTo ) + 1; 353 | 354 | if ( r < EPS ) { 355 | 356 | r = 0; 357 | 358 | if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) { 359 | 360 | this._x = - vFrom.y; 361 | this._y = vFrom.x; 362 | this._z = 0; 363 | this._w = r; 364 | 365 | } else { 366 | 367 | this._x = 0; 368 | this._y = - vFrom.z; 369 | this._z = vFrom.y; 370 | this._w = r; 371 | 372 | } 373 | 374 | } else { 375 | 376 | // crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3 377 | 378 | this._x = vFrom.y * vTo.z - vFrom.z * vTo.y; 379 | this._y = vFrom.z * vTo.x - vFrom.x * vTo.z; 380 | this._z = vFrom.x * vTo.y - vFrom.y * vTo.x; 381 | this._w = r; 382 | 383 | } 384 | 385 | return this.normalize(); 386 | 387 | } 388 | 389 | angleTo( q ) { 390 | 391 | return 2 * Math.acos( Math.abs( math.clamp( this.dot( q ), - 1, 1 ) ) ); 392 | 393 | } 394 | 395 | rotateTowards( q, step ) { 396 | 397 | const angle = this.angleTo( q ); 398 | 399 | if ( angle === 0 ) return this; 400 | 401 | const t = Math.min( 1, step / angle ); 402 | 403 | this.slerp( q, t ); 404 | 405 | return this; 406 | 407 | } 408 | 409 | identity() { 410 | 411 | return this.set( 0, 0, 0, 1 ); 412 | 413 | } 414 | 415 | invert() { 416 | 417 | // quaternion is assumed to have unit length 418 | 419 | return this.conjugate(); 420 | 421 | } 422 | 423 | conjugate() { 424 | 425 | this._x *= - 1; 426 | this._y *= - 1; 427 | this._z *= - 1; 428 | 429 | this._onChangeCallback(); 430 | 431 | return this; 432 | 433 | } 434 | 435 | dot( v ) { 436 | 437 | return this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w; 438 | 439 | } 440 | 441 | lengthSq() { 442 | 443 | return this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w; 444 | 445 | } 446 | 447 | length() { 448 | 449 | return Math.sqrt( this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w ); 450 | 451 | } 452 | 453 | normalize() { 454 | 455 | let l = this.length(); 456 | 457 | if ( l === 0 ) { 458 | 459 | this._x = 0; 460 | this._y = 0; 461 | this._z = 0; 462 | this._w = 1; 463 | 464 | } else { 465 | 466 | l = 1 / l; 467 | 468 | this._x = this._x * l; 469 | this._y = this._y * l; 470 | this._z = this._z * l; 471 | this._w = this._w * l; 472 | 473 | } 474 | 475 | this._onChangeCallback(); 476 | 477 | return this; 478 | 479 | } 480 | 481 | multiply( q, p ) { 482 | 483 | if ( p !== undefined ) { 484 | 485 | console.warn( 'THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead.' ); 486 | return this.multiplyQuaternions( q, p ); 487 | 488 | } 489 | 490 | return this.multiplyQuaternions( this, q ); 491 | 492 | } 493 | 494 | premultiply( q ) { 495 | 496 | return this.multiplyQuaternions( q, this ); 497 | 498 | } 499 | 500 | multiplyQuaternions( a, b ) { 501 | 502 | // from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm 503 | 504 | const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w; 505 | const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w; 506 | 507 | this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby; 508 | this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz; 509 | this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx; 510 | this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz; 511 | 512 | this._onChangeCallback(); 513 | 514 | return this; 515 | 516 | } 517 | 518 | slerp( qb, t ) { 519 | 520 | if ( t === 0 ) return this; 521 | if ( t === 1 ) return this.copy( qb ); 522 | 523 | const x = this._x, y = this._y, z = this._z, w = this._w; 524 | 525 | // http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/ 526 | 527 | let cosHalfTheta = w * qb._w + x * qb._x + y * qb._y + z * qb._z; 528 | 529 | if ( cosHalfTheta < 0 ) { 530 | 531 | this._w = - qb._w; 532 | this._x = - qb._x; 533 | this._y = - qb._y; 534 | this._z = - qb._z; 535 | 536 | cosHalfTheta = - cosHalfTheta; 537 | 538 | } else { 539 | 540 | this.copy( qb ); 541 | 542 | } 543 | 544 | if ( cosHalfTheta >= 1.0 ) { 545 | 546 | this._w = w; 547 | this._x = x; 548 | this._y = y; 549 | this._z = z; 550 | 551 | return this; 552 | 553 | } 554 | 555 | const sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta; 556 | 557 | if ( sqrSinHalfTheta <= Number.EPSILON ) { 558 | 559 | const s = 1 - t; 560 | this._w = s * w + t * this._w; 561 | this._x = s * x + t * this._x; 562 | this._y = s * y + t * this._y; 563 | this._z = s * z + t * this._z; 564 | 565 | this.normalize(); 566 | this._onChangeCallback(); 567 | 568 | return this; 569 | 570 | } 571 | 572 | const sinHalfTheta = Math.sqrt( sqrSinHalfTheta ); 573 | const halfTheta = Math.atan2( sinHalfTheta, cosHalfTheta ); 574 | const ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta, 575 | ratioB = Math.sin( t * halfTheta ) / sinHalfTheta; 576 | 577 | this._w = ( w * ratioA + this._w * ratioB ); 578 | this._x = ( x * ratioA + this._x * ratioB ); 579 | this._y = ( y * ratioA + this._y * ratioB ); 580 | this._z = ( z * ratioA + this._z * ratioB ); 581 | 582 | this._onChangeCallback(); 583 | 584 | return this; 585 | 586 | } 587 | 588 | equals( quaternion ) { 589 | 590 | return ( quaternion._x === this._x ) && ( quaternion._y === this._y ) && ( quaternion._z === this._z ) && ( quaternion._w === this._w ); 591 | 592 | } 593 | 594 | fromArray( array, offset = 0 ) { 595 | 596 | this._x = array[ offset ]; 597 | this._y = array[ offset + 1 ]; 598 | this._z = array[ offset + 2 ]; 599 | this._w = array[ offset + 3 ]; 600 | 601 | this._onChangeCallback(); 602 | 603 | return this; 604 | 605 | } 606 | 607 | toArray( array = [], offset = 0 ) { 608 | 609 | array[ offset ] = this._x; 610 | array[ offset + 1 ] = this._y; 611 | array[ offset + 2 ] = this._z; 612 | array[ offset + 3 ] = this._w; 613 | 614 | return array; 615 | 616 | } 617 | 618 | fromBufferAttribute( attribute, index ) { 619 | 620 | this._x = attribute.getX( index ); 621 | this._y = attribute.getY( index ); 622 | this._z = attribute.getZ( index ); 623 | this._w = attribute.getW( index ); 624 | 625 | return this; 626 | 627 | } 628 | 629 | _onChange( callback ) { 630 | 631 | this._onChangeCallback = callback; 632 | 633 | return this; 634 | 635 | } 636 | 637 | _onChangeCallback() {} 638 | 639 | } -------------------------------------------------------------------------------- /src/math/V2.js: -------------------------------------------------------------------------------- 1 | export class V2 { 2 | 3 | constructor( x = 0, y = 0 ) { 4 | 5 | this.isVector2 = true; 6 | this.x = x; 7 | this.y = y; 8 | 9 | } 10 | 11 | set( x, y ){ 12 | 13 | this.x = x || 0; 14 | this.y = y || 0; 15 | return this; 16 | 17 | } 18 | 19 | distanceTo( v ) { 20 | 21 | return Math.sqrt( this.distanceToSquared( v ) ); 22 | 23 | } 24 | 25 | distanceToSquared( v ) { 26 | 27 | let dx = this.x - v.x, dy = this.y - v.y; 28 | return dx * dx + dy * dy; 29 | 30 | } 31 | 32 | multiplyScalar( scalar ) { 33 | 34 | this.x *= scalar; 35 | this.y *= scalar; 36 | return this; 37 | 38 | } 39 | 40 | divideScalar( scalar ) { 41 | 42 | return this.multiplyScalar( 1 / scalar ); 43 | 44 | } 45 | 46 | length() { 47 | 48 | return Math.sqrt( this.x * this.x + this.y * this.y ); 49 | 50 | } 51 | 52 | normalize() { 53 | 54 | return this.divideScalar( this.length() || 1 ); 55 | 56 | } 57 | 58 | normalised() { 59 | 60 | return new this.constructor( this.x, this.y ).normalize(); 61 | 62 | } 63 | 64 | lengthSq() { 65 | 66 | return this.x * this.x + this.y * this.y; 67 | 68 | } 69 | 70 | add( v ) { 71 | 72 | this.x += v.x; 73 | this.y += v.y; 74 | return this; 75 | 76 | } 77 | 78 | plus( v ) { 79 | 80 | return new this.constructor( this.x + v.x, this.y + v.y ); 81 | 82 | } 83 | 84 | min( v ) { 85 | 86 | this.x -= v.x; 87 | this.y -= v.y; 88 | return this; 89 | 90 | } 91 | 92 | minus( v ) { 93 | 94 | return new V2( this.x - v.x, this.y - v.y ); 95 | 96 | } 97 | 98 | divideBy( value ) { 99 | 100 | return new this.constructor( this.x, this.y ).divideScalar( value ); 101 | 102 | } 103 | 104 | dot( a, b ) { 105 | 106 | return this.x * a.x + this.y * a.y; 107 | 108 | } 109 | 110 | negate() { 111 | 112 | this.x = -this.x; 113 | this.y = -this.y; 114 | return this; 115 | 116 | } 117 | 118 | negated() { 119 | 120 | return new this.constructor( -this.x, -this.y ); 121 | 122 | } 123 | 124 | clone (){ 125 | 126 | return new this.constructor( this.x, this.y ); 127 | 128 | } 129 | 130 | copy( v ) { 131 | 132 | this.x = v.x; 133 | this.y = v.y; 134 | return this; 135 | 136 | } 137 | 138 | cross( v ) { 139 | 140 | return this.x * v.y - this.y * v.x; 141 | 142 | } 143 | 144 | sign( v ) { 145 | 146 | let s = this.cross( v ); 147 | return s >= 0 ? 1 : -1; 148 | 149 | } 150 | 151 | approximatelyEquals( v, t ) { 152 | 153 | if ( t < 0 ) return false; 154 | let xDiff = Math.abs(this.x - v.x); 155 | let yDiff = Math.abs(this.y - v.y); 156 | return ( xDiff < t && yDiff < t ); 157 | 158 | } 159 | 160 | rotate( angle ) { 161 | 162 | let cos = Math.cos( angle ); 163 | let sin = Math.sin( angle ); 164 | let x = this.x * cos - this.y * sin; 165 | let y = this.x * sin + this.y * cos; 166 | this.x = x; 167 | this.y = y; 168 | return this; 169 | 170 | } 171 | 172 | angleTo( v ) { 173 | 174 | let a = this.dot(v) / (Math.sqrt( this.lengthSq() * v.lengthSq() )); 175 | if(a <= -1) return Math.PI; 176 | if(a >= 1) return 0; 177 | return Math.acos( a ); 178 | 179 | } 180 | 181 | getSignedAngle( v ) { 182 | 183 | let a = this.angleTo( v ); 184 | let s = this.sign( v ); 185 | return s === 1 ? a : -a; 186 | 187 | } 188 | 189 | constrainedUV( baselineUV, min, max ) { 190 | 191 | let angle = baselineUV.getSignedAngle( this ); 192 | if( angle > max ) this.copy( baselineUV ).rotate(max); 193 | if( angle < min ) this.copy( baselineUV ).rotate(min); 194 | return this; 195 | 196 | } 197 | 198 | } -------------------------------------------------------------------------------- /src/math/V3.js: -------------------------------------------------------------------------------- 1 | export class V3 { 2 | 3 | constructor( x = 0, y = 0, z = 0 ) { 4 | 5 | this.isVector3 = true; 6 | this.x = x; 7 | this.y = y; 8 | this.z = z; 9 | 10 | } 11 | 12 | set( x, y, z ) { 13 | 14 | this.x = x || 0; 15 | this.y = y || 0; 16 | this.z = z || 0; 17 | return this; 18 | 19 | } 20 | 21 | distanceTo( v ) { 22 | 23 | return Math.sqrt( this.distanceToSquared( v ) ); 24 | 25 | } 26 | 27 | distanceToSquared( v ) { 28 | 29 | let dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z; 30 | 31 | return dx * dx + dy * dy + dz * dz; 32 | 33 | } 34 | 35 | abs() { 36 | 37 | return new this.constructor( 38 | this.x < 0 ? -this.x : this.x, 39 | this.y < 0 ? -this.y : this.y, 40 | this.z < 0 ? -this.z : this.z 41 | ); 42 | 43 | } 44 | 45 | dot( v ) { 46 | 47 | return this.x * v.x + this.y * v.y + this.z * v.z; 48 | 49 | } 50 | 51 | length() { 52 | 53 | return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z ); 54 | 55 | } 56 | 57 | lengthSq() { 58 | 59 | return this.x * this.x + this.y * this.y + this.z * this.z; 60 | 61 | } 62 | 63 | normalize() { 64 | 65 | return this.divideScalar( this.length() || 1 ); 66 | 67 | } 68 | 69 | normalised() { 70 | 71 | return new this.constructor( this.x, this.y, this.z ).normalize(); 72 | 73 | } 74 | 75 | add( v ) { 76 | 77 | this.x += v.x; 78 | this.y += v.y; 79 | this.z += v.z; 80 | return this; 81 | 82 | } 83 | 84 | min( v ) { 85 | 86 | this.x -= v.x; 87 | this.y -= v.y; 88 | this.z -= v.z; 89 | return this; 90 | 91 | } 92 | 93 | plus( v ) { 94 | 95 | return new this.constructor( this.x + v.x, this.y + v.y, this.z + v.z ); 96 | 97 | } 98 | 99 | minus( v ) { 100 | 101 | return new this.constructor( this.x - v.x, this.y - v.y, this.z - v.z ); 102 | 103 | } 104 | 105 | divideBy( s ) { 106 | 107 | return new this.constructor ( this.x / s, this.y / s, this.z / s ); 108 | 109 | } 110 | 111 | multiply( s ) { 112 | 113 | return new this.constructor( this.x * s, this.y * s, this.z * s ); 114 | 115 | } 116 | 117 | 118 | multiplyScalar( scalar ) { 119 | 120 | this.x *= scalar; 121 | this.y *= scalar; 122 | this.z *= scalar; 123 | return this; 124 | 125 | } 126 | 127 | divideScalar( scalar ) { 128 | 129 | return this.multiplyScalar( 1 / scalar ); 130 | 131 | } 132 | 133 | cross( v ) { 134 | 135 | return new this.constructor( this.y * v.z - this.z * v.y, this.z * v.x - this.x * v.z, this.x * v.y - this.y * v.x ); 136 | 137 | } 138 | 139 | crossVectors( a, b ) { 140 | 141 | let ax = a.x, ay = a.y, az = a.z; 142 | let bx = b.x, by = b.y, bz = b.z; 143 | 144 | this.x = ay * bz - az * by; 145 | this.y = az * bx - ax * bz; 146 | this.z = ax * by - ay * bx; 147 | 148 | return this; 149 | 150 | } 151 | 152 | negate() { 153 | 154 | this.x = -this.x; 155 | this.y = -this.y; 156 | this.z = -this.z; 157 | return this; 158 | 159 | } 160 | 161 | negated() { 162 | 163 | return new V3( -this.x, -this.y, -this.z ); 164 | 165 | } 166 | 167 | clone() { 168 | 169 | return new V3( this.x, this.y, this.z ); 170 | 171 | } 172 | 173 | copy( v ) { 174 | 175 | this.x = v.x; 176 | this.y = v.y; 177 | this.z = v.z; 178 | return this; 179 | 180 | } 181 | 182 | approximatelyEquals( v, t ) { 183 | 184 | if ( t < 0 ) return false; 185 | let xDiff = Math.abs(this.x - v.x); 186 | let yDiff = Math.abs(this.y - v.y); 187 | let zDiff = Math.abs(this.z - v.z); 188 | return ( xDiff < t && yDiff < t && zDiff < t ); 189 | 190 | } 191 | 192 | zero() { 193 | 194 | this.x = 0; 195 | this.y = 0; 196 | this.z = 0; 197 | return this; 198 | 199 | } 200 | 201 | /*projectOnPlane_old: function ( planeNormal ) { 202 | 203 | if ( planeNormal.length() <= 0 ){ Tools.error("Plane normal cannot be a zero vector."); return; } 204 | 205 | // Projection of vector b onto plane with normal n is defined as: b - ( b.n / ( |n| squared )) * n 206 | // Note: |n| is length or magnitude of the vector n, NOT its (component-wise) absolute value 207 | let b = this.normalised(); 208 | let n = planeNormal.normalised(); 209 | 210 | return b.min( n.times( _Math.dotProduct( b, planeNormal ) ) ).normalize(); 211 | 212 | },*/ 213 | 214 | rotate( angle, axe ) { 215 | 216 | let cos = Math.cos( angle ); 217 | let sin = Math.sin( angle ); 218 | let x, y, z; 219 | 220 | switch ( axe ){ 221 | case 'X': 222 | x = this.x; 223 | y = this.y * cos - this.z * sin; 224 | z = this.y * sin + this.z * cos; 225 | break 226 | case 'Y': 227 | x = this.z * sin + this.x * cos; 228 | y = this.y; 229 | z = this.z * cos - this.x * sin; 230 | break 231 | case 'Z': 232 | x = this.x * cos - this.y * sin; 233 | y = this.x * sin + this.y * cos; 234 | z = this.z; 235 | break 236 | } 237 | 238 | this.x = x; 239 | this.y = y; 240 | this.z = z; 241 | return this; 242 | 243 | } 244 | 245 | // added 246 | 247 | projectOnVector( vector ) { 248 | 249 | let scalar = vector.dot( this ) / vector.lengthSq(); 250 | return this.copy( vector ).multiplyScalar( scalar ); 251 | 252 | } 253 | 254 | projectOnPlane() { 255 | 256 | let v1 = new this.constructor(); 257 | 258 | return function projectOnPlane( planeNormal ) { 259 | 260 | v1.copy( this ).normalised().projectOnVector( planeNormal.normalised() ); 261 | 262 | return this.min( v1 ).normalize(); 263 | 264 | } 265 | 266 | } 267 | 268 | applyM3( m ) { 269 | 270 | let x = this.x, y = this.y, z = this.z; 271 | let e = m.elements; 272 | 273 | this.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z; 274 | this.y = e[ 3 ] * x + e[ 4 ] * y + e[ 5 ] * z; 275 | this.z = e[ 6 ] * x + e[ 7 ] * y + e[ 8 ] * z; 276 | 277 | return this.normalize(); 278 | 279 | } 280 | 281 | applyMatrix3( m ) { 282 | 283 | let x = this.x, y = this.y, z = this.z; 284 | let e = m.elements; 285 | 286 | this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z; 287 | this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z; 288 | this.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z; 289 | 290 | return this; 291 | 292 | } 293 | 294 | applyQuaternion( q ) { 295 | 296 | let x = this.x, y = this.y, z = this.z; 297 | let qx = q.x, qy = q.y, qz = q.z, qw = q.w; 298 | 299 | // calculate quat * vector 300 | 301 | let ix = qw * x + qy * z - qz * y; 302 | let iy = qw * y + qz * x - qx * z; 303 | let iz = qw * z + qx * y - qy * x; 304 | let iw = - qx * x - qy * y - qz * z; 305 | 306 | // calculate result * inverse quat 307 | 308 | this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy; 309 | this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz; 310 | this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx; 311 | 312 | return this; 313 | 314 | } 315 | 316 | ///// 317 | 318 | sign( v, normal ) { 319 | 320 | let s = this.cross( v ).dot( normal ); 321 | return s >= 0 ? 1 : -1; 322 | 323 | } 324 | 325 | angleTo( v ) { 326 | 327 | let a = this.dot(v) / Math.sqrt( this.lengthSq() * v.lengthSq() ); 328 | if(a <= -1) return Math.PI; 329 | if(a >= 1) return 0; 330 | return Math.acos( a ); 331 | 332 | } 333 | 334 | getSignedAngle( v, normal ) { 335 | 336 | let a = this.angleTo( v ); 337 | let s = this.sign( v, normal ); 338 | return s === 1 ? a : -a; 339 | 340 | } 341 | 342 | constrainedUV( referenceAxis, rotationAxis, mtx, min, max ) { 343 | 344 | let angle = referenceAxis.getSignedAngle( this, rotationAxis ); 345 | if( angle > max ) this.copy( mtx.rotateAboutAxis( referenceAxis, max, rotationAxis ) ); 346 | if( angle < min ) this.copy( mtx.rotateAboutAxis( referenceAxis, min, rotationAxis ) ); 347 | return this; 348 | 349 | } 350 | 351 | limitAngle( base, mtx, max ) { 352 | 353 | let angle = base.angleTo( this ); 354 | if( angle > max ){ 355 | let correctionAxis = base.normalised().cross(this).normalize(); 356 | this.copy( mtx.rotateAboutAxis( base, max, correctionAxis ) ); 357 | } 358 | return this; 359 | 360 | } 361 | 362 | 363 | } -------------------------------------------------------------------------------- /src/solver/HISolver.js: -------------------------------------------------------------------------------- 1 | //import { NONE, GLOBAL_ROTOR, GLOBAL_HINGE, LOCAL_ROTOR, LOCAL_HINGE, J_BALL, J_GLOBAL, J_LOCAL } from '../constants.js'; 2 | import { LOCAL_ABSOLUTE } from '../constants.js'; 3 | import { math } from '../math/Math.js'; 4 | import { V2 } from '../math/V2.js'; 5 | import { Structure2D } from '../core/Structure2D.js'; 6 | import { Chain2D } from '../core/Chain2D.js'; 7 | import { Bone2D } from '../core/Bone2D.js'; 8 | 9 | 10 | export class HISolver { 11 | 12 | constructor( o, THREE ) { 13 | 14 | this.THREE = THREE 15 | 16 | this.isHISolver = true; 17 | this.startBones = null; 18 | this.endBones = null; 19 | 20 | this.scene = o.scene; 21 | 22 | this.target = null; 23 | this.goal = null; 24 | this.swivelAngle = 0; 25 | 26 | this.iteration = 15; 27 | 28 | this.thresholds = { position:0.1, rotation:0.1 }; 29 | 30 | this.solver = new Structure2D( this.scene, this.THREE ); 31 | //this.chain = null; 32 | 33 | this.bones = []; 34 | this.numBones = 0; 35 | 36 | this.rotation = []; 37 | 38 | this.initStructure( o ); 39 | 40 | } 41 | 42 | 43 | initStructure( o ) { 44 | 45 | this.startBones = o.start; 46 | this.endBones = o.end; 47 | this.angles = o.angles; 48 | 49 | let bone = this.startBones, next = bone.children[0]; 50 | 51 | this.bones.push(bone); 52 | 53 | for (let i = 0; i<100; i++) { 54 | 55 | this.bones.push(next); 56 | if( next === this.endBones ) { this.createChain(); break } 57 | 58 | bone = next; 59 | 60 | next = bone.children[0]; 61 | 62 | } 63 | 64 | } 65 | 66 | createChain() { 67 | 68 | this.numBones = this.bones.length; 69 | let chain = new Chain2D(); 70 | //chain.embeddedTarget = new V2(); 71 | //chain.useEmbeddedTarget = true; 72 | chain.setFixedBaseMode(true); 73 | chain.setBaseboneConstraintType( LOCAL_ABSOLUTE ); 74 | 75 | this.fakeBone = new Bone2D( new V2(0, -1), new V2(0, 0) ); 76 | 77 | this.target = new this.THREE.Vector3(); 78 | 79 | let base = new this.THREE.Vector3(); 80 | let p0 = new this.THREE.Vector3(); 81 | let p1 = new this.THREE.Vector3(); 82 | let uv = new V2(); 83 | let lng = 0 84 | 85 | for (let i = 0; i 0 ){ 88 | this.target.add( this.bones[i].position ); 89 | lng = base.distanceTo( this.bones[i].position ); 90 | this.bones[i-1].getWorldPosition( p0 ); 91 | this.bones[i].getWorldPosition( p1 ); 92 | p1.sub( p0 ).normalize(); 93 | 94 | 95 | if(p1.z === 0 ) uv.set( p1.x, p1.y ); 96 | else if(p1.x === 0 ) uv.set( p1.z, p1.y ); 97 | //uvs.push( uv ); 98 | 99 | //console.log( uv, lng, this.angles[i-1][0], this.angles[i-1][1]) 100 | 101 | if(i===1) chain.addBone( new Bone2D( new V2(0, 0), null, uv, lng, this.angles[i-1][0], this.angles[i-1][1] ) ); 102 | //else chain.addConsecutiveBone( uv, lng );//, this.angles[i-1][0], this.angles[i-1][1] ); 103 | else chain.addConsecutiveBone( uv, lng, this.angles[i-1][0], this.angles[i-1][1] ); 104 | } 105 | 106 | } 107 | 108 | //if(this.target.z === 0 ) chain.embeddedTarget.set( this.target.x, this.target.y ); 109 | //else if(this.target.x === 0 ) chain.embeddedTarget.set( this.target.z, this.target.y ); 110 | this.target.set( 10, 20, 0 ) 111 | 112 | this.solver.add( chain, this.target, true ); 113 | 114 | //this.solver.chains[0].embeddedTarget.set(10, 10) 115 | 116 | 117 | //console.log( lengths ); 118 | //console.log( this.bones, this.target, this.solver.chains[0].bones ); 119 | 120 | } 121 | 122 | update() { 123 | 124 | this.solver.update(); 125 | 126 | let bones2d = this.solver.chains[0].bones; 127 | let n = this.numBones-1; 128 | 129 | let a; 130 | 131 | for(let i = 0; i < n; i++){ 132 | 133 | a = i===0 ? math.findAngle( this.fakeBone, bones2d[i] ) : math.findAngle( bones2d[i-1], bones2d[i] ); 134 | this.rotation[i] = a * math.toDeg; 135 | this.rotation[i] += a < 0 ? 180 : -180; 136 | this.rotation[i] *= -1; 137 | 138 | } 139 | 140 | for( let i = 0; i < n; i++ ){ 141 | this.bones[i].rotation.z = this.rotation[i] * math.toRad; 142 | } 143 | 144 | //console.log(this.rotation) 145 | //let r = FIK.math.findAngle(bones[0], bones[1]); 146 | 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /src/solver/IKSolver.js: -------------------------------------------------------------------------------- 1 | //import { NONE, GLOBAL_ROTOR, GLOBAL_HINGE, LOCAL_ROTOR, LOCAL_HINGE, J_BALL, J_GLOBAL, J_LOCAL } from '../constants.js'; 2 | 3 | import { V3 } from '../math/V3.js'; 4 | import { V2 } from '../math/V2.js'; 5 | import { Structure3D } from '../core/Structure3D.js'; 6 | import { Chain3D } from '../core/Chain3D.js'; 7 | import { Bone3D } from '../core/Bone3D.js'; 8 | 9 | export class IKSolver { 10 | 11 | constructor( o ) { 12 | 13 | this.isIKSolver = true; 14 | this.startBones = null; 15 | this.endBones = null; 16 | 17 | this.target = null; 18 | this.goal = null; 19 | this.swivelAngle = 0; 20 | 21 | this.iteration = 40; 22 | 23 | this.thresholds = { position:0.1, rotation:0.1 }; 24 | 25 | this.solver = null; 26 | this.chain = null; 27 | 28 | } 29 | 30 | } --------------------------------------------------------------------------------