diff options
Diffstat (limited to 'o3d/samples/o3djs/serialization.js')
-rw-r--r-- | o3d/samples/o3djs/serialization.js | 744 |
1 files changed, 744 insertions, 0 deletions
diff --git a/o3d/samples/o3djs/serialization.js b/o3d/samples/o3djs/serialization.js new file mode 100644 index 0000000..a8b4ea5 --- /dev/null +++ b/o3d/samples/o3djs/serialization.js @@ -0,0 +1,744 @@ +/* + * Copyright 2009, Google Inc. + * 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. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * 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 + * OWNER 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. + */ + + +/** + * @fileoverview This file provides support for deserializing (loading) + * transform graphs from JSON files. + * + */ + +o3djs.provide('o3djs.serialization'); + +o3djs.require('o3djs.error'); + +/** + * A Module for handling events related to o3d and various browsers. + * @namespace + */ +o3djs.serialization = o3djs.serialization || {}; + +/** + * The oldest supported version of the serializer. It isn't necessary to + * increment this version whenever the format changes. Only change it when the + * deserializer becomes incapable of deserializing an older version. + * @type {number} + */ +o3djs.serialization.supportedVersion = 5; + +/** + * Options for deserialization. + * + * opt_animSource is an optional ParamFloat that will be bound as the source + * param for all animation time params in the scene. opt_async is a bool that + * will make the deserialization process async. + * + * @type {{opt_animSource: !o3d.ParamFloat, opt_async: boolean}} + */ +o3djs.serialization.Options = goog.typedef; + +/** + * A Deserializer incrementally deserializes a transform graph. + * @constructor + * @param {!o3d.Pack} pack The pack to deserialize into. + * @param {!Object} json An object tree conforming to the JSON rules. + */ +o3djs.serialization.Deserializer = function(pack, json) { + /** + * The pack to deserialize into. + * @type {!o3d.Pack} + */ + this.pack = pack; + + /** + * An object tree conforming to the JSON rules. + * @type {!Object} + */ + this.json = json; + + /** + * The archive from which assets referenced from JSON are retreived. + * @type {o3djs.io.ArchiveInfo} + */ + this.archiveInfo = null; + + /** + * A map from classname to a function that will create + * instances of objects. Add entries to support additional classes. + * @type {!Object} + */ + this.createCallbacks = { + 'o3d.VertexBuffer': function(deserializer, json) { + var object = deserializer.pack.createObject('o3d.VertexBuffer'); + if ('custom' in json) { + var rawData = deserializer.archiveInfo.getFileByURI( + 'vertex-buffers.bin'); + object.set(rawData, + json.custom.binaryRange[0], + json.custom.binaryRange[1] - json.custom.binaryRange[0]); + for (var i = 0; i < json.custom.fields.length; ++i) { + deserializer.addObject(json.custom.fields[i], object.fields[i]); + } + } + return object; + }, + + 'o3d.SourceBuffer': function(deserializer, json) { + var object = deserializer.pack.createObject('o3d.SourceBuffer'); + if ('custom' in json) { + var rawData = deserializer.archiveInfo.getFileByURI( + 'vertex-buffers.bin'); + object.set(rawData, + json.custom.binaryRange[0], + json.custom.binaryRange[1] - json.custom.binaryRange[0]); + for (var i = 0; i < json.custom.fields.length; ++i) { + deserializer.addObject(json.custom.fields[i], object.fields[i]); + } + } + return object; + }, + + 'o3d.IndexBuffer': function(deserializer, json) { + var object = deserializer.pack.createObject('o3d.IndexBuffer'); + if ('custom' in json) { + var rawData = deserializer.archiveInfo.getFileByURI( + 'index-buffers.bin'); + object.set(rawData, + json.custom.binaryRange[0], + json.custom.binaryRange[1] - json.custom.binaryRange[0]); + for (var i = 0; i < json.custom.fields.length; ++i) { + deserializer.addObject(json.custom.fields[i], object.fields[i]); + } + } + return object; + }, + + 'o3d.Texture2D': function(deserializer, json) { + if ('o3d.uri' in json.params) { + var uri = json.params['o3d.uri'].value; + var rawData = deserializer.archiveInfo.getFileByURI(uri); + if (!rawData) { + throw 'Could not find texture ' + uri + ' in the archive'; + } + return deserializer.pack.createTextureFromRawData( + rawData, + true); + } else { + return deserializer.pack.createTexture2D( + json.custom.width, + json.custom.height, + json.custom.format, + json.custom.levels, + json.custom.renderSurfacesEnabled); + } + }, + + 'o3d.TextureCUBE': function(deserializer, json) { + if ('o3d.uri' in json.params) { + var uri = json.params['o3d.uri'].value; + var rawData = deserializer.archiveInfo.getFileByURI(uri); + if (!rawData) { + throw 'Could not find texture ' + uri + ' in the archive'; + } + return deserializer.pack.createTextureFromRawData( + rawData, + true); + } else { + return deserializer.pack.createTextureCUBE( + json.custom.edgeLength, + json.custom.format, + json.custom.levels, + json.custom.renderSurfacesEnabled); + } + } + }; + + /** + * A map from classname to a function that will initialize + * instances of the given class from JSON data. Add entries to support + * additional classes. + * @type {!Object} + */ + this.initCallbacks = { + 'o3d.Curve': function(deserializer, object, json) { + if ('custom' in json) { + var rawData = deserializer.archiveInfo.getFileByURI('curve-keys.bin'); + object.set(rawData, + json.custom.binaryRange[0], + json.custom.binaryRange[1] - json.custom.binaryRange[0]); + } + }, + + 'o3d.Effect': function(deserializer, object, json) { + var uriParam = object.getParam('o3d.uri'); + if (uriParam) { + var rawData = deserializer.archiveInfo.getFileByURI(uriParam.value); + if (!rawData) { + throw 'Cannot find shader ' + uriParam.value + ' in archive.'; + } + if (!object.loadFromFXString(rawData.stringValue)) { + throw 'Cannot load shader ' + uriParam.value + ' in archive.'; + } + } + }, + + 'o3d.Skin': function(deserializer, object, json) { + if ('custom' in json) { + var rawData = deserializer.archiveInfo.getFileByURI('skins.bin'); + object.set(rawData, + json.custom.binaryRange[0], + json.custom.binaryRange[1] - json.custom.binaryRange[0]); + } + }, + + 'o3d.SkinEval': function(deserializer, object, json) { + if ('custom' in json) { + for (var i = 0; i < json.custom.vertexStreams.length; ++i) { + var streamJson = json.custom.vertexStreams[i]; + var field = deserializer.getObjectById(streamJson.stream.field); + object.setVertexStream(streamJson.stream.semantic, + streamJson.stream.semanticIndex, + field, + streamJson.stream.startIndex); + if ('bind' in streamJson) { + var source = deserializer.getObjectById(streamJson.bind); + object.bindStream(source, + streamJson.stream.semantic, + streamJson.stream.semanticIndex); + } + } + } + }, + + 'o3d.StreamBank': function(deserializer, object, json) { + if ('custom' in json) { + for (var i = 0; i < json.custom.vertexStreams.length; ++i) { + var streamJson = json.custom.vertexStreams[i]; + var field = deserializer.getObjectById(streamJson.stream.field); + object.setVertexStream(streamJson.stream.semantic, + streamJson.stream.semanticIndex, + field, + streamJson.stream.startIndex); + if ('bind' in streamJson) { + var source = deserializer.getObjectById(streamJson.bind); + object.bindStream(source, + streamJson.stream.semantic, + streamJson.stream.semanticIndex); + } + } + } + } + }; + + if (!('version' in json)) { + throw 'Version in JSON file was missing.'; + } + + if (json.version < o3djs.serialization.supportedVersion) { + throw 'Version in JSON file was ' + json.version + + ' but expected at least version ' + + o3djs.serialization.supportedVersion + '.'; + } + + if (!('objects' in json)) { + throw 'Objects array in JSON file was missing.'; + } + + /** + * An array of all objects deserialized so far, indexed by object id. Id zero + * means null. + * @type {!Array.<(Object|undefined)>} + * @private + */ + this.objectsById_ = [null]; + + /** + * An array of objects deserialized so far, indexed by position in the JSON. + * @type {!Array.<Object>} + * @private + */ + this.objectsByIndex_ = []; + + /** + * Array of all classes present in the JSON. + * @type {!Array.<string>} + * @private + */ + this.classNames_ = []; + for (var className in json.objects) { + this.classNames_.push(className); + } + + /** + * The current phase_ of deserialization. In phase_ 0, objects + * are created and their ids registered. In phase_ 1, objects are + * initialized from JSON data. + * @type {number} + * @private + */ + this.phase_ = 0; + + /** + * Index of the next class to be deserialized in classNames_. + * @type {number} + * @private + */ + this.nextClassIndex_ = 0; + + /** + * Index of the next object of the current class to be deserialized. + * @type {number} + * @private + */ + this.nextObjectIndex_ = 0; + + /** + * Index of the next object to be deserialized in objectsByIndex_. + * @type {number} + * @private + */ + this.globalObjectIndex_ = 0; +}; + +/** + * Get the object with the given id. + * @param {number} id The id to lookup. + * @return {(Object|undefined)} The object with the given id. + */ +o3djs.serialization.Deserializer.prototype.getObjectById = function(id) { + return this.objectsById_[id]; +}; + +/** + * When a creation or init callback creates an object that the Deserializer + * is not aware of, it can associate it with an id using this function, so that + * references to the object can be resolved. + * @param {number} id The is of the object. + * @param {!Object} object The object to register. + */ +o3djs.serialization.Deserializer.prototype.addObject = function( + id, object) { + this.objectsById_[id] = object; +}; + +/** + * Deserialize a value. Identifies reference values and converts + * their object id into an object reference. Otherwise returns the + * value unchanged. + * @param {*} valueJson The JSON representation of the value. + * @return {*} The JavaScript representation of the value. + */ +o3djs.serialization.Deserializer.prototype.deserializeValue = function( + valueJson) { + if (typeof(valueJson) === 'object') { + if (valueJson === null) { + return null; + } + + var valueAsObject = /** @type {!Object} */ (valueJson); + if ('length' in valueAsObject) { + for (var i = 0; i != valueAsObject.length; ++i) { + valueAsObject[i] = this.deserializeValue(valueAsObject[i]); + } + return valueAsObject; + } + + var refId = valueAsObject['ref']; + if (refId !== undefined) { + var referenced = this.objectsById_[refId]; + if (referenced === undefined) { + throw 'Could not find object with id ' + refId + '.'; + } + return referenced; + } + } + + return valueJson; +}; + +/** + * Sets the value of a param on an object or binds a param to another. + * @param {!Object} object The object holding the param. + * @param {(string|number)} paramName The name of the param. + * @param {!Object} propertyJson The JSON representation of the value. + * @private + */ +o3djs.serialization.Deserializer.prototype.setParamValue_ = function( + object, paramName, propertyJson) { + var param = object.getParam(paramName); + if (param === null) + return; + + var valueJson = propertyJson['value']; + if (valueJson !== undefined) { + param.value = this.deserializeValue(valueJson); + } + + var bindId = propertyJson['bind']; + if (bindId !== undefined) { + var referenced = this.objectsById_[bindId]; + if (referenced === undefined) { + throw 'Could not find output param with id ' + bindId + '.'; + } + param.bind(referenced); + } +}; + +/** + * Creates a param on an object and adds it's id so that other objects can + * reference it. + * @param {!Object} object The object to hold the param. + * @param {(string|number)} paramName The name of the param. + * @param {!Object} propertyJson The JSON representation of the value. + * @private + */ +o3djs.serialization.Deserializer.prototype.createAndIdentifyParam_ = + function(object, paramName, propertyJson) { + var propertyClass = propertyJson['class']; + var param; + if (propertyClass !== undefined) { + param = object.createParam(paramName, propertyClass); + } else { + param = object.getParam(paramName); + } + + var paramId = propertyJson['id']; + if (paramId !== undefined && param !== null) { + this.objectsById_[paramId] = param; + } +}; + +/** + * First pass: create all objects and additional params. We need two + * passes to support references to objects that appear later in the + * JSON. + * @param {number} amountOfWork The number of loop iterations to perform of + * this phase_. + * @private + */ +o3djs.serialization.Deserializer.prototype.createObjectsPhase_ = + function(amountOfWork) { + for (; this.nextClassIndex_ < this.classNames_.length; + ++this.nextClassIndex_) { + var className = this.classNames_[this.nextClassIndex_]; + var classJson = this.json.objects[className]; + var numObjects = classJson.length; + for (; this.nextObjectIndex_ < numObjects; ++this.nextObjectIndex_) { + if (amountOfWork-- <= 0) + return; + + var objectJson = classJson[this.nextObjectIndex_]; + var object = undefined; + if ('id' in objectJson) { + object = this.objectsById_[objectJson.id]; + } + if (object === undefined) { + if (className in this.createCallbacks) { + object = this.createCallbacks[className](this, objectJson); + } else { + object = this.pack.createObject(className); + } + } + this.objectsByIndex_[this.globalObjectIndex_++] = object; + if ('id' in objectJson) { + this.objectsById_[objectJson.id] = object; + } + if ('params' in objectJson) { + if ('length' in objectJson.params) { + for (var paramIndex = 0; paramIndex != objectJson.params.length; + ++paramIndex) { + var paramJson = objectJson.params[paramIndex]; + this.createAndIdentifyParam_(object, paramIndex, + paramJson); + } + } else { + for (var paramName in objectJson.params) { + var paramJson = objectJson.params[paramName]; + this.createAndIdentifyParam_(object, paramName, paramJson); + } + } + } + } + this.nextObjectIndex_ = 0; + } + + if (this.nextClassIndex_ === this.classNames_.length) { + this.nextClassIndex_ = 0; + this.nextObjectIndex_ = 0; + this.globalObjectIndex_ = 0; + ++this.phase_; + } +}; + +/** + * Second pass: set property and parameter values and bind parameters. + * @param {number} amountOfWork The number of loop iterations to perform of + * this phase_. + * @private + */ +o3djs.serialization.Deserializer.prototype.setPropertiesPhase_ = function( + amountOfWork) { + for (; this.nextClassIndex_ < this.classNames_.length; + ++this.nextClassIndex_) { + var className = this.classNames_[this.nextClassIndex_]; + var classJson = this.json.objects[className]; + var numObjects = classJson.length; + for (; this.nextObjectIndex_ < numObjects; ++this.nextObjectIndex_) { + if (amountOfWork-- <= 0) + return; + + var objectJson = classJson[this.nextObjectIndex_]; + var object = this.objectsByIndex_[this.globalObjectIndex_++]; + if ('properties' in objectJson) { + for (var propertyName in objectJson.properties) { + if (propertyName in object) { + var propertyJson = objectJson.properties[propertyName]; + var propertyValue = this.deserializeValue(propertyJson); + object[propertyName] = propertyValue; + } + }; + } + if ('params' in objectJson) { + if ('length' in objectJson.params) { + for (var paramIndex = 0; paramIndex != objectJson.params.length; + ++paramIndex) { + var paramJson = objectJson.params[paramIndex]; + this.setParamValue_(/** @type {!Object} */ (object), + paramIndex, + paramJson); + } + } else { + for (var paramName in objectJson.params) { + var paramJson = objectJson.params[paramName]; + this.setParamValue_(/** @type {!Object} */ (object), + paramName, + paramJson); + } + } + } + if (className in this.initCallbacks) { + this.initCallbacks[className](this, object, objectJson); + } + } + this.nextObjectIndex_ = 0; + } + + if (this.nextClassIndex_ === this.classNames_.length) { + this.nextClassIndex_ = 0; + this.nextObjectIndex_ = 0; + this.globalObjectIndex_ = 0; + ++this.phase_; + } +}; + +/** + * Perform a certain number of iterations of the deserializer. Keep calling this + * function until it returns false. + * @param {number} opt_amountOfWork The number of loop iterations to run. If + * not specified, runs the deserialization to completion. + * @return {boolean} Whether work remains to be done. + */ +o3djs.serialization.Deserializer.prototype.run = function( + opt_amountOfWork) { + if (!opt_amountOfWork) { + while (this.run(10000)) { + } + return false; + } else { + switch (this.phase_) { + case 0: + this.createObjectsPhase_(opt_amountOfWork); + break; + case 1: + this.setPropertiesPhase_(opt_amountOfWork); + break; + } + return this.phase_ < 2; + } +}; + +/** + * Deserializes (loads) a transform graph in the background. Invokes + * a callback function on completion passing the pack and the thrown + * exception on failure or the pack and a null exception on success. + * @param {!o3d.Client} client An O3D client object. + * @param {!o3d.Pack} pack The pack to create the deserialized objects + * in. + * @param {number} time The amount of the time (in seconds) the deserializer + * should aim to complete in. + * @param {!function(o3d.Pack, *): void} callback The function that + * is called on completion. The second parameter is null on success or + * the thrown exception on failure. + */ +o3djs.serialization.Deserializer.prototype.runBackground = function( + client, pack, time, callback) { + // TODO: This seems like it needs to be more granular than the + // top level. + // TODO: Passing in the time you want it to take seems counter + // intuitive. I want pass in a % of CPU so I can effectively say + // "deserialize this in such a way so as not to affect my app's + // performance". callbacksRequired = numObjects / amountPerCallback where + // amountPerCallback = number I can do per frame and not affect performance + // too much. + var workToDo = this.json.objects.length * 2; + var timerCallbacks = time * 60; + var amountPerCallback = workToDo / timerCallbacks; + var intervalId; + var that = this; + function deserializeMore() { + var exception = null; + var finished = false; + var failed = false; + var errorCollector = o3djs.error.createErrorCollector(client); + try { + finished = !that.run(amountPerCallback); + } catch(e) { + failed = true; + finished = true; + exception = e; + } + if (errorCollector.errors.length > 0) { + finished = true; + exception = errorCollector.errors.join('\n') + + (exception ? ('\n' + exception.toString()) : ''); + } + errorCollector.finish(); + if (finished) { + window.clearInterval(intervalId); + callback(pack, exception); + } + } + + intervalId = window.setInterval(deserializeMore, 1000 / 60); +}; + +/** + * Creates a deserializer that will incrementally deserialize a + * transform graph. The deserializer object has a method + * called run that does a fixed amount of work and returns. + * It returns true until the transform graph is fully deserialized. + * It returns false from then on. + * @param {!o3d.Pack} pack The pack to create the deserialized + * objects in. + * @param {!Object} json An object tree conforming to the JSON rules. + * @return {!o3djs.serialization.Deserializer} A deserializer object. + */ +o3djs.serialization.createDeserializer = function(pack, json) { + return new o3djs.serialization.Deserializer(pack, json); +}; + +/** + * Deserializes a transform graph. + * @param {!o3d.Pack} pack The pack to create the deserialized + * objects in. + * @param {!Object} json An object tree conforming to the JSON rules. + */ +o3djs.serialization.deserialize = function(pack, json) { + var deserializer = o3djs.serialization.createDeserializer(pack, json); + deserializer.run(); +}; + +/** + * Deserializes a single json object named 'scene.json' from a loaded + * o3djs.io.ArchiveInfo. + * @param {!o3djs.io.archiveInfo} archiveInfo Archive to load from. + * @param {string} sceneJsonUri The relative URI of the scene JSON file within + * the archive. + * @param {!o3d.Client} client An O3D client object. + * @param {!o3d.Pack} pack The pack to create the deserialized objects + * in. + * @param {!o3d.Transform} parent Transform to parent loaded stuff from. + * @param {!function(!o3d.Pack, !o3d.Transform, *): void} callback A function + * that will be called when deserialization is finished. It will be passed + * the pack, the parent transform, and an exception which will be null on + * success. + * @param {!o3djs.serialization.Options} opt_options Options. + */ +o3djs.serialization.deserializeArchive = function(archiveInfo, + sceneJsonUri, + client, + pack, + parent, + callback, + opt_options) { + opt_options = opt_options || { }; + var jsonFile = archiveInfo.getFileByURI(sceneJsonUri); + if (!jsonFile) { + throw 'Could not find ' + sceneJsonUri + ' in archive'; + } + var parsed = eval('(' + jsonFile.stringValue + ')'); + var deserializer = o3djs.serialization.createDeserializer(pack, parsed); + + deserializer.addObject(parsed.o3d_rootObject_root, parent); + deserializer.archiveInfo = archiveInfo; + + var finishCallback = function(pack, exception) { + if (!exception) { + var objects = pack.getObjects('o3d.animSourceOwner', 'o3d.ParamObject'); + if (objects.length > 0) { + // Rebind the output connections of the animSource to the user's param. + if (opt_options.opt_animSource) { + var animSource = objects[0].getParam('animSource'); + var outputConnections = animSource.outputConnections; + for (var ii = 0; ii < outputConnections.length; ++ii) { + outputConnections[ii].bind(opt_options.opt_animSource); + } + } + // Remove special object from pack. + for (var ii = 0; ii < objects.length; ++ii) { + pack.removeObject(objects[ii]); + } + } + } + callback(pack, parent, exception); + }; + + if (opt_options.opt_async) { + // TODO: Remove the 5. See deserializer.runBackground comments. + deserializer.runBackground(client, pack, 5, finishCallback); + } else { + var exception = null; + var errorCollector = o3djs.error.createErrorCollector(client); + try { + deserializer.run(); + } catch (e) { + exception = e; + } + if (errorCollector.errors.length > 0) { + exception = errorCollector.errors.join('\n') + + (exception ? ('\n' + exception.toString()) : ''); + } + errorCollector.finish(); + finishCallback(pack, exception); + } +}; + + |