Skip to content
5 changes: 5 additions & 0 deletions js/animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class AnimationHandler {

this.events = []
this.keyframeInfo = []
this.ikaCubes = [] //Inverse kinematic anchor cubes
this.definedKeyframeInfo = new Map()
this.playstate = new PlayState()
}
Expand All @@ -38,6 +39,10 @@ export class AnimationHandler {
renameCube(oldName, newName) {
this.keyframes.forEach(kf => kf.renameCube(oldName, newName))
this.loopKeyframe.renameCube(oldName, newName)

if(this.ikaCubes.includes(oldName)) {
this.ikaCubes.splice(this.ikaCubes.indexOf(oldName), 1, newName)
}
}

animate(deltaTime) {
Expand Down
24 changes: 24 additions & 0 deletions js/animator/animation_cube_values.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ export class AnimationCubeValues {
this.frameTime = new LinkedElement(dom.find('.input-frame-start'), false).onchange(e => this.activeKeyframeFunc(kf => kf.startTime = e.value)).absNumber()
this.frameLength = new LinkedElement(dom.find('.input-frame-length'), false).onchange(e => this.activeKeyframeFunc(kf => kf.duration = e.value)).absNumber()

//Create the animation inverse kinematic lock element
this.ikaCubes = new LinkedElement(dom.find('.keyframe-inverse-kinematics-anchor'), false, false, true).onchange(e => {
let handler = studio.pth.animationTabs.active
let selected = this.raytracer.oneSelected()

if(handler !== null && selected !== null) {
let name = selected.tabulaCube.name
if(e.value) {
if(!handler.ikaCubes.includes(name)) {
handler.ikaCubes.push(name)
}
} else if(handler.ikaCubes.includes(name)) {
handler.ikaCubes.splice(handler.ikaCubes.indexOf(name), 1)
}
}
})

//Create the animation loop checkbox element
this.animationLoop = new LinkedElement(dom.find('.keyframe-loop'), false, false, true).onchange(e => {
let handler = studio.pth.animationTabs.active
Expand Down Expand Up @@ -148,6 +165,13 @@ export class AnimationCubeValues {
this.cubeGrow.setInternalValue(undefined)
}

let handler = this.pth.animationTabs.active
if(selected !== null && handler !== null) {
this.ikaCubes.setInternalValue(handler.ikaCubes.includes(selected.tabulaCube.name))
} else {
this.ikaCubes.setInternalValue(false)
}

let isSelected = this.raytracer.selectedSet.size === 1
this.cubeSelectionRequired.prop("disabled", !isSelected).toggleClass("is-active", isSelected)
}
Expand Down
29 changes: 15 additions & 14 deletions js/animator/animation_studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export class AnimationStudio {
this.transformControls = display.createTransformControls()
this.group.add(this.transformControls)

this.display = display

//Instantiate all the texture studio stuff.
//Note that some stuff relies on other stuff, so it being in this order is important.
this.gumball = new Gumball(dom, this)
Expand All @@ -58,7 +60,6 @@ export class AnimationStudio {
this.keyframeManager = new KeyframeBoardManager(this, dom.find('.keyframe-board'), dom.find('.input-playback-range'))
this.keyframeSettings = new KeyframeSettings()
this.panelButtons = new PanelButtons(dom, this)
this.display = display
this.methodExporter = new JavaMethodExporter()
this.progressionCanvas = new ProgressionCanvas(dom, this)
this.clock = new Clock()
Expand Down Expand Up @@ -108,10 +109,10 @@ export class AnimationStudio {
* @param {*} updateDisplay whether to update the dispaly (cube values)
* @param {*} updateSilent whether to be silent. Should be true when the rotation is not being set by the player.
* If true, updates the keyframe
* @param {DCMCube} selected the selected cube to update to.
*/
setRotation(values, updateDisplay = true, updateSilent = false) {
let selected = this.raytracer.oneSelected()
if(selected !== null) {
setRotation(values, updateDisplay = true, updateSilent = false, selected = this.raytracer.oneSelected()?.tabulaCube) {
if(selected != null) {
//Update the display if needed. Either silently or not silently.
if(updateDisplay) {
if(updateSilent) {
Expand Down Expand Up @@ -140,7 +141,7 @@ export class AnimationStudio {
//Get the rotation of the cube, and calculate what it would take to get from values to that rottation.
//Set that in the keyframe data.
let arr = selected.cubeGroup.rotation.toArray()
handler.selectedKeyFrame.rotationMap.set(selected.tabulaCube.name, values.map((v, i) => v - arr[i]*180/Math.PI))
handler.selectedKeyFrame.rotationMap.set(selected.name, values.map((v, i) => v - arr[i]*180/Math.PI))
handler.selectedKeyFrame.skip = false
handler.forcedAnimationTicks = null

Expand All @@ -157,10 +158,10 @@ export class AnimationStudio {
* @param {*} updateDisplay whether to update the dispaly (cube values)
* @param {*} updateSilent whether to be silent. Should be true when the poisition is not being set by the player.
* If true, updates the keyframe
* @param {DCMCube} selected the selected cube to update to.
*/
setPosition(values, updateDisplay = true, updateSilent = false) {
let selected = this.raytracer.oneSelected()
if(selected !== null) {
setPosition(values, updateDisplay = true, updateSilent = false, selected = this.raytracer.oneSelected()?.tabulaCube) {
if(selected != null) {
//Update the display if needed. Either silently or not silently.
if(updateDisplay) {
if(updateSilent) {
Expand Down Expand Up @@ -188,7 +189,7 @@ export class AnimationStudio {
//Get the position of the cube, and calculate what it would take to get from values to that rottation.
//Set that in the keyframe data.
let arr = selected.cubeGroup.position.toArray()
handler.selectedKeyFrame.rotationPointMap.set(selected.tabulaCube.name, values.map((v, i) => v - arr[i]))
handler.selectedKeyFrame.rotationPointMap.set(selected.name, values.map((v, i) => v - arr[i]))
handler.selectedKeyFrame.skip = false
handler.forcedAnimationTicks = null

Expand All @@ -205,10 +206,10 @@ export class AnimationStudio {
* @param {*} updateDisplay whether to update the dispaly (cube values)
* @param {*} updateSilent whether to be silent. Should be true when the cube grow is not being set by the player.
* If true, updates the keyframe
* @param {DCMCube} selected the selected cube to update to.
*/
setCubeGrow(values, updateDisplay = true, updateSilent = false) {
let selected = this.raytracer.oneSelected()
if(selected !== null) {
setCubeGrow(values, updateDisplay = true, updateSilent = false, selected = this.raytracer.oneSelected()?.tabulaCube) {
if(selected != null) {
//Update the display if needed. Either silently or not silently.
if(updateDisplay) {
if(updateSilent) {
Expand Down Expand Up @@ -236,8 +237,8 @@ export class AnimationStudio {

//Get the cube grow of the cube, and calculate what it would take to get from values to that rottation.
//Set that in the keyframe data.
let arr = selected.parent.position.toArray().map(e => -e)
handler.selectedKeyFrame.cubeGrowMap.set(selected.tabulaCube.name, values.map((v, i) => v - arr[i]))
let arr = selected.cubeGrowGroup.position.toArray().map(e => -e)
handler.selectedKeyFrame.cubeGrowMap.set(selected.name, values.map((v, i) => v - arr[i]))
handler.selectedKeyFrame.skip = false
handler.forcedAnimationTicks = null

Expand Down
176 changes: 166 additions & 10 deletions js/animator/gumball.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { Vector3 } from "../three.js";
import { BufferAttribute, BufferGeometry, CubeGeometry, Euler, Geometry, Group, Line, LineBasicMaterial, Mesh, MeshLambertMaterial, Object3D, Quaternion, Vector3 } from "../three.js";
import { Bone3D, Chain3D, Structure3D, V3 } from "../fik.js"
import { LinkedSelectableList, LinkedElement, ToggleableElement } from "../util.js";

const translateIK = "translate-ik"
const translate = "translate"

const tempVec = new Vector3()
const tempQuat = new Quaternion()
const worldQuat = new Quaternion()
const tempEuler = new Euler()
tempEuler.order = "ZYX"


const cubeHelperMesh = new Mesh(new CubeGeometry(1/32, 1/32, 1/32), new MeshLambertMaterial({ color: 0x528F15 }))
/**
* The animation gumball.
*/
Expand All @@ -14,22 +26,107 @@ export class Gumball {
this.raytracer = studio.raytracer
this.transformControls = studio.transformControls

let ikSolver = new Structure3D()

//Starting rot/pos is the starting position/rotation for the selected cube.
//Used to interpolate properly
let startingRot = new Vector3()
let startingPos = new Vector3()

let ikData = []
let ikStartingRotation = new Quaternion()
this.ikAnchor = new Object3D()
this.ikAnchor.rotation.order = "ZYX"
studio.group.add(this.ikAnchor)

let ikHelpers = []
let ikLinePositionBuffer = new BufferAttribute(new Float32Array(32*3), 3)
let ikLineHelper = new Line(new BufferGeometry().addAttribute('position', ikLinePositionBuffer), new LineBasicMaterial( { color:0x528F15 } ))

this.transformControls.addEventListener('mouseUp', () => {
ikSolver.clear()
ikData.length = 0
ikHelpers.forEach(data => studio.group.remove(data.mesh))
ikHelpers.length = 0
studio.group.remove(ikLineHelper)
})

let updateHelpers = () => {
ikHelpers.forEach((helper, index) => {
let cube = helper.cube
let mesh = helper.mesh
cube.cubeGroup.getWorldQuaternion(mesh.quaternion)
cube.getWorldPosition(0.5, 0.5, 0.5, mesh.position)

ikLinePositionBuffer.setXYZ(index, mesh.position.x, mesh.position.y, mesh.position.z)
ikLinePositionBuffer.needsUpdate = true
})
}

this.transformControls.addEventListener('mouseDown', () => {
let selected = this.raytracer.oneSelected()
if(selected === null) {
return
}

let handler = studio.pth.animationTabs.active
if(handler !== null && this.transformType.value === translateIK) {
let chain = new Chain3D()
//Get a list from the selected cube to either the root cube, or a cube marked as IK locked
let allCubes = []
for(let cube = selected.tabulaCube; cube.parent !== undefined; cube = cube.parent) {
allCubes.push(cube)
//Cube is ik locked
if(handler.ikaCubes.includes(cube.name)) {
break
}
}

ikLineHelper.geometry.setDrawRange(0, allCubes.length)
studio.group.add(ikLineHelper)

allCubes.reverse()
let previousPosition = null
let previousCube = null
//Iterate over those cubes, createing bones inbetween the cubes.
allCubes.forEach(cube => {
let p = cube.cubeGroup.getWorldPosition(tempVec)
//not the first time
if(previousPosition !== null) {
let start = new V3(...previousPosition)
let end = new V3(p.x, p.y, p.z)
let bone = new Bone3D(start, end)

ikData.push( {
cube: previousCube,
startingWorldRot: previousCube.cubeGroup.getWorldQuaternion(new Quaternion()),
offset: new Vector3(end.x-start.x, end.y-start.y, end.z-start.z).normalize(),
} )
chain.addBone(bone)
}

let mesh = cubeHelperMesh.clone()
studio.group.add(mesh)
ikHelpers.push( { cube, mesh } )

previousPosition = p.toArray()
previousCube = cube
})
if(allCubes.length !== 0) {
ikSolver.add(chain, this.ikAnchor.position)
ikSolver.update()
updateHelpers()
selected.cubeGroup.getWorldQuaternion(ikStartingRotation)
}
}

startingRot.x = selected.cubeGroup.rotation.x
startingRot.y = selected.cubeGroup.rotation.y
startingRot.z = selected.cubeGroup.rotation.z

startingPos.copy(selected.cubeGroup.position)
})


//Handle the rotate data.
this.transformControls.addEventListener('studioRotate', e => {
Expand All @@ -50,14 +147,65 @@ export class Gumball {

//Handle the translate data
this.transformControls.addEventListener('studioTranslate', e => {
let selected = this.raytracer.oneSelected()
if(selected === null) {
return
let selected = this.raytracer.oneSelected()
if(selected !== null && this.transformType.value !== translateIK) {
selected.cubeGroup.position.copy(e.axis).multiplyScalar(e.length).add(startingPos)
studio.setPosition(selected.cubeGroup.position.toArray())
studio.runFrame()
}
})

selected.cubeGroup.position.copy(e.axis).multiplyScalar(e.length).add(startingPos)
studio.setPosition(selected.cubeGroup.position.toArray())
studio.runFrame()
//Handles the inverse kinematics
this.transformControls.addEventListener('objectChange', () => {
let handler = studio.pth.animationTabs.active
if(this.transformType.value === translateIK && handler !== null) {
//We rely on some three.js element math stuff, so we need to make sure the model is animated correctly
studio.pth.model.resetAnimations()
studio.keyframeManager.setForcedAniamtionTicks()
handler.animate(0)
studio.pth.model.modelCache.updateMatrixWorld(true)

let selected = this.raytracer.oneSelected()
if(selected === null) {
return
}
ikSolver.update()
updateHelpers()
let pushData = []

ikData.forEach((d, i) => {
let bone = ikSolver.chains[0].bones[i]
//Get the change in rotation that's been done.
//This way we can preserve the starting rotation.
tempVec.set(bone.end.x-bone.start.x, bone.end.y-bone.start.y, bone.end.z-bone.start.z).normalize()
tempQuat.setFromUnitVectors(d.offset, tempVec)

// parent_world * local = world
// => local = 'parent_world * world
let element = d.cube.cubeGroup
let worldRotation = tempQuat.multiply(d.startingWorldRot)
let quat = element.parent.getWorldQuaternion(worldQuat).inverse().multiply(worldRotation)

//Get the euler angles and set it to the rotation. Push these changes.
let rot = d.cube.cubeGroup.rotation
rot.setFromQuaternion(quat)

let rotations = rot.toArray().map(a => a * 180 / Math.PI)
pushData.push( { rotations, cube: d.cube} )
d.cube.cubeGroup.updateMatrixWorld()
})

//We need to have the selected cube have the same axis. So here it basically "inverts" the parent changes.
selected.cubeGroup.parent.updateMatrixWorld()
let quat = selected.cubeGroup.parent.getWorldQuaternion(worldQuat).inverse().multiply(ikStartingRotation)
tempEuler.setFromQuaternion(quat)
let rotations = tempEuler.toArray().map(a => a * 180 / Math.PI)

//@todo: maybe in the future we can have a way to do all the changes together, rather than one at a time
//as the studio re-animates the entire model at least once when changes are made.
studio.setRotation(rotations)
pushData.forEach(d => studio.setRotation(d.rotations, false, false, d.cube))
}
})

//Creates the transform type and global mode toggle elements.
Expand All @@ -71,19 +219,27 @@ export class Gumball {
* Called when a cube selection changes or the transform type changes.
*/
selectChanged() {
this.setMode(this.transformType.value)
if(this.transformType.value === translateIK) {
this.setMode(translate, true)
} else {
this.setMode(this.transformType.value)
}
}

/**
* Sets the mode for the transform tools, or detaches the transform tools if no cubes are selected.
* @param {string} mode the mode to set as. Should be `translate` or `rotate`
* @param {boolean} isIk true if is inverse kinematics, false otherwise
*/
setMode(mode) {
setMode(mode, isIk = false) {
let selected = this.raytracer.oneSelected()
if(selected === null || mode === null || mode === undefined) {
this.transformControls.detach()
} else {
this.transformControls.attach(selected.parent);
if(isIk) {
selected.cubeGroup.matrixWorld.decompose(this.ikAnchor.position, this.ikAnchor.quaternion, tempVec)
}
this.transformControls.attach(isIk ? this.ikAnchor : selected.parent );
this.transformControls.mode = mode
}
}
Expand Down
2 changes: 1 addition & 1 deletion js/animator/panel_buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class PanelButtons {
return
}
//Updat the slider with the ticks. If visibleTicks isn't null use that. Otherwise use normal ticks.
let ticks = this.playstate.visibleTicks !== null ? this.playstate.visibleTicks : this.playstate.ticks
let ticks = active.playstate.visibleTicks !== null ? active.playstate.visibleTicks : active.playstate.ticks
this.inputPlaybackRange.attr('min', active.minTime).attr('max', active.totalTime).val(ticks)
}

Expand Down
Loading