// 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;