diff --git a/packages/troika-three-text/src/GlyphsGeometry.js b/packages/troika-three-text/src/GlyphsGeometry.js index 31be193e..f8026dea 100644 --- a/packages/troika-three-text/src/GlyphsGeometry.js +++ b/packages/troika-three-text/src/GlyphsGeometry.js @@ -1,10 +1,13 @@ import { + Float32BufferAttribute, + BufferGeometry, PlaneBufferGeometry, InstancedBufferGeometry, InstancedBufferAttribute, Sphere, Box3, - Vector3 + DoubleSide, + BackSide, } from 'three' const GlyphsGeometry = /*#__PURE__*/(() => { @@ -13,11 +16,32 @@ const GlyphsGeometry = /*#__PURE__*/(() => { function getTemplateGeometry(detail) { let geom = templateGeometries[detail] if (!geom) { - geom = templateGeometries[detail] = new PlaneBufferGeometry(1, 1, detail, detail).translate(0.5, 0.5, 0) + // 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 PlaneBufferGeometry(1, 1, detail, detail) + const back = front.clone() + const frontAttrs = front.attributes + const backAttrs = back.attributes + const combined = new 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 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 } return geom } - const tempVec3 = new Vector3() const glyphBoundsAttrName = 'aTroikaGlyphBounds' const glyphIndexAttrName = 'aTroikaGlyphIndex' @@ -80,6 +104,13 @@ const GlyphsGeometry = /*#__PURE__*/(() => { // 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 === BackSide ? verts / 2 : 0, side === DoubleSide ? verts : verts / 2) + } + set detail(detail) { if (detail !== this._detail) { this._detail = detail diff --git a/packages/troika-three-text/src/Text.js b/packages/troika-three-text/src/Text.js index 14136d16..5c91f922 100644 --- a/packages/troika-three-text/src/Text.js +++ b/packages/troika-three-text/src/Text.js @@ -1,6 +1,7 @@ import { Color, DoubleSide, + FrontSide, Matrix4, Mesh, MeshBasicMaterial, @@ -463,6 +464,22 @@ const Text = /*#__PURE__*/(() => { 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 = 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 + } } /**