凯~
文章11
标签12
分类4
Three.js下的经纬度坐标转换

Three.js下的经纬度坐标转换

Three.js下的经纬度坐标转换

本文主要分享了Three.js开发中,如何将Three.js的世界坐标系与经纬度坐标系(WGS-84)统一,可以将模型通过经纬度正确地显示到3D场景中

一、要点

  1. Three.js世界坐标系是什么?

    右手坐标系,如图

    image-20250418161425192

  2. 我们需要解算出一个公式,输入经纬度,输出three.js世界坐标,那么经纬度需要旋转并乘以一定系数才能匹配到three.js世界坐标。

  3. 得出转换公式的条件:

    一个3d模型作为地图,为了方便理解,模型建立的时候就应该平行与xz平面,如图

    image-20250418162140982

    为了确定转换关系,需要2个校准点,我们取3d模型上的2个点作为校准点,这2个点对应的经纬度需要知道,这是必要前提

  4. 根据以上条件,就可以求出经纬度和three.js世界坐标的转换关系,即地图模型导入three.js,不做任何变换,一个带经纬度的点通过转换公式,便能正确的显示在模型地图上(高度不在考虑范围内,室内导航应用默认高度统一,每个楼层的高度也是统一的)。

二、求解步骤

1.方向求解

three.js世界坐标默认xz平面是平行与大地,那么three.js世界坐标需要绕y轴旋转一定角度才能匹配到经纬度坐标系,如图,可以当做地图模型的正北俯视图:

image-20250418165204860

由上图可知,模型的建立依靠的坐标系往往并不依照正北方向,由此要利用模型就必须进行坐标转换,明显的-z方向和正北呈现一个明显的锐角,那么我们该如何求出这个夹角呢?

这时候,前面提到的2个校准坐标点就起作用了,如图:

image-20250418170453014

明显的,我们知道了2个世界坐标系的坐标值和他们对应的经纬度,我们需要利用这些数据去计算夹角(x1y2 x2y2如何获取,可以用代码打印出模型的中心点和边缘点,确定坐标的大致范围,再绘制点,手动调整绘制点的坐标,将2个点移动到我们想要的校准点上,这样就可以知道校准点的three.js世界坐标了)

好,现在我们拥有了四组坐标值,下一步要做的就是通过这些值算出经纬度与three.js坐标系的角度差(正北与三维坐标的夹角)

该用什么思路呢,便是将2个校准点在不同坐标系中连线,对比连线的方位角,笔者的思路就是先求出2个校准点连线相对于负z轴的夹角(three.js坐标系),然后计算2个校准点连线与正北的夹角(经纬度坐标系),这样就可以求出正北和three.js坐标系的负z轴的夹角了

废话不多说直接上代码:

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
// 将角度转为弧度
const degreesToRadians = (degrees) => {
return degrees * Math.PI / 180;
}

// 计算经纬度连线的方位角(弧度)
const calculateBearing = (lat0, lon0, lat1, lon1) => {
const lat0Rad = degreesToRadians(lat0);
const lon0Rad = degreesToRadians(lon0);
const lat1Rad = degreesToRadians(lat1);
const lon1Rad = degreesToRadians(lon1);

const dLon = lon1Rad - lon0Rad;

const y = Math.sin(dLon) * Math.cos(lat1Rad);
const x = Math.cos(lat0Rad) * Math.sin(lat1Rad) - Math.sin(lat0Rad) * Math.cos(lat1Rad) * Math.cos(dLon);
const bearing = Math.atan2(y, x);

return bearing;
}

const calculateAngleDifference = (fixLnglat0, fixLnglat1, fixXy0, fixXy1) => {
// 计算经纬度连线与正北的夹角
const lat0 = fixLnglat0[1];
const lon0 = fixLnglat0[0];
const lat1 = fixLnglat1[1];
const lon1 = fixLnglat1[0];

const bearing = calculateBearing(lat0, lon0, lat1, lon1); // 经纬度连线的方位角

// 计算三维坐标连线与负Z轴的夹角
const deltaX = fixXy1[0] - fixXy0[0];
const deltaZ = fixXy1[2] - fixXy0[2];
const angleWithZ = Math.atan2(deltaX, -deltaZ) * (180 / Math.PI); // 夹角转换为度(如果需要输出度数)

// 计算正北与负Z轴的夹角(弧度)
const angleDifference = bearing - Math.atan2(deltaX, -deltaZ);

return angleDifference;
}

如此我们便得出了2个坐标系的角度关系

2.旋转矩阵与坐标统一

我们的目的是将经纬度转换成three.js坐标,于是首先会想到经纬度乘以我们刚才求出的夹角的旋转矩阵,便可以的到方向和three.js坐标一致的坐标,但是经纬度和米坐标还是不一样的,经度和纬度并不是1比1,这要我们先将经纬度转换成米坐标,有关的转换有现成的库,就是proj4,引用如下:

