Featured image of post three.js 修改UV映射实现球型贴图 -- BufferGeometry对象

three.js 修改UV映射实现球型贴图 -- BufferGeometry对象

前言

网上有很多直接贴贴图的实现方式. 但现在公司的有个全景的项目不能以这种单纯的方式去实现. 综合下来, 现在的需求要求在一个实实在在的空间模型中实现全景的预览, 场景的切换, 上帝, 个人视角的切换等等.

要实现模型中无缝的全景 UV 映射全景图 这要先从 缓存几何模型(BufferGeometry) 说起.

缓存几何模型(BufferGeometry)

该类是一个 几何模型(Geometry) 的高效替代,因为它使用缓存(buffer)来保存所有数据,包括顶点位置、面索引、法向量、颜色、UVs 以及自定义属性。 这节约了向 GPU 传递全部这些数据的成本。但同时也使得 BufferGeometry 要比 几何模型(Geometry) 更难以处理,不是以对象的方式来访问,比如使用 Vector3 来访问位置数据, 以 Color 对象来访问颜色数据,你得从相应的 attribute 缓存中访问原始数据。 这使得 BufferGeometry 很适合用来存储静态对象,也就是当我们创建完模型实例后不太需要去操作它。

相关属性对象

position

对象

geometry.attributes.position

array: 记录几何模型的顶点信息 x, y, z 的集合.
count: 顶点的数量
itemSize: array数据的分组大小

array 是一维数组数组顺序是 [x1,y1,z1,x2,y2,z2…] 依次顺序排列.

normal

对象

geometry.attributes.normal

保存模型中每个顶点处的面或顶点法向量的 x, y, 和 z 分量。

uv

对象

geometry.attributes.uv

保存模型中 UV 坐标信息. [u,v,u1,v1,u2,v2…] - (itemSize: 2)

顶点顺序

添加了一个球体观察点的顺序有一个发现

1
2
3
4
var geometry = new THREE.SphereBufferGeometry(50, 4, 4);
var sphere = new THREE.Mesh(geometry, materialLocation);
scene.add(sphere);
console.log(sphere);

position 的数组顶点数据是这样的顺序:

从头往下: y 值 r ~ 0 ~ -r
从左住右: x 值 r ~ 0 ~ -r
从前往后: z 值 r ~ 0 ~ -r

换句话说就是: 它是顺时针螺旋向下的.

three.js修改UV映射实现球型贴图BufferGeometry对象-2023-11-30-14-40-57

开始的点都是 y = r 时的最头上的点. 这个点在 array 中会出现多次(准确的是应该是水平分割面的数量的次数), 同样的 y = -r 时脚下的点也是如此.

这个会产生一个问题: 上下极点贴图聚合点.

因为这个时候采用一般处理的计算方式时这 4 个点的所有 UV 值计算出来都会是 (0.5,0.5)

压缩线产生的原因

一个横切面的点数是水平分割面的数量+1, 比如你上面的代码水平分割面的数量是 4, 实际的顶点有 5 个. 最后一点的的位置与起始点相重合.

所以在计算 UV 时第一个点与最后一个点计算出来的 UV 值都会是同一个值. 这也是为什么当你是以计算 UV 来贴图实现全景时, 会有一条压缩线的原因.

UV

UV 的取值范围为 0 ~ 1.

四个顶点所对应的坐标如下:

three.js修改UV映射实现球型贴图BufferGeometry对象-2023-11-30-14-41-04

解决压缩线的思路

当了解到整个模型的连接起来的地方是两个无限接近的点时, 就有了相应的解决办法:
把假设你的 UV 起点是 0, 则把另外的那个无线接近的点设置为 1 就可解决;

解决方法

  1. 定位每一圈的最后一个点.
  2. 根据 UV 的起点坐标, 将其设置为终点坐标.

这里只用修改 U 值. V 值还是正常的计算值.

计算 UV

通过遍历所有顶点来计算 UV 坐标.

先说下大概流程:

1
2
3
4
5
- 遍历模型点, 获取点坐标信息.
- 根据点坐标获取X,Y轴的夹角角度.
- X,Y轴夹角角度映射出UV坐标.
- 筛选每一圈的最后一点个并重置终点值.
- 修改UV.

主要方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function calcUvs(geometry) {
  let _g = geometry;
  console.log(_g);

  let _position = geometry.getAttribute("position");
  let uvArray = new Float32Array(2 * _position.count);

  console.log(_position);

  let _uvI = 0;
  let uvP = {};
  for (let i = 0; i < _position.array.length; i += 3) {
    let _x = _position.array[i];
    let _y = _position.array[i + 1];
    let _z = _position.array[i + 2];

    let _r = getRadius(0, 0, 0, _x, _y, _z);

    let angleXY = getAngleXY(Math.round(_r), _x, _y, _z);
    // console.log(angleXY);
    uvP = getUVPosition(angleXY.angleX || 0, angleXY.angleY || 0);

    // 每圈的最后一个点 解决 0 1拼接点的压缩线问题.
    if ((i / 3) % (_g.parameters.widthSegments + 1) == 0) {
      uvP.u = 1;
    }

    uvArray[_uvI] = uvP.u;
    _uvI += 1;
    uvArray[_uvI] = uvP.v;
    _uvI += 1;
  }
  // v.material = material;
  let _uv = _g.getAttribute("uv");
  _uv.needsUpdate = false;
  _uv.setArray(uvArray);
  console.log(_uv);
  // 更新
  _uv.needsUpdate = true;
}
1
2
3
4
// 获取模型点到可视点的半径
function getRadius(x, y, z, x1, y1, z1) {
  return Math.pow(Math.pow(x - x1, 2) + Math.pow(y - y1, 2) + Math.pow(z - z1, 2), 1 / 2);
}
1
2
3
4
5
6
7
// 根据半径和坐标获取角度
function getAngleXY(r, x, y, z) {
  return {
    angleX: Math.atan2(z, x) + Math.PI,
    angleY: Math.asin(y / r),
  };
}
1
2
3
4
5
6
7
// 角度转化为 UV 坐标 UV 范围 0 ~ 1
function getUVPosition(angleX, angleY) {
  return {
    u: (angleX / (2 * Math.PI)) % 1,
    v: 1 / 2 + angleY / Math.PI,
  };
}