-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
RuiChen
committed
Jun 24, 2024
1 parent
bc2473e
commit 877c06d
Showing
8 changed files
with
316 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
{ | ||
"cSpell.words": [ | ||
"FABRIK" | ||
"FABRIK", | ||
"quats" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
.multi-end { | ||
width: 100vw; | ||
height: 100vh; | ||
position: relative; | ||
|
||
.overlay { | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
display: flex; | ||
justify-content: flex-start; | ||
align-self: start; | ||
|
||
.overlay-area { | ||
margin: 8px; | ||
padding: 8px 8px 32px 8px; | ||
background-color: #f5f5f5; | ||
border: 1px solid #e0e0e0; | ||
border-radius: 10px; | ||
|
||
.iteration { | ||
pointer-events: none; | ||
-webkit-user-select: none; | ||
/* Safari */ | ||
-ms-user-select: none; | ||
/* IE 10 and IE 11 */ | ||
user-select: none; | ||
/* Standard syntax */ | ||
} | ||
} | ||
} | ||
|
||
.canvas { | ||
width: 100vw; | ||
height: 100vh; | ||
background-color: #f0f0f0; | ||
border-radius: 10px; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import Camera from './Camera'; | ||
import Bone from './character/bone'; | ||
import XYZValue from './value/xyz_value'; | ||
import { Canvas } from '@react-three/fiber'; | ||
import { Component, ReactNode } from 'react'; | ||
import MultiEndFABRIK from './fabrik/multi-end-fabrik'; | ||
|
||
import './MultiEnd.scss'; | ||
|
||
import BoneVisualizer from './visualizer/BoneVisualizer'; | ||
import TargetVisualizer from './visualizer/TargetVisualizer'; | ||
|
||
interface MultiEndState { | ||
isLoading: boolean; | ||
cameraEnabled: boolean; | ||
fabrikIteration: number; | ||
root: Bone; | ||
} | ||
|
||
class MultiEnd extends Component<any, MultiEndState> { | ||
|
||
private _fabrik: MultiEndFABRIK = new MultiEndFABRIK(); | ||
|
||
private _target1: XYZValue = new XYZValue(0, 0, 0); | ||
private _target2: XYZValue = new XYZValue(0, 0, 0); | ||
|
||
constructor(props: any) { | ||
super(props); | ||
this.state = { | ||
isLoading: true, | ||
cameraEnabled: true, | ||
fabrikIteration: 0, | ||
root: new Bone('root', new XYZValue(0, 0, 0), new XYZValue(0, 0, 0)) | ||
} | ||
} | ||
|
||
componentDidMount(): void { | ||
const bone1 = new Bone('bone1', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
const bone2 = new Bone('bone2', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
const bone3 = new Bone('bone3', new XYZValue(40, 40, 0), new XYZValue(0, 0, 0)); | ||
const bone4 = new Bone('bone4', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
const bone5 = new Bone('bone5', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
const bone6 = new Bone('bone6', new XYZValue(-40, 40, 0), new XYZValue(0, 0, 0)); | ||
const bone7 = new Bone('bone7', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
const bone8 = new Bone('bone8', new XYZValue(0, 60, 0), new XYZValue(0, 0, 0)); | ||
bone7.addChild(bone8); | ||
bone6.addChild(bone7); | ||
bone4.addChild(bone5); | ||
bone3.addChild(bone4); | ||
bone2.addChild(bone6); | ||
bone2.addChild(bone3); | ||
bone1.addChild(bone2); | ||
const root = this.state.root; | ||
root.addChild(bone1); | ||
this._target1 = bone5.world[0]; | ||
this._target2 = bone8.world[0]; | ||
this.setState({ | ||
root: root, | ||
isLoading: false | ||
}); | ||
} | ||
|
||
private _onTarget1Move = (pos: XYZValue): void => { | ||
this._target1 = pos; | ||
const root = this.state.root; | ||
const iteration = this._fabrik.resolve(root, [this._target1, this._target2], ['bone5', 'bone8']); | ||
this.setState({ | ||
root: root, | ||
fabrikIteration: iteration | ||
}); | ||
} | ||
|
||
private _onTarget2Move = (pos: XYZValue): void => { | ||
this._target2 = pos; | ||
const root = this.state.root; | ||
const iteration = this._fabrik.resolve(root, [this._target1, this._target2], ['bone5', 'bone8']); | ||
this.setState({ | ||
root: root, | ||
fabrikIteration: iteration | ||
}); | ||
} | ||
|
||
render(): ReactNode { | ||
if (this.state.isLoading) { | ||
return <div>Loading...</div>; | ||
} | ||
return ( | ||
<div className="multi-end"> | ||
<div className="overlay"> | ||
<div className="overlay-area"> | ||
<span className="iteration">iteration: {this.state.fabrikIteration}</span> | ||
</div> | ||
</div> | ||
<div className="canvas"> | ||
<Canvas> | ||
<Camera disabled={!this.state.cameraEnabled} /> | ||
<ambientLight color="#0f0f0f" intensity={1} /> | ||
<hemisphereLight color="#ffffff" groundColor="#0f0f0f" intensity={1} /> | ||
<gridHelper args={[10000, 1000]} /> | ||
<axesHelper args={[100]} /> | ||
<BoneVisualizer root={this.state.root} /> | ||
<TargetVisualizer | ||
initPos={this._target1} | ||
onTargetMove={this._onTarget1Move} | ||
onDragStart={() => this.setState({ cameraEnabled: false })} | ||
onDragEnd={() => this.setState({ cameraEnabled: true })} | ||
/> | ||
<TargetVisualizer | ||
initPos={this._target2} | ||
onTargetMove={this._onTarget2Move} | ||
onDragStart={() => this.setState({ cameraEnabled: false })} | ||
onDragEnd={() => this.setState({ cameraEnabled: true })} | ||
/> | ||
</Canvas> | ||
</div> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default MultiEnd; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import Bone from '../character/bone'; | ||
import PointGroup from '../value/point_group'; | ||
import Quat, { applyQuatRotation, averageQuat, xyzRotation } from '../value/quat'; | ||
import XYZValue, { moveXYZAlone, subXYZ } from '../value/xyz_value'; | ||
|
||
class MultiEndFABRIK { | ||
private static MAX_ITERATIONS = 20; | ||
private static TOLERANCE = 0.1; | ||
|
||
public resolve(root: Bone, poss: XYZValue[], endNames: string[]): number { | ||
for (let i = 0; i < MultiEndFABRIK.MAX_ITERATIONS; i++) { | ||
const worlds: Map<string, XYZValue> = new Map(); | ||
const forwardWorlds: Map<string, PointGroup> = new Map(); | ||
{ // Initialize | ||
for (const bone of root) { | ||
worlds.set(bone.name, bone.world[0]); | ||
forwardWorlds.set(bone.name, new PointGroup()); | ||
} | ||
} | ||
{ // Forward | ||
const queue: Bone[] = []; | ||
const visited: Set<string> = new Set(); | ||
for (let i = 0; i < endNames.length; i++) { | ||
queue.push(root.find(endNames[i])!); | ||
forwardWorlds.get(endNames[i])!.addPoint(poss[i]); | ||
} | ||
let current: Bone | undefined = queue.shift(); | ||
while (current) { | ||
if (!current.parent) { // skip root | ||
visited.add(current.name); | ||
current = queue.shift(); | ||
continue; | ||
} | ||
if (visited.has(current.name)) { // skip visited | ||
current = queue.shift(); | ||
continue; | ||
} | ||
let allChildrenVisited = true; | ||
for (const child of current.children) { | ||
if (!visited.has(child.name)) { | ||
allChildrenVisited = false; | ||
break; | ||
} | ||
} | ||
// if one of the children is not visited, push current back | ||
// to the queue and continue | ||
if (!allChildrenVisited) { | ||
queue.push(current); | ||
current = queue.shift(); | ||
continue; | ||
} | ||
visited.add(current!.name); | ||
// forward process | ||
const child = forwardWorlds.get(current.name)!; | ||
const parent = worlds.get(current.parent.name)!; | ||
const length = current.pos.length(); | ||
const pg = forwardWorlds.get(current.parent.name)!; | ||
let newWorld = moveXYZAlone(child.centroid, parent, length); | ||
pg.addPoint(newWorld); | ||
forwardWorlds.set(current.parent.name, pg); | ||
queue.push(current.parent); | ||
current = queue.shift(); | ||
} | ||
} | ||
{ // Backward | ||
for (const bone of root) { | ||
if (bone.children.length === 0) { | ||
continue; | ||
} | ||
const quats: Quat[] = []; | ||
const [bw, br] = bone.world; | ||
for (const child of bone.children) { | ||
const v1 = subXYZ(child.world[0], bw); | ||
const v2 = subXYZ(forwardWorlds.get(child.name)!.centroid, bw); | ||
const quat = xyzRotation(v1, v2) | ||
quats.push(quat); | ||
} | ||
const avg = averageQuat(quats); | ||
bone.rotation = applyQuatRotation(br, bone.rotation, avg); | ||
} | ||
} | ||
// Early termination when the target is reached | ||
let allReached = true; | ||
for (let i = 0; i < endNames.length; i++) { | ||
if (subXYZ(root.find(endNames[i])!.world[0], poss[i]).length() > MultiEndFABRIK.TOLERANCE) { | ||
allReached = false; | ||
break; | ||
} | ||
} | ||
if (allReached) { | ||
return i + 1; | ||
} | ||
} | ||
return MultiEndFABRIK.MAX_ITERATIONS; | ||
} | ||
} | ||
|
||
export default MultiEndFABRIK; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import XYZValue from './xyz_value'; | ||
|
||
class PointGroup { | ||
private _points: XYZValue[] = []; | ||
private _centroid = { x: 0, y: 0, z: 0 }; | ||
|
||
public get centroid(): XYZValue { | ||
return new XYZValue(this._centroid.x, this._centroid.y, this._centroid.z); | ||
} | ||
|
||
constructor() { } | ||
|
||
public addPoint(p: XYZValue): void { | ||
this._points.push(p); | ||
this._updateCentroid(); | ||
} | ||
|
||
private _updateCentroid(): void { | ||
let x = 0; | ||
let y = 0; | ||
let z = 0; | ||
for (const p of this._points) { | ||
x += p.x; | ||
y += p.y; | ||
z += p.z; | ||
} | ||
this._centroid = { x: x / this._points.length, y: y / this._points.length, z: z / this._points.length }; | ||
} | ||
} | ||
|
||
export default PointGroup; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters