(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (factory((global.clay = {}))); }(this, (function (exports) { 'use strict'; // 缓动函数来自 https://github.com/sole/tween.js/blob/master/src/Tween.js /** * @namespace clay.animation.easing */ var easing = { /** * @alias clay.animation.easing.linear * @param {number} k * @return {number} */ linear: function(k) { return k; }, /** * @alias clay.animation.easing.quadraticIn * @param {number} k * @return {number} */ quadraticIn: function(k) { return k * k; }, /** * @alias clay.animation.easing.quadraticOut * @param {number} k * @return {number} */ quadraticOut: function(k) { return k * (2 - k); }, /** * @alias clay.animation.easing.quadraticInOut * @param {number} k * @return {number} */ quadraticInOut: function(k) { if ((k *= 2) < 1) { return 0.5 * k * k; } return - 0.5 * (--k * (k - 2) - 1); }, /** * @alias clay.animation.easing.cubicIn * @param {number} k * @return {number} */ cubicIn: function(k) { return k * k * k; }, /** * @alias clay.animation.easing.cubicOut * @param {number} k * @return {number} */ cubicOut: function(k) { return --k * k * k + 1; }, /** * @alias clay.animation.easing.cubicInOut * @param {number} k * @return {number} */ cubicInOut: function(k) { if ((k *= 2) < 1) { return 0.5 * k * k * k; } return 0.5 * ((k -= 2) * k * k + 2); }, /** * @alias clay.animation.easing.quarticIn * @param {number} k * @return {number} */ quarticIn: function(k) { return k * k * k * k; }, /** * @alias clay.animation.easing.quarticOut * @param {number} k * @return {number} */ quarticOut: function(k) { return 1 - (--k * k * k * k); }, /** * @alias clay.animation.easing.quarticInOut * @param {number} k * @return {number} */ quarticInOut: function(k) { if ((k *= 2) < 1) { return 0.5 * k * k * k * k; } return - 0.5 * ((k -= 2) * k * k * k - 2); }, /** * @alias clay.animation.easing.quinticIn * @param {number} k * @return {number} */ quinticIn: function(k) { return k * k * k * k * k; }, /** * @alias clay.animation.easing.quinticOut * @param {number} k * @return {number} */ quinticOut: function(k) { return --k * k * k * k * k + 1; }, /** * @alias clay.animation.easing.quinticInOut * @param {number} k * @return {number} */ quinticInOut: function(k) { if ((k *= 2) < 1) { return 0.5 * k * k * k * k * k; } return 0.5 * ((k -= 2) * k * k * k * k + 2); }, /** * @alias clay.animation.easing.sinusoidalIn * @param {number} k * @return {number} */ sinusoidalIn: function(k) { return 1 - Math.cos(k * Math.PI / 2); }, /** * @alias clay.animation.easing.sinusoidalOut * @param {number} k * @return {number} */ sinusoidalOut: function(k) { return Math.sin(k * Math.PI / 2); }, /** * @alias clay.animation.easing.sinusoidalInOut * @param {number} k * @return {number} */ sinusoidalInOut: function(k) { return 0.5 * (1 - Math.cos(Math.PI * k)); }, /** * @alias clay.animation.easing.exponentialIn * @param {number} k * @return {number} */ exponentialIn: function(k) { return k === 0 ? 0 : Math.pow(1024, k - 1); }, /** * @alias clay.animation.easing.exponentialOut * @param {number} k * @return {number} */ exponentialOut: function(k) { return k === 1 ? 1 : 1 - Math.pow(2, - 10 * k); }, /** * @alias clay.animation.easing.exponentialInOut * @param {number} k * @return {number} */ exponentialInOut: function(k) { if (k === 0) { return 0; } if (k === 1) { return 1; } if ((k *= 2) < 1) { return 0.5 * Math.pow(1024, k - 1); } return 0.5 * (- Math.pow(2, - 10 * (k - 1)) + 2); }, /** * @alias clay.animation.easing.circularIn * @param {number} k * @return {number} */ circularIn: function(k) { return 1 - Math.sqrt(1 - k * k); }, /** * @alias clay.animation.easing.circularOut * @param {number} k * @return {number} */ circularOut: function(k) { return Math.sqrt(1 - (--k * k)); }, /** * @alias clay.animation.easing.circularInOut * @param {number} k * @return {number} */ circularInOut: function(k) { if ((k *= 2) < 1) { return - 0.5 * (Math.sqrt(1 - k * k) - 1); } return 0.5 * (Math.sqrt(1 - (k -= 2) * k) + 1); }, /** * @alias clay.animation.easing.elasticIn * @param {number} k * @return {number} */ elasticIn: function(k) { var s, a = 0.1, p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; }else{ s = p * Math.asin(1 / a) / (2 * Math.PI); } return - (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); }, /** * @alias clay.animation.easing.elasticOut * @param {number} k * @return {number} */ elasticOut: function(k) { var s, a = 0.1, p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; } else{ s = p * Math.asin(1 / a) / (2 * Math.PI); } return (a * Math.pow(2, - 10 * k) * Math.sin((k - s) * (2 * Math.PI) / p) + 1); }, /** * @alias clay.animation.easing.elasticInOut * @param {number} k * @return {number} */ elasticInOut: function(k) { var s, a = 0.1, p = 0.4; if (k === 0) { return 0; } if (k === 1) { return 1; } if (!a || a < 1) { a = 1; s = p / 4; } else{ s = p * Math.asin(1 / a) / (2 * Math.PI); } if ((k *= 2) < 1) { return - 0.5 * (a * Math.pow(2, 10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p)); } return a * Math.pow(2, -10 * (k -= 1)) * Math.sin((k - s) * (2 * Math.PI) / p) * 0.5 + 1; }, /** * @alias clay.animation.easing.backIn * @param {number} k * @return {number} */ backIn: function(k) { var s = 1.70158; return k * k * ((s + 1) * k - s); }, /** * @alias clay.animation.easing.backOut * @param {number} k * @return {number} */ backOut: function(k) { var s = 1.70158; return --k * k * ((s + 1) * k + s) + 1; }, /** * @alias clay.animation.easing.backInOut * @param {number} k * @return {number} */ backInOut: function(k) { var s = 1.70158 * 1.525; if ((k *= 2) < 1) { return 0.5 * (k * k * ((s + 1) * k - s)); } return 0.5 * ((k -= 2) * k * ((s + 1) * k + s) + 2); }, /** * @alias clay.animation.easing.bounceIn * @param {number} k * @return {number} */ bounceIn: function(k) { return 1 - easing.bounceOut(1 - k); }, /** * @alias clay.animation.easing.bounceOut * @param {number} k * @return {number} */ bounceOut: function(k) { if (k < (1 / 2.75)) { return 7.5625 * k * k; } else if (k < (2 / 2.75)) { return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75; } else if (k < (2.5 / 2.75)) { return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375; } else { return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375; } }, /** * @alias clay.animation.easing.bounceInOut * @param {number} k * @return {number} */ bounceInOut: function(k) { if (k < 0.5) { return easing.bounceIn(k * 2) * 0.5; } return easing.bounceOut(k * 2 - 1) * 0.5 + 0.5; } }; function noop () {} /** * @constructor * @alias clay.animation.Clip * @param {Object} [opts] * @param {Object} [opts.target] * @param {number} [opts.life] * @param {number} [opts.delay] * @param {number} [opts.gap] * @param {number} [opts.playbackRate] * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation * @param {string|Function} [opts.easing] * @param {Function} [opts.onframe] * @param {Function} [opts.onfinish] * @param {Function} [opts.onrestart] */ var Clip = function (opts) { opts = opts || {}; /** * @type {string} */ this.name = opts.name || ''; /** * @type {Object} */ this.target = opts.target; /** * @type {number} */ this.life = opts.life || 1000; /** * @type {number} */ this.delay = opts.delay || 0; /** * @type {number} */ this.gap = opts.gap || 0; /** * @type {number} */ this.playbackRate = opts.playbackRate || 1; this._initialized = false; this._elapsedTime = 0; this._loop = opts.loop == null ? false : opts.loop; this.setLoop(this._loop); if (opts.easing != null) { this.setEasing(opts.easing); } /** * @type {Function} */ this.onframe = opts.onframe || noop; /** * @type {Function} */ this.onfinish = opts.onfinish || noop; /** * @type {Function} */ this.onrestart = opts.onrestart || noop; this._paused = false; }; Clip.prototype = { gap: 0, life: 0, delay: 0, /** * @param {number|boolean} loop */ setLoop: function (loop) { this._loop = loop; if (loop) { if (typeof loop === 'number') { this._loopRemained = loop; } else { this._loopRemained = Infinity; } } }, /** * @param {string|Function} easing */ setEasing: function (easing$$1) { if (typeof(easing$$1) === 'string') { easing$$1 = easing[easing$$1]; } this.easing = easing$$1; }, /** * @param {number} time * @return {string} */ step: function (time, deltaTime, silent) { if (!this._initialized) { this._startTime = time + this.delay; this._initialized = true; } if (this._currentTime != null) { deltaTime = time - this._currentTime; } this._currentTime = time; if (this._paused) { return 'paused'; } if (time < this._startTime) { return; } // PENDIGN Sync ? this._elapse(time, deltaTime); var percent = Math.min(this._elapsedTime / this.life, 1); if (percent < 0) { return; } var schedule; if (this.easing) { schedule = this.easing(percent); } else { schedule = percent; } if (!silent) { this.fire('frame', schedule); } if (percent === 1) { if (this._loop && this._loopRemained > 0) { this._restartInLoop(time); this._loopRemained--; return 'restart'; } else { // Mark this clip to be deleted // In the animation.update this._needsRemove = true; return 'finish'; } } else { return null; } }, /** * @param {number} time * @return {string} */ setTime: function (time) { return this.step(time + this._startTime); }, restart: function (time) { // If user leave the page for a while, when he gets back // All clips may be expired and all start from the beginning value(position) // It is clearly wrong, so we use remainder to add a offset var remainder = 0; // Remainder ignored if restart is invoked manually if (time) { this._elapse(time); remainder = this._elapsedTime % this.life; } time = time || Date.now(); this._startTime = time - remainder + this.delay; this._elapsedTime = 0; this._needsRemove = false; this._paused = false; }, getElapsedTime: function () { return this._elapsedTime; }, _restartInLoop: function (time) { this._startTime = time + this.gap; this._elapsedTime = 0; }, _elapse: function (time, deltaTime) { this._elapsedTime += deltaTime * this.playbackRate; }, fire: function (eventType, arg) { var eventName = 'on' + eventType; if (this[eventName]) { this[eventName](this.target, arg); } }, clone: function () { var clip = new this.constructor(); clip.name = this.name; clip._loop = this._loop; clip._loopRemained = this._loopRemained; clip.life = this.life; clip.gap = this.gap; clip.delay = this.delay; return clip; }, /** * Pause the clip. */ pause: function () { this._paused = true; }, /** * Resume the clip. */ resume: function () { this._paused = false; } }; Clip.prototype.constructor = Clip; var arraySlice = Array.prototype.slice; function defaultGetter(target, key) { return target[key]; } function defaultSetter(target, key, value) { target[key] = value; } function interpolateNumber(p0, p1, percent) { return (p1 - p0) * percent + p0; } function interpolateArray(p0, p1, percent, out, arrDim) { var len = p0.length; if (arrDim == 1) { for (var i = 0; i < len; i++) { out[i] = interpolateNumber(p0[i], p1[i], percent); } } else { var len2 = p0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { out[i][j] = interpolateNumber( p0[i][j], p1[i][j], percent ); } } } } function isArrayLike(data) { if (typeof(data) == 'undefined') { return false; } else if (typeof(data) == 'string') { return false; } else { return typeof(data.length) == 'number'; } } function cloneValue(value) { if (isArrayLike(value)) { var len = value.length; if (isArrayLike(value[0])) { var ret = []; for (var i = 0; i < len; i++) { ret.push(arraySlice.call(value[i])); } return ret; } else { return arraySlice.call(value); } } else { return value; } } function catmullRomInterpolateArray( p0, p1, p2, p3, t, t2, t3, out, arrDim ) { var len = p0.length; if (arrDim == 1) { for (var i = 0; i < len; i++) { out[i] = catmullRomInterpolate( p0[i], p1[i], p2[i], p3[i], t, t2, t3 ); } } else { var len2 = p0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { out[i][j] = catmullRomInterpolate( p0[i][j], p1[i][j], p2[i][j], p3[i][j], t, t2, t3 ); } } } } function catmullRomInterpolate(p0, p1, p2, p3, t, t2, t3) { var v0 = (p2 - p0) * 0.5; var v1 = (p3 - p1) * 0.5; return (2 * (p1 - p2) + v0 + v1) * t3 + (- 3 * (p1 - p2) - 2 * v0 - v1) * t2 + v0 * t + p1; } // arr0 is source array, arr1 is target array. // Do some preprocess to avoid error happened when interpolating from arr0 to arr1 function fillArr(arr0, arr1, arrDim) { var arr0Len = arr0.length; var arr1Len = arr1.length; if (arr0Len !== arr1Len) { // FIXME Not work for TypedArray var isPreviousLarger = arr0Len > arr1Len; if (isPreviousLarger) { // Cut the previous arr0.length = arr1Len; } else { // Fill the previous for (var i = arr0Len; i < arr1Len; i++) { arr0.push( arrDim === 1 ? arr1[i] : arraySlice.call(arr1[i]) ); } } } // Handling NaN value var len2 = arr0[0] && arr0[0].length; for (var i = 0; i < arr0.length; i++) { if (arrDim === 1) { if (isNaN(arr0[i])) { arr0[i] = arr1[i]; } } else { for (var j = 0; j < len2; j++) { if (isNaN(arr0[i][j])) { arr0[i][j] = arr1[i][j]; } } } } } function isArraySame(arr0, arr1, arrDim) { if (arr0 === arr1) { return true; } var len = arr0.length; if (len !== arr1.length) { return false; } if (arrDim === 1) { for (var i = 0; i < len; i++) { if (arr0[i] !== arr1[i]) { return false; } } } else { var len2 = arr0[0].length; for (var i = 0; i < len; i++) { for (var j = 0; j < len2; j++) { if (arr0[i][j] !== arr1[i][j]) { return false; } } } } return true; } function createTrackClip(animator, globalEasing, oneTrackDone, keyframes, propName, interpolater, maxTime) { var getter = animator._getter; var setter = animator._setter; var useSpline = globalEasing === 'spline'; var trackLen = keyframes.length; if (!trackLen) { return; } // Guess data type var firstVal = keyframes[0].value; var isValueArray = isArrayLike(firstVal); // For vertices morphing var arrDim = ( isValueArray && isArrayLike(firstVal[0]) ) ? 2 : 1; // Sort keyframe as ascending keyframes.sort(function(a, b) { return a.time - b.time; }); // Percents of each keyframe var kfPercents = []; // Value of each keyframe var kfValues = []; // Easing funcs of each keyframe. var kfEasings = []; var prevValue = keyframes[0].value; var isAllValueEqual = true; for (var i = 0; i < trackLen; i++) { kfPercents.push(keyframes[i].time / maxTime); // Assume value is a color when it is a string var value = keyframes[i].value; // Check if value is equal, deep check if value is array if (!((isValueArray && isArraySame(value, prevValue, arrDim)) || (!isValueArray && value === prevValue))) { isAllValueEqual = false; } prevValue = value; kfValues.push(value); kfEasings.push(keyframes[i].easing); } if (isAllValueEqual) { return; } var lastValue = kfValues[trackLen - 1]; // Polyfill array and NaN value for (var i = 0; i < trackLen - 1; i++) { if (isValueArray) { fillArr(kfValues[i], lastValue, arrDim); } else { if (isNaN(kfValues[i]) && !isNaN(lastValue)) { kfValues[i] = lastValue; } } } isValueArray && fillArr(getter(animator._target, propName), lastValue, arrDim); // Cache the key of last frame to speed up when // animation playback is sequency var cacheKey = 0; var cachePercent = 0; var start; var i, w; var p0, p1, p2, p3; var onframe = function(target, percent) { // Find the range keyframes // kf1-----kf2---------current--------kf3 // find kf2(i) and kf3(i + 1) and do interpolation if (percent < cachePercent) { // Start from next key start = Math.min(cacheKey + 1, trackLen - 1); for (i = start; i >= 0; i--) { if (kfPercents[i] <= percent) { break; } } i = Math.min(i, trackLen - 2); } else { for (i = cacheKey; i < trackLen; i++) { if (kfPercents[i] > percent) { break; } } i = Math.min(i - 1, trackLen - 2); } cacheKey = i; cachePercent = percent; var range = (kfPercents[i + 1] - kfPercents[i]); if (range === 0) { return; } else { w = (percent - kfPercents[i]) / range; // Clamp 0 - 1 w = Math.max(Math.min(1, w), 0); } w = kfEasings[i + 1](w); if (useSpline) { p1 = kfValues[i]; p0 = kfValues[i === 0 ? i : i - 1]; p2 = kfValues[i > trackLen - 2 ? trackLen - 1 : i + 1]; p3 = kfValues[i > trackLen - 3 ? trackLen - 1 : i + 2]; if (interpolater) { setter( target, propName, interpolater( getter(target, propName), p0, p1, p2, p3, w ) ); } else if (isValueArray) { catmullRomInterpolateArray( p0, p1, p2, p3, w, w*w, w*w*w, getter(target, propName), arrDim ); } else { setter( target, propName, catmullRomInterpolate(p0, p1, p2, p3, w, w*w, w*w*w) ); } } else { if (interpolater) { setter( target, propName, interpolater( getter(target, propName), kfValues[i], kfValues[i + 1], w ) ); } else if (isValueArray) { interpolateArray( kfValues[i], kfValues[i+1], w, getter(target, propName), arrDim ); } else { setter( target, propName, interpolateNumber(kfValues[i], kfValues[i+1], w) ); } } }; var clip = new Clip({ target: animator._target, life: maxTime, loop: animator._loop, delay: animator._delay, onframe: onframe, onfinish: oneTrackDone }); if (globalEasing && globalEasing !== 'spline') { clip.setEasing(globalEasing); } return clip; } /** * @description Animator object can only be created by Animation.prototype.animate method. * After created, we can use {@link clay.animation.Animator#when} to add all keyframes and {@link clay.animation.Animator#start} it. * Clips will be automatically created and added to the animation instance which created this deferred object. * * @constructor clay.animation.Animator * * @param {Object} target * @param {boolean} loop * @param {Function} getter * @param {Function} setter * @param {Function} interpolater */ function Animator(target, loop, getter, setter, interpolater) { this._tracks = {}; this._target = target; this._loop = loop || false; this._getter = getter || defaultGetter; this._setter = setter || defaultSetter; this._interpolater = interpolater || null; this._delay = 0; this._doneList = []; this._onframeList = []; this._clipList = []; this._maxTime = 0; this._lastKFTime = 0; } function noopEasing(w) { return w; } Animator.prototype = { constructor: Animator, /** * @param {number} time Keyframe time using millisecond * @param {Object} props A key-value object. Value can be number, 1d and 2d array * @param {string|Function} [easing] * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ when: function (time, props, easing$$1) { this._maxTime = Math.max(time, this._maxTime); easing$$1 = (typeof easing$$1 === 'function' ? easing$$1 : easing[easing$$1]) || noopEasing; for (var propName in props) { if (!this._tracks[propName]) { this._tracks[propName] = []; // If time is 0 // Then props is given initialize value // Else // Initialize value from current prop value if (time !== 0) { this._tracks[propName].push({ time: 0, value: cloneValue( this._getter(this._target, propName) ), easing: easing$$1 }); } } this._tracks[propName].push({ time: parseInt(time), value: props[propName], easing: easing$$1 }); } return this; }, /** * @param {number} time During time since last keyframe * @param {Object} props A key-value object. Value can be number, 1d and 2d array * @param {string|Function} [easing] * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ then: function (duringTime, props, easing$$1) { this.when(duringTime + this._lastKFTime, props, easing$$1); this._lastKFTime += duringTime; return this; }, /** * callback when running animation * @param {Function} callback callback have two args, animating target and current percent * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ during: function (callback) { this._onframeList.push(callback); return this; }, _doneCallback: function () { // Clear all tracks this._tracks = {}; // Clear all clips this._clipList.length = 0; var doneList = this._doneList; var len = doneList.length; for (var i = 0; i < len; i++) { doneList[i].call(this); } }, /** * Start the animation * @param {string|Function} easing * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ start: function (globalEasing) { var self = this; var clipCount = 0; var oneTrackDone = function() { clipCount--; if (clipCount === 0) { self._doneCallback(); } }; var lastClip; for (var propName in this._tracks) { var clip = createTrackClip( this, globalEasing, oneTrackDone, this._tracks[propName], propName, self._interpolater, self._maxTime ); if (clip) { this._clipList.push(clip); clipCount++; // If start after added to animation if (this.animation) { this.animation.addClip(clip); } lastClip = clip; } } // Add during callback on the last clip if (lastClip) { var oldOnFrame = lastClip.onframe; lastClip.onframe = function (target, percent) { oldOnFrame(target, percent); for (var i = 0; i < self._onframeList.length; i++) { self._onframeList[i](target, percent); } }; } if (!clipCount) { this._doneCallback(); } return this; }, /** * Stop the animation * @memberOf clay.animation.Animator.prototype */ stop: function () { for (var i = 0; i < this._clipList.length; i++) { var clip = this._clipList[i]; this.animation.removeClip(clip); } this._clipList = []; }, /** * Delay given milliseconds * @param {number} time * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ delay: function (time){ this._delay = time; return this; }, /** * Callback after animation finished * @param {Function} func * @return {clay.animation.Animator} * @memberOf clay.animation.Animator.prototype */ done: function (func) { if (func) { this._doneList.push(func); } return this; }, /** * Get all clips created in start method. * @return {clay.animation.Clip[]} * @memberOf clay.animation.Animator.prototype */ getClips: function () { return this._clipList; } }; // 1D Blend clip of blend tree // http://docs.unity3d.com/Documentation/Manual/1DBlending.html var clipSortFunc = function (a, b) { return a.position < b.position; }; /** * @typedef {Object} clay.animation.Blend1DClip.IClipInput * @property {number} position * @property {clay.animation.Clip} clip * @property {number} offset */ /** * 1d blending node in animation blend tree. * output clip must have blend1D and copy method * @constructor * @alias clay.animation.Blend1DClip * @extends clay.animation.Clip * * @param {Object} [opts] * @param {string} [opts.name] * @param {Object} [opts.target] * @param {number} [opts.life] * @param {number} [opts.delay] * @param {number} [opts.gap] * @param {number} [opts.playbackRatio] * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation * @param {string|Function} [opts.easing] * @param {Function} [opts.onframe] * @param {Function} [opts.onfinish] * @param {Function} [opts.onrestart] * @param {object[]} [opts.inputs] * @param {number} [opts.position] * @param {clay.animation.Clip} [opts.output] */ var Blend1DClip = function (opts) { opts = opts || {}; Clip.call(this, opts); /** * Output clip must have blend1D and copy method * @type {clay.animation.Clip} */ this.output = opts.output || null; /** * @type {clay.animation.Blend1DClip.IClipInput[]} */ this.inputs = opts.inputs || []; /** * @type {number} */ this.position = 0; this._cacheKey = 0; this._cachePosition = -Infinity; this.inputs.sort(clipSortFunc); }; Blend1DClip.prototype = new Clip(); Blend1DClip.prototype.constructor = Blend1DClip; /** * @param {number} position * @param {clay.animation.Clip} inputClip * @param {number} [offset] * @return {clay.animation.Blend1DClip.IClipInput} */ Blend1DClip.prototype.addInput = function (position, inputClip, offset) { var obj = { position: position, clip: inputClip, offset: offset || 0 }; this.life = Math.max(inputClip.life, this.life); if (!this.inputs.length) { this.inputs.push(obj); return obj; } var len = this.inputs.length; if (this.inputs[0].position > position) { this.inputs.unshift(obj); } else if (this.inputs[len - 1].position <= position) { this.inputs.push(obj); } else { var key = this._findKey(position); this.inputs.splice(key, obj); } return obj; }; Blend1DClip.prototype.step = function (time, dTime, silent) { var ret = Clip.prototype.step.call(this, time); if (ret !== 'finish') { this.setTime(this.getElapsedTime()); } // PENDING Schedule if (!silent && ret !== 'paused') { this.fire('frame'); } return ret; }; Blend1DClip.prototype.setTime = function (time) { var position = this.position; var inputs = this.inputs; var len = inputs.length; var min = inputs[0].position; var max = inputs[len-1].position; if (position <= min || position >= max) { var in0 = position <= min ? inputs[0] : inputs[len-1]; var clip = in0.clip; var offset = in0.offset; clip.setTime((time + offset) % clip.life); // Input clip is a blend clip // PENDING if (clip.output instanceof Clip) { this.output.copy(clip.output); } else { this.output.copy(clip); } } else { var key = this._findKey(position); var in1 = inputs[key]; var in2 = inputs[key + 1]; var clip1 = in1.clip; var clip2 = in2.clip; // Set time on input clips clip1.setTime((time + in1.offset) % clip1.life); clip2.setTime((time + in2.offset) % clip2.life); var w = (this.position - in1.position) / (in2.position - in1.position); var c1 = clip1.output instanceof Clip ? clip1.output : clip1; var c2 = clip2.output instanceof Clip ? clip2.output : clip2; this.output.blend1D(c1, c2, w); } }; /** * Clone a new Blend1D clip * @param {boolean} cloneInputs True if clone the input clips * @return {clay.animation.Blend1DClip} */ Blend1DClip.prototype.clone = function (cloneInputs) { var clip = Clip.prototype.clone.call(this); clip.output = this.output.clone(); for (var i = 0; i < this.inputs.length; i++) { var inputClip = cloneInputs ? this.inputs[i].clip.clone(true) : this.inputs[i].clip; clip.addInput(this.inputs[i].position, inputClip, this.inputs[i].offset); } return clip; }; // Find the key where position in range [inputs[key].position, inputs[key+1].position) Blend1DClip.prototype._findKey = function (position) { var key = -1; var inputs = this.inputs; var len = inputs.length; if (this._cachePosition < position) { for (var i = this._cacheKey; i < len-1; i++) { if (position >= inputs[i].position && position < inputs[i+1].position) { key = i; } } } else { var s = Math.min(len-2, this._cacheKey); for (var i = s; i >= 0; i--) { if (position >= inputs[i].position && position < inputs[i+1].position) { key = i; } } } if (key >= 0) { this._cacheKey = key; this._cachePosition = position; } return key; }; // Delaunay Triangulation // Modified from https://github.com/ironwallaby/delaunay var EPSILON = 1.0 / 1048576.0; function supertriangle(vertices) { var xmin = Number.POSITIVE_INFINITY; var ymin = Number.POSITIVE_INFINITY; var xmax = Number.NEGATIVE_INFINITY; var ymax = Number.NEGATIVE_INFINITY; var i, dx, dy, dmax, xmid, ymid; for (i = vertices.length; i--; ) { if (vertices[i][0] < xmin) { xmin = vertices[i][0]; } if (vertices[i][0] > xmax) { xmax = vertices[i][0]; } if (vertices[i][1] < ymin) { ymin = vertices[i][1]; } if (vertices[i][1] > ymax) { ymax = vertices[i][1]; } } dx = xmax - xmin; dy = ymax - ymin; dmax = Math.max(dx, dy); xmid = xmin + dx * 0.5; ymid = ymin + dy * 0.5; return [ [xmid - 20 * dmax, ymid - dmax], [xmid , ymid + 20 * dmax], [xmid + 20 * dmax, ymid - dmax] ]; } function circumcircle(vertices, i, j, k) { var x1 = vertices[i][0], y1 = vertices[i][1], x2 = vertices[j][0], y2 = vertices[j][1], x3 = vertices[k][0], y3 = vertices[k][1], fabsy1y2 = Math.abs(y1 - y2), fabsy2y3 = Math.abs(y2 - y3), xc, yc, m1, m2, mx1, mx2, my1, my2, dx, dy; /* Check for coincident points */ if (fabsy1y2 < EPSILON && fabsy2y3 < EPSILON) { throw new Error('Eek! Coincident points!'); } if (fabsy1y2 < EPSILON) { m2 = -((x3 - x2) / (y3 - y2)); mx2 = (x2 + x3) / 2.0; my2 = (y2 + y3) / 2.0; xc = (x2 + x1) / 2.0; yc = m2 * (xc - mx2) + my2; } else if (fabsy2y3 < EPSILON) { m1 = -((x2 - x1) / (y2 - y1)); mx1 = (x1 + x2) / 2.0; my1 = (y1 + y2) / 2.0; xc = (x3 + x2) / 2.0; yc = m1 * (xc - mx1) + my1; } else { m1 = -((x2 - x1) / (y2 - y1)); m2 = -((x3 - x2) / (y3 - y2)); mx1 = (x1 + x2) / 2.0; mx2 = (x2 + x3) / 2.0; my1 = (y1 + y2) / 2.0; my2 = (y2 + y3) / 2.0; xc = (m1 * mx1 - m2 * mx2 + my2 - my1) / (m1 - m2); yc = (fabsy1y2 > fabsy2y3) ? m1 * (xc - mx1) + my1 : m2 * (xc - mx2) + my2; } dx = x2 - xc; dy = y2 - yc; return {i: i, j: j, k: k, x: xc, y: yc, r: dx * dx + dy * dy}; } function dedup(edges) { var i, j, a, b, m, n; for (j = edges.length; j; ) { b = edges[--j]; a = edges[--j]; for (i = j; i; ) { n = edges[--i]; m = edges[--i]; if ((a === m && b === n) || (a === n && b === m)) { edges.splice(j, 2); edges.splice(i, 2); break; } } } } var delaunay = { triangulate: function(vertices, key) { var n = vertices.length; var i, j, indices, st, open, closed, edges, dx, dy, a, b, c; /* Bail if there aren't enough vertices to form any triangles. */ if (n < 3) { return []; } /* Slice out the actual vertices from the passed objects. (Duplicate the * array even if we don't, though, since we need to make a supertriangle * later on!) */ vertices = vertices.slice(0); if (key) { for (i = n; i--; ) { vertices[i] = vertices[i][key]; } } /* Make an array of indices into the vertex array, sorted by the * vertices' x-position. Force stable sorting by comparing indices if * the x-positions are equal. */ indices = new Array(n); for (i = n; i--; ) { indices[i] = i; } indices.sort(function(i, j) { var diff = vertices[j][0] - vertices[i][0]; return diff !== 0 ? diff : i - j; }); /* Next, find the vertices of the supertriangle (which contains all other * triangles), and append them onto the end of a (copy of) the vertex * array. */ st = supertriangle(vertices); vertices.push(st[0], st[1], st[2]); /* Initialize the open list (containing the supertriangle and nothing * else) and the closed list (which is empty since we havn't processed * any triangles yet). */ open = [circumcircle(vertices, n + 0, n + 1, n + 2)]; closed = []; edges = []; /* Incrementally add each vertex to the mesh. */ for (i = indices.length; i--; edges.length = 0) { c = indices[i]; /* For each open triangle, check to see if the current point is * inside it's circumcircle. If it is, remove the triangle and add * it's edges to an edge list. */ for (j = open.length; j--; ) { /* If this point is to the right of this triangle's circumcircle, * then this triangle should never get checked again. Remove it * from the open list, add it to the closed list, and skip. */ dx = vertices[c][0] - open[j].x; if (dx > 0.0 && dx * dx > open[j].r) { closed.push(open[j]); open.splice(j, 1); continue; } /* If we're outside the circumcircle, skip this triangle. */ dy = vertices[c][1] - open[j].y; if (dx * dx + dy * dy - open[j].r > EPSILON) { continue; } /* Remove the triangle and add it's edges to the edge list. */ edges.push( open[j].i, open[j].j, open[j].j, open[j].k, open[j].k, open[j].i ); open.splice(j, 1); } /* Remove any doubled edges. */ dedup(edges); /* Add a new triangle for each edge. */ for (j = edges.length; j; ) { b = edges[--j]; a = edges[--j]; open.push(circumcircle(vertices, a, b, c)); } } /* Copy any remaining open triangles to the closed list, and then * remove any triangles that share a vertex with the supertriangle, * building a list of triplets that represent triangles. */ for (i = open.length; i--; ) { closed.push(open[i]); } open.length = 0; for (i = closed.length; i--; ) { if (closed[i].i < n && closed[i].j < n && closed[i].k < n) { open.push(closed[i].i, closed[i].j, closed[i].k); } } /* Yay, we're done! */ return open; }, contains: function(tri, p) { /* Bounding box test first, for quick rejections. */ if ((p[0] < tri[0][0] && p[0] < tri[1][0] && p[0] < tri[2][0]) || (p[0] > tri[0][0] && p[0] > tri[1][0] && p[0] > tri[2][0]) || (p[1] < tri[0][1] && p[1] < tri[1][1] && p[1] < tri[2][1]) || (p[1] > tri[0][1] && p[1] > tri[1][1] && p[1] > tri[2][1])) { return null; } var a = tri[1][0] - tri[0][0]; var b = tri[2][0] - tri[0][0]; var c = tri[1][1] - tri[0][1]; var d = tri[2][1] - tri[0][1]; var i = a * d - b * c; /* Degenerate tri. */ if (i === 0.0) { return null; } var u = (d * (p[0] - tri[0][0]) - b * (p[1] - tri[0][1])) / i, v = (a * (p[1] - tri[0][1]) - c * (p[0] - tri[0][0])) / i; /* If we're outside the tri, fail. */ if (u < 0.0 || v < 0.0 || (u + v) > 1.0) { return null; } return [u, v]; } }; var GLMAT_EPSILON = 0.000001; // Use Array instead of Float32Array. It seems to be much faster and higher precision. var GLMAT_ARRAY_TYPE = Array; // if(!GLMAT_ARRAY_TYPE) { // GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array; // } var GLMAT_RANDOM$1 = Math.random; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 2 Dimensional Vector * @name vec2 */ var vec2 = {}; /** * Creates a new, empty vec2 * * @returns {vec2} a new 2D vector */ vec2.create = function() { var out = new GLMAT_ARRAY_TYPE(2); out[0] = 0; out[1] = 0; return out; }; /** * Creates a new vec2 initialized with values from an existing vector * * @param {vec2} a vector to clone * @returns {vec2} a new 2D vector */ vec2.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(2); out[0] = a[0]; out[1] = a[1]; return out; }; /** * Creates a new vec2 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @returns {vec2} a new 2D vector */ vec2.fromValues = function(x, y) { var out = new GLMAT_ARRAY_TYPE(2); out[0] = x; out[1] = y; return out; }; /** * Copy the values from one vec2 to another * * @param {vec2} out the receiving vector * @param {vec2} a the source vector * @returns {vec2} out */ vec2.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; return out; }; /** * Set the components of a vec2 to the given values * * @param {vec2} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @returns {vec2} out */ vec2.set = function(out, x, y) { out[0] = x; out[1] = y; return out; }; /** * Adds two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; return out; }; /** * Subtracts vector b from vector a * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; return out; }; /** * Alias for {@link vec2.subtract} * @function */ vec2.sub = vec2.subtract; /** * Multiplies two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }; /** * Alias for {@link vec2.multiply} * @function */ vec2.mul = vec2.multiply; /** * Divides two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; return out; }; /** * Alias for {@link vec2.divide} * @function */ vec2.div = vec2.divide; /** * Returns the minimum of two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); return out; }; /** * Returns the maximum of two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); return out; }; /** * Scales a vec2 by a scalar number * * @param {vec2} out the receiving vector * @param {vec2} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec2} out */ vec2.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; return out; }; /** * Adds two vec2's after scaling the second operand by a scalar value * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @param {Number} scale the amount to scale b by before adding * @returns {vec2} out */ vec2.scaleAndAdd = function(out, a, b, scale) { out[0] = a[0] + (b[0] * scale); out[1] = a[1] + (b[1] * scale); return out; }; /** * Calculates the euclidian distance between two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} distance between a and b */ vec2.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1]; return Math.sqrt(x*x + y*y); }; /** * Alias for {@link vec2.distance} * @function */ vec2.dist = vec2.distance; /** * Calculates the squared euclidian distance between two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} squared distance between a and b */ vec2.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1]; return x*x + y*y; }; /** * Alias for {@link vec2.squaredDistance} * @function */ vec2.sqrDist = vec2.squaredDistance; /** * Calculates the length of a vec2 * * @param {vec2} a vector to calculate length of * @returns {Number} length of a */ vec2.length = function (a) { var x = a[0], y = a[1]; return Math.sqrt(x*x + y*y); }; /** * Alias for {@link vec2.length} * @function */ vec2.len = vec2.length; /** * Calculates the squared length of a vec2 * * @param {vec2} a vector to calculate squared length of * @returns {Number} squared length of a */ vec2.squaredLength = function (a) { var x = a[0], y = a[1]; return x*x + y*y; }; /** * Alias for {@link vec2.squaredLength} * @function */ vec2.sqrLen = vec2.squaredLength; /** * Negates the components of a vec2 * * @param {vec2} out the receiving vector * @param {vec2} a vector to negate * @returns {vec2} out */ vec2.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; return out; }; /** * Returns the inverse of the components of a vec2 * * @param {vec2} out the receiving vector * @param {vec2} a vector to invert * @returns {vec2} out */ vec2.inverse = function(out, a) { out[0] = 1.0 / a[0]; out[1] = 1.0 / a[1]; return out; }; /** * Normalize a vec2 * * @param {vec2} out the receiving vector * @param {vec2} a vector to normalize * @returns {vec2} out */ vec2.normalize = function(out, a) { var x = a[0], y = a[1]; var len = x*x + y*y; if (len > 0) { //TODO: evaluate use of glm_invsqrt here? len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; } return out; }; /** * Calculates the dot product of two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} dot product of a and b */ vec2.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1]; }; /** * Computes the cross product of two vec2's * Note that the cross product must by definition produce a 3D vector * * @param {vec3} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec3} out */ vec2.cross = function(out, a, b) { var z = a[0] * b[1] - a[1] * b[0]; out[0] = out[1] = 0; out[2] = z; return out; }; /** * Performs a linear interpolation between two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec2} out */ vec2.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); return out; }; /** * Generates a random vector with the given scale * * @param {vec2} out the receiving vector * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned * @returns {vec2} out */ vec2.random = function (out, scale) { scale = scale || 1.0; var r = GLMAT_RANDOM() * 2.0 * Math.PI; out[0] = Math.cos(r) * scale; out[1] = Math.sin(r) * scale; return out; }; /** * Transforms the vec2 with a mat2 * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat2} m matrix to transform with * @returns {vec2} out */ vec2.transformMat2 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[2] * y; out[1] = m[1] * x + m[3] * y; return out; }; /** * Transforms the vec2 with a mat2d * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat2d} m matrix to transform with * @returns {vec2} out */ vec2.transformMat2d = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[2] * y + m[4]; out[1] = m[1] * x + m[3] * y + m[5]; return out; }; /** * Transforms the vec2 with a mat3 * 3rd vector component is implicitly '1' * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat3} m matrix to transform with * @returns {vec2} out */ vec2.transformMat3 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[3] * y + m[6]; out[1] = m[1] * x + m[4] * y + m[7]; return out; }; /** * Transforms the vec2 with a mat4 * 3rd vector component is implicitly '0' * 4th vector component is implicitly '1' * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec2} out */ vec2.transformMat4 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[4] * y + m[12]; out[1] = m[1] * x + m[5] * y + m[13]; return out; }; /** * Perform some operation over an array of vec2s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec2.forEach = (function() { var vec = vec2.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 2; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; } return a; }; })(); /** * @constructor * @alias clay.Vector2 * @param {number} x * @param {number} y */ var Vector2 = function(x, y) { x = x || 0; y = y || 0; /** * Storage of Vector2, read and write of x, y will change the values in array * All methods also operate on the array instead of x, y components * @name array * @type {Float32Array} * @memberOf clay.Vector2# */ this.array = vec2.fromValues(x, y); /** * Dirty flag is used by the Node to determine * if the matrix is updated to latest * @name _dirty * @type {boolean} * @memberOf clay.Vector2# */ this._dirty = true; }; Vector2.prototype = { constructor: Vector2, /** * Add b to self * @param {clay.Vector2} b * @return {clay.Vector2} */ add: function(b) { vec2.add(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Set x and y components * @param {number} x * @param {number} y * @return {clay.Vector2} */ set: function(x, y) { this.array[0] = x; this.array[1] = y; this._dirty = true; return this; }, /** * Set x and y components from array * @param {Float32Array|number[]} arr * @return {clay.Vector2} */ setArray: function(arr) { this.array[0] = arr[0]; this.array[1] = arr[1]; this._dirty = true; return this; }, /** * Clone a new Vector2 * @return {clay.Vector2} */ clone: function() { return new Vector2(this.x, this.y); }, /** * Copy x, y from b * @param {clay.Vector2} b * @return {clay.Vector2} */ copy: function(b) { vec2.copy(this.array, b.array); this._dirty = true; return this; }, /** * Cross product of self and b, written to a Vector3 out * @param {clay.Vector3} out * @param {clay.Vector2} b * @return {clay.Vector2} */ cross: function(out, b) { vec2.cross(out.array, this.array, b.array); out._dirty = true; return this; }, /** * Alias for distance * @param {clay.Vector2} b * @return {number} */ dist: function(b) { return vec2.dist(this.array, b.array); }, /** * Distance between self and b * @param {clay.Vector2} b * @return {number} */ distance: function(b) { return vec2.distance(this.array, b.array); }, /** * Alias for divide * @param {clay.Vector2} b * @return {clay.Vector2} */ div: function(b) { vec2.div(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Divide self by b * @param {clay.Vector2} b * @return {clay.Vector2} */ divide: function(b) { vec2.divide(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Dot product of self and b * @param {clay.Vector2} b * @return {number} */ dot: function(b) { return vec2.dot(this.array, b.array); }, /** * Alias of length * @return {number} */ len: function() { return vec2.len(this.array); }, /** * Calculate the length * @return {number} */ length: function() { return vec2.length(this.array); }, /** * Linear interpolation between a and b * @param {clay.Vector2} a * @param {clay.Vector2} b * @param {number} t * @return {clay.Vector2} */ lerp: function(a, b, t) { vec2.lerp(this.array, a.array, b.array, t); this._dirty = true; return this; }, /** * Minimum of self and b * @param {clay.Vector2} b * @return {clay.Vector2} */ min: function(b) { vec2.min(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Maximum of self and b * @param {clay.Vector2} b * @return {clay.Vector2} */ max: function(b) { vec2.max(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiply * @param {clay.Vector2} b * @return {clay.Vector2} */ mul: function(b) { vec2.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Mutiply self and b * @param {clay.Vector2} b * @return {clay.Vector2} */ multiply: function(b) { vec2.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Negate self * @return {clay.Vector2} */ negate: function() { vec2.negate(this.array, this.array); this._dirty = true; return this; }, /** * Normalize self * @return {clay.Vector2} */ normalize: function() { vec2.normalize(this.array, this.array); this._dirty = true; return this; }, /** * Generate random x, y components with a given scale * @param {number} scale * @return {clay.Vector2} */ random: function(scale) { vec2.random(this.array, scale); this._dirty = true; return this; }, /** * Scale self * @param {number} scale * @return {clay.Vector2} */ scale: function(s) { vec2.scale(this.array, this.array, s); this._dirty = true; return this; }, /** * Scale b and add to self * @param {clay.Vector2} b * @param {number} scale * @return {clay.Vector2} */ scaleAndAdd: function(b, s) { vec2.scaleAndAdd(this.array, this.array, b.array, s); this._dirty = true; return this; }, /** * Alias for squaredDistance * @param {clay.Vector2} b * @return {number} */ sqrDist: function(b) { return vec2.sqrDist(this.array, b.array); }, /** * Squared distance between self and b * @param {clay.Vector2} b * @return {number} */ squaredDistance: function(b) { return vec2.squaredDistance(this.array, b.array); }, /** * Alias for squaredLength * @return {number} */ sqrLen: function() { return vec2.sqrLen(this.array); }, /** * Squared length of self * @return {number} */ squaredLength: function() { return vec2.squaredLength(this.array); }, /** * Alias for subtract * @param {clay.Vector2} b * @return {clay.Vector2} */ sub: function(b) { vec2.sub(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Subtract b from self * @param {clay.Vector2} b * @return {clay.Vector2} */ subtract: function(b) { vec2.subtract(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Transform self with a Matrix2 m * @param {clay.Matrix2} m * @return {clay.Vector2} */ transformMat2: function(m) { vec2.transformMat2(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Matrix2d m * @param {clay.Matrix2d} m * @return {clay.Vector2} */ transformMat2d: function(m) { vec2.transformMat2d(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Matrix3 m * @param {clay.Matrix3} m * @return {clay.Vector2} */ transformMat3: function(m) { vec2.transformMat3(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Matrix4 m * @param {clay.Matrix4} m * @return {clay.Vector2} */ transformMat4: function(m) { vec2.transformMat4(this.array, this.array, m.array); this._dirty = true; return this; }, toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; // Getter and Setter if (Object.defineProperty) { var proto = Vector2.prototype; /** * @name x * @type {number} * @memberOf clay.Vector2 * @instance */ Object.defineProperty(proto, 'x', { get: function () { return this.array[0]; }, set: function (value) { this.array[0] = value; this._dirty = true; } }); /** * @name y * @type {number} * @memberOf clay.Vector2 * @instance */ Object.defineProperty(proto, 'y', { get: function () { return this.array[1]; }, set: function (value) { this.array[1] = value; this._dirty = true; } }); } // Supply methods that are not in place /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.add = function(out, a, b) { vec2.add(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {number} x * @param {number} y * @return {clay.Vector2} */ Vector2.set = function(out, x, y) { vec2.set(out.array, x, y); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.copy = function(out, b) { vec2.copy(out.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.cross = function(out, a, b) { vec2.cross(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {number} */ Vector2.dist = function(a, b) { return vec2.distance(a.array, b.array); }; /** * @function * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {number} */ Vector2.distance = Vector2.dist; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.div = function(out, a, b) { vec2.divide(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.divide = Vector2.div; /** * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {number} */ Vector2.dot = function(a, b) { return vec2.dot(a.array, b.array); }; /** * @param {clay.Vector2} a * @return {number} */ Vector2.len = function(b) { return vec2.length(b.array); }; // Vector2.length = Vector2.len; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @param {number} t * @return {clay.Vector2} */ Vector2.lerp = function(out, a, b, t) { vec2.lerp(out.array, a.array, b.array, t); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.min = function(out, a, b) { vec2.min(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.max = function(out, a, b) { vec2.max(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.mul = function(out, a, b) { vec2.multiply(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.multiply = Vector2.mul; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @return {clay.Vector2} */ Vector2.negate = function(out, a) { vec2.negate(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @return {clay.Vector2} */ Vector2.normalize = function(out, a) { vec2.normalize(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {number} scale * @return {clay.Vector2} */ Vector2.random = function(out, scale) { vec2.random(out.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {number} scale * @return {clay.Vector2} */ Vector2.scale = function(out, a, scale) { vec2.scale(out.array, a.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @param {number} scale * @return {clay.Vector2} */ Vector2.scaleAndAdd = function(out, a, b, scale) { vec2.scaleAndAdd(out.array, a.array, b.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {number} */ Vector2.sqrDist = function(a, b) { return vec2.sqrDist(a.array, b.array); }; /** * @function * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {number} */ Vector2.squaredDistance = Vector2.sqrDist; /** * @param {clay.Vector2} a * @return {number} */ Vector2.sqrLen = function(a) { return vec2.sqrLen(a.array); }; /** * @function * @param {clay.Vector2} a * @return {number} */ Vector2.squaredLength = Vector2.sqrLen; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.sub = function(out, a, b) { vec2.subtract(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Vector2} b * @return {clay.Vector2} */ Vector2.subtract = Vector2.sub; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Matrix2} m * @return {clay.Vector2} */ Vector2.transformMat2 = function(out, a, m) { vec2.transformMat2(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Matrix2d} m * @return {clay.Vector2} */ Vector2.transformMat2d = function(out, a, m) { vec2.transformMat2d(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {Matrix3} m * @return {clay.Vector2} */ Vector2.transformMat3 = function(out, a, m) { vec2.transformMat3(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector2} out * @param {clay.Vector2} a * @param {clay.Matrix4} m * @return {clay.Vector2} */ Vector2.transformMat4 = function(out, a, m) { vec2.transformMat4(out.array, a.array, m.array); out._dirty = true; return out; }; // 2D Blend clip of blend tree // http://docs.unity3d.com/Documentation/Manual/2DBlending.html /** * @typedef {Object} clay.animation.Blend2DClip.IClipInput * @property {clay.Vector2} position * @property {clay.animation.Clip} clip * @property {number} offset */ /** * 2d blending node in animation blend tree. * output clip must have blend2D method * @constructor * @alias clay.animation.Blend2DClip * @extends clay.animation.Clip * * @param {Object} [opts] * @param {string} [opts.name] * @param {Object} [opts.target] * @param {number} [opts.life] * @param {number} [opts.delay] * @param {number} [opts.gap] * @param {number} [opts.playbackRatio] * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation * @param {string|Function} [opts.easing] * @param {Function} [opts.onframe] * @param {Function} [opts.onfinish] * @param {Function} [opts.onrestart] * @param {object[]} [opts.inputs] * @param {clay.Vector2} [opts.position] * @param {clay.animation.Clip} [opts.output] */ var Blend2DClip = function (opts) { opts = opts || {}; Clip.call(this, opts); /** * Output clip must have blend2D method * @type {clay.animation.Clip} */ this.output = opts.output || null; /** * @type {clay.animation.Blend2DClip.IClipInput[]} */ this.inputs = opts.inputs || []; /** * @type {clay.Vector2} */ this.position = new Vector2(); this._cacheTriangle = null; this._triangles = []; this._updateTriangles(); }; Blend2DClip.prototype = new Clip(); Blend2DClip.prototype.constructor = Blend2DClip; /** * @param {clay.Vector2} position * @param {clay.animation.Clip} inputClip * @param {number} [offset] * @return {clay.animation.Blend2DClip.IClipInput} */ Blend2DClip.prototype.addInput = function (position, inputClip, offset) { var obj = { position : position, clip : inputClip, offset : offset || 0 }; this.inputs.push(obj); this.life = Math.max(inputClip.life, this.life); // TODO Change to incrementally adding this._updateTriangles(); return obj; }; // Delaunay triangulate Blend2DClip.prototype._updateTriangles = function () { var inputs = this.inputs.map(function (a) { return a.position; }); this._triangles = delaunay.triangulate(inputs, 'array'); }; Blend2DClip.prototype.step = function (time, dTime, silent) { var ret = Clip.prototype.step.call(this, time); if (ret !== 'finish') { this.setTime(this.getElapsedTime()); } // PENDING Schedule if (!silent && ret !== 'paused') { this.fire('frame'); } return ret; }; Blend2DClip.prototype.setTime = function (time) { var res = this._findTriangle(this.position); if (!res) { return; } // In Barycentric var a = res[1]; // Percent of clip2 var b = res[2]; // Percent of clip3 var tri = res[0]; var in1 = this.inputs[tri.indices[0]]; var in2 = this.inputs[tri.indices[1]]; var in3 = this.inputs[tri.indices[2]]; var clip1 = in1.clip; var clip2 = in2.clip; var clip3 = in3.clip; clip1.setTime((time + in1.offset) % clip1.life); clip2.setTime((time + in2.offset) % clip2.life); clip3.setTime((time + in3.offset) % clip3.life); var c1 = clip1.output instanceof Clip ? clip1.output : clip1; var c2 = clip2.output instanceof Clip ? clip2.output : clip2; var c3 = clip3.output instanceof Clip ? clip3.output : clip3; this.output.blend2D(c1, c2, c3, a, b); }; /** * Clone a new Blend2D clip * @param {boolean} cloneInputs True if clone the input clips * @return {clay.animation.Blend2DClip} */ Blend2DClip.prototype.clone = function (cloneInputs) { var clip = Clip.prototype.clone.call(this); clip.output = this.output.clone(); for (var i = 0; i < this.inputs.length; i++) { var inputClip = cloneInputs ? this.inputs[i].clip.clone(true) : this.inputs[i].clip; clip.addInput(this.inputs[i].position, inputClip, this.inputs[i].offset); } return clip; }; Blend2DClip.prototype._findTriangle = function (position) { if (this._cacheTriangle) { var res = delaunay.contains(this._cacheTriangle.vertices, position.array); if (res) { return [this._cacheTriangle, res[0], res[1]]; } } for (var i = 0; i < this._triangles.length; i++) { var tri = this._triangles[i]; var res = delaunay.contains(tri.vertices, this.position.array); if (res) { this._cacheTriangle = tri; return [tri, res[0], res[1]]; } } }; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 3 Dimensional Vector * @name vec3 */ var vec3 = {}; /** * Creates a new, empty vec3 * * @returns {vec3} a new 3D vector */ vec3.create = function() { var out = new GLMAT_ARRAY_TYPE(3); out[0] = 0; out[1] = 0; out[2] = 0; return out; }; /** * Creates a new vec3 initialized with values from an existing vector * * @param {vec3} a vector to clone * @returns {vec3} a new 3D vector */ vec3.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(3); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; }; /** * Creates a new vec3 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @returns {vec3} a new 3D vector */ vec3.fromValues = function(x, y, z) { var out = new GLMAT_ARRAY_TYPE(3); out[0] = x; out[1] = y; out[2] = z; return out; }; /** * Copy the values from one vec3 to another * * @param {vec3} out the receiving vector * @param {vec3} a the source vector * @returns {vec3} out */ vec3.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; }; /** * Set the components of a vec3 to the given values * * @param {vec3} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @returns {vec3} out */ vec3.set = function(out, x, y, z) { out[0] = x; out[1] = y; out[2] = z; return out; }; /** * Adds two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; out[2] = a[2] + b[2]; return out; }; /** * Subtracts vector b from vector a * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; return out; }; /** * Alias for {@link vec3.subtract} * @function */ vec3.sub = vec3.subtract; /** * Multiplies two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; return out; }; /** * Alias for {@link vec3.multiply} * @function */ vec3.mul = vec3.multiply; /** * Divides two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; out[2] = a[2] / b[2]; return out; }; /** * Alias for {@link vec3.divide} * @function */ vec3.div = vec3.divide; /** * Returns the minimum of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); out[2] = Math.min(a[2], b[2]); return out; }; /** * Returns the maximum of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); out[2] = Math.max(a[2], b[2]); return out; }; /** * Scales a vec3 by a scalar number * * @param {vec3} out the receiving vector * @param {vec3} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec3} out */ vec3.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; return out; }; /** * Adds two vec3's after scaling the second operand by a scalar value * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @param {Number} scale the amount to scale b by before adding * @returns {vec3} out */ vec3.scaleAndAdd = function(out, a, b, scale) { out[0] = a[0] + (b[0] * scale); out[1] = a[1] + (b[1] * scale); out[2] = a[2] + (b[2] * scale); return out; }; /** * Calculates the euclidian distance between two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} distance between a and b */ vec3.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2]; return Math.sqrt(x*x + y*y + z*z); }; /** * Alias for {@link vec3.distance} * @function */ vec3.dist = vec3.distance; /** * Calculates the squared euclidian distance between two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} squared distance between a and b */ vec3.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2]; return x*x + y*y + z*z; }; /** * Alias for {@link vec3.squaredDistance} * @function */ vec3.sqrDist = vec3.squaredDistance; /** * Calculates the length of a vec3 * * @param {vec3} a vector to calculate length of * @returns {Number} length of a */ vec3.length = function (a) { var x = a[0], y = a[1], z = a[2]; return Math.sqrt(x*x + y*y + z*z); }; /** * Alias for {@link vec3.length} * @function */ vec3.len = vec3.length; /** * Calculates the squared length of a vec3 * * @param {vec3} a vector to calculate squared length of * @returns {Number} squared length of a */ vec3.squaredLength = function (a) { var x = a[0], y = a[1], z = a[2]; return x*x + y*y + z*z; }; /** * Alias for {@link vec3.squaredLength} * @function */ vec3.sqrLen = vec3.squaredLength; /** * Negates the components of a vec3 * * @param {vec3} out the receiving vector * @param {vec3} a vector to negate * @returns {vec3} out */ vec3.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; out[2] = -a[2]; return out; }; /** * Returns the inverse of the components of a vec3 * * @param {vec3} out the receiving vector * @param {vec3} a vector to invert * @returns {vec3} out */ vec3.inverse = function(out, a) { out[0] = 1.0 / a[0]; out[1] = 1.0 / a[1]; out[2] = 1.0 / a[2]; return out; }; /** * Normalize a vec3 * * @param {vec3} out the receiving vector * @param {vec3} a vector to normalize * @returns {vec3} out */ vec3.normalize = function(out, a) { var x = a[0], y = a[1], z = a[2]; var len = x*x + y*y + z*z; if (len > 0) { //TODO: evaluate use of glm_invsqrt here? len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; out[2] = a[2] * len; } return out; }; /** * Calculates the dot product of two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} dot product of a and b */ vec3.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }; /** * Computes the cross product of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.cross = function(out, a, b) { var ax = a[0], ay = a[1], az = a[2], bx = b[0], by = b[1], bz = b[2]; out[0] = ay * bz - az * by; out[1] = az * bx - ax * bz; out[2] = ax * by - ay * bx; return out; }; /** * Performs a linear interpolation between two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec3} out */ vec3.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1], az = a[2]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); out[2] = az + t * (b[2] - az); return out; }; /** * Generates a random vector with the given scale * * @param {vec3} out the receiving vector * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned * @returns {vec3} out */ vec3.random = function (out, scale) { scale = scale || 1.0; var r = GLMAT_RANDOM$1() * 2.0 * Math.PI; var z = (GLMAT_RANDOM$1() * 2.0) - 1.0; var zScale = Math.sqrt(1.0-z*z) * scale; out[0] = Math.cos(r) * zScale; out[1] = Math.sin(r) * zScale; out[2] = z * scale; return out; }; /** * Transforms the vec3 with a mat4. * 4th vector component is implicitly '1' * * @param {vec3} out the receiving vector * @param {vec3} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec3} out */ vec3.transformMat4 = function(out, a, m) { var x = a[0], y = a[1], z = a[2], w = m[3] * x + m[7] * y + m[11] * z + m[15]; w = w || 1.0; out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w; out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w; out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w; return out; }; /** * Transforms the vec3 with a mat3. * * @param {vec3} out the receiving vector * @param {vec3} a the vector to transform * @param {mat4} m the 3x3 matrix to transform with * @returns {vec3} out */ vec3.transformMat3 = function(out, a, m) { var x = a[0], y = a[1], z = a[2]; out[0] = x * m[0] + y * m[3] + z * m[6]; out[1] = x * m[1] + y * m[4] + z * m[7]; out[2] = x * m[2] + y * m[5] + z * m[8]; return out; }; /** * Transforms the vec3 with a quat * * @param {vec3} out the receiving vector * @param {vec3} a the vector to transform * @param {quat} q quaternion to transform with * @returns {vec3} out */ vec3.transformQuat = function(out, a, q) { // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations var x = a[0], y = a[1], z = a[2], qx = q[0], qy = q[1], qz = q[2], qw = q[3], // calculate quat * vec ix = qw * x + qy * z - qz * y, iy = qw * y + qz * x - qx * z, iz = qw * z + qx * y - qy * x, iw = -qx * x - qy * y - qz * z; // calculate result * inverse quat out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; return out; }; /** * Rotate a 3D vector around the x-axis * @param {vec3} out The receiving vec3 * @param {vec3} a The vec3 point to rotate * @param {vec3} b The origin of the rotation * @param {Number} c The angle of rotation * @returns {vec3} out */ vec3.rotateX = function(out, a, b, c){ var p = [], r=[]; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[0]; r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c); r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c); //translate to correct position out[0] = r[0] + b[0]; out[1] = r[1] + b[1]; out[2] = r[2] + b[2]; return out; }; /** * Rotate a 3D vector around the y-axis * @param {vec3} out The receiving vec3 * @param {vec3} a The vec3 point to rotate * @param {vec3} b The origin of the rotation * @param {Number} c The angle of rotation * @returns {vec3} out */ vec3.rotateY = function(out, a, b, c){ var p = [], r=[]; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c); r[1] = p[1]; r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c); //translate to correct position out[0] = r[0] + b[0]; out[1] = r[1] + b[1]; out[2] = r[2] + b[2]; return out; }; /** * Rotate a 3D vector around the z-axis * @param {vec3} out The receiving vec3 * @param {vec3} a The vec3 point to rotate * @param {vec3} b The origin of the rotation * @param {Number} c The angle of rotation * @returns {vec3} out */ vec3.rotateZ = function(out, a, b, c){ var p = [], r=[]; //Translate point to the origin p[0] = a[0] - b[0]; p[1] = a[1] - b[1]; p[2] = a[2] - b[2]; //perform rotation r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c); r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c); r[2] = p[2]; //translate to correct position out[0] = r[0] + b[0]; out[1] = r[1] + b[1]; out[2] = r[2] + b[2]; return out; }; /** * Perform some operation over an array of vec3s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec3.forEach = (function() { var vec = vec3.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 3; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; } return a; }; })(); /** * Get the angle between two 3D vectors * @param {vec3} a The first operand * @param {vec3} b The second operand * @returns {Number} The angle in radians */ vec3.angle = function(a, b) { var tempA = vec3.fromValues(a[0], a[1], a[2]); var tempB = vec3.fromValues(b[0], b[1], b[2]); vec3.normalize(tempA, tempA); vec3.normalize(tempB, tempB); var cosine = vec3.dot(tempA, tempB); if(cosine > 1.0){ return 0; } else { return Math.acos(cosine); } }; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 4 Dimensional Vector * @name vec4 */ var vec4 = {}; /** * Creates a new, empty vec4 * * @returns {vec4} a new 4D vector */ vec4.create = function() { var out = new GLMAT_ARRAY_TYPE(4); out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0; return out; }; /** * Creates a new vec4 initialized with values from an existing vector * * @param {vec4} a vector to clone * @returns {vec4} a new 4D vector */ vec4.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(4); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Creates a new vec4 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {vec4} a new 4D vector */ vec4.fromValues = function(x, y, z, w) { var out = new GLMAT_ARRAY_TYPE(4); out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out; }; /** * Copy the values from one vec4 to another * * @param {vec4} out the receiving vector * @param {vec4} a the source vector * @returns {vec4} out */ vec4.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Set the components of a vec4 to the given values * * @param {vec4} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {vec4} out */ vec4.set = function(out, x, y, z, w) { out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out; }; /** * Adds two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; out[2] = a[2] + b[2]; out[3] = a[3] + b[3]; return out; }; /** * Subtracts vector b from vector a * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; out[3] = a[3] - b[3]; return out; }; /** * Alias for {@link vec4.subtract} * @function */ vec4.sub = vec4.subtract; /** * Multiplies two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; out[3] = a[3] * b[3]; return out; }; /** * Alias for {@link vec4.multiply} * @function */ vec4.mul = vec4.multiply; /** * Divides two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; out[2] = a[2] / b[2]; out[3] = a[3] / b[3]; return out; }; /** * Alias for {@link vec4.divide} * @function */ vec4.div = vec4.divide; /** * Returns the minimum of two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); out[2] = Math.min(a[2], b[2]); out[3] = Math.min(a[3], b[3]); return out; }; /** * Returns the maximum of two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); out[2] = Math.max(a[2], b[2]); out[3] = Math.max(a[3], b[3]); return out; }; /** * Scales a vec4 by a scalar number * * @param {vec4} out the receiving vector * @param {vec4} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec4} out */ vec4.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; out[3] = a[3] * b; return out; }; /** * Adds two vec4's after scaling the second operand by a scalar value * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @param {Number} scale the amount to scale b by before adding * @returns {vec4} out */ vec4.scaleAndAdd = function(out, a, b, scale) { out[0] = a[0] + (b[0] * scale); out[1] = a[1] + (b[1] * scale); out[2] = a[2] + (b[2] * scale); out[3] = a[3] + (b[3] * scale); return out; }; /** * Calculates the euclidian distance between two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} distance between a and b */ vec4.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2], w = b[3] - a[3]; return Math.sqrt(x*x + y*y + z*z + w*w); }; /** * Alias for {@link vec4.distance} * @function */ vec4.dist = vec4.distance; /** * Calculates the squared euclidian distance between two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} squared distance between a and b */ vec4.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2], w = b[3] - a[3]; return x*x + y*y + z*z + w*w; }; /** * Alias for {@link vec4.squaredDistance} * @function */ vec4.sqrDist = vec4.squaredDistance; /** * Calculates the length of a vec4 * * @param {vec4} a vector to calculate length of * @returns {Number} length of a */ vec4.length = function (a) { var x = a[0], y = a[1], z = a[2], w = a[3]; return Math.sqrt(x*x + y*y + z*z + w*w); }; /** * Alias for {@link vec4.length} * @function */ vec4.len = vec4.length; /** * Calculates the squared length of a vec4 * * @param {vec4} a vector to calculate squared length of * @returns {Number} squared length of a */ vec4.squaredLength = function (a) { var x = a[0], y = a[1], z = a[2], w = a[3]; return x*x + y*y + z*z + w*w; }; /** * Alias for {@link vec4.squaredLength} * @function */ vec4.sqrLen = vec4.squaredLength; /** * Negates the components of a vec4 * * @param {vec4} out the receiving vector * @param {vec4} a vector to negate * @returns {vec4} out */ vec4.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; out[2] = -a[2]; out[3] = -a[3]; return out; }; /** * Returns the inverse of the components of a vec4 * * @param {vec4} out the receiving vector * @param {vec4} a vector to invert * @returns {vec4} out */ vec4.inverse = function(out, a) { out[0] = 1.0 / a[0]; out[1] = 1.0 / a[1]; out[2] = 1.0 / a[2]; out[3] = 1.0 / a[3]; return out; }; /** * Normalize a vec4 * * @param {vec4} out the receiving vector * @param {vec4} a vector to normalize * @returns {vec4} out */ vec4.normalize = function(out, a) { var x = a[0], y = a[1], z = a[2], w = a[3]; var len = x*x + y*y + z*z + w*w; if (len > 0) { len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; out[2] = a[2] * len; out[3] = a[3] * len; } return out; }; /** * Calculates the dot product of two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} dot product of a and b */ vec4.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; }; /** * Performs a linear interpolation between two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec4} out */ vec4.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1], az = a[2], aw = a[3]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); out[2] = az + t * (b[2] - az); out[3] = aw + t * (b[3] - aw); return out; }; /** * Generates a random vector with the given scale * * @param {vec4} out the receiving vector * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned * @returns {vec4} out */ vec4.random = function (out, scale) { scale = scale || 1.0; //TODO: This is a pretty awful way of doing this. Find something better. out[0] = GLMAT_RANDOM$1(); out[1] = GLMAT_RANDOM$1(); out[2] = GLMAT_RANDOM$1(); out[3] = GLMAT_RANDOM$1(); vec4.normalize(out, out); vec4.scale(out, out, scale); return out; }; /** * Transforms the vec4 with a mat4. * * @param {vec4} out the receiving vector * @param {vec4} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec4} out */ vec4.transformMat4 = function(out, a, m) { var x = a[0], y = a[1], z = a[2], w = a[3]; out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; return out; }; /** * Transforms the vec4 with a quat * * @param {vec4} out the receiving vector * @param {vec4} a the vector to transform * @param {quat} q quaternion to transform with * @returns {vec4} out */ vec4.transformQuat = function(out, a, q) { var x = a[0], y = a[1], z = a[2], qx = q[0], qy = q[1], qz = q[2], qw = q[3], // calculate quat * vec ix = qw * x + qy * z - qz * y, iy = qw * y + qz * x - qx * z, iz = qw * z + qx * y - qy * x, iw = -qx * x - qy * y - qz * z; // calculate result * inverse quat out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; return out; }; /** * Perform some operation over an array of vec4s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec4s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec4.forEach = (function() { var vec = vec4.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 4; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3]; } return a; }; })(); /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 3x3 Matrix * @name mat3 */ var mat3 = {}; /** * Creates a new identity mat3 * * @returns {mat3} a new 3x3 matrix */ mat3.create = function() { var out = new GLMAT_ARRAY_TYPE(9); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 1; out[5] = 0; out[6] = 0; out[7] = 0; out[8] = 1; return out; }; /** * Copies the upper-left 3x3 values into the given mat3. * * @param {mat3} out the receiving 3x3 matrix * @param {mat4} a the source 4x4 matrix * @returns {mat3} out */ mat3.fromMat4 = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[4]; out[4] = a[5]; out[5] = a[6]; out[6] = a[8]; out[7] = a[9]; out[8] = a[10]; return out; }; /** * Creates a new mat3 initialized with values from an existing matrix * * @param {mat3} a matrix to clone * @returns {mat3} a new 3x3 matrix */ mat3.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(9); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }; /** * Copy the values from one mat3 to another * * @param {mat3} out the receiving matrix * @param {mat3} a the source matrix * @returns {mat3} out */ mat3.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }; /** * Set a mat3 to the identity matrix * * @param {mat3} out the receiving matrix * @returns {mat3} out */ mat3.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 1; out[5] = 0; out[6] = 0; out[7] = 0; out[8] = 1; return out; }; /** * Transpose the values of a mat3 * * @param {mat3} out the receiving matrix * @param {mat3} a the source matrix * @returns {mat3} out */ mat3.transpose = function(out, a) { // If we are transposing ourselves we can skip a few steps but have to cache some values if (out === a) { var a01 = a[1], a02 = a[2], a12 = a[5]; out[1] = a[3]; out[2] = a[6]; out[3] = a01; out[5] = a[7]; out[6] = a02; out[7] = a12; } else { out[0] = a[0]; out[1] = a[3]; out[2] = a[6]; out[3] = a[1]; out[4] = a[4]; out[5] = a[7]; out[6] = a[2]; out[7] = a[5]; out[8] = a[8]; } return out; }; /** * Inverts a mat3 * * @param {mat3} out the receiving matrix * @param {mat3} a the source matrix * @returns {mat3} out */ mat3.invert = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8], b01 = a22 * a11 - a12 * a21, b11 = -a22 * a10 + a12 * a20, b21 = a21 * a10 - a11 * a20, // Calculate the determinant det = a00 * b01 + a01 * b11 + a02 * b21; if (!det) { return null; } det = 1.0 / det; out[0] = b01 * det; out[1] = (-a22 * a01 + a02 * a21) * det; out[2] = (a12 * a01 - a02 * a11) * det; out[3] = b11 * det; out[4] = (a22 * a00 - a02 * a20) * det; out[5] = (-a12 * a00 + a02 * a10) * det; out[6] = b21 * det; out[7] = (-a21 * a00 + a01 * a20) * det; out[8] = (a11 * a00 - a01 * a10) * det; return out; }; /** * Calculates the adjugate of a mat3 * * @param {mat3} out the receiving matrix * @param {mat3} a the source matrix * @returns {mat3} out */ mat3.adjoint = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8]; out[0] = (a11 * a22 - a12 * a21); out[1] = (a02 * a21 - a01 * a22); out[2] = (a01 * a12 - a02 * a11); out[3] = (a12 * a20 - a10 * a22); out[4] = (a00 * a22 - a02 * a20); out[5] = (a02 * a10 - a00 * a12); out[6] = (a10 * a21 - a11 * a20); out[7] = (a01 * a20 - a00 * a21); out[8] = (a00 * a11 - a01 * a10); return out; }; /** * Calculates the determinant of a mat3 * * @param {mat3} a the source matrix * @returns {Number} determinant of a */ mat3.determinant = function (a) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8]; return a00 * (a22 * a11 - a12 * a21) + a01 * (-a22 * a10 + a12 * a20) + a02 * (a21 * a10 - a11 * a20); }; /** * Multiplies two mat3's * * @param {mat3} out the receiving matrix * @param {mat3} a the first operand * @param {mat3} b the second operand * @returns {mat3} out */ mat3.multiply = function (out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8], b00 = b[0], b01 = b[1], b02 = b[2], b10 = b[3], b11 = b[4], b12 = b[5], b20 = b[6], b21 = b[7], b22 = b[8]; out[0] = b00 * a00 + b01 * a10 + b02 * a20; out[1] = b00 * a01 + b01 * a11 + b02 * a21; out[2] = b00 * a02 + b01 * a12 + b02 * a22; out[3] = b10 * a00 + b11 * a10 + b12 * a20; out[4] = b10 * a01 + b11 * a11 + b12 * a21; out[5] = b10 * a02 + b11 * a12 + b12 * a22; out[6] = b20 * a00 + b21 * a10 + b22 * a20; out[7] = b20 * a01 + b21 * a11 + b22 * a21; out[8] = b20 * a02 + b21 * a12 + b22 * a22; return out; }; /** * Alias for {@link mat3.multiply} * @function */ mat3.mul = mat3.multiply; /** * Translate a mat3 by the given vector * * @param {mat3} out the receiving matrix * @param {mat3} a the matrix to translate * @param {vec2} v vector to translate by * @returns {mat3} out */ mat3.translate = function(out, a, v) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8], x = v[0], y = v[1]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a10; out[4] = a11; out[5] = a12; out[6] = x * a00 + y * a10 + a20; out[7] = x * a01 + y * a11 + a21; out[8] = x * a02 + y * a12 + a22; return out; }; /** * Rotates a mat3 by the given angle * * @param {mat3} out the receiving matrix * @param {mat3} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat3} out */ mat3.rotate = function (out, a, rad) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[3], a11 = a[4], a12 = a[5], a20 = a[6], a21 = a[7], a22 = a[8], s = Math.sin(rad), c = Math.cos(rad); out[0] = c * a00 + s * a10; out[1] = c * a01 + s * a11; out[2] = c * a02 + s * a12; out[3] = c * a10 - s * a00; out[4] = c * a11 - s * a01; out[5] = c * a12 - s * a02; out[6] = a20; out[7] = a21; out[8] = a22; return out; }; /** * Scales the mat3 by the dimensions in the given vec2 * * @param {mat3} out the receiving matrix * @param {mat3} a the matrix to rotate * @param {vec2} v the vec2 to scale the matrix by * @returns {mat3} out **/ mat3.scale = function(out, a, v) { var x = v[0], y = v[1]; out[0] = x * a[0]; out[1] = x * a[1]; out[2] = x * a[2]; out[3] = y * a[3]; out[4] = y * a[4]; out[5] = y * a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; return out; }; /** * Copies the values from a mat2d into a mat3 * * @param {mat3} out the receiving matrix * @param {mat2d} a the matrix to copy * @returns {mat3} out **/ mat3.fromMat2d = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = 0; out[3] = a[2]; out[4] = a[3]; out[5] = 0; out[6] = a[4]; out[7] = a[5]; out[8] = 1; return out; }; /** * Calculates a 3x3 matrix from the given quaternion * * @param {mat3} out mat3 receiving operation result * @param {quat} q Quaternion to create matrix from * * @returns {mat3} out */ mat3.fromQuat = function (out, q) { var x = q[0], y = q[1], z = q[2], w = q[3], x2 = x + x, y2 = y + y, z2 = z + z, xx = x * x2, yx = y * x2, yy = y * y2, zx = z * x2, zy = z * y2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2; out[0] = 1 - yy - zz; out[3] = yx - wz; out[6] = zx + wy; out[1] = yx + wz; out[4] = 1 - xx - zz; out[7] = zy - wx; out[2] = zx - wy; out[5] = zy + wx; out[8] = 1 - xx - yy; return out; }; /** * Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix * * @param {mat3} out mat3 receiving operation result * @param {mat4} a Mat4 to derive the normal matrix from * * @returns {mat3} out */ mat3.normalFromMat4 = function (out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32, // Calculate the determinant det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return null; } det = 1.0 / det; out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; out[1] = (a12 * b08 - a10 * b11 - a13 * b07) * det; out[2] = (a10 * b10 - a11 * b08 + a13 * b06) * det; out[3] = (a02 * b10 - a01 * b11 - a03 * b09) * det; out[4] = (a00 * b11 - a02 * b08 + a03 * b07) * det; out[5] = (a01 * b08 - a00 * b10 - a03 * b06) * det; out[6] = (a31 * b05 - a32 * b04 + a33 * b03) * det; out[7] = (a32 * b02 - a30 * b05 - a33 * b01) * det; out[8] = (a30 * b04 - a31 * b02 + a33 * b00) * det; return out; }; /** * Returns Frobenius norm of a mat3 * * @param {mat3} a the matrix to calculate Frobenius norm of * @returns {Number} Frobenius norm */ mat3.frob = function (a) { return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2))) }; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class Quaternion * @name quat */ var quat = {}; /** * Creates a new identity quat * * @returns {quat} a new quaternion */ quat.create = function() { var out = new GLMAT_ARRAY_TYPE(4); out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1; return out; }; /** * Sets a quaternion to represent the shortest rotation from one * vector to another. * * Both vectors are assumed to be unit length. * * @param {quat} out the receiving quaternion. * @param {vec3} a the initial vector * @param {vec3} b the destination vector * @returns {quat} out */ quat.rotationTo = (function() { var tmpvec3 = vec3.create(); var xUnitVec3 = vec3.fromValues(1,0,0); var yUnitVec3 = vec3.fromValues(0,1,0); return function(out, a, b) { var dot = vec3.dot(a, b); if (dot < -0.999999) { vec3.cross(tmpvec3, xUnitVec3, a); if (vec3.length(tmpvec3) < 0.000001) vec3.cross(tmpvec3, yUnitVec3, a); vec3.normalize(tmpvec3, tmpvec3); quat.setAxisAngle(out, tmpvec3, Math.PI); return out; } else if (dot > 0.999999) { out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1; return out; } else { vec3.cross(tmpvec3, a, b); out[0] = tmpvec3[0]; out[1] = tmpvec3[1]; out[2] = tmpvec3[2]; out[3] = 1 + dot; return quat.normalize(out, out); } }; })(); /** * Sets the specified quaternion with values corresponding to the given * axes. Each axis is a vec3 and is expected to be unit length and * perpendicular to all other specified axes. * * @param {vec3} view the vector representing the viewing direction * @param {vec3} right the vector representing the local "right" direction * @param {vec3} up the vector representing the local "up" direction * @returns {quat} out */ quat.setAxes = (function() { var matr = mat3.create(); return function(out, view, right, up) { matr[0] = right[0]; matr[3] = right[1]; matr[6] = right[2]; matr[1] = up[0]; matr[4] = up[1]; matr[7] = up[2]; matr[2] = -view[0]; matr[5] = -view[1]; matr[8] = -view[2]; return quat.normalize(out, quat.fromMat3(out, matr)); }; })(); /** * Creates a new quat initialized with values from an existing quaternion * * @param {quat} a quaternion to clone * @returns {quat} a new quaternion * @function */ quat.clone = vec4.clone; /** * Creates a new quat initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {quat} a new quaternion * @function */ quat.fromValues = vec4.fromValues; /** * Copy the values from one quat to another * * @param {quat} out the receiving quaternion * @param {quat} a the source quaternion * @returns {quat} out * @function */ quat.copy = vec4.copy; /** * Set the components of a quat to the given values * * @param {quat} out the receiving quaternion * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {quat} out * @function */ quat.set = vec4.set; /** * Set a quat to the identity quaternion * * @param {quat} out the receiving quaternion * @returns {quat} out */ quat.identity = function(out) { out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 1; return out; }; /** * Sets a quat from the given angle and rotation axis, * then returns it. * * @param {quat} out the receiving quaternion * @param {vec3} axis the axis around which to rotate * @param {Number} rad the angle in radians * @returns {quat} out **/ quat.setAxisAngle = function(out, axis, rad) { rad = rad * 0.5; var s = Math.sin(rad); out[0] = s * axis[0]; out[1] = s * axis[1]; out[2] = s * axis[2]; out[3] = Math.cos(rad); return out; }; /** * Adds two quat's * * @param {quat} out the receiving quaternion * @param {quat} a the first operand * @param {quat} b the second operand * @returns {quat} out * @function */ quat.add = vec4.add; /** * Multiplies two quat's * * @param {quat} out the receiving quaternion * @param {quat} a the first operand * @param {quat} b the second operand * @returns {quat} out */ quat.multiply = function(out, a, b) { var ax = a[0], ay = a[1], az = a[2], aw = a[3], bx = b[0], by = b[1], bz = b[2], bw = b[3]; out[0] = ax * bw + aw * bx + ay * bz - az * by; out[1] = ay * bw + aw * by + az * bx - ax * bz; out[2] = az * bw + aw * bz + ax * by - ay * bx; out[3] = aw * bw - ax * bx - ay * by - az * bz; return out; }; /** * Alias for {@link quat.multiply} * @function */ quat.mul = quat.multiply; /** * Scales a quat by a scalar number * * @param {quat} out the receiving vector * @param {quat} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {quat} out * @function */ quat.scale = vec4.scale; /** * Rotates a quaternion by the given angle about the X axis * * @param {quat} out quat receiving operation result * @param {quat} a quat to rotate * @param {number} rad angle (in radians) to rotate * @returns {quat} out */ quat.rotateX = function (out, a, rad) { rad *= 0.5; var ax = a[0], ay = a[1], az = a[2], aw = a[3], bx = Math.sin(rad), bw = Math.cos(rad); out[0] = ax * bw + aw * bx; out[1] = ay * bw + az * bx; out[2] = az * bw - ay * bx; out[3] = aw * bw - ax * bx; return out; }; /** * Rotates a quaternion by the given angle about the Y axis * * @param {quat} out quat receiving operation result * @param {quat} a quat to rotate * @param {number} rad angle (in radians) to rotate * @returns {quat} out */ quat.rotateY = function (out, a, rad) { rad *= 0.5; var ax = a[0], ay = a[1], az = a[2], aw = a[3], by = Math.sin(rad), bw = Math.cos(rad); out[0] = ax * bw - az * by; out[1] = ay * bw + aw * by; out[2] = az * bw + ax * by; out[3] = aw * bw - ay * by; return out; }; /** * Rotates a quaternion by the given angle about the Z axis * * @param {quat} out quat receiving operation result * @param {quat} a quat to rotate * @param {number} rad angle (in radians) to rotate * @returns {quat} out */ quat.rotateZ = function (out, a, rad) { rad *= 0.5; var ax = a[0], ay = a[1], az = a[2], aw = a[3], bz = Math.sin(rad), bw = Math.cos(rad); out[0] = ax * bw + ay * bz; out[1] = ay * bw - ax * bz; out[2] = az * bw + aw * bz; out[3] = aw * bw - az * bz; return out; }; /** * Calculates the W component of a quat from the X, Y, and Z components. * Assumes that quaternion is 1 unit in length. * Any existing W component will be ignored. * * @param {quat} out the receiving quaternion * @param {quat} a quat to calculate W component of * @returns {quat} out */ quat.calculateW = function (out, a) { var x = a[0], y = a[1], z = a[2]; out[0] = x; out[1] = y; out[2] = z; out[3] = Math.sqrt(Math.abs(1.0 - x * x - y * y - z * z)); return out; }; /** * Calculates the dot product of two quat's * * @param {quat} a the first operand * @param {quat} b the second operand * @returns {Number} dot product of a and b * @function */ quat.dot = vec4.dot; /** * Performs a linear interpolation between two quat's * * @param {quat} out the receiving quaternion * @param {quat} a the first operand * @param {quat} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {quat} out * @function */ quat.lerp = vec4.lerp; /** * Performs a spherical linear interpolation between two quat * * @param {quat} out the receiving quaternion * @param {quat} a the first operand * @param {quat} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {quat} out */ quat.slerp = function (out, a, b, t) { // benchmarks: // http://jsperf.com/quaternion-slerp-implementations var ax = a[0], ay = a[1], az = a[2], aw = a[3], bx = b[0], by = b[1], bz = b[2], bw = b[3]; 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; }; /** * Calculates the inverse of a quat * * @param {quat} out the receiving quaternion * @param {quat} a quat to calculate inverse of * @returns {quat} out */ quat.invert = function(out, a) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], dot = a0*a0 + a1*a1 + a2*a2 + a3*a3, invDot = dot ? 1.0/dot : 0; // TODO: Would be faster to return [0,0,0,0] immediately if dot == 0 out[0] = -a0*invDot; out[1] = -a1*invDot; out[2] = -a2*invDot; out[3] = a3*invDot; return out; }; /** * Calculates the conjugate of a quat * If the quaternion is normalized, this function is faster than quat.inverse and produces the same result. * * @param {quat} out the receiving quaternion * @param {quat} a quat to calculate conjugate of * @returns {quat} out */ quat.conjugate = function (out, a) { out[0] = -a[0]; out[1] = -a[1]; out[2] = -a[2]; out[3] = a[3]; return out; }; /** * Calculates the length of a quat * * @param {quat} a vector to calculate length of * @returns {Number} length of a * @function */ quat.length = vec4.length; /** * Alias for {@link quat.length} * @function */ quat.len = quat.length; /** * Calculates the squared length of a quat * * @param {quat} a vector to calculate squared length of * @returns {Number} squared length of a * @function */ quat.squaredLength = vec4.squaredLength; /** * Alias for {@link quat.squaredLength} * @function */ quat.sqrLen = quat.squaredLength; /** * Normalize a quat * * @param {quat} out the receiving quaternion * @param {quat} a quaternion to normalize * @returns {quat} out * @function */ quat.normalize = vec4.normalize; /** * Creates a quaternion from the given 3x3 rotation matrix. * * NOTE: The resultant quaternion is not normalized, so you should be sure * to renormalize the quaternion yourself where necessary. * * @param {quat} out the receiving quaternion * @param {mat3} m rotation matrix * @returns {quat} out * @function */ quat.fromMat3 = function(out, m) { // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes // article "Quaternion Calculus and Fast Animation". var fTrace = m[0] + m[4] + m[8]; var fRoot; if ( fTrace > 0.0 ) { // |w| > 1/2, may as well choose w > 1/2 fRoot = Math.sqrt(fTrace + 1.0); // 2w out[3] = 0.5 * fRoot; fRoot = 0.5/fRoot; // 1/(4w) out[0] = (m[5]-m[7])*fRoot; out[1] = (m[6]-m[2])*fRoot; out[2] = (m[1]-m[3])*fRoot; } else { // |w| <= 1/2 var i = 0; if ( m[4] > m[0] ) i = 1; if ( m[8] > m[i*3+i] ) i = 2; var j = (i+1)%3; var k = (i+2)%3; fRoot = Math.sqrt(m[i*3+i]-m[j*3+j]-m[k*3+k] + 1.0); out[i] = 0.5 * fRoot; fRoot = 0.5 / fRoot; out[3] = (m[j*3+k] - m[k*3+j]) * fRoot; out[j] = (m[j*3+i] + m[i*3+j]) * fRoot; out[k] = (m[k*3+i] + m[i*3+k]) * fRoot; } return out; }; // Sampler clip is especially for the animation sampler in glTF // Use Typed Array can reduce a lot of heap memory // 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; }; /** * Extend a sub class from base class * @param {object|Function} makeDefaultOpt default option of this sub class, method of the sub can use this.xxx to access this option * @param {Function} [initialize] Initialize after the sub class is instantiated * @param {Object} [proto] Prototype methods/properties of the sub class * @memberOf clay.core.mixin.extend * @return {Function} */ function derive(makeDefaultOpt, initialize/*optional*/, proto/*optional*/) { if (typeof initialize == 'object') { proto = initialize; initialize = null; } var _super = this; var propList; if (!(makeDefaultOpt instanceof Function)) { // Optimize the property iterate if it have been fixed propList = []; for (var propName in makeDefaultOpt) { if (makeDefaultOpt.hasOwnProperty(propName)) { propList.push(propName); } } } var sub = function(options) { // call super constructor _super.apply(this, arguments); if (makeDefaultOpt instanceof Function) { // Invoke makeDefaultOpt each time if it is a function, So we can make sure each // property in the object will not be shared by mutiple instances extend(this, makeDefaultOpt.call(this, options)); } else { extendWithPropList(this, makeDefaultOpt, propList); } if (this.constructor === sub) { // Initialize function will be called in the order of inherit var initializers = sub.__initializers__; for (var i = 0; i < initializers.length; i++) { initializers[i].apply(this, arguments); } } }; // save super constructor sub.__super__ = _super; // Initialize function will be called after all the super constructor is called if (!_super.__initializers__) { sub.__initializers__ = []; } else { sub.__initializers__ = _super.__initializers__.slice(); } if (initialize) { sub.__initializers__.push(initialize); } var Ctor = function() {}; Ctor.prototype = _super.prototype; sub.prototype = new Ctor(); sub.prototype.constructor = sub; extend(sub.prototype, proto); // extend the derive method as a static method; sub.extend = _super.extend; // DEPCRATED sub.derive = _super.extend; return sub; } function extend(target, source) { if (!source) { return; } for (var name in source) { if (source.hasOwnProperty(name)) { target[name] = source[name]; } } } function extendWithPropList(target, source, propList) { for (var i = 0; i < propList.length; i++) { var propName = propList[i]; target[propName] = source[propName]; } } /** * @alias clay.core.mixin.extend * @mixin */ var extendMixin = { extend: derive, // DEPCRATED derive: derive }; function Handler(action, context) { this.action = action; this.context = context; } /** * @mixin * @alias clay.core.mixin.notifier */ var notifier = { /** * Trigger event * @param {string} name */ trigger: function(name) { if (!this.hasOwnProperty('__handlers__')) { return; } if (!this.__handlers__.hasOwnProperty(name)) { return; } var hdls = this.__handlers__[name]; var l = hdls.length, i = -1, args = arguments; // Optimize advise from backbone switch (args.length) { case 1: while (++i < l) { hdls[i].action.call(hdls[i].context); } return; case 2: while (++i < l) { hdls[i].action.call(hdls[i].context, args[1]); } return; case 3: while (++i < l) { hdls[i].action.call(hdls[i].context, args[1], args[2]); } return; case 4: while (++i < l) { hdls[i].action.call(hdls[i].context, args[1], args[2], args[3]); } return; case 5: while (++i < l) { hdls[i].action.call(hdls[i].context, args[1], args[2], args[3], args[4]); } return; default: while (++i < l) { hdls[i].action.apply(hdls[i].context, Array.prototype.slice.call(args, 1)); } return; } }, /** * Register event handler * @param {string} name * @param {Function} action * @param {Object} [context] * @chainable */ on: function(name, action, context) { if (!name || !action) { return; } var handlers = this.__handlers__ || (this.__handlers__={}); if (!handlers[name]) { handlers[name] = []; } else { if (this.has(name, action)) { return; } } var handler = new Handler(action, context || this); handlers[name].push(handler); return this; }, /** * Register event, event will only be triggered once and then removed * @param {string} name * @param {Function} action * @param {Object} [context] * @chainable */ once: function(name, action, context) { if (!name || !action) { return; } var self = this; function wrapper() { self.off(name, wrapper); action.apply(this, arguments); } return this.on(name, wrapper, context); }, /** * Alias of once('before' + name) * @param {string} name * @param {Function} action * @param {Object} [context] * @chainable */ before: function(name, action, context) { if (!name || !action) { return; } name = 'before' + name; return this.on(name, action, context); }, /** * Alias of once('after' + name) * @param {string} name * @param {Function} action * @param {Object} [context] * @chainable */ after: function(name, action, context) { if (!name || !action) { return; } name = 'after' + name; return this.on(name, action, context); }, /** * Alias of on('success') * @param {Function} action * @param {Object} [context] * @chainable */ success: function(action, context) { return this.once('success', action, context); }, /** * Alias of on('error') * @param {Function} action * @param {Object} [context] * @chainable */ error: function(action, context) { return this.once('error', action, context); }, /** * Remove event listener * @param {Function} action * @param {Object} [context] * @chainable */ off: function(name, action) { var handlers = this.__handlers__ || (this.__handlers__={}); if (!action) { handlers[name] = []; return; } if (handlers[name]) { var hdls = handlers[name]; var retains = []; for (var i = 0; i < hdls.length; i++) { if (action && hdls[i].action !== action) { retains.push(hdls[i]); } } handlers[name] = retains; } return this; }, /** * If registered the event handler * @param {string} name * @param {Function} action * @return {boolean} */ has: function(name, action) { var handlers = this.__handlers__; if (! handlers || ! handlers[name]) { return false; } var hdls = handlers[name]; for (var i = 0; i < hdls.length; i++) { if (hdls[i].action === action) { return true; } } } }; var guid = 0; var ArrayProto = Array.prototype; var nativeForEach = ArrayProto.forEach; /** * Util functions * @namespace clay.core.util */ var util$1 = { /** * Generate GUID * @return {number} * @memberOf clay.core.util */ genGUID: function () { return ++guid; }, /** * Relative path to absolute path * @param {string} path * @param {string} basePath * @return {string} * @memberOf clay.core.util */ relative2absolute: function (path, basePath) { if (!basePath || path.match(/^\//)) { return path; } var pathParts = path.split('/'); var basePathParts = basePath.split('/'); var item = pathParts[0]; while(item === '.' || item === '..') { if (item === '..') { basePathParts.pop(); } pathParts.shift(); item = pathParts[0]; } return basePathParts.join('/') + '/' + pathParts.join('/'); }, /** * Extend target with source * @param {Object} target * @param {Object} source * @return {Object} * @memberOf clay.core.util */ extend: function (target, source) { if (source) { for (var name in source) { if (source.hasOwnProperty(name)) { target[name] = source[name]; } } } return target; }, /** * Extend properties to target if not exist. * @param {Object} target * @param {Object} source * @return {Object} * @memberOf clay.core.util */ defaults: function (target, source) { if (source) { for (var propName in source) { if (target[propName] === undefined) { target[propName] = source[propName]; } } } return target; }, /** * Extend properties with a given property list to avoid for..in.. iteration. * @param {Object} target * @param {Object} source * @param {Array.<string>} propList * @return {Object} * @memberOf clay.core.util */ extendWithPropList: function (target, source, propList) { if (source) { for (var i = 0; i < propList.length; i++) { var propName = propList[i]; target[propName] = source[propName]; } } return target; }, /** * Extend properties to target if not exist. With a given property list avoid for..in.. iteration. * @param {Object} target * @param {Object} source * @param {Array.<string>} propList * @return {Object} * @memberOf clay.core.util */ defaultsWithPropList: function (target, source, propList) { if (source) { for (var i = 0; i < propList.length; i++) { var propName = propList[i]; if (target[propName] == null) { target[propName] = source[propName]; } } } return target; }, /** * @param {Object|Array} obj * @param {Function} iterator * @param {Object} [context] * @memberOf clay.core.util */ each: function (obj, iterator, context) { if (!(obj && iterator)) { return; } if (obj.forEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === + obj.length) { for (var i = 0, len = obj.length; i < len; i++) { iterator.call(context, obj[i], i, obj); } } else { for (var key in obj) { if (obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } } }, /** * Is object * @param {} obj * @return {boolean} * @memberOf clay.core.util */ isObject: function (obj) { return obj === Object(obj); }, /** * Is array ? * @param {} obj * @return {boolean} * @memberOf clay.core.util */ isArray: function (obj) { return Array.isArray(obj); }, /** * Is array like, which have a length property * @param {} obj * @return {boolean} * @memberOf clay.core.util */ isArrayLike: function (obj) { if (!obj) { return false; } else { return obj.length === + obj.length; } }, /** * @param {} obj * @return {} * @memberOf clay.core.util */ clone: function (obj) { if (!util$1.isObject(obj)) { return obj; } else if (util$1.isArray(obj)) { return obj.slice(); } else if (util$1.isArrayLike(obj)) { // is typed array var ret = new obj.constructor(obj.length); for (var i = 0; i < obj.length; i++) { ret[i] = obj[i]; } return ret; } else { return util$1.extend({}, obj); } } }; /** * Base class of all objects * @constructor * @alias clay.core.Base * @mixes clay.core.mixin.notifier */ var Base = function () { /** * @type {number} */ this.__uid__ = util$1.genGUID(); }; Base.__initializers__ = [ function (opts) { util$1.extend(this, opts); } ]; util$1.extend(Base, extendMixin); util$1.extend(Base.prototype, notifier); function get(options) { var xhr = new XMLHttpRequest(); xhr.open('get', options.url); // With response type set browser can get and put binary data // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Sending_and_Receiving_Binary_Data // Default is text, and it can be set // arraybuffer, blob, document, json, text xhr.responseType = options.responseType || 'text'; if (options.onprogress) { //https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest xhr.onprogress = function(e) { if (e.lengthComputable) { var percent = e.loaded / e.total; options.onprogress(percent, e.loaded, e.total); } else { options.onprogress(null); } }; } xhr.onload = function(e) { if (xhr.status >= 400) { options.onerror && options.onerror(); } else { options.onload && options.onload(xhr.response); } }; if (options.onerror) { xhr.onerror = options.onerror; } xhr.send(null); } var request = { get: get }; var supportWebGL; var vendor = {}; /** * If support WebGL * @return {boolean} */ vendor.supportWebGL = function () { if (supportWebGL == null) { try { var canvas = document.createElement('canvas'); var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) { throw new Error(); } } catch (e) { supportWebGL = false; } } return supportWebGL; }; vendor.Int8Array = typeof Int8Array === 'undefined' ? Array : Int8Array; vendor.Uint8Array = typeof Uint8Array === 'undefined' ? Array : Uint8Array; vendor.Uint16Array = typeof Uint16Array === 'undefined' ? Array : Uint16Array; vendor.Uint32Array = typeof Uint32Array === 'undefined' ? Array : Uint32Array; vendor.Int16Array = typeof Int16Array === 'undefined' ? Array : Int16Array; vendor.Float32Array = typeof Float32Array === 'undefined' ? Array : Float32Array; vendor.Float64Array = typeof Float64Array === 'undefined' ? Array : Float64Array; var g = {}; if (typeof window !== 'undefined') { g = window; } else if (typeof global !== 'undefined') { g = global; } vendor.requestAnimationFrame = g.requestAnimationFrame || g.msRequestAnimationFrame || g.mozRequestAnimationFrame || g.webkitRequestAnimationFrame || function (func){ setTimeout(func, 16); }; vendor.createCanvas = function () { return document.createElement('canvas'); }; vendor.createImage = function () { return new g.Image(); }; vendor.request = { get: request.get }; vendor.addEventListener = function (dom, type, func, useCapture) { dom.addEventListener(type, func, useCapture); }; vendor.removeEventListener = function (dom, type, func) { dom.removeEventListener(type, func); }; /** * Animation is global timeline that schedule all clips. each frame animation will set the time of clips to current and update the states of clips * @constructor clay.Timeline * @extends clay.core.Base * * @example * var animation = new clay.Timeline(); * var node = new clay.Node(); * animation.animate(node.position) * .when(1000, { * x: 500, * y: 500 * }) * .when(2000, { * x: 100, * y: 100 * }) * .when(3000, { * z: 10 * }) * .start('spline'); */ var Timeline = Base.extend(function () { return /** @lends clay.Timeline# */{ /** * stage is an object with render method, each frame if there exists any animating clips, stage.render will be called * @type {Object} */ stage: null, _clips: [], _running: false, _time: 0, _paused: false, _pausedTime: 0 }; }, /** @lends clay.Timeline.prototype */ { /** * Add animator * @param {clay.animate.Animator} animator */ addAnimator: function (animator) { animator.animation = this; var clips = animator.getClips(); for (var i = 0; i < clips.length; i++) { this.addClip(clips[i]); } }, /** * @param {clay.animation.Clip} clip */ addClip: function (clip) { if (this._clips.indexOf(clip) < 0) { this._clips.push(clip); } }, /** * @param {clay.animation.Clip} clip */ removeClip: function (clip) { var idx = this._clips.indexOf(clip); if (idx >= 0) { this._clips.splice(idx, 1); } }, /** * Remove animator * @param {clay.animate.Animator} animator */ removeAnimator: function (animator) { var clips = animator.getClips(); for (var i = 0; i < clips.length; i++) { this.removeClip(clips[i]); } animator.animation = null; }, _update: function () { var time = Date.now() - this._pausedTime; var delta = time - this._time; var clips = this._clips; var len = clips.length; var deferredEvents = []; var deferredClips = []; for (var i = 0; i < len; i++) { var clip = clips[i]; var e = clip.step(time, delta, false); // Throw out the events need to be called after // stage.render, like finish if (e) { deferredEvents.push(e); deferredClips.push(clip); } } // Remove the finished clip for (var i = 0; i < len;) { if (clips[i]._needsRemove) { clips[i] = clips[len-1]; clips.pop(); len--; } else { i++; } } len = deferredEvents.length; for (var i = 0; i < len; i++) { deferredClips[i].fire(deferredEvents[i]); } this._time = time; this.trigger('frame', delta); if (this.stage && this.stage.render) { this.stage.render(); } }, /** * Start running animation */ start: function () { var self = this; this._running = true; this._time = Date.now(); this._pausedTime = 0; var requestAnimationFrame = vendor.requestAnimationFrame; function step() { if (self._running) { requestAnimationFrame(step); if (!self._paused) { self._update(); } } } requestAnimationFrame(step); }, /** * Stop running animation */ stop: function () { this._running = false; }, /** * Pause */ pause: function () { if (!this._paused) { this._pauseStart = Date.now(); this._paused = true; } }, /** * Resume */ resume: function () { if (this._paused) { this._pausedTime += Date.now() - this._pauseStart; this._paused = false; } }, /** * Remove all clips */ removeClipsAll: function () { this._clips = []; }, /** * Create an animator * @param {Object} target * @param {Object} [options] * @param {boolean} [options.loop] * @param {Function} [options.getter] * @param {Function} [options.setter] * @param {Function} [options.interpolater] * @return {clay.animation.Animator} */ animate: function (target, options) { options = options || {}; var animator = new Animator( target, options.loop, options.getter, options.setter, options.interpolater ); animator.animation = this; return animator; } }); // DEPRECATED /** * * Animation clip that manage a collection of {@link clay.animation.SamplerTrack} * @constructor * @alias clay.animation.TrackClip * * @extends clay.animation.Clip * @param {Object} [opts] * @param {string} [opts.name] * @param {Object} [opts.target] * @param {number} [opts.life] * @param {number} [opts.delay] * @param {number} [opts.gap] * @param {number} [opts.playbackRatio] * @param {boolean|number} [opts.loop] If loop is a number, it indicate the loop count of animation * @param {string|Function} [opts.easing] * @param {Function} [opts.onframe] * @param {Function} [opts.onfinish] * @param {Function} [opts.onrestart] * @param {Array.<clay.animation.SamplerTrack>} [opts.tracks] */ var TrackClip = function (opts) { opts = opts || {}; Clip.call(this, opts); /** * * @type {clay.animation.SamplerTrack[]} */ this.tracks = opts.tracks || []; this.calcLifeFromTracks(); }; TrackClip.prototype = Object.create(Clip.prototype); TrackClip.prototype.constructor = TrackClip; TrackClip.prototype.step = function (time, dTime, silent) { var ret = Clip.prototype.step.call(this, time, dTime, true); if (ret !== 'finish') { var time = this.getElapsedTime(); // TODO life may be changed. if (this._range) { time = this._range[0] + time; } this.setTime(time); } // PENDING Schedule if (!silent && ret !== 'paused') { this.fire('frame'); } return ret; }; /** * @param {Array.<number>} range */ TrackClip.prototype.setRange = function (range) { this.calcLifeFromTracks(); this._range = range; if (range) { range[1] = Math.min(range[1], this.life); range[0] = Math.min(range[0], this.life); this.life = (range[1] - range[0]); } }; TrackClip.prototype.setTime = function (time) { for (var i = 0; i < this.tracks.length; i++) { this.tracks[i].setTime(time); } }; TrackClip.prototype.calcLifeFromTracks = function () { this.life = 0; for (var i = 0; i < this.tracks.length; i++) { this.life = Math.max(this.life, this.tracks[i].getMaxTime()); } }; /** * @param {clay.animation.SamplerTrack} track */ TrackClip.prototype.addTrack = function (track) { this.tracks.push(track); this.calcLifeFromTracks(); }; /** * @param {clay.animation.SamplerTrack} track */ TrackClip.prototype.removeTarck = function (track) { var idx = this.tracks.indexOf(track); if (idx >= 0) { this.tracks.splice(idx, 1); } }; /** * @param {number} startTime * @param {number} endTime * @param {boolean} isLoop * @return {clay.animation.TrackClip} */ TrackClip.prototype.getSubClip = function (startTime, endTime, isLoop) { var subClip = new TrackClip({ name: this.name }); for (var i = 0; i < this.tracks.length; i++) { var subTrack = this.tracks[i].getSubTrack(startTime, endTime); subClip.addTrack(subTrack); } if (isLoop !== undefined) { subClip.setLoop(isLoop); } subClip.life = endTime - startTime; return subClip; }; /** * 1d blending from two skinning clips * @param {clay.animation.TrackClip} clip1 * @param {clay.animation.TrackClip} clip2 * @param {number} w */ TrackClip.prototype.blend1D = function (clip1, clip2, w) { for (var i = 0; i < this.tracks.length; i++) { var c1 = clip1.tracks[i]; var c2 = clip2.tracks[i]; var tClip = this.tracks[i]; tClip.blend1D(c1, c2, w); } }; /** * Additive blending from two skinning clips * @param {clay.animation.TrackClip} clip1 * @param {clay.animation.TrackClip} clip2 */ TrackClip.prototype.additiveBlend = function (clip1, clip2) { for (var i = 0; i < this.tracks.length; i++) { var c1 = clip1.tracks[i]; var c2 = clip2.tracks[i]; var tClip = this.tracks[i]; tClip.additiveBlend(c1, c2); } }; /** * Subtractive blending from two skinning clips * @param {clay.animation.TrackClip} clip1 * @param {clay.animation.TrackClip} clip2 */ TrackClip.prototype.subtractiveBlend = function (clip1, clip2) { for (var i = 0; i < this.tracks.length; i++) { var c1 = clip1.tracks[i]; var c2 = clip2.tracks[i]; var tClip = this.tracks[i]; tClip.subtractiveBlend(c1, c2); } }; /** * 2D blending from three skinning clips * @param {clay.animation.TrackClip} clip1 * @param {clay.animation.TrackClip} clip2 * @param {clay.animation.TrackClip} clip3 * @param {number} f * @param {number} g */ TrackClip.prototype.blend2D = function (clip1, clip2, clip3, f, g) { for (var i = 0; i < this.tracks.length; i++) { var c1 = clip1.tracks[i]; var c2 = clip2.tracks[i]; var c3 = clip3.tracks[i]; var tClip = this.tracks[i]; tClip.blend2D(c1, c2, c3, f, g); } }; /** * Copy SRT of all joints clips from another TrackClip * @param {clay.animation.TrackClip} clip */ TrackClip.prototype.copy = function (clip) { for (var i = 0; i < this.tracks.length; i++) { var sTrack = clip.tracks[i]; var tTrack = this.tracks[i]; vec3.copy(tTrack.position, sTrack.position); vec3.copy(tTrack.scale, sTrack.scale); quat.copy(tTrack.rotation, sTrack.rotation); } }; TrackClip.prototype.clone = function () { var clip = Clip.prototype.clone.call(this); for (var i = 0; i < this.tracks.length; i++) { clip.addTrack(this.tracks[i].clone()); } clip.life = this.life; return clip; }; var EXTENSION_LIST = [ 'OES_texture_float', 'OES_texture_half_float', 'OES_texture_float_linear', 'OES_texture_half_float_linear', 'OES_standard_derivatives', 'OES_vertex_array_object', 'OES_element_index_uint', 'WEBGL_compressed_texture_s3tc', 'WEBGL_depth_texture', 'EXT_texture_filter_anisotropic', 'EXT_shader_texture_lod', 'WEBGL_draw_buffers', 'EXT_frag_depth', 'EXT_sRGB', 'ANGLE_instanced_arrays' ]; var PARAMETER_NAMES = [ 'MAX_TEXTURE_SIZE', 'MAX_CUBE_MAP_TEXTURE_SIZE' ]; function GLInfo(_gl) { var extensions = {}; var parameters = {}; // Get webgl extension for (var i = 0; i < EXTENSION_LIST.length; i++) { var extName = EXTENSION_LIST[i]; createExtension(extName); } // Get parameters for (var i = 0; i < PARAMETER_NAMES.length; i++) { var name = PARAMETER_NAMES[i]; parameters[name] = _gl.getParameter(_gl[name]); } this.getExtension = function (name) { if (!(name in extensions)) { createExtension(name); } return extensions[name]; }; this.getParameter = function (name) { return parameters[name]; }; function createExtension(name) { if (_gl.getExtension) { var ext = _gl.getExtension(name); if (!ext) { ext = _gl.getExtension('MOZ_' + name); } if (!ext) { ext = _gl.getExtension('WEBKIT_' + name); } extensions[name] = ext; } } } /** * @namespace clay.core.glenum * @see http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.14 */ var glenum = { /* ClearBufferMask */ DEPTH_BUFFER_BIT : 0x00000100, STENCIL_BUFFER_BIT : 0x00000400, COLOR_BUFFER_BIT : 0x00004000, /* BeginMode */ POINTS : 0x0000, LINES : 0x0001, LINE_LOOP : 0x0002, LINE_STRIP : 0x0003, TRIANGLES : 0x0004, TRIANGLE_STRIP : 0x0005, TRIANGLE_FAN : 0x0006, /* AlphaFunction (not supported in ES20) */ /* NEVER */ /* LESS */ /* EQUAL */ /* LEQUAL */ /* GREATER */ /* NOTEQUAL */ /* GEQUAL */ /* ALWAYS */ /* BlendingFactorDest */ ZERO : 0, ONE : 1, SRC_COLOR : 0x0300, ONE_MINUS_SRC_COLOR : 0x0301, SRC_ALPHA : 0x0302, ONE_MINUS_SRC_ALPHA : 0x0303, DST_ALPHA : 0x0304, ONE_MINUS_DST_ALPHA : 0x0305, /* BlendingFactorSrc */ /* ZERO */ /* ONE */ DST_COLOR : 0x0306, ONE_MINUS_DST_COLOR : 0x0307, SRC_ALPHA_SATURATE : 0x0308, /* SRC_ALPHA */ /* ONE_MINUS_SRC_ALPHA */ /* DST_ALPHA */ /* ONE_MINUS_DST_ALPHA */ /* BlendEquationSeparate */ FUNC_ADD : 0x8006, BLEND_EQUATION : 0x8009, BLEND_EQUATION_RGB : 0x8009, /* same as BLEND_EQUATION */ BLEND_EQUATION_ALPHA : 0x883D, /* BlendSubtract */ FUNC_SUBTRACT : 0x800A, FUNC_REVERSE_SUBTRACT : 0x800B, /* Separate Blend Functions */ BLEND_DST_RGB : 0x80C8, BLEND_SRC_RGB : 0x80C9, BLEND_DST_ALPHA : 0x80CA, BLEND_SRC_ALPHA : 0x80CB, CONSTANT_COLOR : 0x8001, ONE_MINUS_CONSTANT_COLOR : 0x8002, CONSTANT_ALPHA : 0x8003, ONE_MINUS_CONSTANT_ALPHA : 0x8004, BLEND_COLOR : 0x8005, /* Buffer Objects */ ARRAY_BUFFER : 0x8892, ELEMENT_ARRAY_BUFFER : 0x8893, ARRAY_BUFFER_BINDING : 0x8894, ELEMENT_ARRAY_BUFFER_BINDING : 0x8895, STREAM_DRAW : 0x88E0, STATIC_DRAW : 0x88E4, DYNAMIC_DRAW : 0x88E8, BUFFER_SIZE : 0x8764, BUFFER_USAGE : 0x8765, CURRENT_VERTEX_ATTRIB : 0x8626, /* CullFaceMode */ FRONT : 0x0404, BACK : 0x0405, FRONT_AND_BACK : 0x0408, /* DepthFunction */ /* NEVER */ /* LESS */ /* EQUAL */ /* LEQUAL */ /* GREATER */ /* NOTEQUAL */ /* GEQUAL */ /* ALWAYS */ /* EnableCap */ /* TEXTURE_2D */ CULL_FACE : 0x0B44, BLEND : 0x0BE2, DITHER : 0x0BD0, STENCIL_TEST : 0x0B90, DEPTH_TEST : 0x0B71, SCISSOR_TEST : 0x0C11, POLYGON_OFFSET_FILL : 0x8037, SAMPLE_ALPHA_TO_COVERAGE : 0x809E, SAMPLE_COVERAGE : 0x80A0, /* ErrorCode */ NO_ERROR : 0, INVALID_ENUM : 0x0500, INVALID_VALUE : 0x0501, INVALID_OPERATION : 0x0502, OUT_OF_MEMORY : 0x0505, /* FrontFaceDirection */ CW : 0x0900, CCW : 0x0901, /* GetPName */ LINE_WIDTH : 0x0B21, ALIASED_POINT_SIZE_RANGE : 0x846D, ALIASED_LINE_WIDTH_RANGE : 0x846E, CULL_FACE_MODE : 0x0B45, FRONT_FACE : 0x0B46, DEPTH_RANGE : 0x0B70, DEPTH_WRITEMASK : 0x0B72, DEPTH_CLEAR_VALUE : 0x0B73, DEPTH_FUNC : 0x0B74, STENCIL_CLEAR_VALUE : 0x0B91, STENCIL_FUNC : 0x0B92, STENCIL_FAIL : 0x0B94, STENCIL_PASS_DEPTH_FAIL : 0x0B95, STENCIL_PASS_DEPTH_PASS : 0x0B96, STENCIL_REF : 0x0B97, STENCIL_VALUE_MASK : 0x0B93, STENCIL_WRITEMASK : 0x0B98, STENCIL_BACK_FUNC : 0x8800, STENCIL_BACK_FAIL : 0x8801, STENCIL_BACK_PASS_DEPTH_FAIL : 0x8802, STENCIL_BACK_PASS_DEPTH_PASS : 0x8803, STENCIL_BACK_REF : 0x8CA3, STENCIL_BACK_VALUE_MASK : 0x8CA4, STENCIL_BACK_WRITEMASK : 0x8CA5, VIEWPORT : 0x0BA2, SCISSOR_BOX : 0x0C10, /* SCISSOR_TEST */ COLOR_CLEAR_VALUE : 0x0C22, COLOR_WRITEMASK : 0x0C23, UNPACK_ALIGNMENT : 0x0CF5, PACK_ALIGNMENT : 0x0D05, MAX_TEXTURE_SIZE : 0x0D33, MAX_VIEWPORT_DIMS : 0x0D3A, SUBPIXEL_BITS : 0x0D50, RED_BITS : 0x0D52, GREEN_BITS : 0x0D53, BLUE_BITS : 0x0D54, ALPHA_BITS : 0x0D55, DEPTH_BITS : 0x0D56, STENCIL_BITS : 0x0D57, POLYGON_OFFSET_UNITS : 0x2A00, /* POLYGON_OFFSET_FILL */ POLYGON_OFFSET_FACTOR : 0x8038, TEXTURE_BINDING_2D : 0x8069, SAMPLE_BUFFERS : 0x80A8, SAMPLES : 0x80A9, SAMPLE_COVERAGE_VALUE : 0x80AA, SAMPLE_COVERAGE_INVERT : 0x80AB, /* GetTextureParameter */ /* TEXTURE_MAG_FILTER */ /* TEXTURE_MIN_FILTER */ /* TEXTURE_WRAP_S */ /* TEXTURE_WRAP_T */ COMPRESSED_TEXTURE_FORMATS : 0x86A3, /* HintMode */ DONT_CARE : 0x1100, FASTEST : 0x1101, NICEST : 0x1102, /* HintTarget */ GENERATE_MIPMAP_HINT : 0x8192, /* DataType */ BYTE : 0x1400, UNSIGNED_BYTE : 0x1401, SHORT : 0x1402, UNSIGNED_SHORT : 0x1403, INT : 0x1404, UNSIGNED_INT : 0x1405, FLOAT : 0x1406, /* PixelFormat */ DEPTH_COMPONENT : 0x1902, ALPHA : 0x1906, RGB : 0x1907, RGBA : 0x1908, LUMINANCE : 0x1909, LUMINANCE_ALPHA : 0x190A, /* PixelType */ /* UNSIGNED_BYTE */ UNSIGNED_SHORT_4_4_4_4 : 0x8033, UNSIGNED_SHORT_5_5_5_1 : 0x8034, UNSIGNED_SHORT_5_6_5 : 0x8363, /* Shaders */ FRAGMENT_SHADER : 0x8B30, VERTEX_SHADER : 0x8B31, MAX_VERTEX_ATTRIBS : 0x8869, MAX_VERTEX_UNIFORM_VECTORS : 0x8DFB, MAX_VARYING_VECTORS : 0x8DFC, MAX_COMBINED_TEXTURE_IMAGE_UNITS : 0x8B4D, MAX_VERTEX_TEXTURE_IMAGE_UNITS : 0x8B4C, MAX_TEXTURE_IMAGE_UNITS : 0x8872, MAX_FRAGMENT_UNIFORM_VECTORS : 0x8DFD, SHADER_TYPE : 0x8B4F, DELETE_STATUS : 0x8B80, LINK_STATUS : 0x8B82, VALIDATE_STATUS : 0x8B83, ATTACHED_SHADERS : 0x8B85, ACTIVE_UNIFORMS : 0x8B86, ACTIVE_ATTRIBUTES : 0x8B89, SHADING_LANGUAGE_VERSION : 0x8B8C, CURRENT_PROGRAM : 0x8B8D, /* StencilFunction */ NEVER : 0x0200, LESS : 0x0201, EQUAL : 0x0202, LEQUAL : 0x0203, GREATER : 0x0204, NOTEQUAL : 0x0205, GEQUAL : 0x0206, ALWAYS : 0x0207, /* StencilOp */ /* ZERO */ KEEP : 0x1E00, REPLACE : 0x1E01, INCR : 0x1E02, DECR : 0x1E03, INVERT : 0x150A, INCR_WRAP : 0x8507, DECR_WRAP : 0x8508, /* StringName */ VENDOR : 0x1F00, RENDERER : 0x1F01, VERSION : 0x1F02, /* TextureMagFilter */ NEAREST : 0x2600, LINEAR : 0x2601, /* TextureMinFilter */ /* NEAREST */ /* LINEAR */ NEAREST_MIPMAP_NEAREST : 0x2700, LINEAR_MIPMAP_NEAREST : 0x2701, NEAREST_MIPMAP_LINEAR : 0x2702, LINEAR_MIPMAP_LINEAR : 0x2703, /* TextureParameterName */ TEXTURE_MAG_FILTER : 0x2800, TEXTURE_MIN_FILTER : 0x2801, TEXTURE_WRAP_S : 0x2802, TEXTURE_WRAP_T : 0x2803, /* TextureTarget */ TEXTURE_2D : 0x0DE1, TEXTURE : 0x1702, TEXTURE_CUBE_MAP : 0x8513, TEXTURE_BINDING_CUBE_MAP : 0x8514, TEXTURE_CUBE_MAP_POSITIVE_X : 0x8515, TEXTURE_CUBE_MAP_NEGATIVE_X : 0x8516, TEXTURE_CUBE_MAP_POSITIVE_Y : 0x8517, TEXTURE_CUBE_MAP_NEGATIVE_Y : 0x8518, TEXTURE_CUBE_MAP_POSITIVE_Z : 0x8519, TEXTURE_CUBE_MAP_NEGATIVE_Z : 0x851A, MAX_CUBE_MAP_TEXTURE_SIZE : 0x851C, /* TextureUnit */ TEXTURE0 : 0x84C0, TEXTURE1 : 0x84C1, TEXTURE2 : 0x84C2, TEXTURE3 : 0x84C3, TEXTURE4 : 0x84C4, TEXTURE5 : 0x84C5, TEXTURE6 : 0x84C6, TEXTURE7 : 0x84C7, TEXTURE8 : 0x84C8, TEXTURE9 : 0x84C9, TEXTURE10 : 0x84CA, TEXTURE11 : 0x84CB, TEXTURE12 : 0x84CC, TEXTURE13 : 0x84CD, TEXTURE14 : 0x84CE, TEXTURE15 : 0x84CF, TEXTURE16 : 0x84D0, TEXTURE17 : 0x84D1, TEXTURE18 : 0x84D2, TEXTURE19 : 0x84D3, TEXTURE20 : 0x84D4, TEXTURE21 : 0x84D5, TEXTURE22 : 0x84D6, TEXTURE23 : 0x84D7, TEXTURE24 : 0x84D8, TEXTURE25 : 0x84D9, TEXTURE26 : 0x84DA, TEXTURE27 : 0x84DB, TEXTURE28 : 0x84DC, TEXTURE29 : 0x84DD, TEXTURE30 : 0x84DE, TEXTURE31 : 0x84DF, ACTIVE_TEXTURE : 0x84E0, /* TextureWrapMode */ REPEAT : 0x2901, CLAMP_TO_EDGE : 0x812F, MIRRORED_REPEAT : 0x8370, /* Uniform Types */ FLOAT_VEC2 : 0x8B50, FLOAT_VEC3 : 0x8B51, FLOAT_VEC4 : 0x8B52, INT_VEC2 : 0x8B53, INT_VEC3 : 0x8B54, INT_VEC4 : 0x8B55, BOOL : 0x8B56, BOOL_VEC2 : 0x8B57, BOOL_VEC3 : 0x8B58, BOOL_VEC4 : 0x8B59, FLOAT_MAT2 : 0x8B5A, FLOAT_MAT3 : 0x8B5B, FLOAT_MAT4 : 0x8B5C, SAMPLER_2D : 0x8B5E, SAMPLER_CUBE : 0x8B60, /* Vertex Arrays */ VERTEX_ATTRIB_ARRAY_ENABLED : 0x8622, VERTEX_ATTRIB_ARRAY_SIZE : 0x8623, VERTEX_ATTRIB_ARRAY_STRIDE : 0x8624, VERTEX_ATTRIB_ARRAY_TYPE : 0x8625, VERTEX_ATTRIB_ARRAY_NORMALIZED : 0x886A, VERTEX_ATTRIB_ARRAY_POINTER : 0x8645, VERTEX_ATTRIB_ARRAY_BUFFER_BINDING : 0x889F, /* Shader Source */ COMPILE_STATUS : 0x8B81, /* Shader Precision-Specified Types */ LOW_FLOAT : 0x8DF0, MEDIUM_FLOAT : 0x8DF1, HIGH_FLOAT : 0x8DF2, LOW_INT : 0x8DF3, MEDIUM_INT : 0x8DF4, HIGH_INT : 0x8DF5, /* Framebuffer Object. */ FRAMEBUFFER : 0x8D40, RENDERBUFFER : 0x8D41, RGBA4 : 0x8056, RGB5_A1 : 0x8057, RGB565 : 0x8D62, DEPTH_COMPONENT16 : 0x81A5, STENCIL_INDEX : 0x1901, STENCIL_INDEX8 : 0x8D48, DEPTH_STENCIL : 0x84F9, RENDERBUFFER_WIDTH : 0x8D42, RENDERBUFFER_HEIGHT : 0x8D43, RENDERBUFFER_INTERNAL_FORMAT : 0x8D44, RENDERBUFFER_RED_SIZE : 0x8D50, RENDERBUFFER_GREEN_SIZE : 0x8D51, RENDERBUFFER_BLUE_SIZE : 0x8D52, RENDERBUFFER_ALPHA_SIZE : 0x8D53, RENDERBUFFER_DEPTH_SIZE : 0x8D54, RENDERBUFFER_STENCIL_SIZE : 0x8D55, FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE : 0x8CD0, FRAMEBUFFER_ATTACHMENT_OBJECT_NAME : 0x8CD1, FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL : 0x8CD2, FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE : 0x8CD3, COLOR_ATTACHMENT0 : 0x8CE0, DEPTH_ATTACHMENT : 0x8D00, STENCIL_ATTACHMENT : 0x8D20, DEPTH_STENCIL_ATTACHMENT : 0x821A, NONE : 0, FRAMEBUFFER_COMPLETE : 0x8CD5, FRAMEBUFFER_INCOMPLETE_ATTACHMENT : 0x8CD6, FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT : 0x8CD7, FRAMEBUFFER_INCOMPLETE_DIMENSIONS : 0x8CD9, FRAMEBUFFER_UNSUPPORTED : 0x8CDD, FRAMEBUFFER_BINDING : 0x8CA6, RENDERBUFFER_BINDING : 0x8CA7, MAX_RENDERBUFFER_SIZE : 0x84E8, INVALID_FRAMEBUFFER_OPERATION : 0x0506, /* WebGL-specific enums */ UNPACK_FLIP_Y_WEBGL : 0x9240, UNPACK_PREMULTIPLY_ALPHA_WEBGL : 0x9241, CONTEXT_LOST_WEBGL : 0x9242, UNPACK_COLORSPACE_CONVERSION_WEBGL : 0x9243, BROWSER_DEFAULT_WEBGL : 0x9244, }; /** * Simple double linked list. Compared with array, it has O(1) remove operation. * @constructor * @alias clay.core.LinkedList */ var LinkedList = function () { /** * @type {clay.core.LinkedList.Entry} */ this.head = null; /** * @type {clay.core.LinkedList.Entry} */ this.tail = null; this._length = 0; }; /** * Insert a new value at the tail * @param {} val * @return {clay.core.LinkedList.Entry} */ LinkedList.prototype.insert = function (val) { var entry = new LinkedList.Entry(val); this.insertEntry(entry); return entry; }; /** * Insert a new value at idx * @param {number} idx * @param {} val * @return {clay.core.LinkedList.Entry} */ LinkedList.prototype.insertAt = function (idx, val) { if (idx < 0) { return; } var next = this.head; var cursor = 0; while (next && cursor != idx) { next = next.next; cursor++; } if (next) { var entry = new LinkedList.Entry(val); var prev = next.prev; if (!prev) { //next is head this.head = entry; } else { prev.next = entry; entry.prev = prev; } entry.next = next; next.prev = entry; } else { this.insert(val); } }; LinkedList.prototype.insertBeforeEntry = function (val, next) { var entry = new LinkedList.Entry(val); var prev = next.prev; if (!prev) { //next is head this.head = entry; } else { prev.next = entry; entry.prev = prev; } entry.next = next; next.prev = entry; this._length++; }; /** * Insert an entry at the tail * @param {clay.core.LinkedList.Entry} entry */ LinkedList.prototype.insertEntry = function (entry) { if (!this.head) { this.head = this.tail = entry; } else { this.tail.next = entry; entry.prev = this.tail; this.tail = entry; } this._length++; }; /** * Remove entry. * @param {clay.core.LinkedList.Entry} entry */ LinkedList.prototype.remove = function (entry) { var prev = entry.prev; var next = entry.next; if (prev) { prev.next = next; } else { // Is head this.head = next; } if (next) { next.prev = prev; } else { // Is tail this.tail = prev; } entry.next = entry.prev = null; this._length--; }; /** * Remove entry at index. * @param {number} idx * @return {} */ LinkedList.prototype.removeAt = function (idx) { if (idx < 0) { return; } var curr = this.head; var cursor = 0; while (curr && cursor != idx) { curr = curr.next; cursor++; } if (curr) { this.remove(curr); return curr.value; } }; /** * Get head value * @return {} */ LinkedList.prototype.getHead = function () { if (this.head) { return this.head.value; } }; /** * Get tail value * @return {} */ LinkedList.prototype.getTail = function () { if (this.tail) { return this.tail.value; } }; /** * Get value at idx * @param {number} idx * @return {} */ LinkedList.prototype.getAt = function (idx) { if (idx < 0) { return; } var curr = this.head; var cursor = 0; while (curr && cursor != idx) { curr = curr.next; cursor++; } return curr.value; }; /** * @param {} value * @return {number} */ LinkedList.prototype.indexOf = function (value) { var curr = this.head; var cursor = 0; while (curr) { if (curr.value === value) { return cursor; } curr = curr.next; cursor++; } }; /** * @return {number} */ LinkedList.prototype.length = function () { return this._length; }; /** * If list is empty */ LinkedList.prototype.isEmpty = function () { return this._length === 0; }; /** * @param {Function} cb * @param {} context */ LinkedList.prototype.forEach = function (cb, context) { var curr = this.head; var idx = 0; var haveContext = typeof(context) != 'undefined'; while (curr) { if (haveContext) { cb.call(context, curr.value, idx); } else { cb(curr.value, idx); } curr = curr.next; idx++; } }; /** * Clear the list */ LinkedList.prototype.clear = function () { this.tail = this.head = null; this._length = 0; }; /** * @constructor * @param {} val */ LinkedList.Entry = function (val) { /** * @type {} */ this.value = val; /** * @type {clay.core.LinkedList.Entry} */ this.next = null; /** * @type {clay.core.LinkedList.Entry} */ this.prev = null; }; /** * LRU Cache * @constructor * @alias clay.core.LRU */ var LRU$1 = function (maxSize) { this._list = new LinkedList(); this._map = {}; this._maxSize = maxSize || 10; }; /** * Set cache max size * @param {number} size */ LRU$1.prototype.setMaxSize = function (size) { this._maxSize = size; }; /** * @param {string} key * @param {} value */ LRU$1.prototype.put = function (key, value) { if (!this._map.hasOwnProperty(key)) { var len = this._list.length(); if (len >= this._maxSize && len > 0) { // Remove the least recently used var leastUsedEntry = this._list.head; this._list.remove(leastUsedEntry); delete this._map[leastUsedEntry.key]; } var entry = this._list.insert(value); entry.key = key; this._map[key] = entry; } }; /** * @param {string} key * @return {} */ LRU$1.prototype.get = function (key) { var entry = this._map[key]; if (this._map.hasOwnProperty(key)) { // Put the latest used entry in the tail if (entry !== this._list.tail) { this._list.remove(entry); this._list.insertEntry(entry); } return entry.value; } }; /** * @param {string} key */ LRU$1.prototype.remove = function (key) { var entry = this._map[key]; if (typeof(entry) !== 'undefined') { delete this._map[key]; this._list.remove(entry); } }; /** * Clear the cache */ LRU$1.prototype.clear = function () { this._list.clear(); this._map = {}; }; /** * @namespace clay.core.color */ var colorUtil = {}; var kCSSColorTable = { 'transparent': [0,0,0,0], 'aliceblue': [240,248,255,1], 'antiquewhite': [250,235,215,1], 'aqua': [0,255,255,1], 'aquamarine': [127,255,212,1], 'azure': [240,255,255,1], 'beige': [245,245,220,1], 'bisque': [255,228,196,1], 'black': [0,0,0,1], 'blanchedalmond': [255,235,205,1], 'blue': [0,0,255,1], 'blueviolet': [138,43,226,1], 'brown': [165,42,42,1], 'burlywood': [222,184,135,1], 'cadetblue': [95,158,160,1], 'chartreuse': [127,255,0,1], 'chocolate': [210,105,30,1], 'coral': [255,127,80,1], 'cornflowerblue': [100,149,237,1], 'cornsilk': [255,248,220,1], 'crimson': [220,20,60,1], 'cyan': [0,255,255,1], 'darkblue': [0,0,139,1], 'darkcyan': [0,139,139,1], 'darkgoldenrod': [184,134,11,1], 'darkgray': [169,169,169,1], 'darkgreen': [0,100,0,1], 'darkgrey': [169,169,169,1], 'darkkhaki': [189,183,107,1], 'darkmagenta': [139,0,139,1], 'darkolivegreen': [85,107,47,1], 'darkorange': [255,140,0,1], 'darkorchid': [153,50,204,1], 'darkred': [139,0,0,1], 'darksalmon': [233,150,122,1], 'darkseagreen': [143,188,143,1], 'darkslateblue': [72,61,139,1], 'darkslategray': [47,79,79,1], 'darkslategrey': [47,79,79,1], 'darkturquoise': [0,206,209,1], 'darkviolet': [148,0,211,1], 'deeppink': [255,20,147,1], 'deepskyblue': [0,191,255,1], 'dimgray': [105,105,105,1], 'dimgrey': [105,105,105,1], 'dodgerblue': [30,144,255,1], 'firebrick': [178,34,34,1], 'floralwhite': [255,250,240,1], 'forestgreen': [34,139,34,1], 'fuchsia': [255,0,255,1], 'gainsboro': [220,220,220,1], 'ghostwhite': [248,248,255,1], 'gold': [255,215,0,1], 'goldenrod': [218,165,32,1], 'gray': [128,128,128,1], 'green': [0,128,0,1], 'greenyellow': [173,255,47,1], 'grey': [128,128,128,1], 'honeydew': [240,255,240,1], 'hotpink': [255,105,180,1], 'indianred': [205,92,92,1], 'indigo': [75,0,130,1], 'ivory': [255,255,240,1], 'khaki': [240,230,140,1], 'lavender': [230,230,250,1], 'lavenderblush': [255,240,245,1], 'lawngreen': [124,252,0,1], 'lemonchiffon': [255,250,205,1], 'lightblue': [173,216,230,1], 'lightcoral': [240,128,128,1], 'lightcyan': [224,255,255,1], 'lightgoldenrodyellow': [250,250,210,1], 'lightgray': [211,211,211,1], 'lightgreen': [144,238,144,1], 'lightgrey': [211,211,211,1], 'lightpink': [255,182,193,1], 'lightsalmon': [255,160,122,1], 'lightseagreen': [32,178,170,1], 'lightskyblue': [135,206,250,1], 'lightslategray': [119,136,153,1], 'lightslategrey': [119,136,153,1], 'lightsteelblue': [176,196,222,1], 'lightyellow': [255,255,224,1], 'lime': [0,255,0,1], 'limegreen': [50,205,50,1], 'linen': [250,240,230,1], 'magenta': [255,0,255,1], 'maroon': [128,0,0,1], 'mediumaquamarine': [102,205,170,1], 'mediumblue': [0,0,205,1], 'mediumorchid': [186,85,211,1], 'mediumpurple': [147,112,219,1], 'mediumseagreen': [60,179,113,1], 'mediumslateblue': [123,104,238,1], 'mediumspringgreen': [0,250,154,1], 'mediumturquoise': [72,209,204,1], 'mediumvioletred': [199,21,133,1], 'midnightblue': [25,25,112,1], 'mintcream': [245,255,250,1], 'mistyrose': [255,228,225,1], 'moccasin': [255,228,181,1], 'navajowhite': [255,222,173,1], 'navy': [0,0,128,1], 'oldlace': [253,245,230,1], 'olive': [128,128,0,1], 'olivedrab': [107,142,35,1], 'orange': [255,165,0,1], 'orangered': [255,69,0,1], 'orchid': [218,112,214,1], 'palegoldenrod': [238,232,170,1], 'palegreen': [152,251,152,1], 'paleturquoise': [175,238,238,1], 'palevioletred': [219,112,147,1], 'papayawhip': [255,239,213,1], 'peachpuff': [255,218,185,1], 'peru': [205,133,63,1], 'pink': [255,192,203,1], 'plum': [221,160,221,1], 'powderblue': [176,224,230,1], 'purple': [128,0,128,1], 'red': [255,0,0,1], 'rosybrown': [188,143,143,1], 'royalblue': [65,105,225,1], 'saddlebrown': [139,69,19,1], 'salmon': [250,128,114,1], 'sandybrown': [244,164,96,1], 'seagreen': [46,139,87,1], 'seashell': [255,245,238,1], 'sienna': [160,82,45,1], 'silver': [192,192,192,1], 'skyblue': [135,206,235,1], 'slateblue': [106,90,205,1], 'slategray': [112,128,144,1], 'slategrey': [112,128,144,1], 'snow': [255,250,250,1], 'springgreen': [0,255,127,1], 'steelblue': [70,130,180,1], 'tan': [210,180,140,1], 'teal': [0,128,128,1], 'thistle': [216,191,216,1], 'tomato': [255,99,71,1], 'turquoise': [64,224,208,1], 'violet': [238,130,238,1], 'wheat': [245,222,179,1], 'white': [255,255,255,1], 'whitesmoke': [245,245,245,1], 'yellow': [255,255,0,1], 'yellowgreen': [154,205,50,1] }; function clampCssByte(i) { // Clamp to integer 0 .. 255. i = Math.round(i); // Seems to be what Chrome does (vs truncation). return i < 0 ? 0 : i > 255 ? 255 : i; } function clampCssAngle(i) { // Clamp to integer 0 .. 360. i = Math.round(i); // Seems to be what Chrome does (vs truncation). return i < 0 ? 0 : i > 360 ? 360 : i; } function clampCssFloat(f) { // Clamp to float 0.0 .. 1.0. return f < 0 ? 0 : f > 1 ? 1 : f; } function parseCssInt(str) { // int or percentage. if (str.length && str.charAt(str.length - 1) === '%') { return clampCssByte(parseFloat(str) / 100 * 255); } return clampCssByte(parseInt(str, 10)); } function parseCssFloat(str) { // float or percentage. if (str.length && str.charAt(str.length - 1) === '%') { return clampCssFloat(parseFloat(str) / 100); } return clampCssFloat(parseFloat(str)); } function cssHueToRgb(m1, m2, h) { if (h < 0) { h += 1; } else if (h > 1) { h -= 1; } if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } if (h * 2 < 1) { return m2; } if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; } return m1; } function lerpNumber(a, b, p) { return a + (b - a) * p; } function setRgba(out, r, g, b, a) { out[0] = r; out[1] = g; out[2] = b; out[3] = a; return out; } function copyRgba(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; } var colorCache = new LRU$1(20); var lastRemovedArr = null; function putToCache(colorStr, rgbaArr) { // Reuse removed array if (lastRemovedArr) { copyRgba(lastRemovedArr, rgbaArr); } lastRemovedArr = colorCache.put(colorStr, lastRemovedArr || (rgbaArr.slice())); } /** * @name clay.core.color.parse * @param {string} colorStr * @param {Array.<number>} out * @return {Array.<number>} */ colorUtil.parse = function (colorStr, rgbaArr) { if (!colorStr) { return; } rgbaArr = rgbaArr || []; var cached = colorCache.get(colorStr); if (cached) { return copyRgba(rgbaArr, cached); } // colorStr may be not string colorStr = colorStr + ''; // Remove all whitespace, not compliant, but should just be more accepting. var str = colorStr.replace(/ /g, '').toLowerCase(); // Color keywords (and transparent) lookup. if (str in kCSSColorTable) { copyRgba(rgbaArr, kCSSColorTable[str]); putToCache(colorStr, rgbaArr); return rgbaArr; } // #abc and #abc123 syntax. if (str.charAt(0) === '#') { if (str.length === 4) { var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. if (!(iv >= 0 && iv <= 0xfff)) { setRgba(rgbaArr, 0, 0, 0, 1); return; // Covers NaN. } setRgba(rgbaArr, ((iv & 0xf00) >> 4) | ((iv & 0xf00) >> 8), (iv & 0xf0) | ((iv & 0xf0) >> 4), (iv & 0xf) | ((iv & 0xf) << 4), 1 ); putToCache(colorStr, rgbaArr); return rgbaArr; } else if (str.length === 7) { var iv = parseInt(str.substr(1), 16); // TODO(deanm): Stricter parsing. if (!(iv >= 0 && iv <= 0xffffff)) { setRgba(rgbaArr, 0, 0, 0, 1); return; // Covers NaN. } setRgba(rgbaArr, (iv & 0xff0000) >> 16, (iv & 0xff00) >> 8, iv & 0xff, 1 ); putToCache(colorStr, rgbaArr); return rgbaArr; } return; } var op = str.indexOf('('), ep = str.indexOf(')'); if (op !== -1 && ep + 1 === str.length) { var fname = str.substr(0, op); var params = str.substr(op + 1, ep - (op + 1)).split(','); var alpha = 1; // To allow case fallthrough. switch (fname) { case 'rgba': if (params.length !== 4) { setRgba(rgbaArr, 0, 0, 0, 1); return; } alpha = parseCssFloat(params.pop()); // jshint ignore:line // Fall through. case 'rgb': if (params.length !== 3) { setRgba(rgbaArr, 0, 0, 0, 1); return; } setRgba(rgbaArr, parseCssInt(params[0]), parseCssInt(params[1]), parseCssInt(params[2]), alpha ); putToCache(colorStr, rgbaArr); return rgbaArr; case 'hsla': if (params.length !== 4) { setRgba(rgbaArr, 0, 0, 0, 1); return; } params[3] = parseCssFloat(params[3]); hsla2rgba(params, rgbaArr); putToCache(colorStr, rgbaArr); return rgbaArr; case 'hsl': if (params.length !== 3) { setRgba(rgbaArr, 0, 0, 0, 1); return; } hsla2rgba(params, rgbaArr); putToCache(colorStr, rgbaArr); return rgbaArr; default: return; } } setRgba(rgbaArr, 0, 0, 0, 1); return; }; colorUtil.parseToFloat = function (colorStr, rgbaArr) { rgbaArr = colorUtil.parse(colorStr, rgbaArr); if (!rgbaArr) { return; } rgbaArr[0] /= 255; rgbaArr[1] /= 255; rgbaArr[2] /= 255; return rgbaArr; }; /** * @name clay.core.color.hsla2rgba * @param {Array.<number>} hsla * @param {Array.<number>} rgba * @return {Array.<number>} rgba */ function hsla2rgba(hsla, rgba) { var h = (((parseFloat(hsla[0]) % 360) + 360) % 360) / 360; // 0 .. 1 // NOTE(deanm): According to the CSS spec s/l should only be // percentages, but we don't bother and let float or percentage. var s = parseCssFloat(hsla[1]); var l = parseCssFloat(hsla[2]); var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; var m1 = l * 2 - m2; rgba = rgba || []; setRgba(rgba, clampCssByte(cssHueToRgb(m1, m2, h + 1 / 3) * 255), clampCssByte(cssHueToRgb(m1, m2, h) * 255), clampCssByte(cssHueToRgb(m1, m2, h - 1 / 3) * 255), 1 ); if (hsla.length === 4) { rgba[3] = hsla[3]; } return rgba; } /** * @name clay.core.color.rgba2hsla * @param {Array.<number>} rgba * @return {Array.<number>} hsla */ function rgba2hsla(rgba) { if (!rgba) { return; } // RGB from 0 to 255 var R = rgba[0] / 255; var G = rgba[1] / 255; var B = rgba[2] / 255; var vMin = Math.min(R, G, B); // Min. value of RGB var vMax = Math.max(R, G, B); // Max. value of RGB var delta = vMax - vMin; // Delta RGB value var L = (vMax + vMin) / 2; var H; var S; // HSL results from 0 to 1 if (delta === 0) { H = 0; S = 0; } else { if (L < 0.5) { S = delta / (vMax + vMin); } else { S = delta / (2 - vMax - vMin); } var deltaR = (((vMax - R) / 6) + (delta / 2)) / delta; var deltaG = (((vMax - G) / 6) + (delta / 2)) / delta; var deltaB = (((vMax - B) / 6) + (delta / 2)) / delta; if (R === vMax) { H = deltaB - deltaG; } else if (G === vMax) { H = (1 / 3) + deltaR - deltaB; } else if (B === vMax) { H = (2 / 3) + deltaG - deltaR; } if (H < 0) { H += 1; } if (H > 1) { H -= 1; } } var hsla = [H * 360, S, L]; if (rgba[3] != null) { hsla.push(rgba[3]); } return hsla; } /** * @name clay.core.color.lift * @param {string} color * @param {number} level * @return {string} */ colorUtil.lift = function (color, level) { var colorArr = colorUtil.parse(color); if (colorArr) { for (var i = 0; i < 3; i++) { if (level < 0) { colorArr[i] = colorArr[i] * (1 - level) | 0; } else { colorArr[i] = ((255 - colorArr[i]) * level + colorArr[i]) | 0; } } return colorUtil.stringify(colorArr, colorArr.length === 4 ? 'rgba' : 'rgb'); } }; /** * @name clay.core.color.toHex * @param {string} color * @return {string} */ colorUtil.toHex = function (color) { var colorArr = colorUtil.parse(color); if (colorArr) { return ((1 << 24) + (colorArr[0] << 16) + (colorArr[1] << 8) + (+colorArr[2])).toString(16).slice(1); } }; /** * Map value to color. Faster than lerp methods because color is represented by rgba array. * @name clay.core.color * @param {number} normalizedValue A float between 0 and 1. * @param {Array.<Array.<number>>} colors List of rgba color array * @param {Array.<number>} [out] Mapped gba color array * @return {Array.<number>} will be null/undefined if input illegal. */ colorUtil.fastLerp = function (normalizedValue, colors, out) { if (!(colors && colors.length) || !(normalizedValue >= 0 && normalizedValue <= 1) ) { return; } out = out || []; var value = normalizedValue * (colors.length - 1); var leftIndex = Math.floor(value); var rightIndex = Math.ceil(value); var leftColor = colors[leftIndex]; var rightColor = colors[rightIndex]; var dv = value - leftIndex; out[0] = clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv)); out[1] = clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv)); out[2] = clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv)); out[3] = clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv)); return out; }; colorUtil.fastMapToColor = colorUtil.fastLerp; /** * @param {number} normalizedValue A float between 0 and 1. * @param {Array.<string>} colors Color list. * @param {boolean=} fullOutput Default false. * @return {(string|Object)} Result color. If fullOutput, * return {color: ..., leftIndex: ..., rightIndex: ..., value: ...}, */ colorUtil.lerp = function (normalizedValue, colors, fullOutput) { if (!(colors && colors.length) || !(normalizedValue >= 0 && normalizedValue <= 1) ) { return; } var value = normalizedValue * (colors.length - 1); var leftIndex = Math.floor(value); var rightIndex = Math.ceil(value); var leftColor = colorUtil.parse(colors[leftIndex]); var rightColor = colorUtil.parse(colors[rightIndex]); var dv = value - leftIndex; var color = colorUtil.stringify( [ clampCssByte(lerpNumber(leftColor[0], rightColor[0], dv)), clampCssByte(lerpNumber(leftColor[1], rightColor[1], dv)), clampCssByte(lerpNumber(leftColor[2], rightColor[2], dv)), clampCssFloat(lerpNumber(leftColor[3], rightColor[3], dv)) ], 'rgba' ); return fullOutput ? { color: color, leftIndex: leftIndex, rightIndex: rightIndex, value: value } : color; }; /** * @deprecated */ colorUtil.mapToColor = colorUtil.lerp; /** * @name clay.core.color * @param {string} color * @param {number=} h 0 ~ 360, ignore when null. * @param {number=} s 0 ~ 1, ignore when null. * @param {number=} l 0 ~ 1, ignore when null. * @return {string} Color string in rgba format. */ colorUtil.modifyHSL = function (color, h, s, l) { color = colorUtil.parse(color); if (color) { color = rgba2hsla(color); h != null && (color[0] = clampCssAngle(h)); s != null && (color[1] = parseCssFloat(s)); l != null && (color[2] = parseCssFloat(l)); return colorUtil.stringify(hsla2rgba(color), 'rgba'); } }; /** * @param {string} color * @param {number=} alpha 0 ~ 1 * @return {string} Color string in rgba format. */ colorUtil.modifyAlpha = function (color, alpha) { color = colorUtil.parse(color); if (color && alpha != null) { color[3] = clampCssFloat(alpha); return colorUtil.stringify(color, 'rgba'); } }; /** * @param {Array.<number>} arrColor like [12,33,44,0.4] * @param {string} type 'rgba', 'hsva', ... * @return {string} Result color. (If input illegal, return undefined). */ colorUtil.stringify = function (arrColor, type) { if (!arrColor || !arrColor.length) { return; } var colorStr = arrColor[0] + ',' + arrColor[1] + ',' + arrColor[2]; if (type === 'rgba' || type === 'hsva' || type === 'hsla') { colorStr += ',' + arrColor[3]; } return type + '(' + colorStr + ')'; }; var parseColor$1 = colorUtil.parseToFloat; var programKeyCache = {}; function getDefineCode(defines) { var defineKeys = Object.keys(defines); defineKeys.sort(); var defineStr = []; // Custom Defines for (var i = 0; i < defineKeys.length; i++) { var key = defineKeys[i]; var value = defines[key]; if (value === null) { defineStr.push(key); } else{ defineStr.push(key + ' ' + value.toString()); } } return defineStr.join('\n'); } function getProgramKey(vertexDefines, fragmentDefines, enabledTextures) { enabledTextures.sort(); var defineStr = []; for (var i = 0; i < enabledTextures.length; i++) { var symbol = enabledTextures[i]; defineStr.push(symbol); } var key = getDefineCode(vertexDefines) + '\n' + getDefineCode(fragmentDefines) + '\n' + defineStr.join('\n'); if (programKeyCache[key]) { return programKeyCache[key]; } var id = util$1.genGUID(); programKeyCache[key] = id; return id; } /** * Material defines the appearance of mesh surface, like `color`, `roughness`, `metalness`, etc. * It contains a {@link clay.Shader} and corresponding uniforms. * * Here is a basic example to create a standard material ```js var material = new clay.Material({ shader: new clay.Shader( clay.Shader.source('clay.vertex'), clay.Shader.source('clay.fragment') ) }); ``` * @constructor clay.Material * @extends clay.core.Base */ var Material = Base.extend(function () { return /** @lends clay.Material# */ { /** * @type {string} */ name: '', /** * @type {Object} */ // uniforms: null, /** * @type {clay.Shader} */ // shader: null, /** * @type {boolean} */ depthTest: true, /** * @type {boolean} */ depthMask: true, /** * @type {boolean} */ transparent: false, /** * Blend func is a callback function when the material * have custom blending * The gl context will be the only argument passed in tho the * blend function * Detail of blend function in WebGL: * http://www.khronos.org/registry/gles/specs/2.0/es_full_spec_2.0.25.pdf * * Example : * function(_gl) { * _gl.blendEquation(_gl.FUNC_ADD); * _gl.blendFunc(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA); * } */ blend: null, /** * If update texture status automatically. */ autoUpdateTextureStatus: true, uniforms: {}, vertexDefines: {}, fragmentDefines: {}, _textureStatus: {}, // shadowTransparentMap : null // PENDING enable the uniform that only used in shader. _enabledUniforms: null, }; }, function () { if (!this.name) { this.name = 'MATERIAL_' + this.__uid__; } if (this.shader) { // Keep status, mainly preset uniforms, vertexDefines and fragmentDefines this.attachShader(this.shader, true); } }, /** @lends clay.Material.prototype */ { precision: 'highp', /** * Set material uniform * @example * mat.setUniform('color', [1, 1, 1, 1]); * @param {string} symbol * @param {number|array|clay.Texture|ArrayBufferView} value */ setUniform: function (symbol, value) { if (value === undefined) { console.warn('Uniform value "' + symbol + '" is undefined'); } var uniform = this.uniforms[symbol]; if (uniform) { if (typeof value === 'string') { // Try to parse as a color. Invalid color string will return null. value = parseColor$1(value) || value; } uniform.value = value; if (this.autoUpdateTextureStatus && uniform.type === 't') { if (value) { this.enableTexture(symbol); } else { this.disableTexture(symbol); } } } }, /** * @param {Object} obj */ setUniforms: function(obj) { for (var key in obj) { var val = obj[key]; this.setUniform(key, val); } }, /** * @param {string} symbol * @return {boolean} */ isUniformEnabled: function (symbol) { return this._enabledUniforms.indexOf(symbol) >= 0; }, getEnabledUniforms: function () { return this._enabledUniforms; }, getTextureUniforms: function () { return this._textureUniforms; }, /** * Alias of setUniform and setUniforms * @param {object|string} symbol * @param {number|array|clay.Texture|ArrayBufferView} [value] */ set: function (symbol, value) { if (typeof(symbol) === 'object') { for (var key in symbol) { var val = symbol[key]; this.setUniform(key, val); } } else { this.setUniform(symbol, value); } }, /** * Get uniform value * @param {string} symbol * @return {number|array|clay.Texture|ArrayBufferView} */ get: function (symbol) { var uniform = this.uniforms[symbol]; if (uniform) { return uniform.value; } }, /** * Attach a shader instance * @param {clay.Shader} shader * @param {boolean} keepStatus If try to keep uniform and texture */ attachShader: function(shader, keepStatus) { var originalUniforms = this.uniforms; // Ignore if uniform can use in shader. this.uniforms = shader.createUniforms(); this.shader = shader; var uniforms = this.uniforms; this._enabledUniforms = Object.keys(uniforms); // Make sure uniforms are set in same order to avoid texture slot wrong this._enabledUniforms.sort(); this._textureUniforms = this._enabledUniforms.filter(function (uniformName) { var type = this.uniforms[uniformName].type; return type === 't' || type === 'tv'; }, this); var originalVertexDefines = this.vertexDefines; var originalFragmentDefines = this.fragmentDefines; this.vertexDefines = util$1.clone(shader.vertexDefines); this.fragmentDefines = util$1.clone(shader.fragmentDefines); if (keepStatus) { for (var symbol in originalUniforms) { if (uniforms[symbol]) { uniforms[symbol].value = originalUniforms[symbol].value; } } util$1.defaults(this.vertexDefines, originalVertexDefines); util$1.defaults(this.fragmentDefines, originalFragmentDefines); } var textureStatus = {}; for (var key in shader.textures) { textureStatus[key] = { shaderType: shader.textures[key].shaderType, type: shader.textures[key].type, enabled: (keepStatus && this._textureStatus[key]) ? this._textureStatus[key].enabled : false }; } this._textureStatus = textureStatus; this._programKey = ''; }, /** * Clone a new material and keep uniforms, shader will not be cloned * @return {clay.Material} */ clone: function () { var material = new this.constructor({ name: this.name, shader: this.shader }); for (var symbol in this.uniforms) { material.uniforms[symbol].value = this.uniforms[symbol].value; } material.depthTest = this.depthTest; material.depthMask = this.depthMask; material.transparent = this.transparent; material.blend = this.blend; material.vertexDefines = util$1.clone(this.vertexDefines); material.fragmentDefines = util$1.clone(this.fragmentDefines); material.enableTexture(this.getEnabledTextures()); material.precision = this.precision; return material; }, /** * Add a #define macro in shader code * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol * @param {number} [val] */ define: function (shaderType, symbol, val) { var vertexDefines = this.vertexDefines; var fragmentDefines = this.fragmentDefines; if (shaderType !== 'vertex' && shaderType !== 'fragment' && shaderType !== 'both' && arguments.length < 3 ) { // shaderType default to be 'both' val = symbol; symbol = shaderType; shaderType = 'both'; } val = val != null ? val : null; if (shaderType === 'vertex' || shaderType === 'both') { if (vertexDefines[symbol] !== val) { vertexDefines[symbol] = val; // Mark as dirty this._programKey = ''; } } if (shaderType === 'fragment' || shaderType === 'both') { if (fragmentDefines[symbol] !== val) { fragmentDefines[symbol] = val; if (shaderType !== 'both') { this._programKey = ''; } } } }, /** * Remove a #define macro in shader code * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ undefine: function (shaderType, symbol) { if (shaderType !== 'vertex' && shaderType !== 'fragment' && shaderType !== 'both' && arguments.length < 2 ) { // shaderType default to be 'both' symbol = shaderType; shaderType = 'both'; } if (shaderType === 'vertex' || shaderType === 'both') { if (this.isDefined('vertex', symbol)) { delete this.vertexDefines[symbol]; // Mark as dirty this._programKey = ''; } } if (shaderType === 'fragment' || shaderType === 'both') { if (this.isDefined('fragment', symbol)) { delete this.fragmentDefines[symbol]; if (shaderType !== 'both') { this._programKey = ''; } } } }, /** * If macro is defined in shader. * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ isDefined: function (shaderType, symbol) { // PENDING hasOwnProperty ? switch (shaderType) { case 'vertex': return this.vertexDefines[symbol] !== undefined; case 'fragment': return this.fragmentDefines[symbol] !== undefined; } }, /** * Get macro value defined in shader. * @param {string} shaderType Can be vertex, fragment or both * @param {string} symbol */ getDefine: function (shaderType, symbol) { switch(shaderType) { case 'vertex': return this.vertexDefines[symbol]; case 'fragment': return this.fragmentDefines[symbol]; } }, /** * Enable a texture, actually it will add a #define macro in the shader code * For example, if texture symbol is diffuseMap, it will add a line `#define DIFFUSEMAP_ENABLED` in the shader code * @param {string} symbol */ enableTexture: function (symbol) { if (Array.isArray(symbol)) { for (var i = 0; i < symbol.length; i++) { this.enableTexture(symbol[i]); } return; } var status = this._textureStatus[symbol]; if (status) { var isEnabled = status.enabled; if (!isEnabled) { status.enabled = true; this._programKey = ''; } } }, /** * Enable all textures used in the shader */ enableTexturesAll: function () { var textureStatus = this._textureStatus; for (var symbol in textureStatus) { textureStatus[symbol].enabled = true; } this._programKey = ''; }, /** * Disable a texture, it remove a #define macro in the shader * @param {string} symbol */ disableTexture: function (symbol) { if (Array.isArray(symbol)) { for (var i = 0; i < symbol.length; i++) { this.disableTexture(symbol[i]); } return; } var status = this._textureStatus[symbol]; if (status) { var isDisabled = ! status.enabled; if (!isDisabled) { status.enabled = false; this._programKey = ''; } } }, /** * Disable all textures used in the shader */ disableTexturesAll: function () { var textureStatus = this._textureStatus; for (var symbol in textureStatus) { textureStatus[symbol].enabled = false; } this._programKey = ''; }, /** * If texture of given type is enabled. * @param {string} symbol * @return {boolean} */ isTextureEnabled: function (symbol) { var textureStatus = this._textureStatus; return !!textureStatus[symbol] && textureStatus[symbol].enabled; }, /** * Get all enabled textures * @return {string[]} */ getEnabledTextures: function () { var enabledTextures = []; var textureStatus = this._textureStatus; for (var symbol in textureStatus) { if (textureStatus[symbol].enabled) { enabledTextures.push(symbol); } } return enabledTextures; }, /** * Mark defines are updated. */ dirtyDefines: function () { this._programKey = ''; }, getProgramKey: function () { if (!this._programKey) { this._programKey = getProgramKey( this.vertexDefines, this.fragmentDefines, this.getEnabledTextures() ); } return this._programKey; } }); var SHADER_STATE_TO_ENABLE = 1; var SHADER_STATE_KEEP_ENABLE = 2; var SHADER_STATE_PENDING = 3; // Enable attribute operation is global to all programs // Here saved the list of all enabled attribute index // http://www.mjbshaw.com/2013/03/webgl-fixing-invalidoperation.html var enabledAttributeList = {}; // some util functions function addLineNumbers(string) { var chunks = string.split('\n'); for (var i = 0, il = chunks.length; i < il; i ++) { // Chrome reports shader errors on lines // starting counting from 1 chunks[i] = (i + 1) + ': ' + chunks[i]; } return chunks.join('\n'); } // Return true or error msg if error happened function checkShaderErrorMsg(_gl, shader, shaderString) { if (!_gl.getShaderParameter(shader, _gl.COMPILE_STATUS)) { return [_gl.getShaderInfoLog(shader), addLineNumbers(shaderString)].join('\n'); } } var tmpFloat32Array16 = new vendor.Float32Array(16); var GLProgram = Base.extend({ uniformSemantics: {}, attributes: {} }, function () { this._locations = {}; this._textureSlot = 0; this._program = null; }, { bind: function (renderer) { this._textureSlot = 0; renderer.gl.useProgram(this._program); }, hasUniform: function (symbol) { var location = this._locations[symbol]; return location !== null && location !== undefined; }, useTextureSlot: function (renderer, texture, slot) { if (texture) { renderer.gl.activeTexture(renderer.gl.TEXTURE0 + slot); // Maybe texture is not loaded yet; if (texture.isRenderable()) { texture.bind(renderer); } else { // Bind texture to null texture.unbind(renderer); } } }, currentTextureSlot: function () { return this._textureSlot; }, resetTextureSlot: function (slot) { this._textureSlot = slot || 0; }, takeCurrentTextureSlot: function (renderer, texture) { var textureSlot = this._textureSlot; this.useTextureSlot(renderer, texture, textureSlot); this._textureSlot++; return textureSlot; }, setUniform: function (_gl, type, symbol, value) { var locationMap = this._locations; var location = locationMap[symbol]; // Uniform is not existed in the shader if (location === null || location === undefined) { return false; } switch (type) { case 'm4': if (!(value instanceof Float32Array)) { // Use Float32Array is much faster than array when uniformMatrix4fv. for (var i = 0; i < value.length; i++) { tmpFloat32Array16[i] = value[i]; } value = tmpFloat32Array16; } _gl.uniformMatrix4fv(location, false, value); break; case '2i': _gl.uniform2i(location, value[0], value[1]); break; case '2f': _gl.uniform2f(location, value[0], value[1]); break; case '3i': _gl.uniform3i(location, value[0], value[1], value[2]); break; case '3f': _gl.uniform3f(location, value[0], value[1], value[2]); break; case '4i': _gl.uniform4i(location, value[0], value[1], value[2], value[3]); break; case '4f': _gl.uniform4f(location, value[0], value[1], value[2], value[3]); break; case '1i': _gl.uniform1i(location, value); break; case '1f': _gl.uniform1f(location, value); break; case '1fv': _gl.uniform1fv(location, value); break; case '1iv': _gl.uniform1iv(location, value); break; case '2iv': _gl.uniform2iv(location, value); break; case '2fv': _gl.uniform2fv(location, value); break; case '3iv': _gl.uniform3iv(location, value); break; case '3fv': _gl.uniform3fv(location, value); break; case '4iv': _gl.uniform4iv(location, value); break; case '4fv': _gl.uniform4fv(location, value); break; case 'm2': case 'm2v': _gl.uniformMatrix2fv(location, false, value); break; case 'm3': case 'm3v': _gl.uniformMatrix3fv(location, false, value); break; case 'm4v': // Raw value if (Array.isArray(value) && Array.isArray(value[0])) { var array = new vendor.Float32Array(value.length * 16); var cursor = 0; for (var i = 0; i < value.length; i++) { var item = value[i]; for (var j = 0; j < 16; j++) { array[cursor++] = item[j]; } } _gl.uniformMatrix4fv(location, false, array); } else { // ArrayBufferView _gl.uniformMatrix4fv(location, false, value); } break; } return true; }, setUniformOfSemantic: function (_gl, semantic, val) { var semanticInfo = this.uniformSemantics[semantic]; if (semanticInfo) { return this.setUniform(_gl, semanticInfo.type, semanticInfo.symbol, val); } return false; }, // Used for creating VAO // Enable the attributes passed in and disable the rest // Example Usage: // enableAttributes(renderer, ["position", "texcoords"]) enableAttributes: function (renderer, attribList, vao) { var _gl = renderer.gl; var program = this._program; var locationMap = this._locations; var enabledAttributeListInContext; if (vao) { enabledAttributeListInContext = vao.__enabledAttributeList; } else { enabledAttributeListInContext = enabledAttributeList[renderer.__uid__]; } if (!enabledAttributeListInContext) { // In vertex array object context // PENDING Each vao object needs to enable attributes again? if (vao) { enabledAttributeListInContext = vao.__enabledAttributeList = []; } else { enabledAttributeListInContext = enabledAttributeList[renderer.__uid__] = []; } } var locationList = []; for (var i = 0; i < attribList.length; i++) { var symbol = attribList[i]; if (!this.attributes[symbol]) { locationList[i] = -1; continue; } var location = locationMap[symbol]; if (location == null) { location = _gl.getAttribLocation(program, symbol); // Attrib location is a number from 0 to ... if (location === -1) { locationList[i] = -1; continue; } locationMap[symbol] = location; } locationList[i] = location; if (!enabledAttributeListInContext[location]) { enabledAttributeListInContext[location] = SHADER_STATE_TO_ENABLE; } else { enabledAttributeListInContext[location] = SHADER_STATE_KEEP_ENABLE; } } for (var i = 0; i < enabledAttributeListInContext.length; i++) { switch(enabledAttributeListInContext[i]){ case SHADER_STATE_TO_ENABLE: _gl.enableVertexAttribArray(i); enabledAttributeListInContext[i] = SHADER_STATE_PENDING; break; case SHADER_STATE_KEEP_ENABLE: enabledAttributeListInContext[i] = SHADER_STATE_PENDING; break; // Expired case SHADER_STATE_PENDING: _gl.disableVertexAttribArray(i); enabledAttributeListInContext[i] = 0; break; } } return locationList; }, getAttribLocation: function (_gl, symbol) { var locationMap = this._locations; var location = locationMap[symbol]; if (location == null) { location = _gl.getAttribLocation(this._program, symbol); locationMap[symbol] = location; } return location; }, buildProgram: function (_gl, shader, vertexShaderCode, fragmentShaderCode) { var vertexShader = _gl.createShader(_gl.VERTEX_SHADER); var program = _gl.createProgram(); _gl.shaderSource(vertexShader, vertexShaderCode); _gl.compileShader(vertexShader); var fragmentShader = _gl.createShader(_gl.FRAGMENT_SHADER); _gl.shaderSource(fragmentShader, fragmentShaderCode); _gl.compileShader(fragmentShader); var msg = checkShaderErrorMsg(_gl, vertexShader, vertexShaderCode); if (msg) { return msg; } msg = checkShaderErrorMsg(_gl, fragmentShader, fragmentShaderCode); if (msg) { return msg; } _gl.attachShader(program, vertexShader); _gl.attachShader(program, fragmentShader); // Force the position bind to location 0; if (shader.attributeSemantics['POSITION']) { _gl.bindAttribLocation(program, 0, shader.attributeSemantics['POSITION'].symbol); } else { // Else choose an attribute and bind to location 0; var keys = Object.keys(this.attributes); _gl.bindAttribLocation(program, 0, keys[0]); } _gl.linkProgram(program); _gl.deleteShader(vertexShader); _gl.deleteShader(fragmentShader); this._program = program; // Save code. this.vertexCode = vertexShaderCode; this.fragmentCode = fragmentShaderCode; if (!_gl.getProgramParameter(program, _gl.LINK_STATUS)) { return 'Could not link program\n' + _gl.getProgramInfoLog(program); } // Cache uniform locations for (var i = 0; i < shader.uniforms.length; i++) { var uniformSymbol = shader.uniforms[i]; this._locations[uniformSymbol] = _gl.getUniformLocation(program, uniformSymbol); } } }); var loopRegex = /for\s*?\(int\s*?_idx_\s*\=\s*([\w-]+)\;\s*_idx_\s*<\s*([\w-]+);\s*_idx_\s*\+\+\s*\)\s*\{\{([\s\S]+?)(?=\}\})\}\}/g; function unrollLoop(shaderStr, defines, lightsNumbers) { // Loop unroll from three.js, https://github.com/mrdoob/three.js/blob/master/src/renderers/webgl/WebGLProgram.js#L175 // In some case like shadowMap in loop use 'i' to index value much slower. // Loop use _idx_ and increased with _idx_++ will be unrolled // Use {{ }} to match the pair so the if statement will not be affected // Write like following // for (int _idx_ = 0; _idx_ < 4; _idx_++) {{ // vec3 color = texture2D(textures[_idx_], uv).rgb; // }} function replace(match, start, end, snippet) { var unroll = ''; // Try to treat as define if (isNaN(start)) { if (start in defines) { start = defines[start]; } else { start = lightNumberDefines[start]; } } if (isNaN(end)) { if (end in defines) { end = defines[end]; } else { end = lightNumberDefines[end]; } } // TODO Error checking for (var idx = parseInt(start); idx < parseInt(end); idx++) { // PENDING Add scope? unroll += '{' + snippet .replace(/float\s*\(\s*_idx_\s*\)/g, idx.toFixed(1)) .replace(/_idx_/g, idx) + '}'; } return unroll; } var lightNumberDefines = {}; for (var lightType in lightsNumbers) { lightNumberDefines[lightType + '_COUNT'] = lightsNumbers[lightType]; } return shaderStr.replace(loopRegex, replace); } function getDefineCode$1(defines, lightsNumbers, enabledTextures) { var defineStr = []; if (lightsNumbers) { for (var lightType in lightsNumbers) { var count = lightsNumbers[lightType]; if (count > 0) { defineStr.push('#define ' + lightType.toUpperCase() + '_COUNT ' + count); } } } if (enabledTextures) { for (var i = 0; i < enabledTextures.length; i++) { var symbol = enabledTextures[i]; defineStr.push('#define ' + symbol.toUpperCase() + '_ENABLED'); } } // Custom Defines for (var symbol in defines) { var value = defines[symbol]; if (value === null) { defineStr.push('#define ' + symbol); } else{ defineStr.push('#define ' + symbol + ' ' + value.toString()); } } return defineStr.join('\n'); } function getExtensionCode(exts) { // Extension declaration must before all non-preprocessor codes // TODO vertex ? extension enum ? var extensionStr = []; for (var i = 0; i < exts.length; i++) { extensionStr.push('#extension GL_' + exts[i] + ' : enable'); } return extensionStr.join('\n'); } function getPrecisionCode(precision) { return ['precision', precision, 'float'].join(' ') + ';\n' + ['precision', precision, 'int'].join(' ') + ';\n' // depth texture may have precision problem on iOS device. + ['precision', precision, 'sampler2D'].join(' ') + ';\n'; } function ProgramManager(renderer) { this._renderer = renderer; this._cache = {}; } ProgramManager.prototype.getProgram = function (renderable, material, scene) { var cache = this._cache; var isSkinnedMesh = renderable.isSkinnedMesh && renderable.isSkinnedMesh(); var isInstancedMesh = renderable.isInstancedMesh && renderable.isInstancedMesh(); var key = 's' + material.shader.shaderID + 'm' + material.getProgramKey(); if (scene) { key += 'se' + scene.getProgramKey(renderable.lightGroup); } if (isSkinnedMesh) { key += ',sk' + renderable.joints.length; } if (isInstancedMesh) { key += ',is'; } var program = cache[key]; if (program) { return program; } var lightsNumbers = scene ? scene.getLightsNumbers(renderable.lightGroup) : {}; var renderer = this._renderer; var _gl = renderer.gl; var enabledTextures = material.getEnabledTextures(); var extraDefineCode = ''; if (isSkinnedMesh) { var skinDefines = { SKINNING: null, JOINT_COUNT: renderable.joints.length }; if (renderable.joints.length > renderer.getMaxJointNumber()) { skinDefines.USE_SKIN_MATRICES_TEXTURE = null; } // TODO Add skinning code? extraDefineCode += '\n' + getDefineCode$1(skinDefines) + '\n'; } if (isInstancedMesh) { extraDefineCode += '\n#define INSTANCING\n'; } // TODO Optimize key generation // VERTEX var vertexDefineStr = extraDefineCode + getDefineCode$1(material.vertexDefines, lightsNumbers, enabledTextures); // FRAGMENT var fragmentDefineStr = extraDefineCode + getDefineCode$1(material.fragmentDefines, lightsNumbers, enabledTextures); var vertexCode = vertexDefineStr + '\n' + material.shader.vertex; var extensions = [ 'OES_standard_derivatives', 'EXT_shader_texture_lod' ].filter(function (ext) { return renderer.getGLExtension(ext) != null; }); if (extensions.indexOf('EXT_shader_texture_lod') >= 0) { fragmentDefineStr += '\n#define SUPPORT_TEXTURE_LOD'; } if (extensions.indexOf('OES_standard_derivatives') >= 0) { fragmentDefineStr += '\n#define SUPPORT_STANDARD_DERIVATIVES'; } var fragmentCode = getExtensionCode(extensions) + '\n' + getPrecisionCode(material.precision) + '\n' + fragmentDefineStr + '\n' + material.shader.fragment; var finalVertexCode = unrollLoop(vertexCode, material.vertexDefines, lightsNumbers); var finalFragmentCode = unrollLoop(fragmentCode, material.fragmentDefines, lightsNumbers); var program = new GLProgram(); program.uniformSemantics = material.shader.uniformSemantics; program.attributes = material.shader.attributes; var errorMsg = program.buildProgram(_gl, material.shader, finalVertexCode, finalFragmentCode); program.__error = errorMsg; cache[key] = program; return program; }; /** * Mainly do the parse and compile of shader string * Support shader code chunk import and export * Support shader semantics * http://www.nvidia.com/object/using_sas.html * https://github.com/KhronosGroup/collada2json/issues/45 */ var uniformRegex = /uniform\s+(bool|float|int|vec2|vec3|vec4|ivec2|ivec3|ivec4|mat2|mat3|mat4|sampler2D|samplerCube)\s+([\s\S]*?);/g; var attributeRegex = /attribute\s+(float|int|vec2|vec3|vec4)\s+([\s\S]*?);/g; // Only parse number define. var defineRegex = /#define\s+(\w+)?(\s+[\d-.]+)?\s*;?\s*\n/g; var uniformTypeMap = { 'bool': '1i', 'int': '1i', 'sampler2D': 't', 'samplerCube': 't', 'float': '1f', 'vec2': '2f', 'vec3': '3f', 'vec4': '4f', 'ivec2': '2i', 'ivec3': '3i', 'ivec4': '4i', 'mat2': 'm2', 'mat3': 'm3', 'mat4': 'm4' }; function createZeroArray(len) { var arr = []; for (var i = 0; i < len; i++) { arr[i] = 0; } return arr; } var uniformValueConstructor = { 'bool': function () { return true; }, 'int': function () { return 0; }, 'float': function () { return 0; }, 'sampler2D': function () { return null; }, 'samplerCube': function () { return null; }, 'vec2': function () { return createZeroArray(2); }, 'vec3': function () { return createZeroArray(3); }, 'vec4': function () { return createZeroArray(4); }, 'ivec2': function () { return createZeroArray(2); }, 'ivec3': function () { return createZeroArray(3); }, 'ivec4': function () { return createZeroArray(4); }, 'mat2': function () { return createZeroArray(4); }, 'mat3': function () { return createZeroArray(9); }, 'mat4': function () { return createZeroArray(16); }, 'array': function () { return []; } }; var attributeSemantics = [ 'POSITION', 'NORMAL', 'BINORMAL', 'TANGENT', 'TEXCOORD', 'TEXCOORD_0', 'TEXCOORD_1', 'COLOR', // Skinning // https://github.com/KhronosGroup/glTF/blob/master/specification/README.md#semantics 'JOINT', 'WEIGHT' ]; var uniformSemantics = [ 'SKIN_MATRIX', // Information about viewport 'VIEWPORT_SIZE', 'VIEWPORT', 'DEVICEPIXELRATIO', // Window size for window relative coordinate // https://www.opengl.org/sdk/docs/man/html/gl_FragCoord.xhtml 'WINDOW_SIZE', // Infomation about camera 'NEAR', 'FAR', // Time 'TIME' ]; var matrixSemantics = [ 'WORLD', 'VIEW', 'PROJECTION', 'WORLDVIEW', 'VIEWPROJECTION', 'WORLDVIEWPROJECTION', 'WORLDINVERSE', 'VIEWINVERSE', 'PROJECTIONINVERSE', 'WORLDVIEWINVERSE', 'VIEWPROJECTIONINVERSE', 'WORLDVIEWPROJECTIONINVERSE', 'WORLDTRANSPOSE', 'VIEWTRANSPOSE', 'PROJECTIONTRANSPOSE', 'WORLDVIEWTRANSPOSE', 'VIEWPROJECTIONTRANSPOSE', 'WORLDVIEWPROJECTIONTRANSPOSE', 'WORLDINVERSETRANSPOSE', 'VIEWINVERSETRANSPOSE', 'PROJECTIONINVERSETRANSPOSE', 'WORLDVIEWINVERSETRANSPOSE', 'VIEWPROJECTIONINVERSETRANSPOSE', 'WORLDVIEWPROJECTIONINVERSETRANSPOSE' ]; var attributeSizeMap = { // WebGL does not support integer attributes 'vec4': 4, 'vec3': 3, 'vec2': 2, 'float': 1 }; var shaderIDCache = {}; var shaderCodeCache = {}; function getShaderID(vertex, fragment) { var key = 'vertex:' + vertex + 'fragment:' + fragment; if (shaderIDCache[key]) { return shaderIDCache[key]; } var id = util$1.genGUID(); shaderIDCache[key] = id; shaderCodeCache[id] = { vertex: vertex, fragment: fragment }; return id; } function removeComment(code) { return code.replace(/[ \t]*\/\/.*\n/g, '' ) // remove // .replace(/[ \t]*\/\*[\s\S]*?\*\//g, '' ); // remove /* */ } function logSyntaxError() { console.error('Wrong uniform/attributes syntax'); } function parseDeclarations(type, line) { var speratorsRegexp = /[,=\(\):]/; var tokens = line // Convert `symbol: [1,2,3]` to `symbol: vec3(1,2,3)` .replace(/:\s*\[\s*(.*)\s*\]/g, '=' + type + '($1)') .replace(/\s+/g, '') .split(/(?=[,=\(\):])/g); var newTokens = []; for (var i = 0; i < tokens.length; i++) { if (tokens[i].match(speratorsRegexp)) { newTokens.push( tokens[i].charAt(0), tokens[i].slice(1) ); } else { newTokens.push(tokens[i]); } } tokens = newTokens; var TYPE_SYMBOL = 0; var TYPE_ASSIGN = 1; var TYPE_VEC = 2; var TYPE_ARR = 3; var TYPE_SEMANTIC = 4; var TYPE_NORMAL = 5; var opType = TYPE_SYMBOL; var declarations = {}; var declarationValue = null; var currentDeclaration; addSymbol(tokens[0]); function addSymbol(symbol) { if (!symbol) { logSyntaxError(); } var arrResult = symbol.match(/\[(.*?)\]/); currentDeclaration = symbol.replace(/\[(.*?)\]/, ''); declarations[currentDeclaration] = {}; if (arrResult) { declarations[currentDeclaration].isArray = true; declarations[currentDeclaration].arraySize = arrResult[1]; } } for (var i = 1; i < tokens.length; i++) { var token = tokens[i]; if (!token) { // Empty token; continue; } if (token === '=') { if (opType !== TYPE_SYMBOL && opType !== TYPE_ARR) { logSyntaxError(); break; } opType = TYPE_ASSIGN; continue; } else if (token === ':') { opType = TYPE_SEMANTIC; continue; } else if (token === ',') { if (opType === TYPE_VEC) { if (!(declarationValue instanceof Array)) { logSyntaxError(); break; } declarationValue.push(+tokens[++i]); } else { opType = TYPE_NORMAL; } continue; } else if (token === ')') { declarations[currentDeclaration].value = new vendor.Float32Array(declarationValue); declarationValue = null; opType = TYPE_NORMAL; continue; } else if (token === '(') { if (opType !== TYPE_VEC) { logSyntaxError(); break; } if (!(declarationValue instanceof Array)) { logSyntaxError(); break; } declarationValue.push(+tokens[++i]); continue; } else if (token.indexOf('vec') >= 0) { if (opType !== TYPE_ASSIGN // Compatitable with old syntax `symbol: [1,2,3]` && opType !== TYPE_SEMANTIC) { logSyntaxError(); break; } opType = TYPE_VEC; declarationValue = []; continue; } else if (opType === TYPE_ASSIGN) { if (type === 'bool') { declarations[currentDeclaration].value = token === 'true'; } else { declarations[currentDeclaration].value = parseFloat(token); } declarationValue = null; continue; } else if (opType === TYPE_SEMANTIC) { var semantic = token; if (attributeSemantics.indexOf(semantic) >= 0 || uniformSemantics.indexOf(semantic) >= 0 || matrixSemantics.indexOf(semantic) >= 0 ) { declarations[currentDeclaration].semantic = semantic; } else if (semantic === 'ignore' || semantic === 'unconfigurable') { declarations[currentDeclaration].ignore = true; } else { // Try to parse as a default tvalue. if (type === 'bool') { declarations[currentDeclaration].value = semantic === 'true'; } else { declarations[currentDeclaration].value = parseFloat(semantic); } } continue; } // treat as symbol. addSymbol(token); opType = TYPE_SYMBOL; } return declarations; } /** * @constructor * @extends clay.core.Base * @alias clay.Shader * @param {string} vertex * @param {string} fragment * @example * // Create a phong shader * var shader = new clay.Shader( * clay.Shader.source('clay.standard.vertex'), * clay.Shader.source('clay.standard.fragment') * ); */ function Shader(vertex, fragment) { // First argument can be { vertex, fragment } if (typeof vertex === 'object') { fragment = vertex.fragment; vertex = vertex.vertex; } vertex = removeComment(vertex); fragment = removeComment(fragment); this._shaderID = getShaderID(vertex, fragment); this._vertexCode = Shader.parseImport(vertex); this._fragmentCode = Shader.parseImport(fragment); /** * @readOnly */ this.attributeSemantics = {}; /** * @readOnly */ this.matrixSemantics = {}; /** * @readOnly */ this.uniformSemantics = {}; /** * @readOnly */ this.matrixSemanticKeys = []; /** * @readOnly */ this.uniformTemplates = {}; /** * @readOnly */ this.attributes = {}; /** * @readOnly */ this.textures = {}; /** * @readOnly */ this.vertexDefines = {}; /** * @readOnly */ this.fragmentDefines = {}; this._parseAttributes(); this._parseUniforms(); this._parseDefines(); } Shader.prototype = { constructor: Shader, // Create a new uniform instance for material createUniforms: function () { var uniforms = {}; for (var symbol in this.uniformTemplates){ var uniformTpl = this.uniformTemplates[symbol]; uniforms[symbol] = { type: uniformTpl.type, value: uniformTpl.value() }; } return uniforms; }, _parseImport: function () { this._vertexCode = Shader.parseImport(this.vertex); this._fragmentCode = Shader.parseImport(this.fragment); }, _addSemanticUniform: function (symbol, uniformType, semantic) { // This case is only for SKIN_MATRIX // TODO if (attributeSemantics.indexOf(semantic) >= 0) { this.attributeSemantics[semantic] = { symbol: symbol, type: uniformType }; } else if (matrixSemantics.indexOf(semantic) >= 0) { var isTranspose = false; var semanticNoTranspose = semantic; if (semantic.match(/TRANSPOSE$/)) { isTranspose = true; semanticNoTranspose = semantic.slice(0, -9); } this.matrixSemantics[semantic] = { symbol: symbol, type: uniformType, isTranspose: isTranspose, semanticNoTranspose: semanticNoTranspose }; } else if (uniformSemantics.indexOf(semantic) >= 0) { this.uniformSemantics[semantic] = { symbol: symbol, type: uniformType }; } }, _addMaterialUniform: function (symbol, type, uniformType, defaultValueFunc, isArray, materialUniforms) { materialUniforms[symbol] = { type: uniformType, value: isArray ? uniformValueConstructor['array'] : (defaultValueFunc || uniformValueConstructor[type]), semantic: null }; }, _parseUniforms: function () { var uniforms = {}; var self = this; var shaderType = 'vertex'; this._uniformList = []; this._vertexCode = this._vertexCode.replace(uniformRegex, _uniformParser); shaderType = 'fragment'; this._fragmentCode = this._fragmentCode.replace(uniformRegex, _uniformParser); self.matrixSemanticKeys = Object.keys(this.matrixSemantics); function makeDefaultValueFunc(value) { return value != null ? function () { return value; } : null; } function _uniformParser(str, type, content) { var declaredUniforms = parseDeclarations(type, content); var uniformMainStr = []; for (var symbol in declaredUniforms) { var uniformInfo = declaredUniforms[symbol]; var semantic = uniformInfo.semantic; var tmpStr = symbol; var uniformType = uniformTypeMap[type]; var defaultValueFunc = makeDefaultValueFunc(declaredUniforms[symbol].value); if (declaredUniforms[symbol].isArray) { tmpStr += '[' + declaredUniforms[symbol].arraySize + ']'; uniformType += 'v'; } uniformMainStr.push(tmpStr); self._uniformList.push(symbol); if (!uniformInfo.ignore) { if (type === 'sampler2D' || type === 'samplerCube') { // Texture is default disabled self.textures[symbol] = { shaderType: shaderType, type: type }; } if (semantic) { // TODO Should not declare multiple symbols if have semantic. self._addSemanticUniform(symbol, uniformType, semantic); } else { self._addMaterialUniform( symbol, type, uniformType, defaultValueFunc, declaredUniforms[symbol].isArray, uniforms ); } } } return uniformMainStr.length > 0 ? 'uniform ' + type + ' ' + uniformMainStr.join(',') + ';\n' : ''; } this.uniformTemplates = uniforms; }, _parseAttributes: function () { var attributes = {}; var self = this; this._vertexCode = this._vertexCode.replace(attributeRegex, _attributeParser); function _attributeParser(str, type, content) { var declaredAttributes = parseDeclarations(type, content); var size = attributeSizeMap[type] || 1; var attributeMainStr = []; for (var symbol in declaredAttributes) { var semantic = declaredAttributes[symbol].semantic; attributes[symbol] = { // TODO Can only be float type: 'float', size: size, semantic: semantic || null }; // TODO Should not declare multiple symbols if have semantic. if (semantic) { if (attributeSemantics.indexOf(semantic) < 0) { throw new Error('Unkown semantic "' + semantic + '"'); } else { self.attributeSemantics[semantic] = { symbol: symbol, type: type }; } } attributeMainStr.push(symbol); } return 'attribute ' + type + ' ' + attributeMainStr.join(',') + ';\n'; } this.attributes = attributes; }, _parseDefines: function () { var self = this; var shaderType = 'vertex'; this._vertexCode = this._vertexCode.replace(defineRegex, _defineParser); shaderType = 'fragment'; this._fragmentCode = this._fragmentCode.replace(defineRegex, _defineParser); function _defineParser(str, symbol, value) { var defines = shaderType === 'vertex' ? self.vertexDefines : self.fragmentDefines; if (!defines[symbol]) { // Haven't been defined by user if (value === 'false') { defines[symbol] = false; } else if (value === 'true') { defines[symbol] = true; } else { defines[symbol] = value // If can parse to float ? (isNaN(parseFloat(value)) ? value.trim() : parseFloat(value)) : null; } } return ''; } }, /** * Clone a new shader * @return {clay.Shader} */ clone: function () { var code = shaderCodeCache[this._shaderID]; var shader = new Shader(code.vertex, code.fragment); return shader; } }; if (Object.defineProperty) { Object.defineProperty(Shader.prototype, 'shaderID', { get: function () { return this._shaderID; } }); Object.defineProperty(Shader.prototype, 'vertex', { get: function () { return this._vertexCode; } }); Object.defineProperty(Shader.prototype, 'fragment', { get: function () { return this._fragmentCode; } }); Object.defineProperty(Shader.prototype, 'uniforms', { get: function () { return this._uniformList; } }); } var importRegex = /(@import)\s*([0-9a-zA-Z_\-\.]*)/g; Shader.parseImport = function (shaderStr) { shaderStr = shaderStr.replace(importRegex, function (str, importSymbol, importName) { var str = Shader.source(importName); if (str) { // Recursively parse return Shader.parseImport(str); } else { console.error('Shader chunk "' + importName + '" not existed in library'); return ''; } }); return shaderStr; }; var exportRegex = /(@export)\s*([0-9a-zA-Z_\-\.]*)\s*\n([\s\S]*?)@end/g; /** * Import shader source * @param {string} shaderStr * @memberOf clay.Shader */ Shader['import'] = function (shaderStr) { shaderStr.replace(exportRegex, function (str, exportSymbol, exportName, code) { var code = code.replace(/(^[\s\t\xa0\u3000]+)|([\u3000\xa0\s\t]+\x24)/g, ''); if (code) { var parts = exportName.split('.'); var obj = Shader.codes; var i = 0; var key; while (i < parts.length - 1) { key = parts[i++]; if (!obj[key]) { obj[key] = {}; } obj = obj[key]; } key = parts[i]; obj[key] = code; } return code; }); }; /** * Library to store all the loaded shader codes * @type {Object} * @readOnly * @memberOf clay.Shader */ Shader.codes = {}; /** * Get shader source * @param {string} name * @return {string} */ Shader.source = function (name) { var parts = name.split('.'); var obj = Shader.codes; var i = 0; while (obj && i < parts.length) { var key = parts[i++]; obj = obj[key]; } if (typeof obj !== 'string') { // FIXME Use default instead console.error('Shader "' + name + '" not existed in library'); return ''; } return obj; }; var prezGlsl = "@export clay.prez.vertex\nuniform mat4 WVP : WORLDVIEWPROJECTION;\nattribute vec3 pos : POSITION;\nattribute vec2 uv : TEXCOORD_0;\nuniform vec2 uvRepeat : [1.0, 1.0];\nuniform vec2 uvOffset : [0.0, 0.0];\n@import clay.chunk.skinning_header\n@import clay.chunk.instancing_header\nvarying vec2 v_Texcoord;\nvoid main()\n{\n vec4 P = vec4(pos, 1.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n P = skinMatrixWS * P;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n P = instanceMat * P;\n#endif\n gl_Position = WVP * P;\n v_Texcoord = uv * uvRepeat + uvOffset;\n}\n@end\n@export clay.prez.fragment\nuniform sampler2D alphaMap;\nuniform float alphaCutoff: 0.0;\nvarying vec2 v_Texcoord;\nvoid main()\n{\n if (alphaCutoff > 0.0) {\n if (texture2D(alphaMap, v_Texcoord).a <= alphaCutoff) {\n discard;\n }\n }\n gl_FragColor = vec4(0.0,0.0,0.0,1.0);\n}\n@end"; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 4x4 Matrix * @name mat4 */ var mat4 = {}; /** * Creates a new identity mat4 * * @returns {mat4} a new 4x4 matrix */ mat4.create = function() { var out = new GLMAT_ARRAY_TYPE(16); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Creates a new mat4 initialized with values from an existing matrix * * @param {mat4} a matrix to clone * @returns {mat4} a new 4x4 matrix */ mat4.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(16); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Copy the values from one mat4 to another * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Set a mat4 to the identity matrix * * @param {mat4} out the receiving matrix * @returns {mat4} out */ mat4.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Transpose the values of a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.transpose = function(out, a) { // If we are transposing ourselves we can skip a few steps but have to cache some values if (out === a) { var a01 = a[1], a02 = a[2], a03 = a[3], a12 = a[6], a13 = a[7], a23 = a[11]; out[1] = a[4]; out[2] = a[8]; out[3] = a[12]; out[4] = a01; out[6] = a[9]; out[7] = a[13]; out[8] = a02; out[9] = a12; out[11] = a[14]; out[12] = a03; out[13] = a13; out[14] = a23; } else { out[0] = a[0]; out[1] = a[4]; out[2] = a[8]; out[3] = a[12]; out[4] = a[1]; out[5] = a[5]; out[6] = a[9]; out[7] = a[13]; out[8] = a[2]; out[9] = a[6]; out[10] = a[10]; out[11] = a[14]; out[12] = a[3]; out[13] = a[7]; out[14] = a[11]; out[15] = a[15]; } return out; }; /** * Inverts a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.invert = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32, // Calculate the determinant det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return null; } det = 1.0 / det; out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return out; }; /** * Calculates the adjugate of a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.adjoint = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22)); out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22)); out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12)); out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12)); out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22)); out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22)); out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12)); out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12)); out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21)); out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21)); out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11)); out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11)); out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21)); out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21)); out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11)); out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11)); return out; }; /** * Calculates the determinant of a mat4 * * @param {mat4} a the source matrix * @returns {Number} determinant of a */ mat4.determinant = function (a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32; // Calculate the determinant return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; }; /** * Multiplies two mat4's * * @param {mat4} out the receiving matrix * @param {mat4} a the first operand * @param {mat4} b the second operand * @returns {mat4} out */ mat4.multiply = function (out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; // Cache only the current line of the second matrix var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; return out; }; /** * Multiplies two affine mat4's * Add by https://github.com/pissang * @param {mat4} out the receiving matrix * @param {mat4} a the first operand * @param {mat4} b the second operand * @returns {mat4} out */ mat4.multiplyAffine = function (out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a10 = a[4], a11 = a[5], a12 = a[6], a20 = a[8], a21 = a[9], a22 = a[10], a30 = a[12], a31 = a[13], a32 = a[14]; // Cache only the current line of the second matrix var b0 = b[0], b1 = b[1], b2 = b[2]; out[0] = b0*a00 + b1*a10 + b2*a20; out[1] = b0*a01 + b1*a11 + b2*a21; out[2] = b0*a02 + b1*a12 + b2*a22; // out[3] = 0; b0 = b[4]; b1 = b[5]; b2 = b[6]; out[4] = b0*a00 + b1*a10 + b2*a20; out[5] = b0*a01 + b1*a11 + b2*a21; out[6] = b0*a02 + b1*a12 + b2*a22; // out[7] = 0; b0 = b[8]; b1 = b[9]; b2 = b[10]; out[8] = b0*a00 + b1*a10 + b2*a20; out[9] = b0*a01 + b1*a11 + b2*a21; out[10] = b0*a02 + b1*a12 + b2*a22; // out[11] = 0; b0 = b[12]; b1 = b[13]; b2 = b[14]; out[12] = b0*a00 + b1*a10 + b2*a20 + a30; out[13] = b0*a01 + b1*a11 + b2*a21 + a31; out[14] = b0*a02 + b1*a12 + b2*a22 + a32; // out[15] = 1; return out; }; /** * Alias for {@link mat4.multiply} * @function */ mat4.mul = mat4.multiply; /** * Alias for {@link mat4.multiplyAffine} * @function */ mat4.mulAffine = mat4.multiplyAffine; /** * Translate a mat4 by the given vector * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to translate * @param {vec3} v vector to translate by * @returns {mat4} out */ mat4.translate = function (out, a, v) { var x = v[0], y = v[1], z = v[2], a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23; if (a === out) { out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; } else { a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; out[12] = a00 * x + a10 * y + a20 * z + a[12]; out[13] = a01 * x + a11 * y + a21 * z + a[13]; out[14] = a02 * x + a12 * y + a22 * z + a[14]; out[15] = a03 * x + a13 * y + a23 * z + a[15]; } return out; }; /** * Scales the mat4 by the dimensions in the given vec3 * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to scale * @param {vec3} v the vec3 to scale the matrix by * @returns {mat4} out **/ mat4.scale = function(out, a, v) { var x = v[0], y = v[1], z = v[2]; out[0] = a[0] * x; out[1] = a[1] * x; out[2] = a[2] * x; out[3] = a[3] * x; out[4] = a[4] * y; out[5] = a[5] * y; out[6] = a[6] * y; out[7] = a[7] * y; out[8] = a[8] * z; out[9] = a[9] * z; out[10] = a[10] * z; out[11] = a[11] * z; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Rotates a mat4 by the given angle * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @param {vec3} axis the axis to rotate around * @returns {mat4} out */ mat4.rotate = function (out, a, rad, axis) { var x = axis[0], y = axis[1], z = axis[2], len = Math.sqrt(x * x + y * y + z * z), s, c, t, a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, b00, b01, b02, b10, b11, b12, b20, b21, b22; if (Math.abs(len) < GLMAT_EPSILON) { return null; } len = 1 / len; x *= len; y *= len; z *= len; s = Math.sin(rad); c = Math.cos(rad); t = 1 - c; a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; // Construct the elements of the rotation matrix b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s; b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s; b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c; // Perform rotation-specific matrix multiplication out[0] = a00 * b00 + a10 * b01 + a20 * b02; out[1] = a01 * b00 + a11 * b01 + a21 * b02; out[2] = a02 * b00 + a12 * b01 + a22 * b02; out[3] = a03 * b00 + a13 * b01 + a23 * b02; out[4] = a00 * b10 + a10 * b11 + a20 * b12; out[5] = a01 * b10 + a11 * b11 + a21 * b12; out[6] = a02 * b10 + a12 * b11 + a22 * b12; out[7] = a03 * b10 + a13 * b11 + a23 * b12; out[8] = a00 * b20 + a10 * b21 + a20 * b22; out[9] = a01 * b20 + a11 * b21 + a21 * b22; out[10] = a02 * b20 + a12 * b21 + a22 * b22; out[11] = a03 * b20 + a13 * b21 + a23 * b22; if (a !== out) { // If the source and destination differ, copy the unchanged last row out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } return out; }; /** * Rotates a matrix by the given angle around the X axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateX = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; if (a !== out) { // If the source and destination differ, copy the unchanged rows out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[4] = a10 * c + a20 * s; out[5] = a11 * c + a21 * s; out[6] = a12 * c + a22 * s; out[7] = a13 * c + a23 * s; out[8] = a20 * c - a10 * s; out[9] = a21 * c - a11 * s; out[10] = a22 * c - a12 * s; out[11] = a23 * c - a13 * s; return out; }; /** * Rotates a matrix by the given angle around the Y axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateY = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; if (a !== out) { // If the source and destination differ, copy the unchanged rows out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[0] = a00 * c - a20 * s; out[1] = a01 * c - a21 * s; out[2] = a02 * c - a22 * s; out[3] = a03 * c - a23 * s; out[8] = a00 * s + a20 * c; out[9] = a01 * s + a21 * c; out[10] = a02 * s + a22 * c; out[11] = a03 * s + a23 * c; return out; }; /** * Rotates a matrix by the given angle around the Z axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateZ = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; if (a !== out) { // If the source and destination differ, copy the unchanged last row out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[0] = a00 * c + a10 * s; out[1] = a01 * c + a11 * s; out[2] = a02 * c + a12 * s; out[3] = a03 * c + a13 * s; out[4] = a10 * c - a00 * s; out[5] = a11 * c - a01 * s; out[6] = a12 * c - a02 * s; out[7] = a13 * c - a03 * s; return out; }; /** * Creates a matrix from a quaternion rotation and vector translation * This is equivalent to (but much faster than): * * mat4.identity(dest); * mat4.translate(dest, vec); * var quatMat = mat4.create(); * quat4.toMat4(quat, quatMat); * mat4.multiply(dest, quatMat); * * @param {mat4} out mat4 receiving operation result * @param {quat4} q Rotation quaternion * @param {vec3} v Translation vector * @returns {mat4} out */ mat4.fromRotationTranslation = function (out, q, v) { // Quaternion math var x = q[0], y = q[1], z = q[2], w = q[3], x2 = x + x, y2 = y + y, z2 = z + z, xx = x * x2, xy = x * y2, xz = x * z2, yy = y * y2, yz = y * z2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2; out[0] = 1 - (yy + zz); out[1] = xy + wz; out[2] = xz - wy; out[3] = 0; out[4] = xy - wz; out[5] = 1 - (xx + zz); out[6] = yz + wx; out[7] = 0; out[8] = xz + wy; out[9] = yz - wx; out[10] = 1 - (xx + yy); out[11] = 0; out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; out[15] = 1; return out; }; mat4.fromQuat = function (out, q) { var x = q[0], y = q[1], z = q[2], w = q[3], x2 = x + x, y2 = y + y, z2 = z + z, xx = x * x2, yx = y * x2, yy = y * y2, zx = z * x2, zy = z * y2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2; out[0] = 1 - yy - zz; out[1] = yx + wz; out[2] = zx - wy; out[3] = 0; out[4] = yx - wz; out[5] = 1 - xx - zz; out[6] = zy + wx; out[7] = 0; out[8] = zx + wy; out[9] = zy - wx; out[10] = 1 - xx - yy; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Generates a frustum matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {Number} left Left bound of the frustum * @param {Number} right Right bound of the frustum * @param {Number} bottom Bottom bound of the frustum * @param {Number} top Top bound of the frustum * @param {Number} near Near bound of the frustum * @param {Number} far Far bound of the frustum * @returns {mat4} out */ mat4.frustum = function (out, left, right, bottom, top, near, far) { var rl = 1 / (right - left), tb = 1 / (top - bottom), nf = 1 / (near - far); out[0] = (near * 2) * rl; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = (near * 2) * tb; out[6] = 0; out[7] = 0; out[8] = (right + left) * rl; out[9] = (top + bottom) * tb; out[10] = (far + near) * nf; out[11] = -1; out[12] = 0; out[13] = 0; out[14] = (far * near * 2) * nf; out[15] = 0; return out; }; /** * Generates a perspective projection matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {number} fovy Vertical field of view in radians * @param {number} aspect Aspect ratio. typically viewport width/height * @param {number} near Near bound of the frustum * @param {number} far Far bound of the frustum * @returns {mat4} out */ mat4.perspective = function (out, fovy, aspect, near, far) { var f = 1.0 / Math.tan(fovy / 2), nf = 1 / (near - far); out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = (far + near) * nf; out[11] = -1; out[12] = 0; out[13] = 0; out[14] = (2 * far * near) * nf; out[15] = 0; return out; }; /** * Generates a orthogonal projection matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {number} left Left bound of the frustum * @param {number} right Right bound of the frustum * @param {number} bottom Bottom bound of the frustum * @param {number} top Top bound of the frustum * @param {number} near Near bound of the frustum * @param {number} far Far bound of the frustum * @returns {mat4} out */ mat4.ortho = function (out, left, right, bottom, top, near, far) { var lr = 1 / (left - right), bt = 1 / (bottom - top), nf = 1 / (near - far); out[0] = -2 * lr; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = -2 * bt; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 2 * nf; out[11] = 0; out[12] = (left + right) * lr; out[13] = (top + bottom) * bt; out[14] = (far + near) * nf; out[15] = 1; return out; }; /** * Generates a look-at matrix with the given eye position, focal point, and up axis * * @param {mat4} out mat4 frustum matrix will be written into * @param {vec3} eye Position of the viewer * @param {vec3} center Point the viewer is looking at * @param {vec3} up vec3 pointing up * @returns {mat4} out */ mat4.lookAt = function (out, eye, center, up) { var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, eyex = eye[0], eyey = eye[1], eyez = eye[2], upx = up[0], upy = up[1], upz = up[2], centerx = center[0], centery = center[1], centerz = center[2]; if (Math.abs(eyex - centerx) < GLMAT_EPSILON && Math.abs(eyey - centery) < GLMAT_EPSILON && Math.abs(eyez - centerz) < GLMAT_EPSILON) { return mat4.identity(out); } z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); z0 *= len; z1 *= len; z2 *= len; x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); out[15] = 1; return out; }; /** * Returns Frobenius norm of a mat4 * * @param {mat4} a the matrix to calculate Frobenius norm of * @returns {Number} Frobenius norm */ mat4.frob = function (a) { return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + Math.pow(a[6], 2) + Math.pow(a[7], 2) + Math.pow(a[8], 2) + Math.pow(a[9], 2) + Math.pow(a[10], 2) + Math.pow(a[11], 2) + Math.pow(a[12], 2) + Math.pow(a[13], 2) + Math.pow(a[14], 2) + Math.pow(a[15], 2) )) }; // TODO Resources like shader, texture, geometry reference management // Trace and find out which shader, texture, geometry can be destroyed // Light header Shader['import'](prezGlsl); var mat4Create = mat4.create; var errorShader = {}; function defaultGetMaterial(renderable) { return renderable.material; } function defaultGetUniform(renderable, material, symbol) { return material.uniforms[symbol].value; } function defaultIsMaterialChanged(renderabled, prevRenderable, material, prevMaterial) { return material !== prevMaterial; } function defaultIfRender(renderable) { return true; } function noop$1() {} var attributeBufferTypeMap = { float: glenum.FLOAT, byte: glenum.BYTE, ubyte: glenum.UNSIGNED_BYTE, short: glenum.SHORT, ushort: glenum.UNSIGNED_SHORT }; function VertexArrayObject(availableAttributes, availableAttributeSymbols, indicesBuffer) { this.availableAttributes = availableAttributes; this.availableAttributeSymbols = availableAttributeSymbols; this.indicesBuffer = indicesBuffer; this.vao = null; } function PlaceHolderTexture(renderer) { var blankCanvas; var webglTexture; this.bind = function (renderer) { if (!blankCanvas) { // TODO Environment not support createCanvas. blankCanvas = vendor.createCanvas(); blankCanvas.width = blankCanvas.height = 1; blankCanvas.getContext('2d'); } var gl = renderer.gl; var firstBind = !webglTexture; if (firstBind) { webglTexture = gl.createTexture(); } gl.bindTexture(gl.TEXTURE_2D, webglTexture); if (firstBind) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, blankCanvas); } }; this.unbind = function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_2D, null); }; this.isRenderable = function () { return true; }; } /** * @constructor clay.Renderer * @extends clay.core.Base */ var Renderer = Base.extend(function () { return /** @lends clay.Renderer# */ { /** * @type {HTMLCanvasElement} * @readonly */ canvas: null, /** * Canvas width, set by resize method * @type {number} * @private */ _width: 100, /** * Canvas width, set by resize method * @type {number} * @private */ _height: 100, /** * Device pixel ratio, set by setDevicePixelRatio method * Specially for high defination display * @see http://www.khronos.org/webgl/wiki/HandlingHighDPI * @type {number} * @private */ devicePixelRatio: (typeof window !== 'undefined' && window.devicePixelRatio) || 1.0, /** * Clear color * @type {number[]} */ clearColor: [0.0, 0.0, 0.0, 0.0], /** * Default: * _gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT | _gl.STENCIL_BUFFER_BIT * @type {number} */ clearBit: 17664, // Settings when getting context // http://www.khronos.org/registry/webgl/specs/latest/#2.4 /** * If enable alpha, default true * @type {boolean} */ alpha: true, /** * If enable depth buffer, default true * @type {boolean} */ depth: true, /** * If enable stencil buffer, default false * @type {boolean} */ stencil: false, /** * If enable antialias, default true * @type {boolean} */ antialias: true, /** * If enable premultiplied alpha, default true * @type {boolean} */ premultipliedAlpha: true, /** * If preserve drawing buffer, default false * @type {boolean} */ preserveDrawingBuffer: false, /** * If throw context error, usually turned on in debug mode * @type {boolean} */ throwError: true, /** * WebGL Context created from given canvas * @type {WebGLRenderingContext} */ gl: null, /** * Renderer viewport, read-only, can be set by setViewport method * @type {Object} */ viewport: {}, /** * Max joint number * @type {number} */ maxJointNumber: 20, // Set by FrameBuffer#bind __currentFrameBuffer: null, _viewportStack: [], _clearStack: [], _sceneRendering: null }; }, function () { if (!this.canvas) { this.canvas = vendor.createCanvas(); } var canvas = this.canvas; try { var opts = { alpha: this.alpha, depth: this.depth, stencil: this.stencil, antialias: this.antialias, premultipliedAlpha: this.premultipliedAlpha, preserveDrawingBuffer: this.preserveDrawingBuffer }; this.gl = canvas.getContext('webgl', opts) || canvas.getContext('experimental-webgl', opts); if (!this.gl) { throw new Error(); } this._glinfo = new GLInfo(this.gl); if (this.gl.targetRenderer) { console.error('Already created a renderer'); } this.gl.targetRenderer = this; this.resize(); } catch (e) { throw 'Error creating WebGL Context ' + e; } // Init managers this._programMgr = new ProgramManager(this); this._placeholderTexture = new PlaceHolderTexture(this); }, /** @lends clay.Renderer.prototype. **/ { /** * Resize the canvas * @param {number} width * @param {number} height */ resize: function(width, height) { var canvas = this.canvas; // http://www.khronos.org/webgl/wiki/HandlingHighDPI // set the display size of the canvas. var dpr = this.devicePixelRatio; if (width != null) { if (canvas.style) { canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; } // set the size of the drawingBuffer canvas.width = width * dpr; canvas.height = height * dpr; this._width = width; this._height = height; } else { this._width = canvas.width / dpr; this._height = canvas.height / dpr; } this.setViewport(0, 0, this._width, this._height); }, /** * Get renderer width * @return {number} */ getWidth: function () { return this._width; }, /** * Get renderer height * @return {number} */ getHeight: function () { return this._height; }, /** * Get viewport aspect, * @return {number} */ getViewportAspect: function () { var viewport = this.viewport; return viewport.width / viewport.height; }, /** * Set devicePixelRatio * @param {number} devicePixelRatio */ setDevicePixelRatio: function(devicePixelRatio) { this.devicePixelRatio = devicePixelRatio; this.resize(this._width, this._height); }, /** * Get devicePixelRatio * @param {number} devicePixelRatio */ getDevicePixelRatio: function () { return this.devicePixelRatio; }, /** * Get WebGL extension * @param {string} name * @return {object} */ getGLExtension: function (name) { return this._glinfo.getExtension(name); }, /** * Get WebGL parameter * @param {string} name * @return {*} */ getGLParameter: function (name) { return this._glinfo.getParameter(name); }, /** * Set rendering viewport * @param {number|Object} x * @param {number} [y] * @param {number} [width] * @param {number} [height] * @param {number} [devicePixelRatio] * Defaultly use the renderere devicePixelRatio * It needs to be 1 when setViewport is called by frameBuffer * * @example * setViewport(0,0,width,height,1) * setViewport({ * x: 0, * y: 0, * width: width, * height: height, * devicePixelRatio: 1 * }) */ setViewport: function (x, y, width, height, dpr) { if (typeof x === 'object') { var obj = x; x = obj.x; y = obj.y; width = obj.width; height = obj.height; dpr = obj.devicePixelRatio; } dpr = dpr || this.devicePixelRatio; this.gl.viewport( x * dpr, y * dpr, width * dpr, height * dpr ); // Use a fresh new object, not write property. this.viewport = { x: x, y: y, width: width, height: height, devicePixelRatio: dpr }; }, /** * Push current viewport into a stack */ saveViewport: function () { this._viewportStack.push(this.viewport); }, /** * Pop viewport from stack, restore in the renderer */ restoreViewport: function () { if (this._viewportStack.length > 0) { this.setViewport(this._viewportStack.pop()); } }, /** * Push current clear into a stack */ saveClear: function () { this._clearStack.push({ clearBit: this.clearBit, clearColor: this.clearColor }); }, /** * Pop clear from stack, restore in the renderer */ restoreClear: function () { if (this._clearStack.length > 0) { var opt = this._clearStack.pop(); this.clearColor = opt.clearColor; this.clearBit = opt.clearBit; } }, bindSceneRendering: function (scene) { this._sceneRendering = scene; }, /** * Render the scene in camera to the screen or binded offline framebuffer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {boolean} [notUpdateScene] If not call the scene.update methods in the rendering, default true * @param {boolean} [preZ] If use preZ optimization, default false * @return {IRenderInfo} */ render: function(scene, camera, notUpdateScene, preZ) { var _gl = this.gl; var clearColor = this.clearColor; if (this.clearBit) { // Must set depth and color mask true before clear _gl.colorMask(true, true, true, true); _gl.depthMask(true); var viewport = this.viewport; var needsScissor = false; var viewportDpr = viewport.devicePixelRatio; if (viewport.width !== this._width || viewport.height !== this._height || (viewportDpr && viewportDpr !== this.devicePixelRatio) || viewport.x || viewport.y ) { needsScissor = true; // http://stackoverflow.com/questions/11544608/how-to-clear-a-rectangle-area-in-webgl // Only clear the viewport _gl.enable(_gl.SCISSOR_TEST); _gl.scissor(viewport.x * viewportDpr, viewport.y * viewportDpr, viewport.width * viewportDpr, viewport.height * viewportDpr); } _gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); _gl.clear(this.clearBit); if (needsScissor) { _gl.disable(_gl.SCISSOR_TEST); } } // If the scene have been updated in the prepass like shadow map // There is no need to update it again if (!notUpdateScene) { scene.update(false); } scene.updateLights(); camera = camera || scene.getMainCamera(); if (!camera) { console.error('Can\'t find camera in the scene.'); return; } camera.update(); var renderList = scene.updateRenderList(camera, true); this._sceneRendering = scene; var opaqueList = renderList.opaque; var transparentList = renderList.transparent; var sceneMaterial = scene.material; scene.trigger('beforerender', this, scene, camera, renderList); // Render pre z if (preZ) { this.renderPreZ(opaqueList, scene, camera); _gl.depthFunc(_gl.LEQUAL); } else { _gl.depthFunc(_gl.LESS); } // Update the depth of transparent list. var worldViewMat = mat4Create(); var posViewSpace = vec3.create(); for (var i = 0; i < transparentList.length; i++) { var renderable = transparentList[i]; mat4.multiplyAffine(worldViewMat, camera.viewMatrix.array, renderable.worldTransform.array); vec3.transformMat4(posViewSpace, renderable.position.array, worldViewMat); renderable.__depth = posViewSpace[2]; } // Render opaque list this.renderPass(opaqueList, camera, { getMaterial: function (renderable) { return sceneMaterial || renderable.material; }, sortCompare: this.opaqueSortCompare }); this.renderPass(transparentList, camera, { getMaterial: function (renderable) { return sceneMaterial || renderable.material; }, sortCompare: this.transparentSortCompare }); scene.trigger('afterrender', this, scene, camera, renderList); // Cleanup this._sceneRendering = null; }, getProgram: function (renderable, renderMaterial, scene) { renderMaterial = renderMaterial || renderable.material; return this._programMgr.getProgram(renderable, renderMaterial, scene); }, validateProgram: function (program) { if (program.__error) { var errorMsg = program.__error; if (errorShader[program.__uid__]) { return; } errorShader[program.__uid__] = true; if (this.throwError) { throw new Error(errorMsg); } else { this.trigger('error', errorMsg); } } }, updatePrograms: function (list, scene, passConfig) { var getMaterial = (passConfig && passConfig.getMaterial) || defaultGetMaterial; scene = scene || null; for (var i = 0; i < list.length; i++) { var renderable = list[i]; var renderMaterial = getMaterial.call(this, renderable); if (i > 0) { var prevRenderable = list[i - 1]; var prevJointsLen = prevRenderable.joints ? prevRenderable.joints.length : 0; var jointsLen = renderable.joints ? renderable.joints.length : 0; // Keep program not change if joints, material, lightGroup are same of two renderables. if (jointsLen === prevJointsLen && renderable.material === prevRenderable.material && renderable.lightGroup === prevRenderable.lightGroup ) { renderable.__program = prevRenderable.__program; continue; } } var program = this._programMgr.getProgram(renderable, renderMaterial, scene); this.validateProgram(program); renderable.__program = program; } }, /** * Render a single renderable list in camera in sequence * @param {clay.Renderable[]} list List of all renderables. * @param {clay.Camera} [camera] Camera provide view matrix and porjection matrix. It can be null. * @param {Object} [passConfig] * @param {Function} [passConfig.getMaterial] Get renderable material. * @param {Function} [passConfig.getUniform] Get material uniform value. * @param {Function} [passConfig.isMaterialChanged] If material changed. * @param {Function} [passConfig.beforeRender] Before render each renderable. * @param {Function} [passConfig.afterRender] After render each renderable * @param {Function} [passConfig.ifRender] If render the renderable. * @param {Function} [passConfig.sortCompare] Sort compare function. * @return {IRenderInfo} */ renderPass: function(list, camera, passConfig) { this.trigger('beforerenderpass', this, list, camera, passConfig); passConfig = passConfig || {}; passConfig.getMaterial = passConfig.getMaterial || defaultGetMaterial; passConfig.getUniform = passConfig.getUniform || defaultGetUniform; // PENDING Better solution? passConfig.isMaterialChanged = passConfig.isMaterialChanged || defaultIsMaterialChanged; passConfig.beforeRender = passConfig.beforeRender || noop$1; passConfig.afterRender = passConfig.afterRender || noop$1; var ifRenderObject = passConfig.ifRender || defaultIfRender; this.updatePrograms(list, this._sceneRendering, passConfig); if (passConfig.sortCompare) { list.sort(passConfig.sortCompare); } // Some common builtin uniforms var viewport = this.viewport; var vDpr = viewport.devicePixelRatio; var viewportUniform = [ viewport.x * vDpr, viewport.y * vDpr, viewport.width * vDpr, viewport.height * vDpr ]; var windowDpr = this.devicePixelRatio; var windowSizeUniform = this.__currentFrameBuffer ? [this.__currentFrameBuffer.getTextureWidth(), this.__currentFrameBuffer.getTextureHeight()] : [this._width * windowDpr, this._height * windowDpr]; // DEPRECATED var viewportSizeUniform = [ viewportUniform[2], viewportUniform[3] ]; var time = Date.now(); // Calculate view and projection matrix if (camera) { mat4.copy(matrices.VIEW, camera.viewMatrix.array); mat4.copy(matrices.PROJECTION, camera.projectionMatrix.array); mat4.copy(matrices.VIEWINVERSE, camera.worldTransform.array); } else { mat4.identity(matrices.VIEW); mat4.identity(matrices.PROJECTION); mat4.identity(matrices.VIEWINVERSE); } mat4.multiply(matrices.VIEWPROJECTION, matrices.PROJECTION, matrices.VIEW); mat4.invert(matrices.PROJECTIONINVERSE, matrices.PROJECTION); mat4.invert(matrices.VIEWPROJECTIONINVERSE, matrices.VIEWPROJECTION); var _gl = this.gl; var scene = this._sceneRendering; var prevMaterial; var prevProgram; var prevRenderable; // Status var depthTest, depthMask; var culling, cullFace, frontFace; var transparent; var drawID; var currentVAO; var materialTakesTextureSlot; // var vaoExt = this.getGLExtension('OES_vertex_array_object'); // not use vaoExt, some platforms may mess it up. var vaoExt = null; for (var i = 0; i < list.length; i++) { var renderable = list[i]; var isSceneNode = renderable.worldTransform != null; var worldM; if (!ifRenderObject(renderable)) { continue; } // Skinned mesh will transformed to joint space. Ignore the mesh transform if (isSceneNode) { worldM = (renderable.isSkinnedMesh && renderable.isSkinnedMesh()) // TODO ? (renderable.offsetMatrix ? renderable.offsetMatrix.array :matrices.IDENTITY) : renderable.worldTransform.array; } var geometry = renderable.geometry; var material = passConfig.getMaterial.call(this, renderable); var program = renderable.__program; var shader = material.shader; var currentDrawID = geometry.__uid__ + '-' + program.__uid__; var drawIDChanged = currentDrawID !== drawID; drawID = currentDrawID; if (drawIDChanged && vaoExt) { // TODO Seems need to be bound to null immediately (or before bind another program?) if vao is changed vaoExt.bindVertexArrayOES(null); } if (isSceneNode) { mat4.copy(matrices.WORLD, worldM); mat4.multiply(matrices.WORLDVIEWPROJECTION, matrices.VIEWPROJECTION, worldM); mat4.multiplyAffine(matrices.WORLDVIEW, matrices.VIEW, worldM); if (shader.matrixSemantics.WORLDINVERSE || shader.matrixSemantics.WORLDINVERSETRANSPOSE) { mat4.invert(matrices.WORLDINVERSE, worldM); } if (shader.matrixSemantics.WORLDVIEWINVERSE || shader.matrixSemantics.WORLDVIEWINVERSETRANSPOSE) { mat4.invert(matrices.WORLDVIEWINVERSE, matrices.WORLDVIEW); } if (shader.matrixSemantics.WORLDVIEWPROJECTIONINVERSE || shader.matrixSemantics.WORLDVIEWPROJECTIONINVERSETRANSPOSE) { mat4.invert(matrices.WORLDVIEWPROJECTIONINVERSE, matrices.WORLDVIEWPROJECTION); } } // Before render hook renderable.beforeRender && renderable.beforeRender(this); passConfig.beforeRender.call(this, renderable, material, prevMaterial); var programChanged = program !== prevProgram; if (programChanged) { // Set lights number program.bind(this); // Set some common uniforms program.setUniformOfSemantic(_gl, 'VIEWPORT', viewportUniform); program.setUniformOfSemantic(_gl, 'WINDOW_SIZE', windowSizeUniform); if (camera) { program.setUniformOfSemantic(_gl, 'NEAR', camera.near); program.setUniformOfSemantic(_gl, 'FAR', camera.far); } program.setUniformOfSemantic(_gl, 'DEVICEPIXELRATIO', vDpr); program.setUniformOfSemantic(_gl, 'TIME', time); // DEPRECATED program.setUniformOfSemantic(_gl, 'VIEWPORT_SIZE', viewportSizeUniform); // Set lights uniforms // TODO needs optimized if (scene) { scene.setLightUniforms(program, renderable.lightGroup, this); } } else { program = prevProgram; } // Program changes also needs reset the materials. if (programChanged || passConfig.isMaterialChanged( renderable, prevRenderable, material, prevMaterial )) { if (material.depthTest !== depthTest) { material.depthTest ? _gl.enable(_gl.DEPTH_TEST) : _gl.disable(_gl.DEPTH_TEST); depthTest = material.depthTest; } if (material.depthMask !== depthMask) { _gl.depthMask(material.depthMask); depthMask = material.depthMask; } if (material.transparent !== transparent) { material.transparent ? _gl.enable(_gl.BLEND) : _gl.disable(_gl.BLEND); transparent = material.transparent; } // TODO cache blending if (material.transparent) { if (material.blend) { material.blend(_gl); } else { // Default blend function _gl.blendEquationSeparate(_gl.FUNC_ADD, _gl.FUNC_ADD); _gl.blendFuncSeparate(_gl.SRC_ALPHA, _gl.ONE_MINUS_SRC_ALPHA, _gl.ONE, _gl.ONE_MINUS_SRC_ALPHA); } } materialTakesTextureSlot = this._bindMaterial( renderable, material, program, prevRenderable || null, prevMaterial || null, prevProgram || null, passConfig.getUniform ); prevMaterial = material; } var matrixSemanticKeys = shader.matrixSemanticKeys; if (isSceneNode) { for (var k = 0; k < matrixSemanticKeys.length; k++) { var semantic = matrixSemanticKeys[k]; var semanticInfo = shader.matrixSemantics[semantic]; var matrix = matrices[semantic]; if (semanticInfo.isTranspose) { var matrixNoTranspose = matrices[semanticInfo.semanticNoTranspose]; mat4.transpose(matrix, matrixNoTranspose); } program.setUniform(_gl, semanticInfo.type, semanticInfo.symbol, matrix); } } if (renderable.cullFace !== cullFace) { cullFace = renderable.cullFace; _gl.cullFace(cullFace); } if (renderable.frontFace !== frontFace) { frontFace = renderable.frontFace; _gl.frontFace(frontFace); } if (renderable.culling !== culling) { culling = renderable.culling; culling ? _gl.enable(_gl.CULL_FACE) : _gl.disable(_gl.CULL_FACE); } // TODO Not update skeleton in each renderable. this._updateSkeleton(renderable, program, materialTakesTextureSlot); if (drawIDChanged) { currentVAO = this._bindVAO(vaoExt, shader, geometry, program); } this._renderObject(renderable, currentVAO, program); // After render hook passConfig.afterRender(this, renderable); renderable.afterRender && renderable.afterRender(this); prevProgram = program; prevRenderable = renderable; } // TODO Seems need to be bound to null immediately if vao is changed? if (vaoExt) { vaoExt.bindVertexArrayOES(null); } this.trigger('afterrenderpass', this, list, camera, passConfig); }, getMaxJointNumber: function () { return this.maxJointNumber; }, _updateSkeleton: function (object, program, slot) { var _gl = this.gl; var skeleton = object.skeleton; // Set pose matrices of skinned mesh if (skeleton) { // TODO Update before culling. skeleton.update(); if (object.joints.length > this.getMaxJointNumber()) { var skinMatricesTexture = skeleton.getSubSkinMatricesTexture(object.__uid__, object.joints); program.useTextureSlot(this, skinMatricesTexture, slot); program.setUniform(_gl, '1i', 'skinMatricesTexture', slot); program.setUniform(_gl, '1f', 'skinMatricesTextureSize', skinMatricesTexture.width); } else { var skinMatricesArray = skeleton.getSubSkinMatrices(object.__uid__, object.joints); program.setUniformOfSemantic(_gl, 'SKIN_MATRIX', skinMatricesArray); } } }, _renderObject: function (renderable, vao, program) { var _gl = this.gl; var geometry = renderable.geometry; var glDrawMode = renderable.mode; if (glDrawMode == null) { glDrawMode = 0x0004; } var ext = null; var isInstanced = renderable.isInstancedMesh && renderable.isInstancedMesh(); if (isInstanced) { ext = this.getGLExtension('ANGLE_instanced_arrays'); if (!ext) { console.warn('Device not support ANGLE_instanced_arrays extension'); return; } } var instancedAttrLocations; if (isInstanced) { instancedAttrLocations = this._bindInstancedAttributes(renderable, program, ext); } if (vao.indicesBuffer) { var uintExt = this.getGLExtension('OES_element_index_uint'); var useUintExt = uintExt && (geometry.indices instanceof Uint32Array); var indicesType = useUintExt ? _gl.UNSIGNED_INT : _gl.UNSIGNED_SHORT; if (isInstanced) { ext.drawElementsInstancedANGLE( glDrawMode, vao.indicesBuffer.count, indicesType, 0, renderable.getInstanceCount() ); } else { _gl.drawElements(glDrawMode, vao.indicesBuffer.count, indicesType, 0); } } else { if (isInstanced) { ext.drawArraysInstancedANGLE(glDrawMode, 0, geometry.vertexCount, renderable.getInstanceCount()); } else { // FIXME Use vertex number in buffer // vertexCount may get the wrong value when geometry forget to mark dirty after update _gl.drawArrays(glDrawMode, 0, geometry.vertexCount); } } if (isInstanced) { for (var i = 0; i < instancedAttrLocations.length; i++) { _gl.disableVertexAttribArray(instancedAttrLocations[i]); } } }, _bindInstancedAttributes: function (renderable, program, ext) { var _gl = this.gl; var instancedBuffers = renderable.getInstancedAttributesBuffers(this); var locations = []; for (var i = 0; i < instancedBuffers.length; i++) { var bufferObj = instancedBuffers[i]; var location = program.getAttribLocation(_gl, bufferObj.symbol); if (location < 0) { continue; } var glType = attributeBufferTypeMap[bufferObj.type] || _gl.FLOAT; _gl.enableVertexAttribArray(location); // TODO _gl.bindBuffer(_gl.ARRAY_BUFFER, bufferObj.buffer); _gl.vertexAttribPointer(location, bufferObj.size, glType, false, 0, 0); ext.vertexAttribDivisorANGLE(location, bufferObj.divisor); locations.push(location); } return locations; }, _bindMaterial: function (renderable, material, program, prevRenderable, prevMaterial, prevProgram, getUniformValue) { var _gl = this.gl; // PENDING Same texture in different material take different slot? // May use shader of other material if shader code are same var sameProgram = prevProgram === program; var currentTextureSlot = program.currentTextureSlot(); var enabledUniforms = material.getEnabledUniforms(); var textureUniforms = material.getTextureUniforms(); var placeholderTexture = this._placeholderTexture; for (var u = 0; u < textureUniforms.length; u++) { var symbol = textureUniforms[u]; var uniformValue = getUniformValue(renderable, material, symbol); var uniformType = material.uniforms[symbol].type; // Not use `instanceof` to determine if a value is texture in Material#bind. // Use type instead, in some case texture may be in different namespaces. // TODO Duck type validate. if (uniformType === 't' && uniformValue) { // Reset slot uniformValue.__slot = -1; } else if (uniformType === 'tv') { for (var i = 0; i < uniformValue.length; i++) { if (uniformValue[i]) { uniformValue[i].__slot = -1; } } } } placeholderTexture.__slot = -1; // Set uniforms for (var u = 0; u < enabledUniforms.length; u++) { var symbol = enabledUniforms[u]; var uniform = material.uniforms[symbol]; var uniformValue = getUniformValue(renderable, material, symbol); var uniformType = uniform.type; var isTexture = uniformType === 't'; if (isTexture) { if (!uniformValue || !uniformValue.isRenderable()) { uniformValue = placeholderTexture; } } // PENDING // When binding two materials with the same shader // Many uniforms will be be set twice even if they have the same value // So add a evaluation to see if the uniform is really needed to be set if (prevMaterial && sameProgram) { var prevUniformValue = getUniformValue(prevRenderable, prevMaterial, symbol); if (isTexture) { if (!prevUniformValue || !prevUniformValue.isRenderable()) { prevUniformValue = placeholderTexture; } } if (prevUniformValue === uniformValue) { if (isTexture) { // Still take the slot to make sure same texture in different materials have same slot. program.takeCurrentTextureSlot(this, null); } else if (uniformType === 'tv' && uniformValue) { for (var i = 0; i < uniformValue.length; i++) { program.takeCurrentTextureSlot(this, null); } } continue; } } if (uniformValue == null) { continue; } else if (isTexture) { if (uniformValue.__slot < 0) { var slot = program.currentTextureSlot(); var res = program.setUniform(_gl, '1i', symbol, slot); if (res) { // Texture uniform is enabled program.takeCurrentTextureSlot(this, uniformValue); uniformValue.__slot = slot; } } // Multiple uniform use same texture.. else { program.setUniform(_gl, '1i', symbol, uniformValue.__slot); } } else if (Array.isArray(uniformValue)) { if (uniformValue.length === 0) { continue; } // Texture Array if (uniformType === 'tv') { if (!program.hasUniform(symbol)) { continue; } var arr = []; for (var i = 0; i < uniformValue.length; i++) { var texture = uniformValue[i]; if (texture.__slot < 0) { var slot = program.currentTextureSlot(); arr.push(slot); program.takeCurrentTextureSlot(this, texture); texture.__slot = slot; } else { arr.push(texture.__slot); } } program.setUniform(_gl, '1iv', symbol, arr); } else { program.setUniform(_gl, uniform.type, symbol, uniformValue); } } else{ program.setUniform(_gl, uniform.type, symbol, uniformValue); } } var newSlot = program.currentTextureSlot(); // Texture slot maybe used out of material. program.resetTextureSlot(currentTextureSlot); return newSlot; }, _bindVAO: function (vaoExt, shader, geometry, program) { var isStatic = !geometry.dynamic; var _gl = this.gl; var vaoId = this.__uid__ + '-' + program.__uid__; var vao = geometry.__vaoCache[vaoId]; if (!vao) { var chunks = geometry.getBufferChunks(this); if (!chunks || !chunks.length) { // Empty mesh return; } var chunk = chunks[0]; var attributeBuffers = chunk.attributeBuffers; var indicesBuffer = chunk.indicesBuffer; var availableAttributes = []; var availableAttributeSymbols = []; for (var a = 0; a < attributeBuffers.length; a++) { var attributeBufferInfo = attributeBuffers[a]; var name = attributeBufferInfo.name; var semantic = attributeBufferInfo.semantic; var symbol; if (semantic) { var semanticInfo = shader.attributeSemantics[semantic]; symbol = semanticInfo && semanticInfo.symbol; } else { symbol = name; } if (symbol && program.attributes[symbol]) { availableAttributes.push(attributeBufferInfo); availableAttributeSymbols.push(symbol); } } vao = new VertexArrayObject( availableAttributes, availableAttributeSymbols, indicesBuffer ); if (isStatic) { geometry.__vaoCache[vaoId] = vao; } } var needsBindAttributes = true; // Create vertex object array cost a lot // So we don't use it on the dynamic object if (vaoExt && isStatic) { // Use vertex array object // http://blog.tojicode.com/2012/10/oesvertexarrayobject-extension.html if (vao.vao == null) { vao.vao = vaoExt.createVertexArrayOES(); } else { needsBindAttributes = false; } vaoExt.bindVertexArrayOES(vao.vao); } var availableAttributes = vao.availableAttributes; var indicesBuffer = vao.indicesBuffer; if (needsBindAttributes) { var locationList = program.enableAttributes(this, vao.availableAttributeSymbols, (vaoExt && isStatic && vao)); // Setting attributes; for (var a = 0; a < availableAttributes.length; a++) { var location = locationList[a]; if (location === -1) { continue; } var attributeBufferInfo = availableAttributes[a]; var buffer = attributeBufferInfo.buffer; var size = attributeBufferInfo.size; var glType = attributeBufferTypeMap[attributeBufferInfo.type] || _gl.FLOAT; _gl.bindBuffer(_gl.ARRAY_BUFFER, buffer); _gl.vertexAttribPointer(location, size, glType, false, 0, 0); } if (geometry.isUseIndices()) { _gl.bindBuffer(_gl.ELEMENT_ARRAY_BUFFER, indicesBuffer.buffer); } } return vao; }, renderPreZ: function (list, scene, camera) { var _gl = this.gl; var preZPassMaterial = this._prezMaterial || new Material({ shader: new Shader(Shader.source('clay.prez.vertex'), Shader.source('clay.prez.fragment')) }); this._prezMaterial = preZPassMaterial; _gl.colorMask(false, false, false, false); _gl.depthMask(true); // Status this.renderPass(list, camera, { ifRender: function (renderable) { return !renderable.ignorePreZ; }, isMaterialChanged: function (renderable, prevRenderable) { var matA = renderable.material; var matB = prevRenderable.material; return matA.get('diffuseMap') !== matB.get('diffuseMap') || (matA.get('alphaCutoff') || 0) !== (matB.get('alphaCutoff') || 0); }, getUniform: function (renderable, depthMaterial, symbol) { if (symbol === 'alphaMap') { return renderable.material.get('diffuseMap'); } else if (symbol === 'alphaCutoff') { if (renderable.material.isDefined('fragment', 'ALPHA_TEST') && renderable.material.get('diffuseMap') ) { var alphaCutoff = renderable.material.get('alphaCutoff'); return alphaCutoff || 0; } return 0; } else if (symbol === 'uvRepeat') { return renderable.material.get('uvRepeat'); } else if (symbol === 'uvOffset') { return renderable.material.get('uvOffset'); } else { return depthMaterial.get(symbol); } }, getMaterial: function () { return preZPassMaterial; }, sort: this.opaqueSortCompare }); _gl.colorMask(true, true, true, true); _gl.depthMask(true); }, /** * Dispose given scene, including all geometris, textures and shaders in the scene * @param {clay.Scene} scene */ disposeScene: function(scene) { this.disposeNode(scene, true, true); scene.dispose(); }, /** * Dispose given node, including all geometries, textures and shaders attached on it or its descendant * @param {clay.Node} node * @param {boolean} [disposeGeometry=false] If dispose the geometries used in the descendant mesh * @param {boolean} [disposeTexture=false] If dispose the textures used in the descendant mesh */ disposeNode: function(root, disposeGeometry, disposeTexture) { // Dettached from parent if (root.getParent()) { root.getParent().remove(root); } var disposedMap = {}; root.traverse(function(node) { var material = node.material; if (node.geometry && disposeGeometry) { node.geometry.dispose(this); } if (disposeTexture && material && !disposedMap[material.__uid__]) { var textureUniforms = material.getTextureUniforms(); for (var u = 0; u < textureUniforms.length; u++) { var uniformName = textureUniforms[u]; var val = material.uniforms[uniformName].value; var uniformType = material.uniforms[uniformName].type; if (!val) { continue; } if (uniformType === 't') { val.dispose && val.dispose(this); } else if (uniformType === 'tv') { for (var k = 0; k < val.length; k++) { if (val[k]) { val[k].dispose && val[k].dispose(this); } } } } disposedMap[material.__uid__] = true; } // Particle system and AmbientCubemap light need to dispose if (node.dispose) { node.dispose(this); } }, this); }, /** * Dispose given geometry * @param {clay.Geometry} geometry */ disposeGeometry: function(geometry) { geometry.dispose(this); }, /** * Dispose given texture * @param {clay.Texture} texture */ disposeTexture: function(texture) { texture.dispose(this); }, /** * Dispose given frame buffer * @param {clay.FrameBuffer} frameBuffer */ disposeFrameBuffer: function(frameBuffer) { frameBuffer.dispose(this); }, /** * Dispose renderer */ dispose: function () {}, /** * Convert screen coords to normalized device coordinates(NDC) * Screen coords can get from mouse event, it is positioned relative to canvas element * NDC can be used in ray casting with Camera.prototype.castRay methods * * @param {number} x * @param {number} y * @param {clay.Vector2} [out] * @return {clay.Vector2} */ screenToNDC: function(x, y, out) { if (!out) { out = new Vector2(); } // Invert y; y = this._height - y; var viewport = this.viewport; var arr = out.array; arr[0] = (x - viewport.x) / viewport.width; arr[0] = arr[0] * 2 - 1; arr[1] = (y - viewport.y) / viewport.height; arr[1] = arr[1] * 2 - 1; return out; } }); /** * Opaque renderables compare function * @param {clay.Renderable} x * @param {clay.Renderable} y * @return {boolean} * @static */ Renderer.opaqueSortCompare = Renderer.prototype.opaqueSortCompare = function(x, y) { // Priority renderOrder -> program -> material -> geometry if (x.renderOrder === y.renderOrder) { if (x.__program === y.__program) { if (x.material === y.material) { return x.geometry.__uid__ - y.geometry.__uid__; } return x.material.__uid__ - y.material.__uid__; } if (x.__program && y.__program) { return x.__program.__uid__ - y.__program.__uid__; } return 0; } return x.renderOrder - y.renderOrder; }; /** * Transparent renderables compare function * @param {clay.Renderable} a * @param {clay.Renderable} b * @return {boolean} * @static */ Renderer.transparentSortCompare = Renderer.prototype.transparentSortCompare = function(x, y) { // Priority renderOrder -> depth -> program -> material -> geometry if (x.renderOrder === y.renderOrder) { if (x.__depth === y.__depth) { if (x.__program === y.__program) { if (x.material === y.material) { return x.geometry.__uid__ - y.geometry.__uid__; } return x.material.__uid__ - y.material.__uid__; } if (x.__program && y.__program) { return x.__program.__uid__ - y.__program.__uid__; } return 0; } // Depth is negative // So farther object has smaller depth value return x.__depth - y.__depth; } return x.renderOrder - y.renderOrder; }; // Temporary variables var matrices = { IDENTITY: mat4Create(), WORLD: mat4Create(), VIEW: mat4Create(), PROJECTION: mat4Create(), WORLDVIEW: mat4Create(), VIEWPROJECTION: mat4Create(), WORLDVIEWPROJECTION: mat4Create(), WORLDINVERSE: mat4Create(), VIEWINVERSE: mat4Create(), PROJECTIONINVERSE: mat4Create(), WORLDVIEWINVERSE: mat4Create(), VIEWPROJECTIONINVERSE: mat4Create(), WORLDVIEWPROJECTIONINVERSE: mat4Create(), WORLDTRANSPOSE: mat4Create(), VIEWTRANSPOSE: mat4Create(), PROJECTIONTRANSPOSE: mat4Create(), WORLDVIEWTRANSPOSE: mat4Create(), VIEWPROJECTIONTRANSPOSE: mat4Create(), WORLDVIEWPROJECTIONTRANSPOSE: mat4Create(), WORLDINVERSETRANSPOSE: mat4Create(), VIEWINVERSETRANSPOSE: mat4Create(), PROJECTIONINVERSETRANSPOSE: mat4Create(), WORLDVIEWINVERSETRANSPOSE: mat4Create(), VIEWPROJECTIONINVERSETRANSPOSE: mat4Create(), WORLDVIEWPROJECTIONINVERSETRANSPOSE: mat4Create() }; /** * @name clay.Renderer.COLOR_BUFFER_BIT * @type {number} */ Renderer.COLOR_BUFFER_BIT = glenum.COLOR_BUFFER_BIT; /** * @name clay.Renderer.DEPTH_BUFFER_BIT * @type {number} */ Renderer.DEPTH_BUFFER_BIT = glenum.DEPTH_BUFFER_BIT; /** * @name clay.Renderer.STENCIL_BUFFER_BIT * @type {number} */ Renderer.STENCIL_BUFFER_BIT = glenum.STENCIL_BUFFER_BIT; /** * @constructor * @alias clay.Vector3 * @param {number} x * @param {number} y * @param {number} z */ var Vector3 = function(x, y, z) { x = x || 0; y = y || 0; z = z || 0; /** * Storage of Vector3, read and write of x, y, z will change the values in array * All methods also operate on the array instead of x, y, z components * @name array * @type {Float32Array} * @memberOf clay.Vector3# */ this.array = vec3.fromValues(x, y, z); /** * Dirty flag is used by the Node to determine * if the matrix is updated to latest * @name _dirty * @type {boolean} * @memberOf clay.Vector3# */ this._dirty = true; }; Vector3.prototype = { constructor: Vector3, /** * Add b to self * @param {clay.Vector3} b * @return {clay.Vector3} */ add: function (b) { vec3.add(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Set x, y and z components * @param {number} x * @param {number} y * @param {number} z * @return {clay.Vector3} */ set: function (x, y, z) { this.array[0] = x; this.array[1] = y; this.array[2] = z; this._dirty = true; return this; }, /** * Set x, y and z components from array * @param {Float32Array|number[]} arr * @return {clay.Vector3} */ setArray: function (arr) { this.array[0] = arr[0]; this.array[1] = arr[1]; this.array[2] = arr[2]; this._dirty = true; return this; }, /** * Clone a new Vector3 * @return {clay.Vector3} */ clone: function () { return new Vector3(this.x, this.y, this.z); }, /** * Copy from b * @param {clay.Vector3} b * @return {clay.Vector3} */ copy: function (b) { vec3.copy(this.array, b.array); this._dirty = true; return this; }, /** * Cross product of self and b, written to a Vector3 out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ cross: function (a, b) { vec3.cross(this.array, a.array, b.array); this._dirty = true; return this; }, /** * Alias for distance * @param {clay.Vector3} b * @return {number} */ dist: function (b) { return vec3.dist(this.array, b.array); }, /** * Distance between self and b * @param {clay.Vector3} b * @return {number} */ distance: function (b) { return vec3.distance(this.array, b.array); }, /** * Alias for divide * @param {clay.Vector3} b * @return {clay.Vector3} */ div: function (b) { vec3.div(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Divide self by b * @param {clay.Vector3} b * @return {clay.Vector3} */ divide: function (b) { vec3.divide(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Dot product of self and b * @param {clay.Vector3} b * @return {number} */ dot: function (b) { return vec3.dot(this.array, b.array); }, /** * Alias of length * @return {number} */ len: function () { return vec3.len(this.array); }, /** * Calculate the length * @return {number} */ length: function () { return vec3.length(this.array); }, /** * Linear interpolation between a and b * @param {clay.Vector3} a * @param {clay.Vector3} b * @param {number} t * @return {clay.Vector3} */ lerp: function (a, b, t) { vec3.lerp(this.array, a.array, b.array, t); this._dirty = true; return this; }, /** * Minimum of self and b * @param {clay.Vector3} b * @return {clay.Vector3} */ min: function (b) { vec3.min(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Maximum of self and b * @param {clay.Vector3} b * @return {clay.Vector3} */ max: function (b) { vec3.max(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiply * @param {clay.Vector3} b * @return {clay.Vector3} */ mul: function (b) { vec3.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Mutiply self and b * @param {clay.Vector3} b * @return {clay.Vector3} */ multiply: function (b) { vec3.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Negate self * @return {clay.Vector3} */ negate: function () { vec3.negate(this.array, this.array); this._dirty = true; return this; }, /** * Normalize self * @return {clay.Vector3} */ normalize: function () { vec3.normalize(this.array, this.array); this._dirty = true; return this; }, /** * Generate random x, y, z components with a given scale * @param {number} scale * @return {clay.Vector3} */ random: function (scale) { vec3.random(this.array, scale); this._dirty = true; return this; }, /** * Scale self * @param {number} scale * @return {clay.Vector3} */ scale: function (s) { vec3.scale(this.array, this.array, s); this._dirty = true; return this; }, /** * Scale b and add to self * @param {clay.Vector3} b * @param {number} scale * @return {clay.Vector3} */ scaleAndAdd: function (b, s) { vec3.scaleAndAdd(this.array, this.array, b.array, s); this._dirty = true; return this; }, /** * Alias for squaredDistance * @param {clay.Vector3} b * @return {number} */ sqrDist: function (b) { return vec3.sqrDist(this.array, b.array); }, /** * Squared distance between self and b * @param {clay.Vector3} b * @return {number} */ squaredDistance: function (b) { return vec3.squaredDistance(this.array, b.array); }, /** * Alias for squaredLength * @return {number} */ sqrLen: function () { return vec3.sqrLen(this.array); }, /** * Squared length of self * @return {number} */ squaredLength: function () { return vec3.squaredLength(this.array); }, /** * Alias for subtract * @param {clay.Vector3} b * @return {clay.Vector3} */ sub: function (b) { vec3.sub(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Subtract b from self * @param {clay.Vector3} b * @return {clay.Vector3} */ subtract: function (b) { vec3.subtract(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Transform self with a Matrix3 m * @param {clay.Matrix3} m * @return {clay.Vector3} */ transformMat3: function (m) { vec3.transformMat3(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Matrix4 m * @param {clay.Matrix4} m * @return {clay.Vector3} */ transformMat4: function (m) { vec3.transformMat4(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Quaternion q * @param {clay.Quaternion} q * @return {clay.Vector3} */ transformQuat: function (q) { vec3.transformQuat(this.array, this.array, q.array); this._dirty = true; return this; }, /** * Trasnform self into projection space with m * @param {clay.Matrix4} m * @return {clay.Vector3} */ applyProjection: function (m) { var v = this.array; m = m.array; // Perspective projection if (m[15] === 0) { var w = -1 / v[2]; v[0] = m[0] * v[0] * w; v[1] = m[5] * v[1] * w; v[2] = (m[10] * v[2] + m[14]) * w; } else { v[0] = m[0] * v[0] + m[12]; v[1] = m[5] * v[1] + m[13]; v[2] = m[10] * v[2] + m[14]; } this._dirty = true; return this; }, eulerFromQuat: function(q, order) { Vector3.eulerFromQuat(this, q, order); }, eulerFromMat3: function (m, order) { Vector3.eulerFromMat3(this, m, order); }, toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; var defineProperty = Object.defineProperty; // Getter and Setter if (defineProperty) { var proto$1 = Vector3.prototype; /** * @name x * @type {number} * @memberOf clay.Vector3 * @instance */ defineProperty(proto$1, 'x', { get: function () { return this.array[0]; }, set: function (value) { this.array[0] = value; this._dirty = true; } }); /** * @name y * @type {number} * @memberOf clay.Vector3 * @instance */ defineProperty(proto$1, 'y', { get: function () { return this.array[1]; }, set: function (value) { this.array[1] = value; this._dirty = true; } }); /** * @name z * @type {number} * @memberOf clay.Vector3 * @instance */ defineProperty(proto$1, 'z', { get: function () { return this.array[2]; }, set: function (value) { this.array[2] = value; this._dirty = true; } }); } // Supply methods that are not in place /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.add = function(out, a, b) { vec3.add(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {number} x * @param {number} y * @param {number} z * @return {clay.Vector3} */ Vector3.set = function(out, x, y, z) { vec3.set(out.array, x, y, z); out._dirty = true; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.copy = function(out, b) { vec3.copy(out.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.cross = function(out, a, b) { vec3.cross(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {number} */ Vector3.dist = function(a, b) { return vec3.distance(a.array, b.array); }; /** * @function * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {number} */ Vector3.distance = Vector3.dist; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.div = function(out, a, b) { vec3.divide(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.divide = Vector3.div; /** * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {number} */ Vector3.dot = function(a, b) { return vec3.dot(a.array, b.array); }; /** * @param {clay.Vector3} a * @return {number} */ Vector3.len = function(b) { return vec3.length(b.array); }; // Vector3.length = Vector3.len; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @param {number} t * @return {clay.Vector3} */ Vector3.lerp = function(out, a, b, t) { vec3.lerp(out.array, a.array, b.array, t); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.min = function(out, a, b) { vec3.min(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.max = function(out, a, b) { vec3.max(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.mul = function(out, a, b) { vec3.multiply(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.multiply = Vector3.mul; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @return {clay.Vector3} */ Vector3.negate = function(out, a) { vec3.negate(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @return {clay.Vector3} */ Vector3.normalize = function(out, a) { vec3.normalize(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {number} scale * @return {clay.Vector3} */ Vector3.random = function(out, scale) { vec3.random(out.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {number} scale * @return {clay.Vector3} */ Vector3.scale = function(out, a, scale) { vec3.scale(out.array, a.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @param {number} scale * @return {clay.Vector3} */ Vector3.scaleAndAdd = function(out, a, b, scale) { vec3.scaleAndAdd(out.array, a.array, b.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {number} */ Vector3.sqrDist = function(a, b) { return vec3.sqrDist(a.array, b.array); }; /** * @function * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {number} */ Vector3.squaredDistance = Vector3.sqrDist; /** * @param {clay.Vector3} a * @return {number} */ Vector3.sqrLen = function(a) { return vec3.sqrLen(a.array); }; /** * @function * @param {clay.Vector3} a * @return {number} */ Vector3.squaredLength = Vector3.sqrLen; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.sub = function(out, a, b) { vec3.subtract(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Vector3} */ Vector3.subtract = Vector3.sub; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {Matrix3} m * @return {clay.Vector3} */ Vector3.transformMat3 = function(out, a, m) { vec3.transformMat3(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Matrix4} m * @return {clay.Vector3} */ Vector3.transformMat4 = function(out, a, m) { vec3.transformMat4(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector3} out * @param {clay.Vector3} a * @param {clay.Quaternion} q * @return {clay.Vector3} */ Vector3.transformQuat = function(out, a, q) { vec3.transformQuat(out.array, a.array, q.array); out._dirty = true; return out; }; function clamp(val, min, max) { return val < min ? min : (val > max ? max : val); } var atan2 = Math.atan2; var asin = Math.asin; var abs = Math.abs; /** * Convert quaternion to euler angle * Quaternion must be normalized * From three.js */ Vector3.eulerFromQuat = function (out, q, order) { out._dirty = true; q = q.array; var target = out.array; var x = q[0], y = q[1], z = q[2], w = q[3]; var x2 = x * x; var y2 = y * y; var z2 = z * z; var w2 = w * w; var order = (order || 'XYZ').toUpperCase(); switch (order) { case 'XYZ': target[0] = atan2(2 * (x * w - y * z), (w2 - x2 - y2 + z2)); target[1] = asin(clamp(2 * (x * z + y * w), - 1, 1)); target[2] = atan2(2 * (z * w - x * y), (w2 + x2 - y2 - z2)); break; case 'YXZ': target[0] = asin(clamp(2 * (x * w - y * z), - 1, 1)); target[1] = atan2(2 * (x * z + y * w), (w2 - x2 - y2 + z2)); target[2] = atan2(2 * (x * y + z * w), (w2 - x2 + y2 - z2)); break; case 'ZXY': target[0] = asin(clamp(2 * (x * w + y * z), - 1, 1)); target[1] = atan2(2 * (y * w - z * x), (w2 - x2 - y2 + z2)); target[2] = atan2(2 * (z * w - x * y), (w2 - x2 + y2 - z2)); break; case 'ZYX': target[0] = atan2(2 * (x * w + z * y), (w2 - x2 - y2 + z2)); target[1] = asin(clamp(2 * (y * w - x * z), - 1, 1)); target[2] = atan2(2 * (x * y + z * w), (w2 + x2 - y2 - z2)); break; case 'YZX': target[0] = atan2(2 * (x * w - z * y), (w2 - x2 + y2 - z2)); target[1] = atan2(2 * (y * w - x * z), (w2 + x2 - y2 - z2)); target[2] = asin(clamp(2 * (x * y + z * w), - 1, 1)); break; case 'XZY': target[0] = atan2(2 * (x * w + y * z), (w2 - x2 + y2 - z2)); target[1] = atan2(2 * (x * z + y * w), (w2 + x2 - y2 - z2)); target[2] = asin(clamp(2 * (z * w - x * y), - 1, 1)); break; default: console.warn('Unkown order: ' + order); } return out; }; /** * Convert rotation matrix to euler angle * from three.js */ Vector3.eulerFromMat3 = function (out, m, order) { // assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled) var te = m.array; var m11 = te[0], m12 = te[3], m13 = te[6]; var m21 = te[1], m22 = te[4], m23 = te[7]; var m31 = te[2], m32 = te[5], m33 = te[8]; var target = out.array; var order = (order || 'XYZ').toUpperCase(); switch (order) { case 'XYZ': target[1] = asin(clamp(m13, -1, 1)); if (abs(m13) < 0.99999) { target[0] = atan2(-m23, m33); target[2] = atan2(-m12, m11); } else { target[0] = atan2(m32, m22); target[2] = 0; } break; case 'YXZ': target[0] = asin(-clamp(m23, -1, 1)); if (abs(m23) < 0.99999) { target[1] = atan2(m13, m33); target[2] = atan2(m21, m22); } else { target[1] = atan2(-m31, m11); target[2] = 0; } break; case 'ZXY': target[0] = asin(clamp(m32, -1, 1)); if (abs(m32) < 0.99999) { target[1] = atan2(-m31, m33); target[2] = atan2(-m12, m22); } else { target[1] = 0; target[2] = atan2(m21, m11); } break; case 'ZYX': target[1] = asin(-clamp(m31, -1, 1)); if (abs(m31) < 0.99999) { target[0] = atan2(m32, m33); target[2] = atan2(m21, m11); } else { target[0] = 0; target[2] = atan2(-m12, m22); } break; case 'YZX': target[2] = asin(clamp(m21, -1, 1)); if (abs(m21) < 0.99999) { target[0] = atan2(-m23, m22); target[1] = atan2(-m31, m11); } else { target[0] = 0; target[1] = atan2(m13, m33); } break; case 'XZY': target[2] = asin(-clamp(m12, -1, 1)); if (abs(m12) < 0.99999) { target[0] = atan2(m32, m22); target[1] = atan2(m13, m11); } else { target[0] = atan2(-m23, m33); target[1] = 0; } break; default: console.warn('Unkown order: ' + order); } out._dirty = true; return out; }; Object.defineProperties(Vector3, { /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ POSITIVE_X: { get: function () { return new Vector3(1, 0, 0); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ NEGATIVE_X: { get: function () { return new Vector3(-1, 0, 0); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ POSITIVE_Y: { get: function () { return new Vector3(0, 1, 0); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ NEGATIVE_Y: { get: function () { return new Vector3(0, -1, 0); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ POSITIVE_Z: { get: function () { return new Vector3(0, 0, 1); } }, /** * @type {clay.Vector3} * @readOnly */ NEGATIVE_Z: { get: function () { return new Vector3(0, 0, -1); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ UP: { get: function () { return new Vector3(0, 1, 0); } }, /** * @type {clay.Vector3} * @readOnly * @memberOf clay.Vector3 */ ZERO: { get: function () { return new Vector3(); } } }); /** * @constructor * @alias clay.Quaternion * @param {number} x * @param {number} y * @param {number} z * @param {number} w */ var Quaternion = function (x, y, z, w) { x = x || 0; y = y || 0; z = z || 0; w = w === undefined ? 1 : w; /** * Storage of Quaternion, read and write of x, y, z, w will change the values in array * All methods also operate on the array instead of x, y, z, w components * @name array * @type {Float32Array} * @memberOf clay.Quaternion# */ this.array = quat.fromValues(x, y, z, w); /** * Dirty flag is used by the Node to determine * if the matrix is updated to latest * @name _dirty * @type {boolean} * @memberOf clay.Quaternion# */ this._dirty = true; }; Quaternion.prototype = { constructor: Quaternion, /** * Add b to self * @param {clay.Quaternion} b * @return {clay.Quaternion} */ add: function (b) { quat.add(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Calculate the w component from x, y, z component * @return {clay.Quaternion} */ calculateW: function () { quat.calculateW(this.array, this.array); this._dirty = true; return this; }, /** * Set x, y and z components * @param {number} x * @param {number} y * @param {number} z * @param {number} w * @return {clay.Quaternion} */ set: function (x, y, z, w) { this.array[0] = x; this.array[1] = y; this.array[2] = z; this.array[3] = w; this._dirty = true; return this; }, /** * Set x, y, z and w components from array * @param {Float32Array|number[]} arr * @return {clay.Quaternion} */ setArray: function (arr) { this.array[0] = arr[0]; this.array[1] = arr[1]; this.array[2] = arr[2]; this.array[3] = arr[3]; this._dirty = true; return this; }, /** * Clone a new Quaternion * @return {clay.Quaternion} */ clone: function () { return new Quaternion(this.x, this.y, this.z, this.w); }, /** * Calculates the conjugate of self If the quaternion is normalized, * this function is faster than invert and produces the same result. * * @return {clay.Quaternion} */ conjugate: function () { quat.conjugate(this.array, this.array); this._dirty = true; return this; }, /** * Copy from b * @param {clay.Quaternion} b * @return {clay.Quaternion} */ copy: function (b) { quat.copy(this.array, b.array); this._dirty = true; return this; }, /** * Dot product of self and b * @param {clay.Quaternion} b * @return {number} */ dot: function (b) { return quat.dot(this.array, b.array); }, /** * Set from the given 3x3 rotation matrix * @param {clay.Matrix3} m * @return {clay.Quaternion} */ fromMat3: function (m) { quat.fromMat3(this.array, m.array); this._dirty = true; return this; }, /** * Set from the given 4x4 rotation matrix * The 4th column and 4th row will be droped * @param {clay.Matrix4} m * @return {clay.Quaternion} */ fromMat4: (function () { var m3 = mat3.create(); return function (m) { mat3.fromMat4(m3, m.array); // TODO Not like mat4, mat3 in glmatrix seems to be row-based mat3.transpose(m3, m3); quat.fromMat3(this.array, m3); this._dirty = true; return this; }; })(), /** * Set to identity quaternion * @return {clay.Quaternion} */ identity: function () { quat.identity(this.array); this._dirty = true; return this; }, /** * Invert self * @return {clay.Quaternion} */ invert: function () { quat.invert(this.array, this.array); this._dirty = true; return this; }, /** * Alias of length * @return {number} */ len: function () { return quat.len(this.array); }, /** * Calculate the length * @return {number} */ length: function () { return quat.length(this.array); }, /** * Linear interpolation between a and b * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @param {number} t * @return {clay.Quaternion} */ lerp: function (a, b, t) { quat.lerp(this.array, a.array, b.array, t); this._dirty = true; return this; }, /** * Alias for multiply * @param {clay.Quaternion} b * @return {clay.Quaternion} */ mul: function (b) { quat.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiplyLeft * @param {clay.Quaternion} a * @return {clay.Quaternion} */ mulLeft: function (a) { quat.multiply(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Mutiply self and b * @param {clay.Quaternion} b * @return {clay.Quaternion} */ multiply: function (b) { quat.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Mutiply a and self * Quaternion mutiply is not commutative, so the result of mutiplyLeft is different with multiply. * @param {clay.Quaternion} a * @return {clay.Quaternion} */ multiplyLeft: function (a) { quat.multiply(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Normalize self * @return {clay.Quaternion} */ normalize: function () { quat.normalize(this.array, this.array); this._dirty = true; return this; }, /** * Rotate self by a given radian about X axis * @param {number} rad * @return {clay.Quaternion} */ rotateX: function (rad) { quat.rotateX(this.array, this.array, rad); this._dirty = true; return this; }, /** * Rotate self by a given radian about Y axis * @param {number} rad * @return {clay.Quaternion} */ rotateY: function (rad) { quat.rotateY(this.array, this.array, rad); this._dirty = true; return this; }, /** * Rotate self by a given radian about Z axis * @param {number} rad * @return {clay.Quaternion} */ rotateZ: function (rad) { quat.rotateZ(this.array, this.array, rad); this._dirty = true; return this; }, /** * Sets self to represent the shortest rotation from Vector3 a to Vector3 b. * a and b needs to be normalized * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Quaternion} */ rotationTo: function (a, b) { quat.rotationTo(this.array, a.array, b.array); this._dirty = true; return this; }, /** * Sets self with values corresponding to the given axes * @param {clay.Vector3} view * @param {clay.Vector3} right * @param {clay.Vector3} up * @return {clay.Quaternion} */ setAxes: function (view, right, up) { quat.setAxes(this.array, view.array, right.array, up.array); this._dirty = true; return this; }, /** * Sets self with a rotation axis and rotation angle * @param {clay.Vector3} axis * @param {number} rad * @return {clay.Quaternion} */ setAxisAngle: function (axis, rad) { quat.setAxisAngle(this.array, axis.array, rad); this._dirty = true; return this; }, /** * Perform spherical linear interpolation between a and b * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @param {number} t * @return {clay.Quaternion} */ slerp: function (a, b, t) { quat.slerp(this.array, a.array, b.array, t); this._dirty = true; return this; }, /** * Alias for squaredLength * @return {number} */ sqrLen: function () { return quat.sqrLen(this.array); }, /** * Squared length of self * @return {number} */ squaredLength: function () { return quat.squaredLength(this.array); }, /** * Set from euler * @param {clay.Vector3} v * @param {String} order */ fromEuler: function (v, order) { return Quaternion.fromEuler(this, v, order); }, toString: function () { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; var defineProperty$1 = Object.defineProperty; // Getter and Setter if (defineProperty$1) { var proto$2 = Quaternion.prototype; /** * @name x * @type {number} * @memberOf clay.Quaternion * @instance */ defineProperty$1(proto$2, 'x', { get: function () { return this.array[0]; }, set: function (value) { this.array[0] = value; this._dirty = true; } }); /** * @name y * @type {number} * @memberOf clay.Quaternion * @instance */ defineProperty$1(proto$2, 'y', { get: function () { return this.array[1]; }, set: function (value) { this.array[1] = value; this._dirty = true; } }); /** * @name z * @type {number} * @memberOf clay.Quaternion * @instance */ defineProperty$1(proto$2, 'z', { get: function () { return this.array[2]; }, set: function (value) { this.array[2] = value; this._dirty = true; } }); /** * @name w * @type {number} * @memberOf clay.Quaternion * @instance */ defineProperty$1(proto$2, 'w', { get: function () { return this.array[3]; }, set: function (value) { this.array[3] = value; this._dirty = true; } }); } // Supply methods that are not in place /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @return {clay.Quaternion} */ Quaternion.add = function (out, a, b) { quat.add(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {number} x * @param {number} y * @param {number} z * @param {number} w * @return {clay.Quaternion} */ Quaternion.set = function (out, x, y, z, w) { quat.set(out.array, x, y, z, w); out._dirty = true; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} b * @return {clay.Quaternion} */ Quaternion.copy = function (out, b) { quat.copy(out.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @return {clay.Quaternion} */ Quaternion.calculateW = function (out, a) { quat.calculateW(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @return {clay.Quaternion} */ Quaternion.conjugate = function (out, a) { quat.conjugate(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @return {clay.Quaternion} */ Quaternion.identity = function (out) { quat.identity(out.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @return {clay.Quaternion} */ Quaternion.invert = function (out, a) { quat.invert(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @return {number} */ Quaternion.dot = function (a, b) { return quat.dot(a.array, b.array); }; /** * @param {clay.Quaternion} a * @return {number} */ Quaternion.len = function (a) { return quat.length(a.array); }; // Quaternion.length = Quaternion.len; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @param {number} t * @return {clay.Quaternion} */ Quaternion.lerp = function (out, a, b, t) { quat.lerp(out.array, a.array, b.array, t); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @param {number} t * @return {clay.Quaternion} */ Quaternion.slerp = function (out, a, b, t) { quat.slerp(out.array, a.array, b.array, t); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @return {clay.Quaternion} */ Quaternion.mul = function (out, a, b) { quat.multiply(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {clay.Quaternion} b * @return {clay.Quaternion} */ Quaternion.multiply = Quaternion.mul; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {number} rad * @return {clay.Quaternion} */ Quaternion.rotateX = function (out, a, rad) { quat.rotateX(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {number} rad * @return {clay.Quaternion} */ Quaternion.rotateY = function (out, a, rad) { quat.rotateY(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @param {number} rad * @return {clay.Quaternion} */ Quaternion.rotateZ = function (out, a, rad) { quat.rotateZ(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Vector3} axis * @param {number} rad * @return {clay.Quaternion} */ Quaternion.setAxisAngle = function (out, axis, rad) { quat.setAxisAngle(out.array, axis.array, rad); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Quaternion} a * @return {clay.Quaternion} */ Quaternion.normalize = function (out, a) { quat.normalize(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} a * @return {number} */ Quaternion.sqrLen = function (a) { return quat.sqrLen(a.array); }; /** * @function * @param {clay.Quaternion} a * @return {number} */ Quaternion.squaredLength = Quaternion.sqrLen; /** * @param {clay.Quaternion} out * @param {clay.Matrix3} m * @return {clay.Quaternion} */ Quaternion.fromMat3 = function (out, m) { quat.fromMat3(out.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Vector3} view * @param {clay.Vector3} right * @param {clay.Vector3} up * @return {clay.Quaternion} */ Quaternion.setAxes = function (out, view, right, up) { quat.setAxes(out.array, view.array, right.array, up.array); out._dirty = true; return out; }; /** * @param {clay.Quaternion} out * @param {clay.Vector3} a * @param {clay.Vector3} b * @return {clay.Quaternion} */ Quaternion.rotationTo = function (out, a, b) { quat.rotationTo(out.array, a.array, b.array); out._dirty = true; return out; }; /** * Set quaternion from euler * @param {clay.Quaternion} out * @param {clay.Vector3} v * @param {String} order */ Quaternion.fromEuler = function (out, v, order) { out._dirty = true; v = v.array; var target = out.array; var c1 = Math.cos(v[0] / 2); var c2 = Math.cos(v[1] / 2); var c3 = Math.cos(v[2] / 2); var s1 = Math.sin(v[0] / 2); var s2 = Math.sin(v[1] / 2); var s3 = Math.sin(v[2] / 2); var order = (order || 'XYZ').toUpperCase(); // http://www.mathworks.com/matlabcentral/fileexchange/ // 20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/ // content/SpinCalc.m switch (order) { case 'XYZ': target[0] = s1 * c2 * c3 + c1 * s2 * s3; target[1] = c1 * s2 * c3 - s1 * c2 * s3; target[2] = c1 * c2 * s3 + s1 * s2 * c3; target[3] = c1 * c2 * c3 - s1 * s2 * s3; break; case 'YXZ': target[0] = s1 * c2 * c3 + c1 * s2 * s3; target[1] = c1 * s2 * c3 - s1 * c2 * s3; target[2] = c1 * c2 * s3 - s1 * s2 * c3; target[3] = c1 * c2 * c3 + s1 * s2 * s3; break; case 'ZXY': target[0] = s1 * c2 * c3 - c1 * s2 * s3; target[1] = c1 * s2 * c3 + s1 * c2 * s3; target[2] = c1 * c2 * s3 + s1 * s2 * c3; target[3] = c1 * c2 * c3 - s1 * s2 * s3; break; case 'ZYX': target[0] = s1 * c2 * c3 - c1 * s2 * s3; target[1] = c1 * s2 * c3 + s1 * c2 * s3; target[2] = c1 * c2 * s3 - s1 * s2 * c3; target[3] = c1 * c2 * c3 + s1 * s2 * s3; break; case 'YZX': target[0] = s1 * c2 * c3 + c1 * s2 * s3; target[1] = c1 * s2 * c3 + s1 * c2 * s3; target[2] = c1 * c2 * s3 - s1 * s2 * c3; target[3] = c1 * c2 * c3 - s1 * s2 * s3; break; case 'XZY': target[0] = s1 * c2 * c3 - c1 * s2 * s3; target[1] = c1 * s2 * c3 - s1 * c2 * s3; target[2] = c1 * c2 * s3 + s1 * s2 * c3; target[3] = c1 * c2 * c3 + s1 * s2 * s3; break; } }; /** * @constructor * @alias clay.Matrix4 */ var Matrix4 = function() { this._axisX = new Vector3(); this._axisY = new Vector3(); this._axisZ = new Vector3(); /** * Storage of Matrix4 * @name array * @type {Float32Array} * @memberOf clay.Matrix4# */ this.array = mat4.create(); /** * @name _dirty * @type {boolean} * @memberOf clay.Matrix4# */ this._dirty = true; }; Matrix4.prototype = { constructor: Matrix4, /** * Set components from array * @param {Float32Array|number[]} arr */ setArray: function (arr) { for (var i = 0; i < this.array.length; i++) { this.array[i] = arr[i]; } this._dirty = true; return this; }, /** * Calculate the adjugate of self, in-place * @return {clay.Matrix4} */ adjoint: function() { mat4.adjoint(this.array, this.array); this._dirty = true; return this; }, /** * Clone a new Matrix4 * @return {clay.Matrix4} */ clone: function() { return (new Matrix4()).copy(this); }, /** * Copy from b * @param {clay.Matrix4} b * @return {clay.Matrix4} */ copy: function(a) { mat4.copy(this.array, a.array); this._dirty = true; return this; }, /** * Calculate matrix determinant * @return {number} */ determinant: function() { return mat4.determinant(this.array); }, /** * Set upper 3x3 part from quaternion * @param {clay.Quaternion} q * @return {clay.Matrix4} */ fromQuat: function(q) { mat4.fromQuat(this.array, q.array); this._dirty = true; return this; }, /** * Set from a quaternion rotation and a vector translation * @param {clay.Quaternion} q * @param {clay.Vector3} v * @return {clay.Matrix4} */ fromRotationTranslation: function(q, v) { mat4.fromRotationTranslation(this.array, q.array, v.array); this._dirty = true; return this; }, /** * Set from Matrix2d, it is used when converting a 2d shape to 3d space. * In 3d space it is equivalent to ranslate on xy plane and rotate about z axis * @param {clay.Matrix2d} m2d * @return {clay.Matrix4} */ fromMat2d: function(m2d) { Matrix4.fromMat2d(this, m2d); return this; }, /** * Set from frustum bounds * @param {number} left * @param {number} right * @param {number} bottom * @param {number} top * @param {number} near * @param {number} far * @return {clay.Matrix4} */ frustum: function (left, right, bottom, top, near, far) { mat4.frustum(this.array, left, right, bottom, top, near, far); this._dirty = true; return this; }, /** * Set to a identity matrix * @return {clay.Matrix4} */ identity: function() { mat4.identity(this.array); this._dirty = true; return this; }, /** * Invert self * @return {clay.Matrix4} */ invert: function() { mat4.invert(this.array, this.array); this._dirty = true; return this; }, /** * Set as a matrix with the given eye position, focal point, and up axis * @param {clay.Vector3} eye * @param {clay.Vector3} center * @param {clay.Vector3} up * @return {clay.Matrix4} */ lookAt: function(eye, center, up) { mat4.lookAt(this.array, eye.array, center.array, up.array); this._dirty = true; return this; }, /** * Alias for mutiply * @param {clay.Matrix4} b * @return {clay.Matrix4} */ mul: function(b) { mat4.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiplyLeft * @param {clay.Matrix4} a * @return {clay.Matrix4} */ mulLeft: function(a) { mat4.mul(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Multiply self and b * @param {clay.Matrix4} b * @return {clay.Matrix4} */ multiply: function(b) { mat4.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Multiply a and self, a is on the left * @param {clay.Matrix3} a * @return {clay.Matrix3} */ multiplyLeft: function(a) { mat4.multiply(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Set as a orthographic projection matrix * @param {number} left * @param {number} right * @param {number} bottom * @param {number} top * @param {number} near * @param {number} far * @return {clay.Matrix4} */ ortho: function(left, right, bottom, top, near, far) { mat4.ortho(this.array, left, right, bottom, top, near, far); this._dirty = true; return this; }, /** * Set as a perspective projection matrix * @param {number} fovy * @param {number} aspect * @param {number} near * @param {number} far * @return {clay.Matrix4} */ perspective: function(fovy, aspect, near, far) { mat4.perspective(this.array, fovy, aspect, near, far); this._dirty = true; return this; }, /** * Rotate self by rad about axis. * Equal to right-multiply a rotaion matrix * @param {number} rad * @param {clay.Vector3} axis * @return {clay.Matrix4} */ rotate: function(rad, axis) { mat4.rotate(this.array, this.array, rad, axis.array); this._dirty = true; return this; }, /** * Rotate self by a given radian about X axis. * Equal to right-multiply a rotaion matrix * @param {number} rad * @return {clay.Matrix4} */ rotateX: function(rad) { mat4.rotateX(this.array, this.array, rad); this._dirty = true; return this; }, /** * Rotate self by a given radian about Y axis. * Equal to right-multiply a rotaion matrix * @param {number} rad * @return {clay.Matrix4} */ rotateY: function(rad) { mat4.rotateY(this.array, this.array, rad); this._dirty = true; return this; }, /** * Rotate self by a given radian about Z axis. * Equal to right-multiply a rotaion matrix * @param {number} rad * @return {clay.Matrix4} */ rotateZ: function(rad) { mat4.rotateZ(this.array, this.array, rad); this._dirty = true; return this; }, /** * Scale self by s * Equal to right-multiply a scale matrix * @param {clay.Vector3} s * @return {clay.Matrix4} */ scale: function(v) { mat4.scale(this.array, this.array, v.array); this._dirty = true; return this; }, /** * Translate self by v. * Equal to right-multiply a translate matrix * @param {clay.Vector3} v * @return {clay.Matrix4} */ translate: function(v) { mat4.translate(this.array, this.array, v.array); this._dirty = true; return this; }, /** * Transpose self, in-place. * @return {clay.Matrix2} */ transpose: function() { mat4.transpose(this.array, this.array); this._dirty = true; return this; }, /** * Decompose a matrix to SRT * @param {clay.Vector3} [scale] * @param {clay.Quaternion} rotation * @param {clay.Vector} position * @see http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.matrix.decompose.aspx */ decomposeMatrix: (function() { var x = vec3.create(); var y = vec3.create(); var z = vec3.create(); var m3 = mat3.create(); return function(scale, rotation, position) { var el = this.array; vec3.set(x, el[0], el[1], el[2]); vec3.set(y, el[4], el[5], el[6]); vec3.set(z, el[8], el[9], el[10]); var sx = vec3.length(x); var sy = vec3.length(y); var sz = vec3.length(z); // if determine is negative, we need to invert one scale var det = this.determinant(); if (det < 0) { sx = -sx; } if (scale) { scale.set(sx, sy, sz); } position.set(el[12], el[13], el[14]); mat3.fromMat4(m3, el); // Not like mat4, mat3 in glmatrix seems to be row-based // Seems fixed in gl-matrix 2.2.2 // https://github.com/toji/gl-matrix/issues/114 // mat3.transpose(m3, m3); m3[0] /= sx; m3[1] /= sx; m3[2] /= sx; m3[3] /= sy; m3[4] /= sy; m3[5] /= sy; m3[6] /= sz; m3[7] /= sz; m3[8] /= sz; quat.fromMat3(rotation.array, m3); quat.normalize(rotation.array, rotation.array); rotation._dirty = true; position._dirty = true; }; })(), toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; var defineProperty$2 = Object.defineProperty; if (defineProperty$2) { var proto$3 = Matrix4.prototype; /** * Z Axis of local transform * @name z * @type {clay.Vector3} * @memberOf clay.Matrix4 * @instance */ defineProperty$2(proto$3, 'z', { get: function () { var el = this.array; this._axisZ.set(el[8], el[9], el[10]); return this._axisZ; }, set: function (v) { // TODO Here has a problem // If only set an item of vector will not work var el = this.array; v = v.array; el[8] = v[0]; el[9] = v[1]; el[10] = v[2]; this._dirty = true; } }); /** * Y Axis of local transform * @name y * @type {clay.Vector3} * @memberOf clay.Matrix4 * @instance */ defineProperty$2(proto$3, 'y', { get: function () { var el = this.array; this._axisY.set(el[4], el[5], el[6]); return this._axisY; }, set: function (v) { var el = this.array; v = v.array; el[4] = v[0]; el[5] = v[1]; el[6] = v[2]; this._dirty = true; } }); /** * X Axis of local transform * @name x * @type {clay.Vector3} * @memberOf clay.Matrix4 * @instance */ defineProperty$2(proto$3, 'x', { get: function () { var el = this.array; this._axisX.set(el[0], el[1], el[2]); return this._axisX; }, set: function (v) { var el = this.array; v = v.array; el[0] = v[0]; el[1] = v[1]; el[2] = v[2]; this._dirty = true; } }); } /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @return {clay.Matrix4} */ Matrix4.adjoint = function(out, a) { mat4.adjoint(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @return {clay.Matrix4} */ Matrix4.copy = function(out, a) { mat4.copy(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} a * @return {number} */ Matrix4.determinant = function(a) { return mat4.determinant(a.array); }; /** * @param {clay.Matrix4} out * @return {clay.Matrix4} */ Matrix4.identity = function(out) { mat4.identity(out.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {number} left * @param {number} right * @param {number} bottom * @param {number} top * @param {number} near * @param {number} far * @return {clay.Matrix4} */ Matrix4.ortho = function(out, left, right, bottom, top, near, far) { mat4.ortho(out.array, left, right, bottom, top, near, far); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {number} fovy * @param {number} aspect * @param {number} near * @param {number} far * @return {clay.Matrix4} */ Matrix4.perspective = function(out, fovy, aspect, near, far) { mat4.perspective(out.array, fovy, aspect, near, far); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Vector3} eye * @param {clay.Vector3} center * @param {clay.Vector3} up * @return {clay.Matrix4} */ Matrix4.lookAt = function(out, eye, center, up) { mat4.lookAt(out.array, eye.array, center.array, up.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @return {clay.Matrix4} */ Matrix4.invert = function(out, a) { mat4.invert(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {clay.Matrix4} b * @return {clay.Matrix4} */ Matrix4.mul = function(out, a, b) { mat4.mul(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {clay.Matrix4} b * @return {clay.Matrix4} */ Matrix4.multiply = Matrix4.mul; /** * @param {clay.Matrix4} out * @param {clay.Quaternion} q * @return {clay.Matrix4} */ Matrix4.fromQuat = function(out, q) { mat4.fromQuat(out.array, q.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Quaternion} q * @param {clay.Vector3} v * @return {clay.Matrix4} */ Matrix4.fromRotationTranslation = function(out, q, v) { mat4.fromRotationTranslation(out.array, q.array, v.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} m4 * @param {clay.Matrix2d} m2d * @return {clay.Matrix4} */ Matrix4.fromMat2d = function(m4, m2d) { m4._dirty = true; var m2d = m2d.array; var m4 = m4.array; m4[0] = m2d[0]; m4[4] = m2d[2]; m4[12] = m2d[4]; m4[1] = m2d[1]; m4[5] = m2d[3]; m4[13] = m2d[5]; return m4; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {number} rad * @param {clay.Vector3} axis * @return {clay.Matrix4} */ Matrix4.rotate = function(out, a, rad, axis) { mat4.rotate(out.array, a.array, rad, axis.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {number} rad * @return {clay.Matrix4} */ Matrix4.rotateX = function(out, a, rad) { mat4.rotateX(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {number} rad * @return {clay.Matrix4} */ Matrix4.rotateY = function(out, a, rad) { mat4.rotateY(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {number} rad * @return {clay.Matrix4} */ Matrix4.rotateZ = function(out, a, rad) { mat4.rotateZ(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {clay.Vector3} v * @return {clay.Matrix4} */ Matrix4.scale = function(out, a, v) { mat4.scale(out.array, a.array, v.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @return {clay.Matrix4} */ Matrix4.transpose = function(out, a) { mat4.transpose(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix4} out * @param {clay.Matrix4} a * @param {clay.Vector3} v * @return {clay.Matrix4} */ Matrix4.translate = function(out, a, v) { mat4.translate(out.array, a.array, v.array); out._dirty = true; return out; }; var vec3Set = vec3.set; var vec3Copy = vec3.copy; /** * Axis aligned bounding box * @constructor * @alias clay.BoundingBox * @param {clay.Vector3} [min] * @param {clay.Vector3} [max] */ var BoundingBox = function (min, max) { /** * Minimum coords of bounding box * @type {clay.Vector3} */ this.min = min || new Vector3(Infinity, Infinity, Infinity); /** * Maximum coords of bounding box * @type {clay.Vector3} */ this.max = max || new Vector3(-Infinity, -Infinity, -Infinity); this.vertices = null; }; BoundingBox.prototype = { constructor: BoundingBox, /** * Update min and max coords from a vertices array * @param {array} vertices */ updateFromVertices: function (vertices) { if (vertices.length > 0) { var min = this.min; var max = this.max; var minArr = min.array; var maxArr = max.array; vec3Copy(minArr, vertices[0]); vec3Copy(maxArr, vertices[0]); for (var i = 1; i < vertices.length; i++) { var vertex = vertices[i]; if (vertex[0] < minArr[0]) { minArr[0] = vertex[0]; } if (vertex[1] < minArr[1]) { minArr[1] = vertex[1]; } if (vertex[2] < minArr[2]) { minArr[2] = vertex[2]; } if (vertex[0] > maxArr[0]) { maxArr[0] = vertex[0]; } if (vertex[1] > maxArr[1]) { maxArr[1] = vertex[1]; } if (vertex[2] > maxArr[2]) { maxArr[2] = vertex[2]; } } min._dirty = true; max._dirty = true; } }, /** * Union operation with another bounding box * @param {clay.BoundingBox} bbox */ union: function (bbox) { var min = this.min; var max = this.max; vec3.min(min.array, min.array, bbox.min.array); vec3.max(max.array, max.array, bbox.max.array); min._dirty = true; max._dirty = true; return this; }, /** * Intersection operation with another bounding box * @param {clay.BoundingBox} bbox */ intersection: function (bbox) { var min = this.min; var max = this.max; vec3.max(min.array, min.array, bbox.min.array); vec3.min(max.array, max.array, bbox.max.array); min._dirty = true; max._dirty = true; return this; }, /** * If intersect with another bounding box * @param {clay.BoundingBox} bbox * @return {boolean} */ intersectBoundingBox: function (bbox) { var _min = this.min.array; var _max = this.max.array; var _min2 = bbox.min.array; var _max2 = bbox.max.array; return ! (_min[0] > _max2[0] || _min[1] > _max2[1] || _min[2] > _max2[2] || _max[0] < _min2[0] || _max[1] < _min2[1] || _max[2] < _min2[2]); }, /** * If contain another bounding box entirely * @param {clay.BoundingBox} bbox * @return {boolean} */ containBoundingBox: function (bbox) { var _min = this.min.array; var _max = this.max.array; var _min2 = bbox.min.array; var _max2 = bbox.max.array; return _min[0] <= _min2[0] && _min[1] <= _min2[1] && _min[2] <= _min2[2] && _max[0] >= _max2[0] && _max[1] >= _max2[1] && _max[2] >= _max2[2]; }, /** * If contain point entirely * @param {clay.Vector3} point * @return {boolean} */ containPoint: function (p) { var _min = this.min.array; var _max = this.max.array; var _p = p.array; return _min[0] <= _p[0] && _min[1] <= _p[1] && _min[2] <= _p[2] && _max[0] >= _p[0] && _max[1] >= _p[1] && _max[2] >= _p[2]; }, /** * If bounding box is finite */ isFinite: function () { var _min = this.min.array; var _max = this.max.array; return isFinite(_min[0]) && isFinite(_min[1]) && isFinite(_min[2]) && isFinite(_max[0]) && isFinite(_max[1]) && isFinite(_max[2]); }, /** * Apply an affine transform matrix to the bounding box * @param {clay.Matrix4} matrix */ applyTransform: function (matrix) { this.transformFrom(this, matrix); }, /** * Get from another bounding box and an affine transform matrix. * @param {clay.BoundingBox} source * @param {clay.Matrix4} matrix */ transformFrom: (function () { // http://dev.theomader.com/transform-bounding-boxes/ var xa = vec3.create(); var xb = vec3.create(); var ya = vec3.create(); var yb = vec3.create(); var za = vec3.create(); var zb = vec3.create(); return function (source, matrix) { var min = source.min.array; var max = source.max.array; var m = matrix.array; xa[0] = m[0] * min[0]; xa[1] = m[1] * min[0]; xa[2] = m[2] * min[0]; xb[0] = m[0] * max[0]; xb[1] = m[1] * max[0]; xb[2] = m[2] * max[0]; ya[0] = m[4] * min[1]; ya[1] = m[5] * min[1]; ya[2] = m[6] * min[1]; yb[0] = m[4] * max[1]; yb[1] = m[5] * max[1]; yb[2] = m[6] * max[1]; za[0] = m[8] * min[2]; za[1] = m[9] * min[2]; za[2] = m[10] * min[2]; zb[0] = m[8] * max[2]; zb[1] = m[9] * max[2]; zb[2] = m[10] * max[2]; min = this.min.array; max = this.max.array; min[0] = Math.min(xa[0], xb[0]) + Math.min(ya[0], yb[0]) + Math.min(za[0], zb[0]) + m[12]; min[1] = Math.min(xa[1], xb[1]) + Math.min(ya[1], yb[1]) + Math.min(za[1], zb[1]) + m[13]; min[2] = Math.min(xa[2], xb[2]) + Math.min(ya[2], yb[2]) + Math.min(za[2], zb[2]) + m[14]; max[0] = Math.max(xa[0], xb[0]) + Math.max(ya[0], yb[0]) + Math.max(za[0], zb[0]) + m[12]; max[1] = Math.max(xa[1], xb[1]) + Math.max(ya[1], yb[1]) + Math.max(za[1], zb[1]) + m[13]; max[2] = Math.max(xa[2], xb[2]) + Math.max(ya[2], yb[2]) + Math.max(za[2], zb[2]) + m[14]; this.min._dirty = true; this.max._dirty = true; return this; }; })(), /** * Apply a projection matrix to the bounding box * @param {clay.Matrix4} matrix */ applyProjection: function (matrix) { var min = this.min.array; var max = this.max.array; var m = matrix.array; // min in min z var v10 = min[0]; var v11 = min[1]; var v12 = min[2]; // max in min z var v20 = max[0]; var v21 = max[1]; var v22 = min[2]; // max in max z var v30 = max[0]; var v31 = max[1]; var v32 = max[2]; if (m[15] === 1) { // Orthographic projection min[0] = m[0] * v10 + m[12]; min[1] = m[5] * v11 + m[13]; max[2] = m[10] * v12 + m[14]; max[0] = m[0] * v30 + m[12]; max[1] = m[5] * v31 + m[13]; min[2] = m[10] * v32 + m[14]; } else { var w = -1 / v12; min[0] = m[0] * v10 * w; min[1] = m[5] * v11 * w; max[2] = (m[10] * v12 + m[14]) * w; w = -1 / v22; max[0] = m[0] * v20 * w; max[1] = m[5] * v21 * w; w = -1 / v32; min[2] = (m[10] * v32 + m[14]) * w; } this.min._dirty = true; this.max._dirty = true; return this; }, updateVertices: function () { var vertices = this.vertices; if (!vertices) { // Cube vertices vertices = []; for (var i = 0; i < 8; i++) { vertices[i] = vec3.fromValues(0, 0, 0); } /** * Eight coords of bounding box * @type {Float32Array[]} */ this.vertices = vertices; } var min = this.min.array; var max = this.max.array; //--- min z // min x vec3Set(vertices[0], min[0], min[1], min[2]); vec3Set(vertices[1], min[0], max[1], min[2]); // max x vec3Set(vertices[2], max[0], min[1], min[2]); vec3Set(vertices[3], max[0], max[1], min[2]); //-- max z vec3Set(vertices[4], min[0], min[1], max[2]); vec3Set(vertices[5], min[0], max[1], max[2]); vec3Set(vertices[6], max[0], min[1], max[2]); vec3Set(vertices[7], max[0], max[1], max[2]); return this; }, /** * Copy values from another bounding box * @param {clay.BoundingBox} bbox */ copy: function (bbox) { var min = this.min; var max = this.max; vec3Copy(min.array, bbox.min.array); vec3Copy(max.array, bbox.max.array); min._dirty = true; max._dirty = true; return this; }, /** * Clone a new bounding box * @return {clay.BoundingBox} */ clone: function () { var boundingBox = new BoundingBox(); boundingBox.copy(this); return boundingBox; } }; var nameId = 0; /** * @constructor clay.Node * @extends clay.core.Base */ var Node = Base.extend(/** @lends clay.Node# */{ /** * Scene node name * @type {string} */ name: '', /** * Position relative to its parent node. aka translation. * @type {clay.Vector3} */ position: null, /** * Rotation relative to its parent node. Represented by a quaternion * @type {clay.Quaternion} */ rotation: null, /** * Scale relative to its parent node * @type {clay.Vector3} */ scale: null, /** * Affine transform matrix relative to its root scene. * @type {clay.Matrix4} */ worldTransform: null, /** * Affine transform matrix relative to its parent node. * Composited with position, rotation and scale. * @type {clay.Matrix4} */ localTransform: null, /** * If the local transform is update from SRT(scale, rotation, translation, which is position here) each frame * @type {boolean} */ autoUpdateLocalTransform: true, /** * Parent of current scene node * @type {?clay.Node} * @private */ _parent: null, /** * The root scene mounted. Null if it is a isolated node * @type {?clay.Scene} * @private */ _scene: null, /** * @type {boolean} * @private */ _needsUpdateWorldTransform: true, /** * @type {boolean} * @private */ _inIterating: false, // Depth for transparent list sorting __depth: 0 }, function () { if (!this.name) { this.name = (this.type || 'NODE') + '_' + (nameId++); } if (!this.position) { this.position = new Vector3(); } if (!this.rotation) { this.rotation = new Quaternion(); } if (!this.scale) { this.scale = new Vector3(1, 1, 1); } this.worldTransform = new Matrix4(); this.localTransform = new Matrix4(); this._children = []; }, /**@lends clay.Node.prototype. */ { /** * @type {?clay.Vector3} * @instance */ target: null, /** * If node and its chilren invisible * @type {boolean} * @instance */ invisible: false, /** * If Node is a skinned mesh * @return {boolean} */ isSkinnedMesh: function () { return false; }, /** * Return true if it is a renderable scene node, like Mesh and ParticleSystem * @return {boolean} */ isRenderable: function () { return false; }, /** * Set the name of the scene node * @param {string} name */ setName: function (name) { var scene = this._scene; if (scene) { var nodeRepository = scene._nodeRepository; delete nodeRepository[this.name]; nodeRepository[name] = this; } this.name = name; }, /** * Add a child node * @param {clay.Node} node */ add: function (node) { var originalParent = node._parent; if (originalParent === this) { return; } if (originalParent) { originalParent.remove(node); } node._parent = this; this._children.push(node); var scene = this._scene; if (scene && scene !== node.scene) { node.traverse(this._addSelfToScene, this); } // Mark children needs update transform // In case child are remove and added again after parent moved node._needsUpdateWorldTransform = true; }, /** * Remove the given child scene node * @param {clay.Node} node */ remove: function (node) { var children = this._children; var idx = children.indexOf(node); if (idx < 0) { return; } children.splice(idx, 1); node._parent = null; if (this._scene) { node.traverse(this._removeSelfFromScene, this); } }, /** * Remove all children */ removeAll: function () { var children = this._children; for (var idx = 0; idx < children.length; idx++) { children[idx]._parent = null; if (this._scene) { children[idx].traverse(this._removeSelfFromScene, this); } } this._children = []; }, /** * Get the scene mounted * @return {clay.Scene} */ getScene: function () { return this._scene; }, /** * Get parent node * @return {clay.Scene} */ getParent: function () { return this._parent; }, _removeSelfFromScene: function (descendant) { descendant._scene.removeFromScene(descendant); descendant._scene = null; }, _addSelfToScene: function (descendant) { this._scene.addToScene(descendant); descendant._scene = this._scene; }, /** * Return true if it is ancestor of the given scene node * @param {clay.Node} node */ isAncestor: function (node) { var parent = node._parent; while(parent) { if (parent === this) { return true; } parent = parent._parent; } return false; }, /** * Get a new created array of all children nodes * @return {clay.Node[]} */ children: function () { return this._children.slice(); }, /** * Get child scene node at given index. * @param {number} idx * @return {clay.Node} */ childAt: function (idx) { return this._children[idx]; }, /** * Get first child with the given name * @param {string} name * @return {clay.Node} */ getChildByName: function (name) { var children = this._children; for (var i = 0; i < children.length; i++) { if (children[i].name === name) { return children[i]; } } }, /** * Get first descendant have the given name * @param {string} name * @return {clay.Node} */ getDescendantByName: function (name) { var children = this._children; for (var i = 0; i < children.length; i++) { var child = children[i]; if (child.name === name) { return child; } else { var res = child.getDescendantByName(name); if (res) { return res; } } } }, /** * Query descendant node by path * @param {string} path * @return {clay.Node} * @example * node.queryNode('root/parent/child'); */ queryNode: function (path) { if (!path) { return; } // TODO Name have slash ? var pathArr = path.split('/'); var current = this; for (var i = 0; i < pathArr.length; i++) { var name = pathArr[i]; // Skip empty if (!name) { continue; } var found = false; var children = current._children; for (var j = 0; j < children.length; j++) { var child = children[j]; if (child.name === name) { current = child; found = true; break; } } // Early return if not found if (!found) { return; } } return current; }, /** * Get query path, relative to rootNode(default is scene) * @param {clay.Node} [rootNode] * @return {string} */ getPath: function (rootNode) { if (!this._parent) { return '/'; } var current = this._parent; var path = this.name; while (current._parent) { path = current.name + '/' + path; if (current._parent == rootNode) { break; } current = current._parent; } if (!current._parent && rootNode) { return null; } return path; }, /** * Depth first traverse all its descendant scene nodes. * * **WARN** Don't do `add`, `remove` operation in the callback during traverse. * @param {Function} callback * @param {Node} [context] */ traverse: function (callback, context) { callback.call(context, this); var _children = this._children; for(var i = 0, len = _children.length; i < len; i++) { _children[i].traverse(callback, context); } }, /** * Traverse all children nodes. * * **WARN** DON'T do `add`, `remove` operation in the callback during iteration. * * @param {Function} callback * @param {Node} [context] */ eachChild: function (callback, context) { var _children = this._children; for(var i = 0, len = _children.length; i < len; i++) { var child = _children[i]; callback.call(context, child, i); } }, /** * Set the local transform and decompose to SRT * @param {clay.Matrix4} matrix */ setLocalTransform: function (matrix) { mat4.copy(this.localTransform.array, matrix.array); this.decomposeLocalTransform(); }, /** * Decompose the local transform to SRT */ decomposeLocalTransform: function (keepScale) { var scale = !keepScale ? this.scale: null; this.localTransform.decomposeMatrix(scale, this.rotation, this.position); }, /** * Set the world transform and decompose to SRT * @param {clay.Matrix4} matrix */ setWorldTransform: function (matrix) { mat4.copy(this.worldTransform.array, matrix.array); this.decomposeWorldTransform(); }, /** * Decompose the world transform to SRT * @function */ decomposeWorldTransform: (function () { var tmp = mat4.create(); return function (keepScale) { var localTransform = this.localTransform; var worldTransform = this.worldTransform; // Assume world transform is updated if (this._parent) { mat4.invert(tmp, this._parent.worldTransform.array); mat4.multiply(localTransform.array, tmp, worldTransform.array); } else { mat4.copy(localTransform.array, worldTransform.array); } var scale = !keepScale ? this.scale: null; localTransform.decomposeMatrix(scale, this.rotation, this.position); }; })(), transformNeedsUpdate: function () { return this.position._dirty || this.rotation._dirty || this.scale._dirty; }, /** * Update local transform from SRT * Notice that local transform will not be updated if _dirty mark of position, rotation, scale is all false */ updateLocalTransform: function () { var position = this.position; var rotation = this.rotation; var scale = this.scale; if (this.transformNeedsUpdate()) { var m = this.localTransform.array; // Transform order, scale->rotation->position mat4.fromRotationTranslation(m, rotation.array, position.array); mat4.scale(m, m, scale.array); rotation._dirty = false; scale._dirty = false; position._dirty = false; this._needsUpdateWorldTransform = true; } }, /** * Update world transform, assume its parent world transform have been updated * @private */ _updateWorldTransformTopDown: function () { var localTransform = this.localTransform.array; var worldTransform = this.worldTransform.array; if (this._parent) { mat4.multiplyAffine( worldTransform, this._parent.worldTransform.array, localTransform ); } else { mat4.copy(worldTransform, localTransform); } }, /** * Update world transform before whole scene is updated. */ updateWorldTransform: function () { // Find the root node which transform needs update; var rootNodeIsDirty = this; while (rootNodeIsDirty && rootNodeIsDirty.getParent() && rootNodeIsDirty.getParent().transformNeedsUpdate() ) { rootNodeIsDirty = rootNodeIsDirty.getParent(); } rootNodeIsDirty.update(); }, /** * Update local transform and world transform recursively * @param {boolean} forceUpdateWorld */ update: function (forceUpdateWorld) { if (this.autoUpdateLocalTransform) { this.updateLocalTransform(); } else { // Transform is manually setted forceUpdateWorld = true; } if (forceUpdateWorld || this._needsUpdateWorldTransform) { this._updateWorldTransformTopDown(); forceUpdateWorld = true; this._needsUpdateWorldTransform = false; } var children = this._children; for(var i = 0, len = children.length; i < len; i++) { children[i].update(forceUpdateWorld); } }, /** * Get bounding box of node * @param {Function} [filter] * @param {clay.BoundingBox} [out] * @return {clay.BoundingBox} */ // TODO Skinning getBoundingBox: (function () { function defaultFilter (el) { return !el.invisible && el.geometry; } var tmpBBox = new BoundingBox(); var tmpMat4 = new Matrix4(); var invWorldTransform = new Matrix4(); return function (filter, out) { out = out || new BoundingBox(); filter = filter || defaultFilter; if (this._parent) { Matrix4.invert(invWorldTransform, this._parent.worldTransform); } else { Matrix4.identity(invWorldTransform); } this.traverse(function (mesh) { if (mesh.geometry && mesh.geometry.boundingBox) { tmpBBox.copy(mesh.geometry.boundingBox); Matrix4.multiply(tmpMat4, invWorldTransform, mesh.worldTransform); tmpBBox.applyTransform(tmpMat4); out.union(tmpBBox); } }, this, defaultFilter); return out; }; })(), /** * Get world position, extracted from world transform * @param {clay.Vector3} [out] * @return {clay.Vector3} */ getWorldPosition: function (out) { // PENDING if (this.transformNeedsUpdate()) { this.updateWorldTransform(); } var m = this.worldTransform.array; if (out) { var arr = out.array; arr[0] = m[12]; arr[1] = m[13]; arr[2] = m[14]; return out; } else { return new Vector3(m[12], m[13], m[14]); } }, /** * Clone a new node * @return {Node} */ clone: function () { var node = new this.constructor(); var children = this._children; node.setName(this.name); node.position.copy(this.position); node.rotation.copy(this.rotation); node.scale.copy(this.scale); for (var i = 0; i < children.length; i++) { node.add(children[i].clone()); } return node; }, /** * Rotate the node around a axis by angle degrees, axis passes through point * @param {clay.Vector3} point Center point * @param {clay.Vector3} axis Center axis * @param {number} angle Rotation angle * @see http://docs.unity3d.com/Documentation/ScriptReference/Transform.RotateAround.html * @function */ rotateAround: (function () { var v = new Vector3(); var RTMatrix = new Matrix4(); // TODO improve performance return function (point, axis, angle) { v.copy(this.position).subtract(point); var localTransform = this.localTransform; localTransform.identity(); // parent node localTransform.translate(point); localTransform.rotate(angle, axis); RTMatrix.fromRotationTranslation(this.rotation, v); localTransform.multiply(RTMatrix); localTransform.scale(this.scale); this.decomposeLocalTransform(); this._needsUpdateWorldTransform = true; }; })(), /** * @param {clay.Vector3} target * @param {clay.Vector3} [up] * @see http://www.opengl.org/sdk/docs/man2/xhtml/gluLookAt.xml * @function */ lookAt: (function () { var m = new Matrix4(); return function (target, up) { m.lookAt(this.position, target, up || this.localTransform.y).invert(); this.setLocalTransform(m); this.target = target; }; })() }); var calcAmbientSHLightEssl = "vec3 calcAmbientSHLight(int idx, vec3 N) {\n int offset = 9 * idx;\n return ambientSHLightCoefficients[0]\n + ambientSHLightCoefficients[1] * N.x\n + ambientSHLightCoefficients[2] * N.y\n + ambientSHLightCoefficients[3] * N.z\n + ambientSHLightCoefficients[4] * N.x * N.z\n + ambientSHLightCoefficients[5] * N.z * N.y\n + ambientSHLightCoefficients[6] * N.y * N.x\n + ambientSHLightCoefficients[7] * (3.0 * N.z * N.z - 1.0)\n + ambientSHLightCoefficients[8] * (N.x * N.x - N.y * N.y);\n}"; var uniformVec3Prefix = 'uniform vec3 '; var uniformFloatPrefix = 'uniform float '; var exportHeaderPrefix = '@export clay.header.'; var exportEnd = '@end'; var unconfigurable = ':unconfigurable;'; var lightEssl = [ exportHeaderPrefix + 'directional_light', uniformVec3Prefix + 'directionalLightDirection[DIRECTIONAL_LIGHT_COUNT]' + unconfigurable, uniformVec3Prefix + 'directionalLightColor[DIRECTIONAL_LIGHT_COUNT]' + unconfigurable, exportEnd, exportHeaderPrefix + 'ambient_light', uniformVec3Prefix + 'ambientLightColor[AMBIENT_LIGHT_COUNT]' + unconfigurable, exportEnd, exportHeaderPrefix + 'ambient_sh_light', uniformVec3Prefix + 'ambientSHLightColor[AMBIENT_SH_LIGHT_COUNT]' + unconfigurable, uniformVec3Prefix + 'ambientSHLightCoefficients[AMBIENT_SH_LIGHT_COUNT * 9]' + unconfigurable, calcAmbientSHLightEssl, exportEnd, exportHeaderPrefix + 'ambient_cubemap_light', uniformVec3Prefix + 'ambientCubemapLightColor[AMBIENT_CUBEMAP_LIGHT_COUNT]' + unconfigurable, 'uniform samplerCube ambientCubemapLightCubemap[AMBIENT_CUBEMAP_LIGHT_COUNT]' + unconfigurable, 'uniform sampler2D ambientCubemapLightBRDFLookup[AMBIENT_CUBEMAP_LIGHT_COUNT]' + unconfigurable, exportEnd, exportHeaderPrefix + 'point_light', uniformVec3Prefix + 'pointLightPosition[POINT_LIGHT_COUNT]' + unconfigurable, uniformFloatPrefix + 'pointLightRange[POINT_LIGHT_COUNT]' + unconfigurable, uniformVec3Prefix + 'pointLightColor[POINT_LIGHT_COUNT]' + unconfigurable, exportEnd, exportHeaderPrefix + 'spot_light', uniformVec3Prefix + 'spotLightPosition[SPOT_LIGHT_COUNT]' + unconfigurable, uniformVec3Prefix + 'spotLightDirection[SPOT_LIGHT_COUNT]' + unconfigurable, uniformFloatPrefix + 'spotLightRange[SPOT_LIGHT_COUNT]' + unconfigurable, uniformFloatPrefix + 'spotLightUmbraAngleCosine[SPOT_LIGHT_COUNT]' + unconfigurable, uniformFloatPrefix + 'spotLightPenumbraAngleCosine[SPOT_LIGHT_COUNT]' + unconfigurable, uniformFloatPrefix + 'spotLightFalloffFactor[SPOT_LIGHT_COUNT]' + unconfigurable, uniformVec3Prefix + 'spotLightColor[SPOT_LIGHT_COUNT]' + unconfigurable, exportEnd ].join('\n'); Shader['import'](lightEssl); /** * @constructor clay.Light * @extends clay.Node */ var Light = Node.extend(function(){ return /** @lends clay.Light# */ { /** * Light RGB color * @type {number[]} */ color: [1, 1, 1], /** * Light intensity * @type {number} */ intensity: 1.0, // Config for shadow map /** * If light cast shadow * @type {boolean} */ castShadow: true, /** * Shadow map size * @type {number} */ shadowResolution: 512, /** * Light group, shader with same `lightGroup` will be affected * * Only useful in forward rendering * @type {number} */ group: 0 }; }, /** @lends clay.Light.prototype. */ { /** * Light type * @type {string} * @memberOf clay.Light# */ type: '', /** * @return {clay.Light} * @memberOf clay.Light.prototype */ clone: function() { var light = Node.prototype.clone.call(this); light.color = Array.prototype.slice.call(this.color); light.intensity = this.intensity; light.castShadow = this.castShadow; light.shadowResolution = this.shadowResolution; return light; } }); /** * @constructor * @alias clay.Plane * @param {clay.Vector3} [normal] * @param {number} [distance] */ var Plane$1 = function(normal, distance) { /** * Normal of the plane * @type {clay.Vector3} */ this.normal = normal || new Vector3(0, 1, 0); /** * Constant of the plane equation, used as distance to the origin * @type {number} */ this.distance = distance || 0; }; Plane$1.prototype = { constructor: Plane$1, /** * Distance from a given point to the plane * @param {clay.Vector3} point * @return {number} */ distanceToPoint: function(point) { return vec3.dot(point.array, this.normal.array) - this.distance; }, /** * Calculate the projection point on the plane * @param {clay.Vector3} point * @param {clay.Vector3} out * @return {clay.Vector3} */ projectPoint: function(point, out) { if (!out) { out = new Vector3(); } var d = this.distanceToPoint(point); vec3.scaleAndAdd(out.array, point.array, this.normal.array, -d); out._dirty = true; return out; }, /** * Normalize the plane's normal and calculate the distance */ normalize: function() { var invLen = 1 / vec3.len(this.normal.array); vec3.scale(this.normal.array, invLen); this.distance *= invLen; }, /** * If the plane intersect a frustum * @param {clay.Frustum} Frustum * @return {boolean} */ intersectFrustum: function(frustum) { // Check if all coords of frustum is on plane all under plane var coords = frustum.vertices; var normal = this.normal.array; var onPlane = vec3.dot(coords[0].array, normal) > this.distance; for (var i = 1; i < 8; i++) { if ((vec3.dot(coords[i].array, normal) > this.distance) != onPlane) { return true; } } }, /** * Calculate the intersection point between plane and a given line * @function * @param {clay.Vector3} start start point of line * @param {clay.Vector3} end end point of line * @param {clay.Vector3} [out] * @return {clay.Vector3} */ intersectLine: (function() { var rd = vec3.create(); return function(start, end, out) { var d0 = this.distanceToPoint(start); var d1 = this.distanceToPoint(end); if ((d0 > 0 && d1 > 0) || (d0 < 0 && d1 < 0)) { return null; } // Ray intersection var pn = this.normal.array; var d = this.distance; var ro = start.array; // direction vec3.sub(rd, end.array, start.array); vec3.normalize(rd, rd); var divider = vec3.dot(pn, rd); // ray is parallel to the plane if (divider === 0) { return null; } if (!out) { out = new Vector3(); } var t = (vec3.dot(pn, ro) - d) / divider; vec3.scaleAndAdd(out.array, ro, rd, -t); out._dirty = true; return out; }; })(), /** * Apply an affine transform matrix to plane * @function * @return {clay.Matrix4} */ applyTransform: (function() { var inverseTranspose = mat4.create(); var normalv4 = vec4.create(); var pointv4 = vec4.create(); pointv4[3] = 1; return function(m4) { m4 = m4.array; // Transform point on plane vec3.scale(pointv4, this.normal.array, this.distance); vec4.transformMat4(pointv4, pointv4, m4); this.distance = vec3.dot(pointv4, this.normal.array); // Transform plane normal mat4.invert(inverseTranspose, m4); mat4.transpose(inverseTranspose, inverseTranspose); normalv4[3] = 0; vec3.copy(normalv4, this.normal.array); vec4.transformMat4(normalv4, normalv4, inverseTranspose); vec3.copy(this.normal.array, normalv4); }; })(), /** * Copy from another plane * @param {clay.Vector3} plane */ copy: function(plane) { vec3.copy(this.normal.array, plane.normal.array); this.normal._dirty = true; this.distance = plane.distance; }, /** * Clone a new plane * @return {clay.Plane} */ clone: function() { var plane = new Plane$1(); plane.copy(this); return plane; } }; var vec3Set$1 = vec3.set; var vec3Copy$1 = vec3.copy; var vec3TranformMat4 = vec3.transformMat4; var mathMin = Math.min; var mathMax = Math.max; /** * @constructor * @alias clay.Frustum */ var Frustum = function() { /** * Eight planes to enclose the frustum * @type {clay.Plane[]} */ this.planes = []; for (var i = 0; i < 6; i++) { this.planes.push(new Plane$1()); } /** * Bounding box of frustum * @type {clay.BoundingBox} */ this.boundingBox = new BoundingBox(); /** * Eight vertices of frustum * @type {Float32Array[]} */ this.vertices = []; for (var i = 0; i < 8; i++) { this.vertices[i] = vec3.fromValues(0, 0, 0); } }; Frustum.prototype = { // http://web.archive.org/web/20120531231005/http://crazyjoke.free.fr/doc/3D/plane%20extraction.pdf /** * Set frustum from a projection matrix * @param {clay.Matrix4} projectionMatrix */ setFromProjection: function(projectionMatrix) { var planes = this.planes; var m = projectionMatrix.array; var m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3]; var m4 = m[4], m5 = m[5], m6 = m[6], m7 = m[7]; var m8 = m[8], m9 = m[9], m10 = m[10], m11 = m[11]; var m12 = m[12], m13 = m[13], m14 = m[14], m15 = m[15]; // Update planes vec3Set$1(planes[0].normal.array, m3 - m0, m7 - m4, m11 - m8); planes[0].distance = -(m15 - m12); planes[0].normalize(); vec3Set$1(planes[1].normal.array, m3 + m0, m7 + m4, m11 + m8); planes[1].distance = -(m15 + m12); planes[1].normalize(); vec3Set$1(planes[2].normal.array, m3 + m1, m7 + m5, m11 + m9); planes[2].distance = -(m15 + m13); planes[2].normalize(); vec3Set$1(planes[3].normal.array, m3 - m1, m7 - m5, m11 - m9); planes[3].distance = -(m15 - m13); planes[3].normalize(); vec3Set$1(planes[4].normal.array, m3 - m2, m7 - m6, m11 - m10); planes[4].distance = -(m15 - m14); planes[4].normalize(); vec3Set$1(planes[5].normal.array, m3 + m2, m7 + m6, m11 + m10); planes[5].distance = -(m15 + m14); planes[5].normalize(); // Perspective projection var boundingBox = this.boundingBox; var vertices = this.vertices; if (m15 === 0) { var aspect = m5 / m0; var zNear = -m14 / (m10 - 1); var zFar = -m14 / (m10 + 1); var farY = -zFar / m5; var nearY = -zNear / m5; // Update bounding box boundingBox.min.set(-farY * aspect, -farY, zFar); boundingBox.max.set(farY * aspect, farY, zNear); // update vertices //--- min z // min x vec3Set$1(vertices[0], -farY * aspect, -farY, zFar); vec3Set$1(vertices[1], -farY * aspect, farY, zFar); // max x vec3Set$1(vertices[2], farY * aspect, -farY, zFar); vec3Set$1(vertices[3], farY * aspect, farY, zFar); //-- max z vec3Set$1(vertices[4], -nearY * aspect, -nearY, zNear); vec3Set$1(vertices[5], -nearY * aspect, nearY, zNear); vec3Set$1(vertices[6], nearY * aspect, -nearY, zNear); vec3Set$1(vertices[7], nearY * aspect, nearY, zNear); } else { // Orthographic projection var left = (-1 - m12) / m0; var right = (1 - m12) / m0; var top = (1 - m13) / m5; var bottom = (-1 - m13) / m5; var near = (-1 - m14) / m10; var far = (1 - m14) / m10; boundingBox.min.set(Math.min(left, right), Math.min(bottom, top), Math.min(far, near)); boundingBox.max.set(Math.max(right, left), Math.max(top, bottom), Math.max(near, far)); var min = boundingBox.min.array; var max = boundingBox.max.array; //--- min z // min x vec3Set$1(vertices[0], min[0], min[1], min[2]); vec3Set$1(vertices[1], min[0], max[1], min[2]); // max x vec3Set$1(vertices[2], max[0], min[1], min[2]); vec3Set$1(vertices[3], max[0], max[1], min[2]); //-- max z vec3Set$1(vertices[4], min[0], min[1], max[2]); vec3Set$1(vertices[5], min[0], max[1], max[2]); vec3Set$1(vertices[6], max[0], min[1], max[2]); vec3Set$1(vertices[7], max[0], max[1], max[2]); } }, /** * Apply a affine transform matrix and set to the given bounding box * @function * @param {clay.BoundingBox} * @param {clay.Matrix4} * @return {clay.BoundingBox} */ getTransformedBoundingBox: (function() { var tmpVec3 = vec3.create(); return function(bbox, matrix) { var vertices = this.vertices; var m4 = matrix.array; var min = bbox.min; var max = bbox.max; var minArr = min.array; var maxArr = max.array; var v = vertices[0]; vec3TranformMat4(tmpVec3, v, m4); vec3Copy$1(minArr, tmpVec3); vec3Copy$1(maxArr, tmpVec3); for (var i = 1; i < 8; i++) { v = vertices[i]; vec3TranformMat4(tmpVec3, v, m4); minArr[0] = mathMin(tmpVec3[0], minArr[0]); minArr[1] = mathMin(tmpVec3[1], minArr[1]); minArr[2] = mathMin(tmpVec3[2], minArr[2]); maxArr[0] = mathMax(tmpVec3[0], maxArr[0]); maxArr[1] = mathMax(tmpVec3[1], maxArr[1]); maxArr[2] = mathMax(tmpVec3[2], maxArr[2]); } min._dirty = true; max._dirty = true; return bbox; }; }) () }; var EPSILON$1 = 1e-5; /** * @constructor * @alias clay.Ray * @param {clay.Vector3} [origin] * @param {clay.Vector3} [direction] */ var Ray = function (origin, direction) { /** * @type {clay.Vector3} */ this.origin = origin || new Vector3(); /** * @type {clay.Vector3} */ this.direction = direction || new Vector3(); }; Ray.prototype = { constructor: Ray, // http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm /** * Calculate intersection point between ray and a give plane * @param {clay.Plane} plane * @param {clay.Vector3} [out] * @return {clay.Vector3} */ intersectPlane: function (plane, out) { var pn = plane.normal.array; var d = plane.distance; var ro = this.origin.array; var rd = this.direction.array; var divider = vec3.dot(pn, rd); // ray is parallel to the plane if (divider === 0) { return null; } if (!out) { out = new Vector3(); } var t = (vec3.dot(pn, ro) - d) / divider; vec3.scaleAndAdd(out.array, ro, rd, -t); out._dirty = true; return out; }, /** * Mirror the ray against plane * @param {clay.Plane} plane */ mirrorAgainstPlane: function (plane) { // Distance to plane var d = vec3.dot(plane.normal.array, this.direction.array); vec3.scaleAndAdd(this.direction.array, this.direction.array, plane.normal.array, -d * 2); this.direction._dirty = true; }, distanceToPoint: (function () { var v = vec3.create(); return function (point) { vec3.sub(v, point, this.origin.array); // Distance from projection point to origin var b = vec3.dot(v, this.direction.array); if (b < 0) { return vec3.distance(this.origin.array, point); } // Squared distance from center to origin var c2 = vec3.lenSquared(v); // Squared distance from center to projection point return Math.sqrt(c2 - b * b); }; })(), /** * Calculate intersection point between ray and sphere * @param {clay.Vector3} center * @param {number} radius * @param {clay.Vector3} out * @return {clay.Vector3} */ intersectSphere: (function () { var v = vec3.create(); return function (center, radius, out) { var origin = this.origin.array; var direction = this.direction.array; center = center.array; vec3.sub(v, center, origin); // Distance from projection point to origin var b = vec3.dot(v, direction); // Squared distance from center to origin var c2 = vec3.squaredLength(v); // Squared distance from center to projection point var d2 = c2 - b * b; var r2 = radius * radius; // No intersection if (d2 > r2) { return; } var a = Math.sqrt(r2 - d2); // First intersect point var t0 = b - a; // Second intersect point var t1 = b + a; if (!out) { out = new Vector3(); } if (t0 < 0) { if (t1 < 0) { return null; } else { vec3.scaleAndAdd(out.array, origin, direction, t1); return out; } } else { vec3.scaleAndAdd(out.array, origin, direction, t0); return out; } }; })(), // http://www.scratchapixel.com/lessons/3d-basic-lessons/lesson-7-intersecting-simple-shapes/ray-box-intersection/ /** * Calculate intersection point between ray and bounding box * @param {clay.BoundingBox} bbox * @param {clay.Vector3} * @return {clay.Vector3} */ intersectBoundingBox: function (bbox, out) { var dir = this.direction.array; var origin = this.origin.array; var min = bbox.min.array; var max = bbox.max.array; var invdirx = 1 / dir[0]; var invdiry = 1 / dir[1]; var invdirz = 1 / dir[2]; var tmin, tmax, tymin, tymax, tzmin, tzmax; if (invdirx >= 0) { tmin = (min[0] - origin[0]) * invdirx; tmax = (max[0] - origin[0]) * invdirx; } else { tmax = (min[0] - origin[0]) * invdirx; tmin = (max[0] - origin[0]) * invdirx; } if (invdiry >= 0) { tymin = (min[1] - origin[1]) * invdiry; tymax = (max[1] - origin[1]) * invdiry; } else { tymax = (min[1] - origin[1]) * invdiry; tymin = (max[1] - origin[1]) * invdiry; } if ((tmin > tymax) || (tymin > tmax)) { return null; } if (tymin > tmin || tmin !== tmin) { tmin = tymin; } if (tymax < tmax || tmax !== tmax) { tmax = tymax; } if (invdirz >= 0) { tzmin = (min[2] - origin[2]) * invdirz; tzmax = (max[2] - origin[2]) * invdirz; } else { tzmax = (min[2] - origin[2]) * invdirz; tzmin = (max[2] - origin[2]) * invdirz; } if ((tmin > tzmax) || (tzmin > tmax)) { return null; } if (tzmin > tmin || tmin !== tmin) { tmin = tzmin; } if (tzmax < tmax || tmax !== tmax) { tmax = tzmax; } if (tmax < 0) { return null; } var t = tmin >= 0 ? tmin : tmax; if (!out) { out = new Vector3(); } vec3.scaleAndAdd(out.array, origin, dir, t); return out; }, // http://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm /** * Calculate intersection point between ray and three triangle vertices * @param {clay.Vector3} a * @param {clay.Vector3} b * @param {clay.Vector3} c * @param {boolean} singleSided, CW triangle will be ignored * @param {clay.Vector3} [out] * @param {clay.Vector3} [barycenteric] barycentric coords * @return {clay.Vector3} */ intersectTriangle: (function () { var eBA = vec3.create(); var eCA = vec3.create(); var AO = vec3.create(); var vCross = vec3.create(); return function (a, b, c, singleSided, out, barycenteric) { var dir = this.direction.array; var origin = this.origin.array; a = a.array; b = b.array; c = c.array; vec3.sub(eBA, b, a); vec3.sub(eCA, c, a); vec3.cross(vCross, eCA, dir); var det = vec3.dot(eBA, vCross); if (singleSided) { if (det > -EPSILON$1) { return null; } } else { if (det > -EPSILON$1 && det < EPSILON$1) { return null; } } vec3.sub(AO, origin, a); var u = vec3.dot(vCross, AO) / det; if (u < 0 || u > 1) { return null; } vec3.cross(vCross, eBA, AO); var v = vec3.dot(dir, vCross) / det; if (v < 0 || v > 1 || (u + v > 1)) { return null; } vec3.cross(vCross, eBA, eCA); var t = -vec3.dot(AO, vCross) / det; if (t < 0) { return null; } if (!out) { out = new Vector3(); } if (barycenteric) { Vector3.set(barycenteric, (1 - u - v), u, v); } vec3.scaleAndAdd(out.array, origin, dir, t); return out; }; })(), /** * Apply an affine transform matrix to the ray * @return {clay.Matrix4} matrix */ applyTransform: function (matrix) { Vector3.add(this.direction, this.direction, this.origin); Vector3.transformMat4(this.origin, this.origin, matrix); Vector3.transformMat4(this.direction, this.direction, matrix); Vector3.sub(this.direction, this.direction, this.origin); Vector3.normalize(this.direction, this.direction); }, /** * Copy values from another ray * @param {clay.Ray} ray */ copy: function (ray) { Vector3.copy(this.origin, ray.origin); Vector3.copy(this.direction, ray.direction); }, /** * Clone a new ray * @return {clay.Ray} */ clone: function () { var ray = new Ray(); ray.copy(this); return ray; } }; /** * @constructor clay.Camera * @extends clay.Node */ var Camera = Node.extend(function () { return /** @lends clay.Camera# */ { /** * Camera projection matrix * @type {clay.Matrix4} */ projectionMatrix: new Matrix4(), /** * Inverse of camera projection matrix * @type {clay.Matrix4} */ invProjectionMatrix: new Matrix4(), /** * View matrix, equal to inverse of camera's world matrix * @type {clay.Matrix4} */ viewMatrix: new Matrix4(), /** * Camera frustum in view space * @type {clay.Frustum} */ frustum: new Frustum() }; }, function () { this.update(true); }, /** @lends clay.Camera.prototype */ { update: function (force) { Node.prototype.update.call(this, force); Matrix4.invert(this.viewMatrix, this.worldTransform); this.updateProjectionMatrix(); Matrix4.invert(this.invProjectionMatrix, this.projectionMatrix); this.frustum.setFromProjection(this.projectionMatrix); }, /** * Set camera view matrix */ setViewMatrix: function (viewMatrix) { Matrix4.copy(this.viewMatrix, viewMatrix); Matrix4.invert(this.worldTransform, viewMatrix); this.decomposeWorldTransform(); }, /** * Decompose camera projection matrix */ decomposeProjectionMatrix: function () {}, /** * Set camera projection matrix * @param {clay.Matrix4} projectionMatrix */ setProjectionMatrix: function (projectionMatrix) { Matrix4.copy(this.projectionMatrix, projectionMatrix); Matrix4.invert(this.invProjectionMatrix, projectionMatrix); this.decomposeProjectionMatrix(); }, /** * Update projection matrix, called after update */ updateProjectionMatrix: function () {}, /** * Cast a picking ray from camera near plane to far plane * @function * @param {clay.Vector2} ndc * @param {clay.Ray} [out] * @return {clay.Ray} */ castRay: (function () { var v4 = vec4.create(); return function (ndc, out) { var ray = out !== undefined ? out : new Ray(); var x = ndc.array[0]; var y = ndc.array[1]; vec4.set(v4, x, y, -1, 1); vec4.transformMat4(v4, v4, this.invProjectionMatrix.array); vec4.transformMat4(v4, v4, this.worldTransform.array); vec3.scale(ray.origin.array, v4, 1 / v4[3]); vec4.set(v4, x, y, 1, 1); vec4.transformMat4(v4, v4, this.invProjectionMatrix.array); vec4.transformMat4(v4, v4, this.worldTransform.array); vec3.scale(v4, v4, 1 / v4[3]); vec3.sub(ray.direction.array, v4, ray.origin.array); vec3.normalize(ray.direction.array, ray.direction.array); ray.direction._dirty = true; ray.origin._dirty = true; return ray; }; })(), /** * @function * @name clone * @return {clay.Camera} * @memberOf clay.Camera.prototype */ }); var IDENTITY = mat4.create(); var WORLDVIEW = mat4.create(); var programKeyCache$1 = {}; function getProgramKey$1(lightNumbers) { var defineStr = []; var lightTypes = Object.keys(lightNumbers); lightTypes.sort(); for (var i = 0; i < lightTypes.length; i++) { var lightType = lightTypes[i]; defineStr.push(lightType + ' ' + lightNumbers[lightType]); } var key = defineStr.join('\n'); if (programKeyCache$1[key]) { return programKeyCache$1[key]; } var id = util$1.genGUID(); programKeyCache$1[key] = id; return id; } function RenderList() { this.opaque = []; this.transparent = []; this._opaqueCount = 0; this._transparentCount = 0; } RenderList.prototype.startCount = function () { this._opaqueCount = 0; this._transparentCount = 0; }; RenderList.prototype.add = function (object, isTransparent) { if (isTransparent) { this.transparent[this._transparentCount++] = object; } else { this.opaque[this._opaqueCount++] = object; } }; RenderList.prototype.endCount = function () { this.transparent.length = this._transparentCount; this.opaque.length = this._opaqueCount; }; /** * @typedef {Object} clay.Scene.RenderList * @property {Array.<clay.Renderable>} opaque * @property {Array.<clay.Renderable>} transparent */ /** * @constructor clay.Scene * @extends clay.Node */ var Scene = Node.extend(function () { return /** @lends clay.Scene# */ { /** * Global material of scene * @type {clay.Material} */ material: null, lights: [], /** * Scene bounding box in view space. * Used when camera needs to adujst the near and far plane automatically * so that the view frustum contains the visible objects as tightly as possible. * Notice: * It is updated after rendering (in the step of frustum culling passingly). So may be not so accurate, but saves a lot of calculation * * @type {clay.BoundingBox} */ viewBoundingBoxLastFrame: new BoundingBox(), // Uniforms for shadow map. shadowUniforms: {}, _cameraList: [], // Properties to save the light information in the scene // Will be set in the render function _lightUniforms: {}, _previousLightNumber: {}, _lightNumber: { // groupId: { // POINT_LIGHT: 0, // DIRECTIONAL_LIGHT: 0, // SPOT_LIGHT: 0, // AMBIENT_LIGHT: 0, // AMBIENT_SH_LIGHT: 0 // } }, _lightProgramKeys: {}, _nodeRepository: {}, _renderLists: new LRU$1(20) }; }, function () { this._scene = this; }, /** @lends clay.Scene.prototype. */ { // Add node to scene addToScene: function (node) { if (node instanceof Camera) { if (this._cameraList.length > 0) { console.warn('Found multiple camera in one scene. Use the fist one.'); } this._cameraList.push(node); } else if (node instanceof Light) { this.lights.push(node); } if (node.name) { this._nodeRepository[node.name] = node; } }, // Remove node from scene removeFromScene: function (node) { var idx; if (node instanceof Camera) { idx = this._cameraList.indexOf(node); if (idx >= 0) { this._cameraList.splice(idx, 1); } } else if (node instanceof Light) { idx = this.lights.indexOf(node); if (idx >= 0) { this.lights.splice(idx, 1); } } if (node.name) { delete this._nodeRepository[node.name]; } }, /** * Get node by name * @param {string} name * @return {Node} * @DEPRECATED */ getNode: function (name) { return this._nodeRepository[name]; }, /** * Set main camera of the scene. * @param {claygl.Camera} camera */ setMainCamera: function (camera) { var idx = this._cameraList.indexOf(camera); if (idx >= 0) { this._cameraList.splice(idx, 1); } this._cameraList.unshift(camera); }, /** * Get main camera of the scene. */ getMainCamera: function () { return this._cameraList[0]; }, getLights: function () { return this.lights; }, updateLights: function () { var lights = this.lights; this._previousLightNumber = this._lightNumber; var lightNumber = {}; for (var i = 0; i < lights.length; i++) { var light = lights[i]; if (light.invisible) { continue; } var group = light.group; if (!lightNumber[group]) { lightNumber[group] = {}; } // User can use any type of light lightNumber[group][light.type] = lightNumber[group][light.type] || 0; lightNumber[group][light.type]++; } this._lightNumber = lightNumber; for (var groupId in lightNumber) { this._lightProgramKeys[groupId] = getProgramKey$1(lightNumber[groupId]); } this._updateLightUniforms(); }, /** * Clone a node and it's children, including mesh, camera, light, etc. * Unlike using `Node#clone`. It will clone skeleton and remap the joints. Material will also be cloned. * * @param {clay.Node} node * @return {clay.Node} */ cloneNode: function (node) { var newNode = node.clone(); var clonedNodesMap = {}; function buildNodesMap(sNode, tNode) { clonedNodesMap[sNode.__uid__] = tNode; for (var i = 0; i < sNode._children.length; i++) { var sChild = sNode._children[i]; var tChild = tNode._children[i]; buildNodesMap(sChild, tChild); } } buildNodesMap(node, newNode); newNode.traverse(function (newChild) { if (newChild.skeleton) { newChild.skeleton = newChild.skeleton.clone(clonedNodesMap); } if (newChild.material) { newChild.material = newChild.material.clone(); } }); return newNode; }, /** * Traverse the scene and add the renderable object to the render list. * It needs camera for the frustum culling. * * @param {clay.Camera} camera * @param {boolean} updateSceneBoundingBox * @return {clay.Scene.RenderList} */ updateRenderList: function (camera, updateSceneBoundingBox) { var id = camera.__uid__; var renderList = this._renderLists.get(id); if (!renderList) { renderList = new RenderList(); this._renderLists.put(id, renderList); } renderList.startCount(); if (updateSceneBoundingBox) { this.viewBoundingBoxLastFrame.min.set(Infinity, Infinity, Infinity); this.viewBoundingBoxLastFrame.max.set(-Infinity, -Infinity, -Infinity); } var sceneMaterialTransparent = this.material && this.material.transparent || false; this._doUpdateRenderList(this, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox); renderList.endCount(); return renderList; }, /** * Get render list. Used after {@link clay.Scene#updateRenderList} * @param {clay.Camera} camera * @return {clay.Scene.RenderList} */ getRenderList: function (camera) { return this._renderLists.get(camera.__uid__); }, _doUpdateRenderList: function (parent, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox) { if (parent.invisible) { return; } // TODO Optimize for (var i = 0; i < parent._children.length; i++) { var child = parent._children[i]; if (child.isRenderable()) { // Frustum culling var worldM = child.isSkinnedMesh() ? IDENTITY : child.worldTransform.array; var geometry = child.geometry; mat4.multiplyAffine(WORLDVIEW, camera.viewMatrix.array, worldM); if (updateSceneBoundingBox && !geometry.boundingBox || !this.isFrustumCulled(child, camera, WORLDVIEW)) { renderList.add(child, child.material.transparent || sceneMaterialTransparent); } } if (child._children.length > 0) { this._doUpdateRenderList(child, camera, sceneMaterialTransparent, renderList, updateSceneBoundingBox); } } }, /** * If an scene object is culled by camera frustum * * Object can be a renderable or a light * * @param {clay.Node} object * @param {clay.Camera} camera * @param {Array.<number>} worldViewMat represented with array * @param {Array.<number>} projectionMat represented with array */ isFrustumCulled: (function () { // Frustum culling // http://www.cse.chalmers.se/~uffe/vfc_bbox.pdf var cullingBoundingBox = new BoundingBox(); var cullingMatrix = new Matrix4(); return function(object, camera, worldViewMat) { // Bounding box can be a property of object(like light) or renderable.geometry // PENDING var geoBBox = object.boundingBox; if (!geoBBox) { if (object.skeleton && object.skeleton.boundingBox) { geoBBox = object.skeleton.boundingBox; } else { geoBBox = object.geometry.boundingBox; } } if (!geoBBox) { return false; } cullingMatrix.array = worldViewMat; cullingBoundingBox.transformFrom(geoBBox, cullingMatrix); // Passingly update the scene bounding box // FIXME exclude very large mesh like ground plane or terrain ? // FIXME Only rendererable which cast shadow ? // FIXME boundingBox becomes much larger after transformd. if (object.castShadow) { this.viewBoundingBoxLastFrame.union(cullingBoundingBox); } // Ignore frustum culling if object is skinned mesh. if (object.frustumCulling) { if (!cullingBoundingBox.intersectBoundingBox(camera.frustum.boundingBox)) { return true; } cullingMatrix.array = camera.projectionMatrix.array; if ( cullingBoundingBox.max.array[2] > 0 && cullingBoundingBox.min.array[2] < 0 ) { // Clip in the near plane cullingBoundingBox.max.array[2] = -1e-20; } cullingBoundingBox.applyProjection(cullingMatrix); var min = cullingBoundingBox.min.array; var max = cullingBoundingBox.max.array; if ( max[0] < -1 || min[0] > 1 || max[1] < -1 || min[1] > 1 || max[2] < -1 || min[2] > 1 ) { return true; } } return false; }; })(), _updateLightUniforms: function () { var lights = this.lights; // Put the light cast shadow before the light not cast shadow lights.sort(lightSortFunc); var lightUniforms = this._lightUniforms; for (var group in lightUniforms) { for (var symbol in lightUniforms[group]) { lightUniforms[group][symbol].value.length = 0; } } for (var i = 0; i < lights.length; i++) { var light = lights[i]; if (light.invisible) { continue; } var group = light.group; for (var symbol in light.uniformTemplates) { var uniformTpl = light.uniformTemplates[symbol]; var value = uniformTpl.value(light); if (value == null) { continue; } if (!lightUniforms[group]) { lightUniforms[group] = {}; } if (!lightUniforms[group][symbol]) { lightUniforms[group][symbol] = { type: '', value: [] }; } var lu = lightUniforms[group][symbol]; lu.type = uniformTpl.type + 'v'; switch (uniformTpl.type) { case '1i': case '1f': case 't': lu.value.push(value); break; case '2f': case '3f': case '4f': for (var j = 0; j < value.length; j++) { lu.value.push(value[j]); } break; default: console.error('Unkown light uniform type ' + uniformTpl.type); } } } }, getLightGroups: function () { var lightGroups = []; for (var groupId in this._lightNumber) { lightGroups.push(groupId); } return lightGroups; }, getNumberChangedLightGroups: function () { var lightGroups = []; for (var groupId in this._lightNumber) { if (this.isLightNumberChanged(groupId)) { lightGroups.push(groupId); } } return lightGroups; }, // Determine if light group is different with since last frame // Used to determine whether to update shader and scene's uniforms in Renderer.render isLightNumberChanged: function (lightGroup) { var prevLightNumber = this._previousLightNumber; var currentLightNumber = this._lightNumber; // PENDING Performance for (var type in currentLightNumber[lightGroup]) { if (!prevLightNumber[lightGroup]) { return true; } if (currentLightNumber[lightGroup][type] !== prevLightNumber[lightGroup][type]) { return true; } } for (var type in prevLightNumber[lightGroup]) { if (!currentLightNumber[lightGroup]) { return true; } if (currentLightNumber[lightGroup][type] !== prevLightNumber[lightGroup][type]) { return true; } } return false; }, getLightsNumbers: function (lightGroup) { return this._lightNumber[lightGroup]; }, getProgramKey: function (lightGroup) { return this._lightProgramKeys[lightGroup]; }, setLightUniforms: (function () { function setUniforms(uniforms, program, renderer) { for (var symbol in uniforms) { var lu = uniforms[symbol]; if (lu.type === 'tv') { if (!program.hasUniform(symbol)) { continue; } var texSlots = []; for (var i = 0; i < lu.value.length; i++) { var texture = lu.value[i]; var slot = program.takeCurrentTextureSlot(renderer, texture); texSlots.push(slot); } program.setUniform(renderer.gl, '1iv', symbol, texSlots); } else { program.setUniform(renderer.gl, lu.type, symbol, lu.value); } } } return function (program, lightGroup, renderer) { setUniforms(this._lightUniforms[lightGroup], program, renderer); // Set shadows setUniforms(this.shadowUniforms, program, renderer); }; })(), /** * Dispose self, clear all the scene objects * But resources of gl like texuture, shader will not be disposed. * Mostly you should use disposeScene method in Renderer to do dispose. */ dispose: function () { this.material = null; this._opaqueList = []; this._transparentList = []; this.lights = []; this._lightUniforms = {}; this._lightNumber = {}; this._nodeRepository = {}; } }); function lightSortFunc(a, b) { if (b.castShadow && !a.castShadow) { return true; } } var DIRTY_PREFIX = '__dt__'; var Cache = function () { this._contextId = 0; this._caches = []; this._context = {}; }; Cache.prototype = { use: function (contextId, documentSchema) { var caches = this._caches; if (!caches[contextId]) { caches[contextId] = {}; if (documentSchema) { caches[contextId] = documentSchema(); } } this._contextId = contextId; this._context = caches[contextId]; }, put: function (key, value) { this._context[key] = value; }, get: function (key) { return this._context[key]; }, dirty: function (field) { field = field || ''; var key = DIRTY_PREFIX + field; this.put(key, true); }, dirtyAll: function (field) { field = field || ''; var key = DIRTY_PREFIX + field; var caches = this._caches; for (var i = 0; i < caches.length; i++) { if (caches[i]) { caches[i][key] = true; } } }, fresh: function (field) { field = field || ''; var key = DIRTY_PREFIX + field; this.put(key, false); }, freshAll: function (field) { field = field || ''; var key = DIRTY_PREFIX + field; var caches = this._caches; for (var i = 0; i < caches.length; i++) { if (caches[i]) { caches[i][key] = false; } } }, isDirty: function (field) { field = field || ''; var key = DIRTY_PREFIX + field; var context = this._context; return !context.hasOwnProperty(key) || context[key] === true; }, deleteContext: function (contextId) { delete this._caches[contextId]; this._context = {}; }, delete: function (key) { delete this._context[key]; }, clearAll: function () { this._caches = {}; }, getContext: function () { return this._context; }, eachContext : function (cb, context) { var keys = Object.keys(this._caches); keys.forEach(function (key) { cb && cb.call(context, key); }); }, miss: function (key) { return ! this._context.hasOwnProperty(key); } }; Cache.prototype.constructor = Cache; function getArrayCtorByType (type) { return ({ 'byte': vendor.Int8Array, 'ubyte': vendor.Uint8Array, 'short': vendor.Int16Array, 'ushort': vendor.Uint16Array })[type] || vendor.Float32Array; } function makeAttrKey(attrName) { return 'attr_' + attrName; } /** * GeometryBase attribute * @alias clay.GeometryBase.Attribute * @constructor */ function Attribute$1(name, type, size, semantic) { /** * Attribute name * @type {string} */ this.name = name; /** * Attribute type * Possible values: * + `'byte'` * + `'ubyte'` * + `'short'` * + `'ushort'` * + `'float'` Most commonly used. * @type {string} */ this.type = type; /** * Size of attribute component. 1 - 4. * @type {number} */ this.size = size; /** * Semantic of this attribute. * Possible values: * + `'POSITION'` * + `'NORMAL'` * + `'BINORMAL'` * + `'TANGENT'` * + `'TEXCOORD'` * + `'TEXCOORD_0'` * + `'TEXCOORD_1'` * + `'COLOR'` * + `'JOINT'` * + `'WEIGHT'` * * In shader, attribute with same semantic will be automatically mapped. For example: * ```glsl * attribute vec3 pos: POSITION * ``` * will use the attribute value with semantic POSITION in geometry, no matter what name it used. * @type {string} */ this.semantic = semantic || ''; /** * Value of the attribute. * @type {TypedArray} */ this.value = null; // Init getter setter switch (size) { case 1: this.get = function (idx) { return this.value[idx]; }; this.set = function (idx, value) { this.value[idx] = value; }; // Copy from source to target this.copy = function (target, source) { this.value[target] = this.value[target]; }; break; case 2: this.get = function (idx, out) { var arr = this.value; out[0] = arr[idx * 2]; out[1] = arr[idx * 2 + 1]; return out; }; this.set = function (idx, val) { var arr = this.value; arr[idx * 2] = val[0]; arr[idx * 2 + 1] = val[1]; }; this.copy = function (target, source) { var arr = this.value; source *= 2; target *= 2; arr[target] = arr[source]; arr[target + 1] = arr[source + 1]; }; break; case 3: this.get = function (idx, out) { var idx3 = idx * 3; var arr = this.value; out[0] = arr[idx3]; out[1] = arr[idx3 + 1]; out[2] = arr[idx3 + 2]; return out; }; this.set = function (idx, val) { var idx3 = idx * 3; var arr = this.value; arr[idx3] = val[0]; arr[idx3 + 1] = val[1]; arr[idx3 + 2] = val[2]; }; this.copy = function (target, source) { var arr = this.value; source *= 3; target *= 3; arr[target] = arr[source]; arr[target + 1] = arr[source + 1]; arr[target + 2] = arr[source + 2]; }; break; case 4: this.get = function (idx, out) { var arr = this.value; var idx4 = idx * 4; out[0] = arr[idx4]; out[1] = arr[idx4 + 1]; out[2] = arr[idx4 + 2]; out[3] = arr[idx4 + 3]; return out; }; this.set = function (idx, val) { var arr = this.value; var idx4 = idx * 4; arr[idx4] = val[0]; arr[idx4 + 1] = val[1]; arr[idx4 + 2] = val[2]; arr[idx4 + 3] = val[3]; }; this.copy = function (target, source) { var arr = this.value; source *= 4; target *= 4; // copyWithin is extremely slow arr[target] = arr[source]; arr[target + 1] = arr[source + 1]; arr[target + 2] = arr[source + 2]; arr[target + 3] = arr[source + 3]; }; } } /** * Set item value at give index. Second parameter val is number if size is 1 * @function * @name clay.GeometryBase.Attribute#set * @param {number} idx * @param {number[]|number} val * @example * geometry.getAttribute('position').set(0, [1, 1, 1]); */ /** * Get item value at give index. Second parameter out is no need if size is 1 * @function * @name clay.GeometryBase.Attribute#set * @param {number} idx * @param {number[]} [out] * @example * geometry.getAttribute('position').get(0, out); */ /** * Initialize attribute with given vertex count * @param {number} nVertex */ Attribute$1.prototype.init = function (nVertex) { if (!this.value || this.value.length !== nVertex * this.size) { var ArrayConstructor = getArrayCtorByType(this.type); this.value = new ArrayConstructor(nVertex * this.size); } }; /** * Initialize attribute with given array. Which can be 1 dimensional or 2 dimensional * @param {Array} array * @example * geometry.getAttribute('position').fromArray( * [-1, 0, 0, 1, 0, 0, 0, 1, 0] * ); * geometry.getAttribute('position').fromArray( * [ [-1, 0, 0], [1, 0, 0], [0, 1, 0] ] * ); */ Attribute$1.prototype.fromArray = function (array) { var ArrayConstructor = getArrayCtorByType(this.type); var value; // Convert 2d array to flat if (array[0] && (array[0].length)) { var n = 0; var size = this.size; value = new ArrayConstructor(array.length * size); for (var i = 0; i < array.length; i++) { for (var j = 0; j < size; j++) { value[n++] = array[i][j]; } } } else { value = new ArrayConstructor(array); } this.value = value; }; Attribute$1.prototype.clone = function(copyValue) { var ret = new Attribute$1(this.name, this.type, this.size, this.semantic); // FIXME if (copyValue) { console.warn('todo'); } return ret; }; function AttributeBuffer(name, type, buffer, size, semantic) { this.name = name; this.type = type; this.buffer = buffer; this.size = size; this.semantic = semantic; // To be set in mesh // symbol in the shader this.symbol = ''; // Needs remove flag this.needsRemove = false; } function IndicesBuffer(buffer) { this.buffer = buffer; this.count = 0; } /** * Base of all geometry. Use {@link clay.Geometry} for common 3D usage. * @constructor clay.GeometryBase * @extends clay.core.Base */ var GeometryBase = Base.extend(function () { return /** @lends clay.GeometryBase# */ { /** * Attributes of geometry. * @type {Object.<string, clay.GeometryBase.Attribute>} */ attributes: {}, /** * Indices of geometry. * @type {Uint16Array|Uint32Array} */ indices: null, /** * Is vertices data dynamically updated. * Attributes value can't be changed after first render if dyanmic is false. * @type {boolean} */ dynamic: true, _enabledAttributes: null, // PENDING // Init it here to avoid deoptimization when it's assigned in application dynamically __used: 0 }; }, function () { // Use cache this._cache = new Cache(); this._attributeList = Object.keys(this.attributes); this.__vaoCache = {}; }, /** @lends clay.GeometryBase.prototype */ { /** * Main attribute will be used to count vertex number * @type {string} */ mainAttribute: '', /** * User defined picking algorithm instead of default * triangle ray intersection * x, y are NDC. * ```typescript * (x, y, renderer, camera, renderable, out) => boolean * ``` * @type {?Function} */ pick: null, /** * User defined ray picking algorithm instead of default * triangle ray intersection * ```typescript * (ray: clay.Ray, renderable: clay.Renderable, out: Array) => boolean * ``` * @type {?Function} */ pickByRay: null, /** * Mark attributes and indices in geometry needs to update. * Usually called after you change the data in attributes. */ dirty: function () { var enabledAttributes = this.getEnabledAttributes(); for (var i = 0; i < enabledAttributes.length; i++) { this.dirtyAttribute(enabledAttributes[i]); } this.dirtyIndices(); this._enabledAttributes = null; this._cache.dirty('any'); }, /** * Mark the indices needs to update. */ dirtyIndices: function () { this._cache.dirtyAll('indices'); }, /** * Mark the attributes needs to update. * @param {string} [attrName] */ dirtyAttribute: function (attrName) { this._cache.dirtyAll(makeAttrKey(attrName)); this._cache.dirtyAll('attributes'); }, /** * Get indices of triangle at given index. * @param {number} idx * @param {Array.<number>} out * @return {Array.<number>} */ getTriangleIndices: function (idx, out) { if (idx < this.triangleCount && idx >= 0) { if (!out) { out = []; } var indices = this.indices; out[0] = indices[idx * 3]; out[1] = indices[idx * 3 + 1]; out[2] = indices[idx * 3 + 2]; return out; } }, /** * Set indices of triangle at given index. * @param {number} idx * @param {Array.<number>} arr */ setTriangleIndices: function (idx, arr) { var indices = this.indices; indices[idx * 3] = arr[0]; indices[idx * 3 + 1] = arr[1]; indices[idx * 3 + 2] = arr[2]; }, isUseIndices: function () { return !!this.indices; }, /** * Initialize indices from an array. * @param {Array} array */ initIndicesFromArray: function (array) { var value; var ArrayConstructor = this.vertexCount > 0xffff ? vendor.Uint32Array : vendor.Uint16Array; // Convert 2d array to flat if (array[0] && (array[0].length)) { var n = 0; var size = 3; value = new ArrayConstructor(array.length * size); for (var i = 0; i < array.length; i++) { for (var j = 0; j < size; j++) { value[n++] = array[i][j]; } } } else { value = new ArrayConstructor(array); } this.indices = value; }, /** * Create a new attribute * @param {string} name * @param {string} type * @param {number} size * @param {string} [semantic] */ createAttribute: function (name, type, size, semantic) { var attrib = new Attribute$1(name, type, size, semantic); if (this.attributes[name]) { this.removeAttribute(name); } this.attributes[name] = attrib; this._attributeList.push(name); return attrib; }, /** * Remove attribute * @param {string} name */ removeAttribute: function (name) { var attributeList = this._attributeList; var idx = attributeList.indexOf(name); if (idx >= 0) { attributeList.splice(idx, 1); delete this.attributes[name]; return true; } return false; }, /** * Get attribute * @param {string} name * @return {clay.GeometryBase.Attribute} */ getAttribute: function (name) { return this.attributes[name]; }, /** * Get enabled attributes name list * Attribute which has the same vertex number with position is treated as a enabled attribute * @return {string[]} */ getEnabledAttributes: function () { var enabledAttributes = this._enabledAttributes; var attributeList = this._attributeList; // Cache if (enabledAttributes) { return enabledAttributes; } var result = []; var nVertex = this.vertexCount; for (var i = 0; i < attributeList.length; i++) { var name = attributeList[i]; var attrib = this.attributes[name]; if (attrib.value) { if (attrib.value.length === nVertex * attrib.size) { result.push(name); } } } this._enabledAttributes = result; return result; }, getBufferChunks: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); var isAttributesDirty = cache.isDirty('attributes'); var isIndicesDirty = cache.isDirty('indices'); if (isAttributesDirty || isIndicesDirty) { this._updateBuffer(renderer.gl, isAttributesDirty, isIndicesDirty); var enabledAttributes = this.getEnabledAttributes(); for (var i = 0; i < enabledAttributes.length; i++) { cache.fresh(makeAttrKey(enabledAttributes[i])); } cache.fresh('attributes'); cache.fresh('indices'); } cache.fresh('any'); return cache.get('chunks'); }, _updateBuffer: function (_gl, isAttributesDirty, isIndicesDirty) { var cache = this._cache; var chunks = cache.get('chunks'); var firstUpdate = false; if (!chunks) { chunks = []; // Intialize chunks[0] = { attributeBuffers: [], indicesBuffer: null }; cache.put('chunks', chunks); firstUpdate = true; } var chunk = chunks[0]; var attributeBuffers = chunk.attributeBuffers; var indicesBuffer = chunk.indicesBuffer; if (isAttributesDirty || firstUpdate) { var attributeList = this.getEnabledAttributes(); var attributeBufferMap = {}; if (!firstUpdate) { for (var i = 0; i < attributeBuffers.length; i++) { attributeBufferMap[attributeBuffers[i].name] = attributeBuffers[i]; } } // FIXME If some attributes removed for (var k = 0; k < attributeList.length; k++) { var name = attributeList[k]; var attribute = this.attributes[name]; var bufferInfo; if (!firstUpdate) { bufferInfo = attributeBufferMap[name]; } var buffer; if (bufferInfo) { buffer = bufferInfo.buffer; } else { buffer = _gl.createBuffer(); } if (cache.isDirty(makeAttrKey(name))) { // Only update when they are dirty. // TODO: Use BufferSubData? _gl.bindBuffer(_gl.ARRAY_BUFFER, buffer); _gl.bufferData(_gl.ARRAY_BUFFER, attribute.value, this.dynamic ? _gl.DYNAMIC_DRAW : _gl.STATIC_DRAW); } attributeBuffers[k] = new AttributeBuffer(name, attribute.type, buffer, attribute.size, attribute.semantic); } // Remove unused attributes buffers. // PENDING for (var i = k; i < attributeBuffers.length; i++) { _gl.deleteBuffer(attributeBuffers[i].buffer); } attributeBuffers.length = k; } if (this.isUseIndices() && (isIndicesDirty || firstUpdate)) { if (!indicesBuffer) { indicesBuffer = new IndicesBuffer(_gl.createBuffer()); chunk.indicesBuffer = indicesBuffer; } indicesBuffer.count = this.indices.length; _gl.bindBuffer(_gl.ELEMENT_ARRAY_BUFFER, indicesBuffer.buffer); _gl.bufferData(_gl.ELEMENT_ARRAY_BUFFER, this.indices, this.dynamic ? _gl.DYNAMIC_DRAW : _gl.STATIC_DRAW); } }, /** * Dispose geometry data in GL context. * @param {clay.Renderer} renderer */ dispose: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); var chunks = cache.get('chunks'); if (chunks) { for (var c = 0; c < chunks.length; c++) { var chunk = chunks[c]; for (var k = 0; k < chunk.attributeBuffers.length; k++) { var attribs = chunk.attributeBuffers[k]; renderer.gl.deleteBuffer(attribs.buffer); } if (chunk.indicesBuffer) { renderer.gl.deleteBuffer(chunk.indicesBuffer.buffer); } } } if (this.__vaoCache) { var vaoExt = renderer.getGLExtension('OES_vertex_array_object'); for (var id in this.__vaoCache) { var vao = this.__vaoCache[id].vao; if (vao) { vaoExt.deleteVertexArrayOES(vao); } } } this.__vaoCache = {}; cache.deleteContext(renderer.__uid__); } }); if (Object.defineProperty) { /** * @name clay.GeometryBase#vertexCount * @type {number} * @readOnly */ Object.defineProperty(GeometryBase.prototype, 'vertexCount', { enumerable: false, get: function () { var mainAttribute = this.attributes[this.mainAttribute]; if (!mainAttribute) { mainAttribute = this.attributes[this._attributeList[0]]; } if (!mainAttribute || !mainAttribute.value) { return 0; } return mainAttribute.value.length / mainAttribute.size; } }); /** * @name clay.GeometryBase#triangleCount * @type {number} * @readOnly */ Object.defineProperty(GeometryBase.prototype, 'triangleCount', { enumerable: false, get: function () { var indices = this.indices; if (!indices) { return 0; } else { return indices.length / 3; } } }); } GeometryBase.STATIC_DRAW = glenum.STATIC_DRAW; GeometryBase.DYNAMIC_DRAW = glenum.DYNAMIC_DRAW; GeometryBase.STREAM_DRAW = glenum.STREAM_DRAW; GeometryBase.AttributeBuffer = AttributeBuffer; GeometryBase.IndicesBuffer = IndicesBuffer; GeometryBase.Attribute = Attribute$1; var vec3Create = vec3.create; var vec3Add = vec3.add; var vec3Set$2 = vec3.set; var Attribute = GeometryBase.Attribute; /** * Geometry in ClayGL contains vertex attributes of mesh. These vertex attributes will be finally provided to the {@link clay.Shader}. * Different {@link clay.Shader} needs different attributes. Here is a list of attributes used in the builtin shaders. * * + position: `clay.basic`, `clay.lambert`, `clay.standard` * + texcoord0: `clay.basic`, `clay.lambert`, `clay.standard` * + color: `clay.basic`, `clay.lambert`, `clay.standard` * + weight: `clay.basic`, `clay.lambert`, `clay.standard` * + joint: `clay.basic`, `clay.lambert`, `clay.standard` * + normal: `clay.lambert`, `clay.standard` * + tangent: `clay.standard` * * #### Create a procedural geometry * * ClayGL provides a couple of builtin procedural geometries. Inlcuding: * * + {@link clay.geometry.Cube} * + {@link clay.geometry.Sphere} * + {@link clay.geometry.Plane} * + {@link clay.geometry.Cylinder} * + {@link clay.geometry.Cone} * + {@link clay.geometry.ParametricSurface} * * It's simple to create a basic geometry with these classes. * ```js var sphere = new clay.geometry.Sphere({ radius: 2 }); ``` * * #### Create the geometry data by yourself * * Usually the vertex attributes data are created by the {@link clay.loader.GLTF} or procedural geometries like {@link clay.geometry.Sphere}. * Besides these, you can create the data manually. Here is a simple example to create a triangle. ```js var TRIANGLE_POSITIONS = [ [-0.5, -0.5, 0], [0.5, -0.5, 0], [0, 0.5, 0] ]; var geometry = new clay.StaticGeometryBase(); // Add triangle vertices to position attribute. geometry.attributes.position.fromArray(TRIANGLE_POSITIONS); ``` * Then you can use the utility methods like `generateVertexNormals`, `generateTangents` to create the remaining necessary attributes. * * * #### Use with custom shaders * * If you wan't to write custom shaders. Don't forget to add SEMANTICS to these attributes. For example * ```glsl uniform mat4 worldViewProjection : WORLDVIEWPROJECTION; uniform mat4 worldInverseTranspose : WORLDINVERSETRANSPOSE; uniform mat4 world : WORLD; attribute vec3 position : POSITION; attribute vec2 texcoord : TEXCOORD_0; attribute vec3 normal : NORMAL; ``` * These `POSITION`, `TEXCOORD_0`, `NORMAL` are SEMANTICS which will map the attributes in shader to the attributes in the GeometryBase * * Available attributes SEMANTICS includes `POSITION`, `TEXCOORD_0`, `TEXCOORD_1` `NORMAL`, `TANGENT`, `COLOR`, `WEIGHT`, `JOINT`. * * * @constructor clay.Geometry * @extends clay.GeometryBase */ var Geometry = GeometryBase.extend(function () { return /** @lends clay.Geometry# */ { /** * Attributes of geometry. Including: * + `position` * + `texcoord0` * + `texcoord1` * + `normal` * + `tangent` * + `color` * + `weight` * + `joint` * + `barycentric` * * @type {Object.<string, clay.Geometry.Attribute>} */ attributes: { position: new Attribute('position', 'float', 3, 'POSITION'), texcoord0: new Attribute('texcoord0', 'float', 2, 'TEXCOORD_0'), texcoord1: new Attribute('texcoord1', 'float', 2, 'TEXCOORD_1'), normal: new Attribute('normal', 'float', 3, 'NORMAL'), tangent: new Attribute('tangent', 'float', 4, 'TANGENT'), color: new Attribute('color', 'float', 4, 'COLOR'), // Skinning attributes // Each vertex can be bind to 4 bones, because the // sum of weights is 1, so the weights is stored in vec3 and the last // can be calculated by 1-w.x-w.y-w.z weight: new Attribute('weight', 'float', 3, 'WEIGHT'), joint: new Attribute('joint', 'float', 4, 'JOINT'), // For wireframe display // http://codeflow.org/entries/2012/aug/02/easy-wireframe-display-with-barycentric-coordinates/ barycentric: new Attribute('barycentric', 'float', 3, null), }, /** * Calculated bounding box of geometry. * @type {clay.BoundingBox} */ boundingBox: null }; }, /** @lends clay.Geometry.prototype */ { mainAttribute: 'position', /** * Update boundingBox of Geometry */ updateBoundingBox: function () { var bbox = this.boundingBox; if (!bbox) { bbox = this.boundingBox = new BoundingBox(); } var posArr = this.attributes.position.value; if (posArr && posArr.length) { var min = bbox.min; var max = bbox.max; var minArr = min.array; var maxArr = max.array; vec3.set(minArr, posArr[0], posArr[1], posArr[2]); vec3.set(maxArr, posArr[0], posArr[1], posArr[2]); for (var i = 3; i < posArr.length;) { var x = posArr[i++]; var y = posArr[i++]; var z = posArr[i++]; if (x < minArr[0]) { minArr[0] = x; } if (y < minArr[1]) { minArr[1] = y; } if (z < minArr[2]) { minArr[2] = z; } if (x > maxArr[0]) { maxArr[0] = x; } if (y > maxArr[1]) { maxArr[1] = y; } if (z > maxArr[2]) { maxArr[2] = z; } } min._dirty = true; max._dirty = true; } }, /** * Generate normals per vertex. */ generateVertexNormals: function () { if (!this.vertexCount) { return; } var indices = this.indices; var attributes = this.attributes; var positions = attributes.position.value; var normals = attributes.normal.value; if (!normals || normals.length !== positions.length) { normals = attributes.normal.value = new vendor.Float32Array(positions.length); } else { // Reset for (var i = 0; i < normals.length; i++) { normals[i] = 0; } } var p1 = vec3Create(); var p2 = vec3Create(); var p3 = vec3Create(); var v21 = vec3Create(); var v32 = vec3Create(); var n = vec3Create(); var len = indices ? indices.length : this.vertexCount; var i1, i2, i3; for (var f = 0; f < len;) { if (indices) { i1 = indices[f++]; i2 = indices[f++]; i3 = indices[f++]; } else { i1 = f++; i2 = f++; i3 = f++; } vec3Set$2(p1, positions[i1*3], positions[i1*3+1], positions[i1*3+2]); vec3Set$2(p2, positions[i2*3], positions[i2*3+1], positions[i2*3+2]); vec3Set$2(p3, positions[i3*3], positions[i3*3+1], positions[i3*3+2]); vec3.sub(v21, p1, p2); vec3.sub(v32, p2, p3); vec3.cross(n, v21, v32); // Already be weighted by the triangle area for (var i = 0; i < 3; i++) { normals[i1*3+i] = normals[i1*3+i] + n[i]; normals[i2*3+i] = normals[i2*3+i] + n[i]; normals[i3*3+i] = normals[i3*3+i] + n[i]; } } for (var i = 0; i < normals.length;) { vec3Set$2(n, normals[i], normals[i+1], normals[i+2]); vec3.normalize(n, n); normals[i++] = n[0]; normals[i++] = n[1]; normals[i++] = n[2]; } this.dirty(); }, /** * Generate normals per face. */ generateFaceNormals: function () { if (!this.vertexCount) { return; } if (!this.isUniqueVertex()) { this.generateUniqueVertex(); } var indices = this.indices; var attributes = this.attributes; var positions = attributes.position.value; var normals = attributes.normal.value; var p1 = vec3Create(); var p2 = vec3Create(); var p3 = vec3Create(); var v21 = vec3Create(); var v32 = vec3Create(); var n = vec3Create(); if (!normals) { normals = attributes.normal.value = new Float32Array(positions.length); } var len = indices ? indices.length : this.vertexCount; var i1, i2, i3; for (var f = 0; f < len;) { if (indices) { i1 = indices[f++]; i2 = indices[f++]; i3 = indices[f++]; } else { i1 = f++; i2 = f++; i3 = f++; } vec3Set$2(p1, positions[i1*3], positions[i1*3+1], positions[i1*3+2]); vec3Set$2(p2, positions[i2*3], positions[i2*3+1], positions[i2*3+2]); vec3Set$2(p3, positions[i3*3], positions[i3*3+1], positions[i3*3+2]); vec3.sub(v21, p1, p2); vec3.sub(v32, p2, p3); vec3.cross(n, v21, v32); vec3.normalize(n, n); for (var i = 0; i < 3; i++) { normals[i1*3 + i] = n[i]; normals[i2*3 + i] = n[i]; normals[i3*3 + i] = n[i]; } } this.dirty(); }, /** * Generate tangents attributes. */ generateTangents: function () { if (!this.vertexCount) { return; } var nVertex = this.vertexCount; var attributes = this.attributes; if (!attributes.tangent.value) { attributes.tangent.value = new Float32Array(nVertex * 4); } var texcoords = attributes.texcoord0.value; var positions = attributes.position.value; var tangents = attributes.tangent.value; var normals = attributes.normal.value; if (!texcoords) { console.warn('Geometry without texcoords can\'t generate tangents.'); return; } var tan1 = []; var tan2 = []; for (var i = 0; i < nVertex; i++) { tan1[i] = [0.0, 0.0, 0.0]; tan2[i] = [0.0, 0.0, 0.0]; } var sdir = [0.0, 0.0, 0.0]; var tdir = [0.0, 0.0, 0.0]; var indices = this.indices; var len = indices ? indices.length : this.vertexCount; var i1, i2, i3; for (var i = 0; i < len;) { if (indices) { i1 = indices[i++]; i2 = indices[i++]; i3 = indices[i++]; } else { i1 = i++; i2 = i++; i3 = i++; } var st1s = texcoords[i1 * 2], st2s = texcoords[i2 * 2], st3s = texcoords[i3 * 2], st1t = texcoords[i1 * 2 + 1], st2t = texcoords[i2 * 2 + 1], st3t = texcoords[i3 * 2 + 1], p1x = positions[i1 * 3], p2x = positions[i2 * 3], p3x = positions[i3 * 3], p1y = positions[i1 * 3 + 1], p2y = positions[i2 * 3 + 1], p3y = positions[i3 * 3 + 1], p1z = positions[i1 * 3 + 2], p2z = positions[i2 * 3 + 2], p3z = positions[i3 * 3 + 2]; var x1 = p2x - p1x, x2 = p3x - p1x, y1 = p2y - p1y, y2 = p3y - p1y, z1 = p2z - p1z, z2 = p3z - p1z; var s1 = st2s - st1s, s2 = st3s - st1s, t1 = st2t - st1t, t2 = st3t - st1t; var r = 1.0 / (s1 * t2 - t1 * s2); sdir[0] = (t2 * x1 - t1 * x2) * r; sdir[1] = (t2 * y1 - t1 * y2) * r; sdir[2] = (t2 * z1 - t1 * z2) * r; tdir[0] = (s1 * x2 - s2 * x1) * r; tdir[1] = (s1 * y2 - s2 * y1) * r; tdir[2] = (s1 * z2 - s2 * z1) * r; vec3Add(tan1[i1], tan1[i1], sdir); vec3Add(tan1[i2], tan1[i2], sdir); vec3Add(tan1[i3], tan1[i3], sdir); vec3Add(tan2[i1], tan2[i1], tdir); vec3Add(tan2[i2], tan2[i2], tdir); vec3Add(tan2[i3], tan2[i3], tdir); } var tmp = vec3Create(); var nCrossT = vec3Create(); var n = vec3Create(); for (var i = 0; i < nVertex; i++) { n[0] = normals[i * 3]; n[1] = normals[i * 3 + 1]; n[2] = normals[i * 3 + 2]; var t = tan1[i]; // Gram-Schmidt orthogonalize vec3.scale(tmp, n, vec3.dot(n, t)); vec3.sub(tmp, t, tmp); vec3.normalize(tmp, tmp); // Calculate handedness. vec3.cross(nCrossT, n, t); tangents[i * 4] = tmp[0]; tangents[i * 4 + 1] = tmp[1]; tangents[i * 4 + 2] = tmp[2]; // PENDING can config ? tangents[i * 4 + 3] = vec3.dot(nCrossT, tan2[i]) < 0.0 ? -1.0 : 1.0; } this.dirty(); }, /** * If vertices are not shared by different indices. */ isUniqueVertex: function () { if (this.isUseIndices()) { return this.vertexCount === this.indices.length; } else { return true; } }, /** * Create a unique vertex for each index. */ generateUniqueVertex: function () { if (!this.vertexCount || !this.indices) { return; } if (this.indices.length > 0xffff) { this.indices = new vendor.Uint32Array(this.indices); } var attributes = this.attributes; var indices = this.indices; var attributeNameList = this.getEnabledAttributes(); var oldAttrValues = {}; for (var a = 0; a < attributeNameList.length; a++) { var name = attributeNameList[a]; oldAttrValues[name] = attributes[name].value; attributes[name].init(this.indices.length); } var cursor = 0; for (var i = 0; i < indices.length; i++) { var ii = indices[i]; for (var a = 0; a < attributeNameList.length; a++) { var name = attributeNameList[a]; var array = attributes[name].value; var size = attributes[name].size; for (var k = 0; k < size; k++) { array[cursor * size + k] = oldAttrValues[name][ii * size + k]; } } indices[i] = cursor; cursor++; } this.dirty(); }, /** * Generate barycentric coordinates for wireframe draw. */ generateBarycentric: function () { if (!this.vertexCount) { return; } if (!this.isUniqueVertex()) { this.generateUniqueVertex(); } var attributes = this.attributes; var array = attributes.barycentric.value; var indices = this.indices; // Already existed; if (array && array.length === indices.length * 3) { return; } array = attributes.barycentric.value = new Float32Array(indices.length * 3); for (var i = 0; i < (indices ? indices.length : this.vertexCount / 3);) { for (var j = 0; j < 3; j++) { var ii = indices ? indices[i++] : (i * 3 + j); array[ii * 3 + j] = 1; } } this.dirty(); }, /** * Apply transform to geometry attributes. * @param {clay.Matrix4} matrix */ applyTransform: function (matrix) { var attributes = this.attributes; var positions = attributes.position.value; var normals = attributes.normal.value; var tangents = attributes.tangent.value; matrix = matrix.array; // Normal Matrix var inverseTransposeMatrix = mat4.create(); mat4.invert(inverseTransposeMatrix, matrix); mat4.transpose(inverseTransposeMatrix, inverseTransposeMatrix); var vec3TransformMat4 = vec3.transformMat4; var vec3ForEach = vec3.forEach; vec3ForEach(positions, 3, 0, null, vec3TransformMat4, matrix); if (normals) { vec3ForEach(normals, 3, 0, null, vec3TransformMat4, inverseTransposeMatrix); } if (tangents) { vec3ForEach(tangents, 4, 0, null, vec3TransformMat4, inverseTransposeMatrix); } if (this.boundingBox) { this.updateBoundingBox(); } }, /** * Dispose geometry data in GL context. * @param {clay.Renderer} renderer */ dispose: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); var chunks = cache.get('chunks'); if (chunks) { for (var c = 0; c < chunks.length; c++) { var chunk = chunks[c]; for (var k = 0; k < chunk.attributeBuffers.length; k++) { var attribs = chunk.attributeBuffers[k]; renderer.gl.deleteBuffer(attribs.buffer); } if (chunk.indicesBuffer) { renderer.gl.deleteBuffer(chunk.indicesBuffer.buffer); } } } if (this.__vaoCache) { var vaoExt = renderer.getGLExtension('OES_vertex_array_object'); for (var id in this.__vaoCache) { var vao = this.__vaoCache[id].vao; if (vao) { vaoExt.deleteVertexArrayOES(vao); } } } this.__vaoCache = {}; cache.deleteContext(renderer.__uid__); } }); Geometry.STATIC_DRAW = GeometryBase.STATIC_DRAW; Geometry.DYNAMIC_DRAW = GeometryBase.DYNAMIC_DRAW; Geometry.STREAM_DRAW = GeometryBase.STREAM_DRAW; Geometry.AttributeBuffer = GeometryBase.AttributeBuffer; Geometry.IndicesBuffer = GeometryBase.IndicesBuffer; Geometry.Attribute = Attribute; /** * @constructor clay.geometry.Plane * @extends clay.Geometry * @param {Object} [opt] * @param {number} [opt.widthSegments] * @param {number} [opt.heightSegments] */ var Plane$3 = Geometry.extend( /** @lends clay.geometry.Plane# */ { dynamic: false, /** * @type {number} */ widthSegments: 1, /** * @type {number} */ heightSegments: 1 }, function() { this.build(); }, /** @lends clay.geometry.Plane.prototype */ { /** * Build plane geometry */ build: function() { var heightSegments = this.heightSegments; var widthSegments = this.widthSegments; var attributes = this.attributes; var positions = []; var texcoords = []; var normals = []; var faces = []; for (var y = 0; y <= heightSegments; y++) { var t = y / heightSegments; for (var x = 0; x <= widthSegments; x++) { var s = x / widthSegments; positions.push([2 * s - 1, 2 * t - 1, 0]); if (texcoords) { texcoords.push([s, t]); } if (normals) { normals.push([0, 0, 1]); } if (x < widthSegments && y < heightSegments) { var i = x + y * (widthSegments + 1); faces.push([i, i + 1, i + widthSegments + 1]); faces.push([i + widthSegments + 1, i + 1, i + widthSegments + 2]); } } } attributes.position.fromArray(positions); attributes.texcoord0.fromArray(texcoords); attributes.normal.fromArray(normals); this.initIndicesFromArray(faces); this.boundingBox = new BoundingBox(); this.boundingBox.min.set(-1, -1, 0); this.boundingBox.max.set(1, 1, 0); } }); var planeMatrix = new Matrix4(); /** * @constructor clay.geometry.Cube * @extends clay.Geometry * @param {Object} [opt] * @param {number} [opt.widthSegments] * @param {number} [opt.heightSegments] * @param {number} [opt.depthSegments] * @param {boolean} [opt.inside] */ var Cube$1 = Geometry.extend( /**@lends clay.geometry.Cube# */ { dynamic: false, /** * @type {number} */ widthSegments: 1, /** * @type {number} */ heightSegments: 1, /** * @type {number} */ depthSegments: 1, /** * @type {boolean} */ inside: false }, function() { this.build(); }, /** @lends clay.geometry.Cube.prototype */ { /** * Build cube geometry */ build: function() { var planes = { 'px': createPlane('px', this.depthSegments, this.heightSegments), 'nx': createPlane('nx', this.depthSegments, this.heightSegments), 'py': createPlane('py', this.widthSegments, this.depthSegments), 'ny': createPlane('ny', this.widthSegments, this.depthSegments), 'pz': createPlane('pz', this.widthSegments, this.heightSegments), 'nz': createPlane('nz', this.widthSegments, this.heightSegments), }; var attrList = ['position', 'texcoord0', 'normal']; var vertexNumber = 0; var faceNumber = 0; for (var pos in planes) { vertexNumber += planes[pos].vertexCount; faceNumber += planes[pos].indices.length; } for (var k = 0; k < attrList.length; k++) { this.attributes[attrList[k]].init(vertexNumber); } this.indices = new vendor.Uint16Array(faceNumber); var faceOffset = 0; var vertexOffset = 0; for (var pos in planes) { var plane = planes[pos]; for (var k = 0; k < attrList.length; k++) { var attrName = attrList[k]; var attrArray = plane.attributes[attrName].value; var attrSize = plane.attributes[attrName].size; var isNormal = attrName === 'normal'; for (var i = 0; i < attrArray.length; i++) { var value = attrArray[i]; if (this.inside && isNormal) { value = -value; } this.attributes[attrName].value[i + attrSize * vertexOffset] = value; } } var len = plane.indices.length; for (var i = 0; i < plane.indices.length; i++) { this.indices[i + faceOffset] = vertexOffset + plane.indices[this.inside ? (len - i - 1) : i]; } faceOffset += plane.indices.length; vertexOffset += plane.vertexCount; } this.boundingBox = new BoundingBox(); this.boundingBox.max.set(1, 1, 1); this.boundingBox.min.set(-1, -1, -1); } }); function createPlane(pos, widthSegments, heightSegments) { planeMatrix.identity(); var plane = new Plane$3({ widthSegments: widthSegments, heightSegments: heightSegments }); switch(pos) { case 'px': Matrix4.translate(planeMatrix, planeMatrix, Vector3.POSITIVE_X); Matrix4.rotateY(planeMatrix, planeMatrix, Math.PI / 2); break; case 'nx': Matrix4.translate(planeMatrix, planeMatrix, Vector3.NEGATIVE_X); Matrix4.rotateY(planeMatrix, planeMatrix, -Math.PI / 2); break; case 'py': Matrix4.translate(planeMatrix, planeMatrix, Vector3.POSITIVE_Y); Matrix4.rotateX(planeMatrix, planeMatrix, -Math.PI / 2); break; case 'ny': Matrix4.translate(planeMatrix, planeMatrix, Vector3.NEGATIVE_Y); Matrix4.rotateX(planeMatrix, planeMatrix, Math.PI / 2); break; case 'pz': Matrix4.translate(planeMatrix, planeMatrix, Vector3.POSITIVE_Z); break; case 'nz': Matrix4.translate(planeMatrix, planeMatrix, Vector3.NEGATIVE_Z); Matrix4.rotateY(planeMatrix, planeMatrix, Math.PI); break; } plane.applyTransform(planeMatrix); return plane; } /** * @constructor clay.geometry.Sphere * @extends clay.Geometry * @param {Object} [opt] * @param {number} [widthSegments] * @param {number} [heightSegments] * @param {number} [phiStart] * @param {number} [phiLength] * @param {number} [thetaStart] * @param {number} [thetaLength] * @param {number} [radius] */ var Sphere$1 = Geometry.extend(/** @lends clay.geometry.Sphere# */ { dynamic: false, /** * @type {number} */ widthSegments: 40, /** * @type {number} */ heightSegments: 20, /** * @type {number} */ phiStart: 0, /** * @type {number} */ phiLength: Math.PI * 2, /** * @type {number} */ thetaStart: 0, /** * @type {number} */ thetaLength: Math.PI, /** * @type {number} */ radius: 1 }, function() { this.build(); }, /** @lends clay.geometry.Sphere.prototype */ { /** * Build sphere geometry */ build: function() { var heightSegments = this.heightSegments; var widthSegments = this.widthSegments; var positionAttr = this.attributes.position; var texcoordAttr = this.attributes.texcoord0; var normalAttr = this.attributes.normal; var vertexCount = (widthSegments + 1) * (heightSegments + 1); positionAttr.init(vertexCount); texcoordAttr.init(vertexCount); normalAttr.init(vertexCount); var IndicesCtor = vertexCount > 0xffff ? Uint32Array : Uint16Array; var indices = this.indices = new IndicesCtor(widthSegments * heightSegments * 6); var x, y, z, u, v, i, j; var radius = this.radius; var phiStart = this.phiStart; var phiLength = this.phiLength; var thetaStart = this.thetaStart; var thetaLength = this.thetaLength; var radius = this.radius; var pos = []; var uv = []; var offset = 0; var divider = 1 / radius; for (j = 0; j <= heightSegments; j ++) { for (i = 0; i <= widthSegments; i ++) { u = i / widthSegments; v = j / heightSegments; // X axis is inverted so texture can be mapped from left to right x = -radius * Math.cos(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); y = radius * Math.cos(thetaStart + v * thetaLength); z = radius * Math.sin(phiStart + u * phiLength) * Math.sin(thetaStart + v * thetaLength); pos[0] = x; pos[1] = y; pos[2] = z; uv[0] = u; uv[1] = v; positionAttr.set(offset, pos); texcoordAttr.set(offset, uv); pos[0] *= divider; pos[1] *= divider; pos[2] *= divider; normalAttr.set(offset, pos); offset++; } } var i1, i2, i3, i4; var len = widthSegments + 1; var n = 0; for (j = 0; j < heightSegments; j ++) { for (i = 0; i < widthSegments; i ++) { i2 = j * len + i; i1 = (j * len + i + 1); i4 = (j + 1) * len + i + 1; i3 = (j + 1) * len + i; indices[n++] = i1; indices[n++] = i2; indices[n++] = i4; indices[n++] = i2; indices[n++] = i3; indices[n++] = i4; } } this.boundingBox = new BoundingBox(); this.boundingBox.max.set(radius, radius, radius); this.boundingBox.min.set(-radius, -radius, -radius); } }); /** * @constructor clay.geometry.ParametricSurface * @extends clay.Geometry * @param {Object} [opt] * @param {Object} [generator] * @param {Function} generator.x * @param {Function} generator.y * @param {Function} generator.z * @param {Array} [generator.u=[0, 1, 0.05]] * @param {Array} [generator.v=[0, 1, 0.05]] */ var ParametricSurface$1 = Geometry.extend( /** @lends clay.geometry.ParametricSurface# */ { dynamic: false, /** * @type {Object} */ generator: null }, function() { this.build(); }, /** @lends clay.geometry.ParametricSurface.prototype */ { /** * Build parametric surface geometry */ build: function () { var generator = this.generator; if (!generator || !generator.x || !generator.y || !generator.z) { throw new Error('Invalid generator'); } var xFunc = generator.x; var yFunc = generator.y; var zFunc = generator.z; var uRange = generator.u || [0, 1, 0.05]; var vRange = generator.v || [0, 1, 0.05]; var uNum = Math.floor((uRange[1] - uRange[0] + uRange[2]) / uRange[2]); var vNum = Math.floor((vRange[1] - vRange[0] + vRange[2]) / vRange[2]); if (!isFinite(uNum) || !isFinite(vNum)) { throw new Error('Infinite generator'); } var vertexNum = uNum * vNum; this.attributes.position.init(vertexNum); this.attributes.texcoord0.init(vertexNum); var pos = []; var texcoord = []; var nVertex = 0; for (var j = 0; j < vNum; j++) { for (var i = 0; i < uNum; i++) { var u = i * uRange[2] + uRange[0]; var v = j * vRange[2] + vRange[0]; pos[0] = xFunc(u, v); pos[1] = yFunc(u, v); pos[2] = zFunc(u, v); texcoord[0] = i / (uNum - 1); texcoord[1] = j / (vNum - 1); this.attributes.position.set(nVertex, pos); this.attributes.texcoord0.set(nVertex, texcoord); nVertex++; } } var IndicesCtor = vertexNum > 0xffff ? Uint32Array : Uint16Array; var nIndices = (uNum - 1) * (vNum - 1) * 6; var indices = this.indices = new IndicesCtor(nIndices); var n = 0; for (var j = 0; j < vNum - 1; j++) { for (var i = 0; i < uNum - 1; i++) { var i2 = j * uNum + i; var i1 = (j * uNum + i + 1); var i4 = (j + 1) * uNum + i + 1; var i3 = (j + 1) * uNum + i; indices[n++] = i1; indices[n++] = i2; indices[n++] = i4; indices[n++] = i2; indices[n++] = i3; indices[n++] = i4; } } this.generateVertexNormals(); this.updateBoundingBox(); } }); /** * Base class for all textures like compressed texture, texture2d, texturecube * TODO mapping */ /** * @constructor * @alias clay.Texture * @extends clay.core.Base */ var Texture = Base.extend( /** @lends clay.Texture# */ { /** * Texture width, readonly when the texture source is image * @type {number} */ width: 512, /** * Texture height, readonly when the texture source is image * @type {number} */ height: 512, /** * Texel data type. * Possible values: * + {@link clay.Texture.UNSIGNED_BYTE} * + {@link clay.Texture.HALF_FLOAT} * + {@link clay.Texture.FLOAT} * + {@link clay.Texture.UNSIGNED_INT_24_8_WEBGL} * + {@link clay.Texture.UNSIGNED_INT} * @type {number} */ type: glenum.UNSIGNED_BYTE, /** * Format of texel data * Possible values: * + {@link clay.Texture.RGBA} * + {@link clay.Texture.DEPTH_COMPONENT} * + {@link clay.Texture.DEPTH_STENCIL} * @type {number} */ format: glenum.RGBA, /** * Texture wrap. Default to be REPEAT. * Possible values: * + {@link clay.Texture.CLAMP_TO_EDGE} * + {@link clay.Texture.REPEAT} * + {@link clay.Texture.MIRRORED_REPEAT} * @type {number} */ wrapS: glenum.REPEAT, /** * Texture wrap. Default to be REPEAT. * Possible values: * + {@link clay.Texture.CLAMP_TO_EDGE} * + {@link clay.Texture.REPEAT} * + {@link clay.Texture.MIRRORED_REPEAT} * @type {number} */ wrapT: glenum.REPEAT, /** * Possible values: * + {@link clay.Texture.NEAREST} * + {@link clay.Texture.LINEAR} * + {@link clay.Texture.NEAREST_MIPMAP_NEAREST} * + {@link clay.Texture.LINEAR_MIPMAP_NEAREST} * + {@link clay.Texture.NEAREST_MIPMAP_LINEAR} * + {@link clay.Texture.LINEAR_MIPMAP_LINEAR} * @type {number} */ minFilter: glenum.LINEAR_MIPMAP_LINEAR, /** * Possible values: * + {@link clay.Texture.NEAREST} * + {@link clay.Texture.LINEAR} * @type {number} */ magFilter: glenum.LINEAR, /** * If enable mimap. * @type {boolean} */ useMipmap: true, /** * Anisotropic filtering, enabled if value is larger than 1 * @see https://developer.mozilla.org/en-US/docs/Web/API/EXT_texture_filter_anisotropic * @type {number} */ anisotropic: 1, // pixelStorei parameters, not available when texture is used as render target // http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml /** * If flip in y axis for given image source * @type {boolean} * @default true */ flipY: true, /** * A flag to indicate if texture source is sRGB */ sRGB: true, /** * @type {number} * @default 4 */ unpackAlignment: 4, /** * @type {boolean} * @default false */ premultiplyAlpha: false, /** * Dynamic option for texture like video * @type {boolean} */ dynamic: false, NPOT: false, // PENDING // Init it here to avoid deoptimization when it's assigned in application dynamically __used: 0 }, function () { this._cache = new Cache(); }, /** @lends clay.Texture.prototype */ { getWebGLTexture: function (renderer) { var _gl = renderer.gl; var cache = this._cache; cache.use(renderer.__uid__); if (cache.miss('webgl_texture')) { // In a new gl context, create new texture and set dirty true cache.put('webgl_texture', _gl.createTexture()); } if (this.dynamic) { this.update(renderer); } else if (cache.isDirty()) { this.update(renderer); cache.fresh(); } return cache.get('webgl_texture'); }, bind: function () {}, unbind: function () {}, /** * Mark texture is dirty and update in the next frame */ dirty: function () { if (this._cache) { this._cache.dirtyAll(); } }, update: function (renderer) {}, // Update the common parameters of texture updateCommon: function (renderer) { var _gl = renderer.gl; _gl.pixelStorei(_gl.UNPACK_FLIP_Y_WEBGL, this.flipY); _gl.pixelStorei(_gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha); _gl.pixelStorei(_gl.UNPACK_ALIGNMENT, this.unpackAlignment); // Use of none-power of two texture // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences if (this.format === glenum.DEPTH_COMPONENT) { this.useMipmap = false; } var sRGBExt = renderer.getGLExtension('EXT_sRGB'); // Fallback if (this.format === Texture.SRGB && !sRGBExt) { this.format = Texture.RGB; } if (this.format === Texture.SRGB_ALPHA && !sRGBExt) { this.format = Texture.RGBA; } this.NPOT = !this.isPowerOfTwo(); }, getAvailableWrapS: function () { if (this.NPOT) { return glenum.CLAMP_TO_EDGE; } return this.wrapS; }, getAvailableWrapT: function () { if (this.NPOT) { return glenum.CLAMP_TO_EDGE; } return this.wrapT; }, getAvailableMinFilter: function () { var minFilter = this.minFilter; if (this.NPOT || !this.useMipmap) { if (minFilter === glenum.NEAREST_MIPMAP_NEAREST || minFilter === glenum.NEAREST_MIPMAP_LINEAR ) { return glenum.NEAREST; } else if (minFilter === glenum.LINEAR_MIPMAP_LINEAR || minFilter === glenum.LINEAR_MIPMAP_NEAREST ) { return glenum.LINEAR; } else { return minFilter; } } else { return minFilter; } }, getAvailableMagFilter: function () { return this.magFilter; }, nextHighestPowerOfTwo: function (x) { --x; for (var i = 1; i < 32; i <<= 1) { x = x | x >> i; } return x + 1; }, /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); var webglTexture = cache.get('webgl_texture'); if (webglTexture){ renderer.gl.deleteTexture(webglTexture); } cache.deleteContext(renderer.__uid__); }, /** * Test if image of texture is valid and loaded. * @return {boolean} */ isRenderable: function () {}, /** * Test if texture size is power of two * @return {boolean} */ isPowerOfTwo: function () {} }); Object.defineProperty(Texture.prototype, 'width', { get: function () { return this._width; }, set: function (value) { this._width = value; } }); Object.defineProperty(Texture.prototype, 'height', { get: function () { return this._height; }, set: function (value) { this._height = value; } }); /* DataType */ /** * @type {number} */ Texture.BYTE = glenum.BYTE; /** * @type {number} */ Texture.UNSIGNED_BYTE = glenum.UNSIGNED_BYTE; /** * @type {number} */ Texture.SHORT = glenum.SHORT; /** * @type {number} */ Texture.UNSIGNED_SHORT = glenum.UNSIGNED_SHORT; /** * @type {number} */ Texture.INT = glenum.INT; /** * @type {number} */ Texture.UNSIGNED_INT = glenum.UNSIGNED_INT; /** * @type {number} */ Texture.FLOAT = glenum.FLOAT; /** * @type {number} */ Texture.HALF_FLOAT = 0x8D61; /** * UNSIGNED_INT_24_8_WEBGL for WEBGL_depth_texture extension * @type {number} */ Texture.UNSIGNED_INT_24_8_WEBGL = 34042; /* PixelFormat */ /** * @type {number} */ Texture.DEPTH_COMPONENT = glenum.DEPTH_COMPONENT; /** * @type {number} */ Texture.DEPTH_STENCIL = glenum.DEPTH_STENCIL; /** * @type {number} */ Texture.ALPHA = glenum.ALPHA; /** * @type {number} */ Texture.RGB = glenum.RGB; /** * @type {number} */ Texture.RGBA = glenum.RGBA; /** * @type {number} */ Texture.LUMINANCE = glenum.LUMINANCE; /** * @type {number} */ Texture.LUMINANCE_ALPHA = glenum.LUMINANCE_ALPHA; /** * @see https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/ * @type {number} */ Texture.SRGB = 0x8C40; /** * @see https://www.khronos.org/registry/webgl/extensions/EXT_sRGB/ * @type {number} */ Texture.SRGB_ALPHA = 0x8C42; /* Compressed Texture */ Texture.COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; Texture.COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; Texture.COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; Texture.COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; /* TextureMagFilter */ /** * @type {number} */ Texture.NEAREST = glenum.NEAREST; /** * @type {number} */ Texture.LINEAR = glenum.LINEAR; /* TextureMinFilter */ /** * @type {number} */ Texture.NEAREST_MIPMAP_NEAREST = glenum.NEAREST_MIPMAP_NEAREST; /** * @type {number} */ Texture.LINEAR_MIPMAP_NEAREST = glenum.LINEAR_MIPMAP_NEAREST; /** * @type {number} */ Texture.NEAREST_MIPMAP_LINEAR = glenum.NEAREST_MIPMAP_LINEAR; /** * @type {number} */ Texture.LINEAR_MIPMAP_LINEAR = glenum.LINEAR_MIPMAP_LINEAR; /* TextureWrapMode */ /** * @type {number} */ Texture.REPEAT = glenum.REPEAT; /** * @type {number} */ Texture.CLAMP_TO_EDGE = glenum.CLAMP_TO_EDGE; /** * @type {number} */ Texture.MIRRORED_REPEAT = glenum.MIRRORED_REPEAT; var mathUtil = {}; mathUtil.isPowerOfTwo = function (value) { return (value & (value - 1)) === 0; }; mathUtil.nextPowerOfTwo = function (value) { value --; value |= value >> 1; value |= value >> 2; value |= value >> 4; value |= value >> 8; value |= value >> 16; value ++; return value; }; mathUtil.nearestPowerOfTwo = function (value) { return Math.pow( 2, Math.round( Math.log( value ) / Math.LN2 ) ); }; var isPowerOfTwo = mathUtil.isPowerOfTwo; function nearestPowerOfTwo(val) { return Math.pow(2, Math.round(Math.log(val) / Math.LN2)); } function convertTextureToPowerOfTwo(texture, canvas) { // var canvas = document.createElement('canvas'); var width = nearestPowerOfTwo(texture.width); var height = nearestPowerOfTwo(texture.height); canvas = canvas || document.createElement('canvas'); canvas.width = width; canvas.height = height; var ctx = canvas.getContext('2d'); ctx.drawImage(texture.image, 0, 0, width, height); return canvas; } /** * @constructor clay.Texture2D * @extends clay.Texture * * @example * ... * var mat = new clay.Material({ * shader: clay.shader.library.get('clay.phong', 'diffuseMap') * }); * var diffuseMap = new clay.Texture2D(); * diffuseMap.load('assets/textures/diffuse.jpg'); * mat.set('diffuseMap', diffuseMap); * ... * diffuseMap.success(function () { * // Wait for the diffuse texture loaded * animation.on('frame', function (frameTime) { * renderer.render(scene, camera); * }); * }); */ var Texture2D = Texture.extend(function () { return /** @lends clay.Texture2D# */ { /** * @type {?HTMLImageElement|HTMLCanvasElemnet} */ // TODO mark dirty when assigned. image: null, /** * Pixels data. Will be ignored if image is set. * @type {?Uint8Array|Float32Array} */ pixels: null, /** * @type {Array.<Object>} * @example * [{ * image: mipmap0, * pixels: null * }, { * image: mipmap1, * pixels: null * }, ....] */ mipmaps: [], /** * If convert texture to power-of-two * @type {boolean} */ convertToPOT: false }; }, { textureType: 'texture2D', update: function (renderer) { var _gl = renderer.gl; _gl.bindTexture(_gl.TEXTURE_2D, this._cache.get('webgl_texture')); this.updateCommon(renderer); var glFormat = this.format; var glType = this.type; // Convert to pot is only available when using image/canvas/video element. var convertToPOT = !!(this.convertToPOT && !this.mipmaps.length && this.image && (this.wrapS === Texture.REPEAT || this.wrapT === Texture.REPEAT) && this.NPOT ); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, convertToPOT ? this.wrapS : this.getAvailableWrapS()); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, convertToPOT ? this.wrapT : this.getAvailableWrapT()); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MAG_FILTER, convertToPOT ? this.magFilter : this.getAvailableMagFilter()); _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, convertToPOT ? this.minFilter : this.getAvailableMinFilter()); var anisotropicExt = renderer.getGLExtension('EXT_texture_filter_anisotropic'); if (anisotropicExt && this.anisotropic > 1) { _gl.texParameterf(_gl.TEXTURE_2D, anisotropicExt.TEXTURE_MAX_ANISOTROPY_EXT, this.anisotropic); } // Fallback to float type if browser don't have half float extension if (glType === 36193) { var halfFloatExt = renderer.getGLExtension('OES_texture_half_float'); if (!halfFloatExt) { glType = glenum.FLOAT; } } if (this.mipmaps.length) { var width = this.width; var height = this.height; for (var i = 0; i < this.mipmaps.length; i++) { var mipmap = this.mipmaps[i]; this._updateTextureData(_gl, mipmap, i, width, height, glFormat, glType, false); width /= 2; height /= 2; } } else { this._updateTextureData(_gl, this, 0, this.width, this.height, glFormat, glType, convertToPOT); if (this.useMipmap && (!this.NPOT || convertToPOT)) { _gl.generateMipmap(_gl.TEXTURE_2D); } } _gl.bindTexture(_gl.TEXTURE_2D, null); }, _updateTextureData: function (_gl, data, level, width, height, glFormat, glType, convertToPOT) { if (data.image) { var imgData = data.image; if (convertToPOT) { this._potCanvas = convertTextureToPowerOfTwo(this, this._potCanvas); imgData = this._potCanvas; } _gl.texImage2D(_gl.TEXTURE_2D, level, glFormat, glFormat, glType, imgData); } else { // Can be used as a blank texture when writing render to texture(RTT) if ( glFormat <= Texture.COMPRESSED_RGBA_S3TC_DXT5_EXT && glFormat >= Texture.COMPRESSED_RGB_S3TC_DXT1_EXT ) { _gl.compressedTexImage2D(_gl.TEXTURE_2D, level, glFormat, width, height, 0, data.pixels); } else { // Is a render target if pixels is null _gl.texImage2D(_gl.TEXTURE_2D, level, glFormat, width, height, 0, glFormat, glType, data.pixels); } } }, /** * @param {clay.Renderer} renderer * @memberOf clay.Texture2D.prototype */ generateMipmap: function (renderer) { var _gl = renderer.gl; if (this.useMipmap && !this.NPOT) { _gl.bindTexture(_gl.TEXTURE_2D, this._cache.get('webgl_texture')); _gl.generateMipmap(_gl.TEXTURE_2D); } }, isPowerOfTwo: function () { return isPowerOfTwo(this.width) && isPowerOfTwo(this.height); }, isRenderable: function () { if (this.image) { return this.image.width > 0 && this.image.height > 0; } else { return !!(this.width && this.height); } }, bind: function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_2D, this.getWebGLTexture(renderer)); }, unbind: function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_2D, null); }, load: function (src, crossOrigin) { var image = vendor.createImage(); if (crossOrigin) { image.crossOrigin = crossOrigin; } var self = this; image.onload = function () { self.dirty(); self.trigger('success', self); }; image.onerror = function () { self.trigger('error', self); }; image.src = src; this.image = image; return this; } }); Object.defineProperty(Texture2D.prototype, 'width', { get: function () { if (this.image) { return this.image.width; } return this._width; }, set: function (value) { if (this.image) { console.warn('Texture from image can\'t set width'); } else { if (this._width !== value) { this.dirty(); } this._width = value; } } }); Object.defineProperty(Texture2D.prototype, 'height', { get: function () { if (this.image) { return this.image.height; } return this._height; }, set: function (value) { if (this.image) { console.warn('Texture from image can\'t set height'); } else { if (this._height !== value) { this.dirty(); } this._height = value; } } }); var isPowerOfTwo$1 = mathUtil.isPowerOfTwo; var targetList = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; /** * @constructor clay.TextureCube * @extends clay.Texture * * @example * ... * var mat = new clay.Material({ * shader: clay.shader.library.get('clay.phong', 'environmentMap') * }); * var envMap = new clay.TextureCube(); * envMap.load({ * 'px': 'assets/textures/sky/px.jpg', * 'nx': 'assets/textures/sky/nx.jpg' * 'py': 'assets/textures/sky/py.jpg' * 'ny': 'assets/textures/sky/ny.jpg' * 'pz': 'assets/textures/sky/pz.jpg' * 'nz': 'assets/textures/sky/nz.jpg' * }); * mat.set('environmentMap', envMap); * ... * envMap.success(function () { * // Wait for the sky texture loaded * animation.on('frame', function (frameTime) { * renderer.render(scene, camera); * }); * }); */ var TextureCube = Texture.extend(function () { return /** @lends clay.TextureCube# */{ /** * @type {boolean} * @default false */ // PENDING cubemap should not flipY in default. // flipY: false, /** * @type {Object} * @property {?HTMLImageElement|HTMLCanvasElemnet} px * @property {?HTMLImageElement|HTMLCanvasElemnet} nx * @property {?HTMLImageElement|HTMLCanvasElemnet} py * @property {?HTMLImageElement|HTMLCanvasElemnet} ny * @property {?HTMLImageElement|HTMLCanvasElemnet} pz * @property {?HTMLImageElement|HTMLCanvasElemnet} nz */ image: { px: null, nx: null, py: null, ny: null, pz: null, nz: null }, /** * Pixels data of each side. Will be ignored if images are set. * @type {Object} * @property {?Uint8Array} px * @property {?Uint8Array} nx * @property {?Uint8Array} py * @property {?Uint8Array} ny * @property {?Uint8Array} pz * @property {?Uint8Array} nz */ pixels: { px: null, nx: null, py: null, ny: null, pz: null, nz: null }, /** * @type {Array.<Object>} */ mipmaps: [] }; }, { textureType: 'textureCube', update: function (renderer) { var _gl = renderer.gl; _gl.bindTexture(_gl.TEXTURE_CUBE_MAP, this._cache.get('webgl_texture')); this.updateCommon(renderer); var glFormat = this.format; var glType = this.type; _gl.texParameteri(_gl.TEXTURE_CUBE_MAP, _gl.TEXTURE_WRAP_S, this.getAvailableWrapS()); _gl.texParameteri(_gl.TEXTURE_CUBE_MAP, _gl.TEXTURE_WRAP_T, this.getAvailableWrapT()); _gl.texParameteri(_gl.TEXTURE_CUBE_MAP, _gl.TEXTURE_MAG_FILTER, this.getAvailableMagFilter()); _gl.texParameteri(_gl.TEXTURE_CUBE_MAP, _gl.TEXTURE_MIN_FILTER, this.getAvailableMinFilter()); var anisotropicExt = renderer.getGLExtension('EXT_texture_filter_anisotropic'); if (anisotropicExt && this.anisotropic > 1) { _gl.texParameterf(_gl.TEXTURE_CUBE_MAP, anisotropicExt.TEXTURE_MAX_ANISOTROPY_EXT, this.anisotropic); } // Fallback to float type if browser don't have half float extension if (glType === 36193) { var halfFloatExt = renderer.getGLExtension('OES_texture_half_float'); if (!halfFloatExt) { glType = glenum.FLOAT; } } if (this.mipmaps.length) { var width = this.width; var height = this.height; for (var i = 0; i < this.mipmaps.length; i++) { var mipmap = this.mipmaps[i]; this._updateTextureData(_gl, mipmap, i, width, height, glFormat, glType); width /= 2; height /= 2; } } else { this._updateTextureData(_gl, this, 0, this.width, this.height, glFormat, glType); if (!this.NPOT && this.useMipmap) { _gl.generateMipmap(_gl.TEXTURE_CUBE_MAP); } } _gl.bindTexture(_gl.TEXTURE_CUBE_MAP, null); }, _updateTextureData: function (_gl, data, level, width, height, glFormat, glType) { for (var i = 0; i < 6; i++) { var target = targetList[i]; var img = data.image && data.image[target]; if (img) { _gl.texImage2D(_gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level, glFormat, glFormat, glType, img); } else { _gl.texImage2D(_gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, level, glFormat, width, height, 0, glFormat, glType, data.pixels && data.pixels[target]); } } }, /** * @param {clay.Renderer} renderer * @memberOf clay.TextureCube.prototype */ generateMipmap: function (renderer) { var _gl = renderer.gl; if (this.useMipmap && !this.NPOT) { _gl.bindTexture(_gl.TEXTURE_CUBE_MAP, this._cache.get('webgl_texture')); _gl.generateMipmap(_gl.TEXTURE_CUBE_MAP); } }, bind: function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_CUBE_MAP, this.getWebGLTexture(renderer)); }, unbind: function (renderer) { renderer.gl.bindTexture(renderer.gl.TEXTURE_CUBE_MAP, null); }, // Overwrite the isPowerOfTwo method isPowerOfTwo: function () { if (this.image.px) { return isPowerOfTwo$1(this.image.px.width) && isPowerOfTwo$1(this.image.px.height); } else { return isPowerOfTwo$1(this.width) && isPowerOfTwo$1(this.height); } }, isRenderable: function () { if (this.image.px) { return isImageRenderable(this.image.px) && isImageRenderable(this.image.nx) && isImageRenderable(this.image.py) && isImageRenderable(this.image.ny) && isImageRenderable(this.image.pz) && isImageRenderable(this.image.nz); } else { return !!(this.width && this.height); } }, load: function (imageList, crossOrigin) { var loading = 0; var self = this; util$1.each(imageList, function (src, target){ var image = vendor.createImage(); if (crossOrigin) { image.crossOrigin = crossOrigin; } image.onload = function () { loading --; if (loading === 0){ self.dirty(); self.trigger('success', self); } }; image.onerror = function () { loading --; }; loading++; image.src = src; self.image[target] = image; }); return this; } }); Object.defineProperty(TextureCube.prototype, 'width', { get: function () { if (this.image && this.image.px) { return this.image.px.width; } return this._width; }, set: function (value) { if (this.image && this.image.px) { console.warn('Texture from image can\'t set width'); } else { if (this._width !== value) { this.dirty(); } this._width = value; } } }); Object.defineProperty(TextureCube.prototype, 'height', { get: function () { if (this.image && this.image.px) { return this.image.px.height; } return this._height; }, set: function (value) { if (this.image && this.image.px) { console.warn('Texture from image can\'t set height'); } else { if (this._height !== value) { this.dirty(); } this._height = value; } } }); function isImageRenderable(image) { return image.width > 0 && image.height > 0; } /** * @constructor * @alias clay.Renderable * @extends clay.Node */ var Renderable = Node.extend(/** @lends clay.Renderable# */ { /** * @type {clay.Material} */ material: null, /** * @type {clay.Geometry} */ geometry: null, /** * @type {number} */ mode: glenum.TRIANGLES, _renderInfo: null }, /** @lends clay.Renderable.prototype */ { __program: null, /** * Group of received light. */ lightGroup: 0, /** * Render order, Nodes with smaller value renders before nodes with larger values. * @type {Number} */ renderOrder: 0, /** * Used when mode is LINES, LINE_STRIP or LINE_LOOP * @type {number} */ // lineWidth: 1, /** * If enable culling * @type {boolean} */ culling: true, /** * Specify which side of polygon will be culled. * Possible values: * + {@link clay.Renderable.BACK} * + {@link clay.Renderable.FRONT} * + {@link clay.Renderable.FRONT_AND_BACK} * @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/cullFace * @type {number} */ cullFace: glenum.BACK, /** * Specify which side is front face. * Possible values: * + {@link clay.Renderable.CW} * + {@link clay.Renderable.CCW} * @see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/frontFace * @type {number} */ frontFace: glenum.CCW, /** * If enable software frustum culling * @type {boolean} */ frustumCulling: true, /** * @type {boolean} */ receiveShadow: true, /** * @type {boolean} */ castShadow: true, /** * @type {boolean} */ ignorePicking: false, /** * @type {boolean} */ ignorePreZ: false, /** * @type {boolean} */ ignoreGBuffer: false, /** * @return {boolean} */ isRenderable: function() { // TODO Shader ? return this.geometry && this.material && this.material.shader && !this.invisible && this.geometry.vertexCount > 0; }, /** * Before render hook * @type {Function} */ beforeRender: function (_gl) {}, /** * Before render hook * @type {Function} */ afterRender: function (_gl, renderStat) {}, getBoundingBox: function (filter, out) { out = Node.prototype.getBoundingBox.call(this, filter, out); if (this.geometry && this.geometry.boundingBox) { out.union(this.geometry.boundingBox); } return out; }, /** * Clone a new renderable * @function * @return {clay.Renderable} */ clone: (function() { var properties = [ 'castShadow', 'receiveShadow', 'mode', 'culling', 'cullFace', 'frontFace', 'frustumCulling', 'renderOrder', 'lineWidth', 'ignorePicking', 'ignorePreZ', 'ignoreGBuffer' ]; return function() { var renderable = Node.prototype.clone.call(this); renderable.geometry = this.geometry; renderable.material = this.material; for (var i = 0; i < properties.length; i++) { var name = properties[i]; // Try not to overwrite the prototype property if (renderable[name] !== this[name]) { renderable[name] = this[name]; } } return renderable; }; })() }); /** * @type {number} */ Renderable.POINTS = glenum.POINTS; /** * @type {number} */ Renderable.LINES = glenum.LINES; /** * @type {number} */ Renderable.LINE_LOOP = glenum.LINE_LOOP; /** * @type {number} */ Renderable.LINE_STRIP = glenum.LINE_STRIP; /** * @type {number} */ Renderable.TRIANGLES = glenum.TRIANGLES; /** * @type {number} */ Renderable.TRIANGLE_STRIP = glenum.TRIANGLE_STRIP; /** * @type {number} */ Renderable.TRIANGLE_FAN = glenum.TRIANGLE_FAN; /** * @type {number} */ Renderable.BACK = glenum.BACK; /** * @type {number} */ Renderable.FRONT = glenum.FRONT; /** * @type {number} */ Renderable.FRONT_AND_BACK = glenum.FRONT_AND_BACK; /** * @type {number} */ Renderable.CW = glenum.CW; /** * @type {number} */ Renderable.CCW = glenum.CCW; /** * @constructor clay.Mesh * @extends clay.Renderable */ var Mesh = Renderable.extend(/** @lends clay.Mesh# */ { /** * Used when it is a skinned mesh * @type {clay.Skeleton} */ skeleton: null, /** * Joints indices Meshes can share the one skeleton instance and each mesh can use one part of joints. Joints indices indicate the index of joint in the skeleton instance * @type {number[]} */ joints: null }, function () { if (!this.joints) { this.joints = []; } }, { /** * Offset matrix used for multiple skinned mesh clone sharing one skeleton * @type {clay.Matrix4} */ offsetMatrix: null, isInstancedMesh: function () { return false; }, isSkinnedMesh: function () { return !!(this.skeleton && this.joints && this.joints.length > 0); }, clone: function () { var mesh = Renderable.prototype.clone.call(this); mesh.skeleton = this.skeleton; if (this.joints) { mesh.joints = this.joints.slice(); } return mesh; } }); // Enums Mesh.POINTS = glenum.POINTS; Mesh.LINES = glenum.LINES; Mesh.LINE_LOOP = glenum.LINE_LOOP; Mesh.LINE_STRIP = glenum.LINE_STRIP; Mesh.TRIANGLES = glenum.TRIANGLES; Mesh.TRIANGLE_STRIP = glenum.TRIANGLE_STRIP; Mesh.TRIANGLE_FAN = glenum.TRIANGLE_FAN; Mesh.BACK = glenum.BACK; Mesh.FRONT = glenum.FRONT; Mesh.FRONT_AND_BACK = glenum.FRONT_AND_BACK; Mesh.CW = glenum.CW; Mesh.CCW = glenum.CCW; /** * @constructor clay.camera.Perspective * @extends clay.Camera */ var Perspective$1 = Camera.extend(/** @lends clay.camera.Perspective# */{ /** * Vertical field of view in degrees * @type {number} */ fov: 50, /** * Aspect ratio, typically viewport width / height * @type {number} */ aspect: 1, /** * Near bound of the frustum * @type {number} */ near: 0.1, /** * Far bound of the frustum * @type {number} */ far: 2000 }, /** @lends clay.camera.Perspective.prototype */ { updateProjectionMatrix: function() { var rad = this.fov / 180 * Math.PI; this.projectionMatrix.perspective(rad, this.aspect, this.near, this.far); }, decomposeProjectionMatrix: function () { var m = this.projectionMatrix.array; var rad = Math.atan(1 / m[5]) * 2; this.fov = rad / Math.PI * 180; this.aspect = m[5] / m[0]; this.near = m[14] / (m[10] - 1); this.far = m[14] / (m[10] + 1); }, /** * @return {clay.camera.Perspective} */ clone: function() { var camera = Camera.prototype.clone.call(this); camera.fov = this.fov; camera.aspect = this.aspect; camera.near = this.near; camera.far = this.far; return camera; } }); /** * @constructor clay.camera.Orthographic * @extends clay.Camera */ var Orthographic$1 = Camera.extend( /** @lends clay.camera.Orthographic# */ { /** * @type {number} */ left: -1, /** * @type {number} */ right: 1, /** * @type {number} */ near: -1, /** * @type {number} */ far: 1, /** * @type {number} */ top: 1, /** * @type {number} */ bottom: -1 }, /** @lends clay.camera.Orthographic.prototype */ { updateProjectionMatrix: function() { this.projectionMatrix.ortho(this.left, this.right, this.bottom, this.top, this.near, this.far); }, decomposeProjectionMatrix: function () { var m = this.projectionMatrix.array; this.left = (-1 - m[12]) / m[0]; this.right = (1 - m[12]) / m[0]; this.top = (1 - m[13]) / m[5]; this.bottom = (-1 - m[13]) / m[5]; this.near = -(-1 - m[14]) / m[10]; this.far = -(1 - m[14]) / m[10]; }, /** * @return {clay.camera.Orthographic} */ clone: function() { var camera = Camera.prototype.clone.call(this); camera.left = this.left; camera.right = this.right; camera.near = this.near; camera.far = this.far; camera.top = this.top; camera.bottom = this.bottom; return camera; } }); var standardEssl = "\n@export clay.standard.chunk.varying\nvarying vec2 v_Texcoord;\nvarying vec3 v_Normal;\nvarying vec3 v_WorldPosition;\nvarying vec3 v_Barycentric;\n#if defined(PARALLAXOCCLUSIONMAP_ENABLED) || defined(NORMALMAP_ENABLED)\nvarying vec3 v_Tangent;\nvarying vec3 v_Bitangent;\n#endif\n#if defined(AOMAP_ENABLED)\nvarying vec2 v_Texcoord2;\n#endif\n#ifdef VERTEX_COLOR\nvarying vec4 v_Color;\n#endif\n@end\n@export clay.standard.chunk.light_header\n#ifdef AMBIENT_LIGHT_COUNT\n@import clay.header.ambient_light\n#endif\n#ifdef AMBIENT_SH_LIGHT_COUNT\n@import clay.header.ambient_sh_light\n#endif\n#ifdef AMBIENT_CUBEMAP_LIGHT_COUNT\n@import clay.header.ambient_cubemap_light\n#endif\n#ifdef POINT_LIGHT_COUNT\n@import clay.header.point_light\n#endif\n#ifdef DIRECTIONAL_LIGHT_COUNT\n@import clay.header.directional_light\n#endif\n#ifdef SPOT_LIGHT_COUNT\n@import clay.header.spot_light\n#endif\n@end\n@export clay.standard.vertex\n#define SHADER_NAME standard\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nuniform mat4 worldInverseTranspose : WORLDINVERSETRANSPOSE;\nuniform mat4 world : WORLD;\nuniform vec2 uvRepeat = vec2(1.0, 1.0);\nuniform vec2 uvOffset = vec2(0.0, 0.0);\nattribute vec3 position : POSITION;\nattribute vec2 texcoord : TEXCOORD_0;\n#if defined(AOMAP_ENABLED)\nattribute vec2 texcoord2 : TEXCOORD_1;\n#endif\nattribute vec3 normal : NORMAL;\nattribute vec4 tangent : TANGENT;\n#ifdef VERTEX_COLOR\nattribute vec4 a_Color : COLOR;\n#endif\nattribute vec3 barycentric;\n@import clay.standard.chunk.varying\n@import clay.chunk.skinning_header\n@import clay.chunk.instancing_header\nvoid main()\n{\n vec4 skinnedPosition = vec4(position, 1.0);\n vec4 skinnedNormal = vec4(normal, 0.0);\n vec4 skinnedTangent = vec4(tangent.xyz, 0.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = skinMatrixWS * skinnedPosition;\n skinnedNormal = skinMatrixWS * skinnedNormal;\n skinnedTangent = skinMatrixWS * skinnedTangent;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n skinnedPosition = instanceMat * skinnedPosition;\n skinnedNormal = instanceMat * skinnedNormal;\n skinnedTangent = instanceMat * skinnedTangent;\n#endif\n gl_Position = worldViewProjection * skinnedPosition;\n v_Texcoord = texcoord * uvRepeat + uvOffset;\n v_WorldPosition = (world * skinnedPosition).xyz;\n v_Barycentric = barycentric;\n v_Normal = normalize((worldInverseTranspose * skinnedNormal).xyz);\n#if defined(PARALLAXOCCLUSIONMAP_ENABLED) || defined(NORMALMAP_ENABLED)\n v_Tangent = normalize((worldInverseTranspose * skinnedTangent).xyz);\n v_Bitangent = normalize(cross(v_Normal, v_Tangent) * tangent.w);\n#endif\n#ifdef VERTEX_COLOR\n v_Color = a_Color;\n#endif\n#if defined(AOMAP_ENABLED)\n v_Texcoord2 = texcoord2;\n#endif\n}\n@end\n@export clay.standard.fragment\n#define PI 3.14159265358979\n#define GLOSSINESS_CHANNEL 0\n#define ROUGHNESS_CHANNEL 0\n#define METALNESS_CHANNEL 1\n#define DIFFUSEMAP_ALPHA_ALPHA\n@import clay.standard.chunk.varying\nuniform mat4 viewInverse : VIEWINVERSE;\n#ifdef NORMALMAP_ENABLED\nuniform sampler2D normalMap;\n#endif\nuniform float normalScale: 1.0;\n#ifdef DIFFUSEMAP_ENABLED\nuniform sampler2D diffuseMap;\n#endif\n#ifdef SPECULARMAP_ENABLED\nuniform sampler2D specularMap;\n#endif\n#ifdef USE_ROUGHNESS\nuniform float roughness : 0.5;\n #ifdef ROUGHNESSMAP_ENABLED\nuniform sampler2D roughnessMap;\n #endif\n#else\nuniform float glossiness: 0.5;\n #ifdef GLOSSINESSMAP_ENABLED\nuniform sampler2D glossinessMap;\n #endif\n#endif\n#ifdef METALNESSMAP_ENABLED\nuniform sampler2D metalnessMap;\n#endif\n#ifdef OCCLUSIONMAP_ENABLED\nuniform sampler2D occlusionMap;\n#endif\n#ifdef ENVIRONMENTMAP_ENABLED\nuniform samplerCube environmentMap;\n #ifdef PARALLAX_CORRECTED\nuniform vec3 environmentBoxMin;\nuniform vec3 environmentBoxMax;\n #endif\n#endif\n#ifdef BRDFLOOKUP_ENABLED\nuniform sampler2D brdfLookup;\n#endif\n#ifdef EMISSIVEMAP_ENABLED\nuniform sampler2D emissiveMap;\n#endif\n#ifdef SSAOMAP_ENABLED\nuniform sampler2D ssaoMap;\nuniform vec4 viewport : VIEWPORT;\n#endif\n#ifdef AOMAP_ENABLED\nuniform sampler2D aoMap;\nuniform float aoIntensity;\n#endif\nuniform vec3 color : [1.0, 1.0, 1.0];\nuniform float alpha : 1.0;\n#ifdef ALPHA_TEST\nuniform float alphaCutoff: 0.9;\n#endif\n#ifdef USE_METALNESS\nuniform float metalness : 0.0;\n#else\nuniform vec3 specularColor : [0.1, 0.1, 0.1];\n#endif\nuniform vec3 emission : [0.0, 0.0, 0.0];\nuniform float emissionIntensity: 1;\nuniform float lineWidth : 0.0;\nuniform vec4 lineColor : [0.0, 0.0, 0.0, 0.6];\n#ifdef ENVIRONMENTMAP_PREFILTER\nuniform float maxMipmapLevel: 5;\n#endif\n@import clay.standard.chunk.light_header\n@import clay.util.calculate_attenuation\n@import clay.util.edge_factor\n@import clay.util.rgbm\n@import clay.util.srgb\n@import clay.plugin.compute_shadow_map\n@import clay.util.parallax_correct\n@import clay.util.ACES\nfloat G_Smith(float g, float ndv, float ndl)\n{\n float roughness = 1.0 - g;\n float k = roughness * roughness / 2.0;\n float G1V = ndv / (ndv * (1.0 - k) + k);\n float G1L = ndl / (ndl * (1.0 - k) + k);\n return G1L * G1V;\n}\nvec3 F_Schlick(float ndv, vec3 spec) {\n return spec + (1.0 - spec) * pow(1.0 - ndv, 5.0);\n}\nfloat D_Phong(float g, float ndh) {\n float a = pow(8192.0, g);\n return (a + 2.0) / 8.0 * pow(ndh, a);\n}\nfloat D_GGX(float g, float ndh) {\n float r = 1.0 - g;\n float a = r * r;\n float tmp = ndh * ndh * (a - 1.0) + 1.0;\n return a / (PI * tmp * tmp);\n}\n#ifdef PARALLAXOCCLUSIONMAP_ENABLED\nuniform float parallaxOcclusionScale : 0.02;\nuniform float parallaxMaxLayers : 20;\nuniform float parallaxMinLayers : 5;\nuniform sampler2D parallaxOcclusionMap;\nmat3 transpose(in mat3 inMat)\n{\n vec3 i0 = inMat[0];\n vec3 i1 = inMat[1];\n vec3 i2 = inMat[2];\n return mat3(\n vec3(i0.x, i1.x, i2.x),\n vec3(i0.y, i1.y, i2.y),\n vec3(i0.z, i1.z, i2.z)\n );\n}\nvec2 parallaxUv(vec2 uv, vec3 viewDir)\n{\n float numLayers = mix(parallaxMaxLayers, parallaxMinLayers, abs(dot(vec3(0.0, 0.0, 1.0), viewDir)));\n float layerHeight = 1.0 / numLayers;\n float curLayerHeight = 0.0;\n vec2 deltaUv = viewDir.xy * parallaxOcclusionScale / (viewDir.z * numLayers);\n vec2 curUv = uv;\n float height = 1.0 - texture2D(parallaxOcclusionMap, curUv).r;\n for (int i = 0; i < 30; i++) {\n curLayerHeight += layerHeight;\n curUv -= deltaUv;\n height = 1.0 - texture2D(parallaxOcclusionMap, curUv).r;\n if (height < curLayerHeight) {\n break;\n }\n }\n vec2 prevUv = curUv + deltaUv;\n float next = height - curLayerHeight;\n float prev = 1.0 - texture2D(parallaxOcclusionMap, prevUv).r - curLayerHeight + layerHeight;\n return mix(curUv, prevUv, next / (next - prev));\n}\n#endif\nvoid main() {\n vec4 albedoColor = vec4(color, alpha);\n#ifdef VERTEX_COLOR\n albedoColor *= v_Color;\n#endif\n#ifdef SRGB_DECODE\n albedoColor = sRGBToLinear(albedoColor);\n#endif\n vec3 eyePos = viewInverse[3].xyz;\n vec3 V = normalize(eyePos - v_WorldPosition);\n vec2 uv = v_Texcoord;\n#if defined(PARALLAXOCCLUSIONMAP_ENABLED) || defined(NORMALMAP_ENABLED)\n mat3 tbn = mat3(v_Tangent, v_Bitangent, v_Normal);\n#endif\n#ifdef PARALLAXOCCLUSIONMAP_ENABLED\n uv = parallaxUv(v_Texcoord, normalize(transpose(tbn) * -V));\n#endif\n#ifdef DIFFUSEMAP_ENABLED\n vec4 texel = texture2D(diffuseMap, uv);\n #ifdef SRGB_DECODE\n texel = sRGBToLinear(texel);\n #endif\n albedoColor.rgb *= texel.rgb;\n #ifdef DIFFUSEMAP_ALPHA_ALPHA\n albedoColor.a *= texel.a;\n #endif\n#endif\n#ifdef USE_METALNESS\n float m = metalness;\n #ifdef METALNESSMAP_ENABLED\n float m2 = texture2D(metalnessMap, uv)[METALNESS_CHANNEL];\n m = clamp(m2 + (m - 0.5) * 2.0, 0.0, 1.0);\n #endif\n vec3 baseColor = albedoColor.rgb;\n albedoColor.rgb = baseColor * (1.0 - m);\n vec3 spec = mix(vec3(0.04), baseColor, m);\n#else\n vec3 spec = specularColor;\n#endif\n#ifdef USE_ROUGHNESS\n float g = clamp(1.0 - roughness, 0.0, 1.0);\n #ifdef ROUGHNESSMAP_ENABLED\n float g2 = 1.0 - texture2D(roughnessMap, uv)[ROUGHNESS_CHANNEL];\n g = clamp(g2 + (g - 0.5) * 2.0, 0.0, 1.0);\n #endif\n#else\n float g = glossiness;\n #ifdef GLOSSINESSMAP_ENABLED\n float g2 = texture2D(glossinessMap, uv)[GLOSSINESS_CHANNEL];\n g = clamp(g2 + (g - 0.5) * 2.0, 0.0, 1.0);\n #endif\n#endif\n#ifdef SPECULARMAP_ENABLED\n spec *= sRGBToLinear(texture2D(specularMap, uv)).rgb;\n#endif\n vec3 N = v_Normal;\n#ifdef DOUBLE_SIDED\n if (dot(N, V) < 0.0) {\n N = -N;\n }\n#endif\n#ifdef NORMALMAP_ENABLED\n if (dot(v_Tangent, v_Tangent) > 0.0) {\n vec3 normalTexel = texture2D(normalMap, uv).xyz;\n if (dot(normalTexel, normalTexel) > 0.0) { N = (normalTexel * 2.0 - 1.0);\n N = normalize(N * vec3(normalScale, normalScale, 1.0));\n tbn[1] = -tbn[1];\n N = normalize(tbn * N);\n }\n }\n#endif\n vec3 diffuseTerm = vec3(0.0, 0.0, 0.0);\n vec3 specularTerm = vec3(0.0, 0.0, 0.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n vec3 fresnelTerm = F_Schlick(ndv, spec);\n#ifdef AMBIENT_LIGHT_COUNT\n for(int _idx_ = 0; _idx_ < AMBIENT_LIGHT_COUNT; _idx_++)\n {{\n diffuseTerm += ambientLightColor[_idx_];\n }}\n#endif\n#ifdef AMBIENT_SH_LIGHT_COUNT\n for(int _idx_ = 0; _idx_ < AMBIENT_SH_LIGHT_COUNT; _idx_++)\n {{\n diffuseTerm += calcAmbientSHLight(_idx_, N) * ambientSHLightColor[_idx_];\n }}\n#endif\n#ifdef POINT_LIGHT_COUNT\n#if defined(POINT_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsPoint[POINT_LIGHT_COUNT];\n if(shadowEnabled)\n {\n computeShadowOfPointLights(v_WorldPosition, shadowContribsPoint);\n }\n#endif\n for(int _idx_ = 0; _idx_ < POINT_LIGHT_COUNT; _idx_++)\n {{\n vec3 lightPosition = pointLightPosition[_idx_];\n vec3 lc = pointLightColor[_idx_];\n float range = pointLightRange[_idx_];\n vec3 L = lightPosition - v_WorldPosition;\n float dist = length(L);\n float attenuation = lightAttenuation(dist, range);\n L /= dist;\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float shadowContrib = 1.0;\n#if defined(POINT_LIGHT_SHADOWMAP_COUNT)\n if(shadowEnabled)\n {\n shadowContrib = shadowContribsPoint[_idx_];\n }\n#endif\n vec3 li = lc * ndl * attenuation * shadowContrib;\n diffuseTerm += li;\n specularTerm += li * fresnelTerm * D_Phong(g, ndh);\n }}\n#endif\n#ifdef DIRECTIONAL_LIGHT_COUNT\n#if defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsDir[DIRECTIONAL_LIGHT_COUNT];\n if(shadowEnabled)\n {\n computeShadowOfDirectionalLights(v_WorldPosition, shadowContribsDir);\n }\n#endif\n for(int _idx_ = 0; _idx_ < DIRECTIONAL_LIGHT_COUNT; _idx_++)\n {{\n vec3 L = -normalize(directionalLightDirection[_idx_]);\n vec3 lc = directionalLightColor[_idx_];\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float shadowContrib = 1.0;\n#if defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT)\n if(shadowEnabled)\n {\n shadowContrib = shadowContribsDir[_idx_];\n }\n#endif\n vec3 li = lc * ndl * shadowContrib;\n diffuseTerm += li;\n specularTerm += li * fresnelTerm * D_Phong(g, ndh);\n }}\n#endif\n#ifdef SPOT_LIGHT_COUNT\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsSpot[SPOT_LIGHT_COUNT];\n if(shadowEnabled)\n {\n computeShadowOfSpotLights(v_WorldPosition, shadowContribsSpot);\n }\n#endif\n for(int i = 0; i < SPOT_LIGHT_COUNT; i++)\n {\n vec3 lightPosition = spotLightPosition[i];\n vec3 spotLightDirection = -normalize(spotLightDirection[i]);\n vec3 lc = spotLightColor[i];\n float range = spotLightRange[i];\n float a = spotLightUmbraAngleCosine[i];\n float b = spotLightPenumbraAngleCosine[i];\n float falloffFactor = spotLightFalloffFactor[i];\n vec3 L = lightPosition - v_WorldPosition;\n float dist = length(L);\n float attenuation = lightAttenuation(dist, range);\n L /= dist;\n float c = dot(spotLightDirection, L);\n float falloff;\n falloff = clamp((c - a) /(b - a), 0.0, 1.0);\n falloff = pow(falloff, falloffFactor);\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float shadowContrib = 1.0;\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT)\n if (shadowEnabled)\n {\n shadowContrib = shadowContribsSpot[i];\n }\n#endif\n vec3 li = lc * attenuation * (1.0 - falloff) * shadowContrib * ndl;\n diffuseTerm += li;\n specularTerm += li * fresnelTerm * D_Phong(g, ndh);\n }\n#endif\n vec4 outColor = albedoColor;\n outColor.rgb *= max(diffuseTerm, vec3(0.0));\n outColor.rgb += max(specularTerm, vec3(0.0));\n#ifdef AMBIENT_CUBEMAP_LIGHT_COUNT\n vec3 L = reflect(-V, N);\n float rough2 = clamp(1.0 - g, 0.0, 1.0);\n float bias2 = rough2 * 5.0;\n vec2 brdfParam2 = texture2D(ambientCubemapLightBRDFLookup[0], vec2(rough2, ndv)).xy;\n vec3 envWeight2 = spec * brdfParam2.x + brdfParam2.y;\n vec3 envTexel2;\n for(int _idx_ = 0; _idx_ < AMBIENT_CUBEMAP_LIGHT_COUNT; _idx_++)\n {{\n #ifdef SUPPORT_TEXTURE_LOD\n envTexel2 = RGBMDecode(textureCubeLodEXT(ambientCubemapLightCubemap[_idx_], L, bias2), 8.12);\n #else\n envTexel2 = RGBMDecode(textureCube(ambientCubemapLightCubemap[_idx_], L), 8.12);\n #endif\n outColor.rgb += ambientCubemapLightColor[_idx_] * envTexel2 * envWeight2;\n }}\n#endif\n#ifdef ENVIRONMENTMAP_ENABLED\n vec3 envWeight = g * fresnelTerm;\n vec3 L = reflect(-V, N);\n #ifdef PARALLAX_CORRECTED\n L = parallaxCorrect(L, v_WorldPosition, environmentBoxMin, environmentBoxMax);\n#endif\n #ifdef ENVIRONMENTMAP_PREFILTER\n float rough = clamp(1.0 - g, 0.0, 1.0);\n float bias = rough * maxMipmapLevel;\n #ifdef SUPPORT_TEXTURE_LOD\n vec3 envTexel = decodeHDR(textureCubeLodEXT(environmentMap, L, bias)).rgb;\n #else\n vec3 envTexel = decodeHDR(textureCube(environmentMap, L)).rgb;\n #endif\n #ifdef BRDFLOOKUP_ENABLED\n vec2 brdfParam = texture2D(brdfLookup, vec2(rough, ndv)).xy;\n envWeight = spec * brdfParam.x + brdfParam.y;\n #endif\n #else\n vec3 envTexel = textureCube(environmentMap, L).xyz;\n #endif\n outColor.rgb += envTexel * envWeight;\n#endif\n float aoFactor = 1.0;\n#ifdef SSAOMAP_ENABLED\n aoFactor = min(texture2D(ssaoMap, (gl_FragCoord.xy - viewport.xy) / viewport.zw).r, aoFactor);\n#endif\n#ifdef AOMAP_ENABLED\n aoFactor = min(1.0 - clamp((1.0 - texture2D(aoMap, v_Texcoord2).r) * aoIntensity, 0.0, 1.0), aoFactor);\n#endif\n#ifdef OCCLUSIONMAP_ENABLED\n aoFactor = min(1.0 - clamp((1.0 - texture2D(occlusionMap, v_Texcoord).r), 0.0, 1.0), aoFactor);\n#endif\n outColor.rgb *= aoFactor;\n vec3 lEmission = emission;\n#ifdef EMISSIVEMAP_ENABLED\n lEmission *= texture2D(emissiveMap, uv).rgb;\n#endif\n outColor.rgb += lEmission * emissionIntensity;\n if(lineWidth > 0.)\n {\n outColor.rgb = mix(outColor.rgb, lineColor.rgb, (1.0 - edgeFactor(lineWidth)) * lineColor.a);\n }\n#ifdef ALPHA_TEST\n if (outColor.a < alphaCutoff) {\n discard;\n }\n#endif\n#ifdef TONEMAPPING\n outColor.rgb = ACESToneMapping(outColor.rgb);\n#endif\n#ifdef SRGB_ENCODE\n outColor = linearTosRGB(outColor);\n#endif\n gl_FragColor = encodeHDR(outColor);\n}\n@end\n@export clay.standardMR.vertex\n@import clay.standard.vertex\n@end\n@export clay.standardMR.fragment\n#define USE_METALNESS\n#define USE_ROUGHNESS\n@import clay.standard.fragment\n@end"; // Import standard shader Shader['import'](standardEssl); var TEXTURE_PROPERTIES = ['diffuseMap', 'normalMap', 'roughnessMap', 'metalnessMap', 'emissiveMap', 'environmentMap', 'brdfLookup', 'ssaoMap', 'aoMap']; var SIMPLE_PROPERTIES = ['color', 'emission', 'emissionIntensity', 'alpha', 'roughness', 'metalness', 'uvRepeat', 'uvOffset', 'aoIntensity', 'alphaCutoff', 'normalScale']; var PROPERTIES_CHANGE_SHADER = ['linear', 'encodeRGBM', 'decodeRGBM', 'doubleSided', 'alphaTest', 'roughnessChannel', 'metalnessChannel', 'environmentMapPrefiltered']; var NUM_DEFINE_MAP = { 'roughnessChannel': 'ROUGHNESS_CHANNEL', 'metalnessChannel': 'METALNESS_CHANNEL' }; var BOOL_DEFINE_MAP = { 'linear': 'SRGB_DECODE', 'encodeRGBM': 'RGBM_ENCODE', 'decodeRGBM': 'RGBM_DECODE', 'doubleSided': 'DOUBLE_SIDED', 'alphaTest': 'ALPHA_TEST', 'environmentMapPrefiltered': 'ENVIRONMENTMAP_PREFILTER' }; var standardShader; /** * Standard material without custom shader. * @constructor clay.StandardMaterial * @extends clay.Base * @example * var mat = new clay.StandardMaterial({ * color: [1, 1, 1], * diffuseMap: diffuseTexture * }); * mat.roughness = 1; */ var StandardMaterial = Material.extend(function () { if (!standardShader) { standardShader = new Shader(Shader.source('clay.standardMR.vertex'), Shader.source('clay.standardMR.fragment')); } return /** @lends clay.StandardMaterial# */ { shader: standardShader }; }, function (option) { // PENDING util$1.extend(this, option); // Extend after shader is created. util$1.defaults(this, /** @lends clay.StandardMaterial# */ { /** * @type {Array.<number>} * @default [1, 1, 1] */ color: [1, 1, 1], /** * @type {Array.<number>} * @default [0, 0, 0] */ emission: [0, 0, 0], /** * @type {number} * @default 0 */ emissionIntensity: 0, /** * @type {number} * @default 0.5 */ roughness: 0.5, /** * @type {number} * @default 0 */ metalness: 0, /** * @type {number} * @default 1 */ alpha: 1, /** * @type {boolean} */ alphaTest: false, /** * Cutoff threshold for alpha test * @type {number} */ alphaCutoff: 0.9, /** * Scalar multiplier applied to each normal vector of normal texture. * * @type {number} * * XXX This value is considered only if a normal texture is specified. */ normalScale: 1.0, /** * @type {boolean} */ // TODO Must disable culling. doubleSided: false, /** * @type {clay.Texture2D} */ diffuseMap: null, /** * @type {clay.Texture2D} */ normalMap: null, /** * @type {clay.Texture2D} */ roughnessMap: null, /** * @type {clay.Texture2D} */ metalnessMap: null, /** * @type {clay.Texture2D} */ emissiveMap: null, /** * @type {clay.TextureCube} */ environmentMap: null, /** * @type {clay.BoundingBox} */ environmentBox: null, /** * BRDF Lookup is generated by clay.util.cubemap.integrateBrdf * @type {clay.Texture2D} */ brdfLookup: null, /** * @type {clay.Texture2D} */ ssaoMap: null, /** * @type {clay.Texture2D} */ aoMap: null, /** * @type {Array.<number>} * @default [1, 1] */ uvRepeat: [1, 1], /** * @type {Array.<number>} * @default [0, 0] */ uvOffset: [0, 0], /** * @type {number} * @default 1 */ aoIntensity: 1, /** * @type {boolean} */ environmentMapPrefiltered: false, /** * @type {boolean} */ linear: false, /** * @type {boolean} */ encodeRGBM: false, /** * @type {boolean} */ decodeRGBM: false, /** * @type {Number} */ roughnessChannel: 0, /** * @type {Number} */ metalnessChannel: 1 }); }, { clone: function () { var material = new StandardMaterial({ name: this.name }); TEXTURE_PROPERTIES.forEach(function (propName) { if (this[propName]) { material[propName] = this[propName]; } }, this); SIMPLE_PROPERTIES.concat(PROPERTIES_CHANGE_SHADER).forEach(function (propName) { material[propName] = this[propName]; }, this); return material; } }); SIMPLE_PROPERTIES.forEach(function (propName) { Object.defineProperty(StandardMaterial.prototype, propName, { get: function () { return this.get(propName); }, set: function (value) { this.setUniform(propName, value); } }); }); TEXTURE_PROPERTIES.forEach(function (propName) { Object.defineProperty(StandardMaterial.prototype, propName, { get: function () { return this.get(propName); }, set: function (value) { this.setUniform(propName, value); } }); }); PROPERTIES_CHANGE_SHADER.forEach(function (propName) { var privateKey = '_' + propName; Object.defineProperty(StandardMaterial.prototype, propName, { get: function () { return this[privateKey]; }, set: function (value) { this[privateKey] = value; if (propName in NUM_DEFINE_MAP) { var defineName = NUM_DEFINE_MAP[propName]; this.define('fragment', defineName, value); } else { var defineName = BOOL_DEFINE_MAP[propName]; value ? this.define('fragment', defineName) : this.undefine('fragment', defineName); } } }); }); Object.defineProperty(StandardMaterial.prototype, 'environmentBox', { get: function () { var envBox = this._environmentBox; if (envBox) { envBox.min.setArray(this.get('environmentBoxMin')); envBox.max.setArray(this.get('environmentBoxMax')); } return envBox; }, set: function (value) { this._environmentBox = value; var uniforms = this.uniforms = this.uniforms || {}; uniforms['environmentBoxMin'] = uniforms['environmentBoxMin'] || { value: null }; uniforms['environmentBoxMax'] = uniforms['environmentBoxMax'] || { value: null }; // TODO Can't detect operation like box.min = new Vector() if (value) { this.setUniform('environmentBoxMin', value.min.array); this.setUniform('environmentBoxMax', value.max.array); } if (value) { this.define('fragment', 'PARALLAX_CORRECTED'); } else { this.undefine('fragment', 'PARALLAX_CORRECTED'); } } }); var _library = {}; function ShaderLibrary () { this._pool = {}; } ShaderLibrary.prototype.get = function(name) { var key = name; if (this._pool[key]) { return this._pool[key]; } else { var source = _library[name]; if (!source) { console.error('Shader "' + name + '"' + ' is not in the library'); return; } var shader = new Shader(source.vertex, source.fragment); this._pool[key] = shader; return shader; } }; ShaderLibrary.prototype.clear = function() { this._pool = {}; }; function template(name, vertex, fragment) { _library[name] = { vertex: vertex, fragment: fragment }; } var defaultLibrary = new ShaderLibrary(); /** * ### Builin shaders * + clay.standard * + clay.basic * + clay.lambert * + clay.wireframe * * @namespace clay.shader.library */ var library = { /** * Create a new shader library. */ createLibrary: function () { return new ShaderLibrary(); }, /** * Get shader from default library. * @param {string} name * @return {clay.Shader} * @memberOf clay.shader.library * @example * clay.shader.library.get('clay.standard') */ get: function () { return defaultLibrary.get.apply(defaultLibrary, arguments); }, /** * @memberOf clay.shader.library * @param {string} name * @param {string} vertex - Vertex shader code * @param {string} fragment - Fragment shader code */ template: template, clear: function () { return defaultLibrary.clear(); } }; /** * @constructor clay.Joint * @extends clay.core.Base */ var Joint = Base.extend( /** @lends clay.Joint# */ { // https://github.com/KhronosGroup/glTF/issues/193#issuecomment-29216576 /** * Joint name * @type {string} */ name: '', /** * Index of joint in the skeleton * @type {number} */ index: -1, /** * Scene node attached to * @type {clay.Node} */ node: null }); var tmpBoundingBox = new BoundingBox(); var tmpMat4 = new Matrix4(); /** * @constructor clay.Skeleton */ var Skeleton = Base.extend(function () { return /** @lends clay.Skeleton# */{ /** * Relative root node that not affect transform of joint. * @type {clay.Node} */ relativeRootNode: null, /** * @type {string} */ name: '', /** * joints * @type {Array.<clay.Joint>} */ joints: [], /** * bounding box with bound geometry. * @type {clay.BoundingBox} */ boundingBox: null, _clips: [], // Matrix to joint space (relative to root joint) _invBindPoseMatricesArray: null, // Use subarray instead of copy back each time computing matrix // http://jsperf.com/subarray-vs-copy-for-array-transform/5 _jointMatricesSubArrays: [], // jointMatrix * currentPoseMatrix // worldTransform is relative to the root bone // still in model space not world space _skinMatricesArray: null, _skinMatricesSubArrays: [], _subSkinMatricesArray: {} }; }, /** @lends clay.Skeleton.prototype */ { /** * Add a skinning clip and create a map between clip and skeleton * @param {clay.animation.SkinningClip} clip * @param {Object} [mapRule] Map between joint name in skeleton and joint name in clip */ addClip: function (clip, mapRule) { // Clip have been exists in for (var i = 0; i < this._clips.length; i++) { if (this._clips[i].clip === clip) { return; } } // Map the joint index in skeleton to joint pose index in clip var maps = []; for (var i = 0; i < this.joints.length; i++) { maps[i] = -1; } // Create avatar for (var i = 0; i < clip.tracks.length; i++) { for (var j = 0; j < this.joints.length; j++) { var joint = this.joints[j]; var track = clip.tracks[i]; var jointName = joint.name; if (mapRule) { jointName = mapRule[jointName]; } if (track.name === jointName) { maps[j] = i; break; } } } this._clips.push({ maps: maps, clip: clip }); return this._clips.length - 1; }, /** * @param {clay.animation.SkinningClip} clip */ removeClip: function (clip) { var idx = -1; for (var i = 0; i < this._clips.length; i++) { if (this._clips[i].clip === clip) { idx = i; break; } } if (idx > 0) { this._clips.splice(idx, 1); } }, /** * Remove all clips */ removeClipsAll: function () { this._clips = []; }, /** * Get clip by index * @param {number} index */ getClip: function (index) { if (this._clips[index]) { return this._clips[index].clip; } }, /** * @return {number} */ getClipNumber: function () { return this._clips.length; }, /** * Calculate joint matrices from node transform * @function */ updateJointMatrices: (function () { var m4 = mat4.create(); return function () { this._invBindPoseMatricesArray = new Float32Array(this.joints.length * 16); this._skinMatricesArray = new Float32Array(this.joints.length * 16); for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; mat4.copy(m4, joint.node.worldTransform.array); mat4.invert(m4, m4); var offset = i * 16; for (var j = 0; j < 16; j++) { this._invBindPoseMatricesArray[offset + j] = m4[j]; } } this.updateMatricesSubArrays(); }; })(), /** * Update boundingBox of each joint bound to geometry. * ASSUME skeleton and geometry joints are matched. * @param {clay.Geometry} geometry */ updateJointsBoundingBoxes: function (geometry) { var attributes = geometry.attributes; var positionAttr = attributes.position; var jointAttr = attributes.joint; var weightAttr = attributes.weight; var jointsBoundingBoxes = []; for (var i = 0; i < this.joints.length; i++) { jointsBoundingBoxes[i] = new BoundingBox(); jointsBoundingBoxes[i].__updated = false; } var vtxJoint = []; var vtxPos = []; var vtxWeight = []; var maxJointIdx = 0; for (var i = 0; i < geometry.vertexCount; i++) { jointAttr.get(i, vtxJoint); positionAttr.get(i, vtxPos); weightAttr.get(i, vtxWeight); for (var k = 0; k < 4; k++) { if (vtxWeight[k] > 0.01) { var jointIdx = vtxJoint[k]; maxJointIdx = Math.max(maxJointIdx, jointIdx); var min = jointsBoundingBoxes[jointIdx].min.array; var max = jointsBoundingBoxes[jointIdx].max.array; jointsBoundingBoxes[jointIdx].__updated = true; min = vec3.min(min, min, vtxPos); max = vec3.max(max, max, vtxPos); } } } this._jointsBoundingBoxes = jointsBoundingBoxes; this.boundingBox = new BoundingBox(); if (maxJointIdx < this.joints.length - 1) { console.warn('Geometry joints and skeleton joints don\'t match'); } }, setJointMatricesArray: function (arr) { this._invBindPoseMatricesArray = arr; this._skinMatricesArray = new Float32Array(arr.length); this.updateMatricesSubArrays(); }, updateMatricesSubArrays: function () { for (var i = 0; i < this.joints.length; i++) { this._jointMatricesSubArrays[i] = this._invBindPoseMatricesArray.subarray(i * 16, (i+1) * 16); this._skinMatricesSubArrays[i] = this._skinMatricesArray.subarray(i * 16, (i+1) * 16); } }, /** * Update skinning matrices */ update: function () { this._setPose(); var jointsBoundingBoxes = this._jointsBoundingBoxes; for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; mat4.multiply( this._skinMatricesSubArrays[i], joint.node.worldTransform.array, this._jointMatricesSubArrays[i] ); } if (this.boundingBox) { this.boundingBox.min.set(Infinity, Infinity, Infinity); this.boundingBox.max.set(-Infinity, -Infinity, -Infinity); for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; var bbox = jointsBoundingBoxes[i]; if (bbox.__updated) { tmpBoundingBox.copy(bbox); tmpMat4.array = this._skinMatricesSubArrays[i]; tmpBoundingBox.applyTransform(tmpMat4); this.boundingBox.union(tmpBoundingBox); } } } }, getSubSkinMatrices: function (meshId, joints) { var subArray = this._subSkinMatricesArray[meshId]; if (!subArray) { subArray = this._subSkinMatricesArray[meshId] = new Float32Array(joints.length * 16); } var cursor = 0; for (var i = 0; i < joints.length; i++) { var idx = joints[i]; for (var j = 0; j < 16; j++) { subArray[cursor++] = this._skinMatricesArray[idx * 16 + j]; } } return subArray; }, getSubSkinMatricesTexture: function (meshId, joints) { var skinMatrices = this.getSubSkinMatrices(meshId, joints); var size; var numJoints = this.joints.length; if (numJoints > 256) { size = 64; } else if (numJoints > 64) { size = 32; } else if (numJoints > 16) { size = 16; } else { size = 8; } var texture = this._skinMatricesTexture = this._skinMatricesTexture || new Texture2D({ type: Texture.FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, useMipmap: false, flipY: false }); texture.width = size; texture.height = size; if (!texture.pixels || texture.pixels.length !== size * size * 4) { texture.pixels = new Float32Array(size * size * 4); } texture.pixels.set(skinMatrices); texture.dirty(); return texture; }, getSkinMatricesTexture: function () { return this._skinMatricesTexture; }, _setPose: function () { if (this._clips[0]) { var clip = this._clips[0].clip; var maps = this._clips[0].maps; for (var i = 0; i < this.joints.length; i++) { var joint = this.joints[i]; if (maps[i] === -1) { continue; } var pose = clip.tracks[maps[i]]; // Not update if there is no data. // PENDING If sync pose.position, pose.rotation, pose.scale if (pose.channels.position) { vec3.copy(joint.node.position.array, pose.position); } if (pose.channels.rotation) { quat.copy(joint.node.rotation.array, pose.rotation); } if (pose.channels.scale) { vec3.copy(joint.node.scale.array, pose.scale); } joint.node.position._dirty = true; joint.node.rotation._dirty = true; joint.node.scale._dirty = true; } } }, clone: function (clonedNodesMap) { var skeleton = new Skeleton(); skeleton.name = this.name; for (var i = 0; i < this.joints.length; i++) { var newJoint = new Joint(); var joint = this.joints[i]; newJoint.name = joint.name; newJoint.index = joint.index; if (clonedNodesMap) { var newNode = clonedNodesMap[joint.node.__uid__]; if (!newNode) { // PENDING console.warn('Can\'t find node'); } newJoint.node = newNode || joint.node; } else { newJoint.node = joint.node; } skeleton.joints.push(newJoint); } if (this._invBindPoseMatricesArray) { var len = this._invBindPoseMatricesArray.length; skeleton._invBindPoseMatricesArray = new Float32Array(len); for (var i = 0; i < len; i++) { skeleton._invBindPoseMatricesArray[i] = this._invBindPoseMatricesArray[i]; } skeleton._skinMatricesArray = new Float32Array(len); skeleton.updateMatricesSubArrays(); } skeleton._jointsBoundingBoxe = (this._jointsBoundingBoxes || []).map(function (bbox) { return bbox.clone(); }); skeleton.update(); return skeleton; } }); var utilGlsl = "\n@export clay.util.rand\nhighp float rand(vec2 uv) {\n const highp float a = 12.9898, b = 78.233, c = 43758.5453;\n highp float dt = dot(uv.xy, vec2(a,b)), sn = mod(dt, 3.141592653589793);\n return fract(sin(sn) * c);\n}\n@end\n@export clay.util.calculate_attenuation\nuniform float attenuationFactor : 5.0;\nfloat lightAttenuation(float dist, float range)\n{\n float attenuation = 1.0;\n attenuation = dist*dist/(range*range+1.0);\n float att_s = attenuationFactor;\n attenuation = 1.0/(attenuation*att_s+1.0);\n att_s = 1.0/(att_s+1.0);\n attenuation = attenuation - att_s;\n attenuation /= 1.0 - att_s;\n return clamp(attenuation, 0.0, 1.0);\n}\n@end\n@export clay.util.edge_factor\n#ifdef SUPPORT_STANDARD_DERIVATIVES\nfloat edgeFactor(float width)\n{\n vec3 d = fwidth(v_Barycentric);\n vec3 a3 = smoothstep(vec3(0.0), d * width, v_Barycentric);\n return min(min(a3.x, a3.y), a3.z);\n}\n#else\nfloat edgeFactor(float width)\n{\n return 1.0;\n}\n#endif\n@end\n@export clay.util.encode_float\nvec4 encodeFloat(const in float depth)\n{\n const vec4 bitShifts = vec4(256.0*256.0*256.0, 256.0*256.0, 256.0, 1.0);\n const vec4 bit_mask = vec4(0.0, 1.0/256.0, 1.0/256.0, 1.0/256.0);\n vec4 res = fract(depth * bitShifts);\n res -= res.xxyz * bit_mask;\n return res;\n}\n@end\n@export clay.util.decode_float\nfloat decodeFloat(const in vec4 color)\n{\n const vec4 bitShifts = vec4(1.0/(256.0*256.0*256.0), 1.0/(256.0*256.0), 1.0/256.0, 1.0);\n return dot(color, bitShifts);\n}\n@end\n@export clay.util.float\n@import clay.util.encode_float\n@import clay.util.decode_float\n@end\n@export clay.util.rgbm_decode\nvec3 RGBMDecode(vec4 rgbm, float range) {\n return range * rgbm.rgb * rgbm.a;\n}\n@end\n@export clay.util.rgbm_encode\nvec4 RGBMEncode(vec3 color, float range) {\n if (dot(color, color) == 0.0) {\n return vec4(0.0);\n }\n vec4 rgbm;\n color /= range;\n rgbm.a = clamp(max(max(color.r, color.g), max(color.b, 1e-6)), 0.0, 1.0);\n rgbm.a = ceil(rgbm.a * 255.0) / 255.0;\n rgbm.rgb = color / rgbm.a;\n return rgbm;\n}\n@end\n@export clay.util.rgbm\n@import clay.util.rgbm_decode\n@import clay.util.rgbm_encode\nvec4 decodeHDR(vec4 color)\n{\n#if defined(RGBM_DECODE) || defined(RGBM)\n return vec4(RGBMDecode(color, 8.12), 1.0);\n#else\n return color;\n#endif\n}\nvec4 encodeHDR(vec4 color)\n{\n#if defined(RGBM_ENCODE) || defined(RGBM)\n return RGBMEncode(color.xyz, 8.12);\n#else\n return color;\n#endif\n}\n@end\n@export clay.util.srgb\nvec4 sRGBToLinear(in vec4 value) {\n return vec4(mix(pow(value.rgb * 0.9478672986 + vec3(0.0521327014), vec3(2.4)), value.rgb * 0.0773993808, vec3(lessThanEqual(value.rgb, vec3(0.04045)))), value.w);\n}\nvec4 linearTosRGB(in vec4 value) {\n return vec4(mix(pow(value.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), value.rgb * 12.92, vec3(lessThanEqual(value.rgb, vec3(0.0031308)))), value.w);\n}\n@end\n@export clay.chunk.skinning_header\n#ifdef SKINNING\nattribute vec3 weight : WEIGHT;\nattribute vec4 joint : JOINT;\n#ifdef USE_SKIN_MATRICES_TEXTURE\nuniform sampler2D skinMatricesTexture : ignore;\nuniform float skinMatricesTextureSize: ignore;\nmat4 getSkinMatrix(sampler2D tex, float idx) {\n float j = idx * 4.0;\n float x = mod(j, skinMatricesTextureSize);\n float y = floor(j / skinMatricesTextureSize) + 0.5;\n vec2 scale = vec2(skinMatricesTextureSize);\n return mat4(\n texture2D(tex, vec2(x + 0.5, y) / scale),\n texture2D(tex, vec2(x + 1.5, y) / scale),\n texture2D(tex, vec2(x + 2.5, y) / scale),\n texture2D(tex, vec2(x + 3.5, y) / scale)\n );\n}\nmat4 getSkinMatrix(float idx) {\n return getSkinMatrix(skinMatricesTexture, idx);\n}\n#else\nuniform mat4 skinMatrix[JOINT_COUNT] : SKIN_MATRIX;\nmat4 getSkinMatrix(float idx) {\n return skinMatrix[int(idx)];\n}\n#endif\n#endif\n@end\n@export clay.chunk.skin_matrix\nmat4 skinMatrixWS = getSkinMatrix(joint.x) * weight.x;\nif (weight.y > 1e-4)\n{\n skinMatrixWS += getSkinMatrix(joint.y) * weight.y;\n}\nif (weight.z > 1e-4)\n{\n skinMatrixWS += getSkinMatrix(joint.z) * weight.z;\n}\nfloat weightW = 1.0-weight.x-weight.y-weight.z;\nif (weightW > 1e-4)\n{\n skinMatrixWS += getSkinMatrix(joint.w) * weightW;\n}\n@end\n@export clay.chunk.instancing_header\n#ifdef INSTANCING\nattribute vec4 instanceMat1;\nattribute vec4 instanceMat2;\nattribute vec4 instanceMat3;\n#endif\n@end\n@export clay.chunk.instancing_matrix\nmat4 instanceMat = mat4(\n vec4(instanceMat1.xyz, 0.0),\n vec4(instanceMat2.xyz, 0.0),\n vec4(instanceMat3.xyz, 0.0),\n vec4(instanceMat1.w, instanceMat2.w, instanceMat3.w, 1.0)\n);\n@end\n@export clay.util.parallax_correct\nvec3 parallaxCorrect(in vec3 dir, in vec3 pos, in vec3 boxMin, in vec3 boxMax) {\n vec3 first = (boxMax - pos) / dir;\n vec3 second = (boxMin - pos) / dir;\n vec3 further = max(first, second);\n float dist = min(further.x, min(further.y, further.z));\n vec3 fixedPos = pos + dir * dist;\n vec3 boxCenter = (boxMax + boxMin) * 0.5;\n return normalize(fixedPos - boxCenter);\n}\n@end\n@export clay.util.clamp_sample\nvec4 clampSample(const in sampler2D texture, const in vec2 coord)\n{\n#ifdef STEREO\n float eye = step(0.5, coord.x) * 0.5;\n vec2 coordClamped = clamp(coord, vec2(eye, 0.0), vec2(0.5 + eye, 1.0));\n#else\n vec2 coordClamped = clamp(coord, vec2(0.0), vec2(1.0));\n#endif\n return texture2D(texture, coordClamped);\n}\n@end\n@export clay.util.ACES\nvec3 ACESToneMapping(vec3 color)\n{\n const float A = 2.51;\n const float B = 0.03;\n const float C = 2.43;\n const float D = 0.59;\n const float E = 0.14;\n return (color * (A * color + B)) / (color * (C * color + D) + E);\n}\n@end"; var basicEssl = "@export clay.basic.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nuniform vec2 uvRepeat : [1.0, 1.0];\nuniform vec2 uvOffset : [0.0, 0.0];\nattribute vec2 texcoord : TEXCOORD_0;\nattribute vec3 position : POSITION;\nattribute vec3 barycentric;\n@import clay.chunk.skinning_header\n@import clay.chunk.instancing_header\nvarying vec2 v_Texcoord;\nvarying vec3 v_Barycentric;\n#ifdef VERTEX_COLOR\nattribute vec4 a_Color : COLOR;\nvarying vec4 v_Color;\n#endif\nvoid main()\n{\n vec4 skinnedPosition = vec4(position, 1.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = skinMatrixWS * skinnedPosition;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n skinnedPosition = instanceMat * skinnedPosition;\n#endif\n v_Texcoord = texcoord * uvRepeat + uvOffset;\n v_Barycentric = barycentric;\n gl_Position = worldViewProjection * skinnedPosition;\n#ifdef VERTEX_COLOR\n v_Color = a_Color;\n#endif\n}\n@end\n@export clay.basic.fragment\n#define DIFFUSEMAP_ALPHA_ALPHA\nvarying vec2 v_Texcoord;\nuniform sampler2D diffuseMap;\nuniform vec3 color : [1.0, 1.0, 1.0];\nuniform vec3 emission : [0.0, 0.0, 0.0];\nuniform float alpha : 1.0;\n#ifdef ALPHA_TEST\nuniform float alphaCutoff: 0.9;\n#endif\n#ifdef VERTEX_COLOR\nvarying vec4 v_Color;\n#endif\nuniform float lineWidth : 0.0;\nuniform vec4 lineColor : [0.0, 0.0, 0.0, 0.6];\nvarying vec3 v_Barycentric;\n@import clay.util.edge_factor\n@import clay.util.rgbm\n@import clay.util.srgb\n@import clay.util.ACES\nvoid main()\n{\n gl_FragColor = vec4(color, alpha);\n#ifdef VERTEX_COLOR\n gl_FragColor *= v_Color;\n#endif\n#ifdef SRGB_DECODE\n gl_FragColor = sRGBToLinear(gl_FragColor);\n#endif\n#ifdef DIFFUSEMAP_ENABLED\n vec4 texel = decodeHDR(texture2D(diffuseMap, v_Texcoord));\n#ifdef SRGB_DECODE\n texel = sRGBToLinear(texel);\n#endif\n#if defined(DIFFUSEMAP_ALPHA_ALPHA)\n gl_FragColor.a = texel.a;\n#endif\n gl_FragColor.rgb *= texel.rgb;\n#endif\n gl_FragColor.rgb += emission;\n if( lineWidth > 0.)\n {\n gl_FragColor.rgb = mix(gl_FragColor.rgb, lineColor.rgb, (1.0 - edgeFactor(lineWidth)) * lineColor.a);\n }\n#ifdef ALPHA_TEST\n if (gl_FragColor.a < alphaCutoff) {\n discard;\n }\n#endif\n#ifdef TONEMAPPING\n gl_FragColor.rgb = ACESToneMapping(gl_FragColor.rgb);\n#endif\n#ifdef SRGB_ENCODE\n gl_FragColor = linearTosRGB(gl_FragColor);\n#endif\n gl_FragColor = encodeHDR(gl_FragColor);\n}\n@end"; var lambertEssl = "\n@export clay.lambert.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nuniform mat4 worldInverseTranspose : WORLDINVERSETRANSPOSE;\nuniform mat4 world : WORLD;\nuniform vec2 uvRepeat : [1.0, 1.0];\nuniform vec2 uvOffset : [0.0, 0.0];\nattribute vec3 position : POSITION;\nattribute vec2 texcoord : TEXCOORD_0;\nattribute vec3 normal : NORMAL;\nattribute vec3 barycentric;\n#ifdef VERTEX_COLOR\nattribute vec4 a_Color : COLOR;\nvarying vec4 v_Color;\n#endif\n@import clay.chunk.skinning_header\n@import clay.chunk.instancing_header\nvarying vec2 v_Texcoord;\nvarying vec3 v_Normal;\nvarying vec3 v_WorldPosition;\nvarying vec3 v_Barycentric;\nvoid main()\n{\n vec4 skinnedPosition = vec4(position, 1.0);\n vec4 skinnedNormal = vec4(normal, 0.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = skinMatrixWS * skinnedPosition;\n skinnedNormal = skinMatrixWS * skinnedNormal;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n skinnedPosition = instanceMat * skinnedPosition;\n skinnedNormal = instanceMat * skinnedNormal;\n#endif\n gl_Position = worldViewProjection * skinnedPosition;\n v_Texcoord = texcoord * uvRepeat + uvOffset;\n v_Normal = normalize((worldInverseTranspose * skinnedNormal).xyz);\n v_WorldPosition = ( world * skinnedPosition ).xyz;\n v_Barycentric = barycentric;\n#ifdef VERTEX_COLOR\n v_Color = a_Color;\n#endif\n}\n@end\n@export clay.lambert.fragment\n#define DIFFUSEMAP_ALPHA_ALPHA\nvarying vec2 v_Texcoord;\nvarying vec3 v_Normal;\nvarying vec3 v_WorldPosition;\nuniform sampler2D diffuseMap;\nuniform sampler2D alphaMap;\nuniform vec3 color : [1.0, 1.0, 1.0];\nuniform vec3 emission : [0.0, 0.0, 0.0];\nuniform float alpha : 1.0;\n#ifdef ALPHA_TEST\nuniform float alphaCutoff: 0.9;\n#endif\nuniform float lineWidth : 0.0;\nuniform vec4 lineColor : [0.0, 0.0, 0.0, 0.6];\nvarying vec3 v_Barycentric;\n#ifdef VERTEX_COLOR\nvarying vec4 v_Color;\n#endif\n#ifdef AMBIENT_LIGHT_COUNT\n@import clay.header.ambient_light\n#endif\n#ifdef AMBIENT_SH_LIGHT_COUNT\n@import clay.header.ambient_sh_light\n#endif\n#ifdef POINT_LIGHT_COUNT\n@import clay.header.point_light\n#endif\n#ifdef DIRECTIONAL_LIGHT_COUNT\n@import clay.header.directional_light\n#endif\n#ifdef SPOT_LIGHT_COUNT\n@import clay.header.spot_light\n#endif\n@import clay.util.calculate_attenuation\n@import clay.util.edge_factor\n@import clay.util.rgbm\n@import clay.plugin.compute_shadow_map\n@import clay.util.ACES\nvoid main()\n{\n gl_FragColor = vec4(color, alpha);\n#ifdef VERTEX_COLOR\n gl_FragColor *= v_Color;\n#endif\n#ifdef SRGB_DECODE\n gl_FragColor = sRGBToLinear(gl_FragColor);\n#endif\n#ifdef DIFFUSEMAP_ENABLED\n vec4 tex = texture2D( diffuseMap, v_Texcoord );\n#ifdef SRGB_DECODE\n tex.rgb = pow(tex.rgb, vec3(2.2));\n#endif\n gl_FragColor.rgb *= tex.rgb;\n#ifdef DIFFUSEMAP_ALPHA_ALPHA\n gl_FragColor.a *= tex.a;\n#endif\n#endif\n vec3 diffuseColor = vec3(0.0, 0.0, 0.0);\n#ifdef AMBIENT_LIGHT_COUNT\n for(int _idx_ = 0; _idx_ < AMBIENT_LIGHT_COUNT; _idx_++)\n {\n diffuseColor += ambientLightColor[_idx_];\n }\n#endif\n#ifdef AMBIENT_SH_LIGHT_COUNT\n for(int _idx_ = 0; _idx_ < AMBIENT_SH_LIGHT_COUNT; _idx_++)\n {{\n diffuseColor += calcAmbientSHLight(_idx_, v_Normal) * ambientSHLightColor[_idx_];\n }}\n#endif\n#ifdef POINT_LIGHT_COUNT\n#if defined(POINT_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsPoint[POINT_LIGHT_COUNT];\n if( shadowEnabled )\n {\n computeShadowOfPointLights(v_WorldPosition, shadowContribsPoint);\n }\n#endif\n for(int i = 0; i < POINT_LIGHT_COUNT; i++)\n {\n vec3 lightPosition = pointLightPosition[i];\n vec3 lightColor = pointLightColor[i];\n float range = pointLightRange[i];\n vec3 lightDirection = lightPosition - v_WorldPosition;\n float dist = length(lightDirection);\n float attenuation = lightAttenuation(dist, range);\n lightDirection /= dist;\n float ndl = dot( v_Normal, lightDirection );\n float shadowContrib = 1.0;\n#if defined(POINT_LIGHT_SHADOWMAP_COUNT)\n if( shadowEnabled )\n {\n shadowContrib = shadowContribsPoint[i];\n }\n#endif\n diffuseColor += lightColor * clamp(ndl, 0.0, 1.0) * attenuation * shadowContrib;\n }\n#endif\n#ifdef DIRECTIONAL_LIGHT_COUNT\n#if defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsDir[DIRECTIONAL_LIGHT_COUNT];\n if(shadowEnabled)\n {\n computeShadowOfDirectionalLights(v_WorldPosition, shadowContribsDir);\n }\n#endif\n for(int i = 0; i < DIRECTIONAL_LIGHT_COUNT; i++)\n {\n vec3 lightDirection = -directionalLightDirection[i];\n vec3 lightColor = directionalLightColor[i];\n float ndl = dot(v_Normal, normalize(lightDirection));\n float shadowContrib = 1.0;\n#if defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT)\n if( shadowEnabled )\n {\n shadowContrib = shadowContribsDir[i];\n }\n#endif\n diffuseColor += lightColor * clamp(ndl, 0.0, 1.0) * shadowContrib;\n }\n#endif\n#ifdef SPOT_LIGHT_COUNT\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT)\n float shadowContribsSpot[SPOT_LIGHT_COUNT];\n if(shadowEnabled)\n {\n computeShadowOfSpotLights(v_WorldPosition, shadowContribsSpot);\n }\n#endif\n for(int i = 0; i < SPOT_LIGHT_COUNT; i++)\n {\n vec3 lightPosition = -spotLightPosition[i];\n vec3 spotLightDirection = -normalize( spotLightDirection[i] );\n vec3 lightColor = spotLightColor[i];\n float range = spotLightRange[i];\n float a = spotLightUmbraAngleCosine[i];\n float b = spotLightPenumbraAngleCosine[i];\n float falloffFactor = spotLightFalloffFactor[i];\n vec3 lightDirection = lightPosition - v_WorldPosition;\n float dist = length(lightDirection);\n float attenuation = lightAttenuation(dist, range);\n lightDirection /= dist;\n float c = dot(spotLightDirection, lightDirection);\n float falloff;\n falloff = clamp((c - a) /( b - a), 0.0, 1.0);\n falloff = pow(falloff, falloffFactor);\n float ndl = dot(v_Normal, lightDirection);\n ndl = clamp(ndl, 0.0, 1.0);\n float shadowContrib = 1.0;\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT)\n if( shadowEnabled )\n {\n shadowContrib = shadowContribsSpot[i];\n }\n#endif\n diffuseColor += lightColor * ndl * attenuation * (1.0-falloff) * shadowContrib;\n }\n#endif\n gl_FragColor.rgb *= diffuseColor;\n gl_FragColor.rgb += emission;\n if(lineWidth > 0.)\n {\n gl_FragColor.rgb = mix(gl_FragColor.rgb, lineColor.rgb, (1.0 - edgeFactor(lineWidth)) * lineColor.a);\n }\n#ifdef ALPHA_TEST\n if (gl_FragColor.a < alphaCutoff) {\n discard;\n }\n#endif\n#ifdef TONEMAPPING\n gl_FragColor.rgb = ACESToneMapping(gl_FragColor.rgb);\n#endif\n#ifdef SRGB_ENCODE\n gl_FragColor = linearTosRGB(gl_FragColor);\n#endif\n gl_FragColor = encodeHDR(gl_FragColor);\n}\n@end"; var wireframeEssl = "@export clay.wireframe.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nuniform mat4 world : WORLD;\nattribute vec3 position : POSITION;\nattribute vec3 barycentric;\n@import clay.chunk.skinning_header\nvarying vec3 v_Barycentric;\nvoid main()\n{\n vec3 skinnedPosition = position;\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = (skinMatrixWS * vec4(position, 1.0)).xyz;\n#endif\n gl_Position = worldViewProjection * vec4(skinnedPosition, 1.0 );\n v_Barycentric = barycentric;\n}\n@end\n@export clay.wireframe.fragment\nuniform vec3 color : [0.0, 0.0, 0.0];\nuniform float alpha : 1.0;\nuniform float lineWidth : 1.0;\nvarying vec3 v_Barycentric;\n@import clay.util.edge_factor\nvoid main()\n{\n gl_FragColor.rgb = color;\n gl_FragColor.a = (1.0-edgeFactor(lineWidth)) * alpha;\n}\n@end"; var skyboxEssl = "@export clay.skybox.vertex\n#define SHADER_NAME skybox\nuniform mat4 world : WORLD;\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\nvarying vec3 v_WorldPosition;\nvoid main()\n{\n v_WorldPosition = (world * vec4(position, 1.0)).xyz;\n gl_Position = worldViewProjection * vec4(position, 1.0);\n}\n@end\n@export clay.skybox.fragment\n#define PI 3.1415926\nuniform mat4 viewInverse : VIEWINVERSE;\n#ifdef EQUIRECTANGULAR\nuniform sampler2D environmentMap;\n#else\nuniform samplerCube environmentMap;\n#endif\nuniform float lod: 0.0;\nvarying vec3 v_WorldPosition;\n@import clay.util.rgbm\n@import clay.util.srgb\n@import clay.util.ACES\nvoid main()\n{\n vec3 eyePos = viewInverse[3].xyz;\n vec3 V = normalize(v_WorldPosition - eyePos);\n#ifdef EQUIRECTANGULAR\n float phi = acos(V.y);\n float theta = atan(-V.x, V.z) + PI * 0.5;\n vec2 uv = vec2(theta / 2.0 / PI, phi / PI);\n vec4 texel = decodeHDR(texture2D(environmentMap, fract(uv)));\n#else\n #if defined(LOD) || defined(SUPPORT_TEXTURE_LOD)\n vec4 texel = decodeHDR(textureCubeLodEXT(environmentMap, V, lod));\n #else\n vec4 texel = decodeHDR(textureCube(environmentMap, V));\n #endif\n#endif\n#ifdef SRGB_DECODE\n texel = sRGBToLinear(texel);\n#endif\n#ifdef TONEMAPPING\n texel.rgb = ACESToneMapping(texel.rgb);\n#endif\n#ifdef SRGB_ENCODE\n texel = linearTosRGB(texel);\n#endif\n gl_FragColor = encodeHDR(vec4(texel.rgb, 1.0));\n}\n@end"; Shader['import'](lightEssl); Shader['import'](utilGlsl); // Some build in shaders Shader['import'](basicEssl); Shader['import'](lambertEssl); Shader['import'](standardEssl); Shader['import'](wireframeEssl); Shader['import'](skyboxEssl); Shader['import'](prezGlsl); library.template('clay.basic', Shader.source('clay.basic.vertex'), Shader.source('clay.basic.fragment')); library.template('clay.lambert', Shader.source('clay.lambert.vertex'), Shader.source('clay.lambert.fragment')); library.template('clay.wireframe', Shader.source('clay.wireframe.vertex'), Shader.source('clay.wireframe.fragment')); library.template('clay.skybox', Shader.source('clay.skybox.vertex'), Shader.source('clay.skybox.fragment')); library.template('clay.prez', Shader.source('clay.prez.vertex'), Shader.source('clay.prez.fragment')); library.template('clay.standard', Shader.source('clay.standard.vertex'), Shader.source('clay.standard.fragment')); library.template('clay.standardMR', Shader.source('clay.standardMR.vertex'), Shader.source('clay.standardMR.fragment')); /** * glTF Loader * Specification https://github.com/KhronosGroup/glTF/blob/master/specification/README.md * * TODO Morph targets */ // Import builtin shader var semanticAttributeMap = { 'NORMAL': 'normal', 'POSITION': 'position', 'TEXCOORD_0': 'texcoord0', 'TEXCOORD_1': 'texcoord1', 'WEIGHTS_0': 'weight', 'JOINTS_0': 'joint', 'COLOR_0': 'color' }; var ARRAY_CTOR_MAP = { 5120: vendor.Int8Array, 5121: vendor.Uint8Array, 5122: vendor.Int16Array, 5123: vendor.Uint16Array, 5125: vendor.Uint32Array, 5126: vendor.Float32Array }; var SIZE_MAP = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 }; function getAccessorData(json, lib, accessorIdx, isIndices) { var accessorInfo = json.accessors[accessorIdx]; var buffer = lib.bufferViews[accessorInfo.bufferView]; var byteOffset = accessorInfo.byteOffset || 0; var ArrayCtor = ARRAY_CTOR_MAP[accessorInfo.componentType] || vendor.Float32Array; var size = SIZE_MAP[accessorInfo.type]; if (size == null && isIndices) { size = 1; } var arr = new ArrayCtor(buffer, byteOffset, size * accessorInfo.count); var quantizeExtension = accessorInfo.extensions && accessorInfo.extensions['WEB3D_quantized_attributes']; if (quantizeExtension) { var decodedArr = new vendor.Float32Array(size * accessorInfo.count); var decodeMatrix = quantizeExtension.decodeMatrix; var decodeOffset; var decodeScale; var decodeOffset = new Array(size); var decodeScale = new Array(size); for (var k = 0; k < size; k++) { decodeOffset[k] = decodeMatrix[size * (size + 1) + k]; decodeScale[k] = decodeMatrix[k * (size + 1) + k]; } for (var i = 0; i < accessorInfo.count; i++) { for (var k = 0; k < size; k++) { decodedArr[i * size + k] = arr[i * size + k] * decodeScale[k] + decodeOffset[k]; } } arr = decodedArr; } return arr; } function base64ToBinary(input, charStart) { var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var lookup = new Uint8Array(130); for (var i = 0; i < chars.length; i++) { lookup[chars.charCodeAt(i)] = i; } // Ignore var len = input.length - charStart; if (input.charAt(len - 1) === '=') { len--; } if (input.charAt(len - 1) === '=') { len--; } var uarray = new Uint8Array((len / 4) * 3); for (var i = 0, j = charStart; i < uarray.length;) { var c1 = lookup[input.charCodeAt(j++)]; var c2 = lookup[input.charCodeAt(j++)]; var c3 = lookup[input.charCodeAt(j++)]; var c4 = lookup[input.charCodeAt(j++)]; uarray[i++] = (c1 << 2) | (c2 >> 4); uarray[i++] = ((c2 & 15) << 4) | (c3 >> 2); uarray[i++] = ((c3 & 3) << 6) | c4; } return uarray.buffer; } /** * @typedef {Object} clay.loader.GLTF.Result * @property {Object} json * @property {clay.Scene} scene * @property {clay.Node} rootNode * @property {clay.Camera[]} cameras * @property {clay.Texture[]} textures * @property {clay.Material[]} materials * @property {clay.Skeleton[]} skeletons * @property {clay.Mesh[]} meshes * @property {clay.animation.TrackClip[]} clips * @property {clay.Node[]} nodes */ /** * @constructor clay.loader.GLTF * @extends clay.core.Base */ var GLTFLoader = Base.extend(/** @lends clay.loader.GLTF# */ { /** * * @type {clay.Node} */ rootNode: null, /** * Root path for uri parsing. * @type {string} */ rootPath: null, /** * Root path for texture uri parsing. Defaultly use the rootPath * @type {string} */ textureRootPath: null, /** * Root path for buffer uri parsing. Defaultly use the rootPath * @type {string} */ bufferRootPath: null, /** * Shader used when creating the materials. * @type {string|clay.Shader} * @default 'clay.standard' */ shader: 'clay.standard', /** * If use {@link clay.StandardMaterial} * @type {string} */ useStandardMaterial: false, /** * If loading the cameras. * @type {boolean} */ includeCamera: true, /** * If loading the animations. * @type {boolean} */ includeAnimation: true, /** * If loading the meshes * @type {boolean} */ includeMesh: true, /** * If loading the textures. * @type {boolean} */ includeTexture: true, /** * @type {string} */ crossOrigin: '', /** * @type {boolean} * @see https://github.com/KhronosGroup/glTF/issues/674 */ textureFlipY: false, /** * If convert texture to power-of-two * @type {boolean} */ textureConvertToPOT: false, shaderLibrary: null }, function () { if (!this.shaderLibrary) { this.shaderLibrary = library.createLibrary(); } }, /** @lends clay.loader.GLTF.prototype */ { /** * @param {string} url */ load: function (url) { var self = this; var isBinary = url.endsWith('.glb'); if (this.rootPath == null) { this.rootPath = url.slice(0, url.lastIndexOf('/')); } vendor.request.get({ url: url, onprogress: function (percent, loaded, total) { self.trigger('progress', percent, loaded, total); }, onerror: function (e) { self.trigger('error', e); }, responseType: isBinary ? 'arraybuffer' : 'text', onload: function (data) { if (isBinary) { self.parseBinary(data); } else { if (typeof data === 'string') { data = JSON.parse(data); } self.parse(data); } } }); }, /** * Parse glTF binary * @param {ArrayBuffer} buffer * @return {clay.loader.GLTF.Result} */ parseBinary: function (buffer) { var header = new Uint32Array(buffer, 0, 4); if (header[0] !== 0x46546C67) { this.trigger('error', 'Invalid glTF binary format: Invalid header'); return; } if (header[0] < 2) { this.trigger('error', 'Only glTF2.0 is supported.'); return; } var dataView = new DataView(buffer, 12); var json; var buffers = []; // Read chunks for (var i = 0; i < dataView.byteLength;) { var chunkLength = dataView.getUint32(i, true); i += 4; var chunkType = dataView.getUint32(i, true); i += 4; // json if (chunkType === 0x4E4F534A) { var arr = new Uint8Array(buffer, i + 12, chunkLength); // TODO, for the browser not support TextDecoder. var decoder = new TextDecoder(); var str = decoder.decode(arr); try { json = JSON.parse(str); } catch (e) { this.trigger('error', 'JSON Parse error:' + e.toString()); return; } } else if (chunkType === 0x004E4942) { buffers.push(buffer.slice(i + 12, i + 12 + chunkLength)); } i += chunkLength; } if (!json) { this.trigger('error', 'Invalid glTF binary format: Can\'t find JSON.'); return; } return this.parse(json, buffers); }, /** * @param {Object} json * @param {ArrayBuffer[]} [buffer] * @return {clay.loader.GLTF.Result} */ parse: function (json, buffers) { var self = this; var lib = { json: json, buffers: [], bufferViews: [], materials: [], textures: [], meshes: [], joints: [], skeletons: [], cameras: [], nodes: [], clips: [] }; // Mount on the root node if given var rootNode = this.rootNode || new Scene(); var loading = 0; function checkLoad() { loading--; if (loading === 0) { afterLoadBuffer(); } } // If already load buffers if (buffers) { lib.buffers = buffers.slice(); afterLoadBuffer(true); } else { // Load buffers util$1.each(json.buffers, function (bufferInfo, idx) { loading++; var path = bufferInfo.uri; self._loadBuffers(path, function (buffer) { lib.buffers[idx] = buffer; checkLoad(); }, checkLoad); }); } function getResult() { return { json: json, scene: self.rootNode ? null : rootNode, rootNode: self.rootNode ? rootNode : null, cameras: lib.cameras, textures: lib.textures, materials: lib.materials, skeletons: lib.skeletons, meshes: lib.instancedMeshes, clips: lib.clips, nodes: lib.nodes }; } function afterLoadBuffer(immediately) { // Buffer not load complete. if (lib.buffers.length !== json.buffers.length) { setTimeout(function () { self.trigger('error', 'Buffer not load complete.'); }); return; } json.bufferViews.forEach(function (bufferViewInfo, idx) { // PENDING Performance lib.bufferViews[idx] = lib.buffers[bufferViewInfo.buffer] .slice(bufferViewInfo.byteOffset || 0, (bufferViewInfo.byteOffset || 0) + (bufferViewInfo.byteLength || 0)); }); lib.buffers = null; if (self.includeMesh) { if (self.includeTexture) { self._parseTextures(json, lib); } self._parseMaterials(json, lib); self._parseMeshes(json, lib); } self._parseNodes(json, lib); // Only support one scene. if (json.scenes) { var sceneInfo = json.scenes[json.scene || 0]; // Default use the first scene. if (sceneInfo) { for (var i = 0; i < sceneInfo.nodes.length; i++) { var node = lib.nodes[sceneInfo.nodes[i]]; node.update(); rootNode.add(node); } } } if (self.includeMesh) { self._parseSkins(json, lib); } if (self.includeAnimation) { self._parseAnimations(json, lib); } if (immediately) { setTimeout(function () { self.trigger('success', getResult()); }); } else { self.trigger('success', getResult()); } } return getResult(); }, /** * Binary file path resolver. User can override it * @param {string} path */ resolveBufferPath: function (path) { if (path && path.match(/^data:(.*?)base64,/)) { return path; } var rootPath = this.bufferRootPath; if (rootPath == null) { rootPath = this.rootPath; } return util$1.relative2absolute(path, rootPath); }, /** * Texture file path resolver. User can override it * @param {string} path */ resolveTexturePath: function (path) { if (path && path.match(/^data:(.*?)base64,/)) { return path; } var rootPath = this.textureRootPath; if (rootPath == null) { rootPath = this.rootPath; } return util$1.relative2absolute(path, rootPath); }, /** * Buffer loader * @param {string} * @param {Function} onsuccess * @param {Function} onerror */ loadBuffer: function (path, onsuccess, onerror) { vendor.request.get({ url: path, responseType: 'arraybuffer', onload: function (buffer) { onsuccess && onsuccess(buffer); }, onerror: function (buffer) { onerror && onerror(buffer); } }); }, _getShader: function () { if (typeof this.shader === 'string') { return this.shaderLibrary.get(this.shader); } else if (this.shader instanceof Shader) { return this.shader; } }, _loadBuffers: function (path, onsuccess, onerror) { var base64Prefix = 'data:application/octet-stream;base64,'; var strStart = path.substr(0, base64Prefix.length); if (strStart === base64Prefix) { onsuccess( base64ToBinary(path, base64Prefix.length) ); } else { this.loadBuffer( this.resolveBufferPath(path), onsuccess, onerror ); } }, // https://github.com/KhronosGroup/glTF/issues/100 // https://github.com/KhronosGroup/glTF/issues/193 _parseSkins: function (json, lib) { // Create skeletons and joints util$1.each(json.skins, function (skinInfo, idx) { var skeleton = new Skeleton({ name: skinInfo.name }); for (var i = 0; i < skinInfo.joints.length; i++) { var nodeIdx = skinInfo.joints[i]; var node = lib.nodes[nodeIdx]; var joint = new Joint({ name: node.name, node: node, index: skeleton.joints.length }); skeleton.joints.push(joint); } skeleton.relativeRootNode = lib.nodes[skinInfo.skeleton] || this.rootNode; if (skinInfo.inverseBindMatrices) { var IBMInfo = json.accessors[skinInfo.inverseBindMatrices]; var buffer = lib.bufferViews[IBMInfo.bufferView]; var offset = IBMInfo.byteOffset || 0; var size = IBMInfo.count * 16; var array = new vendor.Float32Array(buffer, offset, size); skeleton.setJointMatricesArray(array); } else { skeleton.updateJointMatrices(); } lib.skeletons[idx] = skeleton; }, this); function enableSkinningForMesh(mesh, skeleton, jointIndices) { mesh.skeleton = skeleton; mesh.joints = jointIndices; if (!skeleton.boundingBox) { skeleton.updateJointsBoundingBoxes(mesh.geometry); } } function getJointIndex(joint) { return joint.index; } util$1.each(json.nodes, function (nodeInfo, nodeIdx) { if (nodeInfo.skin != null) { var skinIdx = nodeInfo.skin; var skeleton = lib.skeletons[skinIdx]; var node = lib.nodes[nodeIdx]; var jointIndices = skeleton.joints.map(getJointIndex); if (node instanceof Mesh) { enableSkinningForMesh(node, skeleton, jointIndices); } else { // Mesh have multiple primitives var children = node.children(); for (var i = 0; i < children.length; i++) { enableSkinningForMesh(children[i], skeleton, jointIndices); } } } }, this); }, _parseTextures: function (json, lib) { util$1.each(json.textures, function (textureInfo, idx){ // samplers is optional var samplerInfo = (json.samplers && json.samplers[textureInfo.sampler]) || {}; var parameters = {}; ['wrapS', 'wrapT', 'magFilter', 'minFilter'].forEach(function (name) { var value = samplerInfo[name]; if (value != null) { parameters[name] = value; } }); util$1.defaults(parameters, { wrapS: Texture.REPEAT, wrapT: Texture.REPEAT, flipY: this.textureFlipY, convertToPOT: this.textureConvertToPOT }); var target = textureInfo.target || glenum.TEXTURE_2D; var format = textureInfo.format; if (format != null) { parameters.format = format; } if (target === glenum.TEXTURE_2D) { var texture = new Texture2D(parameters); var imageInfo = json.images[textureInfo.source]; var uri; if (imageInfo.uri) { uri = this.resolveTexturePath(imageInfo.uri); } else if (imageInfo.bufferView != null) { uri = URL.createObjectURL(new Blob([lib.bufferViews[imageInfo.bufferView]], { type: imageInfo.mimeType })); } if (uri) { texture.load(uri, this.crossOrigin); lib.textures[idx] = texture; } } }, this); }, _KHRCommonMaterialToStandard: function (materialInfo, lib) { var uniforms = {}; var commonMaterialInfo = materialInfo.extensions['KHR_materials_common']; uniforms = commonMaterialInfo.values || {}; if (typeof uniforms.diffuse === 'number') { uniforms.diffuse = lib.textures[uniforms.diffuse] || null; } if (typeof uniforms.emission === 'number') { uniforms.emission = lib.textures[uniforms.emission] || null; } var enabledTextures = []; if (uniforms['diffuse'] instanceof Texture2D) { enabledTextures.push('diffuseMap'); } if (materialInfo.normalTexture) { enabledTextures.push('normalMap'); } if (uniforms['emission'] instanceof Texture2D) { enabledTextures.push('emissiveMap'); } var material; var isStandardMaterial = this.useStandardMaterial; if (isStandardMaterial) { material = new StandardMaterial({ name: materialInfo.name, doubleSided: materialInfo.doubleSided }); } else { material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'USE_ROUGHNESS'); material.define('fragment', 'USE_METALNESS'); if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } } if (uniforms.transparent) { material.depthMask = false; material.depthTest = true; material.transparent = true; } var diffuseProp = uniforms['diffuse']; if (diffuseProp) { // Color if (Array.isArray(diffuseProp)) { diffuseProp = diffuseProp.slice(0, 3); isStandardMaterial ? (material.color = diffuseProp) : material.set('color', diffuseProp); } else { // Texture isStandardMaterial ? (material.diffuseMap = diffuseProp) : material.set('diffuseMap', diffuseProp); } } var emissionProp = uniforms['emission']; if (emissionProp != null) { // Color if (Array.isArray(emissionProp)) { emissionProp = emissionProp.slice(0, 3); isStandardMaterial ? (material.emission = emissionProp) : material.set('emission', emissionProp); } else { // Texture isStandardMaterial ? (material.emissiveMap = emissionProp) : material.set('emissiveMap', emissionProp); } } if (materialInfo.normalTexture != null) { // TODO texCoord var normalTextureIndex = materialInfo.normalTexture.index; if (isStandardMaterial) { material.normalMap = lib.textures[normalTextureIndex] || null; } else { material.set('normalMap', lib.textures[normalTextureIndex] || null); } } if (uniforms['shininess'] != null) { var glossiness = Math.log(uniforms['shininess']) / Math.log(8192); // Uniform glossiness material.set('glossiness', glossiness); material.set('roughness', 1 - glossiness); } else { material.set('glossiness', 0.3); material.set('roughness', 0.3); } if (uniforms['specular'] != null) { material.set('specularColor', uniforms['specular'].slice(0, 3)); } if (uniforms['transparency'] != null) { material.set('alpha', uniforms['transparency']); } return material; }, _pbrMetallicRoughnessToStandard: function (materialInfo, metallicRoughnessMatInfo, lib) { var alphaTest = materialInfo.alphaMode === 'MASK'; var isStandardMaterial = this.useStandardMaterial; var material; var diffuseMap, roughnessMap, metalnessMap, normalMap, emissiveMap, occlusionMap; var enabledTextures = []; /** * The scalar multiplier applied to each normal vector of the normal texture. * * @type {number} * * XXX This value is ignored if `materialInfo.normalTexture` is not specified. */ var normalScale = 1.0; // TODO texCoord if (metallicRoughnessMatInfo.baseColorTexture) { diffuseMap = lib.textures[metallicRoughnessMatInfo.baseColorTexture.index] || null; diffuseMap && enabledTextures.push('diffuseMap'); } if (metallicRoughnessMatInfo.metallicRoughnessTexture) { roughnessMap = metalnessMap = lib.textures[metallicRoughnessMatInfo.metallicRoughnessTexture.index] || null; roughnessMap && enabledTextures.push('metalnessMap', 'roughnessMap'); } if (materialInfo.normalTexture) { normalMap = lib.textures[materialInfo.normalTexture.index] || null; normalMap && enabledTextures.push('normalMap'); if (typeof materialInfo.normalTexture.scale === 'number') { normalScale = materialInfo.normalTexture.scale; } } if (materialInfo.emissiveTexture) { emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null; emissiveMap && enabledTextures.push('emissiveMap'); } if (materialInfo.occlusionTexture) { occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null; occlusionMap && enabledTextures.push('occlusionMap'); } var baseColor = metallicRoughnessMatInfo.baseColorFactor || [1, 1, 1, 1]; var commonProperties = { diffuseMap: diffuseMap || null, roughnessMap: roughnessMap || null, metalnessMap: metalnessMap || null, normalMap: normalMap || null, occlusionMap: occlusionMap || null, emissiveMap: emissiveMap || null, color: baseColor.slice(0, 3), alpha: baseColor[3], metalness: metallicRoughnessMatInfo.metallicFactor || 0, roughness: metallicRoughnessMatInfo.roughnessFactor || 0, emission: materialInfo.emissiveFactor || [0, 0, 0], emissionIntensity: 1, alphaCutoff: materialInfo.alphaCutoff || 0, normalScale: normalScale }; if (commonProperties.roughnessMap) { // In glTF metallicFactor will do multiply, which is different from StandardMaterial. // So simply ignore it commonProperties.metalness = 0.5; commonProperties.roughness = 0.5; } if (isStandardMaterial) { material = new StandardMaterial(util$1.extend({ name: materialInfo.name, alphaTest: alphaTest, doubleSided: materialInfo.doubleSided, // G channel roughnessChannel: 1, // B Channel metalnessChannel: 2 }, commonProperties)); } else { material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'USE_ROUGHNESS'); material.define('fragment', 'USE_METALNESS'); material.define('fragment', 'ROUGHNESS_CHANNEL', 1); material.define('fragment', 'METALNESS_CHANNEL', 2); material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA'); if (alphaTest) { material.define('fragment', 'ALPHA_TEST'); } if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } material.set(commonProperties); } if (materialInfo.alphaMode === 'BLEND') { material.depthMask = false; material.depthTest = true; material.transparent = true; } return material; }, _pbrSpecularGlossinessToStandard: function (materialInfo, specularGlossinessMatInfo, lib) { var alphaTest = materialInfo.alphaMode === 'MASK'; if (this.useStandardMaterial) { console.error('StandardMaterial doesn\'t support specular glossiness workflow yet'); } var material; var diffuseMap, glossinessMap, specularMap, normalMap, emissiveMap, occlusionMap; var enabledTextures = []; // TODO texCoord if (specularGlossinessMatInfo.diffuseTexture) { diffuseMap = lib.textures[specularGlossinessMatInfo.diffuseTexture.index] || null; diffuseMap && enabledTextures.push('diffuseMap'); } if (specularGlossinessMatInfo.specularGlossinessTexture) { glossinessMap = specularMap = lib.textures[specularGlossinessMatInfo.specularGlossinessTexture.index] || null; glossinessMap && enabledTextures.push('specularMap', 'glossinessMap'); } if (materialInfo.normalTexture) { normalMap = lib.textures[materialInfo.normalTexture.index] || null; normalMap && enabledTextures.push('normalMap'); } if (materialInfo.emissiveTexture) { emissiveMap = lib.textures[materialInfo.emissiveTexture.index] || null; emissiveMap && enabledTextures.push('emissiveMap'); } if (materialInfo.occlusionTexture) { occlusionMap = lib.textures[materialInfo.occlusionTexture.index] || null; occlusionMap && enabledTextures.push('occlusionMap'); } var diffuseColor = specularGlossinessMatInfo.diffuseFactor || [1, 1, 1, 1]; var commonProperties = { diffuseMap: diffuseMap || null, glossinessMap: glossinessMap || null, specularMap: specularMap || null, normalMap: normalMap || null, emissiveMap: emissiveMap || null, occlusionMap: occlusionMap || null, color: diffuseColor.slice(0, 3), alpha: diffuseColor[3], specularColor: specularGlossinessMatInfo.specularFactor || [1, 1, 1], glossiness: specularGlossinessMatInfo.glossinessFactor || 0, emission: materialInfo.emissiveFactor || [0, 0, 0], emissionIntensity: 1, alphaCutoff: materialInfo.alphaCutoff == null ? 0.9 : materialInfo.alphaCutoff }; if (commonProperties.glossinessMap) { // Ignore specularFactor commonProperties.glossiness = 0.5; } if (commonProperties.specularMap) { // Ignore specularFactor commonProperties.specularColor = [1, 1, 1]; } material = new Material({ name: materialInfo.name, shader: this._getShader() }); material.define('fragment', 'GLOSSINESS_CHANNEL', 3); material.define('fragment', 'DIFFUSEMAP_ALPHA_ALPHA'); if (alphaTest) { material.define('fragment', 'ALPHA_TEST'); } if (materialInfo.doubleSided) { material.define('fragment', 'DOUBLE_SIDED'); } material.set(commonProperties); if (materialInfo.alphaMode === 'BLEND') { material.depthMask = false; material.depthTest = true; material.transparent = true; } return material; }, _parseMaterials: function (json, lib) { util$1.each(json.materials, function (materialInfo, idx) { if (materialInfo.extensions && materialInfo.extensions['KHR_materials_common']) { lib.materials[idx] = this._KHRCommonMaterialToStandard(materialInfo, lib); } else if (materialInfo.extensions && materialInfo.extensions['KHR_materials_pbrSpecularGlossiness']) { lib.materials[idx] = this._pbrSpecularGlossinessToStandard(materialInfo, materialInfo.extensions['KHR_materials_pbrSpecularGlossiness'], lib); } else { lib.materials[idx] = this._pbrMetallicRoughnessToStandard(materialInfo, materialInfo.pbrMetallicRoughness || {}, lib); } }, this); }, _parseMeshes: function (json, lib) { var self = this; util$1.each(json.meshes, function (meshInfo, idx) { lib.meshes[idx] = []; // Geometry for (var pp = 0; pp < meshInfo.primitives.length; pp++) { var primitiveInfo = meshInfo.primitives[pp]; var geometry = new Geometry({ dynamic: false, // PENDIGN name: meshInfo.name, boundingBox: new BoundingBox() }); // Parse attributes var semantics = Object.keys(primitiveInfo.attributes); for (var ss = 0; ss < semantics.length; ss++) { var semantic = semantics[ss]; var accessorIdx = primitiveInfo.attributes[semantic]; var attributeInfo = json.accessors[accessorIdx]; var attributeName = semanticAttributeMap[semantic]; if (!attributeName) { continue; } var size = SIZE_MAP[attributeInfo.type]; var attributeArray = getAccessorData(json, lib, accessorIdx); // WebGL attribute buffer not support uint32. // Direct use Float32Array may also have issue. if (attributeArray instanceof vendor.Uint32Array) { attributeArray = new Float32Array(attributeArray); } if (semantic === 'WEIGHTS_0' && size === 4) { // Weight data in QTEK has only 3 component, the last component can be evaluated since it is normalized var weightArray = new attributeArray.constructor(attributeInfo.count * 3); for (var i = 0; i < attributeInfo.count; i++) { var i4 = i * 4, i3 = i * 3; var w1 = attributeArray[i4], w2 = attributeArray[i4 + 1], w3 = attributeArray[i4 + 2], w4 = attributeArray[i4 + 3]; var wSum = w1 + w2 + w3 + w4; weightArray[i3] = w1 / wSum; weightArray[i3 + 1] = w2 / wSum; weightArray[i3 + 2] = w3 / wSum; } geometry.attributes[attributeName].value = weightArray; } else if (semantic === 'COLOR_0' && size === 3) { var colorArray = new attributeArray.constructor(attributeInfo.count * 4); for (var i = 0; i < attributeInfo.count; i++) { var i4 = i * 4, i3 = i * 3; colorArray[i4] = attributeArray[i3]; colorArray[i4 + 1] = attributeArray[i3 + 1]; colorArray[i4 + 2] = attributeArray[i3 + 2]; colorArray[i4 + 3] = 1; } geometry.attributes[attributeName].value = colorArray; } else { geometry.attributes[attributeName].value = attributeArray; } var attributeType = 'float'; if (attributeArray instanceof vendor.Uint16Array) { attributeType = 'ushort'; } else if (attributeArray instanceof vendor.Int16Array) { attributeType = 'short'; } else if (attributeArray instanceof vendor.Uint8Array) { attributeType = 'ubyte'; } else if (attributeArray instanceof vendor.Int8Array) { attributeType = 'byte'; } geometry.attributes[attributeName].type = attributeType; if (semantic === 'POSITION') { // Bounding Box var min = attributeInfo.min; var max = attributeInfo.max; if (min) { geometry.boundingBox.min.set(min[0], min[1], min[2]); } if (max) { geometry.boundingBox.max.set(max[0], max[1], max[2]); } } } // Parse indices if (primitiveInfo.indices != null) { geometry.indices = getAccessorData(json, lib, primitiveInfo.indices, true); if (geometry.vertexCount <= 0xffff && geometry.indices instanceof vendor.Uint32Array) { geometry.indices = new vendor.Uint16Array(geometry.indices); } if(geometry.indices instanceof vendor.Uint8Array) { geometry.indices = new vendor.Uint16Array(geometry.indices); } } var material = lib.materials[primitiveInfo.material]; var materialInfo = (json.materials || [])[primitiveInfo.material]; // Use default material if (!material) { material = new Material({ shader: self._getShader() }); } var mesh = new Mesh({ geometry: geometry, material: material, mode: [Mesh.POINTS, Mesh.LINES, Mesh.LINE_LOOP, Mesh.LINE_STRIP, Mesh.TRIANGLES, Mesh.TRIANGLE_STRIP, Mesh.TRIANGLE_FAN][primitiveInfo.mode] || Mesh.TRIANGLES, ignoreGBuffer: material.transparent }); if (materialInfo != null) { mesh.culling = !materialInfo.doubleSided; } if (!mesh.geometry.attributes.normal.value) { mesh.geometry.generateVertexNormals(); } if (((material instanceof StandardMaterial) && material.normalMap) || (material.isTextureEnabled('normalMap')) ) { if (!mesh.geometry.attributes.tangent.value) { mesh.geometry.generateTangents(); } } if (mesh.geometry.attributes.color.value) { mesh.material.define('VERTEX_COLOR'); } mesh.name = GLTFLoader.generateMeshName(json.meshes, idx, pp); lib.meshes[idx].push(mesh); } }, this); }, _instanceCamera: function (json, nodeInfo) { var cameraInfo = json.cameras[nodeInfo.camera]; if (cameraInfo.type === 'perspective') { var perspectiveInfo = cameraInfo.perspective || {}; return new Perspective$1({ name: nodeInfo.name, aspect: perspectiveInfo.aspectRatio, fov: perspectiveInfo.yfov / Math.PI * 180, far: perspectiveInfo.zfar, near: perspectiveInfo.znear }); } else { var orthographicInfo = cameraInfo.orthographic || {}; return new Orthographic$1({ name: nodeInfo.name, top: orthographicInfo.ymag, right: orthographicInfo.xmag, left: -orthographicInfo.xmag, bottom: -orthographicInfo.ymag, near: orthographicInfo.znear, far: orthographicInfo.zfar }); } }, _parseNodes: function (json, lib) { function instanceMesh(mesh) { return new Mesh({ name: mesh.name, geometry: mesh.geometry, material: mesh.material, culling: mesh.culling, mode: mesh.mode }); } lib.instancedMeshes = []; util$1.each(json.nodes, function (nodeInfo, idx) { var node; if (nodeInfo.camera != null && this.includeCamera) { node = this._instanceCamera(json, nodeInfo); lib.cameras.push(node); } else if (nodeInfo.mesh != null && this.includeMesh) { var primitives = lib.meshes[nodeInfo.mesh]; if (primitives) { if (primitives.length === 1) { // Replace the node with mesh directly node = instanceMesh(primitives[0]); node.setName(nodeInfo.name); lib.instancedMeshes.push(node); } else { node = new Node(); node.setName(nodeInfo.name); for (var j = 0; j < primitives.length; j++) { var newMesh = instanceMesh(primitives[j]); node.add(newMesh); lib.instancedMeshes.push(newMesh); } } } } else { node = new Node(); // PENDING Dulplicate name. node.setName(nodeInfo.name); } if (nodeInfo.matrix) { node.localTransform.setArray(nodeInfo.matrix); node.decomposeLocalTransform(); } else { if (nodeInfo.translation) { node.position.setArray(nodeInfo.translation); } if (nodeInfo.rotation) { node.rotation.setArray(nodeInfo.rotation); } if (nodeInfo.scale) { node.scale.setArray(nodeInfo.scale); } } lib.nodes[idx] = node; }, this); // Build hierarchy util$1.each(json.nodes, function (nodeInfo, idx) { var node = lib.nodes[idx]; if (nodeInfo.children) { for (var i = 0; i < nodeInfo.children.length; i++) { var childIdx = nodeInfo.children[i]; var child = lib.nodes[childIdx]; node.add(child); } } }); }, _parseAnimations: function (json, lib) { function checkChannelPath(channelInfo) { if (channelInfo.path === 'weights') { console.warn('GLTFLoader not support morph targets yet.'); return false; } return true; } function getChannelHash(channelInfo, animationInfo) { return channelInfo.target.node + '_' + animationInfo.samplers[channelInfo.sampler].input; } var timeAccessorMultiplied = {}; util$1.each(json.animations, function (animationInfo, idx) { var channels = animationInfo.channels.filter(checkChannelPath); if (!channels.length) { return; } var tracks = {}; for (var i = 0; i < channels.length; i++) { var channelInfo = channels[i]; var channelHash = getChannelHash(channelInfo, animationInfo); var targetNode = lib.nodes[channelInfo.target.node]; var track = tracks[channelHash]; var samplerInfo = animationInfo.samplers[channelInfo.sampler]; if (!track) { track = tracks[channelHash] = new SamplerTrack({ name: targetNode ? targetNode.name : '', target: targetNode }); track.targetNodeIndex = channelInfo.target.node; track.channels.time = getAccessorData(json, lib, samplerInfo.input); var frameLen = track.channels.time.length; if (!timeAccessorMultiplied[samplerInfo.input]) { for (var k = 0; k < frameLen; k++) { track.channels.time[k] *= 1000; } timeAccessorMultiplied[samplerInfo.input] = true; } } var interpolation = samplerInfo.interpolation || 'LINEAR'; if (interpolation !== 'LINEAR') { console.warn('GLTFLoader only support LINEAR interpolation.'); } var path = channelInfo.target.path; if (path === 'translation') { path = 'position'; } track.channels[path] = getAccessorData(json, lib, samplerInfo.output); } var tracksList = []; for (var hash in tracks) { tracksList.push(tracks[hash]); } var clip = new TrackClip({ name: animationInfo.name, loop: true, tracks: tracksList }); lib.clips.push(clip); }, this); // PENDING var maxLife = lib.clips.reduce(function (maxTime, clip) { return Math.max(maxTime, clip.life); }, 0); lib.clips.forEach(function (clip) { clip.life = maxLife; }); return lib.clips; } }); GLTFLoader.generateMeshName = function (meshes, idx, primitiveIdx) { var meshInfo = meshes[idx]; var meshName = meshInfo.name || ('mesh_' + idx); return primitiveIdx === 0 ? meshName : (meshName + '$' + primitiveIdx); }; /** * @constructor clay.light.Directional * @extends clay.Light * * @example * var light = new clay.light.Directional({ * intensity: 0.5, * color: [1.0, 0.0, 0.0] * }); * light.position.set(10, 10, 10); * light.lookAt(clay.Vector3.ZERO); * scene.add(light); */ var DirectionalLight = Light.extend(/** @lends clay.light.Directional# */ { /** * @type {number} */ shadowBias: 0.001, /** * @type {number} */ shadowSlopeScale: 2.0, /** * Shadow cascade. * Use PSSM technique when it is larger than 1 and have a unique directional light in scene. * @type {number} */ shadowCascade: 1, /** * Available when shadowCascade is larger than 1 and have a unique directional light in scene. * @type {number} */ cascadeSplitLogFactor: 0.2 }, { type: 'DIRECTIONAL_LIGHT', uniformTemplates: { directionalLightDirection: { type: '3f', value: function (instance) { instance.__dir = instance.__dir || new Vector3(); // Direction is target to eye return instance.__dir.copy(instance.worldTransform.z).normalize().negate().array; } }, directionalLightColor: { type: '3f', value: function (instance) { var color = instance.color; var intensity = instance.intensity; return [color[0] * intensity, color[1] * intensity, color[2] * intensity]; } } }, /** * @return {clay.light.Directional} * @memberOf clay.light.Directional.prototype */ clone: function () { var light = Light.prototype.clone.call(this); light.shadowBias = this.shadowBias; light.shadowSlopeScale = this.shadowSlopeScale; return light; } }); /** * @constructor clay.light.Point * @extends clay.Light */ var PointLight = Light.extend(/** @lends clay.light.Point# */ { /** * @type {number} */ range: 100, /** * @type {number} */ castShadow: false }, { type: 'POINT_LIGHT', uniformTemplates: { pointLightPosition: { type: '3f', value: function(instance) { return instance.getWorldPosition().array; } }, pointLightRange: { type: '1f', value: function(instance) { return instance.range; } }, pointLightColor: { type: '3f', value: function(instance) { var color = instance.color; var intensity = instance.intensity; return [color[0] * intensity, color[1] * intensity, color[2] * intensity]; } } }, /** * @return {clay.light.Point} * @memberOf clay.light.Point.prototype */ clone: function() { var light = Light.prototype.clone.call(this); light.range = this.range; return light; } }); /** * @constructor clay.light.Spot * @extends clay.Light */ var SpotLight = Light.extend(/**@lends clay.light.Spot */ { /** * @type {number} */ range: 20, /** * @type {number} */ umbraAngle: 30, /** * @type {number} */ penumbraAngle: 45, /** * @type {number} */ falloffFactor: 2.0, /** * @type {number} */ shadowBias: 0.001, /** * @type {number} */ shadowSlopeScale: 2.0 }, { type: 'SPOT_LIGHT', uniformTemplates: { spotLightPosition: { type: '3f', value: function (instance) { return instance.getWorldPosition().array; } }, spotLightRange: { type: '1f', value: function (instance) { return instance.range; } }, spotLightUmbraAngleCosine: { type: '1f', value: function (instance) { return Math.cos(instance.umbraAngle * Math.PI / 180); } }, spotLightPenumbraAngleCosine: { type: '1f', value: function (instance) { return Math.cos(instance.penumbraAngle * Math.PI / 180); } }, spotLightFalloffFactor: { type: '1f', value: function (instance) { return instance.falloffFactor; } }, spotLightDirection: { type: '3f', value: function (instance) { instance.__dir = instance.__dir || new Vector3(); // Direction is target to eye return instance.__dir.copy(instance.worldTransform.z).negate().array; } }, spotLightColor: { type: '3f', value: function (instance) { var color = instance.color; var intensity = instance.intensity; return [color[0] * intensity, color[1] * intensity, color[2] * intensity]; } } }, /** * @return {clay.light.Spot} * @memberOf clay.light.Spot.prototype */ clone: function () { var light = Light.prototype.clone.call(this); light.range = this.range; light.umbraAngle = this.umbraAngle; light.penumbraAngle = this.penumbraAngle; light.falloffFactor = this.falloffFactor; light.shadowBias = this.shadowBias; light.shadowSlopeScale = this.shadowSlopeScale; return light; } }); /** * @constructor clay.light.Ambient * @extends clay.Light */ var AmbientLight = Light.extend({ castShadow: false }, { type: 'AMBIENT_LIGHT', uniformTemplates: { ambientLightColor: { type: '3f', value: function(instance) { var color = instance.color; var intensity = instance.intensity; return [color[0]*intensity, color[1]*intensity, color[2]*intensity]; } } } /** * @function * @name clone * @return {clay.light.Ambient} * @memberOf clay.light.Ambient.prototype */ }); var KEY_FRAMEBUFFER = 'framebuffer'; var KEY_RENDERBUFFER = 'renderbuffer'; var KEY_RENDERBUFFER_WIDTH = KEY_RENDERBUFFER + '_width'; var KEY_RENDERBUFFER_HEIGHT = KEY_RENDERBUFFER + '_height'; var KEY_RENDERBUFFER_ATTACHED = KEY_RENDERBUFFER + '_attached'; var KEY_DEPTHTEXTURE_ATTACHED = 'depthtexture_attached'; var GL_FRAMEBUFFER = glenum.FRAMEBUFFER; var GL_RENDERBUFFER = glenum.RENDERBUFFER; var GL_DEPTH_ATTACHMENT = glenum.DEPTH_ATTACHMENT; var GL_COLOR_ATTACHMENT0 = glenum.COLOR_ATTACHMENT0; /** * @constructor clay.FrameBuffer * @extends clay.core.Base */ var FrameBuffer = Base.extend( /** @lends clay.FrameBuffer# */ { /** * If use depth buffer * @type {boolean} */ depthBuffer: true, /** * @type {Object} */ viewport: null, _width: 0, _height: 0, _textures: null, _boundRenderer: null, }, function () { // Use cache this._cache = new Cache(); this._textures = {}; }, /**@lends clay.FrameBuffer.prototype. */ { /** * Get attached texture width * {number} */ // FIXME Can't use before #bind getTextureWidth: function () { return this._width; }, /** * Get attached texture height * {number} */ getTextureHeight: function () { return this._height; }, /** * Bind the framebuffer to given renderer before rendering * @param {clay.Renderer} renderer */ bind: function (renderer) { if (renderer.__currentFrameBuffer) { // Already bound if (renderer.__currentFrameBuffer === this) { return; } console.warn('Renderer already bound with another framebuffer. Unbind it first'); } renderer.__currentFrameBuffer = this; var _gl = renderer.gl; _gl.bindFramebuffer(GL_FRAMEBUFFER, this._getFrameBufferGL(renderer)); this._boundRenderer = renderer; var cache = this._cache; cache.put('viewport', renderer.viewport); var hasTextureAttached = false; var width; var height; for (var attachment in this._textures) { hasTextureAttached = true; var obj = this._textures[attachment]; if (obj) { // TODO Do width, height checking, make sure size are same width = obj.texture.width; height = obj.texture.height; // Attach textures this._doAttach(renderer, obj.texture, attachment, obj.target); } } this._width = width; this._height = height; if (!hasTextureAttached && this.depthBuffer) { console.error('Must attach texture before bind, or renderbuffer may have incorrect width and height.'); } if (this.viewport) { renderer.setViewport(this.viewport); } else { renderer.setViewport(0, 0, width, height, 1); } var attachedTextures = cache.get('attached_textures'); if (attachedTextures) { for (var attachment in attachedTextures) { if (!this._textures[attachment]) { var target = attachedTextures[attachment]; this._doDetach(_gl, attachment, target); } } } if (!cache.get(KEY_DEPTHTEXTURE_ATTACHED) && this.depthBuffer) { // Create a new render buffer if (cache.miss(KEY_RENDERBUFFER)) { cache.put(KEY_RENDERBUFFER, _gl.createRenderbuffer()); } var renderbuffer = cache.get(KEY_RENDERBUFFER); if (width !== cache.get(KEY_RENDERBUFFER_WIDTH) || height !== cache.get(KEY_RENDERBUFFER_HEIGHT)) { _gl.bindRenderbuffer(GL_RENDERBUFFER, renderbuffer); _gl.renderbufferStorage(GL_RENDERBUFFER, _gl.DEPTH_COMPONENT16, width, height); cache.put(KEY_RENDERBUFFER_WIDTH, width); cache.put(KEY_RENDERBUFFER_HEIGHT, height); _gl.bindRenderbuffer(GL_RENDERBUFFER, null); } if (!cache.get(KEY_RENDERBUFFER_ATTACHED)) { _gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbuffer); cache.put(KEY_RENDERBUFFER_ATTACHED, true); } } }, /** * Unbind the frame buffer after rendering * @param {clay.Renderer} renderer */ unbind: function (renderer) { // Remove status record on renderer renderer.__currentFrameBuffer = null; var _gl = renderer.gl; _gl.bindFramebuffer(GL_FRAMEBUFFER, null); this._boundRenderer = null; this._cache.use(renderer.__uid__); var viewport = this._cache.get('viewport'); // Reset viewport; if (viewport) { renderer.setViewport(viewport); } this.updateMipmap(renderer); }, // Because the data of texture is changed over time, // Here update the mipmaps of texture each time after rendered; updateMipmap: function (renderer) { var _gl = renderer.gl; for (var attachment in this._textures) { var obj = this._textures[attachment]; if (obj) { var texture = obj.texture; // FIXME some texture format can't generate mipmap if (!texture.NPOT && texture.useMipmap && texture.minFilter === Texture.LINEAR_MIPMAP_LINEAR) { var target = texture.textureType === 'textureCube' ? glenum.TEXTURE_CUBE_MAP : glenum.TEXTURE_2D; _gl.bindTexture(target, texture.getWebGLTexture(renderer)); _gl.generateMipmap(target); _gl.bindTexture(target, null); } } } }, // 0x8CD5, 36053, FRAMEBUFFER_COMPLETE // 0x8CD6, 36054, FRAMEBUFFER_INCOMPLETE_ATTACHMENT // 0x8CD7, 36055, FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT // 0x8CD9, 36057, FRAMEBUFFER_INCOMPLETE_DIMENSIONS // 0x8CDD, 36061, FRAMEBUFFER_UNSUPPORTED checkStatus: function (_gl) { return _gl.checkFramebufferStatus(GL_FRAMEBUFFER); }, _getFrameBufferGL: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); if (cache.miss(KEY_FRAMEBUFFER)) { cache.put(KEY_FRAMEBUFFER, renderer.gl.createFramebuffer()); } return cache.get(KEY_FRAMEBUFFER); }, /** * Attach a texture(RTT) to the framebuffer * @param {clay.Texture} texture * @param {number} [attachment=gl.COLOR_ATTACHMENT0] * @param {number} [target=gl.TEXTURE_2D] */ attach: function (texture, attachment, target) { if (!texture.width) { throw new Error('The texture attached to color buffer is not a valid.'); } // TODO width and height check // If the depth_texture extension is enabled, developers // Can attach a depth texture to the depth buffer // http://blog.tojicode.com/2012/07/using-webgldepthtexture.html attachment = attachment || GL_COLOR_ATTACHMENT0; target = target || glenum.TEXTURE_2D; var boundRenderer = this._boundRenderer; var _gl = boundRenderer && boundRenderer.gl; var attachedTextures; if (_gl) { var cache = this._cache; cache.use(boundRenderer.__uid__); attachedTextures = cache.get('attached_textures'); } // Check if texture attached var previous = this._textures[attachment]; if (previous && previous.target === target && previous.texture === texture && (attachedTextures && attachedTextures[attachment] != null) ) { return; } var canAttach = true; if (boundRenderer) { canAttach = this._doAttach(boundRenderer, texture, attachment, target); // Set viewport again incase attached to different size textures. if (!this.viewport) { boundRenderer.setViewport(0, 0, texture.width, texture.height, 1); } } if (canAttach) { this._textures[attachment] = this._textures[attachment] || {}; this._textures[attachment].texture = texture; this._textures[attachment].target = target; } }, _doAttach: function (renderer, texture, attachment, target) { var _gl = renderer.gl; // Make sure texture is always updated // Because texture width or height may be changed and in this we can't be notified // FIXME awkward; var webglTexture = texture.getWebGLTexture(renderer); // Assume cache has been used. var attachedTextures = this._cache.get('attached_textures'); if (attachedTextures && attachedTextures[attachment]) { var obj = attachedTextures[attachment]; // Check if texture and target not changed if (obj.texture === texture && obj.target === target) { return; } } attachment = +attachment; var canAttach = true; if (attachment === GL_DEPTH_ATTACHMENT || attachment === glenum.DEPTH_STENCIL_ATTACHMENT) { var extension = renderer.getGLExtension('WEBGL_depth_texture'); if (!extension) { console.error('Depth texture is not supported by the browser'); canAttach = false; } if (texture.format !== glenum.DEPTH_COMPONENT && texture.format !== glenum.DEPTH_STENCIL ) { console.error('The texture attached to depth buffer is not a valid.'); canAttach = false; } // Dispose render buffer created previous if (canAttach) { var renderbuffer = this._cache.get(KEY_RENDERBUFFER); if (renderbuffer) { _gl.framebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, null); _gl.deleteRenderbuffer(renderbuffer); this._cache.put(KEY_RENDERBUFFER, false); } this._cache.put(KEY_RENDERBUFFER_ATTACHED, false); this._cache.put(KEY_DEPTHTEXTURE_ATTACHED, true); } } // Mipmap level can only be 0 _gl.framebufferTexture2D(GL_FRAMEBUFFER, attachment, target, webglTexture, 0); if (!attachedTextures) { attachedTextures = {}; this._cache.put('attached_textures', attachedTextures); } attachedTextures[attachment] = attachedTextures[attachment] || {}; attachedTextures[attachment].texture = texture; attachedTextures[attachment].target = target; return canAttach; }, _doDetach: function (_gl, attachment, target) { // Detach a texture from framebuffer // https://github.com/KhronosGroup/WebGL/blob/master/conformance-suites/1.0.0/conformance/framebuffer-test.html#L145 _gl.framebufferTexture2D(GL_FRAMEBUFFER, attachment, target, null, 0); // Assume cache has been used. var attachedTextures = this._cache.get('attached_textures'); if (attachedTextures && attachedTextures[attachment]) { attachedTextures[attachment] = null; } if (attachment === GL_DEPTH_ATTACHMENT || attachment === glenum.DEPTH_STENCIL_ATTACHMENT) { this._cache.put(KEY_DEPTHTEXTURE_ATTACHED, false); } }, /** * Detach a texture * @param {number} [attachment=gl.COLOR_ATTACHMENT0] * @param {number} [target=gl.TEXTURE_2D] */ detach: function (attachment, target) { // TODO depth extension check ? this._textures[attachment] = null; if (this._boundRenderer) { var cache = this._cache; cache.use(this._boundRenderer.__uid__); this._doDetach(this._boundRenderer.gl, attachment, target); } }, /** * Dispose * @param {WebGLRenderingContext} _gl */ dispose: function (renderer) { var _gl = renderer.gl; var cache = this._cache; cache.use(renderer.__uid__); var renderBuffer = cache.get(KEY_RENDERBUFFER); if (renderBuffer) { _gl.deleteRenderbuffer(renderBuffer); } var frameBuffer = cache.get(KEY_FRAMEBUFFER); if (frameBuffer) { _gl.deleteFramebuffer(frameBuffer); } cache.deleteContext(renderer.__uid__); // Clear cache for reusing this._textures = {}; } }); FrameBuffer.DEPTH_ATTACHMENT = GL_DEPTH_ATTACHMENT; FrameBuffer.COLOR_ATTACHMENT0 = GL_COLOR_ATTACHMENT0; FrameBuffer.STENCIL_ATTACHMENT = glenum.STENCIL_ATTACHMENT; FrameBuffer.DEPTH_STENCIL_ATTACHMENT = glenum.DEPTH_STENCIL_ATTACHMENT; var vertexGlsl = "\n@export clay.compositor.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\nattribute vec2 texcoord : TEXCOORD_0;\nvarying vec2 v_Texcoord;\nvoid main()\n{\n v_Texcoord = texcoord;\n gl_Position = worldViewProjection * vec4(position, 1.0);\n}\n@end"; Shader['import'](vertexGlsl); var planeGeo = new Plane$3(); var mesh = new Mesh({ geometry: planeGeo, frustumCulling: false }); var camera$1 = new Orthographic$1(); /** * @constructor clay.compositor.Pass * @extends clay.core.Base */ var Pass = Base.extend(function () { return /** @lends clay.compositor.Pass# */ { /** * Fragment shader string * @type {string} */ // PENDING shader or fragment ? fragment: '', /** * @type {Object} */ outputs: null, /** * @type {clay.Material} */ material: null, /** * @type {Boolean} */ blendWithPrevious: false, /** * @type {Boolean} */ clearColor: false, /** * @type {Boolean} */ clearDepth: true }; }, function() { var shader = new Shader(Shader.source('clay.compositor.vertex'), this.fragment); var material = new Material({ shader: shader }); material.enableTexturesAll(); this.material = material; }, /** @lends clay.compositor.Pass.prototype */ { /** * @param {string} name * @param {} value */ setUniform: function(name, value) { this.material.setUniform(name, value); }, /** * @param {string} name * @return {} */ getUniform: function(name) { var uniform = this.material.uniforms[name]; if (uniform) { return uniform.value; } }, /** * @param {clay.Texture} texture * @param {number} attachment */ attachOutput: function(texture, attachment) { if (!this.outputs) { this.outputs = {}; } attachment = attachment || glenum.COLOR_ATTACHMENT0; this.outputs[attachment] = texture; }, /** * @param {clay.Texture} texture */ detachOutput: function(texture) { for (var attachment in this.outputs) { if (this.outputs[attachment] === texture) { this.outputs[attachment] = null; } } }, bind: function(renderer, frameBuffer) { if (this.outputs) { for (var attachment in this.outputs) { var texture = this.outputs[attachment]; if (texture) { frameBuffer.attach(texture, attachment); } } } if (frameBuffer) { frameBuffer.bind(renderer); } }, unbind: function(renderer, frameBuffer) { frameBuffer.unbind(renderer); }, /** * @param {clay.Renderer} renderer * @param {clay.FrameBuffer} [frameBuffer] */ render: function(renderer, frameBuffer) { var _gl = renderer.gl; if (frameBuffer) { this.bind(renderer, frameBuffer); // MRT Support in chrome // https://www.khronos.org/registry/webgl/sdk/tests/conformance/extensions/ext-draw-buffers.html var ext = renderer.getGLExtension('EXT_draw_buffers'); if (ext && this.outputs) { var bufs = []; for (var attachment in this.outputs) { attachment = +attachment; if (attachment >= _gl.COLOR_ATTACHMENT0 && attachment <= _gl.COLOR_ATTACHMENT0 + 8) { bufs.push(attachment); } } ext.drawBuffersEXT(bufs); } } this.trigger('beforerender', this, renderer); // FIXME Don't clear in each pass in default, let the color overwrite the buffer // FIXME pixels may be discard var clearBit = this.clearDepth ? _gl.DEPTH_BUFFER_BIT : 0; _gl.depthMask(true); if (this.clearColor) { clearBit = clearBit | _gl.COLOR_BUFFER_BIT; _gl.colorMask(true, true, true, true); var cc = this.clearColor; if (Array.isArray(cc)) { _gl.clearColor(cc[0], cc[1], cc[2], cc[3]); } } _gl.clear(clearBit); if (this.blendWithPrevious) { // Blend with previous rendered scene in the final output // FIXME Configure blend. // FIXME It will cause screen blink? _gl.enable(_gl.BLEND); this.material.transparent = true; } else { _gl.disable(_gl.BLEND); this.material.transparent = false; } this.renderQuad(renderer); this.trigger('afterrender', this, renderer); if (frameBuffer) { this.unbind(renderer, frameBuffer); } }, /** * Simply do quad rendering */ renderQuad: function (renderer) { mesh.material = this.material; renderer.renderPass([mesh], camera$1); }, /** * @param {clay.Renderer} renderer */ dispose: function (renderer) {} }); // TODO Should not derived from mesh? Shader.import(skyboxEssl); /** * @constructor clay.plugin.Skybox * * @example * var skyTex = new clay.TextureCube(); * skyTex.load({ * 'px': 'assets/textures/sky/px.jpg', * 'nx': 'assets/textures/sky/nx.jpg' * 'py': 'assets/textures/sky/py.jpg' * 'ny': 'assets/textures/sky/ny.jpg' * 'pz': 'assets/textures/sky/pz.jpg' * 'nz': 'assets/textures/sky/nz.jpg' * }); * var skybox = new clay.plugin.Skybox({ * scene: scene * }); * skybox.material.set('environmentMap', skyTex); */ var Skybox$1 = Mesh.extend(function () { var skyboxShader = new Shader({ vertex: Shader.source('clay.skybox.vertex'), fragment: Shader.source('clay.skybox.fragment') }); var material = new Material({ shader: skyboxShader, depthMask: false }); return { /** * @type {clay.Scene} * @memberOf clay.plugin.Skybox.prototype */ scene: null, geometry: new Cube$1(), material: material, environmentMap: null, culling: false, _dummyCamera: new Perspective$1() }; }, function () { var scene = this.scene; if (scene) { this.attachScene(scene); } if (this.environmentMap) { this.setEnvironmentMap(this.environmentMap); } }, /** @lends clay.plugin.Skybox# */ { /** * Attach the skybox to the scene * @param {clay.Scene} scene */ attachScene: function (scene) { if (this.scene) { this.detachScene(); } scene.skybox = this; this.scene = scene; scene.on('beforerender', this._beforeRenderScene, this); }, /** * Detach from scene */ detachScene: function () { if (this.scene) { this.scene.off('beforerender', this._beforeRenderScene); this.scene.skybox = null; } this.scene = null; }, /** * Dispose skybox * @param {clay.Renderer} renderer */ dispose: function (renderer) { this.detachScene(); this.geometry.dispose(renderer); }, /** * Set environment map * @param {clay.TextureCube} envMap */ setEnvironmentMap: function (envMap) { if (envMap.textureType === 'texture2D') { this.material.define('EQUIRECTANGULAR'); // LINEAR filter can remove the artifacts in pole envMap.minFilter = Texture.LINEAR; } else { this.material.undefine('EQUIRECTANGULAR'); } this.material.set('environmentMap', envMap); }, /** * Get environment map * @return {clay.TextureCube} */ getEnvironmentMap: function () { return this.material.get('environmentMap'); }, _beforeRenderScene: function(renderer, scene, camera) { this.renderSkybox(renderer, camera); }, renderSkybox: function (renderer, camera) { var dummyCamera = this._dummyCamera; dummyCamera.aspect = renderer.getViewportAspect(); dummyCamera.fov = camera.fov || 50; dummyCamera.updateProjectionMatrix(); Matrix4.invert(dummyCamera.invProjectionMatrix, dummyCamera.projectionMatrix); dummyCamera.worldTransform.copy(camera.worldTransform); dummyCamera.viewMatrix.copy(camera.viewMatrix); this.position.copy(camera.getWorldPosition()); this.update(); // Don't remember to disable blend renderer.gl.disable(renderer.gl.BLEND); if (this.material.get('lod') > 0) { this.material.define('fragment', 'LOD'); } else { this.material.undefine('fragment', 'LOD'); } renderer.renderPass([this], dummyCamera); } }); var targets$1 = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; /** * Pass rendering scene to a environment cube map * * @constructor clay.prePass.EnvironmentMap * @extends clay.core.Base * @example * // Example of car reflection * var envMap = new clay.TextureCube({ * width: 256, * height: 256 * }); * var envPass = new clay.prePass.EnvironmentMap({ * position: car.position, * texture: envMap * }); * var carBody = car.getChildByName('body'); * carBody.material.enableTexture('environmentMap'); * carBody.material.set('environmentMap', envMap); * ... * animation.on('frame', function(frameTime) { * envPass.render(renderer, scene); * renderer.render(scene, camera); * }); */ var EnvironmentMapPass = Base.extend(function() { var ret = /** @lends clay.prePass.EnvironmentMap# */ { /** * Camera position * @type {clay.Vector3} * @memberOf clay.prePass.EnvironmentMap# */ position: new Vector3(), /** * Camera far plane * @type {number} * @memberOf clay.prePass.EnvironmentMap# */ far: 1000, /** * Camera near plane * @type {number} * @memberOf clay.prePass.EnvironmentMap# */ near: 0.1, /** * Environment cube map * @type {clay.TextureCube} * @memberOf clay.prePass.EnvironmentMap# */ texture: null, /** * Used if you wan't have shadow in environment map * @type {clay.prePass.ShadowMap} */ shadowMapPass: null, }; var cameras = ret._cameras = { px: new Perspective$1({ fov: 90 }), nx: new Perspective$1({ fov: 90 }), py: new Perspective$1({ fov: 90 }), ny: new Perspective$1({ fov: 90 }), pz: new Perspective$1({ fov: 90 }), nz: new Perspective$1({ fov: 90 }) }; cameras.px.lookAt(Vector3.POSITIVE_X, Vector3.NEGATIVE_Y); cameras.nx.lookAt(Vector3.NEGATIVE_X, Vector3.NEGATIVE_Y); cameras.py.lookAt(Vector3.POSITIVE_Y, Vector3.POSITIVE_Z); cameras.ny.lookAt(Vector3.NEGATIVE_Y, Vector3.NEGATIVE_Z); cameras.pz.lookAt(Vector3.POSITIVE_Z, Vector3.NEGATIVE_Y); cameras.nz.lookAt(Vector3.NEGATIVE_Z, Vector3.NEGATIVE_Y); // FIXME In windows, use one framebuffer only renders one side of cubemap ret._frameBuffer = new FrameBuffer(); return ret; }, /** @lends clay.prePass.EnvironmentMap# */ { /** * @param {string} target * @return {clay.Camera} */ getCamera: function (target) { return this._cameras[target]; }, /** * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {boolean} [notUpdateScene=false] */ render: function(renderer, scene, notUpdateScene) { var _gl = renderer.gl; if (!notUpdateScene) { scene.update(); } // Tweak fov // http://the-witness.net/news/2012/02/seamless-cube-map-filtering/ var n = this.texture.width; var fov = 2 * Math.atan(n / (n - 0.5)) / Math.PI * 180; for (var i = 0; i < 6; i++) { var target = targets$1[i]; var camera = this._cameras[target]; Vector3.copy(camera.position, this.position); camera.far = this.far; camera.near = this.near; camera.fov = fov; if (this.shadowMapPass) { camera.update(); // update boundingBoxLastFrame var bbox = scene.getBoundingBox(); bbox.applyTransform(camera.viewMatrix); scene.viewBoundingBoxLastFrame.copy(bbox); this.shadowMapPass.render(renderer, scene, camera, true); } this._frameBuffer.attach( this.texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i ); this._frameBuffer.bind(renderer); renderer.render(scene, camera, true); this._frameBuffer.unbind(renderer); } }, /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._frameBuffer.dispose(renderer); } }); // http://msdn.microsoft.com/en-us/library/windows/desktop/bb943991(v=vs.85).aspx // https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js var DDS_MAGIC = 0x20534444; var DDSD_MIPMAPCOUNT = 0x20000; var DDSCAPS2_CUBEMAP = 0x200; var DDPF_FOURCC = 0x4; function fourCCToInt32(value) { return value.charCodeAt(0) + (value.charCodeAt(1) << 8) + (value.charCodeAt(2) << 16) + (value.charCodeAt(3) << 24); } var headerLengthInt = 31; // The header length in 32 bit ints var FOURCC_DXT1 = fourCCToInt32('DXT1'); var FOURCC_DXT3 = fourCCToInt32('DXT3'); var FOURCC_DXT5 = fourCCToInt32('DXT5'); // Offsets into the header array var off_magic = 0; var off_size = 1; var off_flags = 2; var off_height = 3; var off_width = 4; var off_mipmapCount = 7; var off_pfFlags = 20; var off_pfFourCC = 21; var off_caps2 = 28; var ret = { parse: function(arrayBuffer, out) { var header = new Int32Array(arrayBuffer, 0, headerLengthInt); if (header[off_magic] !== DDS_MAGIC) { return null; } if (!header(off_pfFlags) & DDPF_FOURCC) { return null; } var fourCC = header(off_pfFourCC); var width = header[off_width]; var height = header[off_height]; var isCubeMap = header[off_caps2] & DDSCAPS2_CUBEMAP; var hasMipmap = header[off_flags] & DDSD_MIPMAPCOUNT; var blockBytes, internalFormat; switch(fourCC) { case FOURCC_DXT1: blockBytes = 8; internalFormat = Texture.COMPRESSED_RGB_S3TC_DXT1_EXT; break; case FOURCC_DXT3: blockBytes = 16; internalFormat = Texture.COMPRESSED_RGBA_S3TC_DXT3_EXT; break; case FOURCC_DXT5: blockBytes = 16; internalFormat = Texture.COMPRESSED_RGBA_S3TC_DXT5_EXT; break; default: return null; } var dataOffset = header[off_size] + 4; // TODO: Suppose all face are existed var faceNumber = isCubeMap ? 6 : 1; var mipmapCount = 1; if (hasMipmap) { mipmapCount = Math.max(1, header[off_mipmapCount]); } var textures = []; for (var f = 0; f < faceNumber; f++) { var _width = width; var _height = height; textures[f] = new Texture2D({ width: _width, height: _height, format: internalFormat }); var mipmaps = []; for (var i = 0; i < mipmapCount; i++) { var dataLength = Math.max(4, _width) / 4 * Math.max(4, _height) / 4 * blockBytes; var byteArray = new Uint8Array(arrayBuffer, dataOffset, dataLength); dataOffset += dataLength; _width *= 0.5; _height *= 0.5; mipmaps[i] = byteArray; } textures[f].pixels = mipmaps[0]; if (hasMipmap) { textures[f].mipmaps = mipmaps; } } // TODO // return isCubeMap ? textures : textures[0]; if (out) { out.width = textures[0].width; out.height = textures[0].height; out.format = textures[0].format; out.pixels = textures[0].pixels; out.mipmaps = textures[0].mipmaps; } else { return textures[0]; } } }; var toChar = String.fromCharCode; var MINELEN = 8; var MAXELEN = 0x7fff; function rgbe2float(rgbe, buffer, offset, exposure) { if (rgbe[3] > 0) { var f = Math.pow(2.0, rgbe[3] - 128 - 8 + exposure); buffer[offset + 0] = rgbe[0] * f; buffer[offset + 1] = rgbe[1] * f; buffer[offset + 2] = rgbe[2] * f; } else { buffer[offset + 0] = 0; buffer[offset + 1] = 0; buffer[offset + 2] = 0; } buffer[offset + 3] = 1.0; return buffer; } function uint82string(array, offset, size) { var str = ''; for (var i = offset; i < size; i++) { str += toChar(array[i]); } return str; } function copyrgbe(s, t) { t[0] = s[0]; t[1] = s[1]; t[2] = s[2]; t[3] = s[3]; } // TODO : check function oldReadColors(scan, buffer, offset, xmax) { var rshift = 0, x = 0, len = xmax; while (len > 0) { scan[x][0] = buffer[offset++]; scan[x][1] = buffer[offset++]; scan[x][2] = buffer[offset++]; scan[x][3] = buffer[offset++]; if (scan[x][0] === 1 && scan[x][1] === 1 && scan[x][2] === 1) { // exp is count of repeated pixels for (var i = (scan[x][3] << rshift) >>> 0; i > 0; i--) { copyrgbe(scan[x-1], scan[x]); x++; len--; } rshift += 8; } else { x++; len--; rshift = 0; } } return offset; } function readColors(scan, buffer, offset, xmax) { if ((xmax < MINELEN) | (xmax > MAXELEN)) { return oldReadColors(scan, buffer, offset, xmax); } var i = buffer[offset++]; if (i != 2) { return oldReadColors(scan, buffer, offset - 1, xmax); } scan[0][1] = buffer[offset++]; scan[0][2] = buffer[offset++]; i = buffer[offset++]; if ((((scan[0][2] << 8) >>> 0) | i) >>> 0 !== xmax) { return null; } for (var i = 0; i < 4; i++) { for (var x = 0; x < xmax;) { var code = buffer[offset++]; if (code > 128) { code = (code & 127) >>> 0; var val = buffer[offset++]; while (code--) { scan[x++][i] = val; } } else { while (code--) { scan[x++][i] = buffer[offset++]; } } } } return offset; } var ret$1 = { // http://www.graphics.cornell.edu/~bjw/rgbe.html // Blender source // http://radsite.lbl.gov/radiance/refer/Notes/picture_format.html parseRGBE: function(arrayBuffer, texture, exposure) { if (exposure == null) { exposure = 0; } var data = new Uint8Array(arrayBuffer); var size = data.length; if (uint82string(data, 0, 2) !== '#?') { return; } // find empty line, next line is resolution info for (var i = 2; i < size; i++) { if (toChar(data[i]) === '\n' && toChar(data[i+1]) === '\n') { break; } } if (i >= size) { // not found return; } // find resolution info line i += 2; var str = ''; for (; i < size; i++) { var _char = toChar(data[i]); if (_char === '\n') { break; } str += _char; } // -Y M +X N var tmp = str.split(' '); var height = parseInt(tmp[1]); var width = parseInt(tmp[3]); if (!width || !height) { return; } // read and decode actual data var offset = i+1; var scanline = []; // memzero for (var x = 0; x < width; x++) { scanline[x] = []; for (var j = 0; j < 4; j++) { scanline[x][j] = 0; } } var pixels = new Float32Array(width * height * 4); var offset2 = 0; for (var y = 0; y < height; y++) { var offset = readColors(scanline, data, offset, width); if (!offset) { return null; } for (var x = 0; x < width; x++) { rgbe2float(scanline[x], pixels, offset2, exposure); offset2 += 4; } } if (!texture) { texture = new Texture2D(); } texture.width = width; texture.height = height; texture.pixels = pixels; // HALF_FLOAT can't use Float32Array texture.type = Texture.FLOAT; return texture; }, parseRGBEFromPNG: function(png) { } }; /** * @alias clay.util.texture */ var textureUtil = { /** * @param {string|object} path * @param {object} [option] * @param {Function} [onsuccess] * @param {Function} [onerror] * @return {clay.Texture} */ loadTexture: function (path, option, onsuccess, onerror) { var texture; if (typeof(option) === 'function') { onsuccess = option; onerror = onsuccess; option = {}; } else { option = option || {}; } if (typeof(path) === 'string') { if (path.match(/.hdr$/) || option.fileType === 'hdr') { texture = new Texture2D({ width: 0, height: 0, sRGB: false }); textureUtil._fetchTexture( path, function (data) { ret$1.parseRGBE(data, texture, option.exposure); texture.dirty(); onsuccess && onsuccess(texture); }, onerror ); return texture; } else if (path.match(/.dds$/) || option.fileType === 'dds') { texture = new Texture2D({ width: 0, height: 0 }); textureUtil._fetchTexture( path, function (data) { ret.parse(data, texture); texture.dirty(); onsuccess && onsuccess(texture); }, onerror ); } else { texture = new Texture2D(); texture.load(path); texture.success(onsuccess); texture.error(onerror); } } else if (typeof path === 'object' && typeof(path.px) !== 'undefined') { texture = new TextureCube(); texture.load(path); texture.success(onsuccess); texture.error(onerror); } return texture; }, /** * Load a panorama texture and render it to a cube map * @param {clay.Renderer} renderer * @param {string} path * @param {clay.TextureCube} cubeMap * @param {object} [option] * @param {boolean} [option.encodeRGBM] * @param {number} [option.exposure] * @param {Function} [onsuccess] * @param {Function} [onerror] */ loadPanorama: function (renderer, path, cubeMap, option, onsuccess, onerror) { var self = this; if (typeof(option) === 'function') { onsuccess = option; onerror = onsuccess; option = {}; } else { option = option || {}; } textureUtil.loadTexture(path, option, function (texture) { // PENDING texture.flipY = option.flipY || false; self.panoramaToCubeMap(renderer, texture, cubeMap, option); texture.dispose(renderer); onsuccess && onsuccess(cubeMap); }, onerror); }, /** * Render a panorama texture to a cube map * @param {clay.Renderer} renderer * @param {clay.Texture2D} panoramaMap * @param {clay.TextureCube} cubeMap * @param {Object} option * @param {boolean} [option.encodeRGBM] */ panoramaToCubeMap: function (renderer, panoramaMap, cubeMap, option) { var environmentMapPass = new EnvironmentMapPass(); var skydome = new Skybox$1({ scene: new Scene() }); skydome.setEnvironmentMap(panoramaMap); option = option || {}; if (option.encodeRGBM) { skydome.material.define('fragment', 'RGBM_ENCODE'); } // Share sRGB cubeMap.sRGB = panoramaMap.sRGB; environmentMapPass.texture = cubeMap; environmentMapPass.render(renderer, skydome.scene); environmentMapPass.texture = null; environmentMapPass.dispose(renderer); return cubeMap; }, /** * Convert height map to normal map * @param {HTMLImageElement|HTMLCanvasElement} image * @param {boolean} [checkBump=false] * @return {HTMLCanvasElement} */ heightToNormal: function (image, checkBump) { var canvas = document.createElement('canvas'); var width = canvas.width = image.width; var height = canvas.height = image.height; var ctx = canvas.getContext('2d'); ctx.drawImage(image, 0, 0, width, height); checkBump = checkBump || false; var srcData = ctx.getImageData(0, 0, width, height); var dstData = ctx.createImageData(width, height); for (var i = 0; i < srcData.data.length; i += 4) { if (checkBump) { var r = srcData.data[i]; var g = srcData.data[i + 1]; var b = srcData.data[i + 2]; var diff = Math.abs(r - g) + Math.abs(g - b); if (diff > 20) { console.warn('Given image is not a height map'); return image; } } // Modified from http://mrdoob.com/lab/javascript/height2normal/ var x1, y1, x2, y2; if (i % (width * 4) === 0) { // left edge x1 = srcData.data[i]; x2 = srcData.data[i + 4]; } else if (i % (width * 4) === (width - 1) * 4) { // right edge x1 = srcData.data[i - 4]; x2 = srcData.data[i]; } else { x1 = srcData.data[i - 4]; x2 = srcData.data[i + 4]; } if (i < width * 4) { // top edge y1 = srcData.data[i]; y2 = srcData.data[i + width * 4]; } else if (i > width * (height - 1) * 4) { // bottom edge y1 = srcData.data[i - width * 4]; y2 = srcData.data[i]; } else { y1 = srcData.data[i - width * 4]; y2 = srcData.data[i + width * 4]; } dstData.data[i] = (x1 - x2) + 127; dstData.data[i + 1] = (y1 - y2) + 127; dstData.data[i + 2] = 255; dstData.data[i + 3] = 255; } ctx.putImageData(dstData, 0, 0); return canvas; }, /** * Convert height map to normal map * @param {HTMLImageElement|HTMLCanvasElement} image * @param {boolean} [checkBump=false] * @param {number} [threshold=20] * @return {HTMLCanvasElement} */ isHeightImage: function (img, downScaleSize, threshold) { if (!img || !img.width || !img.height) { return false; } var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var size = downScaleSize || 32; threshold = threshold || 20; canvas.width = canvas.height = size; ctx.drawImage(img, 0, 0, size, size); var srcData = ctx.getImageData(0, 0, size, size); for (var i = 0; i < srcData.data.length; i += 4) { var r = srcData.data[i]; var g = srcData.data[i + 1]; var b = srcData.data[i + 2]; var diff = Math.abs(r - g) + Math.abs(g - b); if (diff > threshold) { return false; } } return true; }, _fetchTexture: function (path, onsuccess, onerror) { vendor.request.get({ url: path, responseType: 'arraybuffer', onload: onsuccess, onerror: onerror }); }, /** * Create a chessboard texture * @param {number} [size] * @param {number} [unitSize] * @param {string} [color1] * @param {string} [color2] * @return {clay.Texture2D} */ createChessboard: function (size, unitSize, color1, color2) { size = size || 512; unitSize = unitSize || 64; color1 = color1 || 'black'; color2 = color2 || 'white'; var repeat = Math.ceil(size / unitSize); var canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; var ctx = canvas.getContext('2d'); ctx.fillStyle = color2; ctx.fillRect(0, 0, size, size); ctx.fillStyle = color1; for (var i = 0; i < repeat; i++) { for (var j = 0; j < repeat; j++) { var isFill = j % 2 ? (i % 2) : (i % 2 - 1); if (isFill) { ctx.fillRect(i * unitSize, j * unitSize, unitSize, unitSize); } } } var texture = new Texture2D({ image: canvas, anisotropic: 8 }); return texture; }, /** * Create a blank pure color 1x1 texture * @param {string} color * @return {clay.Texture2D} */ createBlank: function (color) { var canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 1; var ctx = canvas.getContext('2d'); ctx.fillStyle = color; ctx.fillRect(0, 0, 1, 1); var texture = new Texture2D({ image: canvas }); return texture; } }; var integrateBRDFShaderCode = "#define SAMPLE_NUMBER 1024\n#define PI 3.14159265358979\nuniform sampler2D normalDistribution;\nuniform vec2 viewportSize : [512, 256];\nconst vec3 N = vec3(0.0, 0.0, 1.0);\nconst float fSampleNumber = float(SAMPLE_NUMBER);\nvec3 importanceSampleNormal(float i, float roughness, vec3 N) {\n vec3 H = texture2D(normalDistribution, vec2(roughness, i)).rgb;\n vec3 upVector = abs(N.y) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);\n vec3 tangentX = normalize(cross(N, upVector));\n vec3 tangentZ = cross(N, tangentX);\n return normalize(tangentX * H.x + N * H.y + tangentZ * H.z);\n}\nfloat G_Smith(float roughness, float NoV, float NoL) {\n float k = roughness * roughness / 2.0;\n float G1V = NoV / (NoV * (1.0 - k) + k);\n float G1L = NoL / (NoL * (1.0 - k) + k);\n return G1L * G1V;\n}\nvoid main() {\n vec2 uv = gl_FragCoord.xy / viewportSize;\n float NoV = uv.x;\n float roughness = uv.y;\n vec3 V;\n V.x = sqrt(1.0 - NoV * NoV);\n V.y = 0.0;\n V.z = NoV;\n float A = 0.0;\n float B = 0.0;\n for (int i = 0; i < SAMPLE_NUMBER; i++) {\n vec3 H = importanceSampleNormal(float(i) / fSampleNumber, roughness, N);\n vec3 L = reflect(-V, H);\n float NoL = clamp(L.z, 0.0, 1.0);\n float NoH = clamp(H.z, 0.0, 1.0);\n float VoH = clamp(dot(V, H), 0.0, 1.0);\n if (NoL > 0.0) {\n float G = G_Smith(roughness, NoV, NoL);\n float G_Vis = G * VoH / (NoH * NoV);\n float Fc = pow(1.0 - VoH, 5.0);\n A += (1.0 - Fc) * G_Vis;\n B += Fc * G_Vis;\n }\n }\n gl_FragColor = vec4(vec2(A, B) / fSampleNumber, 0.0, 1.0);\n}\n"; var prefilterFragCode = "#define SHADER_NAME prefilter\n#define SAMPLE_NUMBER 1024\n#define PI 3.14159265358979\nuniform mat4 viewInverse : VIEWINVERSE;\nuniform samplerCube environmentMap;\nuniform sampler2D normalDistribution;\nuniform float roughness : 0.5;\nvarying vec2 v_Texcoord;\nvarying vec3 v_WorldPosition;\n@import clay.util.rgbm\nvec3 importanceSampleNormal(float i, float roughness, vec3 N) {\n vec3 H = texture2D(normalDistribution, vec2(roughness, i)).rgb;\n vec3 upVector = abs(N.y) > 0.999 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);\n vec3 tangentX = normalize(cross(N, upVector));\n vec3 tangentZ = cross(N, tangentX);\n return normalize(tangentX * H.x + N * H.y + tangentZ * H.z);\n}\nvoid main() {\n vec3 eyePos = viewInverse[3].xyz;\n vec3 V = normalize(v_WorldPosition - eyePos);\n vec3 N = V;\n vec3 prefilteredColor = vec3(0.0);\n float totalWeight = 0.0;\n float fMaxSampleNumber = float(SAMPLE_NUMBER);\n for (int i = 0; i < SAMPLE_NUMBER; i++) {\n vec3 H = importanceSampleNormal(float(i) / fMaxSampleNumber, roughness, N);\n vec3 L = reflect(-V, H);\n float NoL = clamp(dot(N, L), 0.0, 1.0);\n if (NoL > 0.0) {\n prefilteredColor += decodeHDR(textureCube(environmentMap, L)).rgb * NoL;\n totalWeight += NoL;\n }\n }\n gl_FragColor = encodeHDR(vec4(prefilteredColor / totalWeight, 1.0));\n}\n"; // Cubemap prefilter utility // http://www.unrealengine.com/files/downloads/2013SiggraphPresentationsNotes.pdf // http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html var cubemapUtil = {}; var targets = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; // TODO Downsample /** * @name clay.util.cubemap.prefilterEnvironmentMap * @param {clay.Renderer} renderer * @param {clay.Texture} envMap * @param {Object} [textureOpts] * @param {number} [textureOpts.width=64] * @param {number} [textureOpts.height=64] * @param {number} [textureOpts.type] * @param {boolean} [textureOpts.encodeRGBM=false] * @param {boolean} [textureOpts.decodeRGBM=false] * @param {clay.Texture2D} [normalDistribution] * @param {clay.Texture2D} [brdfLookup] */ cubemapUtil.prefilterEnvironmentMap = function ( renderer, envMap, textureOpts, normalDistribution, brdfLookup ) { // Not create other renderer, it is easy having issue of cross reference of resources like framebuffer // PENDING preserveDrawingBuffer? if (!brdfLookup || !normalDistribution) { normalDistribution = cubemapUtil.generateNormalDistribution(); brdfLookup = cubemapUtil.integrateBRDF(renderer, normalDistribution); } textureOpts = textureOpts || {}; var width = textureOpts.width || 64; var height = textureOpts.height || 64; var textureType = textureOpts.type || envMap.type; // Use same type with given envMap var prefilteredCubeMap = new TextureCube({ width: width, height: height, type: textureType, flipY: false, mipmaps: [] }); if (!prefilteredCubeMap.isPowerOfTwo()) { console.warn('Width and height must be power of two to enable mipmap.'); } var size = Math.min(width, height); var mipmapNum = Math.log(size) / Math.log(2) + 1; var prefilterMaterial = new Material({ shader: new Shader({ vertex: Shader.source('clay.skybox.vertex'), fragment: prefilterFragCode }) }); prefilterMaterial.set('normalDistribution', normalDistribution); textureOpts.encodeRGBM && prefilterMaterial.define('fragment', 'RGBM_ENCODE'); textureOpts.decodeRGBM && prefilterMaterial.define('fragment', 'RGBM_DECODE'); var dummyScene = new Scene(); var skyEnv; if (envMap.textureType === 'texture2D') { // Convert panorama to cubemap var envCubemap = new TextureCube({ width: width, height: height, // FIXME FLOAT type will cause GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT error on iOS type: textureType === Texture.FLOAT ? Texture.HALF_FLOAT : textureType }); textureUtil.panoramaToCubeMap(renderer, envMap, envCubemap, { // PENDING encodeRGBM so it can be decoded as RGBM encodeRGBM: textureOpts.decodeRGBM }); envMap = envCubemap; } skyEnv = new Skybox$1({ scene: dummyScene, material: prefilterMaterial }); skyEnv.material.set('environmentMap', envMap); var envMapPass = new EnvironmentMapPass({ texture: prefilteredCubeMap }); // Force to be UNSIGNED_BYTE if (textureOpts.encodeRGBM) { textureType = prefilteredCubeMap.type = Texture.UNSIGNED_BYTE; } var renderTargetTmp = new Texture2D({ width: width, height: height, type: textureType }); var frameBuffer = new FrameBuffer({ depthBuffer: false }); var ArrayCtor = vendor[textureType === Texture.UNSIGNED_BYTE ? 'Uint8Array' : 'Float32Array']; for (var i = 0; i < mipmapNum; i++) { // console.time('prefilter'); prefilteredCubeMap.mipmaps[i] = { pixels: {} }; skyEnv.material.set('roughness', i / (mipmapNum - 1)); // Tweak fov // http://the-witness.net/news/2012/02/seamless-cube-map-filtering/ var n = renderTargetTmp.width; var fov = 2 * Math.atan(n / (n - 0.5)) / Math.PI * 180; for (var j = 0; j < targets.length; j++) { var pixels = new ArrayCtor(renderTargetTmp.width * renderTargetTmp.height * 4); frameBuffer.attach(renderTargetTmp); frameBuffer.bind(renderer); var camera = envMapPass.getCamera(targets[j]); camera.fov = fov; renderer.render(dummyScene, camera); renderer.gl.readPixels( 0, 0, renderTargetTmp.width, renderTargetTmp.height, Texture.RGBA, textureType, pixels ); // var canvas = document.createElement('canvas'); // var ctx = canvas.getContext('2d'); // canvas.width = renderTargetTmp.width; // canvas.height = renderTargetTmp.height; // var imageData = ctx.createImageData(renderTargetTmp.width, renderTargetTmp.height); // for (var k = 0; k < pixels.length; k++) { // imageData.data[k] = pixels[k]; // } // ctx.putImageData(imageData, 0, 0); // document.body.appendChild(canvas); frameBuffer.unbind(renderer); prefilteredCubeMap.mipmaps[i].pixels[targets[j]] = pixels; } renderTargetTmp.width /= 2; renderTargetTmp.height /= 2; renderTargetTmp.dirty(); // console.timeEnd('prefilter'); } frameBuffer.dispose(renderer); renderTargetTmp.dispose(renderer); skyEnv.dispose(renderer); // Remove gpu resource allucated in renderer normalDistribution.dispose(renderer); // renderer.dispose(); return { environmentMap: prefilteredCubeMap, brdfLookup: brdfLookup, normalDistribution: normalDistribution, maxMipmapLevel: mipmapNum }; }; cubemapUtil.integrateBRDF = function (renderer, normalDistribution) { normalDistribution = normalDistribution || cubemapUtil.generateNormalDistribution(); var framebuffer = new FrameBuffer({ depthBuffer: false }); var pass = new Pass({ fragment: integrateBRDFShaderCode }); var texture = new Texture2D({ width: 512, height: 256, type: Texture.HALF_FLOAT, wrapS: Texture.CLAMP_TO_EDGE, wrapT: Texture.CLAMP_TO_EDGE, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, useMipmap: false }); pass.setUniform('normalDistribution', normalDistribution); pass.setUniform('viewportSize', [512, 256]); pass.attachOutput(texture); pass.render(renderer, framebuffer); // FIXME Only chrome and firefox can readPixels with float type. // framebuffer.bind(renderer); // var pixels = new Float32Array(512 * 256 * 4); // renderer.gl.readPixels( // 0, 0, texture.width, texture.height, // Texture.RGBA, Texture.FLOAT, pixels // ); // texture.pixels = pixels; // texture.flipY = false; // texture.dirty(); // framebuffer.unbind(renderer); framebuffer.dispose(renderer); return texture; }; cubemapUtil.generateNormalDistribution = function (roughnessLevels, sampleSize) { // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html // GLSL not support bit operation, use lookup instead // V -> i / N, U -> roughness var roughnessLevels = roughnessLevels || 256; var sampleSize = sampleSize || 1024; var normalDistribution = new Texture2D({ width: roughnessLevels, height: sampleSize, type: Texture.FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, wrapS: Texture.CLAMP_TO_EDGE, wrapT: Texture.CLAMP_TO_EDGE, useMipmap: false }); var pixels = new Float32Array(sampleSize * roughnessLevels * 4); var tmp = []; // function sortFunc(a, b) { // return Math.abs(b) - Math.abs(a); // } for (var j = 0; j < roughnessLevels; j++) { var roughness = j / roughnessLevels; var a = roughness * roughness; for (var i = 0; i < sampleSize; i++) { // http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators // http://stackoverflow.com/questions/1908492/unsigned-integer-in-javascript // http://stackoverflow.com/questions/1822350/what-is-the-javascript-operator-and-how-do-you-use-it var y = (i << 16 | i >>> 16) >>> 0; y = ((y & 1431655765) << 1 | (y & 2863311530) >>> 1) >>> 0; y = ((y & 858993459) << 2 | (y & 3435973836) >>> 2) >>> 0; y = ((y & 252645135) << 4 | (y & 4042322160) >>> 4) >>> 0; y = (((y & 16711935) << 8 | (y & 4278255360) >>> 8) >>> 0) / 4294967296; // CDF var cosTheta = Math.sqrt((1 - y) / (1 + (a * a - 1.0) * y)); tmp[i] = cosTheta; } for (var i = 0; i < sampleSize; i++) { var offset = (i * roughnessLevels + j) * 4; var cosTheta = tmp[i]; var sinTheta = Math.sqrt(1.0 - cosTheta * cosTheta); var x = i / sampleSize; var phi = 2.0 * Math.PI * x; pixels[offset] = sinTheta * Math.cos(phi); pixels[offset + 1] = cosTheta; pixels[offset + 2] = sinTheta * Math.sin(phi); pixels[offset + 3] = 1.0; } } normalDistribution.pixels = pixels; return normalDistribution; }; // https://docs.unrealengine.com/latest/INT/Engine/Rendering/LightingAndShadows/AmbientCubemap/ /** * Ambient cubemap light provides specular parts of Image Based Lighting. * Which is a basic requirement for Physically Based Rendering * @constructor clay.light.AmbientCubemap * @extends clay.Light */ var AmbientCubemapLight = Light.extend({ /** * @type {clay.TextureCube} * @memberOf clay.light.AmbientCubemap# */ cubemap: null, // TODO // range: 100, castShadow: false, _normalDistribution: null, _brdfLookup: null }, /** @lends clay.light.AmbientCubemap# */ { type: 'AMBIENT_CUBEMAP_LIGHT', /** * Do prefitering the cubemap * @param {clay.Renderer} renderer * @param {number} [size=32] */ prefilter: function (renderer, size) { if (!renderer.getGLExtension('EXT_shader_texture_lod')) { console.warn('Device not support textureCubeLodEXT'); return; } if (!this._brdfLookup) { this._normalDistribution = cubemapUtil.generateNormalDistribution(); this._brdfLookup = cubemapUtil.integrateBRDF(renderer, this._normalDistribution); } var cubemap = this.cubemap; if (cubemap.__prefiltered) { return; } var result = cubemapUtil.prefilterEnvironmentMap( renderer, cubemap, { encodeRGBM: true, width: size, height: size }, this._normalDistribution, this._brdfLookup ); this.cubemap = result.environmentMap; this.cubemap.__prefiltered = true; cubemap.dispose(renderer); }, getBRDFLookup: function () { return this._brdfLookup; }, uniformTemplates: { ambientCubemapLightColor: { type: '3f', value: function (instance) { var color = instance.color; var intensity = instance.intensity; return [color[0]*intensity, color[1]*intensity, color[2]*intensity]; } }, ambientCubemapLightCubemap: { type: 't', value: function (instance) { return instance.cubemap; } }, ambientCubemapLightBRDFLookup: { type: 't', value: function (instance) { return instance._brdfLookup; } } } /** * @function * @name clone * @return {clay.light.AmbientCubemap} * @memberOf clay.light.AmbientCubemap.prototype */ }); /** * Spherical Harmonic Ambient Light * @constructor clay.light.AmbientSH * @extends clay.Light */ var AmbientSHLight = Light.extend({ castShadow: false, /** * Spherical Harmonic Coefficients * @type {Array.<number>} * @memberOf clay.light.AmbientSH# */ coefficients: [], }, function () { this._coefficientsTmpArr = new vendor.Float32Array(9 * 3); }, { type: 'AMBIENT_SH_LIGHT', uniformTemplates: { ambientSHLightColor: { type: '3f', value: function (instance) { var color = instance.color; var intensity = instance.intensity; return [color[0] * intensity, color[1] * intensity, color[2] * intensity]; } }, ambientSHLightCoefficients: { type: '3f', value: function (instance) { var coefficientsTmpArr = instance._coefficientsTmpArr; for (var i = 0; i < instance.coefficients.length; i++) { coefficientsTmpArr[i] = instance.coefficients[i]; } return coefficientsTmpArr; } } } /** * @function * @name clone * @return {clay.light.Ambient} * @memberOf clay.light.Ambient.prototype */ }); var TexturePool = function () { this._pool = {}; this._allocatedTextures = []; }; TexturePool.prototype = { constructor: TexturePool, get: function (parameters) { var key = generateKey(parameters); if (!this._pool.hasOwnProperty(key)) { this._pool[key] = []; } var list = this._pool[key]; if (!list.length) { var texture = new Texture2D(parameters); this._allocatedTextures.push(texture); return texture; } return list.pop(); }, put: function (texture) { var key = generateKey(texture); if (!this._pool.hasOwnProperty(key)) { this._pool[key] = []; } var list = this._pool[key]; list.push(texture); }, clear: function (renderer) { for (var i = 0; i < this._allocatedTextures.length; i++) { this._allocatedTextures[i].dispose(renderer); } this._pool = {}; this._allocatedTextures = []; } }; var defaultParams = { width: 512, height: 512, type: glenum.UNSIGNED_BYTE, format: glenum.RGBA, wrapS: glenum.CLAMP_TO_EDGE, wrapT: glenum.CLAMP_TO_EDGE, minFilter: glenum.LINEAR_MIPMAP_LINEAR, magFilter: glenum.LINEAR, useMipmap: true, anisotropic: 1, flipY: true, unpackAlignment: 4, premultiplyAlpha: false }; var defaultParamPropList = Object.keys(defaultParams); function generateKey(parameters) { util$1.defaultsWithPropList(parameters, defaultParams, defaultParamPropList); fallBack(parameters); var key = ''; for (var i = 0; i < defaultParamPropList.length; i++) { var name = defaultParamPropList[i]; var chunk = parameters[name].toString(); key += chunk; } return key; } function fallBack(target) { var IPOT = isPowerOfTwo$2(target.width, target.height); if (target.format === glenum.DEPTH_COMPONENT) { target.useMipmap = false; } if (!IPOT || !target.useMipmap) { if (target.minFilter == glenum.NEAREST_MIPMAP_NEAREST || target.minFilter == glenum.NEAREST_MIPMAP_LINEAR) { target.minFilter = glenum.NEAREST; } else if ( target.minFilter == glenum.LINEAR_MIPMAP_LINEAR || target.minFilter == glenum.LINEAR_MIPMAP_NEAREST ) { target.minFilter = glenum.LINEAR; } } if (!IPOT) { target.wrapS = glenum.CLAMP_TO_EDGE; target.wrapT = glenum.CLAMP_TO_EDGE; } } function isPowerOfTwo$2(width, height) { return (width & (width-1)) === 0 && (height & (height-1)) === 0; } var shadowmapEssl = "@export clay.sm.depth.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\nattribute vec2 texcoord : TEXCOORD_0;\nuniform vec2 uvRepeat = vec2(1.0, 1.0);\nuniform vec2 uvOffset = vec2(0.0, 0.0);\n@import clay.chunk.skinning_header\n@import clay.chunk.instancing_header\nvarying vec4 v_ViewPosition;\nvarying vec2 v_Texcoord;\nvoid main(){\n vec4 P = vec4(position, 1.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n P = skinMatrixWS * P;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n P = instanceMat * P;\n#endif\n v_ViewPosition = worldViewProjection * P;\n gl_Position = v_ViewPosition;\n v_Texcoord = texcoord * uvRepeat + uvOffset;\n}\n@end\n@export clay.sm.depth.fragment\nvarying vec4 v_ViewPosition;\nvarying vec2 v_Texcoord;\nuniform float bias : 0.001;\nuniform float slopeScale : 1.0;\nuniform sampler2D alphaMap;\nuniform float alphaCutoff: 0.0;\n@import clay.util.encode_float\nvoid main(){\n float depth = v_ViewPosition.z / v_ViewPosition.w;\n if (alphaCutoff > 0.0) {\n if (texture2D(alphaMap, v_Texcoord).a <= alphaCutoff) {\n discard;\n }\n }\n#ifdef USE_VSM\n depth = depth * 0.5 + 0.5;\n float moment1 = depth;\n float moment2 = depth * depth;\n #ifdef SUPPORT_STANDARD_DERIVATIVES\n float dx = dFdx(depth);\n float dy = dFdy(depth);\n moment2 += 0.25*(dx*dx+dy*dy);\n #endif\n gl_FragColor = vec4(moment1, moment2, 0.0, 1.0);\n#else\n #ifdef SUPPORT_STANDARD_DERIVATIVES\n float dx = dFdx(depth);\n float dy = dFdy(depth);\n depth += sqrt(dx*dx + dy*dy) * slopeScale + bias;\n #else\n depth += bias;\n #endif\n gl_FragColor = encodeFloat(depth * 0.5 + 0.5);\n#endif\n}\n@end\n@export clay.sm.debug_depth\nuniform sampler2D depthMap;\nvarying vec2 v_Texcoord;\n@import clay.util.decode_float\nvoid main() {\n vec4 tex = texture2D(depthMap, v_Texcoord);\n#ifdef USE_VSM\n gl_FragColor = vec4(tex.rgb, 1.0);\n#else\n float depth = decodeFloat(tex);\n gl_FragColor = vec4(depth, depth, depth, 1.0);\n#endif\n}\n@end\n@export clay.sm.distance.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nuniform mat4 world : WORLD;\nattribute vec3 position : POSITION;\n@import clay.chunk.skinning_header\nvarying vec3 v_WorldPosition;\nvoid main (){\n vec4 P = vec4(position, 1.0);\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n P = skinMatrixWS * P;\n#endif\n#ifdef INSTANCING\n @import clay.chunk.instancing_matrix\n P = instanceMat * P;\n#endif\n gl_Position = worldViewProjection * P;\n v_WorldPosition = (world * P).xyz;\n}\n@end\n@export clay.sm.distance.fragment\nuniform vec3 lightPosition;\nuniform float range : 100;\nvarying vec3 v_WorldPosition;\n@import clay.util.encode_float\nvoid main(){\n float dist = distance(lightPosition, v_WorldPosition);\n#ifdef USE_VSM\n gl_FragColor = vec4(dist, dist * dist, 0.0, 0.0);\n#else\n dist = dist / range;\n gl_FragColor = encodeFloat(dist);\n#endif\n}\n@end\n@export clay.plugin.shadow_map_common\n@import clay.util.decode_float\nfloat tapShadowMap(sampler2D map, vec2 uv, float z){\n vec4 tex = texture2D(map, uv);\n return step(z, decodeFloat(tex) * 2.0 - 1.0);\n}\nfloat pcf(sampler2D map, vec2 uv, float z, float textureSize, vec2 scale) {\n float shadowContrib = tapShadowMap(map, uv, z);\n vec2 offset = vec2(1.0 / textureSize) * scale;\n#ifdef PCF_KERNEL_SIZE\n for (int _idx_ = 0; _idx_ < PCF_KERNEL_SIZE; _idx_++) {{\n shadowContrib += tapShadowMap(map, uv + offset * pcfKernel[_idx_], z);\n }}\n return shadowContrib / float(PCF_KERNEL_SIZE + 1);\n#else\n shadowContrib += tapShadowMap(map, uv+vec2(offset.x, 0.0), z);\n shadowContrib += tapShadowMap(map, uv+vec2(offset.x, offset.y), z);\n shadowContrib += tapShadowMap(map, uv+vec2(-offset.x, offset.y), z);\n shadowContrib += tapShadowMap(map, uv+vec2(0.0, offset.y), z);\n shadowContrib += tapShadowMap(map, uv+vec2(-offset.x, 0.0), z);\n shadowContrib += tapShadowMap(map, uv+vec2(-offset.x, -offset.y), z);\n shadowContrib += tapShadowMap(map, uv+vec2(offset.x, -offset.y), z);\n shadowContrib += tapShadowMap(map, uv+vec2(0.0, -offset.y), z);\n return shadowContrib / 9.0;\n#endif\n}\nfloat pcf(sampler2D map, vec2 uv, float z, float textureSize) {\n return pcf(map, uv, z, textureSize, vec2(1.0));\n}\nfloat chebyshevUpperBound(vec2 moments, float z){\n float p = 0.0;\n z = z * 0.5 + 0.5;\n if (z <= moments.x) {\n p = 1.0;\n }\n float variance = moments.y - moments.x * moments.x;\n variance = max(variance, 0.0000001);\n float mD = moments.x - z;\n float pMax = variance / (variance + mD * mD);\n pMax = clamp((pMax-0.4)/(1.0-0.4), 0.0, 1.0);\n return max(p, pMax);\n}\nfloat computeShadowContrib(\n sampler2D map, mat4 lightVPM, vec3 position, float textureSize, vec2 scale, vec2 offset\n) {\n vec4 posInLightSpace = lightVPM * vec4(position, 1.0);\n posInLightSpace.xyz /= posInLightSpace.w;\n float z = posInLightSpace.z;\n if(all(greaterThan(posInLightSpace.xyz, vec3(-0.99, -0.99, -1.0))) &&\n all(lessThan(posInLightSpace.xyz, vec3(0.99, 0.99, 1.0)))){\n vec2 uv = (posInLightSpace.xy+1.0) / 2.0;\n #ifdef USE_VSM\n vec2 moments = texture2D(map, uv * scale + offset).xy;\n return chebyshevUpperBound(moments, z);\n #else\n return pcf(map, uv * scale + offset, z, textureSize, scale);\n #endif\n }\n return 1.0;\n}\nfloat computeShadowContrib(sampler2D map, mat4 lightVPM, vec3 position, float textureSize) {\n return computeShadowContrib(map, lightVPM, position, textureSize, vec2(1.0), vec2(0.0));\n}\nfloat computeShadowContribOmni(samplerCube map, vec3 direction, float range)\n{\n float dist = length(direction);\n vec4 shadowTex = textureCube(map, direction);\n#ifdef USE_VSM\n vec2 moments = shadowTex.xy;\n float variance = moments.y - moments.x * moments.x;\n float mD = moments.x - dist;\n float p = variance / (variance + mD * mD);\n if(moments.x + 0.001 < dist){\n return clamp(p, 0.0, 1.0);\n }else{\n return 1.0;\n }\n#else\n return step(dist, (decodeFloat(shadowTex) + 0.0002) * range);\n#endif\n}\n@end\n@export clay.plugin.compute_shadow_map\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT) || defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT) || defined(POINT_LIGHT_SHADOWMAP_COUNT)\n#ifdef SPOT_LIGHT_SHADOWMAP_COUNT\nuniform sampler2D spotLightShadowMaps[SPOT_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\nuniform mat4 spotLightMatrices[SPOT_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\nuniform float spotLightShadowMapSizes[SPOT_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\n#endif\n#ifdef DIRECTIONAL_LIGHT_SHADOWMAP_COUNT\n#if defined(SHADOW_CASCADE)\nuniform sampler2D directionalLightShadowMaps[1]:unconfigurable;\nuniform mat4 directionalLightMatrices[SHADOW_CASCADE]:unconfigurable;\nuniform float directionalLightShadowMapSizes[1]:unconfigurable;\nuniform float shadowCascadeClipsNear[SHADOW_CASCADE]:unconfigurable;\nuniform float shadowCascadeClipsFar[SHADOW_CASCADE]:unconfigurable;\n#else\nuniform sampler2D directionalLightShadowMaps[DIRECTIONAL_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\nuniform mat4 directionalLightMatrices[DIRECTIONAL_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\nuniform float directionalLightShadowMapSizes[DIRECTIONAL_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\n#endif\n#endif\n#ifdef POINT_LIGHT_SHADOWMAP_COUNT\nuniform samplerCube pointLightShadowMaps[POINT_LIGHT_SHADOWMAP_COUNT]:unconfigurable;\n#endif\nuniform bool shadowEnabled : true;\n#ifdef PCF_KERNEL_SIZE\nuniform vec2 pcfKernel[PCF_KERNEL_SIZE];\n#endif\n@import clay.plugin.shadow_map_common\n#if defined(SPOT_LIGHT_SHADOWMAP_COUNT)\nvoid computeShadowOfSpotLights(vec3 position, inout float shadowContribs[SPOT_LIGHT_COUNT] ) {\n float shadowContrib;\n for(int _idx_ = 0; _idx_ < SPOT_LIGHT_SHADOWMAP_COUNT; _idx_++) {{\n shadowContrib = computeShadowContrib(\n spotLightShadowMaps[_idx_], spotLightMatrices[_idx_], position,\n spotLightShadowMapSizes[_idx_]\n );\n shadowContribs[_idx_] = shadowContrib;\n }}\n for(int _idx_ = SPOT_LIGHT_SHADOWMAP_COUNT; _idx_ < SPOT_LIGHT_COUNT; _idx_++){{\n shadowContribs[_idx_] = 1.0;\n }}\n}\n#endif\n#if defined(DIRECTIONAL_LIGHT_SHADOWMAP_COUNT)\n#ifdef SHADOW_CASCADE\nvoid computeShadowOfDirectionalLights(vec3 position, inout float shadowContribs[DIRECTIONAL_LIGHT_COUNT]){\n float depth = (2.0 * gl_FragCoord.z - gl_DepthRange.near - gl_DepthRange.far)\n / (gl_DepthRange.far - gl_DepthRange.near);\n float shadowContrib;\n shadowContribs[0] = 1.0;\n for (int _idx_ = 0; _idx_ < SHADOW_CASCADE; _idx_++) {{\n if (\n depth >= shadowCascadeClipsNear[_idx_] &&\n depth <= shadowCascadeClipsFar[_idx_]\n ) {\n shadowContrib = computeShadowContrib(\n directionalLightShadowMaps[0], directionalLightMatrices[_idx_], position,\n directionalLightShadowMapSizes[0],\n vec2(1.0 / float(SHADOW_CASCADE), 1.0),\n vec2(float(_idx_) / float(SHADOW_CASCADE), 0.0)\n );\n shadowContribs[0] = shadowContrib;\n }\n }}\n for(int _idx_ = DIRECTIONAL_LIGHT_SHADOWMAP_COUNT; _idx_ < DIRECTIONAL_LIGHT_COUNT; _idx_++) {{\n shadowContribs[_idx_] = 1.0;\n }}\n}\n#else\nvoid computeShadowOfDirectionalLights(vec3 position, inout float shadowContribs[DIRECTIONAL_LIGHT_COUNT]){\n float shadowContrib;\n for(int _idx_ = 0; _idx_ < DIRECTIONAL_LIGHT_SHADOWMAP_COUNT; _idx_++) {{\n shadowContrib = computeShadowContrib(\n directionalLightShadowMaps[_idx_], directionalLightMatrices[_idx_], position,\n directionalLightShadowMapSizes[_idx_]\n );\n shadowContribs[_idx_] = shadowContrib;\n }}\n for(int _idx_ = DIRECTIONAL_LIGHT_SHADOWMAP_COUNT; _idx_ < DIRECTIONAL_LIGHT_COUNT; _idx_++) {{\n shadowContribs[_idx_] = 1.0;\n }}\n}\n#endif\n#endif\n#if defined(POINT_LIGHT_SHADOWMAP_COUNT)\nvoid computeShadowOfPointLights(vec3 position, inout float shadowContribs[POINT_LIGHT_COUNT] ){\n vec3 lightPosition;\n vec3 direction;\n for(int _idx_ = 0; _idx_ < POINT_LIGHT_SHADOWMAP_COUNT; _idx_++) {{\n lightPosition = pointLightPosition[_idx_];\n direction = position - lightPosition;\n shadowContribs[_idx_] = computeShadowContribOmni(pointLightShadowMaps[_idx_], direction, pointLightRange[_idx_]);\n }}\n for(int _idx_ = POINT_LIGHT_SHADOWMAP_COUNT; _idx_ < POINT_LIGHT_COUNT; _idx_++) {{\n shadowContribs[_idx_] = 1.0;\n }}\n}\n#endif\n#endif\n@end"; var targets$2 = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; Shader['import'](shadowmapEssl); function getDepthMaterialUniform(renderable, depthMaterial, symbol) { if (symbol === 'alphaMap') { return renderable.material.get('diffuseMap'); } else if (symbol === 'alphaCutoff') { if (renderable.material.isDefined('fragment', 'ALPHA_TEST') && renderable.material.get('diffuseMap') ) { var alphaCutoff = renderable.material.get('alphaCutoff'); return alphaCutoff || 0; } return 0; } else if (symbol === 'uvRepeat') { return renderable.material.get('uvRepeat'); } else if (symbol === 'uvOffset') { return renderable.material.get('uvOffset'); } else { return depthMaterial.get(symbol); } } function isDepthMaterialChanged(renderable, prevRenderable) { var matA = renderable.material; var matB = prevRenderable.material; return matA.get('diffuseMap') !== matB.get('diffuseMap') || (matA.get('alphaCutoff') || 0) !== (matB.get('alphaCutoff') || 0); } /** * Pass rendering shadow map. * * @constructor clay.prePass.ShadowMap * @extends clay.core.Base * @example * var shadowMapPass = new clay.prePass.ShadowMap({ * softShadow: clay.prePass.ShadowMap.VSM * }); * ... * animation.on('frame', function (frameTime) { * shadowMapPass.render(renderer, scene, camera); * renderer.render(scene, camera); * }); */ var ShadowMapPass = Base.extend(function () { return /** @lends clay.prePass.ShadowMap# */ { /** * Soft shadow technique. * Can be {@link clay.prePass.ShadowMap.PCF} or {@link clay.prePass.ShadowMap.VSM} * @type {number} */ softShadow: ShadowMapPass.PCF, /** * Soft shadow blur size * @type {number} */ shadowBlur: 1.0, lightFrustumBias: 'auto', kernelPCF: new Float32Array([ 1, 0, 1, 1, -1, 1, 0, 1, -1, 0, -1, -1, 1, -1, 0, -1 ]), precision: 'highp', _lastRenderNotCastShadow: false, _frameBuffer: new FrameBuffer(), _textures: {}, _shadowMapNumber: { 'POINT_LIGHT': 0, 'DIRECTIONAL_LIGHT': 0, 'SPOT_LIGHT': 0 }, _depthMaterials: {}, _distanceMaterials: {}, _receivers: [], _lightsCastShadow: [], _lightCameras: {}, _lightMaterials: {}, _texturePool: new TexturePool() }; }, function () { // Gaussian filter pass for VSM this._gaussianPassH = new Pass({ fragment: Shader.source('clay.compositor.gaussian_blur') }); this._gaussianPassV = new Pass({ fragment: Shader.source('clay.compositor.gaussian_blur') }); this._gaussianPassH.setUniform('blurSize', this.shadowBlur); this._gaussianPassH.setUniform('blurDir', 0.0); this._gaussianPassV.setUniform('blurSize', this.shadowBlur); this._gaussianPassV.setUniform('blurDir', 1.0); this._outputDepthPass = new Pass({ fragment: Shader.source('clay.sm.debug_depth') }); }, { /** * Render scene to shadow textures * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {clay.Camera} sceneCamera * @param {boolean} [notUpdateScene=false] * @memberOf clay.prePass.ShadowMap.prototype */ render: function (renderer, scene, sceneCamera, notUpdateScene) { if (!sceneCamera) { sceneCamera = scene.getMainCamera(); } this.trigger('beforerender', this, renderer, scene, sceneCamera); this._renderShadowPass(renderer, scene, sceneCamera, notUpdateScene); this.trigger('afterrender', this, renderer, scene, sceneCamera); }, /** * Debug rendering of shadow textures * @param {clay.Renderer} renderer * @param {number} size * @memberOf clay.prePass.ShadowMap.prototype */ renderDebug: function (renderer, size) { renderer.saveClear(); var viewport = renderer.viewport; var x = 0, y = 0; var width = size || viewport.width / 4; var height = width; if (this.softShadow === ShadowMapPass.VSM) { this._outputDepthPass.material.define('fragment', 'USE_VSM'); } else { this._outputDepthPass.material.undefine('fragment', 'USE_VSM'); } for (var name in this._textures) { var texture = this._textures[name]; renderer.setViewport(x, y, width * texture.width / texture.height, height); this._outputDepthPass.setUniform('depthMap', texture); this._outputDepthPass.render(renderer); x += width * texture.width / texture.height; } renderer.setViewport(viewport); renderer.restoreClear(); }, _updateReceivers: function (renderer, mesh) { if (mesh.receiveShadow) { this._receivers.push(mesh); mesh.material.set('shadowEnabled', 1); mesh.material.set('pcfKernel', this.kernelPCF); } else { mesh.material.set('shadowEnabled', 0); } if (this.softShadow === ShadowMapPass.VSM) { mesh.material.define('fragment', 'USE_VSM'); mesh.material.undefine('fragment', 'PCF_KERNEL_SIZE'); } else { mesh.material.undefine('fragment', 'USE_VSM'); var kernelPCF = this.kernelPCF; if (kernelPCF && kernelPCF.length) { mesh.material.define('fragment', 'PCF_KERNEL_SIZE', kernelPCF.length / 2); } else { mesh.material.undefine('fragment', 'PCF_KERNEL_SIZE'); } } }, _update: function (renderer, scene) { var self = this; scene.traverse(function (renderable) { if (renderable.isRenderable()) { self._updateReceivers(renderer, renderable); } }); for (var i = 0; i < scene.lights.length; i++) { var light = scene.lights[i]; if (light.castShadow && !light.invisible) { this._lightsCastShadow.push(light); } } }, _renderShadowPass: function (renderer, scene, sceneCamera, notUpdateScene) { // reset for (var name in this._shadowMapNumber) { this._shadowMapNumber[name] = 0; } this._lightsCastShadow.length = 0; this._receivers.length = 0; var _gl = renderer.gl; if (!notUpdateScene) { scene.update(); } if (sceneCamera) { sceneCamera.update(); } scene.updateLights(); this._update(renderer, scene); // Needs to update the receivers again if shadows come from 1 to 0. if (!this._lightsCastShadow.length && this._lastRenderNotCastShadow) { return; } this._lastRenderNotCastShadow = this._lightsCastShadow === 0; _gl.enable(_gl.DEPTH_TEST); _gl.depthMask(true); _gl.disable(_gl.BLEND); // Clear with high-z, so the part not rendered will not been shadowed // TODO // TODO restore _gl.clearColor(1.0, 1.0, 1.0, 1.0); // Shadow uniforms var spotLightShadowMaps = []; var spotLightMatrices = []; var directionalLightShadowMaps = []; var directionalLightMatrices = []; var shadowCascadeClips = []; var pointLightShadowMaps = []; var dirLightHasCascade; // Create textures for shadow map for (var i = 0; i < this._lightsCastShadow.length; i++) { var light = this._lightsCastShadow[i]; if (light.type === 'DIRECTIONAL_LIGHT') { if (dirLightHasCascade) { console.warn('Only one direectional light supported with shadow cascade'); continue; } if (light.shadowCascade > 4) { console.warn('Support at most 4 cascade'); continue; } if (light.shadowCascade > 1) { dirLightHasCascade = light; } this.renderDirectionalLightShadow( renderer, scene, sceneCamera, light, shadowCascadeClips, directionalLightMatrices, directionalLightShadowMaps ); } else if (light.type === 'SPOT_LIGHT') { this.renderSpotLightShadow( renderer, scene, light, spotLightMatrices, spotLightShadowMaps ); } else if (light.type === 'POINT_LIGHT') { this.renderPointLightShadow( renderer, scene, light, pointLightShadowMaps ); } this._shadowMapNumber[light.type]++; } for (var lightType in this._shadowMapNumber) { var number = this._shadowMapNumber[lightType]; var key = lightType + '_SHADOWMAP_COUNT'; for (var i = 0; i < this._receivers.length; i++) { var mesh = this._receivers[i]; var material = mesh.material; if (material.fragmentDefines[key] !== number) { if (number > 0) { material.define('fragment', key, number); } else if (material.isDefined('fragment', key)) { material.undefine('fragment', key); } } } } for (var i = 0; i < this._receivers.length; i++) { var mesh = this._receivers[i]; var material = mesh.material; if (dirLightHasCascade) { material.define('fragment', 'SHADOW_CASCADE', dirLightHasCascade.shadowCascade); } else { material.undefine('fragment', 'SHADOW_CASCADE'); } } var shadowUniforms = scene.shadowUniforms; function getSize(texture) { return texture.height; } if (directionalLightShadowMaps.length > 0) { var directionalLightShadowMapSizes = directionalLightShadowMaps.map(getSize); shadowUniforms.directionalLightShadowMaps = { value: directionalLightShadowMaps, type: 'tv' }; shadowUniforms.directionalLightMatrices = { value: directionalLightMatrices, type: 'm4v' }; shadowUniforms.directionalLightShadowMapSizes = { value: directionalLightShadowMapSizes, type: '1fv' }; if (dirLightHasCascade) { var shadowCascadeClipsNear = shadowCascadeClips.slice(); var shadowCascadeClipsFar = shadowCascadeClips.slice(); shadowCascadeClipsNear.pop(); shadowCascadeClipsFar.shift(); // Iterate from far to near shadowCascadeClipsNear.reverse(); shadowCascadeClipsFar.reverse(); // directionalLightShadowMaps.reverse(); directionalLightMatrices.reverse(); shadowUniforms.shadowCascadeClipsNear = { value: shadowCascadeClipsNear, type: '1fv' }; shadowUniforms.shadowCascadeClipsFar = { value: shadowCascadeClipsFar, type: '1fv' }; } } if (spotLightShadowMaps.length > 0) { var spotLightShadowMapSizes = spotLightShadowMaps.map(getSize); var shadowUniforms = scene.shadowUniforms; shadowUniforms.spotLightShadowMaps = { value: spotLightShadowMaps, type: 'tv' }; shadowUniforms.spotLightMatrices = { value: spotLightMatrices, type: 'm4v' }; shadowUniforms.spotLightShadowMapSizes = { value: spotLightShadowMapSizes, type: '1fv' }; } if (pointLightShadowMaps.length > 0) { shadowUniforms.pointLightShadowMaps = { value: pointLightShadowMaps, type: 'tv' }; } }, renderDirectionalLightShadow: (function () { var splitFrustum = new Frustum(); var splitProjMatrix = new Matrix4(); var cropBBox = new BoundingBox(); var cropMatrix = new Matrix4(); var lightViewMatrix = new Matrix4(); var lightViewProjMatrix = new Matrix4(); var lightProjMatrix = new Matrix4(); return function (renderer, scene, sceneCamera, light, shadowCascadeClips, directionalLightMatrices, directionalLightShadowMaps) { var defaultShadowMaterial = this._getDepthMaterial(light); var passConfig = { getMaterial: function (renderable) { return renderable.shadowDepthMaterial || defaultShadowMaterial; }, isMaterialChanged: isDepthMaterialChanged, getUniform: getDepthMaterialUniform, ifRender: function (renderable) { return renderable.castShadow; }, sortCompare: Renderer.opaqueSortCompare }; // First frame if (!scene.viewBoundingBoxLastFrame.isFinite()) { var boundingBox = scene.getBoundingBox(); scene.viewBoundingBoxLastFrame .copy(boundingBox).applyTransform(sceneCamera.viewMatrix); } // Considering moving speed since the bounding box is from last frame // TODO: add a bias var clippedFar = Math.min(-scene.viewBoundingBoxLastFrame.min.z, sceneCamera.far); var clippedNear = Math.max(-scene.viewBoundingBoxLastFrame.max.z, sceneCamera.near); var lightCamera = this._getDirectionalLightCamera(light, scene, sceneCamera); var lvpMat4Arr = lightViewProjMatrix.array; lightProjMatrix.copy(lightCamera.projectionMatrix); mat4.invert(lightViewMatrix.array, lightCamera.worldTransform.array); mat4.multiply(lightViewMatrix.array, lightViewMatrix.array, sceneCamera.worldTransform.array); mat4.multiply(lvpMat4Arr, lightProjMatrix.array, lightViewMatrix.array); var clipPlanes = []; var isPerspective = sceneCamera instanceof Perspective$1; var scaleZ = (sceneCamera.near + sceneCamera.far) / (sceneCamera.near - sceneCamera.far); var offsetZ = 2 * sceneCamera.near * sceneCamera.far / (sceneCamera.near - sceneCamera.far); for (var i = 0; i <= light.shadowCascade; i++) { var clog = clippedNear * Math.pow(clippedFar / clippedNear, i / light.shadowCascade); var cuni = clippedNear + (clippedFar - clippedNear) * i / light.shadowCascade; var c = clog * light.cascadeSplitLogFactor + cuni * (1 - light.cascadeSplitLogFactor); clipPlanes.push(c); shadowCascadeClips.push(-(-c * scaleZ + offsetZ) / -c); } var texture = this._getTexture(light, light.shadowCascade); directionalLightShadowMaps.push(texture); var viewport = renderer.viewport; var _gl = renderer.gl; this._frameBuffer.attach(texture); this._frameBuffer.bind(renderer); _gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT); for (var i = 0; i < light.shadowCascade; i++) { // Get the splitted frustum var nearPlane = clipPlanes[i]; var farPlane = clipPlanes[i + 1]; if (isPerspective) { mat4.perspective(splitProjMatrix.array, sceneCamera.fov / 180 * Math.PI, sceneCamera.aspect, nearPlane, farPlane); } else { mat4.ortho( splitProjMatrix.array, sceneCamera.left, sceneCamera.right, sceneCamera.bottom, sceneCamera.top, nearPlane, farPlane ); } splitFrustum.setFromProjection(splitProjMatrix); splitFrustum.getTransformedBoundingBox(cropBBox, lightViewMatrix); cropBBox.applyProjection(lightProjMatrix); var _min = cropBBox.min.array; var _max = cropBBox.max.array; _min[0] = Math.max(_min[0], -1); _min[1] = Math.max(_min[1], -1); _max[0] = Math.min(_max[0], 1); _max[1] = Math.min(_max[1], 1); cropMatrix.ortho(_min[0], _max[0], _min[1], _max[1], 1, -1); lightCamera.projectionMatrix.multiplyLeft(cropMatrix); var shadowSize = light.shadowResolution || 512; // Reversed, left to right => far to near renderer.setViewport((light.shadowCascade - i - 1) * shadowSize, 0, shadowSize, shadowSize, 1); var renderList = scene.updateRenderList(lightCamera); renderer.renderPass(renderList.opaque, lightCamera, passConfig); // Filter for VSM if (this.softShadow === ShadowMapPass.VSM) { this._gaussianFilter(renderer, texture, texture.width); } var matrix = new Matrix4(); matrix.copy(lightCamera.viewMatrix) .multiplyLeft(lightCamera.projectionMatrix); directionalLightMatrices.push(matrix.array); lightCamera.projectionMatrix.copy(lightProjMatrix); } this._frameBuffer.unbind(renderer); renderer.setViewport(viewport); }; })(), renderSpotLightShadow: function (renderer, scene, light, spotLightMatrices, spotLightShadowMaps) { var texture = this._getTexture(light); var lightCamera = this._getSpotLightCamera(light); var _gl = renderer.gl; this._frameBuffer.attach(texture); this._frameBuffer.bind(renderer); _gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT); var defaultShadowMaterial = this._getDepthMaterial(light); var passConfig = { getMaterial: function (renderable) { return renderable.shadowDepthMaterial || defaultShadowMaterial; }, isMaterialChanged: isDepthMaterialChanged, getUniform: getDepthMaterialUniform, ifRender: function (renderable) { return renderable.castShadow; }, sortCompare: Renderer.opaqueSortCompare }; var renderList = scene.updateRenderList(lightCamera); renderer.renderPass(renderList.opaque, lightCamera, passConfig); this._frameBuffer.unbind(renderer); // Filter for VSM if (this.softShadow === ShadowMapPass.VSM) { this._gaussianFilter(renderer, texture, texture.width); } var matrix = new Matrix4(); matrix.copy(lightCamera.worldTransform) .invert() .multiplyLeft(lightCamera.projectionMatrix); spotLightShadowMaps.push(texture); spotLightMatrices.push(matrix.array); }, renderPointLightShadow: function (renderer, scene, light, pointLightShadowMaps) { var texture = this._getTexture(light); var _gl = renderer.gl; pointLightShadowMaps.push(texture); var defaultShadowMaterial = this._getDepthMaterial(light); var passConfig = { getMaterial: function (renderable) { return renderable.shadowDepthMaterial || defaultShadowMaterial; }, getUniform: getDepthMaterialUniform, sortCompare: Renderer.opaqueSortCompare }; var renderListEachSide = { px: [], py: [], pz: [], nx: [], ny: [], nz: [] }; var bbox = new BoundingBox(); var lightWorldPosition = light.getWorldPosition().array; var lightBBox = new BoundingBox(); var range = light.range; lightBBox.min.setArray(lightWorldPosition); lightBBox.max.setArray(lightWorldPosition); var extent = new Vector3(range, range, range); lightBBox.max.add(extent); lightBBox.min.sub(extent); var targetsNeedRender = { px: false, py: false, pz: false, nx: false, ny: false, nz: false }; scene.traverse(function (renderable) { if (renderable.isRenderable() && renderable.castShadow) { var geometry = renderable.geometry; if (!geometry.boundingBox) { for (var i = 0; i < targets$2.length; i++) { renderListEachSide[targets$2[i]].push(renderable); } return; } bbox.transformFrom(geometry.boundingBox, renderable.worldTransform); if (!bbox.intersectBoundingBox(lightBBox)) { return; } bbox.updateVertices(); for (var i = 0; i < targets$2.length; i++) { targetsNeedRender[targets$2[i]] = false; } for (var i = 0; i < 8; i++) { var vtx = bbox.vertices[i]; var x = vtx[0] - lightWorldPosition[0]; var y = vtx[1] - lightWorldPosition[1]; var z = vtx[2] - lightWorldPosition[2]; var absx = Math.abs(x); var absy = Math.abs(y); var absz = Math.abs(z); if (absx > absy) { if (absx > absz) { targetsNeedRender[x > 0 ? 'px' : 'nx'] = true; } else { targetsNeedRender[z > 0 ? 'pz' : 'nz'] = true; } } else { if (absy > absz) { targetsNeedRender[y > 0 ? 'py' : 'ny'] = true; } else { targetsNeedRender[z > 0 ? 'pz' : 'nz'] = true; } } } for (var i = 0; i < targets$2.length; i++) { if (targetsNeedRender[targets$2[i]]) { renderListEachSide[targets$2[i]].push(renderable); } } } }); for (var i = 0; i < 6; i++) { var target = targets$2[i]; var camera = this._getPointLightCamera(light, target); this._frameBuffer.attach(texture, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_CUBE_MAP_POSITIVE_X + i); this._frameBuffer.bind(renderer); _gl.clear(_gl.COLOR_BUFFER_BIT | _gl.DEPTH_BUFFER_BIT); renderer.renderPass(renderListEachSide[target], camera, passConfig); } this._frameBuffer.unbind(renderer); }, _getDepthMaterial: function (light) { var shadowMaterial = this._lightMaterials[light.__uid__]; var isPointLight = light.type === 'POINT_LIGHT'; if (!shadowMaterial) { var shaderPrefix = isPointLight ? 'clay.sm.distance.' : 'clay.sm.depth.'; shadowMaterial = new Material({ precision: this.precision, shader: new Shader(Shader.source(shaderPrefix + 'vertex'), Shader.source(shaderPrefix + 'fragment')) }); this._lightMaterials[light.__uid__] = shadowMaterial; } if (light.shadowSlopeScale != null) { shadowMaterial.setUniform('slopeScale', light.shadowSlopeScale); } if (light.shadowBias != null) { shadowMaterial.setUniform('bias', light.shadowBias); } if (this.softShadow === ShadowMapPass.VSM) { shadowMaterial.define('fragment', 'USE_VSM'); } else { shadowMaterial.undefine('fragment', 'USE_VSM'); } if (isPointLight) { shadowMaterial.set('lightPosition', light.getWorldPosition().array); shadowMaterial.set('range', light.range); } return shadowMaterial; }, _gaussianFilter: function (renderer, texture, size) { var parameter = { width: size, height: size, type: Texture.FLOAT }; var tmpTexture = this._texturePool.get(parameter); this._frameBuffer.attach(tmpTexture); this._frameBuffer.bind(renderer); this._gaussianPassH.setUniform('texture', texture); this._gaussianPassH.setUniform('textureWidth', size); this._gaussianPassH.render(renderer); this._frameBuffer.attach(texture); this._gaussianPassV.setUniform('texture', tmpTexture); this._gaussianPassV.setUniform('textureHeight', size); this._gaussianPassV.render(renderer); this._frameBuffer.unbind(renderer); this._texturePool.put(tmpTexture); }, _getTexture: function (light, cascade) { var key = light.__uid__; var texture = this._textures[key]; var resolution = light.shadowResolution || 512; cascade = cascade || 1; if (!texture) { if (light.type === 'POINT_LIGHT') { texture = new TextureCube(); } else { texture = new Texture2D(); } // At most 4 cascade // TODO share with height ? texture.width = resolution * cascade; texture.height = resolution; if (this.softShadow === ShadowMapPass.VSM) { texture.type = Texture.FLOAT; texture.anisotropic = 4; } else { texture.minFilter = glenum.NEAREST; texture.magFilter = glenum.NEAREST; texture.useMipmap = false; } this._textures[key] = texture; } return texture; }, _getPointLightCamera: function (light, target) { if (!this._lightCameras.point) { this._lightCameras.point = { px: new Perspective$1(), nx: new Perspective$1(), py: new Perspective$1(), ny: new Perspective$1(), pz: new Perspective$1(), nz: new Perspective$1() }; } var camera = this._lightCameras.point[target]; camera.far = light.range; camera.fov = 90; camera.position.set(0, 0, 0); switch (target) { case 'px': camera.lookAt(Vector3.POSITIVE_X, Vector3.NEGATIVE_Y); break; case 'nx': camera.lookAt(Vector3.NEGATIVE_X, Vector3.NEGATIVE_Y); break; case 'py': camera.lookAt(Vector3.POSITIVE_Y, Vector3.POSITIVE_Z); break; case 'ny': camera.lookAt(Vector3.NEGATIVE_Y, Vector3.NEGATIVE_Z); break; case 'pz': camera.lookAt(Vector3.POSITIVE_Z, Vector3.NEGATIVE_Y); break; case 'nz': camera.lookAt(Vector3.NEGATIVE_Z, Vector3.NEGATIVE_Y); break; } light.getWorldPosition(camera.position); camera.update(); return camera; }, _getDirectionalLightCamera: (function () { var lightViewMatrix = new Matrix4(); var sceneViewBoundingBox = new BoundingBox(); var lightViewBBox = new BoundingBox(); // Camera of directional light will be adjusted // to contain the view frustum and scene bounding box as tightly as possible return function (light, scene, sceneCamera) { if (!this._lightCameras.directional) { this._lightCameras.directional = new Orthographic$1(); } var camera = this._lightCameras.directional; sceneViewBoundingBox.copy(scene.viewBoundingBoxLastFrame); sceneViewBoundingBox.intersection(sceneCamera.frustum.boundingBox); // Move to the center of frustum(in world space) camera.position .copy(sceneViewBoundingBox.min) .add(sceneViewBoundingBox.max) .scale(0.5) .transformMat4(sceneCamera.worldTransform); camera.rotation.copy(light.rotation); camera.scale.copy(light.scale); camera.updateWorldTransform(); // Transform to light view space Matrix4.invert(lightViewMatrix, camera.worldTransform); Matrix4.multiply(lightViewMatrix, lightViewMatrix, sceneCamera.worldTransform); lightViewBBox.copy(sceneViewBoundingBox).applyTransform(lightViewMatrix); var min = lightViewBBox.min.array; var max = lightViewBBox.max.array; // Move camera to adjust the near to 0 camera.position.set((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, max[2]) .transformMat4(camera.worldTransform); camera.near = 0; camera.far = -min[2] + max[2]; // Make sure receivers not in the frustum will stil receive the shadow. if (isNaN(this.lightFrustumBias)) { camera.far *= 4; } else { camera.far += this.lightFrustumBias; } camera.left = min[0]; camera.right = max[0]; camera.top = max[1]; camera.bottom = min[1]; camera.update(true); return camera; }; })(), _getSpotLightCamera: function (light) { if (!this._lightCameras.spot) { this._lightCameras.spot = new Perspective$1(); } var camera = this._lightCameras.spot; // Update properties camera.fov = light.penumbraAngle * 2; camera.far = light.range; camera.worldTransform.copy(light.worldTransform); camera.updateProjectionMatrix(); mat4.invert(camera.viewMatrix.array, camera.worldTransform.array); return camera; }, /** * @param {clay.Renderer|WebGLRenderingContext} [renderer] * @memberOf clay.prePass.ShadowMap.prototype */ // PENDING Renderer or WebGLRenderingContext dispose: function (renderer) { var _gl = renderer.gl || renderer; if (this._frameBuffer) { this._frameBuffer.dispose(_gl); } for (var name in this._textures) { this._textures[name].dispose(_gl); } this._texturePool.clear(renderer.gl); this._depthMaterials = {}; this._distanceMaterials = {}; this._textures = {}; this._lightCameras = {}; this._shadowMapNumber = { 'POINT_LIGHT': 0, 'DIRECTIONAL_LIGHT': 0, 'SPOT_LIGHT': 0 }; this._meshMaterials = {}; for (var i = 0; i < this._receivers.length; i++) { var mesh = this._receivers[i]; // Mesh may be disposed if (mesh.material) { var material = mesh.material; material.undefine('fragment', 'POINT_LIGHT_SHADOW_COUNT'); material.undefine('fragment', 'DIRECTIONAL_LIGHT_SHADOW_COUNT'); material.undefine('fragment', 'AMBIENT_LIGHT_SHADOW_COUNT'); material.set('shadowEnabled', 0); } } this._receivers = []; this._lightsCastShadow = []; } }); /** * @name clay.prePass.ShadowMap.VSM * @type {number} */ ShadowMapPass.VSM = 1; /** * @name clay.prePass.ShadowMap.PCF * @type {number} */ ShadowMapPass.PCF = 2; /** * @constructor clay.picking.RayPicking * @extends clay.core.Base */ var RayPicking = Base.extend(/** @lends clay.picking.RayPicking# */{ /** * Target scene * @type {clay.Scene} */ scene: null, /** * Target camera * @type {clay.Camera} */ camera: null, /** * Target renderer * @type {clay.Renderer} */ renderer: null }, function () { this._ray = new Ray(); this._ndc = new Vector2(); }, /** @lends clay.picking.RayPicking.prototype */ { /** * Pick the nearest intersection object in the scene * @param {number} x Mouse position x * @param {number} y Mouse position y * @param {boolean} [forcePickAll=false] ignore ignorePicking * @return {clay.picking.RayPicking~Intersection} */ pick: function (x, y, forcePickAll) { var out = this.pickAll(x, y, [], forcePickAll); return out[0] || null; }, /** * Pick all intersection objects, wich will be sorted from near to far * @param {number} x Mouse position x * @param {number} y Mouse position y * @param {Array} [output] * @param {boolean} [forcePickAll=false] ignore ignorePicking * @return {Array.<clay.picking.RayPicking~Intersection>} */ pickAll: function (x, y, output, forcePickAll) { this.renderer.screenToNDC(x, y, this._ndc); this.camera.castRay(this._ndc, this._ray); output = output || []; this._intersectNode(this.scene, output, forcePickAll || false); output.sort(this._intersectionCompareFunc); return output; }, _intersectNode: function (node, out, forcePickAll) { if ((node instanceof Renderable) && node.isRenderable()) { if ((!node.ignorePicking || forcePickAll) && ( // Only triangle mesh support ray picking (node.mode === glenum.TRIANGLES && node.geometry.isUseIndices()) // Or if geometry has it's own pickByRay, pick, implementation || node.geometry.pickByRay || node.geometry.pick ) ) { this._intersectRenderable(node, out); } } for (var i = 0; i < node._children.length; i++) { this._intersectNode(node._children[i], out, forcePickAll); } }, _intersectRenderable: (function () { var v1 = new Vector3(); var v2 = new Vector3(); var v3 = new Vector3(); var ray = new Ray(); var worldInverse = new Matrix4(); return function (renderable, out) { var isSkinnedMesh = renderable.isSkinnedMesh(); ray.copy(this._ray); Matrix4.invert(worldInverse, renderable.worldTransform); // Skinned mesh will ignore the world transform. if (!isSkinnedMesh) { ray.applyTransform(worldInverse); } var geometry = renderable.geometry; var bbox = isSkinnedMesh ? renderable.skeleton.boundingBox : geometry.boundingBox; if (bbox && !ray.intersectBoundingBox(bbox)) { return; } // Use user defined picking algorithm if (geometry.pick) { geometry.pick( this._ndc.x, this._ndc.y, this.renderer, this.camera, renderable, out ); return; } // Use user defined ray picking algorithm else if (geometry.pickByRay) { geometry.pickByRay(ray, renderable, out); return; } var cullBack = (renderable.cullFace === glenum.BACK && renderable.frontFace === glenum.CCW) || (renderable.cullFace === glenum.FRONT && renderable.frontFace === glenum.CW); var point; var indices = geometry.indices; var positionAttr = geometry.attributes.position; var weightAttr = geometry.attributes.weight; var jointAttr = geometry.attributes.joint; var skinMatricesArray; var skinMatrices = []; // Check if valid. if (!positionAttr || !positionAttr.value || !indices) { return; } if (isSkinnedMesh) { skinMatricesArray = renderable.skeleton.getSubSkinMatrices(renderable.__uid__, renderable.joints); for (var i = 0; i < renderable.joints.length; i++) { skinMatrices[i] = skinMatrices[i] || []; for (var k = 0; k < 16; k++) { skinMatrices[i][k] = skinMatricesArray[i * 16 + k]; } } var pos = []; var weight = []; var joint = []; var skinnedPos = []; var tmp = []; var skinnedPositionAttr = geometry.attributes.skinnedPosition; if (!skinnedPositionAttr || !skinnedPositionAttr.value) { geometry.createAttribute('skinnedPosition', 'f', 3); skinnedPositionAttr = geometry.attributes.skinnedPosition; skinnedPositionAttr.init(geometry.vertexCount); } for (var i = 0; i < geometry.vertexCount; i++) { positionAttr.get(i, pos); weightAttr.get(i, weight); jointAttr.get(i, joint); weight[3] = 1 - weight[0] - weight[1] - weight[2]; vec3.set(skinnedPos, 0, 0, 0); for (var k = 0; k < 4; k++) { if (joint[k] >= 0 && weight[k] > 1e-4) { vec3.transformMat4(tmp, pos, skinMatrices[joint[k]]); vec3.scaleAndAdd(skinnedPos, skinnedPos, tmp, weight[k]); } } skinnedPositionAttr.set(i, skinnedPos); } } for (var i = 0; i < indices.length; i += 3) { var i1 = indices[i]; var i2 = indices[i + 1]; var i3 = indices[i + 2]; var finalPosAttr = isSkinnedMesh ? geometry.attributes.skinnedPosition : positionAttr; finalPosAttr.get(i1, v1.array); finalPosAttr.get(i2, v2.array); finalPosAttr.get(i3, v3.array); if (cullBack) { point = ray.intersectTriangle(v1, v2, v3, renderable.culling); } else { point = ray.intersectTriangle(v1, v3, v2, renderable.culling); } if (point) { var pointW = new Vector3(); if (!isSkinnedMesh) { Vector3.transformMat4(pointW, point, renderable.worldTransform); } else { // TODO point maybe not right. Vector3.copy(pointW, point); } out.push(new RayPicking.Intersection( point, pointW, renderable, [i1, i2, i3], i / 3, Vector3.dist(pointW, this._ray.origin) )); } } }; })(), _intersectionCompareFunc: function (a, b) { return a.distance - b.distance; } }); /** * @constructor clay.picking.RayPicking~Intersection * @param {clay.Vector3} point * @param {clay.Vector3} pointWorld * @param {clay.Node} target * @param {Array.<number>} triangle * @param {number} triangleIndex * @param {number} distance */ RayPicking.Intersection = function (point, pointWorld, target, triangle, triangleIndex, distance) { /** * Intersection point in local transform coordinates * @type {clay.Vector3} */ this.point = point; /** * Intersection point in world transform coordinates * @type {clay.Vector3} */ this.pointWorld = pointWorld; /** * Intersection scene node * @type {clay.Node} */ this.target = target; /** * Intersection triangle, which is an array of vertex index * @type {Array.<number>} */ this.triangle = triangle; /** * Index of intersection triangle. */ this.triangleIndex = triangleIndex; /** * Distance from intersection point to ray origin * @type {number} */ this.distance = distance; }; // Spherical Harmonic Helpers var sh = {}; var targets$3 = ['px', 'nx', 'py', 'ny', 'pz', 'nz']; function harmonics(normal, index){ var x = normal[0]; var y = normal[1]; var z = normal[2]; if (index === 0) { return 1.0; } else if (index === 1) { return x; } else if (index === 2) { return y; } else if (index === 3) { return z; } else if (index === 4) { return x * z; } else if (index === 5) { return y * z; } else if (index === 6) { return x * y; } else if (index === 7) { return 3.0 * z * z - 1.0; } else { return x * x - y * y; } } var normalTransform = { px: [2, 1, 0, -1, -1, 1], nx: [2, 1, 0, 1, -1, -1], py: [0, 2, 1, 1, -1, -1], ny: [0, 2, 1, 1, 1, 1], pz: [0, 1, 2, -1, -1, -1], nz: [0, 1, 2, 1, -1, 1] }; // Project on cpu. function projectEnvironmentMapCPU(renderer, cubePixels, width, height) { var coeff = new vendor.Float32Array(9 * 3); var normal = vec3.create(); var texel = vec3.create(); var fetchNormal = vec3.create(); for (var m = 0; m < 9; m++) { var result = vec3.create(); for (var k = 0; k < targets$3.length; k++) { var pixels = cubePixels[targets$3[k]]; var sideResult = vec3.create(); var divider = 0; var i = 0; var transform = normalTransform[targets$3[k]]; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { normal[0] = x / (width - 1.0) * 2.0 - 1.0; // TODO Flip y? normal[1] = y / (height - 1.0) * 2.0 - 1.0; normal[2] = -1.0; vec3.normalize(normal, normal); fetchNormal[0] = normal[transform[0]] * transform[3]; fetchNormal[1] = normal[transform[1]] * transform[4]; fetchNormal[2] = normal[transform[2]] * transform[5]; texel[0] = pixels[i++] / 255; texel[1] = pixels[i++] / 255; texel[2] = pixels[i++] / 255; // RGBM Decode var scale = pixels[i++] / 255 * 8.12; texel[0] *= scale; texel[1] *= scale; texel[2] *= scale; vec3.scaleAndAdd(sideResult, sideResult, texel, harmonics(fetchNormal, m) * -normal[2]); // -normal.z equals cos(theta) of Lambertian divider += -normal[2]; } } vec3.scaleAndAdd(result, result, sideResult, 1 / divider); } coeff[m * 3] = result[0] / 6.0; coeff[m * 3 + 1] = result[1] / 6.0; coeff[m * 3 + 2] = result[2] / 6.0; } return coeff; } /** * @param {clay.Renderer} renderer * @param {clay.Texture} envMap * @param {Object} [textureOpts] * @param {Object} [textureOpts.lod] * @param {boolean} [textureOpts.decodeRGBM] */ sh.projectEnvironmentMap = function (renderer, envMap, opts) { // TODO sRGB opts = opts || {}; opts.lod = opts.lod || 0; var skybox; var dummyScene = new Scene(); var size = 64; if (envMap.textureType === 'texture2D') { skybox = new Skybox$1({ scene: dummyScene, environmentMap: envMap }); } else { size = (envMap.image && envMap.image.px) ? envMap.image.px.width : envMap.width; skybox = new Skybox$1({ scene: dummyScene, environmentMap: envMap }); } // Convert to rgbm var width = Math.ceil(size / Math.pow(2, opts.lod)); var height = Math.ceil(size / Math.pow(2, opts.lod)); var rgbmTexture = new Texture2D({ width: width, height: height }); var framebuffer = new FrameBuffer(); skybox.material.define('fragment', 'RGBM_ENCODE'); if (opts.decodeRGBM) { skybox.material.define('fragment', 'RGBM_DECODE'); } skybox.material.set('lod', opts.lod); var envMapPass = new EnvironmentMapPass({ texture: rgbmTexture }); var cubePixels = {}; for (var i = 0; i < targets$3.length; i++) { cubePixels[targets$3[i]] = new Uint8Array(width * height * 4); var camera = envMapPass.getCamera(targets$3[i]); camera.fov = 90; framebuffer.attach(rgbmTexture); framebuffer.bind(renderer); renderer.render(dummyScene, camera); renderer.gl.readPixels( 0, 0, width, height, Texture.RGBA, Texture.UNSIGNED_BYTE, cubePixels[targets$3[i]] ); framebuffer.unbind(renderer); } skybox.dispose(renderer); framebuffer.dispose(renderer); rgbmTexture.dispose(renderer); return projectEnvironmentMapCPU(renderer, cubePixels, width, height); }; /** * Helpers for creating a common 3d application. * @namespace clay.application */ // TODO createCompositor // TODO Dispose test. geoCache test. // TODO Tonemapping exposure // TODO fitModel. // TODO Particle ? var parseColor = colorUtil.parseToFloat; var EVE_NAMES = ['click', 'dblclick', 'mouseover', 'mouseout', 'mousemove', 'touchstart', 'touchend', 'touchmove', 'mousewheel', 'DOMMouseScroll' ]; /** * @typedef {string|HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} ImageLike */ /** * @typedef {string|HTMLCanvasElement|HTMLImageElement|HTMLVideoElement|clay.Texture2D} TextureLike */ /** * @typedef {string|Array.<number>} Color */ /** * @typedef {HTMLElement|string} DomQuery */ /** * @typedef {Object} App3DNamespace * @property {Function} init Initialization callback that will be called when initing app. * You can return a promise in init to start the loop asynchronously when the promise is resolved. * @property {Function} loop Loop callback that will be called each frame. * @property {boolean} [autoRender=true] If render automatically each frame. * @property {Function} [beforeRender] * @property {Function} [afterRender] * @property {number} [width] Container width. * @property {number} [height] Container height. * @property {number} [devicePixelRatio] * @property {Object.<string, Function>} [methods] Methods that will be injected to App3D#methods. * @property {Object} [graphic] Graphic configuration including shadow, color space. * @property {boolean} [graphic.shadow=false] If enable shadow * @property {boolean} [graphic.linear=false] If use linear color space * @property {boolean} [graphic.tonemapping=false] If enable ACES tone mapping. * @property {boolean} [event=false] If enable mouse/touch event. It will slow down the system if geometries are complex. */ /** * @typedef {Object} StandardMaterialMRConfig * @property {string} [name] * @property {string} [shader='standardMR'] * @property {Color} [color] * @property {number} [alpha] * @property {number} [metalness] * @property {number} [roughness] * @property {Color} [emission] * @property {number} [emissionIntensity] * @property {boolean} [transparent] * @property {TextureLike} [diffuseMap] * @property {TextureLike} [normalMap] * @property {TextureLike} [roughnessMap] * @property {TextureLike} [metalnessMap] * @property {TextureLike} [emissiveMap] */ /** * Using App3D is a much more convenient way to create and manage your 3D application. * * It provides the abilities to: * * + Manage application loop and rendering. * + Collect GPU resource automatically without memory leak concern. * + Mouse event management. * + Create scene objects, materials, textures with simpler code. * + Load models with one line of code. * + Promised interfaces. * * Here is a basic example to create a rotating cube. * ```js var app = clay.application.create('#viewport', { init: function (app) { // Create a perspective camera. // First parameter is the camera position. Which is in front of the cube. // Second parameter is the camera lookAt target. Which is the origin of the world, and where the cube puts. this._camera = app.createCamera([0, 2, 5], [0, 0, 0]); // Create a sample cube this._cube = app.createCube(); // Create a directional light. The direction is from top right to left bottom, away from camera. this._mainLight = app.createDirectionalLight([-1, -1, -1]); }, loop: function (app) { // Simply rotating the cube every frame. this._cube.rotation.rotateY(app.frameTime / 1000); } }); ``` * @constructor * @alias clay.application.App3D * @param {DomQuery} dom Container dom element or a selector string that can be used in `querySelector` * @param {App3DNamespace} appNS Options and namespace used in creating app3D */ function App3D(dom, appNS) { appNS = appNS || {}; appNS.graphic = appNS.graphic || {}; if (appNS.autoRender == null) { appNS.autoRender = true; } if (typeof dom === 'string') { dom = document.querySelector(dom); } if (!dom) { throw new Error('Invalid dom'); } var isDomCanvas = !dom.nodeName // Not in dom environment || dom.nodeName.toUpperCase() === 'CANVAS'; var rendererOpts = {}; isDomCanvas && (rendererOpts.canvas = dom); appNS.devicePixelRatio && (rendererOpts.devicePixelRatio = appNS.devicePixelRatio); var gRenderer = new Renderer(rendererOpts); var gWidth = appNS.width || dom.clientWidth; var gHeight = appNS.height || dom.clientHeight; var gScene = new Scene(); var gTimeline = new Timeline(); var gShadowPass = appNS.graphic.shadow && new ShadowMapPass(); var gRayPicking = appNS.event && new RayPicking({ scene: gScene, renderer: gRenderer }); !isDomCanvas && dom.appendChild(gRenderer.canvas); gRenderer.resize(gWidth, gHeight); var gFrameTime = 0; var gElapsedTime = 0; gTimeline.start(); var userMethods = {}; for (var key in appNS.methods) { userMethods[key] = appNS.methods[key].bind(appNS, this); } Object.defineProperties(this, { /** * Container dom element * @name clay.application.App3D#container * @type {HTMLElement} */ container: { get: function () { return dom; } }, /** * @name clay.application.App3D#renderer * @type {clay.Renderer} */ renderer: { get: function () { return gRenderer; }}, /** * @name clay.application.App3D#scene * @type {clay.Renderer} */ scene: { get: function () { return gScene; }}, /** * @name clay.application.App3D#timeline * @type {clay.Renderer} */ timeline: { get: function () { return gTimeline; }}, /** * Time elapsed since last frame. Can be used in loop to calculate the movement. * @name clay.application.App3D#frameTime * @type {number} */ frameTime: { get: function () { return gFrameTime; }}, /** * Time elapsed since application created. * @name clay.application.App3D#elapsedTime * @type {number} */ elapsedTime: { get: function () { return gElapsedTime; }}, /** * Width of viewport. * @name clay.application.App3D#width * @type {number} */ width: { get: function () { return gRenderer.getWidth(); }}, /** * Height of viewport. * @name clay.application.App3D#height * @type {number} */ height: { get: function () { return gRenderer.getHeight(); }}, /** * Methods from {@link clay.application.create} * @name clay.application.App3D#methods * @type {number} */ methods: { get: function () { return userMethods; } }, _shadowPass: { get: function () { return gShadowPass; } }, _appNS: { get: function () { return appNS; } }, }); /** * Resize the application. Will use the container clientWidth/clientHeight if width/height in parameters are not given. * @function * @memberOf {clay.application.App3D} * @param {number} [width] * @param {number} [height] */ this.resize = function (width, height) { gWidth = width || appNS.width || dom.clientWidth; gHeight = height || dom.height || dom.clientHeight; gRenderer.resize(gWidth, gHeight); }; /** * Dispose the application * @function */ this.dispose = function () { this._disposed = true; if (appNS.dispose) { appNS.dispose(this); } gTimeline.stop(); gRenderer.disposeScene(gScene); gShadowPass && gShadowPass.dispose(gRenderer); dom.innerHTML = ''; EVE_NAMES.forEach(function (eveType) { this[makeHandlerName(eveType)] && vendor.removeEventListener(dom, makeHandlerName(eveType)); }, this); }; gRayPicking && this._initMouseEvents(gRayPicking); this._geoCache = new LRU$1(20); this._texCache = new LRU$1(20); // GPU Resources. this._texturesList = {}; this._geometriesList = {}; // Do init the application. var initPromise = Promise.resolve(appNS.init && appNS.init(this)); // Use the inited camera. gRayPicking && (gRayPicking.camera = gScene.getMainCamera()); if (!appNS.loop) { console.warn('Miss loop method.'); } var self = this; initPromise.then(function () { gTimeline.on('frame', function (frameTime) { gFrameTime = frameTime; gElapsedTime += frameTime; var camera = gScene.getMainCamera(); if (camera) { camera.aspect = gRenderer.getViewportAspect(); } gRayPicking && (gRayPicking.camera = camera); appNS.loop && appNS.loop(self); if (appNS.autoRender) { self.render(); } self.collectResources(); }, this); }); gScene.on('beforerender', function (renderer, scene, camera, renderList) { if (this._inRender) { // Only update graphic options when using #render function. this._updateGraphicOptions(appNS.graphic, renderList.opaque, false); this._updateGraphicOptions(appNS.graphic, renderList.transparent, false); } }, this); } function isImageLikeElement(val) { return (typeof Image !== 'undefined' && val instanceof Image) || (typeof HTMLCanvasElement !== 'undefined' && val instanceof HTMLCanvasElement) || (typeof HTMLVideoElement !== 'undefined' && val instanceof HTMLVideoElement); } function getKeyFromImageLike(val) { return typeof val === 'string' ? val : (val.__key__ || (val.__key__ = util$1.genGUID())); } function makeHandlerName(eveType) { return '_' + eveType + 'Handler'; } function packageEvent(eventType, pickResult, offsetX, offsetY, wheelDelta) { var event = util$1.clone(pickResult); event.type = eventType; event.offsetX = offsetX; event.offsetY = offsetY; if (wheelDelta !== null) { event.wheelDelta = wheelDelta; } return event; } function bubblingEvent(target, event) { while (target && !event.cancelBubble) { target.trigger(event.type, event); target = target.getParent(); } } App3D.prototype._initMouseEvents = function (rayPicking) { var dom = this.container; var oldTarget = null; EVE_NAMES.forEach(function (_eveType) { vendor.addEventListener(dom, _eveType, this[makeHandlerName(_eveType)] = function (e) { if (!rayPicking.camera) { // Not have camera yet. return; } e.preventDefault && e.preventDefault(); var box = dom.getBoundingClientRect(); var offsetX, offsetY; var eveType = _eveType; if (eveType.indexOf('touch') >= 0) { var touch = eveType !== 'touchend' ? e.targetTouches[0] : e.changedTouches[0]; if (eveType === 'touchstart') { eveType = 'mousedown'; } else if (eveType === 'touchend') { eveType = 'mouseup'; } else if (eveType === 'touchmove') { eveType = 'mousemove'; } offsetX = touch.clientX - box.left; offsetY = touch.clientY - box.top; } else { offsetX = e.clientX - box.left; offsetY = e.clientY - box.top; } var pickResult = rayPicking.pick(offsetX, offsetY); var delta; if (eveType === 'DOMMouseScroll' || eveType === 'mousewheel') { delta = (e.wheelDelta) ? e.wheelDelta / 120 : -(e.detail || 0) / 3; } if (pickResult) { // Just ignore silent element. if (pickResult.target.silent) { return; } if (eveType === 'mousemove') { // PENDING touchdown should trigger mouseover event ? var targetChanged = pickResult.target !== oldTarget; if (targetChanged) { oldTarget && bubblingEvent(oldTarget, packageEvent('mouseout', { target: oldTarget }, offsetX, offsetY)); } bubblingEvent(pickResult.target, packageEvent('mousemove', pickResult, offsetX, offsetY)); if (targetChanged) { bubblingEvent(pickResult.target, packageEvent('mouseover', pickResult, offsetX, offsetY)); } } else { bubblingEvent(pickResult.target, packageEvent(eveType, pickResult, offsetX, offsetY, delta)); } oldTarget = pickResult.target; } else if (oldTarget) { bubblingEvent(oldTarget, packageEvent('mouseout', { target: oldTarget }, offsetX, offsetY)); oldTarget = null; } }); }, this); }; App3D.prototype._updateGraphicOptions = function (graphicOpts, list, isSkybox) { var enableTonemapping = !!graphicOpts.tonemapping; var isLinearSpace = !!graphicOpts.linear; var prevMaterial; for (var i = 0; i < list.length; i++) { var mat = list[i].material; if (mat === prevMaterial) { continue; } enableTonemapping ? mat.define('fragment', 'TONEMAPPING') : mat.undefine('fragment', 'TONEMAPPING'); if (isLinearSpace) { var decodeSRGB = true; if (isSkybox && mat.get('environmentMap') && !mat.get('environmentMap').sRGB) { decodeSRGB = false; } decodeSRGB && mat.define('fragment', 'SRGB_DECODE'); mat.define('fragment', 'SRGB_ENCODE'); } else { mat.undefine('fragment', 'SRGB_DECODE'); mat.undefine('fragment', 'SRGB_ENCODE'); } prevMaterial = mat; } }; App3D.prototype._doRender = function (renderer, scene) { var camera = scene.getMainCamera(); renderer.render(scene, camera, true); }; /** * Do render */ App3D.prototype.render = function () { this._inRender = true; var appNS = this._appNS; appNS.beforeRender && appNS.beforeRender(self); var scene = this.scene; var renderer = this.renderer; var shadowPass = this._shadowPass; scene.update(); var skyboxList = []; scene.skybox && skyboxList.push(scene.skybox); scene.skydome && skyboxList.push(scene.skydome); this._updateGraphicOptions(appNS.graphic, skyboxList, true); // Render shadow pass shadowPass && shadowPass.render(renderer, scene, null, true); this._doRender(renderer, scene, true); appNS.afterRender && appNS.afterRender(self); this._inRender = false; }; App3D.prototype.collectResources = function () { var renderer = this.renderer; var scene = this.scene; var texturesList = this._texturesList; var geometriesList = this._geometriesList; // Mark all resources unused; markUnused(texturesList); markUnused(geometriesList); // Collect resources used in this frame. var newTexturesList = []; var newGeometriesList = []; collectResources(scene, newTexturesList, newGeometriesList); // Dispose those unsed resources. checkAndDispose(renderer, texturesList); checkAndDispose(renderer, geometriesList); this._texturesList = newTexturesList; this._geometriesList = newGeometriesList; }; function markUnused(resourceList) { for (var i = 0; i < resourceList.length; i++) { resourceList[i].__used = 0; } } function checkAndDispose(renderer, resourceList) { for (var i = 0; i < resourceList.length; i++) { if (!resourceList[i].__used) { resourceList[i].dispose(renderer); } } } function updateUsed(resource, list) { resource.__used = resource.__used || 0; resource.__used++; if (resource.__used === 1) { // Don't push to the list twice. list.push(resource); } } function collectResources(scene, textureResourceList, geometryResourceList) { var prevMaterial; var prevGeometry; scene.traverse(function (renderable) { if (renderable.isRenderable()) { var geometry = renderable.geometry; var material = renderable.material; // TODO optimize!! if (material !== prevMaterial) { var textureUniforms = material.getTextureUniforms(); for (var u = 0; u < textureUniforms.length; u++) { var uniformName = textureUniforms[u]; var val = material.uniforms[uniformName].value; var uniformType = material.uniforms[uniformName].type; if (!val) { continue; } if (uniformType === 't') { updateUsed(val, textureResourceList); } else if (uniformType === 'tv') { for (var k = 0; k < val.length; k++) { if (val[k]) { updateUsed(val[k], textureResourceList); } } } } } if (geometry !== prevGeometry) { updateUsed(geometry, geometryResourceList); } prevMaterial = material; prevGeometry = geometry; } }); for (var k = 0; k < scene.lights.length; k++) { // Track AmbientCubemap if (scene.lights[k].cubemap) { updateUsed(scene.lights[k].cubemap, textureResourceList); } } } /** * Load a texture from image or string. * @param {ImageLike} img * @param {Object} [opts] Texture options. * @param {boolean} [opts.flipY=true] If flipY. See {@link clay.Texture.flipY} * @param {boolean} [opts.convertToPOT=false] Force convert None Power of Two texture to Power of two so it can be tiled. * @param {number} [opts.anisotropic] Anisotropic filtering. See {@link clay.Texture.anisotropic} * @param {number} [opts.wrapS=clay.Texture.REPEAT] See {@link clay.Texture.wrapS} * @param {number} [opts.wrapT=clay.Texture.REPEAT] See {@link clay.Texture.wrapT} * @param {number} [opts.minFilter=clay.Texture.LINEAR_MIPMAP_LINEAR] See {@link clay.Texture.minFilter} * @param {number} [opts.magFilter=clay.Texture.LINEAR] See {@link clay.Texture.magFilter} * @param {number} [opts.exposure] Only be used when source is a HDR image. * @param {boolean} [useCache] If use cache. * @return {Promise} * @example * app.loadTexture('diffuseMap.jpg') * .then(function (texture) { * material.set('diffuseMap', texture); * }); */ App3D.prototype.loadTexture = function (urlOrImg, opts, useCache) { var self = this; var key = getKeyFromImageLike(urlOrImg); if (useCache) { if (this._texCache.get(key)) { return this._texCache.get(key); } } // TODO Promise ? var promise = new Promise(function (resolve, reject) { var texture = self.loadTextureSync(urlOrImg, opts); if (!texture.isRenderable()) { texture.success(function () { if (self._disposed) { return; } resolve(texture); }); texture.error(function () { if (self._disposed) { return; } reject(); }); } else { resolve(texture); } }); if (useCache) { this._texCache.put(key, promise); } return promise; }; /** * Create a texture from image or string synchronously. Texture can be use directly and don't have to wait for it's loaded. * @param {ImageLike} img * @param {Object} [opts] Texture options. * @param {boolean} [opts.flipY=true] If flipY. See {@link clay.Texture.flipY} * @param {boolean} [opts.convertToPOT=false] Force convert None Power of Two texture to Power of two so it can be tiled. * @param {number} [opts.anisotropic] Anisotropic filtering. See {@link clay.Texture.anisotropic} * @param {number} [opts.wrapS=clay.Texture.REPEAT] See {@link clay.Texture.wrapS} * @param {number} [opts.wrapT=clay.Texture.REPEAT] See {@link clay.Texture.wrapT} * @param {number} [opts.minFilter=clay.Texture.LINEAR_MIPMAP_LINEAR] See {@link clay.Texture.minFilter} * @param {number} [opts.magFilter=clay.Texture.LINEAR] See {@link clay.Texture.magFilter} * @param {number} [opts.exposure] Only be used when source is a HDR image. * @return {clay.Texture2D} * @example * var texture = app.loadTexture('diffuseMap.jpg', { * anisotropic: 8, * flipY: false * }); * material.set('diffuseMap', texture); */ App3D.prototype.loadTextureSync = function (urlOrImg, opts) { var texture = new Texture2D(opts); if (typeof urlOrImg === 'string') { if (urlOrImg.match(/.hdr$|^data:application\/octet-stream/)) { texture = textureUtil.loadTexture(urlOrImg, { exposure: opts && opts.exposure, fileType: 'hdr' }, function () { texture.dirty(); texture.trigger('success'); }); for (var key in opts) { texture[key] = opts[key]; } } else { texture.load(urlOrImg); } } else if (isImageLikeElement(urlOrImg)) { texture.image = urlOrImg; texture.dynamic = urlOrImg instanceof HTMLVideoElement; } return texture; }; /** * Create a texture from image or string synchronously. Texture can be use directly and don't have to wait for it's loaded. * @param {ImageLike} img * @param {Object} [opts] Texture options. * @param {boolean} [opts.flipY=false] If flipY. See {@link clay.Texture.flipY} * @return {Promise} * @example * app.loadTextureCube({ * px: 'skybox/px.jpg', py: 'skybox/py.jpg', pz: 'skybox/pz.jpg', * nx: 'skybox/nx.jpg', ny: 'skybox/ny.jpg', nz: 'skybox/nz.jpg' * }).then(function (texture) { * skybox.setEnvironmentMap(texture); * }) */ App3D.prototype.loadTextureCube = function (imgList, opts) { var textureCube = this.loadTextureCubeSync(imgList, opts); return new Promise(function (resolve, reject) { if (textureCube.isRenderable()) { resolve(textureCube); } else { textureCube.success(function () { resolve(textureCube); }).error(function () { reject(); }); } }); }; /** * Create a texture from image or string synchronously. Texture can be use directly and don't have to wait for it's loaded. * @param {ImageLike} img * @param {Object} [opts] Texture options. * @param {boolean} [opts.flipY=false] If flipY. See {@link clay.Texture.flipY} * @return {clay.TextureCube} * @example * var texture = app.loadTextureCubeSync({ * px: 'skybox/px.jpg', py: 'skybox/py.jpg', pz: 'skybox/pz.jpg', * nx: 'skybox/nx.jpg', ny: 'skybox/ny.jpg', nz: 'skybox/nz.jpg' * }); * skybox.setEnvironmentMap(texture); */ App3D.prototype.loadTextureCubeSync = function (imgList, opts) { opts = opts || {}; opts.flipY = opts.flipY || false; var textureCube = new TextureCube(opts); if (!imgList || !imgList.px || !imgList.nx || !imgList.py || !imgList.ny || !imgList.pz || !imgList.nz) { throw new Error('Invalid cubemap format. Should be an object including px,nx,py,ny,pz,nz'); } if (typeof imgList.px === 'string') { textureCube.load(imgList); } else { textureCube.image = util$1.clone(imgList); } return textureCube; }; /** * Create a material. * @param {Object|StandardMaterialMRConfig} materialConfig. materialConfig contains `shader`, `transparent` and uniforms that used in corresponding uniforms. * Uniforms can be `color`, `alpha` `diffuseMap` etc. * @param {string|clay.Shader} [shader='clay.standardMR'] Default to be standard shader with metalness and roughness workflow. * @param {boolean} [transparent=false] If material is transparent. * @param {boolean} [textureConvertToPOT=false] Force convert None Power of Two texture to Power of two so it can be tiled. * @param {boolean} [textureFlipY=true] If flip y of texture. * @param {Function} [textureLoaded] Callback when single texture loaded. * @param {Function} [texturesReady] Callback when all texture loaded. * @return {clay.Material} */ App3D.prototype.createMaterial = function (matConfig) { matConfig = matConfig || {}; matConfig.shader = matConfig.shader || 'clay.standardMR'; var shader = matConfig.shader instanceof Shader ? matConfig.shader : library.get(matConfig.shader); var material = new Material({ shader: shader }); if (matConfig.name) { material.name = matConfig.name; } var texturesLoading = []; function makeTextureSetter(key) { return function (texture) { material.setUniform(key, texture); matConfig.textureLoaded && matConfig.textureLoaded(key, texture); return texture; }; } for (var key in matConfig) { if (material.uniforms[key]) { var val = matConfig[key]; if ((material.uniforms[key].type === 't' || isImageLikeElement(val)) && !(val instanceof Texture) ) { // Try to load a texture. texturesLoading.push(this.loadTexture(val, { convertToPOT: matConfig.textureConvertToPOT || false, flipY: matConfig.textureFlipY == null ? true : matConfig.textureFlipY }).then(makeTextureSetter(key))); } else { material.setUniform(key, val); } } } if (matConfig.transparent) { matConfig.depthMask = false; matConfig.transparent = true; } if (matConfig.texturesReady) { Promise.all(texturesLoading).then(function (textures) { matConfig.texturesReady(textures); }); } return material; }; /** * Create a cube mesh and add it to the scene or the given parent node. * @function * @param {Object|clay.Material} [material] * @param {clay.Node} [parentNode] Parent node to append. Default to be scene. * @param {Array.<number>|number} [subdivision=1] Subdivision of cube. * Can be a number to represent both width, height and depth dimensions. Or an array to represent them respectively. * @return {clay.Mesh} * @example * // Create a white cube. * app.createCube() */ App3D.prototype.createCube = function (material, parentNode, subdiv) { if (subdiv == null) { subdiv = 1; } if (typeof subdiv === 'number') { subdiv = [subdiv, subdiv, subdiv]; } var geoKey = 'cube-' + subdiv.join('-'); var cube = this._geoCache.get(geoKey); if (!cube) { cube = new Cube$1({ widthSegments: subdiv[0], heightSegments: subdiv[1], depthSegments: subdiv[2] }); cube.generateTangents(); this._geoCache.put(geoKey, cube); } return this.createMesh(cube, material, parentNode); }; /** * Create a cube mesh that camera is inside the cube. * @function * @param {Object|clay.Material} [material] * @param {clay.Node} [parentNode] Parent node to append. Default to be scene. * @param {Array.<number>|number} [subdivision=1] Subdivision of cube. * Can be a number to represent both width, height and depth dimensions. Or an array to represent them respectively. * @return {clay.Mesh} * @example * // Create a white cube inside. * app.createCubeInside() */ App3D.prototype.createCubeInside = function (material, parentNode, subdiv) { if (subdiv == null) { subdiv = 1; } if (typeof subdiv === 'number') { subdiv = [subdiv, subdiv, subdiv]; } var geoKey = 'cubeInside-' + subdiv.join('-'); var cube = this._geoCache.get(geoKey); if (!cube) { cube = new Cube$1({ inside: true, widthSegments: subdiv[0], heightSegments: subdiv[1], depthSegments: subdiv[2] }); cube.generateTangents(); this._geoCache.put(geoKey, cube); } return this.createMesh(cube, material, parentNode); }; /** * Create a sphere mesh and add it to the scene or the given parent node. * @function * @param {Object|clay.Material} [material] * @param {clay.Node} [parentNode] Parent node to append. Default to be scene. * @param {number} [subdivision=20] Subdivision of sphere. * @return {clay.Mesh} * @example * // Create a semi-transparent sphere. * app.createSphere({ * color: [0, 0, 1], * transparent: true, * alpha: 0.5 * }) */ App3D.prototype.createSphere = function (material, parentNode, subdivision) { if (subdivision == null) { subdivision = 20; } var geoKey = 'sphere-' + subdivision; var sphere = this._geoCache.get(geoKey); if (!sphere) { sphere = new Sphere$1({ widthSegments: subdivision * 2, heightSegments: subdivision }); sphere.generateTangents(); this._geoCache.put(geoKey, sphere); } return this.createMesh(sphere, material, parentNode); }; // TODO may be modified? /** * Create a plane mesh and add it to the scene or the given parent node. * @function * @param {Object|clay.Material} [material] * @param {clay.Node} [parentNode] Parent node to append. Default to be scene. * @param {Array.<number>|number} [subdivision=1] Subdivision of plane. * Can be a number to represent both width and height dimensions. Or an array to represent them respectively. * @return {clay.Mesh} * @example * // Create a red color plane. * app.createPlane({ * color: [1, 0, 0] * }) */ App3D.prototype.createPlane = function (material, parentNode, subdiv) { if (subdiv == null) { subdiv = 1; } if (typeof subdiv === 'number') { subdiv = [subdiv, subdiv]; } var geoKey = 'plane-' + subdiv.join('-'); var planeGeo = this._geoCache.get(geoKey); if (!planeGeo) { planeGeo = new Plane$3({ widthSegments: subdiv[0], heightSegments: subdiv[1] }); planeGeo.generateTangents(); this._geoCache.put(geoKey, planeGeo); } return this.createMesh(planeGeo, material, parentNode); }; /** * Create mesh with parametric surface function * @param {Object|clay.Material} [material] * @param {clay.Node} [parentNode] Parent node to append. Default to be scene. * @param {Object} generator * @param {Function} generator.x * @param {Function} generator.y * @param {Function} generator.z * @param {Array} [generator.u=[0, 1, 0.05]] * @param {Array} [generator.v=[0, 1, 0.05]] * @return {clay.Mesh} */ App3D.prototype.createParametricSurface = function (material, parentNode, generator) { var geo = new ParametricSurface$1({ generator: generator }); geo.generateTangents(); return this.createMesh(geo, material, parentNode); }; /** * Create a general mesh with given geometry instance and material config. * @param {clay.Geometry} geometry * @return {clay.Mesh} */ App3D.prototype.createMesh = function (geometry, mat, parentNode) { var mesh = new Mesh({ geometry: geometry, material: mat instanceof Material ? mat : this.createMaterial(mat) }); parentNode = parentNode || this.scene; parentNode.add(mesh); return mesh; }; /** * Create an empty node * @param {clay.Node} parentNode * @return {clay.Node} */ App3D.prototype.createNode = function (parentNode) { var node = new Node(); parentNode = parentNode || this.scene; parentNode.add(node); return node; }; /** * Create a perspective or orthographic camera and add it to the scene. * @param {Array.<number>|clay.Vector3} position * @param {Array.<number>|clay.Vector3} target * @param {string} [type="perspective"] Can be 'perspective' or 'orthographic'(in short 'ortho') * @param {Array.<number>|clay.Vector3} [extent] Extent is available only if type is orthographic. * @return {clay.camera.Perspective|clay.camera.Orthographic} */ App3D.prototype.createCamera = function (position, target, type, extent) { var CameraCtor; var isOrtho = false; if (type === 'ortho' || type === 'orthographic') { isOrtho = true; CameraCtor = Orthographic$1; } else { if (type && type !== 'perspective') { console.error('Unkown camera type ' + type + '. Use default perspective camera'); } CameraCtor = Perspective$1; } var camera = new CameraCtor(); if (position instanceof Vector3) { camera.position.copy(position); } else if (position instanceof Array) { camera.position.setArray(position); } if (target instanceof Array) { target = new Vector3(target[0], target[1], target[2]); } if (target instanceof Vector3) { camera.lookAt(target); } if (extent && isOrtho) { extent = extent.array || extent; camera.left = -extent[0] / 2; camera.right = extent[0] / 2; camera.top = extent[1] / 2; camera.bottom = -extent[1] / 2; camera.near = 0; camera.far = extent[2]; } else { camera.aspect = this.renderer.getViewportAspect(); } this.scene.add(camera); return camera; }; /** * Create a directional light and add it to the scene. * @param {Array.<number>|clay.Vector3} dir A Vector3 or array to represent the direction. * @param {Color} [color='#fff'] Color of directional light, default to be white. * @param {number} [intensity] Intensity of directional light, default to be 1. * * @example * app.createDirectionalLight([-1, -1, -1], '#fff', 2); */ App3D.prototype.createDirectionalLight = function (dir, color, intensity) { var light = new DirectionalLight(); if (dir instanceof Vector3) { dir = dir.array; } light.position.setArray(dir).negate(); light.lookAt(Vector3.ZERO); if (typeof color === 'string') { color = parseColor(color); } color != null && (light.color = color); intensity != null && (light.intensity = intensity); this.scene.add(light); return light; }; /** * Create a spot light and add it to the scene. * @param {Array.<number>|clay.Vector3} position Position of the spot light. * @param {Array.<number>|clay.Vector3} [target] Target position where spot light points to. * @param {number} [range=20] Falloff range of spot light. Default to be 20. * @param {Color} [color='#fff'] Color of spot light, default to be white. * @param {number} [intensity=1] Intensity of spot light, default to be 1. * @param {number} [umbraAngle=30] Umbra angle of spot light. Default to be 30 degree from the middle line. * @param {number} [penumbraAngle=45] Penumbra angle of spot light. Default to be 45 degree from the middle line. * * @example * app.createSpotLight([5, 5, 5], [0, 0, 0], 20, #900); */ App3D.prototype.createSpotLight = function (position, target, range, color, intensity, umbraAngle, penumbraAngle) { var light = new SpotLight(); light.position.setArray(position instanceof Vector3 ? position.array : position); if (target instanceof Array) { target = new Vector3(target[0], target[1], target[2]); } if (target instanceof Vector3) { light.lookAt(target); } if (typeof color === 'string') { color = parseColor(color); } range != null && (light.range = range); color != null && (light.color = color); intensity != null && (light.intensity = intensity); umbraAngle != null && (light.umbraAngle = umbraAngle); penumbraAngle != null && (light.penumbraAngle = penumbraAngle); this.scene.add(light); return light; }; /** * Create a point light. * @param {Array.<number>|clay.Vector3} position Position of point light.. * @param {number} [range=100] Falloff range of point light. * @param {Color} [color='#fff'] Color of point light. * @param {number} [intensity=1] Intensity of point light. */ App3D.prototype.createPointLight = function (position, range, color, intensity) { var light = new PointLight(); light.position.setArray(position instanceof Vector3 ? position.array : position); if (typeof color === 'string') { color = parseColor(color); } range != null && (light.range = range); color != null && (light.color = color); intensity != null && (light.intensity = intensity); this.scene.add(light); return light; }; /** * Create a ambient light. * @param {Color} [color='#fff'] Color of ambient light. * @param {number} [intensity=1] Intensity of ambient light. */ App3D.prototype.createAmbientLight = function (color, intensity) { var light = new AmbientLight(); if (typeof color === 'string') { color = parseColor(color); } color != null && (light.color = color); intensity != null && (light.intensity = intensity); this.scene.add(light); return light; }; /** * Create an cubemap ambient light and an spherical harmonic ambient light * for specular and diffuse lighting in PBR rendering * @param {ImageLike|TextureCube} [envImage] Panorama environment image, HDR format is better. Or a pre loaded texture cube * @param {number} [specularIntenstity=0.7] Intensity of specular light. * @param {number} [diffuseIntenstity=0.7] Intensity of diffuse light. * @param {number} [exposure=1] Exposure of HDR image. Only if image in first paramter is HDR. * @param {number} [prefilteredCubemapSize=32] The size of prefilerted cubemap. Larger value will take more time to do expensive prefiltering. * @return {Promise} */ App3D.prototype.createAmbientCubemapLight = function (envImage, specIntensity, diffIntensity, exposure, prefilteredCubemapSize) { var self = this; if (exposure == null) { exposure = 0; } if (prefilteredCubemapSize == null) { prefilteredCubemapSize = 32; } var scene = this.scene; var loadPromise; if (envImage.textureType === 'textureCube') { loadPromise = envImage.isRenderable() ? Promise.resolve(envImage) : new Promise(function (resolve, reject) { envImage.success(function () { resolve(envImage); }); }); } else { loadPromise = this.loadTexture(envImage, { exposure: exposure }); } return loadPromise.then(function (envTexture) { var specLight = new AmbientCubemapLight({ intensity: specIntensity != null ? specIntensity : 0.7 }); specLight.cubemap = envTexture; envTexture.flipY = false; // TODO Cache prefilter ? specLight.prefilter(self.renderer, 32); var diffLight = new AmbientSHLight({ intensity: diffIntensity != null ? diffIntensity : 0.7, coefficients: sh.projectEnvironmentMap( self.renderer, specLight.cubemap, { lod: 1 } ) }); scene.add(specLight); scene.add(diffLight); return { specular: specLight, diffuse: diffLight, // Original environment map environmentMap: envTexture }; }); }; /** * Load a [glTF](https://github.com/KhronosGroup/glTF) format model. * You can convert FBX/DAE/OBJ format models to [glTF](https://github.com/KhronosGroup/glTF) with [fbx2gltf](https://github.com/pissang/claygl#fbx-to-gltf20-converter) python script, * or simply using the [Clay Viewer](https://github.com/pissang/clay-viewer) client application. * @param {string} url * @param {Object} opts * @param {string|clay.Shader} [opts.shader='clay.standard'] 'basic'|'lambert'|'standard'. * @param {boolean} [opts.waitTextureLoaded=false] If add to scene util textures are all loaded. * @param {boolean} [opts.autoPlayAnimation=true] If autoplay the animation of model. * @param {boolean} [opts.upAxis='y'] Change model to y up if upAxis is 'z' * @param {boolean} [opts.textureFlipY=false] * @param {boolean} [opts.textureConvertToPOT=false] If convert texture to power-of-two * @param {string} [opts.textureRootPath] Root path of texture. Default to be relative with glTF file. * @param {clay.Node} [parentNode] Parent node that model will be mounted. Default to be scene * @return {Promise} */ App3D.prototype.loadModel = function (url, opts, parentNode) { if (typeof url !== 'string') { throw new Error('Invalid URL.'); } opts = opts || {}; if (opts.autoPlayAnimation == null) { opts.autoPlayAnimation = true; } var shader = opts.shader || 'clay.standard'; var loaderOpts = { rootNode: new Node(), shader: shader, textureRootPath: opts.textureRootPath, crossOrigin: 'Anonymous', textureFlipY: opts.textureFlipY, textureConvertToPOT: opts.textureConvertToPOT }; if (opts.upAxis && opts.upAxis.toLowerCase() === 'z') { loaderOpts.rootNode.rotation.identity().rotateX(-Math.PI / 2); } var loader = new GLTFLoader(loaderOpts); parentNode = parentNode || this.scene; var timeline = this.timeline; var self = this; return new Promise(function (resolve, reject) { function afterLoad(result) { if (self._disposed) { return; } parentNode.add(result.rootNode); if (opts.autoPlayAnimation) { result.clips.forEach(function (clip) { timeline.addClip(clip); }); } resolve(result); } loader.success(function (result) { if (self._disposed) { return; } if (!opts.waitTextureLoaded) { afterLoad(result); } else { Promise.all(result.textures.map(function (texture) { if (texture.isRenderable()) { return Promise.resolve(texture); } return new Promise(function (resolve) { texture.success(resolve); texture.error(resolve); }); })).then(function () { afterLoad(result); }).catch(function () { afterLoad(result); }); } }); loader.error(function () { reject(); }); loader.load(url); }); }; // TODO cloneModel /** * Similar to `app.scene.cloneNode`, except it will mount the cloned node to the scene automatically. * See more in {@link clay.Scene#cloneNode} * * @param {clay.Node} node * @param {clay.Node} [parentNode] Parent node that new cloned node will be mounted. * Default to have same parent with source node. * @return {clay.Node} */ App3D.prototype.cloneNode = function (node, parentNode) { parentNode = parentNode || node.getParent(); var newNode = this.scene.cloneNode(node, parentNode); if (parentNode) { parentNode.add(newNode); } return newNode; }; var application = { App3D: App3D, /** * Create a 3D application that will manage the app initialization and loop. * * See more details at {@link clay.application.App3D} * * @name clay.application.create * @method * @param {HTMLElement|string} dom Container dom element or a selector string that can be used in `querySelector` * @param {App3DNamespace} appNS Options and namespace used in creating app3D * * @return {clay.application.App3D} * * @example * clay.application.create('#app', { * init: function (app) { * app.createCube(); * var camera = app.createCamera(); * camera.position.set(0, 0, 2); * }, * loop: function () { // noop } * }) */ create: function (dom, appNS) { return new App3D(dom, appNS); } }; /** * @constructor * @alias clay.async.Task * @mixes clay.core.mixin.notifier */ function Task() { this._fullfilled = false; this._rejected = false; } /** * Task successed * @param {} data */ Task.prototype.resolve = function(data) { this._fullfilled = true; this._rejected = false; this.trigger('success', data); }; /** * Task failed * @param {} err */ Task.prototype.reject = function(err) { this._rejected = true; this._fullfilled = false; this.trigger('error', err); }; /** * If task successed * @return {boolean} */ Task.prototype.isFullfilled = function() { return this._fullfilled; }; /** * If task failed * @return {boolean} */ Task.prototype.isRejected = function() { return this._rejected; }; /** * If task finished, either successed or failed * @return {boolean} */ Task.prototype.isSettled = function() { return this._fullfilled || this._rejected; }; util$1.extend(Task.prototype, notifier); function makeRequestTask(url, responseType) { var task = new Task(); vendor.request.get({ url: url, responseType: responseType, onload: function(res) { task.resolve(res); }, onerror: function(error) { task.reject(error); } }); return task; } /** * Make a vendor.request task * @param {string|object|object[]|string[]} url * @param {string} [responseType] * @example * var task = Task.makeRequestTask('./a.json'); * var task = Task.makeRequestTask({ * url: 'b.bin', * responseType: 'arraybuffer' * }); * var tasks = Task.makeRequestTask(['./a.json', './b.json']); * var tasks = Task.makeRequestTask([ * {url: 'a.json'}, * {url: 'b.bin', responseType: 'arraybuffer'} * ]); * @return {clay.async.Task|clay.async.Task[]} */ Task.makeRequestTask = function(url, responseType) { if (typeof url === 'string') { return makeRequestTask(url, responseType); } else if (url.url) { // Configure object var obj = url; return makeRequestTask(obj.url, obj.responseType); } else if (Array.isArray(url)) { // Url list var urlList = url; var tasks = []; urlList.forEach(function(obj) { var url, responseType; if (typeof obj === 'string') { url = obj; } else if (Object(obj) === obj) { url = obj.url; responseType = obj.responseType; } tasks.push(makeRequestTask(url, responseType)); }); return tasks; } }; /** * @return {clay.async.Task} */ Task.makeTask = function() { return new Task(); }; util$1.extend(Task.prototype, notifier); /** * @constructor * @alias clay.async.TaskGroup * @extends clay.async.Task */ var TaskGroup = function () { Task.apply(this, arguments); this._tasks = []; this._fulfilledNumber = 0; this._rejectedNumber = 0; }; var Ctor = function (){}; Ctor.prototype = Task.prototype; TaskGroup.prototype = new Ctor(); TaskGroup.prototype.constructor = TaskGroup; /** * Wait for all given tasks successed, task can also be any notifier object which will trigger success and error events. Like {@link clay.Texture2D}, {@link clay.TextureCube}, {@link clay.loader.GLTF}. * @param {Array.<clay.async.Task>} tasks * @chainable * @example * // Load texture list * var list = ['a.jpg', 'b.jpg', 'c.jpg'] * var textures = list.map(function (src) { * var texture = new clay.Texture2D(); * texture.load(src); * return texture; * }); * var taskGroup = new clay.async.TaskGroup(); * taskGroup.all(textures).success(function () { * // Do some thing after all textures loaded * }); */ TaskGroup.prototype.all = function (tasks) { var count = 0; var self = this; var data = []; this._tasks = tasks; this._fulfilledNumber = 0; this._rejectedNumber = 0; util$1.each(tasks, function (task, idx) { if (!task || !task.once) { return; } count++; task.once('success', function (res) { count--; self._fulfilledNumber++; // TODO // Some tasks like texture, loader are not inherited from task // We need to set the states here task._fulfilled = true; task._rejected = false; data[idx] = res; if (count === 0) { self.resolve(data); } }); task.once('error', function () { self._rejectedNumber ++; task._fulfilled = false; task._rejected = true; self.reject(task); }); }); if (count === 0) { setTimeout(function () { self.resolve(data); }); return this; } return this; }; /** * Wait for all given tasks finished, either successed or failed * @param {Array.<clay.async.Task>} tasks * @return {clay.async.TaskGroup} */ TaskGroup.prototype.allSettled = function (tasks) { var count = 0; var self = this; var data = []; if (tasks.length === 0) { setTimeout(function () { self.trigger('success', data); }); return this; } this._tasks = tasks; util$1.each(tasks, function (task, idx) { if (!task || !task.once) { return; } count++; task.once('success', function (res) { count--; self._fulfilledNumber++; task._fulfilled = true; task._rejected = false; data[idx] = res; if (count === 0) { self.resolve(data); } }); task.once('error', function (err) { count--; self._rejectedNumber++; task._fulfilled = false; task._rejected = true; // TODO data[idx] = null; if (count === 0) { self.resolve(data); } }); }); return this; }; /** * Get successed sub tasks number, recursive can be true if sub task is also a TaskGroup. * @param {boolean} [recursive] * @return {number} */ TaskGroup.prototype.getFulfilledNumber = function (recursive) { if (recursive) { var nFulfilled = 0; for (var i = 0; i < this._tasks.length; i++) { var task = this._tasks[i]; if (task instanceof TaskGroup) { nFulfilled += task.getFulfilledNumber(recursive); } else if(task._fulfilled) { nFulfilled += 1; } } return nFulfilled; } else { return this._fulfilledNumber; } }; /** * Get failed sub tasks number, recursive can be true if sub task is also a TaskGroup. * @param {boolean} [recursive] * @return {number} */ TaskGroup.prototype.getRejectedNumber = function (recursive) { if (recursive) { var nRejected = 0; for (var i = 0; i < this._tasks.length; i++) { var task = this._tasks[i]; if (task instanceof TaskGroup) { nRejected += task.getRejectedNumber(recursive); } else if(task._rejected) { nRejected += 1; } } return nRejected; } else { return this._rejectedNumber; } }; /** * Get finished sub tasks number, recursive can be true if sub task is also a TaskGroup. * @param {boolean} [recursive] * @return {number} */ TaskGroup.prototype.getSettledNumber = function (recursive) { if (recursive) { var nSettled = 0; for (var i = 0; i < this._tasks.length; i++) { var task = this._tasks[i]; if (task instanceof TaskGroup) { nSettled += task.getSettledNumber(recursive); } else if(task._rejected || task._fulfilled) { nSettled += 1; } } return nSettled; } else { return this._fulfilledNumber + this._rejectedNumber; } }; /** * Get all sub tasks number, recursive can be true if sub task is also a TaskGroup. * @param {boolean} [recursive] * @return {number} */ TaskGroup.prototype.getTaskNumber = function (recursive) { if (recursive) { var nTask = 0; for (var i = 0; i < this._tasks.length; i++) { var task = this._tasks[i]; if (task instanceof TaskGroup) { nTask += task.getTaskNumber(recursive); } else { nTask += 1; } } return nTask; } else { return this._tasks.length; } }; // PENDING // Use topological sort ? /** * Node of graph based post processing. * * @constructor clay.compositor.CompositorNode * @extends clay.core.Base * */ var CompositorNode = Base.extend(function () { return /** @lends clay.compositor.CompositorNode# */ { /** * @type {string} */ name: '', /** * Input links, will be updated by the graph * @example: * inputName: { * node: someNode, * pin: 'xxxx' * } * @type {Object} */ inputLinks: {}, /** * Output links, will be updated by the graph * @example: * outputName: { * node: someNode, * pin: 'xxxx' * } * @type {Object} */ outputLinks: {}, // Save the output texture of previous frame // Will be used when there exist a circular reference _prevOutputTextures: {}, _outputTextures: {}, // Example: { name: 2 } _outputReferences: {}, _rendering: false, // If rendered in this frame _rendered: false, _compositor: null }; }, /** @lends clay.compositor.CompositorNode.prototype */ { // TODO Remove parameter function callback updateParameter: function (outputName, renderer) { var outputInfo = this.outputs[outputName]; var parameters = outputInfo.parameters; var parametersCopy = outputInfo._parametersCopy; if (!parametersCopy) { parametersCopy = outputInfo._parametersCopy = {}; } if (parameters) { for (var key in parameters) { if (key !== 'width' && key !== 'height') { parametersCopy[key] = parameters[key]; } } } var width, height; if (parameters.width instanceof Function) { width = parameters.width.call(this, renderer); } else { width = parameters.width; } if (parameters.height instanceof Function) { height = parameters.height.call(this, renderer); } else { height = parameters.height; } if ( parametersCopy.width !== width || parametersCopy.height !== height ) { if (this._outputTextures[outputName]) { this._outputTextures[outputName].dispose(renderer.gl); } } parametersCopy.width = width; parametersCopy.height = height; return parametersCopy; }, /** * Set parameter * @param {string} name * @param {} value */ setParameter: function (name, value) {}, /** * Get parameter value * @param {string} name * @return {} */ getParameter: function (name) {}, /** * Set parameters * @param {Object} obj */ setParameters: function (obj) { for (var name in obj) { this.setParameter(name, obj[name]); } }, render: function () {}, getOutput: function (renderer /*optional*/, name) { if (name == null) { // Return the output texture without rendering name = renderer; return this._outputTextures[name]; } var outputInfo = this.outputs[name]; if (!outputInfo) { return ; } // Already been rendered in this frame if (this._rendered) { // Force return texture in last frame if (outputInfo.outputLastFrame) { return this._prevOutputTextures[name]; } else { return this._outputTextures[name]; } } else if ( // TODO this._rendering // Solve Circular Reference ) { if (!this._prevOutputTextures[name]) { // Create a blank texture at first pass this._prevOutputTextures[name] = this._compositor.allocateTexture(outputInfo.parameters || {}); } return this._prevOutputTextures[name]; } this.render(renderer); return this._outputTextures[name]; }, removeReference: function (outputName) { this._outputReferences[outputName]--; if (this._outputReferences[outputName] === 0) { var outputInfo = this.outputs[outputName]; if (outputInfo.keepLastFrame) { if (this._prevOutputTextures[outputName]) { this._compositor.releaseTexture(this._prevOutputTextures[outputName]); } this._prevOutputTextures[outputName] = this._outputTextures[outputName]; } else { // Output of this node have alreay been used by all other nodes // Put the texture back to the pool. this._compositor.releaseTexture(this._outputTextures[outputName]); } } }, link: function (inputPinName, fromNode, fromPinName) { // The relationship from output pin to input pin is one-on-multiple this.inputLinks[inputPinName] = { node: fromNode, pin: fromPinName }; if (!fromNode.outputLinks[fromPinName]) { fromNode.outputLinks[fromPinName] = []; } fromNode.outputLinks[fromPinName].push({ node: this, pin: inputPinName }); // Enabled the pin texture in shader this.pass.material.enableTexture(inputPinName); }, clear: function () { this.inputLinks = {}; this.outputLinks = {}; }, updateReference: function (outputName) { if (!this._rendering) { this._rendering = true; for (var inputName in this.inputLinks) { var link = this.inputLinks[inputName]; link.node.updateReference(link.pin); } this._rendering = false; } if (outputName) { this._outputReferences[outputName] ++; } }, beforeFrame: function () { this._rendered = false; for (var name in this.outputLinks) { this._outputReferences[name] = 0; } }, afterFrame: function () { // Put back all the textures to pool for (var name in this.outputLinks) { if (this._outputReferences[name] > 0) { var outputInfo = this.outputs[name]; if (outputInfo.keepLastFrame) { if (this._prevOutputTextures[name]) { this._compositor.releaseTexture(this._prevOutputTextures[name]); } this._prevOutputTextures[name] = this._outputTextures[name]; } else { this._compositor.releaseTexture(this._outputTextures[name]); } } } } }); /** * @constructor clay.compositor.Graph * @extends clay.core.Base */ var Graph = Base.extend(function () { return /** @lends clay.compositor.Graph# */ { /** * @type {Array.<clay.compositor.CompositorNode>} */ nodes: [] }; }, /** @lends clay.compositor.Graph.prototype */ { /** * Mark to update */ dirty: function () { this._dirty = true; }, /** * @param {clay.compositor.CompositorNode} node */ addNode: function (node) { if (this.nodes.indexOf(node) >= 0) { return; } this.nodes.push(node); this._dirty = true; }, /** * @param {clay.compositor.CompositorNode|string} node */ removeNode: function (node) { if (typeof node === 'string') { node = this.getNodeByName(node); } var idx = this.nodes.indexOf(node); if (idx >= 0) { this.nodes.splice(idx, 1); this._dirty = true; } }, /** * @param {string} name * @return {clay.compositor.CompositorNode} */ getNodeByName: function (name) { for (var i = 0; i < this.nodes.length; i++) { if (this.nodes[i].name === name) { return this.nodes[i]; } } }, /** * Update links of graph */ update: function () { for (var i = 0; i < this.nodes.length; i++) { this.nodes[i].clear(); } // Traverse all the nodes and build the graph for (var i = 0; i < this.nodes.length; i++) { var node = this.nodes[i]; if (!node.inputs) { continue; } for (var inputName in node.inputs) { if (!node.inputs[inputName]) { continue; } if (node.pass && !node.pass.material.isUniformEnabled(inputName)) { console.warn('Pin ' + node.name + '.' + inputName + ' not used.'); continue; } var fromPinInfo = node.inputs[inputName]; var fromPin = this.findPin(fromPinInfo); if (fromPin) { node.link(inputName, fromPin.node, fromPin.pin); } else { if (typeof fromPinInfo === 'string') { console.warn('Node ' + fromPinInfo + ' not exist'); } else { console.warn('Pin of ' + fromPinInfo.node + '.' + fromPinInfo.pin + ' not exist'); } } } } }, findPin: function (input) { var node; // Try to take input as a directly a node if (typeof input === 'string' || input instanceof CompositorNode) { input = { node: input }; } if (typeof input.node === 'string') { for (var i = 0; i < this.nodes.length; i++) { var tmp = this.nodes[i]; if (tmp.name === input.node) { node = tmp; } } } else { node = input.node; } if (node) { var inputPin = input.pin; if (!inputPin) { // Use first pin defaultly if (node.outputs) { inputPin = Object.keys(node.outputs)[0]; } } if (node.outputs[inputPin]) { return { node: node, pin: inputPin }; } } } }); /** * Compositor provide graph based post processing * * @constructor clay.compositor.Compositor * @extends clay.compositor.Graph * */ var Compositor = Graph.extend(function() { return { // Output node _outputs: [], _texturePool: new TexturePool(), _frameBuffer: new FrameBuffer({ depthBuffer: false }) }; }, /** @lends clay.compositor.Compositor.prototype */ { addNode: function(node) { Graph.prototype.addNode.call(this, node); node._compositor = this; }, /** * @param {clay.Renderer} renderer */ render: function(renderer, frameBuffer) { if (this._dirty) { this.update(); this._dirty = false; this._outputs.length = 0; for (var i = 0; i < this.nodes.length; i++) { if (!this.nodes[i].outputs) { this._outputs.push(this.nodes[i]); } } } for (var i = 0; i < this.nodes.length; i++) { // Update the reference number of each output texture this.nodes[i].beforeFrame(); } for (var i = 0; i < this._outputs.length; i++) { this._outputs[i].updateReference(); } for (var i = 0; i < this._outputs.length; i++) { this._outputs[i].render(renderer, frameBuffer); } for (var i = 0; i < this.nodes.length; i++) { // Clear up this.nodes[i].afterFrame(); } }, allocateTexture: function (parameters) { return this._texturePool.get(parameters); }, releaseTexture: function (parameters) { this._texturePool.put(parameters); }, getFrameBuffer: function () { return this._frameBuffer; }, /** * Dispose compositor * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._texturePool.clear(renderer); } }); /** * @constructor clay.compositor.SceneNode * @extends clay.compositor.CompositorNode */ var SceneNode$1 = CompositorNode.extend( /** @lends clay.compositor.SceneNode# */ { name: 'scene', /** * @type {clay.Scene} */ scene: null, /** * @type {clay.Camera} */ camera: null, /** * @type {boolean} */ autoUpdateScene: true, /** * @type {boolean} */ preZ: false }, function() { this.frameBuffer = new FrameBuffer(); }, { render: function(renderer) { this._rendering = true; var _gl = renderer.gl; this.trigger('beforerender'); var renderInfo; if (!this.outputs) { renderInfo = renderer.render(this.scene, this.camera, !this.autoUpdateScene, this.preZ); } else { var frameBuffer = this.frameBuffer; for (var name in this.outputs) { var parameters = this.updateParameter(name, renderer); var outputInfo = this.outputs[name]; var texture = this._compositor.allocateTexture(parameters); this._outputTextures[name] = texture; var attachment = outputInfo.attachment || _gl.COLOR_ATTACHMENT0; if (typeof(attachment) == 'string') { attachment = _gl[attachment]; } frameBuffer.attach(texture, attachment); } frameBuffer.bind(renderer); // MRT Support in chrome // https://www.khronos.org/registry/webgl/sdk/tests/conformance/extensions/ext-draw-buffers.html var ext = renderer.getGLExtension('EXT_draw_buffers'); if (ext) { var bufs = []; for (var attachment in this.outputs) { attachment = parseInt(attachment); if (attachment >= _gl.COLOR_ATTACHMENT0 && attachment <= _gl.COLOR_ATTACHMENT0 + 8) { bufs.push(attachment); } } ext.drawBuffersEXT(bufs); } // Always clear // PENDING renderer.saveClear(); renderer.clearBit = glenum.DEPTH_BUFFER_BIT | glenum.COLOR_BUFFER_BIT; renderInfo = renderer.render(this.scene, this.camera, !this.autoUpdateScene, this.preZ); renderer.restoreClear(); frameBuffer.unbind(renderer); } this.trigger('afterrender', renderInfo); this._rendering = false; this._rendered = true; } }); /** * @constructor clay.compositor.TextureNode * @extends clay.compositor.CompositorNode */ var TextureNode$1 = CompositorNode.extend(function() { return /** @lends clay.compositor.TextureNode# */ { /** * @type {clay.Texture2D} */ texture: null, // Texture node must have output without parameters outputs: { color: {} } }; }, function () { }, { getOutput: function (renderer, name) { return this.texture; }, // Do nothing beforeFrame: function () {}, afterFrame: function () {} }); // TODO Shader library // TODO curlnoise demo wrong // PENDING // Use topological sort ? /** * Filter node * * @constructor clay.compositor.FilterNode * @extends clay.compositor.CompositorNode * * @example var node = new clay.compositor.FilterNode({ name: 'fxaa', shader: clay.Shader.source('clay.compositor.fxaa'), inputs: { texture: { node: 'scene', pin: 'color' } }, // Multiple outputs is preserved for MRT support in WebGL2.0 outputs: { color: { attachment: clay.FrameBuffer.COLOR_ATTACHMENT0 parameters: { format: clay.Texture.RGBA, width: 512, height: 512 }, // Node will keep the RTT rendered in last frame keepLastFrame: true, // Force the node output the RTT rendered in last frame outputLastFrame: true } } }); * */ var FilterNode$1 = CompositorNode.extend(function () { return /** @lends clay.compositor.FilterNode# */ { /** * @type {string} */ name: '', /** * @type {Object} */ inputs: {}, /** * @type {Object} */ outputs: null, /** * @type {string} */ shader: '', /** * Input links, will be updated by the graph * @example: * inputName: { * node: someNode, * pin: 'xxxx' * } * @type {Object} */ inputLinks: {}, /** * Output links, will be updated by the graph * @example: * outputName: { * node: someNode, * pin: 'xxxx' * } * @type {Object} */ outputLinks: {}, /** * @type {clay.compositor.Pass} */ pass: null, // Save the output texture of previous frame // Will be used when there exist a circular reference _prevOutputTextures: {}, _outputTextures: {}, // Example: { name: 2 } _outputReferences: {}, _rendering: false, // If rendered in this frame _rendered: false, _compositor: null }; }, function () { var pass = new Pass({ fragment: this.shader }); this.pass = pass; }, /** @lends clay.compositor.FilterNode.prototype */ { /** * @param {clay.Renderer} renderer */ render: function (renderer, frameBuffer) { this.trigger('beforerender', renderer); this._rendering = true; var _gl = renderer.gl; for (var inputName in this.inputLinks) { var link = this.inputLinks[inputName]; var inputTexture = link.node.getOutput(renderer, link.pin); this.pass.setUniform(inputName, inputTexture); } // Output if (!this.outputs) { this.pass.outputs = null; this._compositor.getFrameBuffer().unbind(renderer); this.pass.render(renderer, frameBuffer); } else { this.pass.outputs = {}; var attachedTextures = {}; for (var name in this.outputs) { var parameters = this.updateParameter(name, renderer); if (isNaN(parameters.width)) { this.updateParameter(name, renderer); } var outputInfo = this.outputs[name]; var texture = this._compositor.allocateTexture(parameters); this._outputTextures[name] = texture; var attachment = outputInfo.attachment || _gl.COLOR_ATTACHMENT0; if (typeof(attachment) === 'string') { attachment = _gl[attachment]; } attachedTextures[attachment] = texture; } this._compositor.getFrameBuffer().bind(renderer); for (var attachment in attachedTextures) { // FIXME attachment changes in different nodes this._compositor.getFrameBuffer().attach( attachedTextures[attachment], attachment ); } this.pass.render(renderer); // Because the data of texture is changed over time, // Here update the mipmaps of texture each time after rendered; this._compositor.getFrameBuffer().updateMipmap(renderer); } for (var inputName in this.inputLinks) { var link = this.inputLinks[inputName]; link.node.removeReference(link.pin); } this._rendering = false; this._rendered = true; this.trigger('afterrender', renderer); }, // TODO Remove parameter function callback updateParameter: function (outputName, renderer) { var outputInfo = this.outputs[outputName]; var parameters = outputInfo.parameters; var parametersCopy = outputInfo._parametersCopy; if (!parametersCopy) { parametersCopy = outputInfo._parametersCopy = {}; } if (parameters) { for (var key in parameters) { if (key !== 'width' && key !== 'height') { parametersCopy[key] = parameters[key]; } } } var width, height; if (typeof parameters.width === 'function') { width = parameters.width.call(this, renderer); } else { width = parameters.width; } if (typeof parameters.height === 'function') { height = parameters.height.call(this, renderer); } else { height = parameters.height; } width = Math.ceil(width); height = Math.ceil(height); if ( parametersCopy.width !== width || parametersCopy.height !== height ) { if (this._outputTextures[outputName]) { this._outputTextures[outputName].dispose(renderer); } } parametersCopy.width = width; parametersCopy.height = height; return parametersCopy; }, /** * Set parameter * @param {string} name * @param {} value */ setParameter: function (name, value) { this.pass.setUniform(name, value); }, /** * Get parameter value * @param {string} name * @return {} */ getParameter: function (name) { return this.pass.getUniform(name); }, /** * Set parameters * @param {Object} obj */ setParameters: function (obj) { for (var name in obj) { this.setParameter(name, obj[name]); } }, // /** // * Set shader code // * @param {string} shaderStr // */ // setShader: function (shaderStr) { // var material = this.pass.material; // material.shader.setFragment(shaderStr); // material.attachShader(material.shader, true); // }, /** * Proxy of pass.material.define('fragment', xxx); * @param {string} symbol * @param {number} [val] */ define: function (symbol, val) { this.pass.material.define('fragment', symbol, val); }, /** * Proxy of pass.material.undefine('fragment', xxx) * @param {string} symbol */ undefine: function (symbol) { this.pass.material.undefine('fragment', symbol); }, removeReference: function (outputName) { this._outputReferences[outputName]--; if (this._outputReferences[outputName] === 0) { var outputInfo = this.outputs[outputName]; if (outputInfo.keepLastFrame) { if (this._prevOutputTextures[outputName]) { this._compositor.releaseTexture(this._prevOutputTextures[outputName]); } this._prevOutputTextures[outputName] = this._outputTextures[outputName]; } else { // Output of this node have alreay been used by all other nodes // Put the texture back to the pool. this._compositor.releaseTexture(this._outputTextures[outputName]); } } }, clear: function () { CompositorNode.prototype.clear.call(this); // Default disable all texture this.pass.material.disableTexturesAll(); } }); var coloradjustEssl = "@export clay.compositor.coloradjust\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float brightness : 0.0;\nuniform float contrast : 1.0;\nuniform float exposure : 0.0;\nuniform float gamma : 1.0;\nuniform float saturation : 1.0;\nconst vec3 w = vec3(0.2125, 0.7154, 0.0721);\nvoid main()\n{\n vec4 tex = texture2D( texture, v_Texcoord);\n vec3 color = clamp(tex.rgb + vec3(brightness), 0.0, 1.0);\n color = clamp( (color-vec3(0.5))*contrast+vec3(0.5), 0.0, 1.0);\n color = clamp( color * pow(2.0, exposure), 0.0, 1.0);\n color = clamp( pow(color, vec3(gamma)), 0.0, 1.0);\n float luminance = dot( color, w );\n color = mix(vec3(luminance), color, saturation);\n gl_FragColor = vec4(color, tex.a);\n}\n@end\n@export clay.compositor.brightness\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float brightness : 0.0;\nvoid main()\n{\n vec4 tex = texture2D( texture, v_Texcoord);\n vec3 color = tex.rgb + vec3(brightness);\n gl_FragColor = vec4(color, tex.a);\n}\n@end\n@export clay.compositor.contrast\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float contrast : 1.0;\nvoid main()\n{\n vec4 tex = texture2D( texture, v_Texcoord);\n vec3 color = (tex.rgb-vec3(0.5))*contrast+vec3(0.5);\n gl_FragColor = vec4(color, tex.a);\n}\n@end\n@export clay.compositor.exposure\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float exposure : 0.0;\nvoid main()\n{\n vec4 tex = texture2D(texture, v_Texcoord);\n vec3 color = tex.rgb * pow(2.0, exposure);\n gl_FragColor = vec4(color, tex.a);\n}\n@end\n@export clay.compositor.gamma\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float gamma : 1.0;\nvoid main()\n{\n vec4 tex = texture2D(texture, v_Texcoord);\n vec3 color = pow(tex.rgb, vec3(gamma));\n gl_FragColor = vec4(color, tex.a);\n}\n@end\n@export clay.compositor.saturation\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float saturation : 1.0;\nconst vec3 w = vec3(0.2125, 0.7154, 0.0721);\nvoid main()\n{\n vec4 tex = texture2D(texture, v_Texcoord);\n vec3 color = tex.rgb;\n float luminance = dot(color, w);\n color = mix(vec3(luminance), color, saturation);\n gl_FragColor = vec4(color, tex.a);\n}\n@end"; var blurEssl = "@export clay.compositor.kernel.gaussian_9\nfloat gaussianKernel[9];\ngaussianKernel[0] = 0.07;\ngaussianKernel[1] = 0.09;\ngaussianKernel[2] = 0.12;\ngaussianKernel[3] = 0.14;\ngaussianKernel[4] = 0.16;\ngaussianKernel[5] = 0.14;\ngaussianKernel[6] = 0.12;\ngaussianKernel[7] = 0.09;\ngaussianKernel[8] = 0.07;\n@end\n@export clay.compositor.kernel.gaussian_13\nfloat gaussianKernel[13];\ngaussianKernel[0] = 0.02;\ngaussianKernel[1] = 0.03;\ngaussianKernel[2] = 0.06;\ngaussianKernel[3] = 0.08;\ngaussianKernel[4] = 0.11;\ngaussianKernel[5] = 0.13;\ngaussianKernel[6] = 0.14;\ngaussianKernel[7] = 0.13;\ngaussianKernel[8] = 0.11;\ngaussianKernel[9] = 0.08;\ngaussianKernel[10] = 0.06;\ngaussianKernel[11] = 0.03;\ngaussianKernel[12] = 0.02;\n@end\n@export clay.compositor.gaussian_blur\n#define SHADER_NAME gaussian_blur\nuniform sampler2D texture;varying vec2 v_Texcoord;\nuniform float blurSize : 2.0;\nuniform vec2 textureSize : [512.0, 512.0];\nuniform float blurDir : 0.0;\n@import clay.util.rgbm\n@import clay.util.clamp_sample\nvoid main (void)\n{\n @import clay.compositor.kernel.gaussian_9\n vec2 off = blurSize / textureSize;\n off *= vec2(1.0 - blurDir, blurDir);\n vec4 sum = vec4(0.0);\n float weightAll = 0.0;\n for (int i = 0; i < 9; i++) {\n float w = gaussianKernel[i];\n vec4 texel = decodeHDR(clampSample(texture, v_Texcoord + float(i - 4) * off));\n sum += texel * w;\n weightAll += w;\n }\n gl_FragColor = encodeHDR(sum / max(weightAll, 0.01));\n}\n@end\n"; var lumEssl = "@export clay.compositor.hdr.log_lum\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nconst vec3 w = vec3(0.2125, 0.7154, 0.0721);\n@import clay.util.rgbm\nvoid main()\n{\n vec4 tex = decodeHDR(texture2D(texture, v_Texcoord));\n float luminance = dot(tex.rgb, w);\n luminance = log(luminance + 0.001);\n gl_FragColor = encodeHDR(vec4(vec3(luminance), 1.0));\n}\n@end\n@export clay.compositor.hdr.lum_adaption\nvarying vec2 v_Texcoord;\nuniform sampler2D adaptedLum;\nuniform sampler2D currentLum;\nuniform float frameTime : 0.02;\n@import clay.util.rgbm\nvoid main()\n{\n float fAdaptedLum = decodeHDR(texture2D(adaptedLum, vec2(0.5, 0.5))).r;\n float fCurrentLum = exp(encodeHDR(texture2D(currentLum, vec2(0.5, 0.5))).r);\n fAdaptedLum += (fCurrentLum - fAdaptedLum) * (1.0 - pow(0.98, 30.0 * frameTime));\n gl_FragColor = encodeHDR(vec4(vec3(fAdaptedLum), 1.0));\n}\n@end\n@export clay.compositor.lum\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nconst vec3 w = vec3(0.2125, 0.7154, 0.0721);\nvoid main()\n{\n vec4 tex = texture2D( texture, v_Texcoord );\n float luminance = dot(tex.rgb, w);\n gl_FragColor = vec4(vec3(luminance), 1.0);\n}\n@end"; var lutEssl = "\n@export clay.compositor.lut\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform sampler2D lookup;\nvoid main()\n{\n vec4 tex = texture2D(texture, v_Texcoord);\n float blueColor = tex.b * 63.0;\n vec2 quad1;\n quad1.y = floor(floor(blueColor) / 8.0);\n quad1.x = floor(blueColor) - (quad1.y * 8.0);\n vec2 quad2;\n quad2.y = floor(ceil(blueColor) / 8.0);\n quad2.x = ceil(blueColor) - (quad2.y * 8.0);\n vec2 texPos1;\n texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * tex.r);\n texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * tex.g);\n vec2 texPos2;\n texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * tex.r);\n texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * tex.g);\n vec4 newColor1 = texture2D(lookup, texPos1);\n vec4 newColor2 = texture2D(lookup, texPos2);\n vec4 newColor = mix(newColor1, newColor2, fract(blueColor));\n gl_FragColor = vec4(newColor.rgb, tex.w);\n}\n@end"; var vigentteEssl = "@export clay.compositor.vignette\n#define OUTPUT_ALPHA\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\nuniform float darkness: 1;\nuniform float offset: 1;\n@import clay.util.rgbm\nvoid main()\n{\n vec4 texel = decodeHDR(texture2D(texture, v_Texcoord));\n gl_FragColor.rgb = texel.rgb;\n vec2 uv = (v_Texcoord - vec2(0.5)) * vec2(offset);\n gl_FragColor = encodeHDR(vec4(mix(texel.rgb, vec3(1.0 - darkness), dot(uv, uv)), texel.a));\n}\n@end"; var outputEssl = "@export clay.compositor.output\n#define OUTPUT_ALPHA\nvarying vec2 v_Texcoord;\nuniform sampler2D texture;\n@import clay.util.rgbm\nvoid main()\n{\n vec4 tex = decodeHDR(texture2D(texture, v_Texcoord));\n gl_FragColor.rgb = tex.rgb;\n#ifdef OUTPUT_ALPHA\n gl_FragColor.a = tex.a;\n#else\n gl_FragColor.a = 1.0;\n#endif\n gl_FragColor = encodeHDR(gl_FragColor);\n#ifdef PREMULTIPLY_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n@end"; var brightEssl = "@export clay.compositor.bright\nuniform sampler2D texture;\nuniform float threshold : 1;\nuniform float scale : 1.0;\nuniform vec2 textureSize: [512, 512];\nvarying vec2 v_Texcoord;\nconst vec3 lumWeight = vec3(0.2125, 0.7154, 0.0721);\n@import clay.util.rgbm\nvec4 median(vec4 a, vec4 b, vec4 c)\n{\n return a + b + c - min(min(a, b), c) - max(max(a, b), c);\n}\nvoid main()\n{\n vec4 texel = decodeHDR(texture2D(texture, v_Texcoord));\n#ifdef ANTI_FLICKER\n vec3 d = 1.0 / textureSize.xyx * vec3(1.0, 1.0, 0.0);\n vec4 s1 = decodeHDR(texture2D(texture, v_Texcoord - d.xz));\n vec4 s2 = decodeHDR(texture2D(texture, v_Texcoord + d.xz));\n vec4 s3 = decodeHDR(texture2D(texture, v_Texcoord - d.zy));\n vec4 s4 = decodeHDR(texture2D(texture, v_Texcoord + d.zy));\n texel = median(median(texel, s1, s2), s3, s4);\n#endif\n float lum = dot(texel.rgb , lumWeight);\n vec4 color;\n if (lum > threshold && texel.a > 0.0)\n {\n color = vec4(texel.rgb * scale, texel.a * scale);\n }\n else\n {\n color = vec4(0.0);\n }\n gl_FragColor = encodeHDR(color);\n}\n@end\n"; var downsampleEssl = "@export clay.compositor.downsample\nuniform sampler2D texture;\nuniform vec2 textureSize : [512, 512];\nvarying vec2 v_Texcoord;\n@import clay.util.rgbm\nfloat brightness(vec3 c)\n{\n return max(max(c.r, c.g), c.b);\n}\n@import clay.util.clamp_sample\nvoid main()\n{\n vec4 d = vec4(-1.0, -1.0, 1.0, 1.0) / textureSize.xyxy;\n#ifdef ANTI_FLICKER\n vec3 s1 = decodeHDR(clampSample(texture, v_Texcoord + d.xy)).rgb;\n vec3 s2 = decodeHDR(clampSample(texture, v_Texcoord + d.zy)).rgb;\n vec3 s3 = decodeHDR(clampSample(texture, v_Texcoord + d.xw)).rgb;\n vec3 s4 = decodeHDR(clampSample(texture, v_Texcoord + d.zw)).rgb;\n float s1w = 1.0 / (brightness(s1) + 1.0);\n float s2w = 1.0 / (brightness(s2) + 1.0);\n float s3w = 1.0 / (brightness(s3) + 1.0);\n float s4w = 1.0 / (brightness(s4) + 1.0);\n float oneDivideSum = 1.0 / (s1w + s2w + s3w + s4w);\n vec4 color = vec4(\n (s1 * s1w + s2 * s2w + s3 * s3w + s4 * s4w) * oneDivideSum,\n 1.0\n );\n#else\n vec4 color = decodeHDR(clampSample(texture, v_Texcoord + d.xy));\n color += decodeHDR(clampSample(texture, v_Texcoord + d.zy));\n color += decodeHDR(clampSample(texture, v_Texcoord + d.xw));\n color += decodeHDR(clampSample(texture, v_Texcoord + d.zw));\n color *= 0.25;\n#endif\n gl_FragColor = encodeHDR(color);\n}\n@end"; var upsampleEssl = "\n@export clay.compositor.upsample\n#define HIGH_QUALITY\nuniform sampler2D texture;\nuniform vec2 textureSize : [512, 512];\nuniform float sampleScale: 0.5;\nvarying vec2 v_Texcoord;\n@import clay.util.rgbm\n@import clay.util.clamp_sample\nvoid main()\n{\n#ifdef HIGH_QUALITY\n vec4 d = vec4(1.0, 1.0, -1.0, 0.0) / textureSize.xyxy * sampleScale;\n vec4 s;\n s = decodeHDR(clampSample(texture, v_Texcoord - d.xy));\n s += decodeHDR(clampSample(texture, v_Texcoord - d.wy)) * 2.0;\n s += decodeHDR(clampSample(texture, v_Texcoord - d.zy));\n s += decodeHDR(clampSample(texture, v_Texcoord + d.zw)) * 2.0;\n s += decodeHDR(clampSample(texture, v_Texcoord )) * 4.0;\n s += decodeHDR(clampSample(texture, v_Texcoord + d.xw)) * 2.0;\n s += decodeHDR(clampSample(texture, v_Texcoord + d.zy));\n s += decodeHDR(clampSample(texture, v_Texcoord + d.wy)) * 2.0;\n s += decodeHDR(clampSample(texture, v_Texcoord + d.xy));\n gl_FragColor = encodeHDR(s / 16.0);\n#else\n vec4 d = vec4(-1.0, -1.0, +1.0, +1.0) / textureSize.xyxy;\n vec4 s;\n s = decodeHDR(clampSample(texture, v_Texcoord + d.xy));\n s += decodeHDR(clampSample(texture, v_Texcoord + d.zy));\n s += decodeHDR(clampSample(texture, v_Texcoord + d.xw));\n s += decodeHDR(clampSample(texture, v_Texcoord + d.zw));\n gl_FragColor = encodeHDR(s / 4.0);\n#endif\n}\n@end"; var hdrEssl = "@export clay.compositor.hdr.composite\n#define TONEMAPPING\nuniform sampler2D texture;\n#ifdef BLOOM_ENABLED\nuniform sampler2D bloom;\n#endif\n#ifdef LENSFLARE_ENABLED\nuniform sampler2D lensflare;\nuniform sampler2D lensdirt;\n#endif\n#ifdef LUM_ENABLED\nuniform sampler2D lum;\n#endif\n#ifdef LUT_ENABLED\nuniform sampler2D lut;\n#endif\n#ifdef COLOR_CORRECTION\nuniform float brightness : 0.0;\nuniform float contrast : 1.0;\nuniform float saturation : 1.0;\n#endif\n#ifdef VIGNETTE\nuniform float vignetteDarkness: 1.0;\nuniform float vignetteOffset: 1.0;\n#endif\nuniform float exposure : 1.0;\nuniform float bloomIntensity : 0.25;\nuniform float lensflareIntensity : 1;\nvarying vec2 v_Texcoord;\n@import clay.util.srgb\nvec3 ACESToneMapping(vec3 color)\n{\n const float A = 2.51;\n const float B = 0.03;\n const float C = 2.43;\n const float D = 0.59;\n const float E = 0.14;\n return (color * (A * color + B)) / (color * (C * color + D) + E);\n}\nfloat eyeAdaption(float fLum)\n{\n return mix(0.2, fLum, 0.5);\n}\n#ifdef LUT_ENABLED\nvec3 lutTransform(vec3 color) {\n float blueColor = color.b * 63.0;\n vec2 quad1;\n quad1.y = floor(floor(blueColor) / 8.0);\n quad1.x = floor(blueColor) - (quad1.y * 8.0);\n vec2 quad2;\n quad2.y = floor(ceil(blueColor) / 8.0);\n quad2.x = ceil(blueColor) - (quad2.y * 8.0);\n vec2 texPos1;\n texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);\n texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);\n vec2 texPos2;\n texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r);\n texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g);\n vec4 newColor1 = texture2D(lut, texPos1);\n vec4 newColor2 = texture2D(lut, texPos2);\n vec4 newColor = mix(newColor1, newColor2, fract(blueColor));\n return newColor.rgb;\n}\n#endif\n@import clay.util.rgbm\nvoid main()\n{\n vec4 texel = vec4(0.0);\n vec4 originalTexel = vec4(0.0);\n#ifdef TEXTURE_ENABLED\n texel = decodeHDR(texture2D(texture, v_Texcoord));\n originalTexel = texel;\n#endif\n#ifdef BLOOM_ENABLED\n vec4 bloomTexel = decodeHDR(texture2D(bloom, v_Texcoord));\n texel.rgb += bloomTexel.rgb * bloomIntensity;\n texel.a += bloomTexel.a * bloomIntensity;\n#endif\n#ifdef LENSFLARE_ENABLED\n texel += decodeHDR(texture2D(lensflare, v_Texcoord)) * texture2D(lensdirt, v_Texcoord) * lensflareIntensity;\n#endif\n texel.a = min(texel.a, 1.0);\n#ifdef LUM_ENABLED\n float fLum = texture2D(lum, vec2(0.5, 0.5)).r;\n float adaptedLumDest = 3.0 / (max(0.1, 1.0 + 10.0*eyeAdaption(fLum)));\n float exposureBias = adaptedLumDest * exposure;\n#else\n float exposureBias = exposure;\n#endif\n#ifdef TONEMAPPING\n texel.rgb *= exposureBias;\n texel.rgb = ACESToneMapping(texel.rgb);\n#endif\n texel = linearTosRGB(texel);\n#ifdef LUT_ENABLED\n texel.rgb = lutTransform(clamp(texel.rgb,vec3(0.0),vec3(1.0)));\n#endif\n#ifdef COLOR_CORRECTION\n texel.rgb = clamp(texel.rgb + vec3(brightness), 0.0, 1.0);\n texel.rgb = clamp((texel.rgb - vec3(0.5))*contrast+vec3(0.5), 0.0, 1.0);\n float lum = dot(texel.rgb, vec3(0.2125, 0.7154, 0.0721));\n texel.rgb = mix(vec3(lum), texel.rgb, saturation);\n#endif\n#ifdef VIGNETTE\n vec2 uv = (v_Texcoord - vec2(0.5)) * vec2(vignetteOffset);\n texel.rgb = mix(texel.rgb, vec3(1.0 - vignetteDarkness), dot(uv, uv));\n#endif\n gl_FragColor = encodeHDR(texel);\n#ifdef DEBUG\n #if DEBUG == 1\n gl_FragColor = encodeHDR(decodeHDR(texture2D(texture, v_Texcoord)));\n #elif DEBUG == 2\n gl_FragColor = encodeHDR(decodeHDR(texture2D(bloom, v_Texcoord)) * bloomIntensity);\n #elif DEBUG == 3\n gl_FragColor = encodeHDR(decodeHDR(texture2D(lensflare, v_Texcoord) * lensflareIntensity));\n #endif\n#endif\n if (originalTexel.a <= 0.01 && gl_FragColor.a > 1e-5) {\n gl_FragColor.a = dot(gl_FragColor.rgb, vec3(0.2125, 0.7154, 0.0721));\n }\n#ifdef PREMULTIPLY_ALPHA\n gl_FragColor.rgb *= gl_FragColor.a;\n#endif\n}\n@end"; var lensflareEssl = "@export clay.compositor.lensflare\n#define SAMPLE_NUMBER 8\nuniform sampler2D texture;\nuniform sampler2D lenscolor;\nuniform vec2 textureSize : [512, 512];\nuniform float dispersal : 0.3;\nuniform float haloWidth : 0.4;\nuniform float distortion : 1.0;\nvarying vec2 v_Texcoord;\n@import clay.util.rgbm\nvec4 textureDistorted(\n in vec2 texcoord,\n in vec2 direction,\n in vec3 distortion\n) {\n return vec4(\n decodeHDR(texture2D(texture, texcoord + direction * distortion.r)).r,\n decodeHDR(texture2D(texture, texcoord + direction * distortion.g)).g,\n decodeHDR(texture2D(texture, texcoord + direction * distortion.b)).b,\n 1.0\n );\n}\nvoid main()\n{\n vec2 texcoord = -v_Texcoord + vec2(1.0); vec2 textureOffset = 1.0 / textureSize;\n vec2 ghostVec = (vec2(0.5) - texcoord) * dispersal;\n vec2 haloVec = normalize(ghostVec) * haloWidth;\n vec3 distortion = vec3(-textureOffset.x * distortion, 0.0, textureOffset.x * distortion);\n vec4 result = vec4(0.0);\n for (int i = 0; i < SAMPLE_NUMBER; i++)\n {\n vec2 offset = fract(texcoord + ghostVec * float(i));\n float weight = length(vec2(0.5) - offset) / length(vec2(0.5));\n weight = pow(1.0 - weight, 10.0);\n result += textureDistorted(offset, normalize(ghostVec), distortion) * weight;\n }\n result *= texture2D(lenscolor, vec2(length(vec2(0.5) - texcoord)) / length(vec2(0.5)));\n float weight = length(vec2(0.5) - fract(texcoord + haloVec)) / length(vec2(0.5));\n weight = pow(1.0 - weight, 10.0);\n vec2 offset = fract(texcoord + haloVec);\n result += textureDistorted(offset, normalize(ghostVec), distortion) * weight;\n gl_FragColor = result;\n}\n@end"; var blendEssl = "@export clay.compositor.blend\n#define SHADER_NAME blend\n#ifdef TEXTURE1_ENABLED\nuniform sampler2D texture1;\nuniform float weight1 : 1.0;\n#endif\n#ifdef TEXTURE2_ENABLED\nuniform sampler2D texture2;\nuniform float weight2 : 1.0;\n#endif\n#ifdef TEXTURE3_ENABLED\nuniform sampler2D texture3;\nuniform float weight3 : 1.0;\n#endif\n#ifdef TEXTURE4_ENABLED\nuniform sampler2D texture4;\nuniform float weight4 : 1.0;\n#endif\n#ifdef TEXTURE5_ENABLED\nuniform sampler2D texture5;\nuniform float weight5 : 1.0;\n#endif\n#ifdef TEXTURE6_ENABLED\nuniform sampler2D texture6;\nuniform float weight6 : 1.0;\n#endif\nvarying vec2 v_Texcoord;\n@import clay.util.rgbm\nvoid main()\n{\n vec4 tex = vec4(0.0);\n#ifdef TEXTURE1_ENABLED\n tex += decodeHDR(texture2D(texture1, v_Texcoord)) * weight1;\n#endif\n#ifdef TEXTURE2_ENABLED\n tex += decodeHDR(texture2D(texture2, v_Texcoord)) * weight2;\n#endif\n#ifdef TEXTURE3_ENABLED\n tex += decodeHDR(texture2D(texture3, v_Texcoord)) * weight3;\n#endif\n#ifdef TEXTURE4_ENABLED\n tex += decodeHDR(texture2D(texture4, v_Texcoord)) * weight4;\n#endif\n#ifdef TEXTURE5_ENABLED\n tex += decodeHDR(texture2D(texture5, v_Texcoord)) * weight5;\n#endif\n#ifdef TEXTURE6_ENABLED\n tex += decodeHDR(texture2D(texture6, v_Texcoord)) * weight6;\n#endif\n gl_FragColor = encodeHDR(tex);\n}\n@end"; var fxaaEssl = "@export clay.compositor.fxaa\nuniform sampler2D texture;\nuniform vec4 viewport : VIEWPORT;\nvarying vec2 v_Texcoord;\n#define FXAA_REDUCE_MIN (1.0/128.0)\n#define FXAA_REDUCE_MUL (1.0/8.0)\n#define FXAA_SPAN_MAX 8.0\n@import clay.util.rgbm\nvoid main()\n{\n vec2 resolution = 1.0 / viewport.zw;\n vec3 rgbNW = decodeHDR( texture2D( texture, ( gl_FragCoord.xy + vec2( -1.0, -1.0 ) ) * resolution ) ).xyz;\n vec3 rgbNE = decodeHDR( texture2D( texture, ( gl_FragCoord.xy + vec2( 1.0, -1.0 ) ) * resolution ) ).xyz;\n vec3 rgbSW = decodeHDR( texture2D( texture, ( gl_FragCoord.xy + vec2( -1.0, 1.0 ) ) * resolution ) ).xyz;\n vec3 rgbSE = decodeHDR( texture2D( texture, ( gl_FragCoord.xy + vec2( 1.0, 1.0 ) ) * resolution ) ).xyz;\n vec4 rgbaM = decodeHDR( texture2D( texture, gl_FragCoord.xy * resolution ) );\n vec3 rgbM = rgbaM.xyz;\n float opacity = rgbaM.w;\n vec3 luma = vec3( 0.299, 0.587, 0.114 );\n float lumaNW = dot( rgbNW, luma );\n float lumaNE = dot( rgbNE, luma );\n float lumaSW = dot( rgbSW, luma );\n float lumaSE = dot( rgbSE, luma );\n float lumaM = dot( rgbM, luma );\n float lumaMin = min( lumaM, min( min( lumaNW, lumaNE ), min( lumaSW, lumaSE ) ) );\n float lumaMax = max( lumaM, max( max( lumaNW, lumaNE) , max( lumaSW, lumaSE ) ) );\n vec2 dir;\n dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));\n dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE));\n float dirReduce = max( ( lumaNW + lumaNE + lumaSW + lumaSE ) * ( 0.25 * FXAA_REDUCE_MUL ), FXAA_REDUCE_MIN );\n float rcpDirMin = 1.0 / ( min( abs( dir.x ), abs( dir.y ) ) + dirReduce );\n dir = min( vec2( FXAA_SPAN_MAX, FXAA_SPAN_MAX),\n max( vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),\n dir * rcpDirMin)) * resolution;\n vec3 rgbA = decodeHDR( texture2D( texture, gl_FragCoord.xy * resolution + dir * ( 1.0 / 3.0 - 0.5 ) ) ).xyz;\n rgbA += decodeHDR( texture2D( texture, gl_FragCoord.xy * resolution + dir * ( 2.0 / 3.0 - 0.5 ) ) ).xyz;\n rgbA *= 0.5;\n vec3 rgbB = decodeHDR( texture2D( texture, gl_FragCoord.xy * resolution + dir * -0.5 ) ).xyz;\n rgbB += decodeHDR( texture2D( texture, gl_FragCoord.xy * resolution + dir * 0.5 ) ).xyz;\n rgbB *= 0.25;\n rgbB += rgbA * 0.5;\n float lumaB = dot( rgbB, luma );\n if ( ( lumaB < lumaMin ) || ( lumaB > lumaMax ) )\n {\n gl_FragColor = vec4( rgbA, opacity );\n }\n else {\n gl_FragColor = vec4( rgbB, opacity );\n }\n}\n@end"; // import fxaa3Essl from './source/compositor/fxaa3.glsl.js'; // TODO Must export a module and be used in the other modules. Or it will be tree shaked function register(Shader) { // Some build in shaders Shader['import'](coloradjustEssl); Shader['import'](blurEssl); Shader['import'](lumEssl); Shader['import'](lutEssl); Shader['import'](vigentteEssl); Shader['import'](outputEssl); Shader['import'](brightEssl); Shader['import'](downsampleEssl); Shader['import'](upsampleEssl); Shader['import'](hdrEssl); Shader['import'](lensflareEssl); Shader['import'](blendEssl); Shader['import'](fxaaEssl); } register(Shader); var shaderSourceReg = /^#source\((.*?)\)/; /** * @name clay.createCompositor * @function * @param {Object} json * @param {Object} [opts] * @return {clay.compositor.Compositor} */ function createCompositor(json, opts) { var compositor = new Compositor(); opts = opts || {}; var lib = { textures: {}, parameters: {} }; var afterLoad = function(shaderLib, textureLib) { for (var i = 0; i < json.nodes.length; i++) { var nodeInfo = json.nodes[i]; var node = createNode(nodeInfo, lib, opts); if (node) { compositor.addNode(node); } } }; for (var name in json.parameters) { var paramInfo = json.parameters[name]; lib.parameters[name] = convertParameter(paramInfo); } // TODO load texture asynchronous loadTextures(json, lib, opts, function(textureLib) { lib.textures = textureLib; afterLoad(); }); return compositor; } function createNode(nodeInfo, lib, opts) { var type = nodeInfo.type || 'filter'; var shaderSource; var inputs; var outputs; if (type === 'filter') { var shaderExp = nodeInfo.shader.trim(); var res = shaderSourceReg.exec(shaderExp); if (res) { shaderSource = Shader.source(res[1].trim()); } else if (shaderExp.charAt(0) === '#') { shaderSource = lib.shaders[shaderExp.substr(1)]; } if (!shaderSource) { shaderSource = shaderExp; } if (!shaderSource) { return; } } if (nodeInfo.inputs) { inputs = {}; for (var name in nodeInfo.inputs) { if (typeof nodeInfo.inputs[name] === 'string') { inputs[name] = nodeInfo.inputs[name]; } else { inputs[name] = { node: nodeInfo.inputs[name].node, pin: nodeInfo.inputs[name].pin }; } } } if (nodeInfo.outputs) { outputs = {}; for (var name in nodeInfo.outputs) { var outputInfo = nodeInfo.outputs[name]; outputs[name] = {}; if (outputInfo.attachment != null) { outputs[name].attachment = outputInfo.attachment; } if (outputInfo.keepLastFrame != null) { outputs[name].keepLastFrame = outputInfo.keepLastFrame; } if (outputInfo.outputLastFrame != null) { outputs[name].outputLastFrame = outputInfo.outputLastFrame; } if (outputInfo.parameters) { outputs[name].parameters = convertParameter(outputInfo.parameters); } } } var node; if (type === 'scene') { node = new SceneNode$1({ name: nodeInfo.name, scene: opts.scene, camera: opts.camera, outputs: outputs }); } else if (type === 'texture') { node = new TextureNode$1({ name: nodeInfo.name, outputs: outputs }); } // Default is filter else { node = new FilterNode$1({ name: nodeInfo.name, shader: shaderSource, inputs: inputs, outputs: outputs }); } if (node) { if (nodeInfo.parameters) { for (var name in nodeInfo.parameters) { var val = nodeInfo.parameters[name]; if (typeof val === 'string') { val = val.trim(); if (val.charAt(0) === '#') { val = lib.textures[val.substr(1)]; } else { node.on( 'beforerender', createSizeSetHandler( name, tryConvertExpr(val) ) ); } } else if (typeof val === 'function') { node.on('beforerender', val); } node.setParameter(name, val); } } if (nodeInfo.defines && node.pass) { for (var name in nodeInfo.defines) { var val = nodeInfo.defines[name]; node.pass.material.define('fragment', name, val); } } } return node; } function defaultWidthFunc(width, height) { return width; } function defaultHeightFunc(width, height) { return height; } function convertParameter(paramInfo) { var param = {}; if (!paramInfo) { return param; } ['type', 'minFilter', 'magFilter', 'wrapS', 'wrapT', 'flipY', 'useMipmap'] .forEach(function(name) { var val = paramInfo[name]; if (val != null) { // Convert string to enum if (typeof val === 'string') { val = Texture[val]; } param[name] = val; } }); var sizeScale = paramInfo.scale || 1; ['width', 'height'] .forEach(function(name) { if (paramInfo[name] != null) { var val = paramInfo[name]; if (typeof val === 'string') { val = val.trim(); param[name] = createSizeParser( name, tryConvertExpr(val), sizeScale ); } else { param[name] = val; } } }); if (!param.width) { param.width = defaultWidthFunc; } if (!param.height) { param.height = defaultHeightFunc; } if (paramInfo.useMipmap != null) { param.useMipmap = paramInfo.useMipmap; } return param; } function loadTextures(json, lib, opts, callback) { if (!json.textures) { callback({}); return; } var textures = {}; var loading = 0; var cbd = false; var textureRootPath = opts.textureRootPath; util$1.each(json.textures, function(textureInfo, name) { var texture; var path = textureInfo.path; var parameters = convertParameter(textureInfo.parameters); if (Array.isArray(path) && path.length === 6) { if (textureRootPath) { path = path.map(function(item) { return util$1.relative2absolute(item, textureRootPath); }); } texture = new TextureCube(parameters); } else if(typeof path === 'string') { if (textureRootPath) { path = util$1.relative2absolute(path, textureRootPath); } texture = new Texture2D(parameters); } else { return; } texture.load(path); loading++; texture.once('success', function() { textures[name] = texture; loading--; if (loading === 0) { callback(textures); cbd = true; } }); }); if (loading === 0 && !cbd) { callback(textures); } } function createSizeSetHandler(name, exprFunc) { return function (renderer) { // PENDING viewport size or window size var dpr = renderer.getDevicePixelRatio(); // PENDING If multiply dpr ? var width = renderer.getWidth(); var height = renderer.getHeight(); var result = exprFunc(width, height, dpr); this.setParameter(name, result); }; } function createSizeParser(name, exprFunc, scale) { scale = scale || 1; return function (renderer) { var dpr = renderer.getDevicePixelRatio(); var width = renderer.getWidth() * scale; var height = renderer.getHeight() * scale; return exprFunc(width, height, dpr); }; } function tryConvertExpr(string) { // PENDING var exprRes = /^expr\((.*)\)$/.exec(string); if (exprRes) { try { var func = new Function('width', 'height', 'dpr', 'return ' + exprRes[1]); // Try run t func(1, 1); return func; } catch (e) { throw new Error('Invalid expression.'); } } } // DEPRECATED var gbufferEssl = "@export clay.deferred.gbuffer.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\n#if defined(SECOND_PASS) || defined(FIRST_PASS)\nattribute vec2 texcoord : TEXCOORD_0;\nuniform vec2 uvRepeat;\nuniform vec2 uvOffset;\nvarying vec2 v_Texcoord;\n#endif\n#ifdef FIRST_PASS\nuniform mat4 worldInverseTranspose : WORLDINVERSETRANSPOSE;\nuniform mat4 world : WORLD;\nvarying vec3 v_Normal;\nattribute vec3 normal : NORMAL;\nattribute vec4 tangent : TANGENT;\nvarying vec3 v_Tangent;\nvarying vec3 v_Bitangent;\nvarying vec3 v_WorldPosition;\n#endif\n@import clay.chunk.skinning_header\n#ifdef THIRD_PASS\nuniform mat4 prevWorldViewProjection;\nvarying vec4 v_ViewPosition;\nvarying vec4 v_PrevViewPosition;\n#ifdef SKINNING\n#ifdef USE_SKIN_MATRICES_TEXTURE\nuniform sampler2D prevSkinMatricesTexture;\nmat4 getPrevSkinMatrix(float idx) {\n return getSkinMatrix(prevSkinMatricesTexture, idx);\n}\n#else\nuniform mat4 prevSkinMatrix[JOINT_COUNT];\nmat4 getPrevSkinMatrix(float idx) {\n return prevSkinMatrix[int(idx)];\n}\n#endif\n#endif\n#endif\nvoid main()\n{\n vec3 skinnedPosition = position;\n vec3 prevSkinnedPosition = position;\n#ifdef FIRST_PASS\n vec3 skinnedNormal = normal;\n vec3 skinnedTangent = tangent.xyz;\n bool hasTangent = dot(tangent, tangent) > 0.0;\n#endif\n#ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = (skinMatrixWS * vec4(position, 1.0)).xyz;\n #ifdef FIRST_PASS\n skinnedNormal = (skinMatrixWS * vec4(normal, 0.0)).xyz;\n if (hasTangent) {\n skinnedTangent = (skinMatrixWS * vec4(tangent.xyz, 0.0)).xyz;\n }\n #endif\n #ifdef THIRD_PASS\n {\n mat4 prevSkinMatrixWS = getPrevSkinMatrix(joint.x) * weight.x;\n if (weight.y > 1e-4) { prevSkinMatrixWS += getPrevSkinMatrix(joint.y) * weight.y; }\n if (weight.z > 1e-4) { prevSkinMatrixWS += getPrevSkinMatrix(joint.z) * weight.z; }\n float weightW = 1.0-weight.x-weight.y-weight.z;\n if (weightW > 1e-4) { prevSkinMatrixWS += getPrevSkinMatrix(joint.w) * weightW; }\n prevSkinnedPosition = (prevSkinMatrixWS * vec4(position, 1.0)).xyz;\n }\n #endif\n#endif\n#if defined(SECOND_PASS) || defined(FIRST_PASS)\n v_Texcoord = texcoord * uvRepeat + uvOffset;\n#endif\n#ifdef FIRST_PASS\n v_Normal = normalize((worldInverseTranspose * vec4(skinnedNormal, 0.0)).xyz);\n if (hasTangent) {\n v_Tangent = normalize((worldInverseTranspose * vec4(skinnedTangent, 0.0)).xyz);\n v_Bitangent = normalize(cross(v_Normal, v_Tangent) * tangent.w);\n }\n v_WorldPosition = (world * vec4(skinnedPosition, 1.0)).xyz;\n#endif\n#ifdef THIRD_PASS\n v_ViewPosition = worldViewProjection * vec4(skinnedPosition, 1.0);\n v_PrevViewPosition = prevWorldViewProjection * vec4(prevSkinnedPosition, 1.0);\n#endif\n gl_Position = worldViewProjection * vec4(skinnedPosition, 1.0);\n}\n@end\n@export clay.deferred.gbuffer1.fragment\nuniform mat4 viewInverse : VIEWINVERSE;\nuniform float glossiness;\nvarying vec2 v_Texcoord;\nvarying vec3 v_Normal;\nvarying vec3 v_WorldPosition;\nuniform sampler2D normalMap;\nuniform sampler2D diffuseMap;\nvarying vec3 v_Tangent;\nvarying vec3 v_Bitangent;\nuniform sampler2D roughGlossMap;\nuniform bool useRoughGlossMap;\nuniform bool useRoughness;\nuniform bool doubleSided;\nuniform float alphaCutoff: 0.0;\nuniform float alpha: 1.0;\nuniform int roughGlossChannel: 0;\nfloat indexingTexel(in vec4 texel, in int idx) {\n if (idx == 3) return texel.a;\n else if (idx == 1) return texel.g;\n else if (idx == 2) return texel.b;\n else return texel.r;\n}\nvoid main()\n{\n vec3 N = v_Normal;\n if (doubleSided) {\n vec3 eyePos = viewInverse[3].xyz;\n vec3 V = eyePos - v_WorldPosition;\n if (dot(N, V) < 0.0) {\n N = -N;\n }\n }\n if (alphaCutoff > 0.0) {\n float a = texture2D(diffuseMap, v_Texcoord).a * alpha;\n if (a < alphaCutoff) {\n discard;\n }\n }\n if (dot(v_Tangent, v_Tangent) > 0.0) {\n vec3 normalTexel = texture2D(normalMap, v_Texcoord).xyz;\n if (dot(normalTexel, normalTexel) > 0.0) { N = normalTexel * 2.0 - 1.0;\n mat3 tbn = mat3(v_Tangent, v_Bitangent, v_Normal);\n N = normalize(tbn * N);\n }\n }\n gl_FragColor.rgb = (N + 1.0) * 0.5;\n float g = glossiness;\n if (useRoughGlossMap) {\n float g2 = indexingTexel(texture2D(roughGlossMap, v_Texcoord), roughGlossChannel);\n if (useRoughness) {\n g2 = 1.0 - g2;\n }\n g = clamp(g2 + (g - 0.5) * 2.0, 0.0, 1.0);\n }\n gl_FragColor.a = g + 0.005;\n}\n@end\n@export clay.deferred.gbuffer2.fragment\nuniform sampler2D diffuseMap;\nuniform sampler2D metalnessMap;\nuniform vec3 color;\nuniform float metalness;\nuniform bool useMetalnessMap;\nuniform bool linear;\nuniform float alphaCutoff: 0.0;\nuniform float alpha: 1.0;\nvarying vec2 v_Texcoord;\n@import clay.util.srgb\nvoid main()\n{\n float m = metalness;\n if (useMetalnessMap) {\n vec4 metalnessTexel = texture2D(metalnessMap, v_Texcoord);\n m = clamp(metalnessTexel.r + (m * 2.0 - 1.0), 0.0, 1.0);\n }\n vec4 texel = texture2D(diffuseMap, v_Texcoord);\n if (linear) {\n texel = sRGBToLinear(texel);\n }\n if (alphaCutoff > 0.0) {\n float a = texel.a * alpha;\n if (a < alphaCutoff) {\n discard;\n }\n }\n gl_FragColor.rgb = texel.rgb * color;\n gl_FragColor.a = m + 0.005;\n}\n@end\n@export clay.deferred.gbuffer3.fragment\nuniform bool firstRender;\nvarying vec4 v_ViewPosition;\nvarying vec4 v_PrevViewPosition;\nvoid main()\n{\n vec2 a = v_ViewPosition.xy / v_ViewPosition.w;\n vec2 b = v_PrevViewPosition.xy / v_PrevViewPosition.w;\n if (firstRender) {\n gl_FragColor = vec4(0.0);\n }\n else {\n gl_FragColor = vec4((a - b) * 0.5 + 0.5, 0.0, 1.0);\n }\n}\n@end\n@export clay.deferred.gbuffer.debug\n@import clay.deferred.chunk.light_head\nuniform sampler2D gBufferTexture4;\nuniform int debug: 0;\nvoid main ()\n{\n @import clay.deferred.chunk.gbuffer_read\n if (debug == 0) {\n gl_FragColor = vec4(N, 1.0);\n }\n else if (debug == 1) {\n gl_FragColor = vec4(vec3(z), 1.0);\n }\n else if (debug == 2) {\n gl_FragColor = vec4(position, 1.0);\n }\n else if (debug == 3) {\n gl_FragColor = vec4(vec3(glossiness), 1.0);\n }\n else if (debug == 4) {\n gl_FragColor = vec4(vec3(metalness), 1.0);\n }\n else if (debug == 5) {\n gl_FragColor = vec4(albedo, 1.0);\n }\n else {\n vec4 color = texture2D(gBufferTexture4, uv);\n color.rg -= 0.5;\n color.rg *= 2.0;\n gl_FragColor = color;\n }\n}\n@end"; var chunkEssl = "@export clay.deferred.chunk.light_head\nuniform sampler2D gBufferTexture1;\nuniform sampler2D gBufferTexture2;\nuniform sampler2D gBufferTexture3;\nuniform vec2 windowSize: WINDOW_SIZE;\nuniform vec4 viewport: VIEWPORT;\nuniform mat4 viewProjectionInv;\n#ifdef DEPTH_ENCODED\n@import clay.util.decode_float\n#endif\n@end\n@export clay.deferred.chunk.gbuffer_read\n vec2 uv = gl_FragCoord.xy / windowSize;\n vec2 uv2 = (gl_FragCoord.xy - viewport.xy) / viewport.zw;\n vec4 texel1 = texture2D(gBufferTexture1, uv);\n vec4 texel3 = texture2D(gBufferTexture3, uv);\n if (dot(texel1.rgb, vec3(1.0)) == 0.0) {\n discard;\n }\n float glossiness = texel1.a;\n float metalness = texel3.a;\n vec3 N = texel1.rgb * 2.0 - 1.0;\n float z = texture2D(gBufferTexture2, uv).r * 2.0 - 1.0;\n vec2 xy = uv2 * 2.0 - 1.0;\n vec4 projectedPos = vec4(xy, z, 1.0);\n vec4 p4 = viewProjectionInv * projectedPos;\n vec3 position = p4.xyz / p4.w;\n vec3 albedo = texel3.rgb;\n vec3 diffuseColor = albedo * (1.0 - metalness);\n vec3 specularColor = mix(vec3(0.04), albedo, metalness);\n@end\n@export clay.deferred.chunk.light_equation\nfloat D_Phong(in float g, in float ndh) {\n float a = pow(8192.0, g);\n return (a + 2.0) / 8.0 * pow(ndh, a);\n}\nfloat D_GGX(in float g, in float ndh) {\n float r = 1.0 - g;\n float a = r * r;\n float tmp = ndh * ndh * (a - 1.0) + 1.0;\n return a / (3.1415926 * tmp * tmp);\n}\nvec3 F_Schlick(in float ndv, vec3 spec) {\n return spec + (1.0 - spec) * pow(1.0 - ndv, 5.0);\n}\nvec3 lightEquation(\n in vec3 lightColor, in vec3 diffuseColor, in vec3 specularColor,\n in float ndl, in float ndh, in float ndv, in float g\n)\n{\n return ndl * lightColor\n * (diffuseColor + D_Phong(g, ndh) * F_Schlick(ndv, specularColor));\n}\n@end"; Shader.import(gbufferEssl); Shader.import(chunkEssl); function createFillCanvas(color) { var canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; var ctx = canvas.getContext('2d'); ctx.fillStyle = color || '#000'; ctx.fillRect(0, 0, 1, 1); return canvas; } // TODO specularColor // TODO Performance improvement function getGetUniformHook1(defaultNormalMap, defaultRoughnessMap, defaultDiffuseMap) { return function (renderable, gBufferMat, symbol) { var standardMaterial = renderable.material; if (symbol === 'doubleSided') { return standardMaterial.isDefined('fragment', 'DOUBLE_SIDED'); } else if (symbol === 'uvRepeat' || symbol === 'uvOffset' || symbol === 'alpha') { return standardMaterial.get(symbol); } else if (symbol === 'normalMap') { return standardMaterial.get(symbol) || defaultNormalMap; } else if (symbol === 'diffuseMap') { return standardMaterial.get(symbol) || defaultDiffuseMap; } else if (symbol === 'alphaCutoff') { // TODO DIFFUSEMAP_ALPHA_ALPHA if (standardMaterial.isDefined('fragment', 'ALPHA_TEST')) { var alphaCutoff = standardMaterial.get('alphaCutoff'); return alphaCutoff || 0; } return 0; } else { var useRoughnessWorkflow = standardMaterial.isDefined('fragment', 'USE_ROUGHNESS'); var roughGlossMap = useRoughnessWorkflow ? standardMaterial.get('roughnessMap') : standardMaterial.get('glossinessMap'); switch (symbol) { case 'glossiness': return useRoughnessWorkflow ? (1.0 - standardMaterial.get('roughness')) : standardMaterial.get('glossiness'); case 'roughGlossMap': return roughGlossMap; case 'useRoughGlossMap': return !!roughGlossMap; case 'useRoughness': return useRoughnessWorkflow; case 'roughGlossChannel': return useRoughnessWorkflow ? standardMaterial.getDefine('fragment', 'ROUGHNESS_CHANNEL') : standardMaterial.getDefine('fragment', 'GLOSSINESS_CHANNEL'); } } }; } function getGetUniformHook2(defaultDiffuseMap, defaultMetalnessMap) { return function (renderable, gBufferMat, symbol) { var standardMaterial = renderable.material; switch (symbol) { case 'color': case 'uvRepeat': case 'uvOffset': case 'alpha': return standardMaterial.get(symbol); case 'metalness': return standardMaterial.get('metalness') || 0; case 'diffuseMap': return standardMaterial.get(symbol) || defaultDiffuseMap; case 'metalnessMap': return standardMaterial.get(symbol) || defaultMetalnessMap; case 'useMetalnessMap': return !!standardMaterial.get('metalnessMap'); case 'linear': return standardMaterial.isDefined('SRGB_DECODE'); case 'alphaCutoff': // TODO DIFFUSEMAP_ALPHA_ALPHA if (standardMaterial.isDefined('fragment', 'ALPHA_TEST')) { var alphaCutoff = standardMaterial.get('alphaCutoff'); return alphaCutoff || 0.0; } return 0.0; } }; } /** * GBuffer is provided for deferred rendering and SSAO, SSR pass. * It will do three passes rendering to four target textures. See * + {@link clay.deferred.GBuffer#getTargetTexture1} * + {@link clay.deferred.GBuffer#getTargetTexture2} * + {@link clay.deferred.GBuffer#getTargetTexture3} * + {@link clay.deferred.GBuffer#getTargetTexture4} * @constructor * @alias clay.deferred.GBuffer * @extends clay.core.Base */ var GBuffer = Base.extend(function () { var commonTextureOpts = { minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, wrapS: Texture.CLAMP_TO_EDGE, wrapT: Texture.CLAMP_TO_EDGE, }; return /** @lends clay.deferred.GBuffer# */ { /** * If enable gbuffer texture 1. * @type {boolean} */ enableTargetTexture1: true, /** * If enable gbuffer texture 2. * @type {boolean} */ enableTargetTexture2: true, /** * If enable gbuffer texture 3. * @type {boolean} */ enableTargetTexture3: true, /** * If enable gbuffer texture 4. * @type {boolean} */ enableTargetTexture4: false, renderTransparent: false, _gBufferRenderList: [], // - R: normal.x // - G: normal.y // - B: normal.z // - A: glossiness _gBufferTex1: new Texture2D(Object.assign({ // PENDING type: Texture.HALF_FLOAT }, commonTextureOpts)), // - R: depth _gBufferTex2: new Texture2D(Object.assign({ // format: Texture.DEPTH_COMPONENT, // type: Texture.UNSIGNED_INT format: Texture.DEPTH_STENCIL, type: Texture.UNSIGNED_INT_24_8_WEBGL }, commonTextureOpts)), // - R: albedo.r // - G: albedo.g // - B: albedo.b // - A: metalness _gBufferTex3: new Texture2D(commonTextureOpts), _gBufferTex4: new Texture2D(Object.assign({ // FLOAT Texture has bug on iOS. is HALF_FLOAT enough? type: Texture.HALF_FLOAT }, commonTextureOpts)), _defaultNormalMap: new Texture2D({ image: createFillCanvas('#000') }), _defaultRoughnessMap: new Texture2D({ image: createFillCanvas('#fff') }), _defaultMetalnessMap: new Texture2D({ image: createFillCanvas('#fff') }), _defaultDiffuseMap: new Texture2D({ image: createFillCanvas('#fff') }), _frameBuffer: new FrameBuffer(), _gBufferMaterial1: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer1.fragment') ), vertexDefines: { FIRST_PASS: null }, fragmentDefines: { FIRST_PASS: null } }), _gBufferMaterial2: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer2.fragment') ), vertexDefines: { SECOND_PASS: null }, fragmentDefines: { SECOND_PASS: null } }), _gBufferMaterial3: new Material({ shader: new Shader( Shader.source('clay.deferred.gbuffer.vertex'), Shader.source('clay.deferred.gbuffer3.fragment') ), vertexDefines: { THIRD_PASS: null }, fragmentDefines: { THIRD_PASS: null } }), _debugPass: new Pass({ fragment: Shader.source('clay.deferred.gbuffer.debug') }) }; }, /** @lends clay.deferred.GBuffer# */{ /** * Set G Buffer size. * @param {number} width * @param {number} height */ resize: function (width, height) { if (this._gBufferTex1.width === width && this._gBufferTex1.height === height ) { return; } this._gBufferTex1.width = width; this._gBufferTex1.height = height; this._gBufferTex2.width = width; this._gBufferTex2.height = height; this._gBufferTex3.width = width; this._gBufferTex3.height = height; this._gBufferTex4.width = width; this._gBufferTex4.height = height; }, // TODO is dpr needed? setViewport: function (x, y, width, height, dpr) { var viewport; if (typeof x === 'object') { viewport = x; } else { viewport = { x: x, y: y, width: width, height: height, devicePixelRatio: dpr || 1 }; } this._frameBuffer.viewport = viewport; }, getViewport: function () { if (this._frameBuffer.viewport) { return this._frameBuffer.viewport; } else { return { x: 0, y: 0, width: this._gBufferTex1.width, height: this._gBufferTex1.height, devicePixelRatio: 1 }; } }, /** * Update GBuffer * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {Object} opts */ update: function (renderer, scene, camera, opts) { opts = opts || {}; var gl = renderer.gl; var frameBuffer = this._frameBuffer; var viewport = frameBuffer.viewport; var renderList = scene.updateRenderList(camera, true); var opaqueList = renderList.opaque; var transparentList = renderList.transparent; var offset = 0; var gBufferRenderList = this._gBufferRenderList; for (var i = 0; i < opaqueList.length; i++) { if (!opaqueList[i].ignoreGBuffer) { gBufferRenderList[offset++] = opaqueList[i]; } } if (this.renderTransparent) { for (var i = 0; i < transparentList.length; i++) { if (!transparentList[i].ignoreGBuffer) { gBufferRenderList[offset++] = transparentList[i]; } } } gBufferRenderList.length = offset; gl.clearColor(0, 0, 0, 0); gl.depthMask(true); gl.colorMask(true, true, true, true); gl.disable(gl.BLEND); var enableTargetTexture1 = this.enableTargetTexture1; var enableTargetTexture2 = this.enableTargetTexture2; var enableTargetTexture3 = this.enableTargetTexture3; var enableTargetTexture4 = this.enableTargetTexture4; if (!enableTargetTexture1 && !enableTargetTexture3 && !enableTargetTexture4) { console.warn('Can\'t disable targetTexture1, targetTexture3, targetTexture4 both'); enableTargetTexture1 = true; } if (enableTargetTexture2) { frameBuffer.attach(opts.targetTexture2 || this._gBufferTex2, renderer.gl.DEPTH_STENCIL_ATTACHMENT); } function clearViewport() { if (viewport) { var dpr = viewport.devicePixelRatio; // use scissor to make sure only clear the viewport gl.enable(gl.SCISSOR_TEST); gl.scissor(viewport.x * dpr, viewport.y * dpr, viewport.width * dpr, viewport.height * dpr); } gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); if (viewport) { gl.disable(gl.SCISSOR_TEST); } } function isMaterialChanged(renderable, prevRenderable) { return renderable.material !== prevRenderable.material; } // PENDING, scene.boundingBoxLastFrame needs be updated if have shadow renderer.bindSceneRendering(scene); if (enableTargetTexture1) { // Pass 1 frameBuffer.attach(opts.targetTexture1 || this._gBufferTex1); frameBuffer.bind(renderer); clearViewport(); var gBufferMaterial1 = this._gBufferMaterial1; var passConfig = { getMaterial: function () { return gBufferMaterial1; }, getUniform: getGetUniformHook1(this._defaultNormalMap, this._defaultRoughnessMap, this._defaultDiffuseMap), isMaterialChanged: isMaterialChanged, sortCompare: renderer.opaqueSortCompare }; // FIXME Use MRT if possible renderer.renderPass(gBufferRenderList, camera, passConfig); } if (enableTargetTexture3) { // Pass 2 frameBuffer.attach(opts.targetTexture3 || this._gBufferTex3); frameBuffer.bind(renderer); clearViewport(); var gBufferMaterial2 = this._gBufferMaterial2; var passConfig = { getMaterial: function () { return gBufferMaterial2; }, getUniform: getGetUniformHook2(this._defaultDiffuseMap, this._defaultMetalnessMap), isMaterialChanged: isMaterialChanged, sortCompare: renderer.opaqueSortCompare }; renderer.renderPass(gBufferRenderList, camera, passConfig); } if (enableTargetTexture4) { frameBuffer.bind(renderer); frameBuffer.attach(opts.targetTexture4 || this._gBufferTex4); clearViewport(); // Remove jittering in temporal aa. // PENDING. Better solution? camera.update(); var gBufferMaterial3 = this._gBufferMaterial3; var cameraViewProj = mat4.create(); mat4.multiply(cameraViewProj, camera.projectionMatrix.array, camera.viewMatrix.array); var passConfig = { getMaterial: function () { return gBufferMaterial3; }, afterRender: function (renderer, renderable) { var isSkinnedMesh = renderable.isSkinnedMesh(); if (isSkinnedMesh) { var skeleton = renderable.skeleton; var joints = renderable.joints; if (joints.length > renderer.getMaxJointNumber()) { var skinMatricesTexture = skeleton.getSubSkinMatricesTexture(renderable.__uid__, joints); var prevSkinMatricesTexture = renderable.__prevSkinMatricesTexture; if (!prevSkinMatricesTexture) { prevSkinMatricesTexture = renderable.__prevSkinMatricesTexture = new Texture2D({ type: Texture.FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST, useMipmap: false, flipY: false }); } if (!prevSkinMatricesTexture.pixels || prevSkinMatricesTexture.pixels.length !== skinMatricesTexture.pixels.length ) { prevSkinMatricesTexture.pixels = new Float32Array(skinMatricesTexture.pixels); } else { for (var i = 0; i < skinMatricesTexture.pixels.length; i++) { prevSkinMatricesTexture.pixels[i] = skinMatricesTexture.pixels[i]; } } prevSkinMatricesTexture.width = skinMatricesTexture.width; prevSkinMatricesTexture.height = skinMatricesTexture.height; } else { var skinMatricesArray = skeleton.getSubSkinMatrices(renderable.__uid__, joints); if (!renderable.__prevSkinMatricesArray || renderable.__prevSkinMatricesArray.length !== skinMatricesArray.length) { renderable.__prevSkinMatricesArray = new Float32Array(skinMatricesArray.length); } renderable.__prevSkinMatricesArray.set(skinMatricesArray); } } renderable.__prevWorldViewProjection = renderable.__prevWorldViewProjection || mat4.create(); if (isSkinnedMesh) { // Ignore world transform of skinned mesh. mat4.copy(renderable.__prevWorldViewProjection, cameraViewProj); } else { mat4.multiply(renderable.__prevWorldViewProjection, cameraViewProj, renderable.worldTransform.array); } }, getUniform: function (renderable, gBufferMat, symbol) { if (symbol === 'prevWorldViewProjection') { return renderable.__prevWorldViewProjection; } else if (symbol === 'prevSkinMatrix') { return renderable.__prevSkinMatricesArray; } else if (symbol === 'prevSkinMatricesTexture') { return renderable.__prevSkinMatricesTexture; } else if (symbol === 'firstRender') { return !renderable.__prevWorldViewProjection; } else { return gBufferMat.get(symbol); } }, isMaterialChanged: function () { // Always update prevWorldViewProjection return true; }, sortCompare: renderer.opaqueSortCompare }; renderer.renderPass(gBufferRenderList, camera, passConfig); } renderer.bindSceneRendering(null); frameBuffer.unbind(renderer); }, /** * Debug output of gBuffer. Use `type` parameter to choos the debug output type, which can be: * * + 'normal' * + 'depth' * + 'position' * + 'glossiness' * + 'metalness' * + 'albedo' * + 'velocity' * * @param {clay.Renderer} renderer * @param {clay.Camera} camera * @param {string} [type='normal'] */ renderDebug: function (renderer, camera, type, viewport) { var debugTypes = { normal: 0, depth: 1, position: 2, glossiness: 3, metalness: 4, albedo: 5, velocity: 6 }; if (debugTypes[type] == null) { console.warn('Unkown type "' + type + '"'); // Default use normal type = 'normal'; } renderer.saveClear(); renderer.saveViewport(); renderer.clearBit = renderer.gl.DEPTH_BUFFER_BIT; if (viewport) { renderer.setViewport(viewport); } var viewProjectionInv = new Matrix4(); Matrix4.multiply(viewProjectionInv, camera.worldTransform, camera.invProjectionMatrix); var debugPass = this._debugPass; debugPass.setUniform('viewportSize', [renderer.getWidth(), renderer.getHeight()]); debugPass.setUniform('gBufferTexture1', this._gBufferTex1); debugPass.setUniform('gBufferTexture2', this._gBufferTex2); debugPass.setUniform('gBufferTexture3', this._gBufferTex3); debugPass.setUniform('gBufferTexture4', this._gBufferTex4); debugPass.setUniform('debug', debugTypes[type]); debugPass.setUniform('viewProjectionInv', viewProjectionInv.array); debugPass.render(renderer); renderer.restoreViewport(); renderer.restoreClear(); }, /** * Get first target texture. * Channel storage: * + R: normal.x * 0.5 + 0.5 * + G: normal.y * 0.5 + 0.5 * + B: normal.z * 0.5 + 0.5 * + A: glossiness * @return {clay.Texture2D} */ getTargetTexture1: function () { return this._gBufferTex1; }, /** * Get second target texture. * Channel storage: * + R: depth * @return {clay.Texture2D} */ getTargetTexture2: function () { return this._gBufferTex2; }, /** * Get third target texture. * Channel storage: * + R: albedo.r * + G: albedo.g * + B: albedo.b * + A: metalness * @return {clay.Texture2D} */ getTargetTexture3: function () { return this._gBufferTex3; }, /** * Get fourth target texture. * Channel storage: * + R: velocity.r * + G: velocity.g * @return {clay.Texture2D} */ getTargetTexture4: function () { return this._gBufferTex4; }, /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._gBufferTex1.dispose(renderer); this._gBufferTex2.dispose(renderer); this._gBufferTex3.dispose(renderer); this._defaultNormalMap.dispose(renderer); this._defaultRoughnessMap.dispose(renderer); this._defaultMetalnessMap.dispose(renderer); this._defaultDiffuseMap.dispose(renderer); this._frameBuffer.dispose(renderer); } }); /** * @constructor clay.geometry.Cone * @extends clay.Geometry * @param {Object} [opt] * @param {number} [opt.topRadius] * @param {number} [opt.bottomRadius] * @param {number} [opt.height] * @param {number} [opt.capSegments] * @param {number} [opt.heightSegments] */ var Cone$1 = Geometry.extend(/** @lends clay.geometry.Cone# */ { dynamic: false, /** * @type {number} */ topRadius: 0, /** * @type {number} */ bottomRadius: 1, /** * @type {number} */ height: 2, /** * @type {number} */ capSegments: 20, /** * @type {number} */ heightSegments: 1 }, function() { this.build(); }, /** @lends clay.geometry.Cone.prototype */ { /** * Build cone geometry */ build: function() { var positions = []; var texcoords = []; var faces = []; positions.length = 0; texcoords.length = 0; faces.length = 0; // Top cap var capSegRadial = Math.PI * 2 / this.capSegments; var topCap = []; var bottomCap = []; var r1 = this.topRadius; var r2 = this.bottomRadius; var y = this.height / 2; var c1 = vec3.fromValues(0, y, 0); var c2 = vec3.fromValues(0, -y, 0); for (var i = 0; i < this.capSegments; i++) { var theta = i * capSegRadial; var x = r1 * Math.sin(theta); var z = r1 * Math.cos(theta); topCap.push(vec3.fromValues(x, y, z)); x = r2 * Math.sin(theta); z = r2 * Math.cos(theta); bottomCap.push(vec3.fromValues(x, -y, z)); } // Build top cap positions.push(c1); // FIXME texcoords.push(vec2.fromValues(0, 1)); var n = this.capSegments; for (var i = 0; i < n; i++) { positions.push(topCap[i]); // FIXME texcoords.push(vec2.fromValues(i / n, 0)); faces.push([0, i+1, (i+1) % n + 1]); } // Build bottom cap var offset = positions.length; positions.push(c2); texcoords.push(vec2.fromValues(0, 1)); for (var i = 0; i < n; i++) { positions.push(bottomCap[i]); // FIXME texcoords.push(vec2.fromValues(i / n, 0)); faces.push([offset, offset+((i+1) % n + 1), offset+i+1]); } // Build side offset = positions.length; var n2 = this.heightSegments; for (var i = 0; i < n; i++) { for (var j = 0; j < n2+1; j++) { var v = j / n2; positions.push(vec3.lerp(vec3.create(), topCap[i], bottomCap[i], v)); texcoords.push(vec2.fromValues(i / n, v)); } } for (var i = 0; i < n; i++) { for (var j = 0; j < n2; j++) { var i1 = i * (n2 + 1) + j; var i2 = ((i + 1) % n) * (n2 + 1) + j; var i3 = ((i + 1) % n) * (n2 + 1) + j + 1; var i4 = i * (n2 + 1) + j + 1; faces.push([offset+i2, offset+i1, offset+i4]); faces.push([offset+i4, offset+i3, offset+i2]); } } this.attributes.position.fromArray(positions); this.attributes.texcoord0.fromArray(texcoords); this.initIndicesFromArray(faces); this.generateVertexNormals(); this.boundingBox = new BoundingBox(); var r = Math.max(this.topRadius, this.bottomRadius); this.boundingBox.min.set(-r, -this.height/2, -r); this.boundingBox.max.set(r, this.height/2, r); } }); /** * @constructor clay.geometry.Cylinder * @extends clay.Geometry * @param {Object} [opt] * @param {number} [opt.radius] * @param {number} [opt.height] * @param {number} [opt.capSegments] * @param {number} [opt.heightSegments] */ var Cylinder$1 = Geometry.extend( /** @lends clay.geometry.Cylinder# */ { dynamic: false, /** * @type {number} */ radius: 1, /** * @type {number} */ height: 2, /** * @type {number} */ capSegments: 50, /** * @type {number} */ heightSegments: 1 }, function() { this.build(); }, /** @lends clay.geometry.Cylinder.prototype */ { /** * Build cylinder geometry */ build: function() { var cone = new Cone$1({ topRadius: this.radius, bottomRadius: this.radius, capSegments: this.capSegments, heightSegments: this.heightSegments, height: this.height }); this.attributes.position.value = cone.attributes.position.value; this.attributes.normal.value = cone.attributes.normal.value; this.attributes.texcoord0.value = cone.attributes.texcoord0.value; this.indices = cone.indices; this.boundingBox = cone.boundingBox; } }); var lightvolumeGlsl = "@export clay.deferred.light_volume.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\nvarying vec3 v_Position;\nvoid main()\n{\n gl_Position = worldViewProjection * vec4(position, 1.0);\n v_Position = position;\n}\n@end"; var spotGlsl = "@export clay.deferred.spot_light\n@import clay.deferred.chunk.light_head\n@import clay.deferred.chunk.light_equation\n@import clay.util.calculate_attenuation\nuniform vec3 lightPosition;\nuniform vec3 lightDirection;\nuniform vec3 lightColor;\nuniform float umbraAngleCosine;\nuniform float penumbraAngleCosine;\nuniform float lightRange;\nuniform float falloffFactor;\nuniform vec3 eyePosition;\n#ifdef SHADOWMAP_ENABLED\nuniform sampler2D lightShadowMap;\nuniform mat4 lightMatrix;\nuniform float lightShadowMapSize;\n#endif\n@import clay.plugin.shadow_map_common\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 L = lightPosition - position;\n vec3 V = normalize(eyePosition - position);\n float dist = length(L);\n L /= dist;\n float attenuation = lightAttenuation(dist, lightRange);\n float c = dot(-normalize(lightDirection), L);\n float falloff = clamp((c - umbraAngleCosine) / (penumbraAngleCosine - umbraAngleCosine), 0.0, 1.0);\n falloff = pow(falloff, falloffFactor);\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n gl_FragColor.rgb = (1.0 - falloff) * attenuation * lightEquation(\n lightColor, diffuseColor, specularColor, ndl, ndh, ndv, glossiness\n );\n#ifdef SHADOWMAP_ENABLED\n float shadowContrib = computeShadowContrib(\n lightShadowMap, lightMatrix, position, lightShadowMapSize\n );\n gl_FragColor.rgb *= shadowContrib;\n#endif\n gl_FragColor.a = 1.0;\n}\n@end\n"; var directionalGlsl = "@export clay.deferred.directional_light\n@import clay.deferred.chunk.light_head\n@import clay.deferred.chunk.light_equation\nuniform vec3 lightDirection;\nuniform vec3 lightColor;\nuniform vec3 eyePosition;\n#ifdef SHADOWMAP_ENABLED\nuniform sampler2D lightShadowMap;\nuniform float lightShadowMapSize;\nuniform mat4 lightMatrices[SHADOW_CASCADE];\nuniform float shadowCascadeClipsNear[SHADOW_CASCADE];\nuniform float shadowCascadeClipsFar[SHADOW_CASCADE];\n#endif\n@import clay.plugin.shadow_map_common\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 L = -normalize(lightDirection);\n vec3 V = normalize(eyePosition - position);\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n gl_FragColor.rgb = lightEquation(\n lightColor, diffuseColor, specularColor, ndl, ndh, ndv, glossiness\n );\n#ifdef SHADOWMAP_ENABLED\n float shadowContrib = 1.0;\n for (int _idx_ = 0; _idx_ < SHADOW_CASCADE; _idx_++) {{\n if (\n z >= shadowCascadeClipsNear[_idx_] &&\n z <= shadowCascadeClipsFar[_idx_]\n ) {\n shadowContrib = computeShadowContrib(\n lightShadowMap, lightMatrices[_idx_], position, lightShadowMapSize,\n vec2(1.0 / float(SHADOW_CASCADE), 1.0),\n vec2(float(_idx_) / float(SHADOW_CASCADE), 0.0)\n );\n }\n }}\n gl_FragColor.rgb *= shadowContrib;\n#endif\n gl_FragColor.a = 1.0;\n}\n@end\n"; var ambientGlsl = "@export clay.deferred.ambient_light\nuniform sampler2D gBufferTexture1;\nuniform sampler2D gBufferTexture3;\nuniform vec3 lightColor;\nuniform vec2 windowSize: WINDOW_SIZE;\nvoid main()\n{\n vec2 uv = gl_FragCoord.xy / windowSize;\n vec4 texel1 = texture2D(gBufferTexture1, uv);\n if (dot(texel1.rgb, vec3(1.0)) == 0.0) {\n discard;\n }\n vec3 albedo = texture2D(gBufferTexture3, uv).rgb;\n gl_FragColor.rgb = lightColor * albedo;\n gl_FragColor.a = 1.0;\n}\n@end"; var ambientshGlsl = "@export clay.deferred.ambient_sh_light\nuniform sampler2D gBufferTexture1;\nuniform sampler2D gBufferTexture3;\nuniform vec3 lightColor;\nuniform vec3 lightCoefficients[9];\nuniform vec2 windowSize: WINDOW_SIZE;\nvec3 calcAmbientSHLight(vec3 N) {\n return lightCoefficients[0]\n + lightCoefficients[1] * N.x\n + lightCoefficients[2] * N.y\n + lightCoefficients[3] * N.z\n + lightCoefficients[4] * N.x * N.z\n + lightCoefficients[5] * N.z * N.y\n + lightCoefficients[6] * N.y * N.x\n + lightCoefficients[7] * (3.0 * N.z * N.z - 1.0)\n + lightCoefficients[8] * (N.x * N.x - N.y * N.y);\n}\nvoid main()\n{\n vec2 uv = gl_FragCoord.xy / windowSize;\n vec4 texel1 = texture2D(gBufferTexture1, uv);\n if (dot(texel1.rgb, vec3(1.0)) == 0.0) {\n discard;\n }\n vec3 N = texel1.rgb * 2.0 - 1.0;\n vec3 albedo = texture2D(gBufferTexture3, uv).rgb;\n gl_FragColor.rgb = lightColor * albedo * calcAmbientSHLight(N);\n gl_FragColor.a = 1.0;\n}\n@end"; var ambientcubemapGlsl = "@export clay.deferred.ambient_cubemap_light\n@import clay.deferred.chunk.light_head\nuniform vec3 lightColor;\nuniform samplerCube lightCubemap;\nuniform sampler2D brdfLookup;\nuniform vec3 eyePosition;\n@import clay.util.rgbm\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 V = normalize(eyePosition - position);\n vec3 L = reflect(-V, N);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n float rough = clamp(1.0 - glossiness, 0.0, 1.0);\n float bias = rough * 5.0;\n vec2 brdfParam = texture2D(brdfLookup, vec2(rough, ndv)).xy;\n vec3 envWeight = specularColor * brdfParam.x + brdfParam.y;\n vec3 envTexel = RGBMDecode(textureCubeLodEXT(lightCubemap, L, bias), 8.12);\n gl_FragColor.rgb = lightColor * envTexel * envWeight;\n gl_FragColor.a = 1.0;\n}\n@end"; var pointGlsl = "@export clay.deferred.point_light\n@import clay.deferred.chunk.light_head\n@import clay.util.calculate_attenuation\n@import clay.deferred.chunk.light_equation\nuniform vec3 lightPosition;\nuniform vec3 lightColor;\nuniform float lightRange;\nuniform vec3 eyePosition;\n#ifdef SHADOWMAP_ENABLED\nuniform samplerCube lightShadowMap;\nuniform float lightShadowMapSize;\n#endif\nvarying vec3 v_Position;\n@import clay.plugin.shadow_map_common\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 L = lightPosition - position;\n vec3 V = normalize(eyePosition - position);\n float dist = length(L);\n L /= dist;\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n float attenuation = lightAttenuation(dist, lightRange);\n gl_FragColor.rgb = attenuation * lightEquation(\n lightColor, diffuseColor, specularColor, ndl, ndh, ndv, glossiness\n );\n#ifdef SHADOWMAP_ENABLED\n float shadowContrib = computeShadowContribOmni(\n lightShadowMap, -L * dist, lightRange\n );\n gl_FragColor.rgb *= clamp(shadowContrib, 0.0, 1.0);\n#endif\n gl_FragColor.a = 1.0;\n}\n@end"; var sphereGlsl = "@export clay.deferred.sphere_light\n@import clay.deferred.chunk.light_head\n@import clay.util.calculate_attenuation\n@import clay.deferred.chunk.light_equation\nuniform vec3 lightPosition;\nuniform vec3 lightColor;\nuniform float lightRange;\nuniform float lightRadius;\nuniform vec3 eyePosition;\nvarying vec3 v_Position;\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 L = lightPosition - position;\n vec3 V = normalize(eyePosition - position);\n float dist = length(L);\n vec3 R = reflect(V, N);\n float tmp = dot(L, R);\n vec3 cToR = tmp * R - L;\n float d = length(cToR);\n L = L + cToR * clamp(lightRadius / d, 0.0, 1.0);\n L = normalize(L);\n vec3 H = normalize(L + V);\n float ndl = clamp(dot(N, L), 0.0, 1.0);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n float attenuation = lightAttenuation(dist, lightRange);\n gl_FragColor.rgb = lightColor * ndl * attenuation;\n glossiness = clamp(glossiness - lightRadius / 2.0 / dist, 0.0, 1.0);\n gl_FragColor.rgb = attenuation * lightEquation(\n lightColor, diffuseColor, specularColor, ndl, ndh, ndv, glossiness\n );\n gl_FragColor.a = 1.0;\n}\n@end"; var tubeGlsl = "@export clay.deferred.tube_light\n@import clay.deferred.chunk.light_head\n@import clay.util.calculate_attenuation\n@import clay.deferred.chunk.light_equation\nuniform vec3 lightPosition;\nuniform vec3 lightColor;\nuniform float lightRange;\nuniform vec3 lightExtend;\nuniform vec3 eyePosition;\nvarying vec3 v_Position;\nvoid main()\n{\n @import clay.deferred.chunk.gbuffer_read\n vec3 L = lightPosition - position;\n vec3 V = normalize(eyePosition - position);\n vec3 R = reflect(V, N);\n vec3 L0 = lightPosition - lightExtend - position;\n vec3 L1 = lightPosition + lightExtend - position;\n vec3 LD = L1 - L0;\n float len0 = length(L0);\n float len1 = length(L1);\n float irra = 2.0 * clamp(dot(N, L0) / (2.0 * len0) + dot(N, L1) / (2.0 * len1), 0.0, 1.0);\n float LDDotR = dot(R, LD);\n float t = (LDDotR * dot(R, L0) - dot(L0, LD)) / (dot(LD, LD) - LDDotR * LDDotR);\n t = clamp(t, 0.0, 1.0);\n L = L0 + t * LD;\n float dist = length(L);\n L /= dist;\n vec3 H = normalize(L + V);\n float ndh = clamp(dot(N, H), 0.0, 1.0);\n float ndv = clamp(dot(N, V), 0.0, 1.0);\n glossiness = clamp(glossiness - 0.0 / 2.0 / dist, 0.0, 1.0);\n gl_FragColor.rgb = lightColor * irra * lightAttenuation(dist, lightRange)\n * (diffuseColor + D_Phong(glossiness, ndh) * F_Schlick(ndv, specularColor));\n gl_FragColor.a = 1.0;\n}\n@end"; // Light-pre pass deferred rendering // http://www.realtimerendering.com/blog/deferred-lighting-approaches/ // Light shaders Shader.import(prezGlsl); Shader.import(utilGlsl); Shader.import(lightvolumeGlsl); // Light shaders Shader.import(spotGlsl); Shader.import(directionalGlsl); Shader.import(ambientGlsl); Shader.import(ambientshGlsl); Shader.import(ambientcubemapGlsl); Shader.import(pointGlsl); Shader.import(sphereGlsl); Shader.import(tubeGlsl); Shader.import(prezGlsl); /** * Deferred renderer * @constructor * @alias clay.deferred.Renderer * @extends clay.core.Base */ var DeferredRenderer = Base.extend(function () { var fullQuadVertex = Shader.source('clay.compositor.vertex'); var lightVolumeVertex = Shader.source('clay.deferred.light_volume.vertex'); var directionalLightShader = new Shader(fullQuadVertex, Shader.source('clay.deferred.directional_light')); var lightAccumulateBlendFunc = function (gl) { gl.blendEquation(gl.FUNC_ADD); gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); }; var createLightPassMat = function (shader) { return new Material({ shader: shader, blend: lightAccumulateBlendFunc, transparent: true, depthMask: false }); }; var createVolumeShader = function (name) { return new Shader(lightVolumeVertex, Shader.source('clay.deferred.' + name)); }; // Rotate and positioning to fit the spot light // Which the cusp of cone pointing to the positive z // and positioned on the origin var coneGeo = new Cone$1({ capSegments: 10 }); var mat = new Matrix4(); mat.rotateX(Math.PI / 2) .translate(new Vector3(0, -1, 0)); coneGeo.applyTransform(mat); var cylinderGeo = new Cylinder$1({ capSegments: 10 }); // Align with x axis mat.identity().rotateZ(Math.PI / 2); cylinderGeo.applyTransform(mat); return /** @lends clay.deferred.Renderer# */ { /** * Provide ShadowMapPass for shadow rendering. * @type {clay.prePass.ShadowMap} */ shadowMapPass: null, /** * If enable auto resizing from given defualt renderer size. * @type {boolean} */ autoResize: true, _createLightPassMat: createLightPassMat, _gBuffer: new GBuffer(), _lightAccumFrameBuffer: new FrameBuffer(), _lightAccumTex: new Texture2D({ // FIXME Device not support float texture type: Texture.HALF_FLOAT, minFilter: Texture.NEAREST, magFilter: Texture.NEAREST }), _fullQuadPass: new Pass({ blendWithPrevious: true }), _directionalLightMat: createLightPassMat(directionalLightShader), _ambientMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_light') )), _ambientSHMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_sh_light') )), _ambientCubemapMat: createLightPassMat(new Shader( fullQuadVertex, Shader.source('clay.deferred.ambient_cubemap_light') )), _spotLightShader: createVolumeShader('spot_light'), _pointLightShader: createVolumeShader('point_light'), _sphereLightShader: createVolumeShader('sphere_light'), _tubeLightShader: createVolumeShader('tube_light'), _lightSphereGeo: new Sphere$1({ widthSegments: 10, heightSegements: 10 }), _lightConeGeo: coneGeo, _lightCylinderGeo: cylinderGeo, _outputPass: new Pass({ fragment: Shader.source('clay.compositor.output') }) }; }, /** @lends clay.deferred.Renderer# */ { /** * Do render * @param {clay.Renderer} renderer * @param {clay.Scene} scene * @param {clay.Camera} camera * @param {Object} [opts] * @param {boolean} [opts.renderToTarget = false] If not ouput and render to the target texture * @param {boolean} [opts.notUpdateShadow = true] If not update the shadow. * @param {boolean} [opts.notUpdateScene = true] If not update the scene. */ render: function (renderer, scene, camera, opts) { opts = opts || {}; opts.renderToTarget = opts.renderToTarget || false; opts.notUpdateShadow = opts.notUpdateShadow || false; opts.notUpdateScene = opts.notUpdateScene || false; if (!opts.notUpdateScene) { scene.update(false, true); } scene.updateLights(); // Render list will be updated in gbuffer. camera.update(true); // PENDING For stereo rendering var dpr = renderer.getDevicePixelRatio(); if (this.autoResize && (renderer.getWidth() * dpr !== this._lightAccumTex.width || renderer.getHeight() * dpr !== this._lightAccumTex.height) ) { this.resize(renderer.getWidth() * dpr, renderer.getHeight() * dpr); } this._gBuffer.update(renderer, scene, camera); // Accumulate light buffer this._accumulateLightBuffer(renderer, scene, camera, !opts.notUpdateShadow); if (!opts.renderToTarget) { this._outputPass.setUniform('texture', this._lightAccumTex); this._outputPass.render(renderer); // this._gBuffer.renderDebug(renderer, camera, 'normal'); } }, /** * @return {clay.Texture2D} */ getTargetTexture: function () { return this._lightAccumTex; }, /** * @return {clay.FrameBuffer} */ getTargetFrameBuffer: function () { return this._lightAccumFrameBuffer; }, /** * @return {clay.deferred.GBuffer} */ getGBuffer: function () { return this._gBuffer; }, // TODO is dpr needed? setViewport: function (x, y, width, height, dpr) { this._gBuffer.setViewport(x, y, width, height, dpr); this._lightAccumFrameBuffer.viewport = this._gBuffer.getViewport(); }, // getFullQuadLightPass: function () { // return this._fullQuadPass; // }, /** * Set renderer size. * @param {number} width * @param {number} height */ resize: function (width, height) { this._lightAccumTex.width = width; this._lightAccumTex.height = height; // PENDING viewport ? this._gBuffer.resize(width, height); }, _accumulateLightBuffer: function (renderer, scene, camera, updateShadow) { var gl = renderer.gl; var lightAccumTex = this._lightAccumTex; var lightAccumFrameBuffer = this._lightAccumFrameBuffer; var eyePosition = camera.getWorldPosition().array; // Update volume meshes for (var i = 0; i < scene.lights.length; i++) { if (!scene.lights[i].invisible) { this._updateLightProxy(scene.lights[i]); } } var shadowMapPass = this.shadowMapPass; if (shadowMapPass && updateShadow) { gl.clearColor(1, 1, 1, 1); this._prepareLightShadow(renderer, scene, camera); } this.trigger('beforelightaccumulate', renderer, scene, camera, updateShadow); lightAccumFrameBuffer.attach(lightAccumTex); lightAccumFrameBuffer.bind(renderer); var clearColor = renderer.clearColor; var viewport = lightAccumFrameBuffer.viewport; if (viewport) { var dpr = viewport.devicePixelRatio; // use scissor to make sure only clear the viewport gl.enable(gl.SCISSOR_TEST); gl.scissor(viewport.x * dpr, viewport.y * dpr, viewport.width * dpr, viewport.height * dpr); } gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); gl.clear(gl.COLOR_BUFFER_BIT); gl.enable(gl.BLEND); if (viewport) { gl.disable(gl.SCISSOR_TEST); } this.trigger('startlightaccumulate', renderer, scene, camera); var viewProjectionInv = new Matrix4(); Matrix4.multiply(viewProjectionInv, camera.worldTransform, camera.invProjectionMatrix); var volumeMeshList = []; for (var i = 0; i < scene.lights.length; i++) { var light = scene.lights[i]; if (light.invisible) { continue; } var uTpl = light.uniformTemplates; var volumeMesh = light.volumeMesh || light.__volumeMesh; if (volumeMesh) { var material = volumeMesh.material; // Volume mesh will affect the scene bounding box when rendering // if castShadow is true volumeMesh.castShadow = false; var unknownLightType = false; switch (light.type) { case 'POINT_LIGHT': material.setUniform('lightColor', uTpl.pointLightColor.value(light)); material.setUniform('lightRange', uTpl.pointLightRange.value(light)); material.setUniform('lightPosition', uTpl.pointLightPosition.value(light)); break; case 'SPOT_LIGHT': material.setUniform('lightPosition', uTpl.spotLightPosition.value(light)); material.setUniform('lightColor', uTpl.spotLightColor.value(light)); material.setUniform('lightRange', uTpl.spotLightRange.value(light)); material.setUniform('lightDirection', uTpl.spotLightDirection.value(light)); material.setUniform('umbraAngleCosine', uTpl.spotLightUmbraAngleCosine.value(light)); material.setUniform('penumbraAngleCosine', uTpl.spotLightPenumbraAngleCosine.value(light)); material.setUniform('falloffFactor', uTpl.spotLightFalloffFactor.value(light)); break; case 'SPHERE_LIGHT': material.setUniform('lightColor', uTpl.sphereLightColor.value(light)); material.setUniform('lightRange', uTpl.sphereLightRange.value(light)); material.setUniform('lightRadius', uTpl.sphereLightRadius.value(light)); material.setUniform('lightPosition', uTpl.sphereLightPosition.value(light)); break; case 'TUBE_LIGHT': material.setUniform('lightColor', uTpl.tubeLightColor.value(light)); material.setUniform('lightRange', uTpl.tubeLightRange.value(light)); material.setUniform('lightExtend', uTpl.tubeLightExtend.value(light)); material.setUniform('lightPosition', uTpl.tubeLightPosition.value(light)); break; default: unknownLightType = true; } if (unknownLightType) { continue; } material.setUniform('eyePosition', eyePosition); material.setUniform('viewProjectionInv', viewProjectionInv.array); material.setUniform('gBufferTexture1', this._gBuffer.getTargetTexture1()); material.setUniform('gBufferTexture2', this._gBuffer.getTargetTexture2()); material.setUniform('gBufferTexture3', this._gBuffer.getTargetTexture3()); volumeMeshList.push(volumeMesh); } else { var pass = this._fullQuadPass; var unknownLightType = false; // Full quad light switch (light.type) { case 'AMBIENT_LIGHT': pass.material = this._ambientMat; pass.material.setUniform('lightColor', uTpl.ambientLightColor.value(light)); break; case 'AMBIENT_SH_LIGHT': pass.material = this._ambientSHMat; pass.material.setUniform('lightColor', uTpl.ambientSHLightColor.value(light)); pass.material.setUniform('lightCoefficients', uTpl.ambientSHLightCoefficients.value(light)); break; case 'AMBIENT_CUBEMAP_LIGHT': pass.material = this._ambientCubemapMat; pass.material.setUniform('lightColor', uTpl.ambientCubemapLightColor.value(light)); pass.material.setUniform('lightCubemap', uTpl.ambientCubemapLightCubemap.value(light)); pass.material.setUniform('brdfLookup', uTpl.ambientCubemapLightBRDFLookup.value(light)); break; case 'DIRECTIONAL_LIGHT': var hasShadow = shadowMapPass && light.castShadow; pass.material = this._directionalLightMat; pass.material[hasShadow ? 'define' : 'undefine']('fragment', 'SHADOWMAP_ENABLED'); if (hasShadow) { pass.material.define('fragment', 'SHADOW_CASCADE', light.shadowCascade); } pass.material.setUniform('lightColor', uTpl.directionalLightColor.value(light)); pass.material.setUniform('lightDirection', uTpl.directionalLightDirection.value(light)); break; default: // Unkonw light type unknownLightType = true; } if (unknownLightType) { continue; } var passMaterial = pass.material; passMaterial.setUniform('eyePosition', eyePosition); passMaterial.setUniform('viewProjectionInv', viewProjectionInv.array); passMaterial.setUniform('gBufferTexture1', this._gBuffer.getTargetTexture1()); passMaterial.setUniform('gBufferTexture2', this._gBuffer.getTargetTexture2()); passMaterial.setUniform('gBufferTexture3', this._gBuffer.getTargetTexture3()); // TODO if (shadowMapPass && light.castShadow) { passMaterial.setUniform('lightShadowMap', light.__shadowMap); passMaterial.setUniform('lightMatrices', light.__lightMatrices); passMaterial.setUniform('shadowCascadeClipsNear', light.__cascadeClipsNear); passMaterial.setUniform('shadowCascadeClipsFar', light.__cascadeClipsFar); passMaterial.setUniform('lightShadowMapSize', light.shadowResolution); } pass.renderQuad(renderer); } } this._renderVolumeMeshList(renderer, scene, camera, volumeMeshList); this.trigger('lightaccumulate', renderer, scene, camera); lightAccumFrameBuffer.unbind(renderer); this.trigger('afterlightaccumulate', renderer, scene, camera); }, _prepareLightShadow: (function () { var worldView = new Matrix4(); return function (renderer, scene, camera) { for (var i = 0; i < scene.lights.length; i++) { var light = scene.lights[i]; var volumeMesh = light.volumeMesh || light.__volumeMesh; if (!light.castShadow || light.invisible) { continue; } switch (light.type) { case 'POINT_LIGHT': case 'SPOT_LIGHT': // Frustum culling Matrix4.multiply(worldView, camera.viewMatrix, volumeMesh.worldTransform); if (scene.isFrustumCulled(volumeMesh, camera, worldView.array)) { continue; } this._prepareSingleLightShadow( renderer, scene, camera, light, volumeMesh.material ); break; case 'DIRECTIONAL_LIGHT': this._prepareSingleLightShadow( renderer, scene, camera, light, null ); } } }; })(), _prepareSingleLightShadow: function (renderer, scene, camera, light, material) { switch (light.type) { case 'POINT_LIGHT': var shadowMaps = []; this.shadowMapPass.renderPointLightShadow( renderer, scene, light, shadowMaps ); material.setUniform('lightShadowMap', shadowMaps[0]); material.setUniform('lightShadowMapSize', light.shadowResolution); break; case 'SPOT_LIGHT': var shadowMaps = []; var lightMatrices = []; this.shadowMapPass.renderSpotLightShadow( renderer, scene, light, lightMatrices, shadowMaps ); material.setUniform('lightShadowMap', shadowMaps[0]); material.setUniform('lightMatrix', lightMatrices[0]); material.setUniform('lightShadowMapSize', light.shadowResolution); break; case 'DIRECTIONAL_LIGHT': var shadowMaps = []; var lightMatrices = []; var cascadeClips = []; this.shadowMapPass.renderDirectionalLightShadow( renderer, scene, camera, light, cascadeClips, lightMatrices, shadowMaps ); var cascadeClipsNear = cascadeClips.slice(); var cascadeClipsFar = cascadeClips.slice(); cascadeClipsNear.pop(); cascadeClipsFar.shift(); // Iterate from far to near cascadeClipsNear.reverse(); cascadeClipsFar.reverse(); lightMatrices.reverse(); light.__cascadeClipsNear = cascadeClipsNear; light.__cascadeClipsFar = cascadeClipsFar; light.__shadowMap = shadowMaps[0]; light.__lightMatrices = lightMatrices; break; } }, // Update light volume mesh // Light volume mesh is rendered in light accumulate pass instead of full quad. // It will reduce pixels significantly when local light is relatively small. // And we can use custom volume mesh to shape the light. // // See "Deferred Shading Optimizations" in GDC2011 _updateLightProxy: function (light) { var volumeMesh; if (light.volumeMesh) { volumeMesh = light.volumeMesh; } else { switch (light.type) { // Only local light (point and spot) needs volume mesh. // Directional and ambient light renders in full quad case 'POINT_LIGHT': case 'SPHERE_LIGHT': var shader = light.type === 'SPHERE_LIGHT' ? this._sphereLightShader : this._pointLightShader; // Volume mesh created automatically if (!light.__volumeMesh) { light.__volumeMesh = new Mesh({ material: this._createLightPassMat(shader), geometry: this._lightSphereGeo, // Disable culling // if light volume mesh intersect camera near plane // We need mesh inside can still be rendered culling: false }); } volumeMesh = light.__volumeMesh; var r = light.range + (light.radius || 0); volumeMesh.scale.set(r, r, r); break; case 'SPOT_LIGHT': light.__volumeMesh = light.__volumeMesh || new Mesh({ material: this._createLightPassMat(this._spotLightShader), geometry: this._lightConeGeo, culling: false }); volumeMesh = light.__volumeMesh; var aspect = Math.tan(light.penumbraAngle * Math.PI / 180); var range = light.range; volumeMesh.scale.set(aspect * range, aspect * range, range / 2); break; case 'TUBE_LIGHT': light.__volumeMesh = light.__volumeMesh || new Mesh({ material: this._createLightPassMat(this._tubeLightShader), geometry: this._lightCylinderGeo, culling: false }); volumeMesh = light.__volumeMesh; var range = light.range; volumeMesh.scale.set(light.length / 2 + range, range, range); break; } } if (volumeMesh) { volumeMesh.update(); // Apply light transform Matrix4.multiply(volumeMesh.worldTransform, light.worldTransform, volumeMesh.worldTransform); var hasShadow = this.shadowMapPass && light.castShadow; volumeMesh.material[hasShadow ? 'define' : 'undefine']('fragment', 'SHADOWMAP_ENABLED'); } }, _renderVolumeMeshList: (function () { var worldView = new Matrix4(); var preZMaterial = new Material({ shader: new Shader(Shader.source('clay.prez.vertex'), Shader.source('clay.prez.fragment')) }); function getPreZMaterial() { return preZMaterial; } return function (renderer, scene, camera, volumeMeshList) { var gl = renderer.gl; gl.depthFunc(gl.LEQUAL); for (var i = 0; i < volumeMeshList.length; i++) { var volumeMesh = volumeMeshList[i]; // Frustum culling Matrix4.multiply(worldView, camera.viewMatrix, volumeMesh.worldTransform); if (scene.isFrustumCulled(volumeMesh, camera, worldView.array)) { continue; } // Use prez to avoid one pixel rendered twice gl.colorMask(false, false, false, false); gl.depthMask(true); // depthMask must be enabled before clear DEPTH_BUFFER gl.clear(gl.DEPTH_BUFFER_BIT); renderer.renderPass([volumeMesh], camera, { getMaterial: getPreZMaterial }); // Render light gl.colorMask(true, true, true, true); volumeMesh.material.depthMask = true; renderer.renderPass([volumeMesh], camera); } gl.depthFunc(gl.LESS); }; })(), /** * @param {clay.Renderer} renderer */ dispose: function (renderer) { this._gBuffer.dispose(renderer); this._lightAccumFrameBuffer.dispose(renderer); this._lightAccumTex.dispose(renderer); this._lightConeGeo.dispose(renderer); this._lightCylinderGeo.dispose(renderer); this._lightSphereGeo.dispose(renderer); this._fullQuadPass.dispose(renderer); this._outputPass.dispose(renderer); this._directionalLightMat.dispose(renderer); this.shadowMapPass.dispose(renderer); } }); /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 2x2 Matrix * @name mat2 */ var mat2 = {}; /** * Creates a new identity mat2 * * @returns {mat2} a new 2x2 matrix */ mat2.create = function() { var out = new GLMAT_ARRAY_TYPE(4); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; return out; }; /** * Creates a new mat2 initialized with values from an existing matrix * * @param {mat2} a matrix to clone * @returns {mat2} a new 2x2 matrix */ mat2.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(4); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Copy the values from one mat2 to another * * @param {mat2} out the receiving matrix * @param {mat2} a the source matrix * @returns {mat2} out */ mat2.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Set a mat2 to the identity matrix * * @param {mat2} out the receiving matrix * @returns {mat2} out */ mat2.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; return out; }; /** * Transpose the values of a mat2 * * @param {mat2} out the receiving matrix * @param {mat2} a the source matrix * @returns {mat2} out */ mat2.transpose = function(out, a) { // If we are transposing ourselves we can skip a few steps but have to cache some values if (out === a) { var a1 = a[1]; out[1] = a[2]; out[2] = a1; } else { out[0] = a[0]; out[1] = a[2]; out[2] = a[1]; out[3] = a[3]; } return out; }; /** * Inverts a mat2 * * @param {mat2} out the receiving matrix * @param {mat2} a the source matrix * @returns {mat2} out */ mat2.invert = function(out, a) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], // Calculate the determinant det = a0 * a3 - a2 * a1; if (!det) { return null; } det = 1.0 / det; out[0] = a3 * det; out[1] = -a1 * det; out[2] = -a2 * det; out[3] = a0 * det; return out; }; /** * Calculates the adjugate of a mat2 * * @param {mat2} out the receiving matrix * @param {mat2} a the source matrix * @returns {mat2} out */ mat2.adjoint = function(out, a) { // Caching this value is nessecary if out == a var a0 = a[0]; out[0] = a[3]; out[1] = -a[1]; out[2] = -a[2]; out[3] = a0; return out; }; /** * Calculates the determinant of a mat2 * * @param {mat2} a the source matrix * @returns {Number} determinant of a */ mat2.determinant = function (a) { return a[0] * a[3] - a[2] * a[1]; }; /** * Multiplies two mat2's * * @param {mat2} out the receiving matrix * @param {mat2} a the first operand * @param {mat2} b the second operand * @returns {mat2} out */ mat2.multiply = function (out, a, b) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3]; var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = a0 * b0 + a2 * b1; out[1] = a1 * b0 + a3 * b1; out[2] = a0 * b2 + a2 * b3; out[3] = a1 * b2 + a3 * b3; return out; }; /** * Alias for {@link mat2.multiply} * @function */ mat2.mul = mat2.multiply; /** * Rotates a mat2 by the given angle * * @param {mat2} out the receiving matrix * @param {mat2} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat2} out */ mat2.rotate = function (out, a, rad) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], s = Math.sin(rad), c = Math.cos(rad); out[0] = a0 * c + a2 * s; out[1] = a1 * c + a3 * s; out[2] = a0 * -s + a2 * c; out[3] = a1 * -s + a3 * c; return out; }; /** * Scales the mat2 by the dimensions in the given vec2 * * @param {mat2} out the receiving matrix * @param {mat2} a the matrix to rotate * @param {vec2} v the vec2 to scale the matrix by * @returns {mat2} out **/ mat2.scale = function(out, a, v) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], v0 = v[0], v1 = v[1]; out[0] = a0 * v0; out[1] = a1 * v0; out[2] = a2 * v1; out[3] = a3 * v1; return out; }; /** * Returns Frobenius norm of a mat2 * * @param {mat2} a the matrix to calculate Frobenius norm of * @returns {Number} Frobenius norm */ mat2.frob = function (a) { return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2))) }; /** * Returns L, D and U matrices (Lower triangular, Diagonal and Upper triangular) by factorizing the input matrix * @param {mat2} L the lower triangular matrix * @param {mat2} D the diagonal matrix * @param {mat2} U the upper triangular matrix * @param {mat2} a the input matrix to factorize */ mat2.LDU = function (L, D, U, a) { L[2] = a[2]/a[0]; U[0] = a[0]; U[1] = a[1]; U[3] = a[3] - L[2] * U[1]; return [L, D, U]; }; /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 2x3 Matrix * @name mat2d * * @description * A mat2d contains six elements defined as: * <pre> * [a, c, tx, * b, d, ty] * </pre> * This is a short form for the 3x3 matrix: * <pre> * [a, c, tx, * b, d, ty, * 0, 0, 1] * </pre> * The last row is ignored so the array is shorter and operations are faster. */ var mat2d = {}; /** * Creates a new identity mat2d * * @returns {mat2d} a new 2x3 matrix */ mat2d.create = function() { var out = new GLMAT_ARRAY_TYPE(6); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 0; return out; }; /** * Creates a new mat2d initialized with values from an existing matrix * * @param {mat2d} a matrix to clone * @returns {mat2d} a new 2x3 matrix */ mat2d.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(6); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; return out; }; /** * Copy the values from one mat2d to another * * @param {mat2d} out the receiving matrix * @param {mat2d} a the source matrix * @returns {mat2d} out */ mat2d.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; return out; }; /** * Set a mat2d to the identity matrix * * @param {mat2d} out the receiving matrix * @returns {mat2d} out */ mat2d.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 0; return out; }; /** * Inverts a mat2d * * @param {mat2d} out the receiving matrix * @param {mat2d} a the source matrix * @returns {mat2d} out */ mat2d.invert = function(out, a) { var aa = a[0], ab = a[1], ac = a[2], ad = a[3], atx = a[4], aty = a[5]; var det = aa * ad - ab * ac; if(!det){ return null; } det = 1.0 / det; out[0] = ad * det; out[1] = -ab * det; out[2] = -ac * det; out[3] = aa * det; out[4] = (ac * aty - ad * atx) * det; out[5] = (ab * atx - aa * aty) * det; return out; }; /** * Calculates the determinant of a mat2d * * @param {mat2d} a the source matrix * @returns {Number} determinant of a */ mat2d.determinant = function (a) { return a[0] * a[3] - a[1] * a[2]; }; /** * Multiplies two mat2d's * * @param {mat2d} out the receiving matrix * @param {mat2d} a the first operand * @param {mat2d} b the second operand * @returns {mat2d} out */ mat2d.multiply = function (out, a, b) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5]; out[0] = a0 * b0 + a2 * b1; out[1] = a1 * b0 + a3 * b1; out[2] = a0 * b2 + a2 * b3; out[3] = a1 * b2 + a3 * b3; out[4] = a0 * b4 + a2 * b5 + a4; out[5] = a1 * b4 + a3 * b5 + a5; return out; }; /** * Alias for {@link mat2d.multiply} * @function */ mat2d.mul = mat2d.multiply; /** * Rotates a mat2d by the given angle * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat2d} out */ mat2d.rotate = function (out, a, rad) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], s = Math.sin(rad), c = Math.cos(rad); out[0] = a0 * c + a2 * s; out[1] = a1 * c + a3 * s; out[2] = a0 * -s + a2 * c; out[3] = a1 * -s + a3 * c; out[4] = a4; out[5] = a5; return out; }; /** * Scales the mat2d by the dimensions in the given vec2 * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to translate * @param {vec2} v the vec2 to scale the matrix by * @returns {mat2d} out **/ mat2d.scale = function(out, a, v) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], v0 = v[0], v1 = v[1]; out[0] = a0 * v0; out[1] = a1 * v0; out[2] = a2 * v1; out[3] = a3 * v1; out[4] = a4; out[5] = a5; return out; }; /** * Translates the mat2d by the dimensions in the given vec2 * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to translate * @param {vec2} v the vec2 to translate the matrix by * @returns {mat2d} out **/ mat2d.translate = function(out, a, v) { var a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], v0 = v[0], v1 = v[1]; out[0] = a0; out[1] = a1; out[2] = a2; out[3] = a3; out[4] = a0 * v0 + a2 * v1 + a4; out[5] = a1 * v0 + a3 * v1 + a5; return out; }; /** * Returns Frobenius norm of a mat2d * * @param {mat2d} a the matrix to calculate Frobenius norm of * @returns {Number} Frobenius norm */ mat2d.frob = function (a) { return(Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2) + Math.pow(a[2], 2) + Math.pow(a[3], 2) + Math.pow(a[4], 2) + Math.pow(a[5], 2) + 1)) }; /** * @fileoverview gl-matrix - High performance matrix and vector operations * @author Brandon Jones * @author Colin MacKenzie IV * @version 2.2.2 */ /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var glmatrix = { vec2: vec2, vec3: vec3, vec4: vec4, mat2: mat2, mat2d: mat2d, mat3: mat3, mat4: mat4, quat: quat }; // DEPRECATED var tmpBoundingBox$1 = new BoundingBox(); /** * @constructor clay.InstancedMesh * @extends clay.Mesh */ var InstancedMesh = Mesh.extend(function () { return /** @lends clay.InstancedMesh# */ { /** * Instances array. Each object in array must have node property * @type Array * @example * var node = new clay.Node() * instancedMesh.instances.push({ * node: node * }); */ instances: [], instancedAttributes: {}, _attributesSymbols: [] }; }, function () { this._cache = new Cache(); this.createInstancedAttribute('instanceMat1', 'float', 4, 1); this.createInstancedAttribute('instanceMat2', 'float', 4, 1); this.createInstancedAttribute('instanceMat3', 'float', 4, 1); }, { isInstancedMesh: function () { return true; }, getInstanceCount: function () { return this.instances.length; }, removeAttribute: function (symbol) { var idx = this._attributesSymbols.indexOf(symbol); if (idx >= 0) { this._attributesSymbols.splice(idx, 1); } delete this.instancedAttributes[symbol]; }, createInstancedAttribute: function (symbol, type, size, divisor) { if (this.instancedAttributes[symbol]) { return; } this.instancedAttributes[symbol] = { symbol: symbol, type: type, size: size, divisor: divisor == null ? 1 : divisor, value: null }; this._attributesSymbols.push(symbol); }, getInstancedAttributesBuffers: function (renderer) { var cache = this._cache; cache.use(renderer.__uid__); var buffers = cache.get('buffers') || []; if (cache.isDirty('dirty')) { var gl = renderer.gl; for (var i = 0; i < this._attributesSymbols.length; i++) { var attr = this.instancedAttributes[this._attributesSymbols[i]]; var bufferObj = buffers[i]; if (!bufferObj) { bufferObj = { buffer: gl.createBuffer() }; buffers[i] = bufferObj; } bufferObj.symbol = attr.symbol; bufferObj.divisor = attr.divisor; bufferObj.size = attr.size; bufferObj.type = attr.type; gl.bindBuffer(gl.ARRAY_BUFFER, bufferObj.buffer); gl.bufferData(gl.ARRAY_BUFFER, attr.value, gl.DYNAMIC_DRAW); } cache.fresh('dirty'); cache.put('buffers', buffers); } return buffers; }, update: function (forceUpdateWorld) { Mesh.prototype.update.call(this, forceUpdateWorld); var arraySize = this.getInstanceCount() * 4; var instancedAttributes = this.instancedAttributes; var instanceMat1 = instancedAttributes.instanceMat1.value; var instanceMat2 = instancedAttributes.instanceMat2.value; var instanceMat3 = instancedAttributes.instanceMat3.value; if (!instanceMat1 || instanceMat1.length !== arraySize) { instanceMat1 = instancedAttributes.instanceMat1.value = new Float32Array(arraySize); instanceMat2 = instancedAttributes.instanceMat2.value = new Float32Array(arraySize); instanceMat3 = instancedAttributes.instanceMat3.value = new Float32Array(arraySize); } var sourceBoundingBox = (this.skeleton && this.skeleton.boundingBox) || this.geometry.boundingBox; var needUpdateBoundingBox = sourceBoundingBox != null && (this.castShadow || this.frustumCulling); if (needUpdateBoundingBox && this.instances.length > 0) { this.boundingBox = this.boundingBox || new BoundingBox(); this.boundingBox.min.set(Infinity, Infinity, Infinity); this.boundingBox.max.set(-Infinity, -Infinity, -Infinity); } else { this.boundingBox = null; } for (var i = 0; i < this.instances.length; i++) { var instance = this.instances[i]; var node = instance.node; if (!node) { throw new Error('Instance must include node'); } var transform = node.worldTransform.array; var i4 = i * 4; instanceMat1[i4] = transform[0]; instanceMat1[i4 + 1] = transform[1]; instanceMat1[i4 + 2] = transform[2]; instanceMat1[i4 + 3] = transform[12]; instanceMat2[i4] = transform[4]; instanceMat2[i4 + 1] = transform[5]; instanceMat2[i4 + 2] = transform[6]; instanceMat2[i4 + 3] = transform[13]; instanceMat3[i4] = transform[8]; instanceMat3[i4 + 1] = transform[9]; instanceMat3[i4 + 2] = transform[10]; instanceMat3[i4 + 3] = transform[14]; // Update bounding box if (needUpdateBoundingBox) { tmpBoundingBox$1.transformFrom(sourceBoundingBox, node.worldTransform); this.boundingBox.union(tmpBoundingBox$1, this.boundingBox); } } this._cache.dirty('dirty'); } }); /** * @constructor clay.light.Sphere * @extends {clay.Light} */ var SphereLight = Light.extend( /** @lends clay.light.Sphere# */ { /** * @type {number} */ range: 100, /** * @type {number} */ radius: 5 }, { type: 'SPHERE_LIGHT', uniformTemplates: { sphereLightPosition: { type: '3f', value: function(instance) { return instance.getWorldPosition().array; } }, sphereLightRange: { type: '1f', value: function(instance) { return instance.range; } }, sphereLightRadius: { type: '1f', value: function(instance) { return instance.radius; } }, sphereLightColor: { type: '3f', value: function(instance) { var color = instance.color; var intensity = instance.intensity; return [color[0]*intensity, color[1]*intensity, color[2]*intensity]; } } } }); /** * @constructor clay.light.Tube * @extends {clay.Light} */ var TubeLight = Light.extend( /** @lends clay.light.Tube# */ { /** * @type {number} */ range: 100, /** * @type {number} */ length: 10 }, { type: 'TUBE_LIGHT', uniformTemplates: { tubeLightPosition: { type: '3f', value: function(instance) { return instance.getWorldPosition().array; } }, tubeLightExtend: { type: '3f', value: (function() { var x = new Vector3(); return function(instance) { // Extend in x axis return x.copy(instance.worldTransform.x) .normalize().scale(instance.length / 2).array; }; })() }, tubeLightRange: { type: '1f', value: function(instance) { return instance.range; } }, tubeLightColor: { type: '3f', value: function(instance) { var color = instance.color; var intensity = instance.intensity; return [color[0]*intensity, color[1]*intensity, color[2]*intensity]; } } } }); /** * @constructor clay.loader.FX * @extends clay.core.Base */ var FXLoader = Base.extend(/** @lends clay.loader.FX# */ { /** * @type {string} */ rootPath: '', /** * @type {string} */ textureRootPath: '', /** * @type {string} */ shaderRootPath: '', /** * @type {clay.Scene} */ scene: null, /** * @type {clay.Camera} */ camera: null }, /** @lends clay.loader.FX.prototype */ { /** * @param {string} url */ load: function(url) { var self = this; if (!this.rootPath) { this.rootPath = url.slice(0, url.lastIndexOf('/')); } vendor.request.get({ url: url, onprogress: function(percent, loaded, total) { self.trigger('progress', percent, loaded, total); }, onerror: function(e) { self.trigger('error', e); }, responseType: 'text', onload: function (data) { createCompositor(JSON.parse(data), { textureRootPath: this.textureRootPath || this.rootPath, camera: this.camera, scene: this.scene }); } }); } }); /** * @constructor * @alias clay.Matrix2 */ var Matrix2 = function() { /** * Storage of Matrix2 * @name array * @type {Float32Array} * @memberOf clay.Matrix2# */ this.array = mat2.create(); /** * @name _dirty * @type {boolean} * @memberOf clay.Matrix2# */ this._dirty = true; }; Matrix2.prototype = { constructor: Matrix2, /** * Set components from array * @param {Float32Array|number[]} arr */ setArray: function (arr) { for (var i = 0; i < this.array.length; i++) { this.array[i] = arr[i]; } this._dirty = true; return this; }, /** * Clone a new Matrix2 * @return {clay.Matrix2} */ clone: function() { return (new Matrix2()).copy(this); }, /** * Copy from b * @param {clay.Matrix2} b * @return {clay.Matrix2} */ copy: function(b) { mat2.copy(this.array, b.array); this._dirty = true; return this; }, /** * Calculate the adjugate of self, in-place * @return {clay.Matrix2} */ adjoint: function() { mat2.adjoint(this.array, this.array); this._dirty = true; return this; }, /** * Calculate matrix determinant * @return {number} */ determinant: function() { return mat2.determinant(this.array); }, /** * Set to a identity matrix * @return {clay.Matrix2} */ identity: function() { mat2.identity(this.array); this._dirty = true; return this; }, /** * Invert self * @return {clay.Matrix2} */ invert: function() { mat2.invert(this.array, this.array); this._dirty = true; return this; }, /** * Alias for mutiply * @param {clay.Matrix2} b * @return {clay.Matrix2} */ mul: function(b) { mat2.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiplyLeft * @param {clay.Matrix2} a * @return {clay.Matrix2} */ mulLeft: function(a) { mat2.mul(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Multiply self and b * @param {clay.Matrix2} b * @return {clay.Matrix2} */ multiply: function(b) { mat2.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Multiply a and self, a is on the left * @param {clay.Matrix2} a * @return {clay.Matrix2} */ multiplyLeft: function(a) { mat2.multiply(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Rotate self by a given radian * @param {number} rad * @return {clay.Matrix2} */ rotate: function(rad) { mat2.rotate(this.array, this.array, rad); this._dirty = true; return this; }, /** * Scale self by s * @param {clay.Vector2} s * @return {clay.Matrix2} */ scale: function(v) { mat2.scale(this.array, this.array, v.array); this._dirty = true; return this; }, /** * Transpose self, in-place. * @return {clay.Matrix2} */ transpose: function() { mat2.transpose(this.array, this.array); this._dirty = true; return this; }, toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; /** * @param {Matrix2} out * @param {Matrix2} a * @return {Matrix2} */ Matrix2.adjoint = function(out, a) { mat2.adjoint(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @return {clay.Matrix2} */ Matrix2.copy = function(out, a) { mat2.copy(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2} a * @return {number} */ Matrix2.determinant = function(a) { return mat2.determinant(a.array); }; /** * @param {clay.Matrix2} out * @return {clay.Matrix2} */ Matrix2.identity = function(out) { mat2.identity(out.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @return {clay.Matrix2} */ Matrix2.invert = function(out, a) { mat2.invert(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @param {clay.Matrix2} b * @return {clay.Matrix2} */ Matrix2.mul = function(out, a, b) { mat2.mul(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @param {clay.Matrix2} b * @return {clay.Matrix2} */ Matrix2.multiply = Matrix2.mul; /** * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @param {number} rad * @return {clay.Matrix2} */ Matrix2.rotate = function(out, a, rad) { mat2.rotate(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix2} out * @param {clay.Matrix2} a * @param {clay.Vector2} v * @return {clay.Matrix2} */ Matrix2.scale = function(out, a, v) { mat2.scale(out.array, a.array, v.array); out._dirty = true; return out; }; /** * @param {Matrix2} out * @param {Matrix2} a * @return {Matrix2} */ Matrix2.transpose = function(out, a) { mat2.transpose(out.array, a.array); out._dirty = true; return out; }; /** * @constructor * @alias clay.Matrix2d */ var Matrix2d = function() { /** * Storage of Matrix2d * @name array * @type {Float32Array} * @memberOf clay.Matrix2d# */ this.array = mat2d.create(); /** * @name _dirty * @type {boolean} * @memberOf clay.Matrix2d# */ this._dirty = true; }; Matrix2d.prototype = { constructor: Matrix2d, /** * Set components from array * @param {Float32Array|number[]} arr */ setArray: function (arr) { for (var i = 0; i < this.array.length; i++) { this.array[i] = arr[i]; } this._dirty = true; return this; }, /** * Clone a new Matrix2d * @return {clay.Matrix2d} */ clone: function() { return (new Matrix2d()).copy(this); }, /** * Copy from b * @param {clay.Matrix2d} b * @return {clay.Matrix2d} */ copy: function(b) { mat2d.copy(this.array, b.array); this._dirty = true; return this; }, /** * Calculate matrix determinant * @return {number} */ determinant: function() { return mat2d.determinant(this.array); }, /** * Set to a identity matrix * @return {clay.Matrix2d} */ identity: function() { mat2d.identity(this.array); this._dirty = true; return this; }, /** * Invert self * @return {clay.Matrix2d} */ invert: function() { mat2d.invert(this.array, this.array); this._dirty = true; return this; }, /** * Alias for mutiply * @param {clay.Matrix2d} b * @return {clay.Matrix2d} */ mul: function(b) { mat2d.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiplyLeft * @param {clay.Matrix2d} a * @return {clay.Matrix2d} */ mulLeft: function(b) { mat2d.mul(this.array, b.array, this.array); this._dirty = true; return this; }, /** * Multiply self and b * @param {clay.Matrix2d} b * @return {clay.Matrix2d} */ multiply: function(b) { mat2d.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Multiply a and self, a is on the left * @param {clay.Matrix2d} a * @return {clay.Matrix2d} */ multiplyLeft: function(b) { mat2d.multiply(this.array, b.array, this.array); this._dirty = true; return this; }, /** * Rotate self by a given radian * @param {number} rad * @return {clay.Matrix2d} */ rotate: function(rad) { mat2d.rotate(this.array, this.array, rad); this._dirty = true; return this; }, /** * Scale self by s * @param {clay.Vector2} s * @return {clay.Matrix2d} */ scale: function(s) { mat2d.scale(this.array, this.array, s.array); this._dirty = true; return this; }, /** * Translate self by v * @param {clay.Vector2} v * @return {clay.Matrix2d} */ translate: function(v) { mat2d.translate(this.array, this.array, v.array); this._dirty = true; return this; }, toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @return {clay.Matrix2d} */ Matrix2d.copy = function(out, a) { mat2d.copy(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2d} a * @return {number} */ Matrix2d.determinant = function(a) { return mat2d.determinant(a.array); }; /** * @param {clay.Matrix2d} out * @return {clay.Matrix2d} */ Matrix2d.identity = function(out) { mat2d.identity(out.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @return {clay.Matrix2d} */ Matrix2d.invert = function(out, a) { mat2d.invert(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @param {clay.Matrix2d} b * @return {clay.Matrix2d} */ Matrix2d.mul = function(out, a, b) { mat2d.mul(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @param {clay.Matrix2d} b * @return {clay.Matrix2d} */ Matrix2d.multiply = Matrix2d.mul; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @param {number} rad * @return {clay.Matrix2d} */ Matrix2d.rotate = function(out, a, rad) { mat2d.rotate(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @param {clay.Vector2} v * @return {clay.Matrix2d} */ Matrix2d.scale = function(out, a, v) { mat2d.scale(out.array, a.array, v.array); out._dirty = true; return out; }; /** * @param {clay.Matrix2d} out * @param {clay.Matrix2d} a * @param {clay.Vector2} v * @return {clay.Matrix2d} */ Matrix2d.translate = function(out, a, v) { mat2d.translate(out.array, a.array, v.array); out._dirty = true; return out; }; /** * @constructor * @alias clay.Matrix3 */ var Matrix3 = function () { /** * Storage of Matrix3 * @name array * @type {Float32Array} * @memberOf clay.Matrix3# */ this.array = mat3.create(); /** * @name _dirty * @type {boolean} * @memberOf clay.Matrix3# */ this._dirty = true; }; Matrix3.prototype = { constructor: Matrix3, /** * Set components from array * @param {Float32Array|number[]} arr */ setArray: function (arr) { for (var i = 0; i < this.array.length; i++) { this.array[i] = arr[i]; } this._dirty = true; return this; }, /** * Calculate the adjugate of self, in-place * @return {clay.Matrix3} */ adjoint: function () { mat3.adjoint(this.array, this.array); this._dirty = true; return this; }, /** * Clone a new Matrix3 * @return {clay.Matrix3} */ clone: function () { return (new Matrix3()).copy(this); }, /** * Copy from b * @param {clay.Matrix3} b * @return {clay.Matrix3} */ copy: function (b) { mat3.copy(this.array, b.array); this._dirty = true; return this; }, /** * Calculate matrix determinant * @return {number} */ determinant: function () { return mat3.determinant(this.array); }, /** * Copy the values from Matrix2d a * @param {clay.Matrix2d} a * @return {clay.Matrix3} */ fromMat2d: function (a) { mat3.fromMat2d(this.array, a.array); this._dirty = true; return this; }, /** * Copies the upper-left 3x3 values of Matrix4 * @param {clay.Matrix4} a * @return {clay.Matrix3} */ fromMat4: function (a) { mat3.fromMat4(this.array, a.array); this._dirty = true; return this; }, /** * Calculates a rotation matrix from the given quaternion * @param {clay.Quaternion} q * @return {clay.Matrix3} */ fromQuat: function (q) { mat3.fromQuat(this.array, q.array); this._dirty = true; return this; }, /** * Set to a identity matrix * @return {clay.Matrix3} */ identity: function () { mat3.identity(this.array); this._dirty = true; return this; }, /** * Invert self * @return {clay.Matrix3} */ invert: function () { mat3.invert(this.array, this.array); this._dirty = true; return this; }, /** * Alias for mutiply * @param {clay.Matrix3} b * @return {clay.Matrix3} */ mul: function (b) { mat3.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiplyLeft * @param {clay.Matrix3} a * @return {clay.Matrix3} */ mulLeft: function (a) { mat3.mul(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Multiply self and b * @param {clay.Matrix3} b * @return {clay.Matrix3} */ multiply: function (b) { mat3.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Multiply a and self, a is on the left * @param {clay.Matrix3} a * @return {clay.Matrix3} */ multiplyLeft: function (a) { mat3.multiply(this.array, a.array, this.array); this._dirty = true; return this; }, /** * Rotate self by a given radian * @param {number} rad * @return {clay.Matrix3} */ rotate: function (rad) { mat3.rotate(this.array, this.array, rad); this._dirty = true; return this; }, /** * Scale self by s * @param {clay.Vector2} s * @return {clay.Matrix3} */ scale: function (v) { mat3.scale(this.array, this.array, v.array); this._dirty = true; return this; }, /** * Translate self by v * @param {clay.Vector2} v * @return {clay.Matrix3} */ translate: function (v) { mat3.translate(this.array, this.array, v.array); this._dirty = true; return this; }, /** * Calculates a 3x3 normal matrix (transpose inverse) from the 4x4 matrix * @param {clay.Matrix4} a */ normalFromMat4: function (a) { mat3.normalFromMat4(this.array, a.array); this._dirty = true; return this; }, /** * Transpose self, in-place. * @return {clay.Matrix2} */ transpose: function () { mat3.transpose(this.array, this.array); this._dirty = true; return this; }, toString: function () { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @return {clay.Matrix3} */ Matrix3.adjoint = function (out, a) { mat3.adjoint(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @return {clay.Matrix3} */ Matrix3.copy = function (out, a) { mat3.copy(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} a * @return {number} */ Matrix3.determinant = function (a) { return mat3.determinant(a.array); }; /** * @param {clay.Matrix3} out * @return {clay.Matrix3} */ Matrix3.identity = function (out) { mat3.identity(out.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @return {clay.Matrix3} */ Matrix3.invert = function (out, a) { mat3.invert(out.array, a.array); return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @param {clay.Matrix3} b * @return {clay.Matrix3} */ Matrix3.mul = function (out, a, b) { mat3.mul(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @param {clay.Matrix3} b * @return {clay.Matrix3} */ Matrix3.multiply = Matrix3.mul; /** * @param {clay.Matrix3} out * @param {clay.Matrix2d} a * @return {clay.Matrix3} */ Matrix3.fromMat2d = function (out, a) { mat3.fromMat2d(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix4} a * @return {clay.Matrix3} */ Matrix3.fromMat4 = function (out, a) { mat3.fromMat4(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Quaternion} a * @return {clay.Matrix3} */ Matrix3.fromQuat = function (out, q) { mat3.fromQuat(out.array, q.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix4} a * @return {clay.Matrix3} */ Matrix3.normalFromMat4 = function (out, a) { mat3.normalFromMat4(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @param {number} rad * @return {clay.Matrix3} */ Matrix3.rotate = function (out, a, rad) { mat3.rotate(out.array, a.array, rad); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @param {clay.Vector2} v * @return {clay.Matrix3} */ Matrix3.scale = function (out, a, v) { mat3.scale(out.array, a.array, v.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @return {clay.Matrix3} */ Matrix3.transpose = function (out, a) { mat3.transpose(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Matrix3} out * @param {clay.Matrix3} a * @param {clay.Vector2} v * @return {clay.Matrix3} */ Matrix3.translate = function (out, a, v) { mat3.translate(out.array, a.array, v.array); out._dirty = true; return out; }; /** * Random or constant 1d, 2d, 3d vector generator * @constructor * @alias clay.Value */ var Value = function() {}; /** * @function * @param {number|clay.Vector2|clay.Vector3} [out] * @return {number|clay.Vector2|clay.Vector3} */ Value.prototype.get = function(out) {}; // Constant var ConstantValue = function(val) { this.get = function() { return val; }; }; ConstantValue.prototype = new Value(); ConstantValue.prototype.constructor = ConstantValue; // Vector var VectorValue = function(val) { var Constructor = val.constructor; this.get = function(out) { if (!out) { out = new Constructor(); } out.copy(val); return out; }; }; VectorValue.prototype = new Value(); VectorValue.prototype.constructor = VectorValue; //Random 1D var Random1D = function(min, max) { var range = max - min; this.get = function() { return Math.random() * range + min; }; }; Random1D.prototype = new Value(); Random1D.prototype.constructor = Random1D; // Random2D var Random2D = function(min, max) { var rangeX = max.x - min.x; var rangeY = max.y - min.y; this.get = function(out) { if (!out) { out = new Vector2(); } Vector2.set( out, rangeX * Math.random() + min.array[0], rangeY * Math.random() + min.array[1] ); return out; }; }; Random2D.prototype = new Value(); Random2D.prototype.constructor = Random2D; var Random3D = function(min, max) { var rangeX = max.x - min.x; var rangeY = max.y - min.y; var rangeZ = max.z - min.z; this.get = function(out) { if (!out) { out = new Vector3(); } Vector3.set( out, rangeX * Math.random() + min.array[0], rangeY * Math.random() + min.array[1], rangeZ * Math.random() + min.array[2] ); return out; }; }; Random3D.prototype = new Value(); Random3D.prototype.constructor = Random3D; // Factory methods /** * Create a constant 1d value generator * @param {number} constant * @return {clay.Value} */ Value.constant = function(constant) { return new ConstantValue(constant); }; /** * Create a constant vector value(2d or 3d) generator * @param {clay.Vector2|clay.Vector3} vector * @return {clay.Value} */ Value.vector = function(vector) { return new VectorValue(vector); }; /** * Create a random 1d value generator * @param {number} min * @param {number} max * @return {clay.Value} */ Value.random1D = function(min, max) { return new Random1D(min, max); }; /** * Create a random 2d value generator * @param {clay.Vector2} min * @param {clay.Vector2} max * @return {clay.Value} */ Value.random2D = function(min, max) { return new Random2D(min, max); }; /** * Create a random 3d value generator * @param {clay.Vector3} min * @param {clay.Vector3} max * @return {clay.Value} */ Value.random3D = function(min, max) { return new Random3D(min, max); }; /** * @constructor * @alias clay.Vector4 * @param {number} x * @param {number} y * @param {number} z * @param {number} w */ var Vector4 = function(x, y, z, w) { x = x || 0; y = y || 0; z = z || 0; w = w || 0; /** * Storage of Vector4, read and write of x, y, z, w will change the values in array * All methods also operate on the array instead of x, y, z, w components * @name array * @type {Float32Array} * @memberOf clay.Vector4# */ this.array = vec4.fromValues(x, y, z, w); /** * Dirty flag is used by the Node to determine * if the matrix is updated to latest * @name _dirty * @type {boolean} * @memberOf clay.Vector4# */ this._dirty = true; }; Vector4.prototype = { constructor: Vector4, /** * Add b to self * @param {clay.Vector4} b * @return {clay.Vector4} */ add: function(b) { vec4.add(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Set x, y and z components * @param {number} x * @param {number} y * @param {number} z * @param {number} w * @return {clay.Vector4} */ set: function(x, y, z, w) { this.array[0] = x; this.array[1] = y; this.array[2] = z; this.array[3] = w; this._dirty = true; return this; }, /** * Set x, y, z and w components from array * @param {Float32Array|number[]} arr * @return {clay.Vector4} */ setArray: function(arr) { this.array[0] = arr[0]; this.array[1] = arr[1]; this.array[2] = arr[2]; this.array[3] = arr[3]; this._dirty = true; return this; }, /** * Clone a new Vector4 * @return {clay.Vector4} */ clone: function() { return new Vector4(this.x, this.y, this.z, this.w); }, /** * Copy from b * @param {clay.Vector4} b * @return {clay.Vector4} */ copy: function(b) { vec4.copy(this.array, b.array); this._dirty = true; return this; }, /** * Alias for distance * @param {clay.Vector4} b * @return {number} */ dist: function(b) { return vec4.dist(this.array, b.array); }, /** * Distance between self and b * @param {clay.Vector4} b * @return {number} */ distance: function(b) { return vec4.distance(this.array, b.array); }, /** * Alias for divide * @param {clay.Vector4} b * @return {clay.Vector4} */ div: function(b) { vec4.div(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Divide self by b * @param {clay.Vector4} b * @return {clay.Vector4} */ divide: function(b) { vec4.divide(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Dot product of self and b * @param {clay.Vector4} b * @return {number} */ dot: function(b) { return vec4.dot(this.array, b.array); }, /** * Alias of length * @return {number} */ len: function() { return vec4.len(this.array); }, /** * Calculate the length * @return {number} */ length: function() { return vec4.length(this.array); }, /** * Linear interpolation between a and b * @param {clay.Vector4} a * @param {clay.Vector4} b * @param {number} t * @return {clay.Vector4} */ lerp: function(a, b, t) { vec4.lerp(this.array, a.array, b.array, t); this._dirty = true; return this; }, /** * Minimum of self and b * @param {clay.Vector4} b * @return {clay.Vector4} */ min: function(b) { vec4.min(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Maximum of self and b * @param {clay.Vector4} b * @return {clay.Vector4} */ max: function(b) { vec4.max(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Alias for multiply * @param {clay.Vector4} b * @return {clay.Vector4} */ mul: function(b) { vec4.mul(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Mutiply self and b * @param {clay.Vector4} b * @return {clay.Vector4} */ multiply: function(b) { vec4.multiply(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Negate self * @return {clay.Vector4} */ negate: function() { vec4.negate(this.array, this.array); this._dirty = true; return this; }, /** * Normalize self * @return {clay.Vector4} */ normalize: function() { vec4.normalize(this.array, this.array); this._dirty = true; return this; }, /** * Generate random x, y, z, w components with a given scale * @param {number} scale * @return {clay.Vector4} */ random: function(scale) { vec4.random(this.array, scale); this._dirty = true; return this; }, /** * Scale self * @param {number} scale * @return {clay.Vector4} */ scale: function(s) { vec4.scale(this.array, this.array, s); this._dirty = true; return this; }, /** * Scale b and add to self * @param {clay.Vector4} b * @param {number} scale * @return {clay.Vector4} */ scaleAndAdd: function(b, s) { vec4.scaleAndAdd(this.array, this.array, b.array, s); this._dirty = true; return this; }, /** * Alias for squaredDistance * @param {clay.Vector4} b * @return {number} */ sqrDist: function(b) { return vec4.sqrDist(this.array, b.array); }, /** * Squared distance between self and b * @param {clay.Vector4} b * @return {number} */ squaredDistance: function(b) { return vec4.squaredDistance(this.array, b.array); }, /** * Alias for squaredLength * @return {number} */ sqrLen: function() { return vec4.sqrLen(this.array); }, /** * Squared length of self * @return {number} */ squaredLength: function() { return vec4.squaredLength(this.array); }, /** * Alias for subtract * @param {clay.Vector4} b * @return {clay.Vector4} */ sub: function(b) { vec4.sub(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Subtract b from self * @param {clay.Vector4} b * @return {clay.Vector4} */ subtract: function(b) { vec4.subtract(this.array, this.array, b.array); this._dirty = true; return this; }, /** * Transform self with a Matrix4 m * @param {clay.Matrix4} m * @return {clay.Vector4} */ transformMat4: function(m) { vec4.transformMat4(this.array, this.array, m.array); this._dirty = true; return this; }, /** * Transform self with a Quaternion q * @param {clay.Quaternion} q * @return {clay.Vector4} */ transformQuat: function(q) { vec4.transformQuat(this.array, this.array, q.array); this._dirty = true; return this; }, toString: function() { return '[' + Array.prototype.join.call(this.array, ',') + ']'; }, toArray: function () { return Array.prototype.slice.call(this.array); } }; var defineProperty$3 = Object.defineProperty; // Getter and Setter if (defineProperty$3) { var proto$4 = Vector4.prototype; /** * @name x * @type {number} * @memberOf clay.Vector4 * @instance */ defineProperty$3(proto$4, 'x', { get: function () { return this.array[0]; }, set: function (value) { this.array[0] = value; this._dirty = true; } }); /** * @name y * @type {number} * @memberOf clay.Vector4 * @instance */ defineProperty$3(proto$4, 'y', { get: function () { return this.array[1]; }, set: function (value) { this.array[1] = value; this._dirty = true; } }); /** * @name z * @type {number} * @memberOf clay.Vector4 * @instance */ defineProperty$3(proto$4, 'z', { get: function () { return this.array[2]; }, set: function (value) { this.array[2] = value; this._dirty = true; } }); /** * @name w * @type {number} * @memberOf clay.Vector4 * @instance */ defineProperty$3(proto$4, 'w', { get: function () { return this.array[3]; }, set: function (value) { this.array[3] = value; this._dirty = true; } }); } // Supply methods that are not in place /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.add = function(out, a, b) { vec4.add(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {number} x * @param {number} y * @param {number} z * @return {clay.Vector4} */ Vector4.set = function(out, x, y, z, w) { vec4.set(out.array, x, y, z, w); out._dirty = true; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.copy = function(out, b) { vec4.copy(out.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {number} */ Vector4.dist = function(a, b) { return vec4.distance(a.array, b.array); }; /** * @function * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {number} */ Vector4.distance = Vector4.dist; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.div = function(out, a, b) { vec4.divide(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.divide = Vector4.div; /** * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {number} */ Vector4.dot = function(a, b) { return vec4.dot(a.array, b.array); }; /** * @param {clay.Vector4} a * @return {number} */ Vector4.len = function(b) { return vec4.length(b.array); }; // Vector4.length = Vector4.len; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @param {number} t * @return {clay.Vector4} */ Vector4.lerp = function(out, a, b, t) { vec4.lerp(out.array, a.array, b.array, t); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.min = function(out, a, b) { vec4.min(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.max = function(out, a, b) { vec4.max(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.mul = function(out, a, b) { vec4.multiply(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.multiply = Vector4.mul; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @return {clay.Vector4} */ Vector4.negate = function(out, a) { vec4.negate(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @return {clay.Vector4} */ Vector4.normalize = function(out, a) { vec4.normalize(out.array, a.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {number} scale * @return {clay.Vector4} */ Vector4.random = function(out, scale) { vec4.random(out.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {number} scale * @return {clay.Vector4} */ Vector4.scale = function(out, a, scale) { vec4.scale(out.array, a.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @param {number} scale * @return {clay.Vector4} */ Vector4.scaleAndAdd = function(out, a, b, scale) { vec4.scaleAndAdd(out.array, a.array, b.array, scale); out._dirty = true; return out; }; /** * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {number} */ Vector4.sqrDist = function(a, b) { return vec4.sqrDist(a.array, b.array); }; /** * @function * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {number} */ Vector4.squaredDistance = Vector4.sqrDist; /** * @param {clay.Vector4} a * @return {number} */ Vector4.sqrLen = function(a) { return vec4.sqrLen(a.array); }; /** * @function * @param {clay.Vector4} a * @return {number} */ Vector4.squaredLength = Vector4.sqrLen; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.sub = function(out, a, b) { vec4.subtract(out.array, a.array, b.array); out._dirty = true; return out; }; /** * @function * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Vector4} b * @return {clay.Vector4} */ Vector4.subtract = Vector4.sub; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Matrix4} m * @return {clay.Vector4} */ Vector4.transformMat4 = function(out, a, m) { vec4.transformMat4(out.array, a.array, m.array); out._dirty = true; return out; }; /** * @param {clay.Vector4} out * @param {clay.Vector4} a * @param {clay.Quaternion} q * @return {clay.Vector4} */ Vector4.transformQuat = function(out, a, q) { vec4.transformQuat(out.array, a.array, q.array); out._dirty = true; return out; }; /** * @constructor * @alias clay.particle.Particle */ var Particle = function() { /** * @type {clay.Vector3} */ this.position = new Vector3(); /** * Use euler angle to represent particle rotation * @type {clay.Vector3} */ this.rotation = new Vector3(); /** * @type {?clay.Vector3} */ this.velocity = null; /** * @type {?clay.Vector3} */ this.angularVelocity = null; /** * @type {number} */ this.life = 1; /** * @type {number} */ this.age = 0; /** * @type {number} */ this.spriteSize = 1; /** * @type {number} */ this.weight = 1; /** * @type {clay.particle.Emitter} */ this.emitter = null; }; /** * Update particle position * @param {number} deltaTime */ Particle.prototype.update = function(deltaTime) { if (this.velocity) { vec3.scaleAndAdd(this.position.array, this.position.array, this.velocity.array, deltaTime); } if (this.angularVelocity) { vec3.scaleAndAdd(this.rotation.array, this.rotation.array, this.angularVelocity.array, deltaTime); } }; /** * @constructor clay.particle.Emitter * @extends clay.core.Base */ var Emitter = Base.extend( /** @lends clay.particle.Emitter# */ { /** * Maximum number of particles created by this emitter * @type {number} */ max: 1000, /** * Number of particles created by this emitter each shot * @type {number} */ amount: 20, // Init status for each particle /** * Particle life generator * @type {?clay.Value.<number>} */ life: null, /** * Particle position generator * @type {?clay.Value.<clay.Vector3>} */ position: null, /** * Particle rotation generator * @type {?clay.Value.<clay.Vector3>} */ rotation: null, /** * Particle velocity generator * @type {?clay.Value.<clay.Vector3>} */ velocity: null, /** * Particle angular velocity generator * @type {?clay.Value.<clay.Vector3>} */ angularVelocity: null, /** * Particle sprite size generator * @type {?clay.Value.<number>} */ spriteSize: null, /** * Particle weight generator * @type {?clay.Value.<number>} */ weight: null, _particlePool: null }, function() { this._particlePool = []; // TODO Reduce heap memory for (var i = 0; i < this.max; i++) { var particle = new Particle(); particle.emitter = this; this._particlePool.push(particle); if (this.velocity) { particle.velocity = new Vector3(); } if (this.angularVelocity) { particle.angularVelocity = new Vector3(); } } }, /** @lends clay.particle.Emitter.prototype */ { /** * Emitter number of particles and push them to a given particle list. Emmit number is defined by amount property * @param {Array.<clay.particle.Particle>} out */ emit: function(out) { var amount = Math.min(this._particlePool.length, this.amount); var particle; for (var i = 0; i < amount; i++) { particle = this._particlePool.pop(); // Initialize particle status if (this.position) { this.position.get(particle.position); } if (this.rotation) { this.rotation.get(particle.rotation); } if (this.velocity) { this.velocity.get(particle.velocity); } if (this.angularVelocity) { this.angularVelocity.get(particle.angularVelocity); } if (this.life) { particle.life = this.life.get(); } if (this.spriteSize) { particle.spriteSize = this.spriteSize.get(); } if (this.weight) { particle.weight = this.weight.get(); } particle.age = 0; out.push(particle); } }, /** * Kill a dead particle and put it back in the pool * @param {clay.particle.Particle} particle */ kill: function(particle) { this._particlePool.push(particle); } }); /** * Create a constant 1d value generator. Alias for {@link clay.Value.constant} * @function clay.particle.Emitter.constant */ Emitter.constant = Value.constant; /** * Create a constant vector value(2d or 3d) generator. Alias for {@link clay.Value.vector} * @function clay.particle.Emitter.vector */ Emitter.vector = Value.vector; /** * Create a random 1d value generator. Alias for {@link clay.Value.random1D} * @function clay.particle.Emitter.random1D */ Emitter.random1D = Value.random1D; /** * Create a random 2d value generator. Alias for {@link clay.Value.random2D} * @function clay.particle.Emitter.random2D */ Emitter.random2D = Value.random2D; /** * Create a random 3d value generator. Alias for {@link clay.Value.random3D} * @function clay.particle.Emitter.random3D */ Emitter.random3D = Value.random3D; /** * @constructor clay.particle.Field * @extends clay.core.Base */ var Field = Base.extend({}, { /** * Apply a field to the particle and update the particle velocity * @param {clay.Vector3} velocity * @param {clay.Vector3} position * @param {number} weight * @param {number} deltaTime * @memberOf clay.particle.Field.prototype */ applyTo: function(velocity, position, weight, deltaTime) {} }); /** * @constructor clay.particle.ForceField * @extends clay.particle.Field */ var ForceField = Field.extend(function() { return { force: new Vector3() }; }, { applyTo: function(velocity, position, weight, deltaTime) { if (weight > 0) { vec3.scaleAndAdd(velocity.array, velocity.array, this.force.array, deltaTime / weight); } } }); var particleEssl = "@export clay.particle.vertex\nuniform mat4 worldView : WORLDVIEW;\nuniform mat4 projection : PROJECTION;\nattribute vec3 position : POSITION;\nattribute vec3 normal : NORMAL;\n#ifdef UV_ANIMATION\nattribute vec2 texcoord0 : TEXCOORD_0;\nattribute vec2 texcoord1 : TEXCOORD_1;\nvarying vec2 v_Uv0;\nvarying vec2 v_Uv1;\n#endif\nvarying float v_Age;\nvoid main() {\n v_Age = normal.x;\n float rotation = normal.y;\n vec4 worldViewPosition = worldView * vec4(position, 1.0);\n gl_Position = projection * worldViewPosition;\n float w = gl_Position.w;\n gl_PointSize = normal.z * projection[0].x / w;\n #ifdef UV_ANIMATION\n v_Uv0 = texcoord0;\n v_Uv1 = texcoord1;\n #endif\n}\n@end\n@export clay.particle.fragment\nuniform sampler2D sprite;\nuniform sampler2D gradient;\nuniform vec3 color : [1.0, 1.0, 1.0];\nuniform float alpha : 1.0;\nvarying float v_Age;\n#ifdef UV_ANIMATION\nvarying vec2 v_Uv0;\nvarying vec2 v_Uv1;\n#endif\nvoid main() {\n vec4 color = vec4(color, alpha);\n #ifdef SPRITE_ENABLED\n #ifdef UV_ANIMATION\n color *= texture2D(sprite, mix(v_Uv0, v_Uv1, gl_PointCoord));\n #else\n color *= texture2D(sprite, gl_PointCoord);\n #endif\n #endif\n #ifdef GRADIENT_ENABLED\n color *= texture2D(gradient, vec2(v_Age, 0.5));\n #endif\n gl_FragColor = color;\n}\n@end"; Shader['import'](particleEssl); var particleShader = new Shader(Shader.source('clay.particle.vertex'), Shader.source('clay.particle.fragment')); /** * @constructor clay.particle.ParticleRenderable * @extends clay.Renderable * * @example * var particleRenderable = new clay.particle.ParticleRenderable({ * spriteAnimationTileX: 4, * spriteAnimationTileY: 4, * spriteAnimationRepeat: 1 * }); * scene.add(particleRenderable); * // Enable uv animation in the shader * particleRenderable.material.define('both', 'UV_ANIMATION'); * var Emitter = clay.particle.Emitter; * var Vector3 = clay.Vector3; * var emitter = new Emitter({ * max: 2000, * amount: 100, * life: Emitter.random1D(10, 20), * position: Emitter.vector(new Vector3()), * velocity: Emitter.random3D(new Vector3(-10, 0, -10), new Vector3(10, 0, 10)); * }); * particleRenderable.addEmitter(emitter); * var gravityField = new clay.particle.ForceField(); * gravityField.force.y = -10; * particleRenderable.addField(gravityField); * ... * animation.on('frame', function(frameTime) { * particleRenderable.updateParticles(frameTime); * renderer.render(scene, camera); * }); */ var ParticleRenderable = Renderable.extend(/** @lends clay.particle.ParticleRenderable# */ { /** * @type {boolean} */ loop: true, /** * @type {boolean} */ oneshot: false, /** * Duration of particle system in milliseconds * @type {number} */ duration: 1, // UV Animation /** * @type {number} */ spriteAnimationTileX: 1, /** * @type {number} */ spriteAnimationTileY: 1, /** * @type {number} */ spriteAnimationRepeat: 0, mode: Renderable.POINTS, ignorePicking: true, _elapsedTime: 0, _emitting: true }, function(){ this.geometry = new Geometry({ dynamic: true }); if (!this.material) { this.material = new Material({ shader: particleShader, transparent: true, depthMask: false }); this.material.enableTexture('sprite'); } this._particles = []; this._fields = []; this._emitters = []; }, /** @lends clay.particle.ParticleRenderable.prototype */ { culling: false, frustumCulling: false, castShadow: false, receiveShadow: false, /** * Add emitter * @param {clay.particle.Emitter} emitter */ addEmitter: function(emitter) { this._emitters.push(emitter); }, /** * Remove emitter * @param {clay.particle.Emitter} emitter */ removeEmitter: function(emitter) { this._emitters.splice(this._emitters.indexOf(emitter), 1); }, /** * Add field * @param {clay.particle.Field} field */ addField: function(field) { this._fields.push(field); }, /** * Remove field * @param {clay.particle.Field} field */ removeField: function(field) { this._fields.splice(this._fields.indexOf(field), 1); }, /** * Reset the particle system. */ reset: function() { // Put all the particles back for (var i = 0; i < this._particles.length; i++) { var p = this._particles[i]; p.emitter.kill(p); } this._particles.length = 0; this._elapsedTime = 0; this._emitting = true; }, /** * @param {number} deltaTime */ updateParticles: function(deltaTime) { // MS => Seconds deltaTime /= 1000; this._elapsedTime += deltaTime; var particles = this._particles; if (this._emitting) { for (var i = 0; i < this._emitters.length; i++) { this._emitters[i].emit(particles); } if (this.oneshot) { this._emitting = false; } } // Aging var len = particles.length; for (var i = 0; i < len;) { var p = particles[i]; p.age += deltaTime; if (p.age >= p.life) { p.emitter.kill(p); particles[i] = particles[len-1]; particles.pop(); len--; } else { i++; } } for (var i = 0; i < len; i++) { // Update var p = particles[i]; if (this._fields.length > 0) { for (var j = 0; j < this._fields.length; j++) { this._fields[j].applyTo(p.velocity, p.position, p.weight, deltaTime); } } p.update(deltaTime); } this._updateVertices(); }, _updateVertices: function() { var geometry = this.geometry; // If has uv animation var animTileX = this.spriteAnimationTileX; var animTileY = this.spriteAnimationTileY; var animRepeat = this.spriteAnimationRepeat; var nUvAnimFrame = animTileY * animTileX * animRepeat; var hasUvAnimation = nUvAnimFrame > 1; var positions = geometry.attributes.position.value; // Put particle status in normal var normals = geometry.attributes.normal.value; var uvs = geometry.attributes.texcoord0.value; var uvs2 = geometry.attributes.texcoord1.value; var len = this._particles.length; if (!positions || positions.length !== len * 3) { // TODO Optimize positions = geometry.attributes.position.value = new Float32Array(len * 3); normals = geometry.attributes.normal.value = new Float32Array(len * 3); if (hasUvAnimation) { uvs = geometry.attributes.texcoord0.value = new Float32Array(len * 2); uvs2 = geometry.attributes.texcoord1.value = new Float32Array(len * 2); } } var invAnimTileX = 1 / animTileX; for (var i = 0; i < len; i++) { var particle = this._particles[i]; var offset = i * 3; for (var j = 0; j < 3; j++) { positions[offset + j] = particle.position.array[j]; normals[offset] = particle.age / particle.life; // normals[offset + 1] = particle.rotation; normals[offset + 1] = 0; normals[offset + 2] = particle.spriteSize; } var offset2 = i * 2; if (hasUvAnimation) { // TODO var p = particle.age / particle.life; var stage = Math.round(p * (nUvAnimFrame - 1)) * animRepeat; var v = Math.floor(stage * invAnimTileX); var u = stage - v * animTileX; uvs[offset2] = u / animTileX; uvs[offset2 + 1] = 1 - v / animTileY; uvs2[offset2] = (u + 1) / animTileX; uvs2[offset2 + 1] = 1 - (v + 1) / animTileY; } } geometry.dirty(); }, /** * @return {boolean} */ isFinished: function() { return this._elapsedTime > this.duration && !this.loop; }, /** * @param {clay.Renderer} renderer */ dispose: function(renderer) { // Put all the particles back for (var i = 0; i < this._particles.length; i++) { var p = this._particles[i]; p.emitter.kill(p); } this.geometry.dispose(renderer); // TODO Dispose texture ? }, /** * @return {clay.particle.ParticleRenderable} */ clone: function() { var particleRenderable = new ParticleRenderable({ material: this.material }); particleRenderable.loop = this.loop; particleRenderable.duration = this.duration; particleRenderable.oneshot = this.oneshot; particleRenderable.spriteAnimationRepeat = this.spriteAnimationRepeat; particleRenderable.spriteAnimationTileY = this.spriteAnimationTileY; particleRenderable.spriteAnimationTileX = this.spriteAnimationTileX; particleRenderable.position.copy(this.position); particleRenderable.rotation.copy(this.rotation); particleRenderable.scale.copy(this.scale); for (var i = 0; i < this._children.length; i++) { particleRenderable.add(this._children[i].clone()); } return particleRenderable; } }); var colorEssl = "@export clay.picking.color.vertex\nuniform mat4 worldViewProjection : WORLDVIEWPROJECTION;\nattribute vec3 position : POSITION;\n@import clay.chunk.skinning_header\nvoid main(){\n vec3 skinnedPosition = position;\n #ifdef SKINNING\n @import clay.chunk.skin_matrix\n skinnedPosition = (skinMatrixWS * vec4(position, 1.0)).xyz;\n #endif\n gl_Position = worldViewProjection * vec4(skinnedPosition, 1.0);\n}\n@end\n@end\n@export clay.picking.color.fragment\nuniform vec4 color : [1.0, 1.0, 1.0, 1.0];\nvoid main() {\n gl_FragColor = color;\n}\n@end"; Shader.import(colorEssl); /** * Pixel picking is gpu based picking, which is fast and accurate. * But not like ray picking, it can't get the intersection point and triangle. * @constructor clay.picking.PixelPicking * @extends clay.core.Base */ var PixelPicking = Base.extend(function() { return /** @lends clay.picking.PixelPicking# */ { /** * Target renderer * @type {clay.Renderer} */ renderer: null, /** * Downsample ratio of hidden frame buffer * @type {number} */ downSampleRatio: 1, width: 100, height: 100, lookupOffset: 1, _frameBuffer: null, _texture: null, _shader: null, _idMaterials: [], _lookupTable: [], _meshMaterials: [], _idOffset: 0 }; }, function() { if (this.renderer) { this.width = this.renderer.getWidth(); this.height = this.renderer.getHeight(); } this._init(); }, /** @lends clay.picking.PixelPicking.prototype */ { _init: function() { this._texture = new Texture2D({ width: this.width * this.downSampleRatio, height: this.height * this.downSampleRatio }); this._frameBuffer = new FrameBuffer(); this._shader = new Shader(Shader.source('clay.picking.color.vertex'), Shader.source('clay.picking.color.fragment')); }, /** * Set picking presision * @param {number} ratio */ setPrecision: function(ratio) { this._texture.width = this.width * ratio; this._texture.height = this.height * ratio; this.downSampleRatio = ratio; }, resize: function(width, height) { this._texture.width = width * this.downSampleRatio; this._texture.height = height * this.downSampleRatio; this.width = width; this.height = height; this._texture.dirty(); }, /** * Update the picking framebuffer * @param {number} ratio */ update: function(scene, camera) { var renderer = this.renderer; if (renderer.getWidth() !== this.width || renderer.getHeight() !== this.height) { this.resize(renderer.width, renderer.height); } this._frameBuffer.attach(this._texture); this._frameBuffer.bind(renderer); this._idOffset = this.lookupOffset; this._setMaterial(scene); renderer.render(scene, camera); this._restoreMaterial(); this._frameBuffer.unbind(renderer); }, _setMaterial: function(root) { for (var i =0; i < root._children.length; i++) { var child = root._children[i]; if (child.geometry && child.material && child.material.shader) { var id = this._idOffset++; var idx = id - this.lookupOffset; var material = this._idMaterials[idx]; if (!material) { material = new Material({ shader: this._shader }); var color = packID(id); color[0] /= 255; color[1] /= 255; color[2] /= 255; color[3] = 1.0; material.set('color', color); this._idMaterials[idx] = material; } this._meshMaterials[idx] = child.material; this._lookupTable[idx] = child; child.material = material; } if (child._children.length) { this._setMaterial(child); } } }, /** * Pick the object * @param {number} x Mouse position x * @param {number} y Mouse position y * @return {clay.Node} */ pick: function(x, y) { var renderer = this.renderer; var ratio = this.downSampleRatio; x = Math.ceil(ratio * x); y = Math.ceil(ratio * (this.height - y)); this._frameBuffer.bind(renderer); var pixel = new Uint8Array(4); var _gl = renderer.gl; // TODO out of bounds ? // preserveDrawingBuffer ? _gl.readPixels(x, y, 1, 1, _gl.RGBA, _gl.UNSIGNED_BYTE, pixel); this._frameBuffer.unbind(renderer); // Skip interpolated pixel because of anti alias if (pixel[3] === 255) { var id = unpackID(pixel[0], pixel[1], pixel[2]); if (id) { var el = this._lookupTable[id - this.lookupOffset]; return el; } } }, _restoreMaterial: function() { for (var i = 0; i < this._lookupTable.length; i++) { this._lookupTable[i].material = this._meshMaterials[i]; } }, dispose: function(renderer) { this._frameBuffer.dispose(renderer); } }); function packID(id){ var r = id >> 16; var g = (id - (r << 8)) >> 8; var b = id - (r << 16) - (g<<8); return [r, g, b]; } function unpackID(r, g, b){ return (r << 16) + (g<<8) + b; } var doc = typeof document === 'undefined' ? {} : document; /** * @constructor clay.plugin.FreeControl * @example * var control = new clay.plugin.FreeControl({ * target: camera, * domElement: renderer.canvas * }); * ... * timeline.on('frame', function(frameTime) { * control.update(frameTime); * renderer.render(scene, camera); * }); */ var FreeControl = Base.extend(function() { return /** @lends clay.plugin.FreeControl# */ { /** * Scene node to control, mostly it is a camera * @type {clay.Node} */ target: null, /** * Target dom to bind with mouse events * @type {HTMLElement} */ domElement: null, /** * Mouse move sensitivity * @type {number} */ sensitivity: 1, /** * Target move speed * @type {number} */ speed: 0.4, /** * Up axis * @type {clay.Vector3} */ up: new Vector3(0, 1, 0), /** * If lock vertical movement * @type {boolean} */ verticalMoveLock: false, /** * @type {clay.Timeline} */ timeline: null, _moveForward: false, _moveBackward: false, _moveLeft: false, _moveRight: false, _offsetPitch: 0, _offsetRoll: 0 }; }, function() { this._lockChange = this._lockChange.bind(this); this._keyDown = this._keyDown.bind(this); this._keyUp = this._keyUp.bind(this); this._mouseMove = this._mouseMove.bind(this); if (this.domElement) { this.init(); } }, /** @lends clay.plugin.FreeControl.prototype */ { /** * init control */ init: function() { // Use pointer lock // http://www.html5rocks.com/en/tutorials/pointerlock/intro/ var el = this.domElement; //Must request pointer lock after click event, can't not do it directly //Why ? ? vendor.addEventListener(el, 'click', this._requestPointerLock); vendor.addEventListener(doc, 'pointerlockchange', this._lockChange); vendor.addEventListener(doc, 'mozpointerlockchange', this._lockChange); vendor.addEventListener(doc, 'webkitpointerlockchange', this._lockChange); vendor.addEventListener(doc, 'keydown', this._keyDown); vendor.addEventListener(doc, 'keyup', this._keyUp); if (this.timeline) { this.timeline.on('frame', this._detectMovementChange, this); } }, /** * Dispose control */ dispose: function() { var el = this.domElement; el.exitPointerLock = el.exitPointerLock || el.mozExitPointerLock || el.webkitExitPointerLock; if (el.exitPointerLock) { el.exitPointerLock(); } vendor.removeEventListener(el, 'click', this._requestPointerLock); vendor.removeEventListener(doc, 'pointerlockchange', this._lockChange); vendor.removeEventListener(doc, 'mozpointerlockchange', this._lockChange); vendor.removeEventListener(doc, 'webkitpointerlockchange', this._lockChange); vendor.removeEventListener(doc, 'keydown', this._keyDown); vendor.removeEventListener(doc, 'keyup', this._keyUp); if (this.timeline) { this.timeline.off('frame', this._detectMovementChange); } }, _requestPointerLock: function() { var el = this; el.requestPointerLock = el.requestPointerLock || el.mozRequestPointerLock || el.webkitRequestPointerLock; el.requestPointerLock(); }, /** * Control update. Should be invoked every frame * @param {number} frameTime Frame time */ update: function (frameTime) { var target = this.target; var position = this.target.position; var xAxis = target.localTransform.x.normalize(); var zAxis = target.localTransform.z.normalize(); if (this.verticalMoveLock) { zAxis.y = 0; zAxis.normalize(); } var speed = this.speed * frameTime / 20; if (this._moveForward) { // Opposite direction of z position.scaleAndAdd(zAxis, -speed); } if (this._moveBackward) { position.scaleAndAdd(zAxis, speed); } if (this._moveLeft) { position.scaleAndAdd(xAxis, -speed / 2); } if (this._moveRight) { position.scaleAndAdd(xAxis, speed / 2); } target.rotateAround(target.position, this.up, -this._offsetPitch * frameTime * Math.PI / 360); var xAxis = target.localTransform.x; target.rotateAround(target.position, xAxis, -this._offsetRoll * frameTime * Math.PI / 360); this._offsetRoll = this._offsetPitch = 0; }, _lockChange: function() { if ( doc.pointerLockElement === this.domElement || doc.mozPointerLockElement === this.domElement || doc.webkitPointerLockElement === this.domElement ) { vendor.addEventListener(doc, 'mousemove', this._mouseMove, false); } else { vendor.removeEventListener(doc, 'mousemove', this._mouseMove); } }, _mouseMove: function(e) { var dx = e.movementX || e.mozMovementX || e.webkitMovementX || 0; var dy = e.movementY || e.mozMovementY || e.webkitMovementY || 0; this._offsetPitch += dx * this.sensitivity / 200; this._offsetRoll += dy * this.sensitivity / 200; // Trigger change event to remind renderer do render this.trigger('change'); }, _detectMovementChange: function (frameTime) { if (this._moveForward || this._moveBackward || this._moveLeft || this._moveRight) { this.trigger('change'); } this.update(frameTime); }, _keyDown: function(e) { switch(e.keyCode) { case 87: //w case 37: //up arrow this._moveForward = true; break; case 83: //s case 40: //down arrow this._moveBackward = true; break; case 65: //a case 37: //left arrow this._moveLeft = true; break; case 68: //d case 39: //right arrow this._moveRight = true; break; } // Trigger change event to remind renderer do render this.trigger('change'); }, _keyUp: function(e) { switch(e.keyCode) { case 87: //w case 37: //up arrow this._moveForward = false; break; case 83: //s case 40: //down arrow this._moveBackward = false; break; case 65: //a case 37: //left arrow this._moveLeft = false; break; case 68: //d case 39: //right arrow this._moveRight = false; break; } } }); /** * Gamepad Control plugin. * * @constructor clay.plugin.GamepadControl * * @example * init: function(app) { * this._gamepadControl = new clay.plugin.GamepadControl({ * target: camera, * onStandardGamepadReady: customCallback * }); * }, * * loop: function(app) { * this._gamepadControl.update(app.frameTime); * } */ var GamepadControl = Base.extend(function() { return /** @lends clay.plugin.GamepadControl# */ { /** * Scene node to control, mostly it is a camera. * * @type {clay.Node} */ target: null, /** * Move speed. * * @type {number} */ moveSpeed: 0.1, /** * Look around speed. * * @type {number} */ lookAroundSpeed: 0.1, /** * Up axis. * * @type {clay.Vector3} */ up: new Vector3(0, 1, 0), /** * Timeline. * * @type {clay.Timeline} */ timeline: null, /** * Function to be called when a standard gamepad is ready to use. * * @type {function} */ onStandardGamepadReady: function(gamepad){}, /** * Function to be called when a gamepad is disconnected. * * @type {function} */ onGamepadDisconnected: function(gamepad){}, // Private properties: _moveForward: false, _moveBackward: false, _moveLeft: false, _moveRight: false, _offsetPitch: 0, _offsetRoll: 0, _connectedGamepadIndex: 0, _standardGamepadAvailable: false, _gamepadAxisThreshold: 0.3 }; }, function() { this._checkGamepadCompatibility = this._checkGamepadCompatibility.bind(this); this._disconnectGamepad = this._disconnectGamepad.bind(this); this._getStandardGamepad = this._getStandardGamepad.bind(this); this._scanPressedGamepadButtons = this._scanPressedGamepadButtons.bind(this); this._scanInclinedGamepadAxes = this._scanInclinedGamepadAxes.bind(this); this.update = this.update.bind(this); // If browser supports Gamepad API: if (typeof navigator.getGamepads === 'function') { this.init(); } }, /** @lends clay.plugin.GamepadControl.prototype */ { /** * Init. control. */ init: function() { /** * When user begins to interact with connected gamepad: * * @see https://w3c.github.io/gamepad/#dom-gamepadevent */ vendor.addEventListener(window, 'gamepadconnected', this._checkGamepadCompatibility); if (this.timeline) { this.timeline.on('frame', this.update); } vendor.addEventListener(window, 'gamepaddisconnected', this._disconnectGamepad); }, /** * Dispose control. */ dispose: function() { vendor.removeEventListener(window, 'gamepadconnected', this._checkGamepadCompatibility); if (this.timeline) { this.timeline.off('frame', this.update); } vendor.removeEventListener(window, 'gamepaddisconnected', this._disconnectGamepad); }, /** * Control's update. Should be invoked every frame. * * @param {number} frameTime Frame time. */ update: function (frameTime) { if (!this._standardGamepadAvailable) { return; } this._scanPressedGamepadButtons(); this._scanInclinedGamepadAxes(); // Update target depending on user input. var target = this.target; var position = this.target.position; var xAxis = target.localTransform.x.normalize(); var zAxis = target.localTransform.z.normalize(); var moveSpeed = this.moveSpeed * frameTime / 20; if (this._moveForward) { // Opposite direction of z. position.scaleAndAdd(zAxis, -moveSpeed); } if (this._moveBackward) { position.scaleAndAdd(zAxis, moveSpeed); } if (this._moveLeft) { position.scaleAndAdd(xAxis, -moveSpeed); } if (this._moveRight) { position.scaleAndAdd(xAxis, moveSpeed); } target.rotateAround(target.position, this.up, -this._offsetPitch * frameTime * Math.PI / 360); var xAxis = target.localTransform.x; target.rotateAround(target.position, xAxis, -this._offsetRoll * frameTime * Math.PI / 360); /* * If necessary: trigger `update` event. * XXX This can economize rendering OPs. */ if (this._moveForward === true || this._moveBackward === true || this._moveLeft === true || this._moveRight === true || this._offsetPitch !== 0 || this._offsetRoll !== 0) { this.trigger('update'); } // Reset values to avoid lost of control. this._moveForward = this._moveBackward = this._moveLeft = this._moveRight = false; this._offsetPitch = this._offsetRoll = 0; }, // Private methods: _checkGamepadCompatibility: function(event) { /** * If connected gamepad has a **standard** layout: * * @see https://w3c.github.io/gamepad/#remapping about standard. */ if (event.gamepad.mapping === 'standard') { this._standardGamepadIndex = event.gamepad.index; this._standardGamepadAvailable = true; this.onStandardGamepadReady(event.gamepad); } }, _disconnectGamepad: function(event) { this._standardGamepadAvailable = false; this.onGamepadDisconnected(event.gamepad); }, _getStandardGamepad: function() { return navigator.getGamepads()[this._standardGamepadIndex]; }, _scanPressedGamepadButtons: function() { var gamepadButtons = this._getStandardGamepad().buttons; // For each gamepad button: for (var gamepadButtonId = 0; gamepadButtonId < gamepadButtons.length; gamepadButtonId++) { // Get user input. var gamepadButton = gamepadButtons[gamepadButtonId]; if (gamepadButton.pressed) { switch (gamepadButtonId) { // D-pad Up case 12: this._moveForward = true; break; // D-pad Down case 13: this._moveBackward = true; break; // D-pad Left case 14: this._moveLeft = true; break; // D-pad Right case 15: this._moveRight = true; break; } } } }, _scanInclinedGamepadAxes: function() { var gamepadAxes = this._getStandardGamepad().axes; // For each gamepad axis: for (var gamepadAxisId = 0; gamepadAxisId < gamepadAxes.length; gamepadAxisId++) { // Get user input. var gamepadAxis = gamepadAxes[gamepadAxisId]; // XXX We use a threshold because axes are never neutral. if (Math.abs(gamepadAxis) > this._gamepadAxisThreshold) { switch (gamepadAxisId) { // Left stick X± case 0: this._moveLeft = gamepadAxis < 0; this._moveRight = gamepadAxis > 0; break; // Left stick Y± case 1: this._moveForward = gamepadAxis < 0; this._moveBackward = gamepadAxis > 0; break; // Right stick X± case 2: this._offsetPitch += gamepadAxis * this.lookAroundSpeed; break; // Right stick Y± case 3: this._offsetRoll += gamepadAxis * this.lookAroundSpeed; break; } } } } }); var GestureMgr = function () { this._track = []; }; GestureMgr.prototype = { constructor: GestureMgr, recognize: function (event, target, root) { this._doTrack(event, target, root); return this._recognize(event); }, clear: function () { this._track.length = 0; return this; }, _doTrack: function (event, target, root) { var touches = event.targetTouches; if (!touches) { return; } var trackItem = { points: [], touches: [], target: target, event: event }; for (var i = 0, len = touches.length; i < len; i++) { var touch = touches[i]; trackItem.points.push([touch.clientX, touch.clientY]); trackItem.touches.push(touch); } this._track.push(trackItem); }, _recognize: function (event) { for (var eventName in recognizers) { if (recognizers.hasOwnProperty(eventName)) { var gestureInfo = recognizers[eventName](this._track, event); if (gestureInfo) { return gestureInfo; } } } } }; function dist(pointPair) { var dx = pointPair[1][0] - pointPair[0][0]; var dy = pointPair[1][1] - pointPair[0][1]; return Math.sqrt(dx * dx + dy * dy); } function center(pointPair) { return [ (pointPair[0][0] + pointPair[1][0]) / 2, (pointPair[0][1] + pointPair[1][1]) / 2 ]; } var recognizers = { pinch: function (track, event) { var trackLen = track.length; if (!trackLen) { return; } var pinchEnd = (track[trackLen - 1] || {}).points; var pinchPre = (track[trackLen - 2] || {}).points || pinchEnd; if (pinchPre && pinchPre.length > 1 && pinchEnd && pinchEnd.length > 1 ) { var pinchScale = dist(pinchEnd) / dist(pinchPre); !isFinite(pinchScale) && (pinchScale = 1); event.pinchScale = pinchScale; var pinchCenter = center(pinchEnd); event.pinchX = pinchCenter[0]; event.pinchY = pinchCenter[1]; return { type: 'pinch', target: track[0].target, event: event }; } } }; var uvs = [[0, 0], [0, 1], [1, 1], [1, 0]]; var tris = [0, 1, 2, 2, 3, 0]; var InfinitePlane = Mesh.extend({ camera: null, plane: null, maxGrid: 0, // TODO frustumCulling: false }, function () { var geometry = this.geometry = new Geometry({ dynamic: true }); geometry.attributes.position.init(6); geometry.attributes.normal.init(6); geometry.attributes.texcoord0.init(6); geometry.indices = new Uint16Array(6); this.plane = new Plane$1(); }, { updateGeometry: function () { var coords = this._unProjectGrid(); if (!coords) { return; } var positionAttr = this.geometry.attributes.position; var normalAttr = this.geometry.attributes.normal; var texcoords = this.geometry.attributes.texcoord0; var indices = this.geometry.indices; for (var i = 0; i < 6; i++) { var idx = tris[i]; positionAttr.set(i, coords[idx].array); normalAttr.set(i, this.plane.normal.array); texcoords.set(i, uvs[idx]); indices[i] = i; } this.geometry.dirty(); }, // http://fileadmin.cs.lth.se/graphics/theses/projects/projgrid/ _unProjectGrid: (function () { var planeViewSpace = new Plane$1(); var lines = [ 0, 1, 0, 2, 1, 3, 2, 3, 4, 5, 4, 6, 5, 7, 6, 7, 0, 4, 1, 5, 2, 6, 3, 7 ]; var start = new Vector3(); var end = new Vector3(); var points = []; // 1----2 // | | // 0----3 var coords = []; for (var i = 0; i < 4; i++) { coords[i] = new Vector3(0, 0); } var ray = new Ray(); return function () { planeViewSpace.copy(this.plane); planeViewSpace.applyTransform(this.camera.viewMatrix); var frustumVertices = this.camera.frustum.vertices; var nPoints = 0; // Intersect with lines of frustum for (var i = 0; i < 12; i++) { start.array = frustumVertices[lines[i * 2]]; end.array = frustumVertices[lines[i * 2 + 1]]; var point = planeViewSpace.intersectLine(start, end, points[nPoints]); if (point) { if (!points[nPoints]) { points[nPoints] = point; } nPoints++; } } if (nPoints === 0) { return; } for (var i = 0; i < nPoints; i++) { points[i].applyProjection(this.camera.projectionMatrix); } var minX = points[0].array[0]; var minY = points[0].array[1]; var maxX = points[0].array[0]; var maxY = points[0].array[1]; for (var i = 1; i < nPoints; i++) { maxX = Math.max(maxX, points[i].array[0]); maxY = Math.max(maxY, points[i].array[1]); minX = Math.min(minX, points[i].array[0]); minY = Math.min(minY, points[i].array[1]); } if (minX == maxX || minY == maxY) { return; } coords[0].array[0] = minX; coords[0].array[1] = minY; coords[1].array[0] = minX; coords[1].array[1] = maxY; coords[2].array[0] = maxX; coords[2].array[1] = maxY; coords[3].array[0] = maxX; coords[3].array[1] = minY; for (var i = 0; i < 4; i++) { this.camera.castRay(coords[i], ray); ray.intersectPlane(this.plane, coords[i]); } return coords; }; })() }); var MOUSE_BUTTON_KEY_MAP = { left: 0, middle: 1, right: 2 }; function convertToArray(val) { if (!Array.isArray(val)) { val = [val, val]; } return val; } /** * @constructor * @alias clay.plugin.OrbitControl * @extends clay.core.Base */ var OrbitControl = Base.extend(function () { return /** @lends clay.plugin.OrbitControl# */ { /** * @type {clay.Timeline} */ timeline: null, /** * @type {HTMLElement} */ domElement: null, /** * @type {clay.Node} */ target: null, /** * @type {clay.Vector3} */ _center: new Vector3(), /** * Minimum distance to the center * @type {number} * @default 0.5 */ minDistance: 0.1, /** * Maximum distance to the center * @type {number} * @default 2 */ maxDistance: 1000, /** * Only available when camera is orthographic */ maxOrthographicSize: 300, /** * Only available when camera is orthographic */ minOrthographicSize: 30, /** * Aspect of orthographic camera * Only available when camera is orthographic */ orthographicAspect: 1, /** * Minimum alpha rotation */ minAlpha: -90, /** * Maximum alpha rotation */ maxAlpha: 90, /** * Minimum beta rotation */ minBeta: -Infinity, /** * Maximum beta rotation */ maxBeta: Infinity, /** * Start auto rotating after still for the given time */ autoRotateAfterStill: 0, /** * Direction of autoRotate. cw or ccw when looking top down. */ autoRotateDirection: 'cw', /** * Degree per second */ autoRotateSpeed: 60, panMouseButton: 'middle', rotateMouseButton: 'left', /** * Pan or rotate * @type {String} */ _mode: 'rotate', /** * @param {number} */ damping: 0.8, /** * @param {number} */ rotateSensitivity: 1, /** * @param {number} */ zoomSensitivity: 1, /** * @param {number} */ panSensitivity: 1, _needsUpdate: false, _rotating: false, // Rotation around yAxis _phi: 0, // Rotation around xAxis _theta: 0, _mouseX: 0, _mouseY: 0, _rotateVelocity: new Vector2(), _panVelocity: new Vector2(), _distance: 20, _zoomSpeed: 0, _stillTimeout: 0, _animators: [], _gestureMgr: new GestureMgr() }; }, function () { // Each OrbitControl has it's own handler this._mouseDownHandler = this._mouseDownHandler.bind(this); this._mouseWheelHandler = this._mouseWheelHandler.bind(this); this._mouseMoveHandler = this._mouseMoveHandler.bind(this); this._mouseUpHandler = this._mouseUpHandler.bind(this); this._pinchHandler = this._pinchHandler.bind(this); this.init(); }, /** @lends clay.plugin.OrbitControl# */ { /** * Initialize. * Mouse event binding */ init: function () { var dom = this.domElement; vendor.addEventListener(dom, 'touchstart', this._mouseDownHandler); vendor.addEventListener(dom, 'mousedown', this._mouseDownHandler); vendor.addEventListener(dom, 'wheel', this._mouseWheelHandler); if (this.timeline) { this.timeline.on('frame', this.update, this); } if (this.target) { this.decomposeTransform(); } }, /** * Dispose. * Mouse event unbinding */ dispose: function () { var dom = this.domElement; vendor.removeEventListener(dom, 'touchstart', this._mouseDownHandler); vendor.removeEventListener(dom, 'touchmove', this._mouseMoveHandler); vendor.removeEventListener(dom, 'touchend', this._mouseUpHandler); vendor.removeEventListener(dom, 'mousedown', this._mouseDownHandler); vendor.removeEventListener(dom, 'mousemove', this._mouseMoveHandler); vendor.removeEventListener(dom, 'mouseup', this._mouseUpHandler); vendor.removeEventListener(dom, 'wheel', this._mouseWheelHandler); vendor.removeEventListener(dom, 'mouseout', this._mouseUpHandler); if (this.timeline) { this.timeline.off('frame', this.update); } this.stopAllAnimation(); }, /** * Get distance * @return {number} */ getDistance: function () { return this._distance; }, /** * Set distance * @param {number} distance */ setDistance: function (distance) { this._distance = distance; this._needsUpdate = true; }, /** * Get size of orthographic viewing volume * @return {number} */ getOrthographicSize: function () { return this._orthoSize; }, /** * Set size of orthographic viewing volume * @param {number} size */ setOrthographicSize: function (size) { this._orthoSize = size; this._needsUpdate = true; }, /** * Get alpha rotation * Alpha angle for top-down rotation. Positive to rotate to top. * * Which means camera rotation around x axis. */ getAlpha: function () { return this._theta / Math.PI * 180; }, /** * Get beta rotation * Beta angle for left-right rotation. Positive to rotate to right. * * Which means camera rotation around y axis. */ getBeta: function () { return -this._phi / Math.PI * 180; }, /** * Get control center * @return {Array.<number>} */ getCenter: function () { return this._center.toArray(); }, /** * Set alpha rotation angle * @param {number} alpha */ setAlpha: function (alpha) { alpha = Math.max(Math.min(this.maxAlpha, alpha), this.minAlpha); this._theta = alpha / 180 * Math.PI; this._needsUpdate = true; }, /** * Set beta rotation angle * @param {number} beta */ setBeta: function (beta) { beta = Math.max(Math.min(this.maxBeta, beta), this.minBeta); this._phi = -beta / 180 * Math.PI; this._needsUpdate = true; }, /** * Set control center * @param {Array.<number>} center */ setCenter: function (centerArr) { this._center.setArray(centerArr); }, setOption: function (opts) { opts = opts || {}; ['autoRotate', 'autoRotateAfterStill', 'autoRotateDirection', 'autoRotateSpeed', 'damping', 'minDistance', 'maxDistance', 'minOrthographicSize', 'maxOrthographicSize', 'orthographicAspect', 'minAlpha', 'maxAlpha', 'minBeta', 'maxBeta', 'rotateSensitivity', 'zoomSensitivity', 'panSensitivity' ].forEach(function (key) { if (opts[key] != null) { this[key] = opts[key]; } }, this); if (opts.distance != null) { this.setDistance(opts.distance); } if (opts.orthographicSize != null) { this.setOrthographicSize(opts.orthographicSize); } if (opts.alpha != null) { this.setAlpha(opts.alpha); } if (opts.beta != null) { this.setBeta(opts.beta); } if (opts.center) { this.setCenter(opts.center); } if (this.target) { this._updateTransform(); this.target.update(); } }, /** * @param {Object} opts * @param {number} opts.distance * @param {number} opts.orthographicSize * @param {number} opts.alpha * @param {number} opts.beta * @param {Array.<number>} opts.center * @param {number} [opts.duration=1000] * @param {number} [opts.easing='linear'] * @param {number} [opts.done] */ animateTo: function (opts) { var self = this; var obj = {}; var target = {}; var timeline = this.timeline; if (!timeline) { return; } if (opts.distance != null) { obj.distance = this.getDistance(); target.distance = opts.distance; } if (opts.orthographicSize != null) { obj.orthographicSize = this.getOrthographicSize(); target.orthographicSize = opts.orthographicSize; } if (opts.alpha != null) { obj.alpha = this.getAlpha(); target.alpha = opts.alpha; } if (opts.beta != null) { obj.beta = this.getBeta(); target.beta = opts.beta; } if (opts.center != null) { obj.center = this.getCenter(); target.center = opts.center; } return this._addAnimator( timeline.animate(obj) .when(opts.duration || 1000, target) .during(function () { if (obj.alpha != null) { self.setAlpha(obj.alpha); } if (obj.beta != null) { self.setBeta(obj.beta); } if (obj.distance != null) { self.setDistance(obj.distance); } if (obj.orthographicSize != null) { self.setOrthographicSize(obj.orthographicSize); } if (obj.center != null) { self.setCenter(obj.center); } self._needsUpdate = true; }) .done(opts.done) ).start(opts.easing || 'linear'); }, /** * Stop all animations */ stopAllAnimation: function () { for (var i = 0; i < this._animators.length; i++) { this._animators[i].stop(); } this._animators.length = 0; }, _isAnimating: function () { return this._animators.length > 0; }, /** * Call update each frame * @param {number} deltaTime Frame time */ update: function (deltaTime) { deltaTime = deltaTime || 16; if (this._rotating) { var radian = (this.autoRotateDirection === 'cw' ? 1 : -1) * this.autoRotateSpeed / 180 * Math.PI; this._phi -= radian * deltaTime / 1000; this._needsUpdate = true; } else if (this._rotateVelocity.len() > 0) { this._needsUpdate = true; } if (Math.abs(this._zoomSpeed) > 0.01 || this._panVelocity.len() > 0) { this._needsUpdate = true; } if (!this._needsUpdate) { return; } // Fixed deltaTime this._updateDistanceOrSize(Math.min(deltaTime, 50)); this._updatePan(Math.min(deltaTime, 50)); this._updateRotate(Math.min(deltaTime, 50)); this._updateTransform(); this.target.update(); this.trigger('update'); this._needsUpdate = false; }, _updateRotate: function (deltaTime) { var velocity = this._rotateVelocity; this._phi = velocity.y * deltaTime / 20 + this._phi; this._theta = velocity.x * deltaTime / 20 + this._theta; this.setAlpha(this.getAlpha()); this.setBeta(this.getBeta()); this._vectorDamping(velocity, this.damping); }, _updateDistanceOrSize: function (deltaTime) { this._setDistance(this._distance + this._zoomSpeed * deltaTime / 20); if (!(this.target instanceof Perspective$1)) { this._setOrthoSize(this._orthoSize + this._zoomSpeed * deltaTime / 20); } this._zoomSpeed *= Math.pow(this.damping, deltaTime / 16); }, _setDistance: function (distance) { this._distance = Math.max(Math.min(distance, this.maxDistance), this.minDistance); }, _setOrthoSize: function (size) { this._orthoSize = Math.max(Math.min(size, this.maxOrthographicSize), this.minOrthographicSize); var camera = this.target; var cameraHeight = this._orthoSize; // TODO var cameraWidth = cameraHeight * this.orthographicAspect; camera.left = -cameraWidth / 2; camera.right = cameraWidth / 2; camera.top = cameraHeight / 2; camera.bottom = -cameraHeight / 2; }, _updatePan: function (deltaTime) { var velocity = this._panVelocity; var len = this._distance; var target = this.target; var yAxis = target.worldTransform.y; var xAxis = target.worldTransform.x; // PENDING this._center .scaleAndAdd(xAxis, -velocity.x * len / 200) .scaleAndAdd(yAxis, -velocity.y * len / 200); this._vectorDamping(velocity, 0); velocity.x = velocity.y = 0; }, _updateTransform: function () { var camera = this.target; var dir = new Vector3(); var theta = this._theta + Math.PI / 2; var phi = this._phi + Math.PI / 2; var r = Math.sin(theta); dir.x = r * Math.cos(phi); dir.y = -Math.cos(theta); dir.z = r * Math.sin(phi); camera.position.copy(this._center).scaleAndAdd(dir, this._distance); camera.rotation.identity() // First around y, then around x .rotateY(-this._phi) .rotateX(-this._theta); }, _startCountingStill: function () { clearTimeout(this._stillTimeout); var time = this.autoRotateAfterStill; var self = this; if (!isNaN(time) && time > 0) { this._stillTimeout = setTimeout(function () { self._rotating = true; }, time * 1000); } }, _vectorDamping: function (v, damping) { var speed = v.len(); speed = speed * damping; if (speed < 1e-4) { speed = 0; } v.normalize().scale(speed); }, decomposeTransform: function () { if (!this.target) { return; } this.target.updateWorldTransform(); var forward = this.target.worldTransform.z; var alpha = Math.asin(forward.y); var beta = Math.atan2(forward.x, forward.z); this._theta = alpha; this._phi = -beta; this.setBeta(this.getBeta()); this.setAlpha(this.getAlpha()); this._setDistance(this.target.position.dist(this._center)); if (!(this.target instanceof Perspective$1)){ this._setOrthoSize(this.target.top - this.target.bottom); } }, _mouseDownHandler: function (e) { if (this._isAnimating()) { return; } var x = e.clientX; var y = e.clientY; // Touch if (e.targetTouches) { var touch = e.targetTouches[0]; x = touch.clientX; y = touch.clientY; this._mode = 'rotate'; this._processGesture(e, 'start'); } else { if (e.button === MOUSE_BUTTON_KEY_MAP[this.rotateMouseButton]) { this._mode = 'rotate'; } else if (e.button === MOUSE_BUTTON_KEY_MAP[this.panMouseButton]) { this._mode = 'pan'; /** * Vendors like Mozilla provide a mouse-driven panning feature * that is activated when the middle mouse button is pressed. * * @see https://w3c.github.io/uievents/#event-type-mousedown */ e.preventDefault(); } else { this._mode = null; } } var dom = this.domElement; vendor.addEventListener(dom, 'touchmove', this._mouseMoveHandler); vendor.addEventListener(dom, 'touchend', this._mouseUpHandler); vendor.addEventListener(dom, 'mousemove', this._mouseMoveHandler); vendor.addEventListener(dom, 'mouseup', this._mouseUpHandler); vendor.addEventListener(dom, 'mouseout', this._mouseUpHandler); // Reset rotate velocity this._rotateVelocity.set(0, 0); this._rotating = false; if (this.autoRotate) { this._startCountingStill(); } this._mouseX = x; this._mouseY = y; }, _mouseMoveHandler: function (e) { if (this._isAnimating()) { return; } var x = e.clientX; var y = e.clientY; var haveGesture; // Touch if (e.targetTouches) { var touch = e.targetTouches[0]; x = touch.clientX; y = touch.clientY; haveGesture = this._processGesture(e, 'change'); } var panSensitivity = convertToArray(this.panSensitivity); var rotateSensitivity = convertToArray(this.rotateSensitivity); if (!haveGesture) { if (this._mode === 'rotate') { this._rotateVelocity.y += (x - this._mouseX) / this.domElement.clientWidth * 2 * rotateSensitivity[0]; this._rotateVelocity.x += (y - this._mouseY) / this.domElement.clientHeight * 2 * rotateSensitivity[1]; } else if (this._mode === 'pan') { this._panVelocity.x += (x - this._mouseX) / this.domElement.clientWidth * panSensitivity[0] * 400; this._panVelocity.y += (-y + this._mouseY) / this.domElement.clientHeight * panSensitivity[1] * 400; } } this._mouseX = x; this._mouseY = y; e.preventDefault && e.preventDefault(); }, _mouseWheelHandler: function (e) { if (this._isAnimating()) { return; } var delta = e.deltaY; if (delta === 0) { return; } this._zoomHandler(e, delta > 0 ? 1 : -1); }, _pinchHandler: function (e) { if (this._isAnimating()) { return; } this._zoomHandler(e, e.pinchScale > 1 ? 0.4 : -0.4); }, _zoomHandler: function (e, delta) { var speed; if (this.target instanceof Perspective$1) { speed = Math.max(Math.max(Math.min( this._distance - this.minDistance, this.maxDistance - this._distance )) / 20, 0.5); } else { speed = Math.max(Math.max(Math.min( this._orthoSize - this.minOrthographicSize, this.maxOrthographicSize - this._orthoSize )) / 20, 0.5); } this._zoomSpeed = (delta > 0 ? -1 : 1) * speed * this.zoomSensitivity; this._rotating = false; if (this.autoRotate && this._mode === 'rotate') { this._startCountingStill(); } e.preventDefault && e.preventDefault(); }, _mouseUpHandler: function (event) { var dom = this.domElement; vendor.removeEventListener(dom, 'touchmove', this._mouseMoveHandler); vendor.removeEventListener(dom, 'touchend', this._mouseUpHandler); vendor.removeEventListener(dom, 'mousemove', this._mouseMoveHandler); vendor.removeEventListener(dom, 'mouseup', this._mouseUpHandler); vendor.removeEventListener(dom, 'mouseout', this._mouseUpHandler); this._processGesture(event, 'end'); }, _addAnimator: function (animator) { var animators = this._animators; animators.push(animator); animator.done(function () { var idx = animators.indexOf(animator); if (idx >= 0) { animators.splice(idx, 1); } }); return animator; }, _processGesture: function (event, stage) { var gestureMgr = this._gestureMgr; stage === 'start' && gestureMgr.clear(); var gestureInfo = gestureMgr.recognize( event, null, this.domElement ); stage === 'end' && gestureMgr.clear(); // Do not do any preventDefault here. Upper application do that if necessary. if (gestureInfo) { var type = gestureInfo.type; event.gestureEvent = type; this._pinchHandler(gestureInfo.event); } return gestureInfo; } }); /** * If auto rotate the target * @type {boolean} * @default false */ Object.defineProperty(OrbitControl.prototype, 'autoRotate', { get: function () { return this._autoRotate; }, set: function (val) { this._autoRotate = val; this._rotating = val; } }); Object.defineProperty(OrbitControl.prototype, 'target', { get: function () { return this._target; }, set: function (val) { if (val && val.target) { this.setCenter(val.target.toArray()); } this._target = val; this.decomposeTransform(); } }); /** * StaticGeometry can not be changed once they've been setup */ /** * @constructor clay.StaticGeometry * @extends clay.Geometry */ var StaticGeometry = Geometry.extend({ dynamic: false }); // TODO test /** * @namespace clay.util.mesh */ var meshUtil = { /** * Merge multiple meshes to one. * Note that these meshes must have the same material * * @param {Array.<clay.Mesh>} meshes * @param {boolean} applyWorldTransform * @return {clay.Mesh} * @memberOf clay.util.mesh */ merge: function (meshes, applyWorldTransform) { if (! meshes.length) { return; } var templateMesh = meshes[0]; var templateGeo = templateMesh.geometry; var material = templateMesh.material; var geometry = new Geometry({ dynamic: false }); geometry.boundingBox = new BoundingBox(); var attributeNames = templateGeo.getEnabledAttributes(); for (var i = 0; i < attributeNames.length; i++) { var name = attributeNames[i]; var attr = templateGeo.attributes[name]; // Extend custom attributes if (!geometry.attributes[name]) { geometry.attributes[name] = attr.clone(false); } } var inverseTransposeMatrix = mat4.create(); // Initialize the array data and merge bounding box var nVertex = 0; var nFace = 0; for (var k = 0; k < meshes.length; k++) { var currentGeo = meshes[k].geometry; if (currentGeo.boundingBox) { currentGeo.boundingBox.applyTransform(applyWorldTransform ? meshes[k].worldTransform : meshes[k].localTransform); geometry.boundingBox.union(currentGeo.boundingBox); } nVertex += currentGeo.vertexCount; nFace += currentGeo.triangleCount; } for (var n = 0; n < attributeNames.length; n++) { var name = attributeNames[n]; var attrib = geometry.attributes[name]; attrib.init(nVertex); } if (nVertex >= 0xffff) { geometry.indices = new Uint32Array(nFace * 3); } else { geometry.indices = new Uint16Array(nFace * 3); } var vertexOffset = 0; var indicesOffset = 0; var useIndices = templateGeo.isUseIndices(); for (var mm = 0; mm < meshes.length; mm++) { var mesh = meshes[mm]; var currentGeo = mesh.geometry; var nVertex = currentGeo.vertexCount; var matrix = applyWorldTransform ? mesh.worldTransform.array : mesh.localTransform.array; mat4.invert(inverseTransposeMatrix, matrix); mat4.transpose(inverseTransposeMatrix, inverseTransposeMatrix); for (var nn = 0; nn < attributeNames.length; nn++) { var name = attributeNames[nn]; var currentAttr = currentGeo.attributes[name]; var targetAttr = geometry.attributes[name]; // Skip the unused attributes; if (!currentAttr.value.length) { continue; } var len = currentAttr.value.length; var size = currentAttr.size; var offset = vertexOffset * size; var count = len / size; for (var i = 0; i < len; i++) { targetAttr.value[offset + i] = currentAttr.value[i]; } // Transform position, normal and tangent if (name === 'position') { vec3.forEach(targetAttr.value, size, offset, count, vec3.transformMat4, matrix); } else if (name === 'normal' || name === 'tangent') { vec3.forEach(targetAttr.value, size, offset, count, vec3.transformMat4, inverseTransposeMatrix); } } if (useIndices) { var len = currentGeo.indices.length; for (var i = 0; i < len; i++) { geometry.indices[i + indicesOffset] = currentGeo.indices[i] + vertexOffset; } indicesOffset += len; } vertexOffset += nVertex; } return new Mesh({ material: material, geometry: geometry }); }, /** * Split mesh into sub meshes, each mesh will have maxJointNumber joints. * @param {clay.Mesh} mesh * @param {number} maxJointNumber * @param {boolean} inPlace * @return {clay.Node} * * @memberOf clay.util.mesh */ // FIXME, Have issues on some models splitByJoints: function (mesh, maxJointNumber, inPlace) { var geometry = mesh.geometry; var skeleton = mesh.skeleton; var material = mesh.material; var joints = mesh.joints; if (!geometry || !skeleton || !joints.length) { return; } if (joints.length < maxJointNumber) { return mesh; } var indices = geometry.indices; var faceLen = geometry.triangleCount; var rest = faceLen; var isFaceAdded = []; var jointValues = geometry.attributes.joint.value; for (var i = 0; i < faceLen; i++) { isFaceAdded[i] = false; } var addedJointIdxPerFace = []; var buckets = []; var getJointByIndex = function (idx) { return joints[idx]; }; while (rest > 0) { var bucketTriangles = []; var bucketJointReverseMap = []; var bucketJoints = []; var subJointNumber = 0; for (var i = 0; i < joints.length; i++) { bucketJointReverseMap[i] = -1; } for (var f = 0; f < faceLen; f++) { if (isFaceAdded[f]) { continue; } var canAddToBucket = true; var addedNumber = 0; for (var i = 0; i < 3; i++) { var idx = indices[f * 3 + i]; for (var j = 0; j < 4; j++) { var jointIdx = jointValues[idx * 4 + j]; if (jointIdx >= 0) { if (bucketJointReverseMap[jointIdx] === -1) { if (subJointNumber < maxJointNumber) { bucketJointReverseMap[jointIdx] = subJointNumber; bucketJoints[subJointNumber++] = jointIdx; addedJointIdxPerFace[addedNumber++] = jointIdx; } else { canAddToBucket = false; } } } } } if (!canAddToBucket) { // Reverse operation for (var i = 0; i < addedNumber; i++) { bucketJointReverseMap[addedJointIdxPerFace[i]] = -1; bucketJoints.pop(); subJointNumber--; } } else { bucketTriangles.push(indices.subarray(f * 3, (f + 1) * 3)); isFaceAdded[f] = true; rest--; } } buckets.push({ triangles: bucketTriangles, joints: bucketJoints.map(getJointByIndex), jointReverseMap: bucketJointReverseMap }); } var root = new Node({ name: mesh.name }); var attribNames = geometry.getEnabledAttributes(); attribNames.splice(attribNames.indexOf('joint'), 1); // Map from old vertex index to new vertex index var newIndices = []; for (var b = 0; b < buckets.length; b++) { var bucket = buckets[b]; var jointReverseMap = bucket.jointReverseMap; var subJointNumber = bucket.joints.length; var subGeo = new Geometry(); var subMesh = new Mesh({ name: [mesh.name, i].join('-'), // DON'T clone material. material: material, geometry: subGeo, skeleton: skeleton, joints: bucket.joints.slice() }); var nVertex = 0; var nVertex2 = geometry.vertexCount; for (var i = 0; i < nVertex2; i++) { newIndices[i] = -1; } // Count sub geo number for (var f = 0; f < bucket.triangles.length; f++) { var face = bucket.triangles[f]; for (var i = 0; i < 3; i++) { var idx = face[i]; if (newIndices[idx] === -1) { newIndices[idx] = nVertex; nVertex++; } } } for (var a = 0; a < attribNames.length; a++) { var attribName = attribNames[a]; var subAttrib = subGeo.attributes[attribName]; subAttrib.init(nVertex); } subGeo.attributes.joint.value = new Float32Array(nVertex * 4); if (nVertex > 0xffff) { subGeo.indices = new Uint32Array(bucket.triangles.length * 3); } else { subGeo.indices = new Uint16Array(bucket.triangles.length * 3); } var indicesOffset = 0; nVertex = 0; for (var i = 0; i < nVertex2; i++) { newIndices[i] = -1; } for (var f = 0; f < bucket.triangles.length; f++) { var triangle = bucket.triangles[f]; for (var i = 0; i < 3; i++) { var idx = triangle[i]; if (newIndices[idx] === -1) { newIndices[idx] = nVertex; for (var a = 0; a < attribNames.length; a++) { var attribName = attribNames[a]; var attrib = geometry.attributes[attribName]; var subAttrib = subGeo.attributes[attribName]; var size = attrib.size; for (var j = 0; j < size; j++) { subAttrib.value[nVertex * size + j] = attrib.value[idx * size + j]; } } for (var j = 0; j < 4; j++) { var jointIdx = geometry.attributes.joint.value[idx * 4 + j]; var offset = nVertex * 4 + j; if (jointIdx >= 0) { subGeo.attributes.joint.value[offset] = jointReverseMap[jointIdx]; } else { subGeo.attributes.joint.value[offset] = -1; } } nVertex++; } subGeo.indices[indicesOffset++] = newIndices[idx]; } } subGeo.updateBoundingBox(); root.add(subMesh); } var children = mesh.children(); for (var i = 0; i < children.length; i++) { root.add(children[i]); } root.position.copy(mesh.position); root.rotation.copy(mesh.rotation); root.scale.copy(mesh.scale); if (inPlace) { if (mesh.getParent()) { var parent = mesh.getParent(); parent.remove(mesh); parent.add(root); } } return root; } }; var META = { version: 1.0, type: 'Geometry', generator: 'util.transferable.toObject' }; /** * @alias clay.util.transferable */ var transferableUtil = { /** * Convert geometry to a object containing transferable data * @param {Geometry} geometry geometry * @param {Boolean} shallow whether shallow copy * @returns {Object} { data : data, buffers : buffers }, buffers is the transferable list */ toObject : function (geometry, shallow) { if (!geometry) { return null; } var data = { metadata : util$1.extend({}, META) }; //transferable buffers var buffers = []; //dynamic data.dynamic = geometry.dynamic; //bounding box if (geometry.boundingBox) { data.boundingBox = { min : geometry.boundingBox.min.toArray(), max : geometry.boundingBox.max.toArray() }; } //indices if (geometry.indices && geometry.indices.length > 0) { data.indices = copyIfNecessary(geometry.indices, shallow); buffers.push(data.indices.buffer); } //attributes data.attributes = {}; for (var p in geometry.attributes) { if (geometry.attributes.hasOwnProperty(p)) { var attr = geometry.attributes[p]; //ignore empty attributes if (attr && attr.value && attr.value.length > 0) { attr = data.attributes[p] = copyAttribute(attr, shallow); buffers.push(attr.value.buffer); } } } return { data : data, buffers : buffers }; }, /** * Reproduce a geometry from object generated by toObject * @param {Object} object object generated by toObject * @returns {Geometry} geometry */ toGeometry : function (object) { if (!object) { return null; } if (object.data && object.buffers) { return transferableUtil.toGeometry(object.data); } if (!object.metadata || object.metadata.generator !== META.generator) { throw new Error('[util.transferable.toGeometry] the object is not generated by util.transferable.'); } //basic options var options = { dynamic : object.dynamic, indices : object.indices }; if (object.boundingBox) { var min = new Vector3().setArray(object.boundingBox.min); var max = new Vector3().setArray(object.boundingBox.max); options.boundingBox = new BoundingBox(min, max); } var geometry = new Geometry(options); //attributes for (var p in object.attributes) { if (object.attributes.hasOwnProperty(p)) { var attr = object.attributes[p]; geometry.attributes[p] = new Geometry.Attribute(attr.name, attr.type, attr.size, attr.semantic); geometry.attributes[p].value = attr.value; } } return geometry; } }; function copyAttribute(attr, shallow) { return { name : attr.name, type : attr.type, size : attr.size, semantic : attr.semantic, value : copyIfNecessary(attr.value, shallow) }; } function copyIfNecessary(arr, shallow) { if (!shallow) { return new arr.constructor(arr); } else { return arr; } } /** * @name clay.version */ var version = '1.3.0'; var outputEssl$1 = "@export clay.vr.disorter.output.vertex\nattribute vec2 texcoord: TEXCOORD_0;\nattribute vec3 position: POSITION;\nvarying vec2 v_Texcoord;\nvoid main()\n{\n v_Texcoord = texcoord;\n gl_Position = vec4(position.xy, 0.5, 1.0);\n}\n@end\n@export clay.vr.disorter.output.fragment\nuniform sampler2D texture;\nvarying vec2 v_Texcoord;\nvoid main()\n{\n gl_FragColor = texture2D(texture, v_Texcoord);\n}\n@end"; // https://github.com/googlevr/webvr-polyfill/blob/master/src/cardboard-distorter.js // Use webvr may have scale problem. // https://github.com/googlevr/webvr-polyfill/issues/140 // https://github.com/googlevr/webvr-polyfill/search?q=SCALE&type=Issues&utf8=%E2%9C%93 // https://github.com/googlevr/webvr-polyfill/issues/147 Shader.import(outputEssl$1); function lerp (a, b, t) { return a * (1 - t) + b * t; } var CardboardDistorter = Base.extend(function () { return { clearColor: [0, 0, 0, 1], _mesh: new Mesh({ geometry: new Geometry({ dynamic: true }), culling: false, material: new Material({ // FIXME Why disable depthMask will be wrong // depthMask: false, depthTest: false, shader: new Shader({ vertex: Shader.source('clay.vr.disorter.output.vertex'), fragment: Shader.source('clay.vr.disorter.output.fragment') }) }) }), _fakeCamera: new Perspective$1() }; }, { render: function (renderer, sourceTexture) { var clearColor = this.clearColor; var gl = renderer.gl; gl.clearColor(clearColor[0], clearColor[1], clearColor[2], clearColor[3]); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.BLEND); this._mesh.material.set('texture', sourceTexture); // Full size? renderer.saveViewport(); renderer.setViewport(0, 0, renderer.getWidth(), renderer.getHeight()); renderer.renderPass([this._mesh], this._fakeCamera); renderer.restoreViewport(); // this._mesh.material.shader.bind(renderer); // this._mesh.material.bind(renderer); // this._mesh.render(renderer.gl); }, updateFromVRDisplay: function (vrDisplay) { // FIXME if (vrDisplay.deviceInfo_) { // Hardcoded mesh size this._updateMesh(20, 20, vrDisplay.deviceInfo_); } else { console.warn('Cant get vrDisplay.deviceInfo_, seems code changed'); } }, _updateMesh: function (width, height, deviceInfo) { var positionAttr = this._mesh.geometry.attributes.position; var texcoordAttr = this._mesh.geometry.attributes.texcoord0; positionAttr.init(2 * width * height); texcoordAttr.init(2 * width * height); var lensFrustum = deviceInfo.getLeftEyeVisibleTanAngles(); var noLensFrustum = deviceInfo.getLeftEyeNoLensTanAngles(); var viewport = deviceInfo.getLeftEyeVisibleScreenRect(noLensFrustum); var vidx = 0; var pos = []; var uv = []; // Vertices for (var e = 0; e < 2; e++) { for (var j = 0; j < height; j++) { for (var i = 0; i < width; i++, vidx++) { var u = i / (width - 1); var v = j / (height - 1); // Grid points regularly spaced in StreoScreen, and barrel distorted in // the mesh. var s = u; var t = v; var x = lerp(lensFrustum[0], lensFrustum[2], u); var y = lerp(lensFrustum[3], lensFrustum[1], v); var d = Math.sqrt(x * x + y * y); var r = deviceInfo.distortion.distortInverse(d); var p = x * r / d; var q = y * r / d; u = (p - noLensFrustum[0]) / (noLensFrustum[2] - noLensFrustum[0]); v = (q - noLensFrustum[3]) / (noLensFrustum[1] - noLensFrustum[3]); // Convert u,v to mesh screen coordinates. u = (viewport.x + u * viewport.width - 0.5) * 2.0; //* aspect; v = (viewport.y + v * viewport.height - 0.5) * 2.0; pos[0] = u; pos[1] = v; pos[2] = 0; uv[0] = s * 0.5 + e * 0.5; uv[1] = t; positionAttr.set(vidx, pos); texcoordAttr.set(vidx, uv); } } var w = lensFrustum[2] - lensFrustum[0]; lensFrustum[0] = -(w + lensFrustum[0]); lensFrustum[2] = w - lensFrustum[2]; w = noLensFrustum[2] - noLensFrustum[0]; noLensFrustum[0] = -(w + noLensFrustum[0]); noLensFrustum[2] = w - noLensFrustum[2]; viewport.x = 1 - (viewport.x + viewport.width); } // Indices var indices = new Uint16Array(2 * (width - 1) * (height - 1) * 6); var halfwidth = width / 2; var halfheight = height / 2; var vidx = 0; var iidx = 0; for (var e = 0; e < 2; e++) { for (var j = 0; j < height; j++) { for (var i = 0; i < width; i++, vidx++) { if (i === 0 || j === 0) { continue; } // Build a quad. Lower right and upper left quadrants have quads with // the triangle diagonal flipped to get the vignette to interpolate // correctly. if ((i <= halfwidth) == (j <= halfheight)) { // Quad diagonal lower left to upper right. indices[iidx++] = vidx; indices[iidx++] = vidx - width - 1; indices[iidx++] = vidx - width; indices[iidx++] = vidx - width - 1; indices[iidx++] = vidx; indices[iidx++] = vidx - 1; } else { // Quad diagonal upper left to lower right. indices[iidx++] = vidx - 1; indices[iidx++] = vidx - width; indices[iidx++] = vidx; indices[iidx++] = vidx - width; indices[iidx++] = vidx - 1; indices[iidx++] = vidx - width - 1; } } } } this._mesh.geometry.indices = indices; this._mesh.geometry.dirty(); } }); var tmpProjectionMatrix = new Matrix4(); var StereoCamera = Node.extend(function () { return { aspect: 0.5, _leftCamera: new Perspective$1(), _rightCamera: new Perspective$1(), _eyeLeft: new Matrix4(), _eyeRight: new Matrix4(), _frameData: null }; }, { updateFromCamera: function (camera, focus, zoom, eyeSep) { if (camera.transformNeedsUpdate()) { console.warn('Node transform is not updated'); } focus = focus == null ? 10 : focus; zoom = zoom == null ? 1 : zoom; eyeSep = eyeSep == null ? 0.064 : eyeSep; var fov = camera.fov; var aspect = camera.aspect * this.aspect; var near = camera.near; // Off-axis stereoscopic effect based on // http://paulbourke.net/stereographics/stereorender/ tmpProjectionMatrix.copy(camera.projectionMatrix); var eyeSep = eyeSep / 2; var eyeSepOnProjection = eyeSep * near / focus; var ymax = (near * Math.tan(Math.PI / 180 * fov * 0.5 ) ) / zoom; var xmin, xmax; // translate xOffset this._eyeLeft.array[12] = - eyeSep; this._eyeRight.array[12] = eyeSep; // for left eye xmin = - ymax * aspect + eyeSepOnProjection; xmax = ymax * aspect + eyeSepOnProjection; tmpProjectionMatrix.array[0] = 2 * near / (xmax - xmin); tmpProjectionMatrix.array[8] = (xmax + xmin ) / (xmax - xmin); this._leftCamera.projectionMatrix.copy(tmpProjectionMatrix); // for right eye xmin = - ymax * aspect - eyeSepOnProjection; xmax = ymax * aspect - eyeSepOnProjection; tmpProjectionMatrix.array[0] = 2 * near / (xmax - xmin); tmpProjectionMatrix.array[8] = (xmax + xmin ) / (xmax - xmin); this._rightCamera.projectionMatrix.copy(tmpProjectionMatrix); this._leftCamera.worldTransform .copy(camera.worldTransform) .multiply(this._eyeLeft); this._rightCamera.worldTransform .copy(camera.worldTransform) .multiply(this._eyeRight); this._leftCamera.decomposeWorldTransform(); this._leftCamera.decomposeProjectionMatrix(); this._rightCamera.decomposeWorldTransform(); this._rightCamera.decomposeProjectionMatrix(); }, updateFromVRDisplay: function (vrDisplay, parentNode) { if (typeof VRFrameData === 'undefined') { return; } var frameData = this._frameData || (this._frameData = new VRFrameData()); vrDisplay.getFrameData(frameData); var leftCamera = this._leftCamera; var rightCamera = this._rightCamera; leftCamera.projectionMatrix.setArray(frameData.leftProjectionMatrix); leftCamera.decomposeProjectionMatrix(); leftCamera.viewMatrix.setArray(frameData.leftViewMatrix); leftCamera.setViewMatrix(leftCamera.viewMatrix); rightCamera.projectionMatrix.setArray(frameData.rightProjectionMatrix); rightCamera.decomposeProjectionMatrix(); rightCamera.viewMatrix.setArray(frameData.rightViewMatrix); rightCamera.setViewMatrix(rightCamera.viewMatrix); if (parentNode && parentNode.worldTransform) { if (parentNode.transformNeedsUpdate()) { console.warn('Node transform is not updated'); } leftCamera.worldTransform.multiplyLeft(parentNode.worldTransform); leftCamera.decomposeWorldTransform(); rightCamera.worldTransform.multiplyLeft(parentNode.worldTransform); rightCamera.decomposeWorldTransform(); } }, getLeftCamera: function () { return this._leftCamera; }, getRightCamera: function () { return this._rightCamera; } }); /** @namespace clay */ /** @namespace clay.math */ /** @namespace clay.animation */ /** @namespace clay.async */ /** @namespace clay.camera */ /** @namespace clay.compositor */ /** @namespace clay.core */ /** @namespace clay.geometry */ /** @namespace clay.helper */ /** @namespace clay.light */ /** @namespace clay.loader */ /** @namespace clay.particle */ /** @namespace clay.plugin */ /** @namespace clay.prePass */ /** @namespace clay.shader */ /** @namespace clay.texture */ /** @namespace clay.util */ var animation = { Animator : Animator, Blend1DClip : Blend1DClip, Blend2DClip : Blend2DClip, Clip : Clip, easing : easing, SamplerTrack : SamplerTrack, Timeline : Timeline, TrackClip : TrackClip }; var async = { Task : Task, TaskGroup : TaskGroup }; var camera = { Orthographic : Orthographic$1, Perspective : Perspective$1 }; var compositor = { Compositor : Compositor, CompositorNode : CompositorNode, createCompositor : createCompositor, FilterNode : FilterNode$1, Graph : Graph, Pass : Pass, SceneNode : SceneNode$1, TextureNode : TextureNode$1, TexturePool : TexturePool }; var core = { Base : Base, Cache : Cache, color : colorUtil, glenum : glenum, GLInfo : GLInfo, LinkedList : LinkedList, LRU : LRU$1, mixin : { extend : extendMixin, notifier : notifier }, request : request, util : util$1, vendor : vendor }; var deferred = { GBuffer : GBuffer, Renderer : DeferredRenderer }; var dep = { glmatrix : glmatrix }; var geometry = { Cone : Cone$1, Cube : Cube$1, Cylinder : Cylinder$1, ParametricSurface : ParametricSurface$1, Plane : Plane$3, Sphere : Sphere$1 }; var light = { Ambient : AmbientLight, AmbientCubemap : AmbientCubemapLight, AmbientSH : AmbientSHLight, Directional : DirectionalLight, Point : PointLight, Sphere : SphereLight, Spot : SpotLight, Tube : TubeLight }; var loader = { FX : FXLoader, GLTF : GLTFLoader }; var math = { BoundingBox : BoundingBox, Frustum : Frustum, Matrix2 : Matrix2, Matrix2d : Matrix2d, Matrix3 : Matrix3, Matrix4 : Matrix4, Plane : Plane$1, Quaternion : Quaternion, Ray : Ray, util : mathUtil, Value : Value, Vector2 : Vector2, Vector3 : Vector3, Vector4 : Vector4 }; var particle = { Emitter : Emitter, Field : Field, ForceField : ForceField, Particle : Particle, ParticleRenderable : ParticleRenderable }; var picking = { PixelPicking : PixelPicking, RayPicking : RayPicking }; var plugin = { FreeControl : FreeControl, GamepadControl : GamepadControl, GestureMgr : GestureMgr, InfinitePlane : InfinitePlane, OrbitControl : OrbitControl, Skybox : Skybox$1, Skydome : Skybox$1 }; var prePass = { EnvironmentMap : EnvironmentMapPass, ShadowMap : ShadowMapPass }; var shader = { library : library, registerBuiltinCompositor : register, source : { header : { light : lightEssl } } }; var util = { cubemap : cubemapUtil, dds : ret, delaunay : delaunay, hdr : ret$1, mesh : meshUtil, sh : sh, texture : textureUtil, transferable : transferableUtil }; var vr = { CardboardDistorter : CardboardDistorter, StereoCamera : StereoCamera }; exports.animation = animation; exports.application = application; exports.async = async; exports.Camera = Camera; exports.camera = camera; exports.compositor = compositor; exports.core = core; exports.createCompositor = createCompositor; exports.deferred = deferred; exports.dep = dep; exports.FrameBuffer = FrameBuffer; exports.Geometry = Geometry; exports.geometry = geometry; exports.GeometryBase = GeometryBase; exports.InstancedMesh = InstancedMesh; exports.Joint = Joint; exports.Light = Light; exports.light = light; exports.loader = loader; exports.Material = Material; exports.math = math; exports.BoundingBox = BoundingBox; exports.Frustum = Frustum; exports.Matrix2 = Matrix2; exports.Matrix2d = Matrix2d; exports.Matrix3 = Matrix3; exports.Matrix4 = Matrix4; exports.Plane = Plane$1; exports.Quaternion = Quaternion; exports.Ray = Ray; exports.Value = Value; exports.Vector2 = Vector2; exports.Vector3 = Vector3; exports.Vector4 = Vector4; exports.Mesh = Mesh; exports.Node = Node; exports.particle = particle; exports.picking = picking; exports.plugin = plugin; exports.prePass = prePass; exports.Renderable = Renderable; exports.Renderer = Renderer; exports.Scene = Scene; exports.Shader = Shader; exports.shader = shader; exports.Skeleton = Skeleton; exports.StandardMaterial = StandardMaterial; exports.StaticGeometry = StaticGeometry; exports.Texture = Texture; exports.Texture2D = Texture2D; exports.TextureCube = TextureCube; exports.Timeline = Timeline; exports.util = util; exports.version = version; exports.vr = vr; Object.defineProperty(exports, '__esModule', { value: true }); })));