diff --git a/tutorial/doppleganger.js b/tutorial/doppleganger.js new file mode 100644 index 0000000..f887185 --- /dev/null +++ b/tutorial/doppleganger.js @@ -0,0 +1,526 @@ +"use strict"; +//====================================== +// +// doppleganger.js +// +// Created by Timothy Dedischew on 04/21/2017. +// Copyright 2017 High Fidelity, Inc. +// Copyright 2020 Vircadia contibutors. +// Copyright 2022 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// +/* global module */ +// @module doppleganger +// +// This module contains the `Doppleganger` class implementation for creating an inspectable replica of +// an Avatar (as a model directly in front of and facing them). Joint positions and rotations are copied +// over in an update thread, so that the model automatically mirrors the Avatar's joint movements. +// An Avatar can then for example walk around "themselves" and examine from the back, etc. +// +// This should be helpful for inspecting your own look and debugging avatars, etc. +// +// The doppleganger is created as a local entity so that others do not see it -- this also allows for the +// highest possible update rate when keeping joint data in sync. +// +// NOTE: THIS IS A MODIFIED VERSION SPECIFICALLY FOR THE TUTORIAL. + +module.exports = Doppleganger; + +// @property {bool} - when set true, Script.update will be used instead of setInterval for syncing joint data +Doppleganger.USE_SCRIPT_UPDATE = false; + +// @property {int} - the frame rate to target when using setInterval for joint updates +Doppleganger.TARGET_FPS = 60; + +// @property {int} - the maximum time in seconds to wait for the model overlay to finish loading +Doppleganger.MAX_WAIT_SECS = 10; + +// @function - derive mirrored joint names from a list of regular joint names +// @param {Array} - list of joint names to mirror +// @return {Array} - list of mirrored joint names (note: entries for non-mirrored joints will be `undefined`) + +var Setarry; + +Doppleganger.getMirroredJointNames = function(jointNames) { + return jointNames.map(function(name, i) { + if (/Left/.test(name)) { + return name.replace('Left', 'Right'); + } + if (/Right/.test(name)) { + return name.replace('Right', 'Left'); + } + return undefined; + }); +}; + +// @class Doppleganger - Creates a new instance of a Doppleganger. +// @param {Avatar} [options.avatar=MyAvatar] - Avatar used to retrieve position and joint data. +// @param {bool} [options.mirrored=true] - Apply "symmetric mirroring" of Left/Right joints. +// @param {bool} [options.autoUpdate=true] - Automatically sync joint data. +function Doppleganger(options) { + options = options || {}; + this.avatar = options.avatar || MyAvatar; + this.mirrored = 'mirrored' in options ? options.mirrored : false; + this.autoUpdate = 'autoUpdate' in options ? options.autoUpdate : true; + + // @public + this.active = false; // whether doppleganger is currently being displayed/updated + this.entityID = null; // current doppleganger's Entity id + this.frame = 0; // current joint update frame + + // @signal - emitted when .active state changes + this.activeChanged = signal(function(active, reason) {}); + // @signal - emitted once model overlay is either loaded or errors out + this.addingEntity = signal(function(error, result){}); + // @signal - emitted each time the model overlay's joint data has been synchronized + this.jointsUpdated = signal(function(entityID){}); +} + +Doppleganger.prototype = { + // @public @method - toggles doppleganger on/off + toggle: function() { + if (this.active) { + log('toggling off'); + this.stop(); + }else{ + log('toggling on'); + this.start(); + } + return this.active; + }, + + // @public @method - synchronize the joint data between Avatar / doppleganger + update: function() { + this.frame++; + try { + if (!this.entityID) { + throw new Error('!this.entityID'); + } + + if (this.avatar.skeletonModelURL !== this.skeletonModelURL) { + //return this.stop('avatar_changed'); + } + + var rotations = this.avatar.getJointRotations(); + var translations = this.avatar.getJointTranslations(); + var size = rotations.length; + + + // note: this mismatch can happen when the avatar's model is actively changing + if (size !== translations.length || + (this.jointStateCount && size !== this.jointStateCount)) { + log('mismatched joint counts (avatar model likely changed)', size, translations.length, this.jointStateCount); + this.stop('avatar_changed_joints'); + return; + } + this.jointStateCount = size; + + + if (this.mirrored) { + var mirroredIndexes = this.mirroredIndexes; + var outRotations = new Array(size); + var outTranslations = new Array(size); + for (var i=0; i < size; i++) { + var index = mirroredIndexes[i]; + if (index < 0 || index === false) { + index = i; + } + var rot = rotations[index]; + var trans = translations[index]; + trans.x *= -1; + rot.y *= -1; + rot.z *= -1; + outRotations[i] = rot; + outTranslations[i] = trans; + } + rotations = outRotations; + translations = outTranslations; + } + + + Entities.editEntity(this.entityID, { + jointRotations: rotations, + jointTranslations: translations, + jointRotationsSet: Setarry, + jointTranslationsSet: Setarry + }); + + this.jointsUpdated(this.entityID); + } catch (e) { + //log('.update error: '+ e, index); + this.stop('update_error'); + } + }, + + // @public @method - show the doppleganger (and start the update thread, if options.autoUpdate was specified). + // @param {vec3} [options.position=(in front of avatar)] - starting position + // @param {quat} [options.orientation=avatar.orientation] - starting orientation + start: function(options) { + + + options = options || {}; + if (this.entityID) { + //log('start() called but entity model already exists', this.entityID); + return; + } + var avatar = this.avatar; + if (!avatar.jointNames.length) { + return this.stop('joints_unavailable'); + } + + this.frame = 0; + this.position = options.position || Vec3.sum(avatar.position, Quat.getForward(avatar.orientation)); + this.orientation = options.orientation || avatar.orientation; + this.skeletonModelURL = avatar.skeletonModelURL; + this.jointStateCount = 0; + this.jointNames = avatar.jointNames; + this.mirroredNames = Doppleganger.getMirroredJointNames(this.jointNames); + //log(this.mirroredNames); + this.mirroredIndexes = this.mirroredNames.map(function(name) { + return name ? avatar.getJointIndex(name) : false; + }); + //log(this.mirroredIndexes); + var prop = { + type: "Model", + name: 'Doppelganger', //added + visible: false, // normally false + modelURL: this.skeletonModelURL, //was field: url + position: this.position, + rotation: this.orientation + }; + + this.entityID = Entities.addEntity(prop, "local"); + + var allJoints = avatar.getJointRotations(); + var nbrJoints = allJoints.length; + Setarry = Array(nbrJoints); + for (var i=0; i < nbrJoints; i++) { + Setarry[i] = true; + } + + this.onAddingEntity = function(error, result) { + + if (error) { + return this.stop(error); + } + log('ModelEntity is ready; # joints == ' + result.jointNames.length); + Entities.editEntity(this.entityID, { visible: true }); + + this.syncVerticalPosition('LeftFoot'); + + if (this.autoUpdate) { + this._createUpdateThread(); + } + }; + this.addingEntity.connect(this, 'onAddingEntity'); + + log('doppleganger created; entityID =', this.entityID); + + // trigger clean up (and stop updates) if the overlay gets deleted + this.onDeletedEntity = function(uuid) { + if (uuid === this.entityID) { + log('onDeletedEntity', uuid); + this.stop('entity_deleted'); + } + }; + Entities.deletingEntity.connect(this, 'onDeletedEntity'); + + if ('onLoadComplete' in avatar) { + // stop the current doppleganger if Avatar loads a different model URL + this.onLoadComplete = function() { + if (avatar.skeletonModelURL !== this.skeletonModelURL) { + //this.stop('avatar_changed_load'); + } + }; + avatar.onLoadComplete.connect(this, 'onLoadComplete'); + } + + this.activeChanged(this.active = true, 'start'); + this._waitForModel(ModelCache.prefetch(this.skeletonModelURL)); + }, + + // @public @method - hide the doppleganger + // @param {String} [reason=stop] - the reason stop was called + stop: function(reason) { + reason = reason || 'stop'; + if (this.onUpdate) { + Script.update.disconnect(this, 'onUpdate'); + delete this.onUpdate; + } + if (this._interval) { + Script.clearInterval(this._interval); + this._interval = undefined; + } + if (this.onDeletedEntity) { + Entities.deletingEntity.disconnect(this, 'onDeletedEntity'); + delete this.onDeletedEntity; + } + if (this.onLoadComplete) { + this.avatar.onLoadComplete.disconnect(this, 'onLoadComplete'); + delete this.onLoadComplete; + } + if (this.onAddingEntity) { + this.addingEntity.disconnect(this, 'onAddingEntity'); + } + if (this.entityID) { + Entities.deleteEntity(this.entityID); + this.entityID = undefined; + } + if (this.active) { + this.activeChanged(this.active = false, reason); + } else if (reason) { + log('already stopped so not triggering another activeChanged; latest reason was:', reason); + } + }, + + // @public @method - Reposition the doppleganger so it sees "eye to eye" with the Avatar. + // @param {String} [byJointName=Hips] - the reference joint used to align the Doppleganger and Avatar + syncVerticalPosition: function(byJointName) { + byJointName = byJointName || 'Hips'; + + var dopplePosition = Entities.getEntityProperties(this.entityID, ["position"]); + var doppleJointIndex = Entities.getJointIndex( this.entityID, byJointName ); + var doppleJointPosition = Vec3.sum(Entities.getAbsoluteJointTranslationInObjectFrame( this.entityID, doppleJointIndex ), dopplePosition); + + //log("Joint Pos = " + JSON.stringify(doppleJointPosition)); + + var avatarPosition = this.avatar.position; + var avatarJointIndex = this.avatar.getJointIndex(byJointName); + var avatarJointPosition = Vec3.sum(this.avatar.getAbsoluteJointTranslationInObjectFrame(avatarJointIndex), avatarPosition); + + //log("AV Joint Pos = " + JSON.stringify(avatarJointPosition)); + + dopplePosition.position.y = avatarJointPosition.y - doppleJointPosition.y; + this.position = dopplePosition.position; + Entities.editEntity(this.entityID, { position: this.position }); + }, + + // @private @method - creates the update thread to synchronize joint data + _createUpdateThread: function() { + if (Doppleganger.USE_SCRIPT_UPDATE) { + log('creating Script.update thread'); + this.onUpdate = this.update; + Script.update.connect(this, 'onUpdate'); + } else { + log('creating Script.setInterval thread @ ~', Doppleganger.TARGET_FPS +'fps'); + var timeout = 1000 / Doppleganger.TARGET_FPS; + this._interval = Script.setInterval(bind(this, 'update'), timeout); + } + }, + + // @private @method - waits for model to load and handles timeouts + // @param {ModelResource} resource - a prefetched resource to monitor loading state against + _waitForModel: function(resource) { + var RECHECK_MS = 50; + var id = this.entityID, + watchdogTimer = null; + + function waitForJointNames() { + var error = null, result = null; + if (!watchdogTimer) { + error = 'joints_unavailable'; + } else if (resource.state === Resource.State.FAILED) { + error = 'prefetch_failed'; + } else if (resource.state === Resource.State.FINISHED) { + var names = Entities.getJointNames(id); + if (Array.isArray(names) && names.length) { + result = { entityID: id, jointNames: names }; + } + } + if (error || result !== null) { + Script.clearInterval(this._interval); + this._interval = null; + if (watchdogTimer) { + Script.clearTimeout(watchdogTimer); + } + this.addingEntity(error, result); + } + } + watchdogTimer = Script.setTimeout(function() { + watchdogTimer = null; + }, Doppleganger.MAX_WAIT_SECS * 1000); + this._interval = Script.setInterval(bind(this, waitForJointNames), RECHECK_MS); + } +}; + +// @function - bind a function to a `this` context +// @param {Object} - the `this` context +// @param {Function|String} - function or method name +function bind(thiz, method) { + method = thiz[method] || method; + return function() { + return method.apply(thiz, arguments); + }; +} + +// @function - Qt signal polyfill +function signal(template) { + var callbacks = []; + return Object.defineProperties(function() { + var args = [].slice.call(arguments); + callbacks.forEach(function(obj) { + obj.handler.apply(obj.scope, args); + }); + }, { + connect: { value: function(scope, handler) { + callbacks.push({scope: scope, handler: scope[handler] || handler || scope}); + }}, + disconnect: { value: function(scope, handler) { + var match = {scope: scope, handler: scope[handler] || handler || scope}; + callbacks = callbacks.filter(function(obj) { + return !(obj.scope === match.scope && obj.handler === match.handler); + }); + }} + }); +} + +// @function - debug logging +function log() { + //print('doppleganger | ' + [].slice.call(arguments).join(' ')); +} + +// -- ADVANCED DEBUGGING -- +// @function - Add debug joint indicators / extra debugging info. +// @param {Doppleganger} - existing Doppleganger instance to add controls to +// +// @note: +// * rightclick toggles mirror mode on/off +// * shift-rightclick toggles the debug indicators on/off +// * clicking on an indicator displays the joint name and mirrored joint name in the debug log. +// +// Example use: +// var doppleganger = new Doppleganger(); +// Doppleganger.addDebugControls(doppleganger); +Doppleganger.addDebugControls = function(doppleganger) { + DebugControls.COLOR_DEFAULT = { red: 255, blue: 255, green: 255 }; + DebugControls.COLOR_SELECTED = { red: 0, blue: 255, green: 0 }; + + function DebugControls() { + this.enableIndicators = true; + this.selectedJointName = null; + this.debugEntityIDs = undefined; + this.jointSelected = signal(function(result) {}); + } + DebugControls.prototype = { + start: function() { + if (!this.onMousePressEvent) { + this.onMousePressEvent = this._onMousePressEvent; + Controller.mousePressEvent.connect(this, 'onMousePressEvent'); + } + }, + + stop: function() { + this.removeIndicators(); + if (this.onMousePressEvent) { + Controller.mousePressEvent.disconnect(this, 'onMousePressEvent'); + delete this.onMousePressEvent; + } + }, + + createIndicators: function(jointNames) { + this.jointNames = jointNames; + return jointNames.map(function(name, i) { + return Entities.addEntity({ + type: "Shape", + shape: 'Icosahedron', + scale: 0.1, + solid: false, + alpha: 0.5 + }); + }); + }, + + removeIndicators: function() { + if (this.debugEntityIDs) { + this.debugEntityIDs.forEach(Entities.deleteEntity); + this.debugEntityIDs = undefined; + } + }, + + onJointsUpdated: function(entityID) { + if (!this.enableIndicators) { + return; + } + var jointNames = Entities.getJointNames(entityID), + jointOrientations = Entities.getEntityProperties(entityID, ['jointRotations']), //was jointOrientations + jointPositions = Entities.getEntityProperties(entityID, ['jointTranslations']), //was jointPositions + //selectedIndex = jointNames.indexOf(this.selectedJointName); + selectedIndex = Entities.getJointIndex( entityID, this.selectedJointName ); + + if (!this.debugEntityIDs) { + this.debugEntityIDs = this.createIndicators(jointNames); + } + + // batch all updates into a single call (using the editOverlays({ id: {props...}, ... }) API) + var updatedOverlays = this.debugEntityIDs.reduce(function(updates, id, i) { + updates[id] = { + position: jointPositions.jointTranslations[i], + rotation: jointOrientations.jointRotations[i], + color: i === selectedIndex ? DebugControls.COLOR_SELECTED : DebugControls.COLOR_DEFAULT, + solid: i === selectedIndex + }; + return updates; + }, {}); + //Entities.editOverlays(updatedOverlays); + }, + + _onMousePressEvent: function(evt) { + if (!evt.isLeftButton || !this.enableIndicators || !this.debugEntityIDs) { + return; + } + var ray = Camera.computePickRay(evt.x, evt.y), + hit = Entities.findRayIntersection(ray, true, this.debugEntityIDs); + + hit.jointIndex = this.debugEntityIDs.indexOf(hit.entityID); + hit.jointName = this.jointNames[hit.jointIndex]; + this.jointSelected(hit); + } + }; + + if ('$debugControls' in doppleganger) { + throw new Error('only one set of debug controls can be added per doppleganger'); + } + var debugControls = new DebugControls(); + doppleganger.$debugControls = debugControls; + + function onMousePressEvent(evt) { + if (evt.isRightButton) { + if (evt.isShifted) { + debugControls.enableIndicators = !debugControls.enableIndicators; + if (!debugControls.enableIndicators) { + debugControls.removeIndicators(); + } + } else { + doppleganger.mirrored = !doppleganger.mirrored; + } + } + } + + doppleganger.activeChanged.connect(function(active) { + if (active) { + debugControls.start(); + doppleganger.jointsUpdated.connect(debugControls, 'onJointsUpdated'); + Controller.mousePressEvent.connect(onMousePressEvent); + } else { + Controller.mousePressEvent.disconnect(onMousePressEvent); + doppleganger.jointsUpdated.disconnect(debugControls, 'onJointsUpdated'); + debugControls.stop(); + } + }); + + debugControls.jointSelected.connect(function(hit) { + debugControls.selectedJointName = hit.jointName; + if (hit.jointIndex < 0) { + return; + } + hit.mirroredJointName = Doppleganger.getMirroredJointNames([hit.jointName])[0]; + log('selected joint:', JSON.stringify(hit, 0, 2)); + }); + + Script.scriptEnding.connect(debugControls, 'removeIndicators'); + + return doppleganger; +}; diff --git a/tutorial/fonts/LICENSE.txt b/tutorial/fonts/LICENSE.txt new file mode 100644 index 0000000..75b5248 --- /dev/null +++ b/tutorial/fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tutorial/fonts/Roboto-Bold.ttf b/tutorial/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..3742457 Binary files /dev/null and b/tutorial/fonts/Roboto-Bold.ttf differ diff --git a/tutorial/fonts/Roboto-BoldItalic.ttf b/tutorial/fonts/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..e85e7fb Binary files /dev/null and b/tutorial/fonts/Roboto-BoldItalic.ttf differ diff --git a/tutorial/fonts/Roboto-Italic.ttf b/tutorial/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..c9df607 Binary files /dev/null and b/tutorial/fonts/Roboto-Italic.ttf differ diff --git a/tutorial/fonts/Roboto-Regular.ttf b/tutorial/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..3d6861b Binary files /dev/null and b/tutorial/fonts/Roboto-Regular.ttf differ diff --git a/tutorial/wizard.html b/tutorial/wizard.html new file mode 100644 index 0000000..4a5e1be --- /dev/null +++ b/tutorial/wizard.html @@ -0,0 +1,490 @@ + + + + + + Quick Configuration + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + +
+

