From 11969bf82dd39582a2b06a58f66e769039f2ff21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 20 Jul 2026 21:02:57 +0200 Subject: [PATCH 1/2] [Experimental] [3D text] Upgrade to troika-three-text 0.52.4 --- extensions/community/Text3D.json | 1456 +++++++++++++++++++++++++----- 1 file changed, 1254 insertions(+), 202 deletions(-) diff --git a/extensions/community/Text3D.json b/extensions/community/Text3D.json index 9b90ab28c..89eb1a8ac 100644 --- a/extensions/community/Text3D.json +++ b/extensions/community/Text3D.json @@ -1,9 +1,10 @@ { "author": "", "category": "Text", + "dimension": "", "extensionNamespace": "", - "gdevelopVersion": ">=5.5.222", "fullName": "3D text", + "gdevelopVersion": ">=5.5.222", "helpPath": "", "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWZvcm1hdC10ZXh0IiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE4LjUsNEwxOS42Niw4LjM1TDE4LjcsOC42MUMxOC4yNSw3Ljc0IDE3Ljc5LDYuODcgMTcuMjYsNi40M0MxNi43Myw2IDE2LjExLDYgMTUuNSw2SDEzVjE2LjVDMTMsMTcgMTMsMTcuNSAxMy4zMywxNy43NUMxMy42NywxOCAxNC4zMywxOCAxNSwxOFYxOUg5VjE4QzkuNjcsMTggMTAuMzMsMTggMTAuNjcsMTcuNzVDMTEsMTcuNSAxMSwxNyAxMSwxNi41VjZIOC41QzcuODksNiA3LjI3LDYgNi43NCw2LjQzQzYuMjEsNi44NyA1Ljc1LDcuNzQgNS4zLDguNjFMNC4zNCw4LjM1TDUuNSw0SDE4LjVaIiAvPjwvc3ZnPg==", "name": "Text3D", @@ -11,6 +12,10 @@ "shortDescription": "Display texts in 3D scenes.", "version": "0.2.1", "description": "Display texts with 3D position and rotation.", + "origin": { + "identifier": "Text3D", + "name": "gdevelop-extension-store" + }, "tags": [ "3d" ], @@ -18,6 +23,8 @@ "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" ], "dependencies": [], + "globalVariables": [], + "sceneVariables": [], "eventsFunctions": [ { "description": "Define helper classes JavaScript code.", @@ -53,8 +60,25 @@ "}", "", "", - "// troika-THREE-text 0.49.0", + "// troika-three-text 0.52.4", "// https://github.com/protectwise/troika/tree/main/packages/troika-THREE-text", + "//", + "// To update run this command:", + "// npm i troika-three-text", + "//", + "// and copy the content of those files:", + "// - node_modules/troika-worker-utils/dist/troika-worker-utils.umd.js", + "// - node_modules/troika-three-text/dist/troika-three-text.umd.js", + "// - node_modules/webgl-sdf-generator/dist/webgl-sdf-generator.js", + "// - node_modules/bidi-js/dist/bidi.js", + "//", + "// Replace `three.` by `THREE.`", + "// Remove any occurrence of `troikaWorkerUtils.`", + "// Remove any occurrence of `troikaThreeUtils.`", + "// Replace `createSDFGenerator__default['default']` by `SDFGenerator`", + "// Replace `bidiFactory__default['default']` by `bidiFactory`", + "// Replace the 2 assign definitions with this:", + "// const assign = Object.assign;", "", " /**", " * Main content for the worker that handles the loading and execution of", @@ -242,8 +266,17 @@ " var init = options.init;", "", " // Resolve dependencies", - " dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) { return dep && dep._getInitResult ? dep._getInitResult() : dep; }", - " ) : [];", + " dependencies = Array.isArray(dependencies) ? dependencies.map(function (dep) {", + " if (dep) {", + " // If it's a worker module, use its main thread impl", + " dep = dep.onMainThread || dep;", + " // If it's a main thread worker module, use its init return value", + " if (dep._getInitResult) {", + " dep = dep._getInitResult();", + " }", + " }", + " return dep", + " }) : [];", "", " // Invoke init with the resolved dependencies", " var initPromise = Promise.all(dependencies).then(function (deps) {", @@ -316,9 +349,7 @@ " var getTransferables = options.getTransferables;", " var workerId = options.workerId;", "", - " if (!supportsWorkers()) {", - " return defineMainThreadModule(options)", - " }", + " var onMainThread = defineMainThreadModule(options);", "", " if (workerId == null) {", " workerId = '#default';", @@ -349,6 +380,10 @@ " var args = [], len = arguments.length;", " while ( len-- ) args[ len ] = arguments[ len ];", "", + " if (!supportsWorkers()) {", + " return onMainThread.apply(void 0, args)", + " }", + "", " // Register this module if needed", " if (!registrationPromise) {", " registrationPromise = callWorker(workerId,'registerModule', moduleFunc.workerModuleData);", @@ -378,6 +413,9 @@ " init: stringifyFunction(init),", " getTransferables: getTransferables && stringifyFunction(getTransferables)", " };", + "", + " moduleFunc.onMainThread = onMainThread;", + "", " return moduleFunc", " }", "", @@ -1058,13 +1096,13 @@ " // - this character is whitespace", " if (", " (prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||", - " /\\s/.test(text[i])", + " (i > 0 && /\\s/.test(text[i]))", " ) {", " charResolutions[i] = charResolutions[i - 1];", " if (prevCharResult === NEEDS_FALLBACK) {", " fallbackRanges[fallbackRanges.length - 1][1] = i;", " }", - " } else {", + " } else {", " for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {", " if (j === jLen) {", " // none of the user fonts matched; needs fallback", @@ -1435,7 +1473,7 @@ " baseline: -halfLeading - ascender * fontSizeMult, // baseline offset from top of line height", " // cap: -halfLeading - capHeight * fontSizeMult, // cap from top of line height", " // ex: -halfLeading - xHeight * fontSizeMult, // ex from top of line height", - " caretTop: (ascender + descender) / 2 * fontSizeMult + caretHeight / 2,", + " caretTop,", " caretBottom: caretTop - caretHeight", " };", " metricsByFont.set(fontObj, fontData);", @@ -1578,7 +1616,7 @@ " anchorY === 'top-ex' ? -lines[0].ex :", " anchorY === 'middle' ? totalHeight / 2 :", " anchorY === 'bottom' ? totalHeight :", - " anchorY === 'bottom-baseline' ? lines[lines.length - 1].baseline :", + " anchorY === 'bottom-baseline' ? -lines[lines.length - 1].baseline :", " parsePercent(anchorY) * totalHeight;", " }", " }", @@ -3879,6 +3917,7 @@ " sdfMargin: 1 / 16,", " sdfExponent: 9,", " textureWidth: 2048,", + " useWorker: true,", " };", " const tempColor = /*#__PURE__*/new THREE.Color();", " let hasRequested = false;", @@ -3917,6 +3956,7 @@ " * reasonably large number of glyphs (default glyph size of 64^2 and safe texture size of", " * 2048^2, times 4 channels, allows for 4096 glyphs.) This can be increased if you need to", " * increase the glyph size and/or have an extraordinary number of glyphs.", + " * @param {Boolean} config.useWorker - Whether to run typesetting in a web worker. Defaults to true.", " */", " function configureTextBuilder(config) {", " if (hasRequested) {", @@ -4055,7 +4095,8 @@ " const {sdfTexture, sdfCanvas} = atlas;", "", " // Issue request to the typesetting engine in the worker", - " typesetInWorker(args).then(result => {", + " const typeset = CONFIG.useWorker ? typesetInWorker : typesetOnMainThread;", + " typeset(args).then(result => {", " const {glyphIds, glyphFontIndices, fontData, glyphPositions, fontSize, timings} = result;", " const neededSDFs = [];", " const glyphBounds = new Float32Array(glyphIds.length * 4);", @@ -4261,16 +4302,8 @@ " getTextRenderInfo({ font, sdfGlyphSize, text }, callback);", " }", "", - "", " // Local assign impl so we don't have to import troika-core", - " function assign(toObj, fromObj) {", - " for (let key in fromObj) {", - " if (fromObj.hasOwnProperty(key)) {", - " toObj[key] = fromObj[key];", - " }", - " }", - " return toObj", - " }", + " const assign = Object.assign;", "", " // Utility for making URLs absolute", " let linkEl;", @@ -4342,6 +4375,8 @@ " }", " });", "", + " const typesetOnMainThread = typesetInWorker.onMainThread;", + "", " function dumpSDFTextures() {", " Object.keys(atlases).forEach(size => {", " const canvas = atlases[size].sdfCanvas;", @@ -4362,29 +4397,7 @@ " function getTemplateGeometry(detail) {", " let geom = templateGeometries[detail];", " if (!geom) {", - " // Geometry is two planes back-to-back, which will always be rendered FrontSide only but", - " // appear as DoubleSide by default. FrontSide/BackSide are emulated using drawRange.", - " // We do it this way to avoid the performance hit of two draw calls for DoubleSide materials", - " // introduced by Three.js in r130 - see https://github.com/mrdoob/THREE.js/pull/21967", - " const front = new THREE.PlaneGeometry(1, 1, detail, detail);", - " const back = front.clone();", - " const frontAttrs = front.attributes;", - " const backAttrs = back.attributes;", - " const combined = new THREE.BufferGeometry();", - " const vertCount = frontAttrs.uv.count;", - " for (let i = 0; i < vertCount; i++) {", - " backAttrs.position.array[i * 3] *= -1; // flip position x", - " backAttrs.normal.array[i * 3 + 2] *= -1; // flip normal z", - " }", - " ['position', 'normal', 'uv'].forEach(name => {", - " combined.setAttribute(name, new THREE.Float32BufferAttribute(", - " [...frontAttrs[name].array, ...backAttrs[name].array],", - " frontAttrs[name].itemSize)", - " );", - " });", - " combined.setIndex([...front.index.array, ...back.index.array.map(n => n + vertCount)]);", - " combined.translate(0.5, 0.5, 0);", - " geom = templateGeometries[detail] = combined;", + " geom = templateGeometries[detail] = new THREE.PlaneGeometry(1, 1, detail, detail).translate(0.5, 0.5, 0);", " }", " return geom", " }", @@ -4450,13 +4463,6 @@ " // No-op; we'll sync the boundingBox proactively when needed.", " }", "", - " // Since our base geometry contains triangles for both front and back sides, we can emulate", - " // the \"side\" by restricting the draw range.", - " setSide(side) {", - " const verts = this.getIndex().count;", - " this.setDrawRange(side === THREE.BackSide ? verts / 2 : 0, side === THREE.DoubleSide ? verts : verts / 2);", - " }", - "", " set detail(detail) {", " if (detail !== this._detail) {", " this._detail = detail;", @@ -4498,9 +4504,9 @@ " */", " updateGlyphs(glyphBounds, glyphAtlasIndices, blockBounds, chunkedBounds, glyphColors) {", " // Update the instance attributes", - " updateBufferAttr(this, glyphBoundsAttrName, glyphBounds, 4);", - " updateBufferAttr(this, glyphIndexAttrName, glyphAtlasIndices, 1);", - " updateBufferAttr(this, glyphColorAttrName, glyphColors, 3);", + " this.updateAttributeData(glyphBoundsAttrName, glyphBounds, 4);", + " this.updateAttributeData(glyphIndexAttrName, glyphAtlasIndices, 1);", + " this.updateAttributeData(glyphColorAttrName, glyphColors, 3);", " this._blockBounds = blockBounds;", " this._chunkedBounds = chunkedBounds;", " this.instanceCount = glyphAtlasIndices.length;", @@ -4562,29 +4568,31 @@ " }", " this.instanceCount = count;", " }", - " }", "", - "", - " function updateBufferAttr(geom, attrName, newArray, itemSize) {", - " const attr = geom.getAttribute(attrName);", - " if (newArray) {", - " // If length isn't changing, just update the attribute's array data", - " if (attr && attr.array.length === newArray.length) {", - " attr.array.set(newArray);", - " attr.needsUpdate = true;", - " } else {", - " geom.setAttribute(attrName, new THREE.InstancedBufferAttribute(newArray, itemSize));", - " // If the new attribute has a different size, we also have to (as of r117) manually clear the", - " // internal cached max instance count. See https://github.com/mrdoob/THREE.js/issues/19706", - " // It's unclear if this is a threejs bug or a truly unsupported scenario; discussion in", - " // that ticket is ambiguous as to whether replacing a BufferAttribute with one of a", - " // different size is supported, but https://github.com/mrdoob/THREE.js/pull/17418 strongly", - " // implies it should be supported. It's possible we need to", - " delete geom._maxInstanceCount; //for r117+, could be fragile", - " geom.dispose(); //for r118+, more robust feeling, but more heavy-handed than I'd like", + " /**", + " * Utility for updating instance attributes with automatic resizing", + " */", + " updateAttributeData(attrName, newArray, itemSize) {", + " const attr = this.getAttribute(attrName);", + " if (newArray) {", + " // If length isn't changing, just update the attribute's array data", + " if (attr && attr.array.length === newArray.length) {", + " attr.array.set(newArray);", + " attr.needsUpdate = true;", + " } else {", + " this.setAttribute(attrName, new THREE.InstancedBufferAttribute(newArray, itemSize));", + " // If the new attribute has a different size, we also have to (as of r117) manually clear the", + " // internal cached max instance count. See https://github.com/mrdoob/three.js/issues/19706", + " // It's unclear if this is a threejs bug or a truly unsupported scenario; discussion in", + " // that ticket is ambiguous as to whether replacing a BufferAttribute with one of a", + " // different size is supported, but https://github.com/mrdoob/three.js/pull/17418 strongly", + " // implies it should be supported. It's possible we need to", + " delete this._maxInstanceCount; //for r117+, could be fragile", + " this.dispose(); //for r118+, more robust feeling, but more heavy-handed than I'd like", + " }", + " } else if (attr) {", + " this.deleteAttribute(attrName);", " }", - " } else if (attr) {", - " geom.deleteAttribute(attrName);", " }", " }", "", @@ -4596,7 +4604,7 @@ "", " /**", " * Recursively expands all `#include ` statements within string of shader code.", - " * Copied from THREE's WebGLProgram#parseIncludes for external use.", + " * 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", @@ -4611,9 +4619,9 @@ " }", "", " /*", - " * This is a direct copy of MathUtils.generateUUID from Three.js, to preserve compatibility with THREE", + " * This is a direct copy of MathUtils.generateUUID from Three.js, to preserve compatibility with three", " * versions before 0.113.0 as it was changed from Math to MathUtils in that version.", - " * https://github.com/mrdoob/THREE.js/blob/dd8b5aa3b270c17096b90945cd2d6d1b13aaec53/src/math/MathUtils.js#L16", + " * https://github.com/mrdoob/three.js/blob/dd8b5aa3b270c17096b90945cd2d6d1b13aaec53/src/math/MathUtils.js#L16", " */", "", " const _lut = [];", @@ -4640,23 +4648,6 @@ "", " }", "", - " // Local assign polyfill to avoid importing troika-core", - " const assign$1 = 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 (Object.prototype.hasOwnProperty.call(source, prop)) {", - " target[prop] = source[prop];", - " }", - " }", - " }", - " }", - " return target", - " };", - "", - "", " const epoch = Date.now();", " const CONSTRUCTOR_CACHE = new WeakMap();", " const SHADER_UPGRADE_CACHE = new Map();", @@ -4673,41 +4664,42 @@ " * @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", + " * @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", + " * @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", + " * @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", + " * @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", + " * @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`,", + " * @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", + " * @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", + " * @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", + " * @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`", + " * @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", + " * @param {function({fragmentShader: string, vertexShader:string}):", + " * {fragmentShader: string, vertexShader:string}} 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", + " * @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", @@ -4760,7 +4752,7 @@ " // Inject upgraded shaders and uniforms into the program", " shaderInfo.vertexShader = upgradedShaders.vertexShader;", " shaderInfo.fragmentShader = upgradedShaders.fragmentShader;", - " assign$1(shaderInfo.uniforms, this.uniforms);", + " assign(shaderInfo.uniforms, this.uniforms);", "", " // Inject auto-updating time uniform if requested", " if (options.timeUniform) {", @@ -4791,10 +4783,10 @@ " derived.uuid = generateUUID();", "", " // Merge uniforms, defines, and extensions", - " derived.uniforms = assign$1({}, base.uniforms, options.uniforms);", - " derived.defines = assign$1({}, base.defines, options.defines);", + " 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$1({}, base.extensions, options.extensions);", + " derived.extensions = assign({}, base.extensions, options.extensions);", "", " // Don't inherit EventDispatcher listeners", " derived._listeners = undefined;", @@ -4806,6 +4798,20 @@ " constructor: {value: DerivedMaterial},", " isDerivedMaterial: {value: true},", "", + " type: {", + " get: () => baseMaterial.type,", + " set: (value) => {baseMaterial.type = value;}", + " },", + "", + " isDerivedFrom: {", + " writable: true,", + " configurable: true,", + " value: function (testMaterial) {", + " const base = this.baseMaterial;", + " return testMaterial === base || (base.isDerivedMaterial && base.isDerivedFrom(testMaterial)) || false", + " }", + " },", + "", " customProgramCacheKey: {", " writable: true,", " configurable: true,", @@ -4829,9 +4835,9 @@ " value: function (source) {", " baseMaterial.copy.call(this, source);", " if (!baseMaterial.isShaderMaterial && !baseMaterial.isDerivedMaterial) {", - " assign$1(this.extensions, source.extensions);", - " assign$1(this.defines, source.defines);", - " assign$1(this.uniforms, THREE.UniformsUtils.clone(source.uniforms));", + " assign(this.extensions, source.extensions);", + " assign(this.defines, source.defines);", + " assign(this.uniforms, THREE.UniformsUtils.clone(source.uniforms));", " }", " return this", " }", @@ -4940,7 +4946,7 @@ " // 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,", + " /^[ \\t]*#include <((?:tonemapping|encodings|colorspace|fog|premultiplied_alpha|dithering)_fragment)>/gm,", " '\\n//!BEGIN_POST_CHUNK $1\\n$&\\n//!END_POST_CHUNK\\n'", " );", " fragmentShader = expandShaderIncludes(fragmentShader);", @@ -5047,6 +5053,284 @@ " return id", " }", "", + " // Copied from threejs WebGLPrograms.js so we can resolve builtin materials to their shaders", + " // TODO how can we keep this from getting stale?", + " const MATERIAL_TYPES_TO_SHADERS = {", + " MeshDepthMaterial: 'depth',", + " MeshDistanceMaterial: 'distanceRGBA',", + " MeshNormalMaterial: 'normal',", + " MeshBasicMaterial: 'basic',", + " MeshLambertMaterial: 'lambert',", + " MeshPhongMaterial: 'phong',", + " MeshToonMaterial: 'toon',", + " MeshStandardMaterial: 'physical',", + " MeshPhysicalMaterial: 'physical',", + " MeshMatcapMaterial: 'matcap',", + " LineBasicMaterial: 'basic',", + " LineDashedMaterial: 'dashed',", + " PointsMaterial: 'points',", + " ShadowMaterial: 'shadow',", + " SpriteMaterial: 'sprite'", + " };", + "", + " /**", + " * Given a Three.js `Material` instance, find the shaders/uniforms that will be", + " * used to render that material.", + " *", + " * @param material - the Material instance", + " * @return {object} - the material's shader info: `{uniforms:{}, fragmentShader:'', vertexShader:''}`", + " */", + " function getShadersForMaterial(material) {", + " let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type];", + " return builtinType ? THREE.ShaderLib[builtinType] : material //TODO fallback for unknown type?", + " }", + "", + " /**", + " * Find all uniforms and their types within a shader code string.", + " *", + " * @param {string} shader - The shader code to parse", + " * @return {object} mapping of uniform names to their glsl type", + " */", + " function getShaderUniformTypes(shader) {", + " let uniformRE = /\\buniform\\s+(int|float|vec[234]|mat[34])\\s+([A-Za-z_][\\w]*)/g;", + " let uniforms = Object.create(null);", + " let match;", + " while ((match = uniformRE.exec(shader)) !== null) {", + " uniforms[match[2]] = match[1];", + " }", + " return uniforms", + " }", + "", + " /**", + " * Helper for smoothing out the `m.getInverse(x)` --> `m.copy(x).invert()` conversion", + " * that happened in ThreeJS r123.", + " * @param {Matrix4} srcMatrix", + " * @param {Matrix4} [tgtMatrix]", + " */", + " function invertMatrix4(srcMatrix, tgtMatrix = new THREE.Matrix4()) {", + " if (typeof tgtMatrix.invert === 'function') {", + " tgtMatrix.copy(srcMatrix).invert();", + " } else {", + " tgtMatrix.getInverse(srcMatrix);", + " }", + " return tgtMatrix", + " }", + "", + " /*", + " Input geometry is a cylinder with r=1, height in y dimension from 0 to 1,", + " divided into a reasonable number of height segments.", + " */", + "", + " const vertexDefs = `", + "uniform vec3 pointA;", + "uniform vec3 controlA;", + "uniform vec3 controlB;", + "uniform vec3 pointB;", + "uniform float radius;", + "varying float bezierT;", + "", + "vec3 cubicBezier(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {", + " float t2 = 1.0 - t;", + " float b0 = t2 * t2 * t2;", + " float b1 = 3.0 * t * t2 * t2;", + " float b2 = 3.0 * t * t * t2;", + " float b3 = t * t * t;", + " return b0 * p1 + b1 * c1 + b2 * c2 + b3 * p2;", + "}", + "", + "vec3 cubicBezierDerivative(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {", + " float t2 = 1.0 - t;", + " return -3.0 * p1 * t2 * t2 +", + " c1 * (3.0 * t2 * t2 - 6.0 * t2 * t) +", + " c2 * (6.0 * t2 * t - 3.0 * t * t) +", + " 3.0 * p2 * t * t;", + "}", + "`;", + "", + " const vertexTransform = `", + "float t = position.y;", + "bezierT = t;", + "vec3 bezierCenterPos = cubicBezier(pointA, controlA, controlB, pointB, t);", + "vec3 bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t));", + "", + "// Make \"sideways\" always perpendicular to the camera ray; this ensures that any twists", + "// in the cylinder occur where you won't see them: ", + "vec3 viewDirection = normalMatrix * vec3(0.0, 0.0, 1.0);", + "if (bezierDir == viewDirection) {", + " bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t == 1.0 ? t - 0.0001 : t + 0.0001));", + "}", + "vec3 sideways = normalize(cross(bezierDir, viewDirection));", + "vec3 upish = normalize(cross(sideways, bezierDir));", + "", + "// Build a matrix for transforming this disc in the cylinder:", + "mat4 discTx;", + "discTx[0].xyz = sideways * radius;", + "discTx[1].xyz = bezierDir * radius;", + "discTx[2].xyz = upish * radius;", + "discTx[3].xyz = bezierCenterPos;", + "discTx[3][3] = 1.0;", + "", + "// Apply transform, ignoring original y", + "position = (discTx * vec4(position.x, 0.0, position.z, 1.0)).xyz;", + "normal = normalize(mat3(discTx) * normal);", + "`;", + "", + " const fragmentDefs = `", + "uniform vec3 dashing;", + "varying float bezierT;", + "`;", + "", + " const fragmentMainIntro = `", + "if (dashing.x + dashing.y > 0.0) {", + " float dashFrac = mod(bezierT - dashing.z, dashing.x + dashing.y);", + " if (dashFrac > dashing.x) {", + " discard;", + " }", + "}", + "`;", + "", + " // Debugging: separate color for each of the 6 sides:", + " // const fragmentColorTransform = `", + " // float sideNum = floor(vUV.x * 6.0);", + " // vec3 mixColor = sideNum < 1.0 ? vec3(1.0, 0.0, 0.0) :", + " // sideNum < 2.0 ? vec3(0.0, 1.0, 1.0) :", + " // sideNum < 3.0 ? vec3(1.0, 1.0, 0.0) :", + " // sideNum < 4.0 ? vec3(0.0, 0.0, 1.0) :", + " // sideNum < 5.0 ? vec3(0.0, 1.0, 0.0) :", + " // vec3(1.0, 0.0, 1.0);", + " // gl_FragColor.xyz = mix(gl_FragColor.xyz, mixColor, 0.5);", + " // `", + "", + "", + "", + " function createBezierMeshMaterial(baseMaterial) {", + " return createDerivedMaterial(", + " baseMaterial,", + " {", + " chained: true,", + " uniforms: {", + " pointA: {value: new THREE.Vector3()},", + " controlA: {value: new THREE.Vector3()},", + " controlB: {value: new THREE.Vector3()},", + " pointB: {value: new THREE.Vector3()},", + " radius: {value: 0.01},", + " dashing: {value: new THREE.Vector3()} //on, off, offset", + " },", + " vertexDefs,", + " vertexTransform,", + " fragmentDefs,", + " fragmentMainIntro", + " }", + " )", + " }", + "", + " let geometry = null;", + "", + " const defaultBaseMaterial = /*#__PURE__*/new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide});", + "", + "", + " /**", + " * A ThreeJS `Mesh` that bends a tube shape along a 3D cubic bezier path. The bending is done", + " * by deforming a straight cylindrical geometry in the vertex shader based on a set of four", + " * control point uniforms. It patches the necessary GLSL into the mesh's assigned `material`", + " * automatically.", + " *", + " * The cubiz bezier path is determined by its four `Vector3` properties:", + " * - `pointA`", + " * - `controlA`", + " * - `controlB`", + " * - `pointB`", + " *", + " * The tube's radius is controlled by its `radius` property, which defaults to `0.01`.", + " *", + " * You can also give the tube a dashed appearance with two properties:", + " *", + " * - `dashArray` - an array of two numbers, defining the length of \"on\" and \"off\" parts of", + " * the dash. Each is a 0-1 ratio of the entire path's length. (Actually this is the `t` length", + " * used as input to the cubic bezier function, not its visible length.)", + " * - `dashOffset` - offset of where the dash starts. You can animate this to make the dashes move.", + " *", + " * Note that the dashes will appear like a hollow tube, not solid. This will be more apparent on", + " * thicker tubes.", + " *", + " * TODO: proper geometry bounding sphere and raycasting", + " * TODO: allow control of the geometry's segment counts", + " */", + " class BezierMesh extends THREE.Mesh {", + " static getGeometry() {", + " return geometry || (geometry =", + " new THREE.CylinderGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0)", + " )", + " }", + "", + " constructor() {", + " super(", + " BezierMesh.getGeometry(),", + " defaultBaseMaterial", + " );", + "", + " this.pointA = new THREE.Vector3();", + " this.controlA = new THREE.Vector3();", + " this.controlB = new THREE.Vector3();", + " this.pointB = new THREE.Vector3();", + " this.radius = 0.01;", + " this.dashArray = new THREE.Vector2();", + " this.dashOffset = 0;", + "", + " // TODO - disabling frustum culling until I figure out how to customize the", + " // geometry's bounding sphere that gets used", + " this.frustumCulled = false;", + " }", + "", + " // Handler for automatically wrapping the base material with our upgrades. We do the wrapping", + " // lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.", + " get material() {", + " let derivedMaterial = this._derivedMaterial;", + " const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultBaseMaterial.clone());", + " if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) {", + " derivedMaterial = this._derivedMaterial = createBezierMeshMaterial(baseMaterial);", + " // dispose the derived material when its base material is disposed:", + " baseMaterial.addEventListener('dispose', function onDispose() {", + " baseMaterial.removeEventListener('dispose', onDispose);", + " derivedMaterial.dispose();", + " });", + " }", + " return derivedMaterial", + " }", + " set material(baseMaterial) {", + " this._baseMaterial = baseMaterial;", + " }", + "", + " // Create and update material for shadows upon request:", + " get customDepthMaterial() {", + " return this.material.getDepthMaterial()", + " }", + " set customDepthMaterial(m) {", + " // future: let the user override with their own?", + " }", + " get customDistanceMaterial() {", + " return this.material.getDistanceMaterial()", + " }", + " set customDistanceMaterial(m) {", + " // future: let the user override with their own?", + " }", + "", + " onBeforeRender() {", + " const {uniforms} = this.material;", + " const {pointA, controlA, controlB, pointB, radius, dashArray, dashOffset} = this;", + " uniforms.pointA.value.copy(pointA);", + " uniforms.controlA.value.copy(controlA);", + " uniforms.controlB.value.copy(controlB);", + " uniforms.pointB.value.copy(pointB);", + " uniforms.radius.value = radius;", + " uniforms.dashing.value.set(dashArray.x, dashArray.y, dashOffset || 0);", + " }", + "", + " raycast(/*raycaster, intersects*/) {", + " // TODO - just fail for now", + " }", + " }", + "", " // language=GLSL", " const VERTEX_DEFS = `", "uniform vec2 uTroikaSDFTextureSize;", @@ -5055,7 +5339,7 @@ "uniform vec4 uTroikaClipRect;", "uniform mat3 uTroikaOrient;", "uniform bool uTroikaUseGlyphColors;", - "uniform float uTroikaDistanceOffset;", + "uniform float uTroikaEdgeOffset;", "uniform float uTroikaBlurRadius;", "uniform vec2 uTroikaPositionOffset;", "uniform float uTroikaCurveRadius;", @@ -5076,8 +5360,8 @@ "bounds.yw -= uTroikaPositionOffset.y;", "", "vec4 outlineBounds = vec4(", - " bounds.xy - uTroikaDistanceOffset - uTroikaBlurRadius,", - " bounds.zw + uTroikaDistanceOffset + uTroikaBlurRadius", + " bounds.xy - uTroikaEdgeOffset - uTroikaBlurRadius,", + " bounds.zw + uTroikaEdgeOffset + uTroikaBlurRadius", ");", "vec4 clippedBounds = vec4(", " clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw),", @@ -5123,9 +5407,8 @@ "uniform vec2 uTroikaSDFTextureSize;", "uniform float uTroikaSDFGlyphSize;", "uniform float uTroikaSDFExponent;", - "uniform float uTroikaDistanceOffset;", + "uniform float uTroikaEdgeOffset;", "uniform float uTroikaFillOpacity;", - "uniform float uTroikaOutlineOpacity;", "uniform float uTroikaBlurRadius;", "uniform vec3 uTroikaStrokeColor;", "uniform float uTroikaStrokeWidth;", @@ -5228,7 +5511,7 @@ "float fragDistance = troikaGetFragDistValue();", "float edgeAlpha = uTroikaSDFDebug ?", " troikaGlyphUvToSdfValue(vTroikaGlyphUV) :", - " troikaGetEdgeAlpha(fragDistance, uTroikaDistanceOffset, max(aaDist, uTroikaBlurRadius));", + " troikaGetEdgeAlpha(fragDistance, uTroikaEdgeOffset, max(aaDist, uTroikaBlurRadius));", "", "#if !defined(IS_DEPTH_MATERIAL) && !defined(IS_DISTANCE_MATERIAL)", "vec4 fillRGBA = gl_FragColor;", @@ -5265,8 +5548,7 @@ " uTroikaSDFExponent: {value: 0},", " uTroikaTotalBounds: {value: new THREE.Vector4(0,0,0,0)},", " uTroikaClipRect: {value: new THREE.Vector4(0,0,0,0)},", - " uTroikaDistanceOffset: {value: 0},", - " uTroikaOutlineOpacity: {value: 0},", + " uTroikaEdgeOffset: {value: 0},", " uTroikaFillOpacity: {value: 1},", " uTroikaPositionOffset: {value: new THREE.Vector2()},", " uTroikaCurveRadius: {value: 0},", @@ -5304,6 +5586,9 @@ " // Force transparency - TODO is this reasonable?", " textMaterial.transparent = true;", "", + " // Force single draw call when double-sided", + " textMaterial.forceSinglePass = true;", + "", " Object.defineProperties(textMaterial, {", " isTroikaTextMaterial: {value: true},", "", @@ -5796,22 +6081,6 @@ " if (material.isTroikaTextMaterial) {", " this._prepareForRender(material);", " }", - "", - " // We need to force the material to FrontSide to avoid the double-draw-call performance hit", - " // introduced in Three.js r130: https://github.com/mrdoob/THREE.js/pull/21967 - The sidedness", - " // is instead applied via drawRange in the GlyphsGeometry.", - " material._hadOwnSide = material.hasOwnProperty('side');", - " this.geometry.setSide(material._actualSide = material.side);", - " material.side = THREE.FrontSide;", - " }", - "", - " onAfterRender(renderer, scene, camera, geometry, material, group) {", - " // Restore original material side", - " if (material._hadOwnSide) {", - " material.side = material._actualSide;", - " } else {", - " delete material.side; // back to inheriting from base material", - " }", " }", "", " /**", @@ -5836,13 +6105,21 @@ " return this._textRenderInfo || null", " }", "", + " /**", + " * Create the text derived material from the base material. Can be overridden to use a custom", + " * derived material.", + " */", + " createDerivedMaterial(baseMaterial) {", + " return createTextDerivedMaterial(baseMaterial)", + " }", + "", " // Handler for automatically wrapping the base material with our upgrades. We do the wrapping", " // lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.", " get material() {", " let derivedMaterial = this._derivedMaterial;", " const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultMaterial.clone());", - " if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) {", - " derivedMaterial = this._derivedMaterial = createTextDerivedMaterial(baseMaterial);", + " if (!derivedMaterial || !derivedMaterial.isDerivedFrom(baseMaterial)) {", + " derivedMaterial = this._derivedMaterial = this.createDerivedMaterial(baseMaterial);", " // dispose the derived material when its base material is disposed:", " baseMaterial.addEventListener('dispose', function onDispose() {", " baseMaterial.removeEventListener('dispose', onDispose);", @@ -5853,7 +6130,7 @@ " // feature (see GlyphsGeometry which sets up `groups` for this purpose) Doing it with multi", " // materials ensures the layers are always rendered consecutively in a consistent order.", " // Each layer will trigger onBeforeRender with the appropriate material.", - " if (this.outlineWidth || this.outlineBlur || this.outlineOffsetX || this.outlineOffsetY) {", + " if (this.hasOutline()) {", " let outlineMaterial = derivedMaterial._outlineMtl;", " if (!outlineMaterial) {", " outlineMaterial = derivedMaterial._outlineMtl = Object.create(derivedMaterial, {", @@ -5884,6 +6161,10 @@ " }", " }", "", + " hasOutline() {", + " return !!(this.outlineWidth || this.outlineBlur || this.outlineOffsetX || this.outlineOffsetY)", + " }", + "", " get glyphGeometryDetail() {", " return this.geometry.detail", " }", @@ -5902,9 +6183,15 @@ " get customDepthMaterial() {", " return first(this.material).getDepthMaterial()", " }", + " set customDepthMaterial(m) {", + " // future: let the user override with their own?", + " }", " get customDistanceMaterial() {", " return first(this.material).getDistanceMaterial()", " }", + " set customDistanceMaterial(m) {", + " // future: let the user override with their own?", + " }", "", " _prepareForRender(material) {", " const isOutline = material.isTextOutlineMaterial;", @@ -5946,7 +6233,7 @@ " fillOpacity = this.fillOpacity;", " }", "", - " uniforms.uTroikaDistanceOffset.value = distanceOffset;", + " uniforms.uTroikaEdgeOffset.value = distanceOffset;", " uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY);", " uniforms.uTroikaBlurRadius.value = blurRadius;", " uniforms.uTroikaStrokeWidth.value = strokeWidth;", @@ -6102,6 +6389,491 @@ " });", " });", "", + " const syncStartEvent$1 = { type: \"syncstart\" };", + " const syncCompleteEvent$1 = { type: \"synccomplete\" };", + " const memberIndexAttrName = \"aTroikaTextBatchMemberIndex\";", + "", + "", + " /*", + " Data texture packing strategy:", + "", + " # Common:", + " 0-15: matrix", + " 16-19: uTroikaTotalBounds", + " 20-23: uTroikaClipRect", + " 24: diffuse (color/outlineColor)", + " 25: uTroikaFillOpacity (fillOpacity/outlineOpacity)", + " 26: uTroikaCurveRadius", + " 27: ", + "", + " # Main:", + " 28: uTroikaStrokeWidth", + " 29: uTroikaStrokeColor", + " 30: uTroikaStrokeOpacity", + "", + " # Outline:", + " 28-29: uTroikaPositionOffset", + " 30: uTroikaEdgeOffset", + " 31: uTroikaBlurRadius", + " */", + " const floatsPerMember = 32;", + "", + " const tempBox3 = new THREE.Box3();", + " const tempColor$1 = new THREE.Color();", + "", + " /**", + " * @experimental", + " *", + " * A specialized `Text` implementation that accepts any number of `Text` children", + " * and automatically batches them together to render in a single draw call.", + " *", + " * The `material` of each child `Text` will be ignored, and the `material` of the", + " * `BatchedText` will be used for all of them instead.", + " *", + " * NOTE: This only works in WebGL2 or where the OES_texture_float extension is available.", + " */", + " class BatchedText extends Text {", + " constructor () {", + " super();", + "", + " /**", + " * @typedef {Object} PackingInfo", + " * @property {number} index - the packing order index when last packed, or -1", + " * @property {boolean} dirty - whether it has synced since last pack", + " */", + "", + " /**", + " * @type {Map}", + " */", + " this._members = new Map();", + " this._dataTextures = {};", + "", + " this._onMemberSynced = (e) => {", + " this._members.get(e.target).dirty = true;", + " };", + " }", + "", + " /**", + " * @override", + " * Batch any Text objects added as children", + " */", + " add (...objects) {", + " for (let i = 0; i < objects.length; i++) {", + " if (objects[i] instanceof Text) {", + " this.addText(objects[i]);", + " } else {", + " super.add(objects[i]);", + " }", + " }", + " return this;", + " }", + "", + " /**", + " * @override", + " */", + " remove (...objects) {", + " for (let i = 0; i < objects.length; i++) {", + " if (objects[i] instanceof Text) {", + " this.removeText(objects[i]);", + " } else {", + " super.remove(objects[i]);", + " }", + " }", + " return this;", + " }", + "", + " /**", + " * @param {Text} text", + " */", + " addText (text) {", + " if (!this._members.has(text)) {", + " this._members.set(text, {", + " index: -1,", + " glyphCount: -1,", + " dirty: true", + " });", + " text.addEventListener(\"synccomplete\", this._onMemberSynced);", + " }", + " }", + "", + " /**", + " * @param {Text} text", + " */", + " removeText (text) {", + " this._needsRepack = true;", + " text.removeEventListener(\"synccomplete\", this._onMemberSynced);", + " this._members.delete(text);", + " }", + "", + " /**", + " * Use the custom derivation with extra batching logic", + " */", + " createDerivedMaterial (baseMaterial) {", + " return createBatchedTextMaterial(baseMaterial);", + " }", + "", + " updateMatrixWorld (force) {", + " super.updateMatrixWorld(force);", + " this.updateBounds();", + " }", + "", + " /**", + " * Update the batched geometry bounds to hold all members", + " */", + " updateBounds () {", + " // Update member local matrices and the overall bounds", + " const bbox = this.geometry.boundingBox.makeEmpty();", + " this._members.forEach((_, text) => {", + " if (text.matrixAutoUpdate) text.updateMatrix(); // ignore world matrix", + " tempBox3.copy(text.geometry.boundingBox).applyMatrix4(text.matrix);", + " bbox.union(tempBox3);", + " });", + " bbox.getBoundingSphere(this.geometry.boundingSphere);", + " }", + "", + " /** @override */", + " hasOutline() {", + " // Iterator.some() not supported in Safari", + " for (let member of this._members.keys()) {", + " if (member.hasOutline()) return true;", + " }", + " return false;", + " }", + "", + " /**", + " * @override", + " * Copy member matrices and uniform values into the data texture", + " */", + " _prepareForRender (material) {", + " const isOutline = material.isTextOutlineMaterial;", + " material.uniforms.uTroikaIsOutline.value = isOutline;", + "", + " // Resize the texture to fit in powers of 2", + " let texture = this._dataTextures[isOutline ? 'outline' : 'main'];", + " const dataLength = Math.pow(2, Math.ceil(Math.log2(this._members.size * floatsPerMember)));", + " if (!texture || dataLength !== texture.image.data.length) {", + " // console.log(`resizing: ${dataLength}`);", + " if (texture) texture.dispose();", + " const width = Math.min(dataLength / 4, 1024);", + " texture = this._dataTextures[isOutline ? 'outline' : 'main'] = new THREE.DataTexture(", + " new Float32Array(dataLength),", + " width,", + " dataLength / 4 / width,", + " THREE.RGBAFormat,", + " THREE.FloatType", + " );", + " }", + "", + " const texData = texture.image.data;", + " const setTexData = (index, value) => {", + " if (value !== texData[index]) {", + " texData[index] = value;", + " texture.needsUpdate = true;", + " }", + " };", + " this._members.forEach(({ index, dirty }, text) => {", + " if (index > -1) {", + " const startIndex = index * floatsPerMember;", + "", + " // Matrix", + " const matrix = text.matrix.elements;", + " for (let i = 0; i < 16; i++) {", + " setTexData(startIndex + i, matrix[i]);", + " }", + "", + " // Let the member populate the uniforms, since that does all the appropriate", + " // logic and handling of defaults, and we'll just grab the results from there", + " text._prepareForRender(material);", + " const {", + " uTroikaTotalBounds,", + " uTroikaClipRect,", + " uTroikaPositionOffset,", + " uTroikaEdgeOffset,", + " uTroikaBlurRadius,", + " uTroikaStrokeWidth,", + " uTroikaStrokeColor,", + " uTroikaStrokeOpacity,", + " uTroikaFillOpacity,", + " uTroikaCurveRadius,", + " } = material.uniforms;", + "", + " // Total bounds for uv", + " for (let i = 0; i < 4; i++) {", + " setTexData(startIndex + 16 + i, uTroikaTotalBounds.value.getComponent(i));", + " }", + "", + " // Clip rect", + " for (let i = 0; i < 4; i++) {", + " setTexData(startIndex + 20 + i, uTroikaClipRect.value.getComponent(i));", + " }", + "", + " // Color", + " let color = isOutline ? (text.outlineColor || 0) : text.color;", + " if (color == null) color = this.color;", + " if (color == null) color = this.material.color;", + " if (color == null) color = 0xffffff;", + " setTexData(startIndex + 24, tempColor$1.set(color).getHex());", + "", + " // Fill opacity / outline opacity", + " setTexData(startIndex + 25, uTroikaFillOpacity.value);", + "", + " // Curve radius", + " setTexData(startIndex + 26, uTroikaCurveRadius.value);", + "", + " if (isOutline) {", + " // Outline properties", + " setTexData(startIndex + 28, uTroikaPositionOffset.value.x);", + " setTexData(startIndex + 29, uTroikaPositionOffset.value.y);", + " setTexData(startIndex + 30, uTroikaEdgeOffset.value);", + " setTexData(startIndex + 31, uTroikaBlurRadius.value);", + " } else {", + " // Stroke properties", + " setTexData(startIndex + 28, uTroikaStrokeWidth.value);", + " setTexData(startIndex + 29, tempColor$1.set(uTroikaStrokeColor.value).getHex());", + " setTexData(startIndex + 30, uTroikaStrokeOpacity.value);", + " }", + " }", + " });", + " material.setMatrixTexture(texture);", + "", + " // For the non-member-specific uniforms:", + " super._prepareForRender(material);", + " }", + "", + " sync (callback) {", + " // TODO: skip members updating their geometries, just use textRenderInfo directly", + "", + " // Trigger sync on all members that need it", + " let syncPromises = this._needsRepack ? [] : null;", + " this._needsRepack = false;", + " this._members.forEach((packingInfo, text) => {", + " if (packingInfo.dirty || text._needsSync) {", + " packingInfo.dirty = false;", + " (syncPromises || (syncPromises = [])).push(new Promise(resolve => {", + " if (text._needsSync) {", + " text.sync(resolve);", + " } else {", + " resolve();", + " }", + " }));", + " }", + " });", + "", + " // If any needed syncing, wait for them and then repack the batched geometry", + " if (syncPromises) {", + " this.dispatchEvent(syncStartEvent$1);", + "", + " Promise.all(syncPromises).then(() => {", + " const { geometry } = this;", + " const batchedAttributes = geometry.attributes;", + " let memberIndexes = batchedAttributes[memberIndexAttrName] && batchedAttributes[memberIndexAttrName].array || new Uint16Array(0);", + " let batchedGlyphIndexes = batchedAttributes[glyphIndexAttrName] && batchedAttributes[glyphIndexAttrName].array || new Float32Array(0);", + " let batchedGlyphBounds = batchedAttributes[glyphBoundsAttrName] && batchedAttributes[glyphBoundsAttrName].array || new Float32Array(0);", + "", + " // Initial pass to collect total glyph count and resize the arrays if needed", + " let totalGlyphCount = 0;", + " this._members.forEach((packingInfo, { textRenderInfo }) => {", + " if (textRenderInfo) {", + " totalGlyphCount += textRenderInfo.glyphAtlasIndices.length;", + " this._textRenderInfo = textRenderInfo; // TODO - need this, but be smarter", + " }", + " });", + " if (totalGlyphCount !== memberIndexes.length) {", + " memberIndexes = cloneAndResize(memberIndexes, totalGlyphCount);", + " batchedGlyphIndexes = cloneAndResize(batchedGlyphIndexes, totalGlyphCount);", + " batchedGlyphBounds = cloneAndResize(batchedGlyphBounds, totalGlyphCount * 4);", + " }", + "", + " // Populate batch arrays", + " let memberIndex = 0;", + " let glyphIndex = 0;", + " this._members.forEach((packingInfo, { textRenderInfo }) => {", + " if (textRenderInfo) {", + " const glyphCount = textRenderInfo.glyphAtlasIndices.length;", + " memberIndexes.fill(memberIndex, glyphIndex, glyphIndex + glyphCount);", + "", + " // TODO can skip these for members that are not dirty or shifting overall position:", + " batchedGlyphIndexes.set(textRenderInfo.glyphAtlasIndices, glyphIndex, glyphIndex + glyphCount);", + " batchedGlyphBounds.set(textRenderInfo.glyphBounds, glyphIndex * 4, (glyphIndex + glyphCount) * 4);", + "", + " glyphIndex += glyphCount;", + " packingInfo.index = memberIndex++;", + " }", + " });", + "", + " // Update the geometry attributes", + " geometry.updateAttributeData(memberIndexAttrName, memberIndexes, 1);", + " geometry.getAttribute(memberIndexAttrName).setUsage(THREE.DynamicDrawUsage);", + " geometry.updateAttributeData(glyphIndexAttrName, batchedGlyphIndexes, 1);", + " geometry.updateAttributeData(glyphBoundsAttrName, batchedGlyphBounds, 4);", + "", + " this.updateBounds();", + "", + " this.dispatchEvent(syncCompleteEvent$1);", + " if (callback) {", + " callback();", + " }", + " });", + " }", + " }", + "", + " copy (source) {", + " if (source instanceof BatchedText) {", + " super.copy(source);", + " this._members.forEach((_, text) => this.removeText(text));", + " source._members.forEach((_, text) => this.addText(text));", + " }", + " return this;", + " }", + "", + " dispose () {", + " super.dispose();", + " Object.values(this._dataTextures).forEach(tex => tex.dispose());", + " }", + " }", + "", + " function cloneAndResize (source, newLength) {", + " const copy = new source.constructor(newLength);", + " copy.set(source.subarray(0, newLength));", + " return copy;", + " }", + "", + " function createBatchedTextMaterial (baseMaterial) {", + " const texUniformName = \"uTroikaMatricesTexture\";", + " const texSizeUniformName = \"uTroikaMatricesTextureSize\";", + "", + " // Due to how vertexTransform gets injected, the matrix transforms must happen", + " // in the base material of TextDerivedMaterial, but other transforms to its", + " // shader must come after, so we sandwich it between two derivations.", + "", + " // Transform the vertex position", + " let batchMaterial = createDerivedMaterial(baseMaterial, {", + " chained: true,", + " uniforms: {", + " [texSizeUniformName]: { value: new THREE.Vector2() },", + " [texUniformName]: { value: null }", + " },", + " // language=GLSL", + " vertexDefs: `", + " uniform highp sampler2D ${texUniformName};", + " uniform vec2 ${texSizeUniformName};", + " attribute float ${memberIndexAttrName};", + "", + " vec4 troikaBatchTexel(float offset) {", + " offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0;", + " float w = ${texSizeUniformName}.x;", + " vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName};", + " return texture2D(${texUniformName}, uv);", + " }", + " `,", + " // language=GLSL prefix=\"void main() {\" suffix=\"}\"", + " vertexTransform: `", + " mat4 matrix = mat4(", + " troikaBatchTexel(0.0),", + " troikaBatchTexel(1.0),", + " troikaBatchTexel(2.0),", + " troikaBatchTexel(3.0)", + " );", + " position.xyz = (matrix * vec4(position, 1.0)).xyz;", + " `,", + " });", + "", + " // Add the text shaders", + " batchMaterial = createTextDerivedMaterial(batchMaterial);", + "", + " // Now make other changes to the derived text shader code", + " batchMaterial = createDerivedMaterial(batchMaterial, {", + " chained: true,", + " uniforms: {", + " uTroikaIsOutline: {value: false},", + " },", + " customRewriter(shaders) {", + " // Convert some text shader uniforms to varyings", + " const varyingUniforms = [", + " 'uTroikaTotalBounds',", + " 'uTroikaClipRect',", + " 'uTroikaPositionOffset',", + " 'uTroikaEdgeOffset',", + " 'uTroikaBlurRadius',", + " 'uTroikaStrokeWidth',", + " 'uTroikaStrokeColor',", + " 'uTroikaStrokeOpacity',", + " 'uTroikaFillOpacity',", + " 'uTroikaCurveRadius',", + " 'diffuse'", + " ];", + " varyingUniforms.forEach(uniformName => {", + " shaders = uniformToVarying(shaders, uniformName);", + " });", + " return shaders", + " },", + " // language=GLSL", + " vertexDefs: `", + " uniform bool uTroikaIsOutline;", + " vec3 troikaFloatToColor(float v) {", + " return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;", + " }", + " `,", + " // language=GLSL prefix=\"void main() {\" suffix=\"}\"", + " vertexTransform: `", + " uTroikaTotalBounds = troikaBatchTexel(4.0);", + " uTroikaClipRect = troikaBatchTexel(5.0);", + " ", + " vec4 data = troikaBatchTexel(6.0);", + " diffuse = troikaFloatToColor(data.x);", + " uTroikaFillOpacity = data.y;", + " uTroikaCurveRadius = data.z;", + " ", + " data = troikaBatchTexel(7.0);", + " if (uTroikaIsOutline) {", + " if (data == vec4(0.0)) { // degenerate if zero outline", + " position = vec3(0.0);", + " } else {", + " uTroikaPositionOffset = data.xy;", + " uTroikaEdgeOffset = data.z;", + " uTroikaBlurRadius = data.w;", + " }", + " } else {", + " uTroikaStrokeWidth = data.x;", + " uTroikaStrokeColor = troikaFloatToColor(data.y);", + " uTroikaStrokeOpacity = data.z;", + " }", + " `,", + " });", + "", + " batchMaterial.setMatrixTexture = (texture) => {", + " batchMaterial.uniforms[texUniformName].value = texture;", + " batchMaterial.uniforms[texSizeUniformName].value.set(texture.image.width, texture.image.height);", + " };", + " return batchMaterial;", + " }", + "", + " /**", + " * Turn a uniform into a varying/writeable value.", + " * - If the uniform was used in the fragment shader, it will become a varying in both shaders.", + " * - If the uniform was only used in the vertex shader, it will become a writeable var.", + " */", + " function uniformToVarying({vertexShader, fragmentShader}, uniformName, varyingName = uniformName) {", + " const uniformRE = new RegExp(`uniform\\\\s+(bool|float|vec[234]|mat[34])\\\\s+${uniformName}\\\\b`);", + "", + " let type;", + " let hadFragmentUniform = false;", + " fragmentShader = fragmentShader.replace(uniformRE, ($0, $1) => {", + " hadFragmentUniform = true;", + " return `varying ${type = $1} ${varyingName}`", + " });", + "", + " let hadVertexUniform = false;", + " vertexShader = vertexShader.replace(uniformRE, (_, $1) => {", + " hadVertexUniform = true;", + " return `${hadFragmentUniform ? 'varying' : ''} ${type = $1} ${varyingName}`", + " });", + " if (!hadVertexUniform) {", + " vertexShader = `${hadFragmentUniform ? 'varying' : ''} ${type} ${varyingName};\\n${vertexShader}`;", + " }", + " return {vertexShader, fragmentShader}", + " }", + "", " //=== Utility functions for dealing with carets and selection ranges ===//", "", " /**", @@ -6653,15 +7425,153 @@ "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "DefineHelperClasses" + }, + { + "functionName": "TextColor" + }, + { + "functionName": "Font" + }, + { + "functionName": "SetFont" + }, + { + "functionName": "OutlineColor" + }, + { + "functionName": "OutlineThickness" + }, + { + "functionName": "ShadowDistance" + }, + { + "functionName": "ShadowAngle" + }, + { + "functionName": "ShadowBlurRadius" + }, + { + "functionName": "ShadowColor" + }, + { + "functionName": "ShadowOpacity" + }, + { + "functionName": "WrappingWidth" + } + ] + }, "eventsBasedBehaviors": [], "eventsBasedObjects": [ { + "areaMaxX": 64, + "areaMaxY": 64, + "areaMaxZ": 64, + "areaMinX": 0, + "areaMinY": 0, + "areaMinZ": 0, "defaultName": "Text", "description": "A text in 3D.", "fullName": "3D text", + "helpPath": "", + "iconUrl": "", "is3D": true, "isTextContainer": true, + "isUsingLegacyInstancesRenderer": true, "name": "Text3D", + "previewIconUrl": "", + "objects": [ + { + "assetStoreId": "", + "bold": false, + "italic": false, + "name": "Text", + "smoothed": true, + "type": "TextObject::Text", + "underlined": false, + "variables": [], + "effects": [], + "behaviors": [], + "string": "Text", + "font": "", + "textAlignment": "left", + "characterSize": 20, + "color": { + "b": 0, + "g": 0, + "r": 0 + }, + "content": { + "bold": false, + "isOutlineEnabled": false, + "isShadowEnabled": false, + "italic": false, + "outlineColor": "255;255;255", + "outlineThickness": 2, + "shadowAngle": 90, + "shadowBlurRadius": 2, + "shadowColor": "0;0;0", + "shadowDistance": 4, + "shadowOpacity": 127, + "smoothed": true, + "underlined": false, + "text": "Text", + "font": "", + "textAlignment": "left", + "verticalTextAlignment": "top", + "characterSize": 20, + "lineHeight": 0, + "color": "0;0;0" + } + } + ], + "objectsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "objectName": "Text" + } + ] + }, + "objectsGroups": [], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "camera2DPlaneMaxDrawingDistance": 5000, + "camera3DFarPlaneDistance": 10000, + "camera3DFieldOfView": 45, + "camera3DNearPlaneDistance": 3, + "cameraType": "", + "followBaseLayerCamera": false, + "isLightingLayer": false, + "isLocked": false, + "name": "", + "renderingType": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + } + ], + "instances": [], + "editionSettings": [], "eventsFunctions": [ { "fullName": "", @@ -7564,6 +8474,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "FontSize", + "group": "Style", "name": "SetFontSize", "sentence": "", "events": [ @@ -7810,6 +8721,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "WrappingWidth", + "group": "Style", "name": "SetWrappingWidth", "sentence": "", "events": [ @@ -7986,6 +8898,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "Color", + "group": "Style", "name": "SetColor", "sentence": "", "events": [ @@ -8069,6 +8982,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "Font", + "group": "Style", "name": "SetFont", "sentence": "", "events": [ @@ -8178,6 +9092,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "LineSpacing", + "group": "Style", "name": "SetLineSpacing", "sentence": "", "events": [ @@ -8424,6 +9339,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "OutlineThickness", + "group": "Outline", "name": "SetOutlineThickness", "sentence": "", "events": [ @@ -8587,6 +9503,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "OutlineColor", + "group": "Outline", "name": "SetOutlineColor", "sentence": "", "events": [ @@ -8868,6 +9785,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "ShadowColor", + "group": "Shadow", "name": "SetShadowColor", "sentence": "", "events": [ @@ -8968,6 +9886,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "ShadowDistance", + "group": "Shadow", "name": "SetShadowDistance", "sentence": "", "events": [ @@ -9065,6 +9984,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "ShadowAngle", + "group": "Shadow", "name": "SetShadowAngle", "sentence": "", "events": [ @@ -9255,6 +10175,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "ShadowBlurRadius", + "group": "Shadow", "name": "SetShadowBlurRadius", "sentence": "", "events": [ @@ -9419,6 +10340,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "ShadowOpacity", + "group": "Shadow", "name": "SetShadowOpacity", "sentence": "", "events": [ @@ -9910,6 +10832,7 @@ "fullName": "", "functionType": "ActionWithOperator", "getterName": "TextAlignment", + "group": "Style", "name": "SetTextAlignment", "sentence": "", "events": [ @@ -9977,16 +10900,188 @@ "objectGroups": [] } ], + "eventsFunctionsFolderStructure": { + "folderName": "__ROOT", + "children": [ + { + "functionName": "onCreated" + }, + { + "functionName": "doStepPostEvents" + }, + { + "functionName": "UpdateText" + }, + { + "functionName": "IsTextUpToDate" + }, + { + "functionName": "UpdateGeometry" + }, + { + "functionName": "HasGeometryUpdated" + }, + { + "functionName": "onHotReloading" + }, + { + "functionName": "UpdateFromProperties" + }, + { + "functionName": "TextWidth" + }, + { + "functionName": "TextHeight" + }, + { + "folderName": "Style", + "children": [ + { + "functionName": "FontSize" + }, + { + "functionName": "SetFontSize" + }, + { + "functionName": "IsWrapping" + }, + { + "functionName": "SetWrapping" + }, + { + "functionName": "WrappingWidth" + }, + { + "functionName": "SetWrappingWidth" + }, + { + "functionName": "Color" + }, + { + "functionName": "SetColor" + }, + { + "functionName": "Font" + }, + { + "functionName": "SetFont" + }, + { + "functionName": "LineSpacing" + }, + { + "functionName": "SetLineSpacing" + }, + { + "functionName": "IsBold" + }, + { + "functionName": "SetBold" + }, + { + "functionName": "IsItalic" + }, + { + "functionName": "SetItalic" + }, + { + "functionName": "TextAlignment" + }, + { + "functionName": "SetTextAlignment" + } + ] + }, + { + "functionName": "UpdateWrappingWidth" + }, + { + "folderName": "Outline", + "children": [ + { + "functionName": "IsOutlineEnabled" + }, + { + "functionName": "SetOutlineEnabled" + }, + { + "functionName": "OutlineThickness" + }, + { + "functionName": "SetOutlineThickness" + }, + { + "functionName": "OutlineColor" + }, + { + "functionName": "SetOutlineColor" + } + ] + }, + { + "functionName": "UpdateOutlineThickness" + }, + { + "folderName": "Shadow", + "children": [ + { + "functionName": "IsShadowEnabled" + }, + { + "functionName": "SetShadowEnabled" + }, + { + "functionName": "ShadowColor" + }, + { + "functionName": "SetShadowColor" + }, + { + "functionName": "ShadowDistance" + }, + { + "functionName": "SetShadowDistance" + }, + { + "functionName": "ShadowAngle" + }, + { + "functionName": "SetShadowAngle" + }, + { + "functionName": "ShadowBlurRadius" + }, + { + "functionName": "SetShadowBlurRadius" + }, + { + "functionName": "ShadowOpacity" + }, + { + "functionName": "SetShadowOpacity" + } + ] + }, + { + "functionName": "UpdateShadowOffset" + }, + { + "functionName": "UpdateShadowBlurRadius" + }, + { + "functionName": "UpdateShadowOpacity" + } + ] + }, "propertyDescriptors": [ { "value": "Top", "type": "String", "label": "", - "description": "", - "group": "", "extraInformation": [ "Text" ], + "choices": [], "hidden": true, "name": "TextVerticalAnchorOrigin" }, @@ -9994,9 +11089,6 @@ "value": "Top", "type": "String", "label": "", - "description": "", - "group": "", - "extraInformation": [], "hidden": true, "name": "TextVerticalAnchorTarget" }, @@ -10005,73 +11097,33 @@ "type": "Number", "unit": "Dimensionless", "label": "Line spacing", - "description": "", - "group": "", - "extraInformation": [], "name": "LineSpacing" }, { "value": "false", "type": "Boolean", "label": "", - "description": "", - "group": "", - "extraInformation": [], "hidden": true, "name": "IsGeometryUpToDate" } ], - "objects": [ - { - "assetStoreId": "", - "bold": false, - "italic": false, - "name": "Text", - "smoothed": true, - "type": "TextObject::Text", - "underlined": false, - "variables": [], - "effects": [], - "behaviors": [], - "string": "Text", - "font": "", - "textAlignment": "left", - "characterSize": 20, - "color": { - "b": 0, - "g": 0, - "r": 0 - }, - "content": { - "bold": false, - "isOutlineEnabled": false, - "isShadowEnabled": false, - "italic": false, - "outlineColor": "255;255;255", - "outlineThickness": 2, - "shadowAngle": 90, - "shadowBlurRadius": 2, - "shadowColor": "0;0;0", - "shadowDistance": 4, - "shadowOpacity": 127, - "smoothed": true, - "underlined": false, - "text": "Text", - "font": "", - "textAlignment": "left", - "characterSize": 20, - "color": "0;0;0" - } - } - ], - "objectsFolderStructure": { + "propertiesFolderStructure": { "folderName": "__ROOT", "children": [ { - "objectName": "Text" + "propertyName": "TextVerticalAnchorOrigin" + }, + { + "propertyName": "TextVerticalAnchorTarget" + }, + { + "propertyName": "LineSpacing" + }, + { + "propertyName": "IsGeometryUpToDate" } ] } } ] -} \ No newline at end of file +} From 5ea317dd44c40d713ccacf101503b9c13ff6c3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 21 Jul 2026 11:49:18 +0200 Subject: [PATCH 2/2] Rebuild the lib --- extensions/community/Text3D.json | 345 ++++--------------------------- 1 file changed, 42 insertions(+), 303 deletions(-) diff --git a/extensions/community/Text3D.json b/extensions/community/Text3D.json index 89eb1a8ac..bf06c52d6 100644 --- a/extensions/community/Text3D.json +++ b/extensions/community/Text3D.json @@ -10,7 +10,7 @@ "name": "Text3D", "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/0b4ef00ba832f327e84d09b036d18f3fc8d4805e3020f95b034a23f558e17a6c_format-text.svg", "shortDescription": "Display texts in 3D scenes.", - "version": "0.2.1", + "version": "0.3.0", "description": "Display texts with 3D position and rotation.", "origin": { "identifier": "Text3D", @@ -63,22 +63,14 @@ "// troika-three-text 0.52.4", "// https://github.com/protectwise/troika/tree/main/packages/troika-THREE-text", "//", - "// To update run this command:", - "// npm i troika-three-text", - "//", - "// and copy the content of those files:", - "// - node_modules/troika-worker-utils/dist/troika-worker-utils.umd.js", - "// - node_modules/troika-three-text/dist/troika-three-text.umd.js", - "// - node_modules/webgl-sdf-generator/dist/webgl-sdf-generator.js", - "// - node_modules/bidi-js/dist/bidi.js", - "//", - "// Replace `three.` by `THREE.`", - "// Remove any occurrence of `troikaWorkerUtils.`", - "// Remove any occurrence of `troikaThreeUtils.`", - "// Replace `createSDFGenerator__default['default']` by `SDFGenerator`", - "// Replace `bidiFactory__default['default']` by `bidiFactory`", - "// Replace the 2 assign definitions with this:", - "// const assign = Object.assign;", + "// To update this lib:", + "// - Clone https://github.com/D8H/troika", + "// - Run: npm install", + "// - Run: npm start build", + "// - Checkout the branch \"build-extension\"", + "// - Run: npm start build", + "// - Copy the content of dist/troika-three-text.umd.js above", + "// - Replace `three.` by `THREE.`", "", " /**", " * Main content for the worker that handles the loading and execution of", @@ -4302,8 +4294,16 @@ " getTextRenderInfo({ font, sdfGlyphSize, text }, callback);", " }", "", + "", " // Local assign impl so we don't have to import troika-core", - " const assign = Object.assign;", + " function assign(toObj, fromObj) {", + " for (let key in fromObj) {", + " if (fromObj.hasOwnProperty(key)) {", + " toObj[key] = fromObj[key];", + " }", + " }", + " return toObj", + " }", "", " // Utility for making URLs absolute", " let linkEl;", @@ -4648,6 +4648,23 @@ "", " }", "", + " // Local assign polyfill to avoid importing troika-core", + " const assign$1 = 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 (Object.prototype.hasOwnProperty.call(source, prop)) {", + " target[prop] = source[prop];", + " }", + " }", + " }", + " }", + " return target", + " };", + "", + "", " const epoch = Date.now();", " const CONSTRUCTOR_CACHE = new WeakMap();", " const SHADER_UPGRADE_CACHE = new Map();", @@ -4752,7 +4769,7 @@ " // Inject upgraded shaders and uniforms into the program", " shaderInfo.vertexShader = upgradedShaders.vertexShader;", " shaderInfo.fragmentShader = upgradedShaders.fragmentShader;", - " assign(shaderInfo.uniforms, this.uniforms);", + " assign$1(shaderInfo.uniforms, this.uniforms);", "", " // Inject auto-updating time uniform if requested", " if (options.timeUniform) {", @@ -4783,10 +4800,10 @@ " derived.uuid = generateUUID();", "", " // Merge uniforms, defines, and extensions", - " derived.uniforms = assign({}, base.uniforms, options.uniforms);", - " derived.defines = assign({}, base.defines, options.defines);", + " derived.uniforms = assign$1({}, base.uniforms, options.uniforms);", + " derived.defines = assign$1({}, 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);", + " derived.extensions = assign$1({}, base.extensions, options.extensions);", "", " // Don't inherit EventDispatcher listeners", " derived._listeners = undefined;", @@ -4835,9 +4852,9 @@ " 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));", + " assign$1(this.extensions, source.extensions);", + " assign$1(this.defines, source.defines);", + " assign$1(this.uniforms, THREE.UniformsUtils.clone(source.uniforms));", " }", " return this", " }", @@ -5053,284 +5070,6 @@ " return id", " }", "", - " // Copied from threejs WebGLPrograms.js so we can resolve builtin materials to their shaders", - " // TODO how can we keep this from getting stale?", - " const MATERIAL_TYPES_TO_SHADERS = {", - " MeshDepthMaterial: 'depth',", - " MeshDistanceMaterial: 'distanceRGBA',", - " MeshNormalMaterial: 'normal',", - " MeshBasicMaterial: 'basic',", - " MeshLambertMaterial: 'lambert',", - " MeshPhongMaterial: 'phong',", - " MeshToonMaterial: 'toon',", - " MeshStandardMaterial: 'physical',", - " MeshPhysicalMaterial: 'physical',", - " MeshMatcapMaterial: 'matcap',", - " LineBasicMaterial: 'basic',", - " LineDashedMaterial: 'dashed',", - " PointsMaterial: 'points',", - " ShadowMaterial: 'shadow',", - " SpriteMaterial: 'sprite'", - " };", - "", - " /**", - " * Given a Three.js `Material` instance, find the shaders/uniforms that will be", - " * used to render that material.", - " *", - " * @param material - the Material instance", - " * @return {object} - the material's shader info: `{uniforms:{}, fragmentShader:'', vertexShader:''}`", - " */", - " function getShadersForMaterial(material) {", - " let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type];", - " return builtinType ? THREE.ShaderLib[builtinType] : material //TODO fallback for unknown type?", - " }", - "", - " /**", - " * Find all uniforms and their types within a shader code string.", - " *", - " * @param {string} shader - The shader code to parse", - " * @return {object} mapping of uniform names to their glsl type", - " */", - " function getShaderUniformTypes(shader) {", - " let uniformRE = /\\buniform\\s+(int|float|vec[234]|mat[34])\\s+([A-Za-z_][\\w]*)/g;", - " let uniforms = Object.create(null);", - " let match;", - " while ((match = uniformRE.exec(shader)) !== null) {", - " uniforms[match[2]] = match[1];", - " }", - " return uniforms", - " }", - "", - " /**", - " * Helper for smoothing out the `m.getInverse(x)` --> `m.copy(x).invert()` conversion", - " * that happened in ThreeJS r123.", - " * @param {Matrix4} srcMatrix", - " * @param {Matrix4} [tgtMatrix]", - " */", - " function invertMatrix4(srcMatrix, tgtMatrix = new THREE.Matrix4()) {", - " if (typeof tgtMatrix.invert === 'function') {", - " tgtMatrix.copy(srcMatrix).invert();", - " } else {", - " tgtMatrix.getInverse(srcMatrix);", - " }", - " return tgtMatrix", - " }", - "", - " /*", - " Input geometry is a cylinder with r=1, height in y dimension from 0 to 1,", - " divided into a reasonable number of height segments.", - " */", - "", - " const vertexDefs = `", - "uniform vec3 pointA;", - "uniform vec3 controlA;", - "uniform vec3 controlB;", - "uniform vec3 pointB;", - "uniform float radius;", - "varying float bezierT;", - "", - "vec3 cubicBezier(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {", - " float t2 = 1.0 - t;", - " float b0 = t2 * t2 * t2;", - " float b1 = 3.0 * t * t2 * t2;", - " float b2 = 3.0 * t * t * t2;", - " float b3 = t * t * t;", - " return b0 * p1 + b1 * c1 + b2 * c2 + b3 * p2;", - "}", - "", - "vec3 cubicBezierDerivative(vec3 p1, vec3 c1, vec3 c2, vec3 p2, float t) {", - " float t2 = 1.0 - t;", - " return -3.0 * p1 * t2 * t2 +", - " c1 * (3.0 * t2 * t2 - 6.0 * t2 * t) +", - " c2 * (6.0 * t2 * t - 3.0 * t * t) +", - " 3.0 * p2 * t * t;", - "}", - "`;", - "", - " const vertexTransform = `", - "float t = position.y;", - "bezierT = t;", - "vec3 bezierCenterPos = cubicBezier(pointA, controlA, controlB, pointB, t);", - "vec3 bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t));", - "", - "// Make \"sideways\" always perpendicular to the camera ray; this ensures that any twists", - "// in the cylinder occur where you won't see them: ", - "vec3 viewDirection = normalMatrix * vec3(0.0, 0.0, 1.0);", - "if (bezierDir == viewDirection) {", - " bezierDir = normalize(cubicBezierDerivative(pointA, controlA, controlB, pointB, t == 1.0 ? t - 0.0001 : t + 0.0001));", - "}", - "vec3 sideways = normalize(cross(bezierDir, viewDirection));", - "vec3 upish = normalize(cross(sideways, bezierDir));", - "", - "// Build a matrix for transforming this disc in the cylinder:", - "mat4 discTx;", - "discTx[0].xyz = sideways * radius;", - "discTx[1].xyz = bezierDir * radius;", - "discTx[2].xyz = upish * radius;", - "discTx[3].xyz = bezierCenterPos;", - "discTx[3][3] = 1.0;", - "", - "// Apply transform, ignoring original y", - "position = (discTx * vec4(position.x, 0.0, position.z, 1.0)).xyz;", - "normal = normalize(mat3(discTx) * normal);", - "`;", - "", - " const fragmentDefs = `", - "uniform vec3 dashing;", - "varying float bezierT;", - "`;", - "", - " const fragmentMainIntro = `", - "if (dashing.x + dashing.y > 0.0) {", - " float dashFrac = mod(bezierT - dashing.z, dashing.x + dashing.y);", - " if (dashFrac > dashing.x) {", - " discard;", - " }", - "}", - "`;", - "", - " // Debugging: separate color for each of the 6 sides:", - " // const fragmentColorTransform = `", - " // float sideNum = floor(vUV.x * 6.0);", - " // vec3 mixColor = sideNum < 1.0 ? vec3(1.0, 0.0, 0.0) :", - " // sideNum < 2.0 ? vec3(0.0, 1.0, 1.0) :", - " // sideNum < 3.0 ? vec3(1.0, 1.0, 0.0) :", - " // sideNum < 4.0 ? vec3(0.0, 0.0, 1.0) :", - " // sideNum < 5.0 ? vec3(0.0, 1.0, 0.0) :", - " // vec3(1.0, 0.0, 1.0);", - " // gl_FragColor.xyz = mix(gl_FragColor.xyz, mixColor, 0.5);", - " // `", - "", - "", - "", - " function createBezierMeshMaterial(baseMaterial) {", - " return createDerivedMaterial(", - " baseMaterial,", - " {", - " chained: true,", - " uniforms: {", - " pointA: {value: new THREE.Vector3()},", - " controlA: {value: new THREE.Vector3()},", - " controlB: {value: new THREE.Vector3()},", - " pointB: {value: new THREE.Vector3()},", - " radius: {value: 0.01},", - " dashing: {value: new THREE.Vector3()} //on, off, offset", - " },", - " vertexDefs,", - " vertexTransform,", - " fragmentDefs,", - " fragmentMainIntro", - " }", - " )", - " }", - "", - " let geometry = null;", - "", - " const defaultBaseMaterial = /*#__PURE__*/new THREE.MeshStandardMaterial({color: 0xffffff, side: THREE.DoubleSide});", - "", - "", - " /**", - " * A ThreeJS `Mesh` that bends a tube shape along a 3D cubic bezier path. The bending is done", - " * by deforming a straight cylindrical geometry in the vertex shader based on a set of four", - " * control point uniforms. It patches the necessary GLSL into the mesh's assigned `material`", - " * automatically.", - " *", - " * The cubiz bezier path is determined by its four `Vector3` properties:", - " * - `pointA`", - " * - `controlA`", - " * - `controlB`", - " * - `pointB`", - " *", - " * The tube's radius is controlled by its `radius` property, which defaults to `0.01`.", - " *", - " * You can also give the tube a dashed appearance with two properties:", - " *", - " * - `dashArray` - an array of two numbers, defining the length of \"on\" and \"off\" parts of", - " * the dash. Each is a 0-1 ratio of the entire path's length. (Actually this is the `t` length", - " * used as input to the cubic bezier function, not its visible length.)", - " * - `dashOffset` - offset of where the dash starts. You can animate this to make the dashes move.", - " *", - " * Note that the dashes will appear like a hollow tube, not solid. This will be more apparent on", - " * thicker tubes.", - " *", - " * TODO: proper geometry bounding sphere and raycasting", - " * TODO: allow control of the geometry's segment counts", - " */", - " class BezierMesh extends THREE.Mesh {", - " static getGeometry() {", - " return geometry || (geometry =", - " new THREE.CylinderGeometry(1, 1, 1, 6, 64).translate(0, 0.5, 0)", - " )", - " }", - "", - " constructor() {", - " super(", - " BezierMesh.getGeometry(),", - " defaultBaseMaterial", - " );", - "", - " this.pointA = new THREE.Vector3();", - " this.controlA = new THREE.Vector3();", - " this.controlB = new THREE.Vector3();", - " this.pointB = new THREE.Vector3();", - " this.radius = 0.01;", - " this.dashArray = new THREE.Vector2();", - " this.dashOffset = 0;", - "", - " // TODO - disabling frustum culling until I figure out how to customize the", - " // geometry's bounding sphere that gets used", - " this.frustumCulled = false;", - " }", - "", - " // Handler for automatically wrapping the base material with our upgrades. We do the wrapping", - " // lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.", - " get material() {", - " let derivedMaterial = this._derivedMaterial;", - " const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultBaseMaterial.clone());", - " if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial) {", - " derivedMaterial = this._derivedMaterial = createBezierMeshMaterial(baseMaterial);", - " // dispose the derived material when its base material is disposed:", - " baseMaterial.addEventListener('dispose', function onDispose() {", - " baseMaterial.removeEventListener('dispose', onDispose);", - " derivedMaterial.dispose();", - " });", - " }", - " return derivedMaterial", - " }", - " set material(baseMaterial) {", - " this._baseMaterial = baseMaterial;", - " }", - "", - " // Create and update material for shadows upon request:", - " get customDepthMaterial() {", - " return this.material.getDepthMaterial()", - " }", - " set customDepthMaterial(m) {", - " // future: let the user override with their own?", - " }", - " get customDistanceMaterial() {", - " return this.material.getDistanceMaterial()", - " }", - " set customDistanceMaterial(m) {", - " // future: let the user override with their own?", - " }", - "", - " onBeforeRender() {", - " const {uniforms} = this.material;", - " const {pointA, controlA, controlB, pointB, radius, dashArray, dashOffset} = this;", - " uniforms.pointA.value.copy(pointA);", - " uniforms.controlA.value.copy(controlA);", - " uniforms.controlB.value.copy(controlB);", - " uniforms.pointB.value.copy(pointB);", - " uniforms.radius.value = radius;", - " uniforms.dashing.value.set(dashArray.x, dashArray.y, dashOffset || 0);", - " }", - "", - " raycast(/*raycaster, intersects*/) {", - " // TODO - just fail for now", - " }", - " }", - "", " // language=GLSL", " const VERTEX_DEFS = `", "uniform vec2 uTroikaSDFTextureSize;",