用Three.js搞个3D词云

2D词云经常用,是时候升级了,用一下3D词云!

1.球坐标

除了常见的笛卡尔坐标系,极坐标系,还有一种坐标系,球坐标。通过以坐标原点为参考点,由方位角、仰角和距离构成一个三维坐标。

在three.js数学库里有个球坐标threejs.org/docs/#api/z...

Spherical( radius : Float, phi : Float, theta : Float )

  • radius:半径,或者说从该点到原点的(欧几里得距离,即直线距离)。默认值为1.0。范围[0,无穷)
  • phi:与y轴(向上)的极角(以弧度为单位)。 默认值为 0。范围[0,PI]
  • theta:绕y轴(向上)的赤道角(方位角)(以弧度为单位)。 默认值为 0。范围[0,2*PI]

极角(phi)位于正 y 轴和负 y 轴上,与其的夹角。赤道角(方位角)(theta)从正 z 开始,环绕一圈。

2.线性插值函数

从开始值到结束值,映射到[0,1]的区间,通过[0,1]范围的值可以得到一个开始值到结束值之间的线性映射的值。 我们通常手动这样写

js 复制代码
原始值val范围min,max
新值value范围newMin,newMax
value=newMin+ ((val-min)/(max-min))*(newMax-newMin)

在three.js数学工具里https://threejs.org/docs/?q=Math#api/zh/math/MathUtils.lerp

lerp(x:Float,y:Float,t:Float):Float

  • x:开始值。
  • y:结束值。
  • t:闭合区间[0,1]中的插值因子。

返回基于给定间隔从两个已知点线性插值的值-t=0将返回x,t=1将返回y。

使用

js 复制代码
value=lerp(newMin,newMax,(val-min)/(max-min))

3.画文本

  • canvas文本
js 复制代码
/**
 *canvas文本
 * @param {String} text 文本字符串
 * @param {Number} fontSize 字体大小
 * @param {String} color 颜色
 * @returns
 */
export function getCanvasText(text, fontSize, color, bg) {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = fontSize + 'px Arial';
  ctx.fillStyle = color;
  let padding = 5;
  //测量文本大小,并设置canvas宽高预留padding
  canvas.width = ctx.measureText(text + '').width + padding * 2;
  canvas.height = fontSize * 1.2 + padding * 2;
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = bg;

  ctx.rect(0, 0, canvas.width, canvas.height);
  ctx.fill();

  ctx.font = fontSize + 'px Arial';
  ctx.fillStyle = color;
  ctx.fillText(text, padding, fontSize + padding * 0.5);
  return canvas;
}
  • 文本网格
js 复制代码
/***
 * 文本网格
 * @param {String} text 文本字符串
 * @param {Number} fontSize 字体大小
 * @param {String} color 颜色
 */
export function getTextMesh(THREE, text, fontSize, color) {  
  const canvas = getCanvasText(text, fontSize * 10, color, 'rgba(0,0,0,0)');
  const map = new THREE.CanvasTexture(canvas);
  map.wrapS = THREE.RepeatWrapping;
  map.wrapT = THREE.RepeatWrapping;
  const material = new THREE.MeshBasicMaterial({
    map: map,
    transparent: true,
    side: THREE.DoubleSide
  });
 
  const geometry = new THREE.PlaneGeometry(canvas.width * 0.1, canvas.height * 0.1);
  const mesh = new THREE.Mesh(geometry, material);
  return { material, geometry, canvas, mesh };
}

注意:canvas贴图一定要放大倍数,否则会近看模糊, 为了保持大小,创建二维平面板时可以对应缩小比例

4.开搞

  • 计算文本坐标,将球面上的坐标点分成that.data.length个
js 复制代码
const vector = new THREE.Vector3();

const phi = Math.acos(THREE.MathUtils.lerp(-1, 1, idx / (that.data.length - 1)));

const theta = Math.sqrt(that.data.length * Math.PI) * phi;

vector.setFromSphericalCoords(that.radius, phi, theta);

因为反余弦函数的值域范围刚好是[0,PI],定义域范围是[-1,1],那么我们可以通过lerp(-1,1,t)的线性插值函数得到对应的极角,这里使用的是数据索引idx
setFromSphericalCoords通过球坐标转成三维坐标

js 复制代码
//文本大小线性插值,根据数据值大小做映射
 let s = THREE.MathUtils.lerp(
                that.minFontSize,
                that.maxFontSize,
                (item.value - min) / size
              );

              let { mesh, geometry } = getTextMesh(THREE, text, s, that.color);

              mesh.name = 'text' + idx;
              
              mesh.position.set(vector.x, vector.y, vector.z);             

              textGroup.add(mesh);
  • 将文本坐标设置成球坐标转换后的笛卡尔三维坐标,我们会发现文本全部垂直,虽然在一个球体对应的坐标上,但是并没有形成一个友好的球体。我们需要的效果是让字体沿着球面摆放
js 复制代码
 geometry.lookAt(vector);
geometry.translate(vector.x, vector.y, vector.z);

将二维平面几何看向坐标点,然后按着坐标点进行移动,即可得到一个文本球体

  • 可以增加个雾来增加层次感
js 复制代码
this.scene.fog = new THREE.FogExp2(new THREE.Color('#000000'), 0.003);
  • 3D词云在数据多的情况下很好看,但是数据少的时候就显得很空,这时候可以加个球框
js 复制代码
  const g = new THREE.IcosahedronGeometry(that.radius * 2, 2);
            const m = new THREE.MeshBasicMaterial({
              color: that.color,
              transparent: true,
              opacity: 0.2,
              wireframe: true
            });
            const mm = new THREE.Mesh(g, m);
            this.objGroup.add(mm);
  • 加个自动旋转
js 复制代码
   animateAction() {
          if (this.objGroup) {
            if (this.objGroup.rotation.y >= Math.PI * 2) {
              this.objGroup.rotation.y = 0;
            } else {
              this.objGroup.rotation.y += 0.001;
            }
          }
        }
  • 加个随机颜色
js 复制代码
   const color = `rgb(${Math.random() * 255},${Math.random() * 255},${
                Math.random() * 255
              })`;
              
              let { mesh, geometry } = getTextMesh(THREE, text, s, color);

GitHub

https://github.com/xiaolidan00/my-three

相关推荐
行者-全栈开发4 小时前
【前端/JS框架】CVE-2026-3854:GitHub Enterprise Server X-Stat注入XSS漏洞深度剖析与紧急修复指南
前端·javascript·github
计算机魔术师5 小时前
AMD砸82亿买下李飞飞,这次不卖芯片卖什么?
前端
hzxpaipai6 小时前
杭州企业官网怎么建设?从策划到上线的5步建站方法
运维·服务器·前端·网络
怕浪猫6 小时前
2026年金九银十,我面了22个前端
前端·javascript·面试
IT_陈寒6 小时前
Vue 这个响应式陷阱,我的头发都掉没了
前端·人工智能·后端
前端的日常6 小时前
一个人+AI做情侣食谱小程序,30天纯赚
前端·javascript·后端
qq_316837756 小时前
阿里云百炼 fun-asr-realtime uniapp vue3 调用
javascript·vue.js·uni-app
庄园特聘拆椅狂魔7 小时前
Java 后端转全栈的第一课:从前端项目搭建到技术选
java·开发语言·前端
triumph_passion7 小时前
前端路由策略:Memory、Hash 与 History
前端
李剑一7 小时前
AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!
前端·vue.js