pex-renderer
Version:
Physically Based Renderer for Pex
198 lines (168 loc) • 5.45 kB
JavaScript
const quat = require('pex-math/quat')
const vec3 = require('pex-math/vec3')
const vec4 = require('pex-math/vec4')
const utils = require('pex-math/utils')
const Signal = require('signals')
let currentOutputVec3 = vec3.create()
let currentOutputQuat = quat.create()
// Assumptions:
// - all channels have the same time length
// - animation channels can reference other entities
// - currently all animations track time by themselves
function Animation(opts) {
this.type = 'Animation'
this.entity = null
this.enabled = true
this.playing = false
this.loop = false
this.time = 0 // seconds
this.prevTime = Date.now() // ms
this.channels = opts.channels || []
this.changed = new Signal()
this.needsUpdate = true
this.set(opts)
}
Animation.prototype.init = function(entity) {
this.entity = entity
}
Animation.prototype.set = function(opts) {
Object.assign(this, opts)
Object.keys(opts).forEach((prop) => this.changed.dispatch(prop))
if (opts.autoplay || opts.playing) {
this.playing = true
// reset timer to avoid jumps
this.time = 0
this.prevTime = Date.now()
}
this.needsUpdate = true
}
Animation.prototype.update = function() {
if (!this.enabled) return
if (this.playing) {
const animationLength = this.channels[0].input[
this.channels[0].input.length - 1
]
const now = Date.now()
const deltaTime = (now - this.prevTime) / 1000
this.prevTime = now
this.time += deltaTime
if (this.time > animationLength) {
if (this.loop) {
this.time %= animationLength
} else {
this.time = 0
this.set({ playing: false })
}
}
this.needsUpdate = true
}
if (!this.needsUpdate) return
this.needsUpdate = false
for (let i = 0; i < this.channels.length; i++) {
const channel = this.channels[i]
const inputData = channel.input
let prevIndex
let nextIndex
for (let j = 0; j < inputData.length; j++) {
nextIndex = j
if (inputData[j] >= this.time) {
break
}
prevIndex = nextIndex
}
const isRotation = channel.path === 'rotation'
const outputData = channel.output
const prevInput = inputData[prevIndex]
const nextInput = inputData[nextIndex]
const scale = nextInput - prevInput
const t = (this.time - prevInput) / scale
if (prevIndex !== undefined) {
switch (channel.interpolation) {
case 'STEP':
if (isRotation) {
quat.set(currentOutputQuat, outputData[prevIndex])
} else {
vec3.set(currentOutputVec3, outputData[prevIndex])
}
break
case 'CUBICSPLINE': {
const vec = isRotation ? vec4 : vec3
const tt = t * t
const ttt = tt * t
// Each input value corresponds to three output values of the same type: in-tangent, data point, and out-tangent.
// p0
const prevPosition = vec.copy(outputData[prevIndex * 3 + 1])
// p1
const nextPos = vec.copy(outputData[nextIndex * 3 + 1])
// m0 = (tk+1 - tk)bk
const prevOutTangent = prevIndex
? vec.scale(vec.copy(outputData[prevIndex * 3 + 2]), scale)
: vec.create()
// m1 = (tk+1 - tk)ak+1
const nextInTangent =
nextIndex !== inputData.length - 1
? vec.scale(vec.copy(outputData[prevIndex * 3]), scale)
: vec.create()
// p(t) = (2t³ - 3t² + 1)p0 + (t³ - 2t² + t)m0 + (-2t³ + 3t²)p1 + (t³ - t²)m1
const p0 = vec.scale(prevPosition, 2 * ttt - 3 * tt + 1)
const m0 = vec.scale(prevOutTangent, ttt - 2 * tt + t)
const p1 = vec.scale(nextPos, -2 * ttt + 3 * tt)
const m1 = vec.scale(nextInTangent, ttt - tt)
if (isRotation) {
quat.set(
currentOutputQuat,
quat.normalize([
p0[0] + m0[0] + p1[0] + m1[0],
p0[1] + m0[1] + p1[1] + m1[1],
p0[2] + m0[2] + p1[2] + m1[2],
p0[3] + m0[3] + p1[3] + m1[3]
])
)
} else {
vec3.set(
currentOutputVec3,
vec3.add(vec3.add(vec3.add(p0, m0), p1), m1)
)
}
break
}
default:
// LINEAR
if (isRotation) {
quat.slerp(
quat.set(currentOutputQuat, outputData[prevIndex]),
outputData[nextIndex],
t
)
} else {
vec3.set(
currentOutputVec3,
outputData[nextIndex].map((output, index) =>
utils.lerp(outputData[prevIndex][index], output, t)
)
)
}
}
if (isRotation) {
channel.target.transform.set({
rotation: quat.copy(currentOutputQuat)
})
} else if (channel.path === 'translation') {
channel.target.transform.set({
position: vec3.copy(currentOutputVec3)
})
} else if (channel.path === 'scale') {
channel.target.transform.set({
scale: vec3.copy(currentOutputVec3)
})
} else if (channel.path === 'weights') {
channel.target.getComponent('Morph').set({
weights: outputData[nextIndex].slice()
})
}
}
}
}
module.exports = function createMorph(opts) {
return new Animation(opts)
}