前言
网上有很多直接贴贴图的实现方式. 但现在公司的有个全景的项目不能以这种单纯的方式去实现. 综合下来, 现在的需求要求在一个实实在在的空间模型中实现全景的预览, 场景的切换, 上帝, 个人视角的切换等等.
要实现模型中无缝的全景 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
换句话说就是: 它是顺时针螺旋向下的.
开始的点都是 y = r 时的最头上的点. 这个点在 array 中会出现多次(准确的是应该是水平分割面的数量的次数), 同样的 y = -r 时脚下的点也是如此.
这个会产生一个问题: 上下极点贴图聚合点.
因为这个时候采用一般处理的计算方式时这 4 个点的所有 UV 值计算出来都会是 (0.5,0.5)
压缩线产生的原因
一个横切面的点数是水平分割面的数量+1, 比如你上面的代码水平分割面的数量是 4, 实际的顶点有 5 个. 最后一点的的位置与起始点相重合.
所以在计算 UV 时第一个点与最后一个点计算出来的 UV 值都会是同一个值. 这也是为什么当你是以计算 UV 来贴图实现全景时, 会有一条压缩线的原因.
UV
UV 的取值范围为 0 ~ 1.
四个顶点所对应的坐标如下:
解决压缩线的思路
当了解到整个模型的连接起来的地方是两个无限接近的点时, 就有了相应的解决办法:
把假设你的 UV 起点是 0, 则把另外的那个无线接近的点设置为 1 就可解决;
解决方法
- 定位每一圈的最后一个点.
- 根据 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,
};
}
|