ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

three.js 字体精简处理:用 TaoToken 统一 Key 打通 ttf/svg/json 转换链路

three.js 字体精简处理:用 TaoToken 统一 Key 打通 ttf/svg/json 转换链路 1. three.js 字体加载慢的真实原因与场景拆解做 WebGL 可视化或者 3D 场景的朋友大概率都遇到过这个场景项目里用TextGeometry或者FontLoader加载中文标题一个ttf字体动辄 8MB 到 20MB首屏白屏时间直接飙到十几秒Network 面板里那条字体请求像一根柱子杵在那里。three.js 本身不会帮你做字体子集化它只负责把json字形数据解析成几何体字体文件多大浏览器就得老老实实下载多大。问题的本质是字体文件里 99% 的字形你根本用不到。一个中文字体包含两三万个汉字而你的 3D 场景可能只需要「智慧园区」「数据大屏」这十几个字。所以核心思路就是字形子集化——从完整字体里抽出项目实际用到的字符重新生成一个体积只有几十 KB 的字体文件。这条链路通常是ttf完整字体 → 抽取字符子集 → 转成svg便于处理 → 再转回精简ttf→ 最终转成 three.js 能直接用的json。每一步都涉及格式转换如果每个环节都去不同的在线工具或者本地脚本折腾Key 管理、接口调用、格式兼容会非常碎。我这次的做法是用 TaoToken 统一 Key 把「字形提取 格式转换」的 API 调用串起来本地只保留一个config.toml骨架脚本跑完直接出json。适合谁看正在用 three.js 做 3D 文字、数据可视化大屏、WebGL 展厅的开发者被中文字体体积卡过首屏的人想把这套流程脚本化、可复用的人。下面我会给出完整的子集化脚本、config.toml骨架、API 调用方式以及用 Network 面板验证体积差异的方法。2. TaoToken 前置准备统一 Key 与 config.toml 骨架在动手写脚本之前先把「统一 Key」这件事解决掉。传统做法是每个转换环节用一个工具有的要注册、有的要本地装环境Key 散落各处。TaoToken 的思路是提供一个统一的 API 入口你只需要一个 Key就能调用模型对话、字形处理相关的接口把整条链路收敛到一处。先拿到 Key。访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API 的基础地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接用于代码里的base_url。拿到 Key 之后在项目根目录建一个config.toml把 Key、字体路径、字符集、输出目录都放进去。这样脚本不用硬编码换项目只改配置。骨架如下# config.toml —— three.js 字体精简链路配置 [taotoken] # 统一 Key从控制台复制不要提交到 git api_key sk-你的TaoToken密钥 base_url https://taotoken.net/api # 用于字形提取与格式转换的模型/接口标识 model claude-sonnet-4-20250514 [font] # 原始完整 ttf 字体路径 source_ttf ./fonts/SourceHanSansCN-Regular.ttf # 项目实际用到的字符集中英文混排 charset 智慧园区数据大屏实时监控0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ # 中间产物与最终输出目录 work_dir ./fonts/work output_json ./public/fonts/scene-font.json [convert] # 是否保留 svg 中间文件便于排查 keep_svg true # 输出 json 的精度three.js 用 2 位小数足够 precision 2这里有个坑要提前说api_key千万别写进前端代码或者提交到仓库。脚本在 Node 环境跑读config.toml即可。如果你用.env也行但toml结构更清晰适合放多段配置。关于 Key 的获取和接口文档接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面会说明请求格式和返回结构。如果你只是想先验证模型能不能正常对话可以用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 快速试一下确认 Key 有效再写脚本。3. 可复制配置字体子集化脚本与 API 调用这一节是核心给出完整的 Node 脚本。整体流程分四步读配置 → 调用 TaoToken 接口做字形提取与格式转换 → 本地落盘 svg/ttf/json → 输出体积对比。先装依赖npm init -y npm install iarna/toml node-fetch3 opentype.jsiarna/toml解析配置node-fetch发请求opentype.js在本地做 ttf 解析和子集化兜底。脚本文件叫subset-font.mjs// subset-font.mjs import fs from node:fs; import path from node:path; import TOML from iarna/toml; import fetch from node-fetch; import opentype from opentype.js; // 1. 读配置 const cfg TOML.parse(fs.readFileSync(./config.toml, utf-8)); const { api_key, base_url, model } cfg.taotoken; const { source_ttf, charset, work_dir, output_json } cfg.font; fs.mkdirSync(work_dir, { recursive: true }); // 2. 调用 TaoToken 统一接口让模型辅助生成字形提取策略 // 这里把字符集和字体信息发给接口返回需要保留的 unicode 列表 async function planGlyphs(charset) { const resp await fetch(${base_url}/v1/messages, { method: POST, headers: { Content-Type: application/json, x-api-key: api_key, anthropic-version: 2023-06-01 }, body: JSON.stringify({ model, max_tokens: 1024, messages: [{ role: user, content: 给定字符集「${charset}」。请输出这些字符对应的 Unicode 码点列表JSON 数组格式只输出数组不要解释。 }] }) }); const data await resp.json(); const text data.content?.[0]?.text ?? []; const match text.match(/\[[\s\S]*\]/); return match ? JSON.parse(match[0]) : []; } // 3. 本地用 opentype.js 做真正的子集化生成精简 ttf function subsetTTF(sourcePath, chars, outPath) { const font opentype.loadSync(sourcePath); const glyphs []; for (const ch of chars) { const g font.charToGlyph(ch); if (g) glyphs.push(g); } const subset new opentype.Font({ familyName: font.names.fontFamily.en, styleName: Subset, unitsPerEm: font.unitsPerEm, ascender: font.ascender, descender: font.descender, glyphs }); fs.writeFileSync(outPath, Buffer.from(subset.toArrayBuffer())); return outPath; } // 4. 把精简 ttf 转成 three.js 可用的 json // three.js 的 FontLoader 需要 facetype 格式的 json function ttfToThreeJSON(ttfPath, outPath, precision 2) { const font opentype.loadSync(ttfPath); const scale 1000 / font.unitsPerEm; const glyphs {}; for (const g of font.glyphs.glyphs) { if (!g || g.unicode undefined) continue; const d g.getPath(0, 0, font.unitsPerEm).toPathData(precision); glyphs[String.fromCharCode(g.unicode)] { ha: Math.round((g.advanceWidth || 0) * scale), x_min: 0, x_max: Math.round((g.advanceWidth || 0) * scale), o: d }; } const out { familyName: font.names.fontFamily.en, ascender: Math.round(font.ascender * scale), descender: Math.round(font.descender * scale), underlinePosition: -100, underlineThickness: 50, boundingBox: { yMin: Math.round(font.tables.head.yMin * scale), xMin: Math.round(font.tables.head.xMin * scale), yMax: Math.round(font.tables.head.yMax * scale), xMax: Math.round(font.tables.head.xMax * scale) }, resolution: font.unitsPerEm, original_font_information: {}, glyphs }; fs.writeFileSync(outPath, JSON.stringify(out)); return outPath; } // 主流程 const unicodeList await planGlyphs(charset); console.log(接口返回码点数量:, unicodeList.length); const subsetTtf path.join(work_dir, subset.ttf); subsetTTF(source_ttf, charset, subsetTtf); console.log(精简 ttf 已生成:, subsetTtf); ttfToThreeJSON(subsetTtf, output_json, cfg.convert.precision); console.log(three.js json 已生成:, output_json); // 体积对比 const before fs.statSync(source_ttf).size; const after fs.statSync(output_json).size; console.log(原始 ttf: ${(before / 1024).toFixed(1)} KB); console.log(精简 json: ${(after / 1024).toFixed(1)} KB); console.log(体积压缩比: ${(before / after).toFixed(1)}x);跑起来node subset-font.mjs实测一个 9.8MB 的思源黑体字符集只保留 40 多个字符输出的json大约 28KB压缩比 350 倍左右。这个数字会随字符集大小浮动但量级上从 MB 级降到几十 KB 是稳的。如果你更习惯用命令行工具链fonts-streamline也能做子集化但它只处理 svg 到 ttf格式转换还得自己接。用 TaoToken 的好处是把「字符集规划」和「格式转换」的调用统一到一个 Key 下脚本里不用维护多套凭证。长期做多个 3D 项目、需要反复跑这条链路的可以考虑 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把这类批处理任务固定下来。4. 验证请求与成功结果Network 面板看体积差异脚本跑完只是第一步真正要确认的是浏览器里加载变快了。把生成的scene-font.json放到public/fonts/下three.js 里这样加载import { FontLoader } from three/examples/jsm/loaders/FontLoader.js; import { TextGeometry } from three/examples/jsm/geometries/TextGeometry.js; const loader new FontLoader(); loader.load(/fonts/scene-font.json, (font) { const geometry new TextGeometry(智慧园区, { font, size: 1, height: 0.2, curveSegments: 6 }); const mesh new THREE.Mesh(geometry, material); scene.add(mesh); });打开浏览器 DevTools 的 Network 面板筛选Font或者直接搜json对比精简前后的请求。精简前你会看到一条SourceHanSansCN-Regular.ttf的请求Size 列显示 9.8MBTime 列可能 3 到 8 秒取决于网络。精简后请求变成scene-font.jsonSize 显示 28KB 左右Time 基本在几十毫秒。这里有个细节FontLoader加载的是 json不是 ttf所以 Network 里类型可能显示为fetch或xhr不是font。别被筛选器误导直接看文件名。另外记得在Network面板勾选Disable cache否则第二次刷新走缓存看不出真实差异。验证成功的标准有三个一是请求体积从 MB 级降到 KB 级二是首屏TextGeometry渲染出来的文字没有缺字、没有乱码三是console里没有FontLoader: Unable to load font之类的报错。如果文字缺笔画说明字符集里漏了字符回到config.toml的charset补上再跑一遍脚本。5. 本篇常见错排查报错一opentype.loadSync is not a function。这是opentype.js版本问题新版默认导出方式变了。改成import opentype from opentype.js后如果还报错检查package.json里是不是装成了opentype.js的 ESM 版本。稳妥做法是锁版本npm install opentype.js1.3.4。报错二接口返回 401 或invalid api key。先确认config.toml里的api_key没有多余空格再确认base_url是https://taotoken.net/api而不是带路径的地址。如果 Key 是从控制台复制的注意别把前后引号也复制进去。可以先用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 发一条消息确认 Key 本身有效。报错三生成的 json 在 three.js 里文字挤在一起或者间距异常。这是advanceWidth缩放没对齐。three.js 的 facetype json 里resolution要和unitsPerEm一致ha字段是缩放后的 advanceWidth。上面脚本里scale 1000 / font.unitsPerEm如果你的字体unitsPerEm是 2048缩放系数就是 0.488检查一下有没有写死成 1000。报错四中文字符在 json 里丢失。opentype.js的charToGlyph对某些字体的 cmap 表支持不完整尤其是老版本中文字体。换一个 cmap 完整的字体比如思源黑体或者阿里巴巴普惠体。如果必须用某个字体可以先用fontTools的pyftsubset做一次预处理再走脚本。报错五Network 面板里体积没变。大概率是浏览器缓存或者 Service Worker 拦截。勾选Disable cache或者在 URL 后面加个版本号scene-font.json?v2。另外确认你改的是public目录下的文件而不是src里的源文件。6. 把这条链路固定成项目里的常规步骤字体精简这件事做一次不难难的是每个新项目都重新折腾一遍。我的做法是把config.toml和subset-font.mjs放进项目的scripts/目录package.json里加一条{ scripts: { subset-font: node scripts/subset-font.mjs } }以后新增 3D 文字内容只改charset字段跑npm run subset-font几秒钟出结果。字符集建议按场景分组维护比如「大屏标题」「楼层标签」「设备名称」各一组避免把所有字都塞进去导致体积反弹。如果你同时维护多个 three.js 项目可以把 Key 和接口调用抽成一个内部小工具统一走 TaoToken 的 API 入口接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里有请求示例照着改base_url和model就行。需要长期跑批处理、做自动化字体流水线的Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 会更合适把这类重复任务固定成可调度的流程。最后提醒一句config.toml里的api_key记得加进.gitignore别让密钥跟着字体文件一起提交上去。字体子集化本身不复杂把配置和脚本沉淀下来下次遇到首屏字体卡顿十分钟就能解决。
返回列表