Welcome to Overte!

+
+ Let's get you setup to experience the virtual world.
+ First, we need to select some performance and graphics quality options.

+ Press Continue when you are ready.
+
+   + + +
+
+
+ + + + + + + + + + + +
+

Quality

+
+ What level of visual quality would you like?
+ Remember! If you do not have a powerful computer,
you may want to set this to low or medium at most.
+

+
       Very Low Quality Slow Laptop / Very Slow Computer
+
       Low Quality Average Laptop / Slow Computer
+
       Medium Quality Average Computer - Recommended
+
       High Quality Gaming Computer
+
+ + + +
+
+
+ + + + + + + + + + + +
+

Performance

+
+ Do you want a smooth experience (high refresh rate)
or do you want to conserve power and resources (low refresh rate) on your computer?
+ Note: This does not apply to virtual reality headsets. +

+
       Not Smooth (20 Hz) Conserve Power
+
       Smooth (30 Hz) Use Average Resources
+
       Very Smooth (60 Hz) Use Maximum Resources - Recommended
+
+
+ + + +
+
+
+ + + + + + + + + + + +
+

Display Name

+
+ What should people call you?
+ This is simply a nickname, it will be shown in place of your username (if you have one). +

+       NAME: +
+
+ + + +
+
+
+ + + + + + + + + + + +
+

All done!

+
+ Now you're almost ready to go!
+ Press Complete to save your setup.
+ Then take a look at the other information kiosks after completing this wizard.
+
+ + + +
+
+
+ + +