UNPKG

aframe-gui

Version:
1,386 lines (1,249 loc) 232 kB
(function (THREE, aframe) { 'use strict'; function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } function _interopNamespace(e) { if (e && e.__esModule) { return e; } else { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n['default'] = e; return Object.freeze(n); } } var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE); var aframe__default = /*#__PURE__*/_interopDefaultLegacy(aframe); /** * Lightweight thenable implementation that is entirely self-contained within a single * function with no external dependencies so it can be easily shipped across to a WorkerModule. * * This implementation conforms fully to the Promises/A+ spec so it can safely interoperate * with other thenable implementations. https://github.com/promises-aplus/promises-spec * * *However*, it is _not_ a full implementation of ES2015 Promises, e.g. it does not * have the same constructor signature and does not expose a `catch` method or the static * `resolve`/`reject`/`all`/`race` initializer methods. If you need to hand a Thenable * instance off to consuming code that may expect a true Promise, you'll want to wrap it * in a native-or-polyfilled Promise first. * * (Why yet another Promises/A+ implementation? Great question. We needed a polyfill-like * thing that was (a) wrapped in a single function for easy serialization across to a Worker, * and (b) was as small as possible -- at ~900B minified (~500B gzipped) this is the smallest * implementation I've found. And also, exercises like this are challenging and fun.) */ function BespokeThenable() { var state = 0; // 0=pending, 1=fulfilled, -1=rejected var queue = []; var value; var scheduled = 0; var completeCalled = 0; function then(onResolve, onReject) { var nextThenable = BespokeThenable(); function handleNext() { var cb = state > 0 ? onResolve : onReject; if (isFn(cb)) { try { var result = cb(value); if (result === nextThenable) { recursiveError(); } var resultThen = getThenableThen(result); if (resultThen) { resultThen.call(result, nextThenable.resolve, nextThenable.reject); } else { nextThenable.resolve(result); } } catch (err) { nextThenable.reject(err); } } else { nextThenable[state > 0 ? 'resolve' : 'reject'](value); } } queue.push(handleNext); if (state) { scheduleQueueFlush(); } return nextThenable } var resolve = oneTime(function (val) { if (!completeCalled) { complete(1, val); } }); var reject = oneTime(function (reason) { if (!completeCalled) { complete(-1, reason); } }); function complete(st, val) { completeCalled++; var ignoreThrow = 0; try { if (val === thenableObj) { recursiveError(); } var valThen = st > 0 && getThenableThen(val); if (valThen) { valThen.call(val, oneTime(function (v) { ignoreThrow++; complete(1, v); }), oneTime(function (v) { ignoreThrow++; complete(-1, v); })); } else { state = st; value = val; scheduleQueueFlush(); } } catch(e) { if (!state && !ignoreThrow) { complete(-1, e); } } } function scheduleQueueFlush() { if (!scheduled) { setTimeout(flushQueue, 0); //TODO setImmediate or postMessage approach if available? scheduled = 1; } } function flushQueue() { var q = queue; scheduled = 0; queue = []; q.forEach(callIt); } function callIt(fn) { fn(); } function getThenableThen(val) { var valThen = val && (isFn(val) || typeof val === 'object') && val.then; return isFn(valThen) && valThen } function oneTime(fn) { var called = 0; return function() { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; if (!called++) { fn.apply(this, args); } } } function recursiveError() { throw new TypeError('Chaining cycle detected') } var isFn = function (v) { return typeof v === 'function'; }; var thenableObj = { then: then, resolve: resolve, reject: reject }; return thenableObj } /** * Thenable implementation that uses a native Promise under the covers. This implementation * is preferred if Promise is available, for better performance and dev tools integration. * @constructor */ function NativePromiseThenable() { var resolve, reject; var promise = new Promise(function (res, rej) { resolve = res; reject = rej; }); return { then: promise.then.bind(promise), resolve: resolve, reject: reject } } /** * Promise.all() impl: */ BespokeThenable.all = NativePromiseThenable.all = function(items) { var resultCount = 0; var results = []; var out = DefaultThenable(); if (items.length === 0) { out.resolve([]); } else { items.forEach(function (item, i) { var itemThenable = DefaultThenable(); itemThenable.resolve(item); itemThenable.then(function (res) { resultCount++; results[i] = res; if (resultCount === items.length) { out.resolve(results); } }, out.reject); }); } return out }; /** * Choose the best Thenable implementation and export it as the default. */ var DefaultThenable = typeof Promise === 'function' ? NativePromiseThenable : BespokeThenable; /** * Main content for the worker that handles the loading and execution of * modules within it. */ function workerBootstrap() { var modules = Object.create(null); // Handle messages for registering a module function registerModule(ref, callback) { var id = ref.id; var name = ref.name; var dependencies = ref.dependencies; if ( dependencies === void 0 ) dependencies = []; var init = ref.init; if ( init === void 0 ) init = function(){}; var getTransferables = ref.getTransferables; if ( getTransferables === void 0 ) getTransferables = null; // Only register once if (modules[id]) { return } try { // If any dependencies are modules, ensure they're registered and grab their value dependencies = dependencies.map(function (dep) { if (dep && dep.isWorkerModule) { registerModule(dep, function (depResult) { if (depResult instanceof Error) { throw depResult } }); dep = modules[dep.id].value; } return dep }); // Rehydrate functions init = rehydrate(("<" + name + ">.init"), init); if (getTransferables) { getTransferables = rehydrate(("<" + name + ">.getTransferables"), getTransferables); } // Initialize the module and store its value var value = null; if (typeof init === 'function') { value = init.apply(void 0, dependencies); } else { console.error('worker module init function failed to rehydrate'); } modules[id] = { id: id, value: value, getTransferables: getTransferables }; callback(value); } catch(err) { if (!(err && err.noLog)) { console.error(err); } callback(err); } } // Handle messages for calling a registered module's result function function callModule(ref, callback) { var ref$1; var id = ref.id; var args = ref.args; if (!modules[id] || typeof modules[id].value !== 'function') { callback(new Error(("Worker module " + id + ": not found or its 'init' did not return a function"))); } try { var result = (ref$1 = modules[id]).value.apply(ref$1, args); if (result && typeof result.then === 'function') { result.then(handleResult, function (rej) { return callback(rej instanceof Error ? rej : new Error('' + rej)); }); } else { handleResult(result); } } catch(err) { callback(err); } function handleResult(result) { try { var tx = modules[id].getTransferables && modules[id].getTransferables(result); if (!tx || !Array.isArray(tx) || !tx.length) { tx = undefined; //postMessage is very picky about not passing null or empty transferables } callback(result, tx); } catch(err) { console.error(err); callback(err); } } } function rehydrate(name, str) { var result = void 0; self.troikaDefine = function (r) { return result = r; }; var url = URL.createObjectURL( new Blob( [("/** " + (name.replace(/\*/g, '')) + " **/\n\ntroikaDefine(\n" + str + "\n)")], {type: 'application/javascript'} ) ); try { importScripts(url); } catch(err) { console.error(err); } URL.revokeObjectURL(url); delete self.troikaDefine; return result } // Handler for all messages within the worker self.addEventListener('message', function (e) { var ref = e.data; var messageId = ref.messageId; var action = ref.action; var data = ref.data; try { // Module registration if (action === 'registerModule') { registerModule(data, function (result) { if (result instanceof Error) { postMessage({ messageId: messageId, success: false, error: result.message }); } else { postMessage({ messageId: messageId, success: true, result: {isCallable: typeof result === 'function'} }); } }); } // Invocation if (action === 'callModule') { callModule(data, function (result, transferables) { if (result instanceof Error) { postMessage({ messageId: messageId, success: false, error: result.message }); } else { postMessage({ messageId: messageId, success: true, result: result }, transferables || undefined); } }); } } catch(err) { postMessage({ messageId: messageId, success: false, error: err.stack }); } }); } /** * Fallback for `defineWorkerModule` that behaves identically but runs in the main * thread, for when the execution environment doesn't support web workers or they * are disallowed due to e.g. CSP security restrictions. */ function defineMainThreadModule(options) { var moduleFunc = function() { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; return moduleFunc._getInitResult().then(function (initResult) { if (typeof initResult === 'function') { return initResult.apply(void 0, args) } else { throw new Error('Worker module function was called but `init` did not return a callable function') } }) }; moduleFunc._getInitResult = function() { // We can ignore getTransferables in main thread. TODO workerId? var dependencies = options.dependencies; var init = options.init; // Resolve dependencies dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) { return dep && dep._getInitResult ? dep._getInitResult() : dep; } ) : []; // Invoke init with the resolved dependencies var initThenable = DefaultThenable.all(dependencies).then(function (deps) { return init.apply(null, deps) }); // Cache the resolved promise for subsequent calls moduleFunc._getInitResult = function () { return initThenable; }; return initThenable }; return moduleFunc } var supportsWorkers = function () { var supported = false; // Only attempt worker initialization in browsers; elsewhere it would just be // noise e.g. loading into a Node environment for SSR. if (typeof window !== 'undefined' && typeof window.document !== 'undefined') { try { // TODO additional checks for things like importScripts within the worker? // Would need to be an async check. var worker = new Worker( URL.createObjectURL(new Blob([''], { type: 'application/javascript' })) ); worker.terminate(); supported = true; } catch (err) { if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') ; else { console.log( ("Troika createWorkerModule: web workers not allowed; falling back to main thread execution. Cause: [" + (err.message) + "]") ); } } } // Cached result supportsWorkers = function () { return supported; }; return supported }; var _workerModuleId = 0; var _messageId = 0; var _allowInitAsString = false; var workers = Object.create(null); var openRequests = /*#__PURE__*/(function () { var obj = Object.create(null); obj._count = 0; return obj })(); /** * Define a module of code that will be executed with a web worker. This provides a simple * interface for moving chunks of logic off the main thread, and managing their dependencies * among one another. * * @param {object} options * @param {function} options.init - The main function that initializes the module. This will be run * within the worker, and will be passed the resolved dependencies as arguments. Its * return value becomes the module's content, which can then be used by other modules * that depend on it. This function can perform any logic using those dependencies, but * must not depend on anything from its parent closures. * @param {array} [options.dependencies] - Provides any dependencies required by the init function: * - Primitives like strings, numbers, booleans * - Raw functions; these will be stringified and rehydrated within the worker so they * must not depend on anything from their parent closures * - Other worker modules; these will be resolved within the worker, and therefore modules * that provide functions can be called without having to cross the worker/main thread * boundary. * @param {function} [options.getTransferables] - An optional function that will be run in the worker * just before posting the response value from a module call back to the main thread. * It will be passed that response value, and if it returns an array then that will be * used as the "transferables" parameter to `postMessage`. Use this if there are values * in the response that can/should be transfered rather than cloned. * @param {string} [options.name] - A descriptive name for this module; this can be useful for * debugging but is not currently used for anything else. * @param {string} [options.workerId] - By default all modules will run in the same dedicated worker, * but if you want to use multiple workers you can pass a `workerId` to indicate a specific * worker to spawn. Note that each worker is completely standalone and no data or state will * be shared between them. If a worker module is used as a dependency by worker modules * using different `workerId`s, then that dependency will be re-registered in each worker. * @return {function(...[*]): {then}} */ function defineWorkerModule(options) { if ((!options || typeof options.init !== 'function') && !_allowInitAsString) { throw new Error('requires `options.init` function') } var dependencies = options.dependencies; var init = options.init; var getTransferables = options.getTransferables; var workerId = options.workerId; if (!supportsWorkers()) { return defineMainThreadModule(options) } if (workerId == null) { workerId = '#default'; } var id = "workerModule" + (++_workerModuleId); var name = options.name || id; var registrationThenable = null; dependencies = dependencies && dependencies.map(function (dep) { // Wrap raw functions as worker modules with no dependencies if (typeof dep === 'function' && !dep.workerModuleData) { _allowInitAsString = true; dep = defineWorkerModule({ workerId: workerId, name: ("<" + name + "> function dependency: " + (dep.name)), init: ("function(){return (\n" + (stringifyFunction(dep)) + "\n)}") }); _allowInitAsString = false; } // Grab postable data for worker modules if (dep && dep.workerModuleData) { dep = dep.workerModuleData; } return dep }); function moduleFunc() { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; // Register this module if needed if (!registrationThenable) { registrationThenable = callWorker(workerId,'registerModule', moduleFunc.workerModuleData); } // Invoke the module, returning a thenable return registrationThenable.then(function (ref) { var isCallable = ref.isCallable; if (isCallable) { return callWorker(workerId,'callModule', {id: id, args: args}) } else { throw new Error('Worker module function was called but `init` did not return a callable function') } }) } moduleFunc.workerModuleData = { isWorkerModule: true, id: id, name: name, dependencies: dependencies, init: stringifyFunction(init), getTransferables: getTransferables && stringifyFunction(getTransferables) }; return moduleFunc } /** * Stringifies a function into a form that can be deserialized in the worker * @param fn */ function stringifyFunction(fn) { var str = fn.toString(); // If it was defined in object method/property format, it needs to be modified if (!/^function/.test(str) && /^\w+\s*\(/.test(str)) { str = 'function ' + str; } return str } function getWorker(workerId) { var worker = workers[workerId]; if (!worker) { // Bootstrap the worker's content var bootstrap = stringifyFunction(workerBootstrap); // Create the worker from the bootstrap function content worker = workers[workerId] = new Worker( URL.createObjectURL( new Blob( [("/** Worker Module Bootstrap: " + (workerId.replace(/\*/g, '')) + " **/\n\n;(" + bootstrap + ")()")], {type: 'application/javascript'} ) ) ); // Single handler for response messages from the worker worker.onmessage = function (e) { var response = e.data; var msgId = response.messageId; var callback = openRequests[msgId]; if (!callback) { throw new Error('WorkerModule response with empty or unknown messageId') } delete openRequests[msgId]; openRequests.count--; callback(response); }; } return worker } // Issue a call to the worker with a callback to handle the response function callWorker(workerId, action, data) { var thenable = DefaultThenable(); var messageId = ++_messageId; openRequests[messageId] = function (response) { if (response.success) { thenable.resolve(response.result); } else { thenable.reject(new Error(("Error in worker " + action + " call: " + (response.error)))); } }; openRequests._count++; if (openRequests.count > 1000) { //detect leaks console.warn('Large number of open WorkerModule requests, some may not be returning'); } getWorker(workerId).postMessage({ messageId: messageId, action: action, data: data }); return thenable } /** * Just the {@link Thenable} function wrapped as a worker module. If another worker * module needs Thenable as a dependency, it's better to pass this module rather than * the raw function in its `dependencies` array so it only gets registered once. */ var ThenableWorkerModule = /*#__PURE__*/defineWorkerModule({ name: 'Thenable', dependencies: [DefaultThenable], init: function(Thenable) { return Thenable } }); /** * Regular expression for matching the `void main() {` opener line in GLSL. * @type {RegExp} */ const voidMainRegExp = /\bvoid\s+main\s*\(\s*\)\s*{/g; /** * Recursively expands all `#include <xyz>` statements within string of shader code. * Copied from three's WebGLProgram#parseIncludes for external use. * * @param {string} source - The GLSL source code to evaluate * @return {string} The GLSL code with all includes expanded */ function expandShaderIncludes( source ) { const pattern = /^[ \t]*#include +<([\w\d./]+)>/gm; function replace(match, include) { let chunk = THREE.ShaderChunk[include]; return chunk ? expandShaderIncludes(chunk) : match } return source.replace( pattern, replace ) } // Local assign polyfill to avoid importing troika-core const assign = Object.assign || function(/*target, ...sources*/) { let target = arguments[0]; for (let i = 1, len = arguments.length; i < len; i++) { let source = arguments[i]; if (source) { for (let prop in source) { if (source.hasOwnProperty(prop)) { target[prop] = source[prop]; } } } } return target }; const epoch = Date.now(); const CONSTRUCTOR_CACHE = new WeakMap(); const SHADER_UPGRADE_CACHE = new Map(); // Material ids must be integers, but we can't access the increment from Three's `Material` module, // so let's choose a sufficiently large starting value that should theoretically never collide. let materialInstanceId = 1e10; /** * A utility for creating a custom shader material derived from another material's * shaders. This allows you to inject custom shader logic and transforms into the * builtin ThreeJS materials without having to recreate them from scratch. * * @param {THREE.Material} baseMaterial - the original material to derive from * * @param {Object} options - How the base material should be modified. * @param {Object} options.defines - Custom `defines` for the material * @param {Object} options.extensions - Custom `extensions` for the material, e.g. `{derivatives: true}` * @param {Object} options.uniforms - Custom `uniforms` for use in the modified shader. These can * be accessed and manipulated via the resulting material's `uniforms` property, just like * in a ShaderMaterial. You do not need to repeat the base material's own uniforms here. * @param {String} options.timeUniform - If specified, a uniform of this name will be injected into * both shaders, and it will automatically be updated on each render frame with a number of * elapsed milliseconds. The "zero" epoch time is not significant so don't rely on this as a * true calendar time. * @param {String} options.vertexDefs - Custom GLSL code to inject into the vertex shader's top-level * definitions, above the `void main()` function. * @param {String} options.vertexMainIntro - Custom GLSL code to inject at the top of the vertex * shader's `void main` function. * @param {String} options.vertexMainOutro - Custom GLSL code to inject at the end of the vertex * shader's `void main` function. * @param {String} options.vertexTransform - Custom GLSL code to manipulate the `position`, `normal`, * and/or `uv` vertex attributes. This code will be wrapped within a standalone function with * those attributes exposed by their normal names as read/write values. * @param {String} options.fragmentDefs - Custom GLSL code to inject into the fragment shader's top-level * definitions, above the `void main()` function. * @param {String} options.fragmentMainIntro - Custom GLSL code to inject at the top of the fragment * shader's `void main` function. * @param {String} options.fragmentMainOutro - Custom GLSL code to inject at the end of the fragment * shader's `void main` function. You can manipulate `gl_FragColor` here but keep in mind it goes * after any of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), so if you * want those to apply to your changes use `fragmentColorTransform` instead. * @param {String} options.fragmentColorTransform - Custom GLSL code to manipulate the `gl_FragColor` * output value. Will be injected near the end of the `void main` function, but before any * of ThreeJS's color postprocessing shader chunks (tonemapping, fog, etc.), and before the * `fragmentMainOutro`. * @param {function<{vertexShader,fragmentShader}>:{vertexShader,fragmentShader}} options.customRewriter - A function * for performing custom rewrites of the full shader code. Useful if you need to do something * special that's not covered by the other builtin options. This function will be executed before * any other transforms are applied. * @param {boolean} options.chained - Set to `true` to prototype-chain the derived material to the base * material, rather than the default behavior of copying it. This allows the derived material to * automatically pick up changes made to the base material and its properties. This can be useful * where the derived material is hidden from the user as an implementation detail, allowing them * to work with the original material like normal. But it can result in unexpected behavior if not * handled carefully. * * @return {THREE.Material} * * The returned material will also have two new methods, `getDepthMaterial()` and `getDistanceMaterial()`, * which can be called to get a variant of the derived material for use in shadow casting. If the * target mesh is expected to cast shadows, then you can assign these to the mesh's `customDepthMaterial` * (for directional and spot lights) and/or `customDistanceMaterial` (for point lights) properties to * allow the cast shadow to honor your derived shader's vertex transforms and discarded fragments. These * will also set a custom `#define IS_DEPTH_MATERIAL` or `#define IS_DISTANCE_MATERIAL` that you can look * for in your derived shaders with `#ifdef` to customize their behavior for the depth or distance * scenarios, e.g. skipping antialiasing or expensive shader logic. */ function createDerivedMaterial(baseMaterial, options) { // Generate a key that is unique to the content of these `options`. We'll use this // throughout for caching and for generating the upgraded shader code. This increases // the likelihood that the resulting shaders will line up across multiple calls so // their GL programs can be shared and cached. const optionsKey = getKeyForOptions(options); // First check to see if we've already derived from this baseMaterial using this // unique set of options, and if so reuse the constructor to avoid some allocations. let ctorsByDerivation = CONSTRUCTOR_CACHE.get(baseMaterial); if (!ctorsByDerivation) { CONSTRUCTOR_CACHE.set(baseMaterial, (ctorsByDerivation = Object.create(null))); } if (ctorsByDerivation[optionsKey]) { return new ctorsByDerivation[optionsKey]() } const privateBeforeCompileProp = `_onBeforeCompile${optionsKey}`; // Private onBeforeCompile handler that injects the modified shaders and uniforms when // the renderer switches to this material's program const onBeforeCompile = function (shaderInfo) { baseMaterial.onBeforeCompile.call(this, shaderInfo); // Upgrade the shaders, caching the result by incoming source code const cacheKey = optionsKey + '|||' + shaderInfo.vertexShader + '|||' + shaderInfo.fragmentShader; let upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey]; if (!upgradedShaders) { const upgraded = upgradeShaders(shaderInfo, options, optionsKey); upgradedShaders = SHADER_UPGRADE_CACHE[cacheKey] = upgraded; } // Inject upgraded shaders and uniforms into the program shaderInfo.vertexShader = upgradedShaders.vertexShader; shaderInfo.fragmentShader = upgradedShaders.fragmentShader; assign(shaderInfo.uniforms, this.uniforms); // Inject auto-updating time uniform if requested if (options.timeUniform) { shaderInfo.uniforms[options.timeUniform] = { get value() {return Date.now() - epoch} }; } // Users can still add their own handlers on top of ours if (this[privateBeforeCompileProp]) { this[privateBeforeCompileProp](shaderInfo); } }; const DerivedMaterial = function DerivedMaterial() { return derive(options.chained ? baseMaterial : baseMaterial.clone()) }; const derive = function(base) { // Prototype chain to the base material const derived = Object.create(base, descriptor); // Store the baseMaterial for reference; this is always the original even when cloning Object.defineProperty(derived, 'baseMaterial', { value: baseMaterial }); // Needs its own ids Object.defineProperty(derived, 'id', { value: materialInstanceId++ }); derived.uuid = THREE.MathUtils.generateUUID(); // Merge uniforms, defines, and extensions derived.uniforms = assign({}, base.uniforms, options.uniforms); derived.defines = assign({}, base.defines, options.defines); derived.defines[`TROIKA_DERIVED_MATERIAL_${optionsKey}`] = ''; //force a program change from the base material derived.extensions = assign({}, base.extensions, options.extensions); // Don't inherit EventDispatcher listeners derived._listeners = undefined; return derived }; const descriptor = { constructor: {value: DerivedMaterial}, isDerivedMaterial: {value: true}, customProgramCacheKey: { value: function () { return optionsKey } }, onBeforeCompile: { get() { return onBeforeCompile }, set(fn) { this[privateBeforeCompileProp] = fn; } }, copy: { writable: true, configurable: true, value: function (source) { baseMaterial.copy.call(this, source); if (!baseMaterial.isShaderMaterial && !baseMaterial.isDerivedMaterial) { assign(this.extensions, source.extensions); assign(this.defines, source.defines); assign(this.uniforms, THREE.UniformsUtils.clone(source.uniforms)); } return this } }, clone: { writable: true, configurable: true, value: function () { const newBase = new baseMaterial.constructor(); return derive(newBase).copy(this) } }, /** * Utility to get a MeshDepthMaterial that will honor this derived material's vertex * transformations and discarded fragments. */ getDepthMaterial: { writable: true, configurable: true, value: function() { let depthMaterial = this._depthMaterial; if (!depthMaterial) { depthMaterial = this._depthMaterial = createDerivedMaterial( baseMaterial.isDerivedMaterial ? baseMaterial.getDepthMaterial() : new THREE.MeshDepthMaterial({ depthPacking: THREE.RGBADepthPacking }), options ); depthMaterial.defines.IS_DEPTH_MATERIAL = ''; depthMaterial.uniforms = this.uniforms; //automatically recieve same uniform values } return depthMaterial } }, /** * Utility to get a MeshDistanceMaterial that will honor this derived material's vertex * transformations and discarded fragments. */ getDistanceMaterial: { writable: true, configurable: true, value: function() { let distanceMaterial = this._distanceMaterial; if (!distanceMaterial) { distanceMaterial = this._distanceMaterial = createDerivedMaterial( baseMaterial.isDerivedMaterial ? baseMaterial.getDistanceMaterial() : new THREE.MeshDistanceMaterial(), options ); distanceMaterial.defines.IS_DISTANCE_MATERIAL = ''; distanceMaterial.uniforms = this.uniforms; //automatically recieve same uniform values } return distanceMaterial } }, dispose: { writable: true, configurable: true, value() { const {_depthMaterial, _distanceMaterial} = this; if (_depthMaterial) _depthMaterial.dispose(); if (_distanceMaterial) _distanceMaterial.dispose(); baseMaterial.dispose.call(this); } } }; ctorsByDerivation[optionsKey] = DerivedMaterial; return new DerivedMaterial() } function upgradeShaders({vertexShader, fragmentShader}, options, key) { let { vertexDefs, vertexMainIntro, vertexMainOutro, vertexTransform, fragmentDefs, fragmentMainIntro, fragmentMainOutro, fragmentColorTransform, customRewriter, timeUniform } = options; vertexDefs = vertexDefs || ''; vertexMainIntro = vertexMainIntro || ''; vertexMainOutro = vertexMainOutro || ''; fragmentDefs = fragmentDefs || ''; fragmentMainIntro = fragmentMainIntro || ''; fragmentMainOutro = fragmentMainOutro || ''; // Expand includes if needed if (vertexTransform || customRewriter) { vertexShader = expandShaderIncludes(vertexShader); } if (fragmentColorTransform || customRewriter) { // We need to be able to find postprocessing chunks after include expansion in order to // put them after the fragmentColorTransform, so mark them with comments first. Even if // this particular derivation doesn't have a fragmentColorTransform, other derivations may, // so we still mark them. fragmentShader = fragmentShader.replace( /^[ \t]*#include <((?:tonemapping|encodings|fog|premultiplied_alpha|dithering)_fragment)>/gm, '\n//!BEGIN_POST_CHUNK $1\n$&\n//!END_POST_CHUNK\n' ); fragmentShader = expandShaderIncludes(fragmentShader); } // Apply custom rewriter function if (customRewriter) { let res = customRewriter({vertexShader, fragmentShader}); vertexShader = res.vertexShader; fragmentShader = res.fragmentShader; } // The fragmentColorTransform needs to go before any postprocessing chunks, so extract // those and re-insert them into the outro in the correct place: if (fragmentColorTransform) { let postChunks = []; fragmentShader = fragmentShader.replace( /^\/\/!BEGIN_POST_CHUNK[^]+?^\/\/!END_POST_CHUNK/gm, // [^]+? = non-greedy match of any chars including newlines match => { postChunks.push(match); return '' } ); fragmentMainOutro = `${fragmentColorTransform}\n${postChunks.join('\n')}\n${fragmentMainOutro}`; } // Inject auto-updating time uniform if requested if (timeUniform) { const code = `\nuniform float ${timeUniform};\n`; vertexDefs = code + vertexDefs; fragmentDefs = code + fragmentDefs; } // Inject a function for the vertexTransform and rename all usages of position/normal/uv if (vertexTransform) { vertexDefs = `${vertexDefs} vec3 troika_position_${key}; vec3 troika_normal_${key}; vec2 troika_uv_${key}; void troikaVertexTransform${key}(inout vec3 position, inout vec3 normal, inout vec2 uv) { ${vertexTransform} } `; vertexMainIntro = ` troika_position_${key} = vec3(position); troika_normal_${key} = vec3(normal); troika_uv_${key} = vec2(uv); troikaVertexTransform${key}(troika_position_${key}, troika_normal_${key}, troika_uv_${key}); ${vertexMainIntro} `; vertexShader = vertexShader.replace(/\b(position|normal|uv)\b/g, (match, match1, index, fullStr) => { return /\battribute\s+vec[23]\s+$/.test(fullStr.substr(0, index)) ? match1 : `troika_${match1}_${key}` }); } // Inject defs and intro/outro snippets vertexShader = injectIntoShaderCode(vertexShader, key, vertexDefs, vertexMainIntro, vertexMainOutro); fragmentShader = injectIntoShaderCode(fragmentShader, key, fragmentDefs, fragmentMainIntro, fragmentMainOutro); return { vertexShader, fragmentShader } } function injectIntoShaderCode(shaderCode, id, defs, intro, outro) { if (intro || outro || defs) { shaderCode = shaderCode.replace(voidMainRegExp, ` ${defs} void troikaOrigMain${id}() {` ); shaderCode += ` void main() { ${intro} troikaOrigMain${id}(); ${outro} }`; } return shaderCode } function optionsJsonReplacer(key, value) { return key === 'uniforms' ? undefined : typeof value === 'function' ? value.toString() : value } let _idCtr = 0; const optionsHashesToIds = new Map(); function getKeyForOptions(options) { const optionsHash = JSON.stringify(options, optionsJsonReplacer); let id = optionsHashesToIds.get(optionsHash); if (id == null) { optionsHashesToIds.set(optionsHash, (id = ++_idCtr)); } return id } /** * Initializes and returns a function to generate an SDF texture for a given glyph. * @param {function} createGlyphSegmentsIndex - factory for a GlyphSegmentsIndex implementation. * @param {number} config.sdfExponent * @param {number} config.sdfMargin * * @return {function(Object): {renderingBounds: [minX, minY, maxX, maxY], textureData: Uint8Array}} */ function createSDFGenerator(createGlyphSegmentsIndex, config) { const { sdfExponent, sdfMargin } = config; /** * How many straight line segments to use when approximating a glyph's quadratic/cubic bezier curves. */ const CURVE_POINTS = 16; /** * Find the point on a quadratic bezier curve at t where t is in the range [0, 1] */ function pointOnQuadraticBezier(x0, y0, x1, y1, x2, y2, t) { const t2 = 1 - t; return { x: t2 * t2 * x0 + 2 * t2 * t * x1 + t * t * x2, y: t2 * t2 * y0 + 2 * t2 * t * y1 + t * t * y2 } } /** * Find the point on a cubic bezier curve at t where t is in the range [0, 1] */ function pointOnCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, t) { const t2 = 1 - t; return { x: t2 * t2 * t2 * x0 + 3 * t2 * t2 * t * x1 + 3 * t2 * t * t * x2 + t * t * t * x3, y: t2 * t2 * t2 * y0 + 3 * t2 * t2 * t * y1 + 3 * t2 * t * t * y2 + t * t * t * y3 } } /** * Generate an SDF texture segment for a single glyph. * @param {object} glyphObj * @param {number} sdfSize - the length of one side of the SDF image. * Larger images encode more details. Must be a power of 2. * @return {{textureData: Uint8Array, renderingBounds: *[]}} */ function generateSDF(glyphObj, sdfSize) { //console.time('glyphSDF') const textureData = new Uint8Array(sdfSize * sdfSize); // Determine mapping between glyph grid coords and sdf grid coords const glyphW = glyphObj.xMax - glyphObj.xMin; const glyphH = glyphObj.yMax - glyphObj.yMin; // Choose a maximum search distance radius in font units, based on the glyph's max dimensions const fontUnitsMaxSearchDist = Math.max(glyphW, glyphH); // Margin - add an extra 0.5 over the configured value because the outer 0.5 doesn't contain // useful interpolated values and will be ignored anyway. const fontUnitsMargin = Math.max(glyphW, glyphH) / sdfSize * (sdfMargin * sdfSize + 0.5); // Metrics of the texture/quad in font units const textureMinFontX = glyphObj.xMin - fontUnitsMargin; const textureMinFontY = glyphObj.yMin - fontUnitsMargin; const textureMaxFontX = glyphObj.xMax + fontUnitsMargin; const textureMaxFontY = glyphObj.yMax + fontUnitsMargin; const fontUnitsTextureWidth = textureMaxFontX - textureMinFontX; const fontUnitsTextureHeight = textureMaxFontY - textureMinFontY; const fontUnitsTextureMaxDim = Math.max(fontUnitsTextureWidth, fontUnitsTextureHeight); function textureXToFontX(x) { return textureMinFontX + fontUnitsTextureWidth * x / sdfSize } function textureYToFontY(y) { return textureMinFontY + fontUnitsTextureHeight * y / sdfSize } if (glyphObj.pathCommandCount) { //whitespace chars will have no commands, so we can skip all this // Decompose all paths into straight line segments and add them to a quadtree const lineSegmentsIndex = createGlyphSegmentsIndex(glyphObj); let firstX, firstY, prevX, prevY; glyphObj.forEachPathCommand((type, x0, y0, x1, y1, x2, y2) => { switch (type) { case 'M': prevX = firstX = x0; prevY = firstY = y0; break case 'L': if (x0 !== prevX || y0 !== prevY) { //yup, some fonts have zero-length line commands lineSegmentsIndex.addLineSegment(prevX, prevY, (prevX = x0), (prevY = y0)); } break case 'Q': { let prevPoint = {x: prevX, y: prevY}; for (let i = 1; i < CURVE_POINTS; i++) { let nextPoint = pointOnQuadraticBezier( prevX, prevY, x0, y0, x1, y1, i / (CURVE_POINTS - 1) ); lineSegmentsIndex.addLineSegment(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y); prevPoint = nextPoint; } prevX = x1; prevY = y1; break } case 'C': { let prevPoint = {x: prevX, y: prevY}; for (let i = 1; i < CURVE_POINTS; i++) { let nextPoint = pointOnCubicBezier( prevX, prevY, x0, y0, x1, y1, x2, y2, i / (CURVE_POINTS - 1) ); lineSegmentsIndex.addLineSegment(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y); prevPoint = nextPoint; } prevX = x2; prevY = y2; break } case 'Z': if (prevX !== firstX || prevY !== firstY) { lineSegmentsIndex.addLineSegment(prevX, prevY, firstX, firstY); } break } }); // For each target SDF texel, find the distance from its center to its nearest line segment, // map that distance to an alpha value, and write that alpha to the texel for (let sdfX = 0; sdfX < sdfSize; sdfX++) { for (let sdfY = 0; sdfY < sdfSize; sdfY++) { const signedDist = lineSegmentsIndex.findNearestSignedDistance( textureXToFontX(sdfX + 0.5), textureYToFontY(sdfY + 0.5), fontUnitsMaxSearchDist ); // Use an exponential scale to ensure the texels very near the glyph path have adequate // precision, while allowing the distance field to cover the entire texture, given that // there are only 8 bits available. Formula visualized: https://www.desmos.com/calculator/uiaq5aqiam let alpha = Math.pow((1 - Math.abs(signedDist) / fontUnitsTextureMaxDim), sdfExponent) / 2; if (signedDist < 0) { alpha = 1 - alpha; } alpha = Math.max(0, Math.min(255, Math.round(alpha * 255))); //clamp textureData[sdfY * sdfSize + sdfX] = alpha; } } } //console.timeEnd('glyphSDF') return { textureData: textureData, renderingBounds: [ textureMinFontX, textureMinFontY, textureMaxFontX, textureMaxFontY ] } } return generateSDF } /** * Creates a self-contained environment for processing text rendering requests. * * It is important that this function has no closure dependencies, so that it can be easily injected * into the source for a Worker without requiring a build step or complex dependency loading. All its * dependencies must be passed in at initialization. * * @param {function} fontParser - a function that accepts an ArrayBuffer of the font data and returns * a standardized structure giving access to the font and its glyphs: * { * unitsPerEm: number, * ascender: number, * descender: number, * forEachGlyph(string, fontSize, letterSpacing, callback) { * //invokes callback for each glyph to render, passing it an object: * callback({ * index: number, * advanceWidth: number, * xMin: number, * yMin: number, * xMax: number, * yMax: number, * pathCommandCount: number, * forEachPathCommand(callback) { * //invokes callback for each path command, with args: * callback( * type: 'M|L|C|Q|Z', * ...args //0 to 6 args depending on the type * ) * } * }) * } * } * @param {function} sdfGenerator - a function that accepts a glyph object and generates an SDF texture * from it. * @param {Object} config * @return {Object} */ function createFontProcessor(fontParser, sdfGenerator, config) { const { defaultFontURL } = config; /** * @private * Holds data about font glyphs and how they relate to SDF atlases * * { * 'fontUrl@sdfSize': { * fontObj: {}, //result of the fontParser * glyphs: { * [glyphIndex]: { * atlasIndex: 0, * glyphObj: {}, //glyph object from the fontParser * renderingBounds: [x0, y0, x1, y1] * }, * ... * }, * glyphCount: 123 * } * } */ const fontAtlases = Object.create(null); /** * Holds parsed font objects by url */ const fonts = Object.create(null); const INF = Infinity; /** * Load a given font url */ function doLoadFont(url, callback) { function tryLoad() { const onError = err => { console.error(`Failure loading font ${url}${url === defaultFontURL ? '' : '; trying fallback'}`, err); if (url !== defaultFontURL) { url = defaultFontURL; tryLoad(); } }; try { const request = new XMLHttpRequest(); request.open('get', url, true); request.responseType = 'arraybuffer'; request.onload = function () { if (request.status >= 400) { onError(new Error(request.statusText)); } else if (request.status > 0) { try { const fontObj = fontParser(request.response); callback(fontObj); } catch (e) { onError(e); } } }; request.onerror = onError; request.send(); } catch(err) { onError(err); } } tryLoad(); } /** * Load a given font url if needed, invoking a callback when it's loaded. If already * loaded, the callback will be called synchronously. */ function loadFont(fontUrl, callback) { if (!fontUrl) fontUrl = defaultFontURL; let font = fonts[fontUrl]; if (font) { // if currently loading font, add to callbacks, otherwise execute immediately if (font.pending) { font.pending.push(callback); } else { callback(font); } } else { fonts[fontUrl] = {pending: [callback]}; doLoadFont(fontUrl, fontObj => { let callbacks = fonts[fontUrl].pending; fonts[fontUrl] = fontObj; callbacks.forEach(cb => cb(fontObj)); }); } } /** * Get the atlas data for a given font url, loading it from the network and initializing * its atlas data objects if necessary. */ function getSdfAtlas(fontUrl, sdfGly