如何在Three.js中居中THREE.Points文本网格?
解决文本网格居中显示问题
问题背景
通过SVG路径创建顶点制作文本网格时,无论文本长度如何,字母的起始位置始终固定,无法居中显示;长文本还可能超出屏幕范围,需要实现文本始终居中且完整呈现的效果。
问题分析
当前代码中,文本从SVG的x=0位置开始绘制,再通过固定的svgViewBoxWidth/2偏移尝试居中,但这个固定值无法适配不同长度的文本——短文本会偏左,长文本可能超出可视范围。核心问题是没有基于文本实际的边界尺寸来计算居中偏移量。
解决方案步骤
- 获取文本实际边界:通过opentype.js的
getBoundingBox方法,获取文本的宽度、高度及偏移坐标,这是精准居中的基础。 - 调整文本绘制起始位置:让文本在SVG画布内先居中,而非从x=0开始绘制。
- 修正顶点坐标计算:基于文本的实际边界,确保所有顶点最终在Three.js场景中居中。
- 自适应缩放:根据文本宽度动态调整缩放比例,保证长文本完整显示。
修改后的关键代码
async _setupTextModel() { const font = await load(UnDotumBold); // 1. 获取文本的实际包围盒 const fontSize = 1000; const textBoundingBox = font.getBoundingBox(text, 0, fontSize, fontSize); const textWidth = textBoundingBox.x2 - textBoundingBox.x1; const textHeight = textBoundingBox.y2 - textBoundingBox.y1; // 计算文本在SVG中居中的起始偏移量 // SVG的y轴向下,需调整y方向偏移保证文本垂直居中 const textOffsetX = (1920 - textWidth) / 2 - textBoundingBox.x1; const textOffsetY = fontSize - (1920 - textHeight) / 2 + textBoundingBox.y1; // 创建SVG并生成路径 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); svg.setAttribute("viewBox", [0, 0, 1920, 1920].join(" ")); g.setAttribute("fill", "black"); let pathMarkup = ""; // 2. 使用计算好的偏移量绘制文本,确保文本在SVG中居中 const fontPaths = font.getPaths(text, textOffsetX, textOffsetY, fontSize); fontPaths.forEach((fontPath) => { const path = fontPath.toSVG(); pathMarkup += path; }); g.insertAdjacentHTML("beforeend", pathMarkup); document.body.appendChild(svg); svg.appendChild(g); const paths = svg.querySelectorAll("path"); const svgViewBoxWidth = svg.viewBox.baseVal.width; const svgViewBoxHeight = svg.viewBox.baseVal.height; svg.remove(); // 生成顶点 this._textVertices = []; const sizes = []; const delay = 1; const gradient = chroma.scale([ "ef476f", "ffd166", "06d6a0", "118ab2", "073b4c", ]); const colorRGB = { r: Math.random() * 100, g: Math.random() * 100, b: Math.random() * 100, }; const positionXYZ = { x: Math.random() * 3000, y: Math.random() * 3000, z: Math.random() * 3000, }; const timeline = gsap.timeline({ onReverseComplete: () => { timeline.timeScale(1); timeline.play(0); }, }); paths.forEach((path) => { const length = path.getTotalLength(); for (let i = 0; i < length; i += 30) { const pointLength = i; const point = path.getPointAtLength(pointLength); // 3. 基于SVG viewBox中心计算顶点坐标,此时文本已在SVG中居中,转换后自然居中 const vector = new THREE.Vector3( point.x - svgViewBoxWidth / 2, -point.y + svgViewBoxHeight / 2, (Math.random() - 0.5) * 15, ); const start = new THREE.Vector3( vector.x + (Math.random() - 0.5) * positionXYZ.x, vector.y + (Math.random() - 0.5) * positionXYZ.y, vector.z + (Math.random() - 0.5) * positionXYZ.z, ); const coloursX = point.x / svgViewBoxWidth + (Math.random() - 0.5) * 0.2; const color = gradient(coloursX).rgb(); this._textVertices.push(vector); vector.r = 1 - (vector.z + 7.5) / colorRGB.r; vector.g = 1 - (vector.z + 7.5) / colorRGB.g; vector.b = 1 - (vector.z + 7.5) / colorRGB.b; timeline.from( vector, { x: start.x, y: start.y, z: start.z, r: color[0] / 255, g: color[1] / 255, b: color[2] / 255, duration: "random(0.5, 1.5)", ease: "power2.out", }, delay * 0.0012, ); sizes.push(25 * this.renderer.getPixelRatio()); } }); // 创建文本网格 this._textGeometry = new THREE.BufferGeometry().setFromPoints( this._textVertices, ); // 移除原无效的color属性,保留与_tick方法对应的customColor逻辑 this._textGeometry.setAttribute( "size", new THREE.Float32BufferAttribute(sizes, 1), ); const textMaterial = new THREE.ShaderMaterial({ uniforms: { pointTexture: { value: new THREE.TextureLoader().load(ALPHAMAP.CIRCLE), }, }, vertexShader: this._vertexShader, fragmentShader: this._fragmentShader, transparent: true, depthTest: false, }); this._textMesh = new THREE.Points(this._textGeometry, textMaterial); this._textMesh.scale.set(0.1, 0.1, 0.1); // 4. 根据文本宽度自适应缩放,避免长文本超出屏幕 const maxTextWidth = 1600; // 设定SVG中允许的最大文本宽度 const scaleFactor = textWidth > maxTextWidth ? maxTextWidth / textWidth : 1; this._textMesh.scale.multiplyScalar(scaleFactor); this._textGroup = new THREE.Group(); this._textGroup.add(this._textMesh); this._textGroup.position.set(0, 0, -240); this._init(); }
额外说明
- 原代码中
colors数组为空,导致color属性无效,已移除该无效代码,保留与_tick方法匹配的customColor更新逻辑。 - 自适应缩放的
maxTextWidth值可根据实际场景调整,确保不同长度的文本都能完整显示。
内容的提问来源于stack exchange,提问作者LSS
相关产品推荐
相关产品推荐