1
2
3
4
5
6
7
8
9
10
import proj4 from 'proj4'
const firstProjection =
'PROJCS["CGCS2000 / 3-degree Gauss-Kruger CM 120E",GEOGCS["China Geodetic Coordinate System 2000",DATUM["China_2000",SPHEROID["CGCS2000",6378137,298.257222101,AUTHORITY["EPSG","1024"]],AUTHORITY["EPSG","1043"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4490"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",120],PARAMETER["scale_factor",1],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","4549"]]';

const secondProjection =
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]';
const fixLnglat0 = [121.31155131270114, 31.143545887099073];

// 经纬度点转4549坐标,即米坐标,需要根据不同经度去查询对应的firstProjection
const xy0 = proj4(secondProjection, firstProjection, fixLnglat0);

好了,我们现在可以处理角度旋转了,旋转矩阵公式如下:

1
2
x1=x0⋅cos⁡(c)−y0⋅sin⁡(c)
y1=x0⋅sin⁡(c)+y0⋅cos⁡(c)

依据这个得出旋转函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
const rotationTransX = (fixLnglat0, angleDifference) => {
const cosAngle = Math.cos(angleDifference);
const sinAngle = Math.sin(angleDifference);

return fixLnglat0[0] * cosAngle - fixLnglat0[1] * sinAngle;
}

const rotationTransY = (fixLnglat0, angleDifference) => {
const cosAngle = Math.cos(angleDifference);
const sinAngle = Math.sin(angleDifference);

return fixLnglat0[0] * sinAngle + fixLnglat0[1] * cosAngle;
}

对坐标进行旋转之后我们就可以去计算经纬度差值与三维坐标差值的正确比例,有这个比例我们就可以去计算经纬度对应的three.js三维坐标,比例计算代码如下:

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
const fixLnglat0 = [121.31155131270114, 31.143545887099073]; // 经纬度点0
const fixLnglat1 = [121.31524160667905, 31.141933166985034]; // 经纬度点1
const fixXy0 = [4.43, 0, -43.95]; // 对应的三维坐标点0
const fixXy1 = [28.37, 0, -12.75]; // 对应的三维坐标点1

// 计算经纬度差值与三维坐标差值的正确比例,代入2组校准坐标:fixLnglat0, fixLnglat1为经纬度,fixXy0, fixXy1为对应的three.js坐标
const calculateLatLonToCoordRatio = (fixLnglat0, fixLnglat1, fixXy0, fixXy1, angleDifference) => {
const xy0 = proj4(secondProjection, firstProjection, fixLnglat0);//经纬度转米
const xy1 = proj4(secondProjection, firstProjection, fixLnglat1);

const deltaLon1 = rotationTransX(xy1, angleDifference);
const deltaLon0 = rotationTransX(xy0, angleDifference);
const deltaLat1 = rotationTransY(xy1, angleDifference);
const deltaLat0 = rotationTransY(xy0, angleDifference);

const deltaLon = deltaLon1 - deltaLon0; // 经度差
const deltaLat = deltaLat1 - deltaLat0; // 纬度差

// 确保比例计算精确
const deltaX = fixXy1[0] - fixXy0[0];
const deltaZ = fixXy1[2] - fixXy0[2];

const ratioX = deltaX / deltaLon;
const ratioZ = deltaZ / deltaLat;

return { ratioX, ratioZ };
}

由此我们得出了x方向和z方向的坐标比例

3.计算最终的three.js坐标

接下来就是最后一步,代入任意经纬度,计算最终的对应的three.js坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const latLonToThreeJsCoords = (lat, lon) => {
// 经纬度转米
const xy = proj4(secondProjection, firstProjection, [lon, lat]);
const fixxy = proj4(secondProjection, firstProjection, fixLnglat0);

//米坐标旋转后求差值
const deltaLon = rotationTransX(xy, angleDifference) - rotationTransX(fixxy, angleDifference);
const deltaLat = rotationTransY(xy, angleDifference) - rotationTransY(fixxy, angleDifference);


// 获取经纬度差值与三维坐标差值的比例
const {
ratioX,
ratioZ
} = calculateLatLonToCoordRatio(fixLnglat0, fixLnglat1, fixXy0, fixXy1, angleDifference);
console.log("ratioX", 1/ratioX);


const x = fixXy0[0] + ratioX * deltaLon;
const z = fixXy0[2] + ratioZ * deltaLat;

return [x, z]; // 返回三维坐标(忽略y轴,假设为0)
}

4.完整代码

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import proj4 from 'proj4'
const firstProjection =
'PROJCS["CGCS2000 / 3-degree Gauss-Kruger CM 120E",GEOGCS["China Geodetic Coordinate System 2000",DATUM["China_2000",SPHEROID["CGCS2000",6378137,298.257222101,AUTHORITY["EPSG","1024"]],AUTHORITY["EPSG","1043"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4490"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",120],PARAMETER["scale_factor",1],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","4549"]]';

const secondProjection =
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]';
const fixLnglat0 = [121.31155131270114, 31.143545887099073];

const fixLnglat0 = [121.31155131270114, 31.143545887099073]; // 经纬度点0
const fixLnglat1 = [121.31524160667905, 31.141933166985034]; // 经纬度点1
const fixXy0 = [4.43, 0, -43.95]; // 对应的三维坐标点0
const fixXy1 = [28.37, 0, -12.75]; // 对应的三维坐标点1

