// Sampler clip is especially for the animation sampler in glTF
// Use Typed Array can reduce a lot of heap memory
import quat from '../glmatrix/quat';
import vec3 from '../glmatrix/vec3';
// lerp function with offset in large array
function vec3lerp(out, a, b, t, oa, ob) {
var ax = a[oa];
var ay = a[oa + 1];
var az = a[oa + 2];
out[0] = ax + t * (b[ob] - ax);
out[1] = ay + t * (b[ob + 1] - ay);
out[2] = az + t * (b[ob + 2] - az);
return out;
}
function quatSlerp(out, a, b, t, oa, ob) {
// benchmarks:
// http://jsperf.com/quaternion-slerp-implementations
var ax = a[0 + oa], ay = a[1 + oa], az = a[2 + oa], aw = a[3 + oa],
bx = b[0 + ob], by = b[1 + ob], bz = b[2 + ob], bw = b[3 + ob];
var omega, cosom, sinom, scale0, scale1;
// calc cosine
cosom = ax * bx + ay * by + az * bz + aw * bw;
// adjust signs (if necessary)
if (cosom < 0.0) {
cosom = -cosom;
bx = - bx;
by = - by;
bz = - bz;
bw = - bw;
}
// calculate coefficients
if ((1.0 - cosom) > 0.000001) {
// standard case (slerp)
omega = Math.acos(cosom);
sinom = Math.sin(omega);
scale0 = Math.sin((1.0 - t) * omega) / sinom;
scale1 = Math.sin(t * omega) / sinom;
}
else {
// 'from' and 'to' quaternions are very close
// ... so we can do a linear interpolation
scale0 = 1.0 - t;
scale1 = t;
}
// calculate final values
out[0] = scale0 * ax + scale1 * bx;
out[1] = scale0 * ay + scale1 * by;
out[2] = scale0 * az + scale1 * bz;
out[3] = scale0 * aw + scale1 * bw;
return out;
}
/**
* SamplerTrack manages `position`, `rotation`, `scale` tracks in animation of single scene node.
* @constructor
* @alias clay.animation.SamplerTrack
* @param {Object} [opts]
* @param {string} [opts.name] Track name
* @param {clay.Node} [opts.target] Target node's transform will updated automatically
*/
var SamplerTrack = function (opts) {
opts = opts || {};
this.name = opts.name || '';
/**
* @param {clay.Node}
*/
this.target = opts.target || null;
/**
* @type {Array}
*/
this.position = vec3.create();
/**
* Rotation is represented by a quaternion
* @type {Array}
*/
this.rotation = quat.create();
/**
* @type {Array}
*/
this.scale = vec3.fromValues(1, 1, 1);
this.channels = {
time: null,
position: null,
rotation: null,
scale: null
};
this._cacheKey = 0;
this._cacheTime = 0;
};
SamplerTrack.prototype.setTime = function (time) {
if (!this.channels.time) {
return;
}
var channels = this.channels;
var len = channels.time.length;
var key = -1;
// Only one frame
if (len === 1) {
if (channels.rotation) {
quat.copy(this.rotation, channels.rotation);
}
if (channels.position) {
vec3.copy(this.position, channels.position);
}
if (channels.scale) {
vec3.copy(this.scale, channels.scale);
}
return;
}
// Clamp
else if (time <= channels.time[0]) {
time = channels.time[0];
key = 0;
}
else if (time >= channels.time[len - 1]) {
time = channels.time[len - 1];
key = len - 2;
}
else {
if (time < this._cacheTime) {
var s = Math.min(len - 1, this._cacheKey + 1);
for (var i = s; i >= 0; i--) {
if (channels.time[i - 1] <= time && channels.time[i] > time) {
key = i - 1;
break;
}
}
}
else {
for (var i = this._cacheKey; i < len - 1; i++) {
if (channels.time[i] <= time && channels.time[i + 1] > time) {
key = i;
break;
}
}
}
}
if (key > -1) {
this._cacheKey = key;
this._cacheTime = time;
var start = key;
var end = key + 1;
var startTime = channels.time[start];
var endTime = channels.time[end];
var range = endTime - startTime;
var percent = range === 0 ? 0 : (time - startTime) / range;
if (channels.rotation) {
quatSlerp(this.rotation, channels.rotation, channels.rotation, percent, start * 4, end * 4);
}
if (channels.position) {
vec3lerp(this.position, channels.position, channels.position, percent, start * 3, end * 3);
}
if (channels.scale) {
vec3lerp(this.scale, channels.scale, channels.scale, percent, start * 3, end * 3);
}
}
// Loop handling
if (key === len - 2) {
this._cacheKey = 0;
this._cacheTime = 0;
}
this.updateTarget();
};
/**
* Update transform of target node manually
*/
SamplerTrack.prototype.updateTarget = function () {
var channels = this.channels;
if (this.target) {
// Only update target prop if have data.
if (channels.position) {
this.target.position.setArray(this.position);
}
if (channels.rotation) {
this.target.rotation.setArray(this.rotation);
}
if (channels.scale) {
this.target.scale.setArray(this.scale);
}
}
};
/**
* @return {number}
*/
SamplerTrack.prototype.getMaxTime = function () {
return this.channels.time[this.channels.time.length - 1];
};
/**
* @param {number} startTime
* @param {number} endTime
* @return {clay.animation.SamplerTrack}
*/
SamplerTrack.prototype.getSubTrack = function (startTime, endTime) {
var subClip = new SamplerTrack({
name: this.name
});
var minTime = this.channels.time[0];
startTime = Math.min(Math.max(startTime, minTime), this.life);
endTime = Math.min(Math.max(endTime, minTime), this.life);
var rangeStart = this._findRange(startTime);
var rangeEnd = this._findRange(endTime);
var count = rangeEnd[0] - rangeStart[0] + 1;
if (rangeStart[1] === 0 && rangeEnd[1] === 0) {
count -= 1;
}
if (this.channels.rotation) {
subClip.channels.rotation = new Float32Array(count * 4);
}
if (this.channels.position) {
subClip.channels.position = new Float32Array(count * 3);
}
if (this.channels.scale) {
subClip.channels.scale = new Float32Array(count * 3);
}
if (this.channels.time) {
subClip.channels.time = new Float32Array(count);
}
// Clip at the start
this.setTime(startTime);
for (var i = 0; i < 3; i++) {
subClip.channels.rotation[i] = this.rotation[i];
subClip.channels.position[i] = this.position[i];
subClip.channels.scale[i] = this.scale[i];
}
subClip.channels.time[0] = 0;
subClip.channels.rotation[3] = this.rotation[3];
for (var i = 1; i < count-1; i++) {
var i2;
for (var j = 0; j < 3; j++) {
i2 = rangeStart[0] + i;
subClip.channels.rotation[i * 4 + j] = this.channels.rotation[i2 * 4 + j];
subClip.channels.position[i * 3 + j] = this.channels.position[i2 * 3 + j];
subClip.channels.scale[i * 3 + j] = this.channels.scale[i2 * 3 + j];
}
subClip.channels.time[i] = this.channels.time[i2] - startTime;
subClip.channels.rotation[i * 4 + 3] = this.channels.rotation[i2 * 4 + 3];
}
// Clip at the end
this.setTime(endTime);
for (var i = 0; i < 3; i++) {
subClip.channels.rotation[(count - 1) * 4 + i] = this.rotation[i];
subClip.channels.position[(count - 1) * 3 + i] = this.position[i];
subClip.channels.scale[(count - 1) * 3 + i] = this.scale[i];
}
subClip.channels.time[(count - 1)] = endTime - startTime;
subClip.channels.rotation[(count - 1) * 4 + 3] = this.rotation[3];
// TODO set back ?
subClip.life = endTime - startTime;
return subClip;
};
SamplerTrack.prototype._findRange = function (time) {
var channels = this.channels;
var len = channels.time.length;
var start = -1;
for (var i = 0; i < len - 1; i++) {
if (channels.time[i] <= time && channels.time[i+1] > time) {
start = i;
}
}
var percent = 0;
if (start >= 0) {
var startTime = channels.time[start];
var endTime = channels.time[start+1];
var percent = (time-startTime) / (endTime-startTime);
}
// Percent [0, 1)
return [start, percent];
};
/**
* 1D blending between two clips
* @function
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c1
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c2
* @param {number} w
*/
SamplerTrack.prototype.blend1D = function (t1, t2, w) {
vec3.lerp(this.position, t1.position, t2.position, w);
vec3.lerp(this.scale, t1.scale, t2.scale, w);
quat.slerp(this.rotation, t1.rotation, t2.rotation, w);
};
/**
* 2D blending between three clips
* @function
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c1
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c2
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c3
* @param {number} f
* @param {number} g
*/
SamplerTrack.prototype.blend2D = (function () {
var q1 = quat.create();
var q2 = quat.create();
return function (t1, t2, t3, f, g) {
var a = 1 - f - g;
this.position[0] = t1.position[0] * a + t2.position[0] * f + t3.position[0] * g;
this.position[1] = t1.position[1] * a + t2.position[1] * f + t3.position[1] * g;
this.position[2] = t1.position[2] * a + t2.position[2] * f + t3.position[2] * g;
this.scale[0] = t1.scale[0] * a + t2.scale[0] * f + t3.scale[0] * g;
this.scale[1] = t1.scale[1] * a + t2.scale[1] * f + t3.scale[1] * g;
this.scale[2] = t1.scale[2] * a + t2.scale[2] * f + t3.scale[2] * g;
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb205403(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.quaternion.xmquaternionbarycentric(v=vs.85).aspx
var s = f + g;
if (s === 0) {
quat.copy(this.rotation, t1.rotation);
}
else {
quat.slerp(q1, t1.rotation, t2.rotation, s);
quat.slerp(q2, t1.rotation, t3.rotation, s);
quat.slerp(this.rotation, q1, q2, g / s);
}
};
})();
/**
* Additive blending between two clips
* @function
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c1
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c2
*/
SamplerTrack.prototype.additiveBlend = function (t1, t2) {
vec3.add(this.position, t1.position, t2.position);
vec3.add(this.scale, t1.scale, t2.scale);
quat.multiply(this.rotation, t2.rotation, t1.rotation);
};
/**
* Subtractive blending between two clips
* @function
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c1
* @param {clay.animation.SamplerTrack|clay.animation.TransformTrack} c2
*/
SamplerTrack.prototype.subtractiveBlend = function (t1, t2) {
vec3.sub(this.position, t1.position, t2.position);
vec3.sub(this.scale, t1.scale, t2.scale);
quat.invert(this.rotation, t2.rotation);
quat.multiply(this.rotation, this.rotation, t1.rotation);
};
/**
* Clone a new SamplerTrack
* @return {clay.animation.SamplerTrack}
*/
SamplerTrack.prototype.clone = function () {
var track = SamplerTrack.prototype.clone.call(this);
track.channels = {
time: this.channels.time || null,
position: this.channels.position || null,
rotation: this.channels.rotation || null,
scale: this.channels.scale || null
};
vec3.copy(track.position, this.position);
quat.copy(track.rotation, this.rotation);
vec3.copy(track.scale, this.scale);
track.target = this.target;
track.updateTarget();
return track;
};
export default SamplerTrack;