const angleDifference = calculateAngleDifference(fixLnglat0, fixLnglat1, fixXy0, fixXy1);

console.log("angleDifference", angleDifference);

const rotationTransX = (fixLnglat0, angleDifference) => {
const cosAngle = Math.cos(angleDifference);
const sinAngle = Math.sin(angleDifference);

return fixLnglat0[0] * cosAngle - fixLnglat0[1] * sinAngle;
}

const rotationTransY = (fixLnglat0, angleDifference) => {
const cosAngle = Math.cos(angleDifference);
const sinAngle = Math.sin(angleDifference);

return fixLnglat0[0] * sinAngle + fixLnglat0[1] * cosAngle;
}

// 计算经纬度差值与三维坐标差值的正确比例,代入2组校准坐标:fixLnglat0, fixLnglat1为经纬度,fixXy0, fixXy1为对应的three.js坐标
const calculateLatLonToCoordRatio = (fixLnglat0, fixLnglat1, fixXy0, fixXy1, angleDifference) => {
const xy0 = proj4(secondProjection, firstProjection, fixLnglat0);//经纬度转米
const xy1 = proj4(secondProjection, firstProjection, fixLnglat1);

const deltaLon1 = rotationTransX(xy1, angleDifference);
const deltaLon0 = rotationTransX(xy0, angleDifference);
const deltaLat1 = rotationTransY(xy1, angleDifference);
const deltaLat0 = rotationTransY(xy0, angleDifference);

const deltaLon = deltaLon1 - deltaLon0; // 经度差
const deltaLat = deltaLat1 - deltaLat0; // 纬度差

// 确保比例计算精确
const deltaX = fixXy1[0] - fixXy0[0];
const deltaZ = fixXy1[2] - fixXy0[2];

const ratioX = deltaX / deltaLon;
const ratioZ = deltaZ / deltaLat;

return { ratioX, ratioZ };
}

const latLonToThreeJsCoords = (lat, lon) => {
// 经纬度转米
const xy = proj4(secondProjection, firstProjection, [lon, lat]);
const fixxy = proj4(secondProjection, firstProjection, fixLnglat0);

//米坐标旋转后求差值
const deltaLon = rotationTransX(xy, angleDifference) - rotationTransX(fixxy, angleDifference);
const deltaLat = rotationTransY(xy, angleDifference) - rotationTransY(fixxy, angleDifference);


// 获取经纬度差值与三维坐标差值的比例
const {
ratioX,
ratioZ
} = calculateLatLonToCoordRatio(fixLnglat0, fixLnglat1, fixXy0, fixXy1, angleDifference);
console.log("ratioX", 1/ratioX);


const x = fixXy0[0] + ratioX * deltaLon;
const z = fixXy0[2] + ratioZ * deltaLat;

return [x, z]; // 返回三维坐标(忽略y轴,假设为0)
}

// 将角度转为弧度
const degreesToRadians = (degrees) => {
return degrees * Math.PI / 180;
}

// 计算经纬度连线的方位角(弧度)
const calculateBearing = (lat0, lon0, lat1, lon1) => {
const lat0Rad = degreesToRadians(lat0);
const lon0Rad = degreesToRadians(lon0);
const lat1Rad = degreesToRadians(lat1);
const lon1Rad = degreesToRadians(lon1);

const dLon = lon1Rad - lon0Rad;

const y = Math.sin(dLon) * Math.cos(lat1Rad);
const x = Math.cos(lat0Rad) * Math.sin(lat1Rad) - Math.sin(lat0Rad) * Math.cos(lat1Rad) * Math.cos(dLon);
const bearing = Math.atan2(y, x);

return bearing;
}

const calculateAngleDifference = (fixLnglat0, fixLnglat1, fixXy0, fixXy1) => {
// 计算经纬度连线与正北的夹角
const lat0 = fixLnglat0[1];
const lon0 = fixLnglat0[0];
const lat1 = fixLnglat1[1];
const lon1 = fixLnglat1[0];

const bearing = calculateBearing(lat0, lon0, lat1, lon1); // 经纬度连线的方位角

// 计算三维坐标连线与负Z轴的夹角
const deltaX = fixXy1[0] - fixXy0[0];
const deltaZ = fixXy1[2] - fixXy0[2];
const angleWithZ = Math.atan2(deltaX, -deltaZ) * (180 / Math.PI); // 夹角转换为度(如果需要输出度数)

// 计算正北与负Z轴的夹角(弧度)
const angleDifference = bearing - Math.atan2(deltaX, -deltaZ);

return angleDifference;
}

//使用示例
//输入经纬度,转换成 three.js 坐标
const lat = 31.142235878203852; // 输入纬度
const lon = 121.31308429914422; // 输入经度
const threeJsCoords = latLonToThreeJsCoords(lat, lon);

三、结束语

以上就是经纬度坐标系转three.js世界坐标系相关的思路与实现。

本文作者:凯~
本文链接:https://blog.diyultra.top/2025/04/21/05threejstolnglat/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×