# 坐标转换

## 基本概念
 ### 什么是屏幕坐标系？
ThreeJS使用了canvas画布绘制图形，屏幕坐标即画布上的像素位置。注意 vjmap3d 的 `project`/`unproject`/`screenToNdc`/`ndcToScreen` 用的是**浏览器视口 client 坐标**（如 `event.clientX/clientY`，内部会加减容器 `getBoundingClientRect()` 的 left/top），不是 canvas 局部坐标（`offsetX/offsetY`）。

![](https://vjmap.com/blogimages/image-20240531171653866.png)

### 什么是世界坐标系？
世界坐标系（三维坐标系）分为两种，左手坐标系和右手坐标系。

左手坐标系：BabylonJS

右手坐标系：ThreeJS（下图就是右手坐标系）

![](https://vjmap.com/blogimages/image-20240531172119225.png)

### 什么是标准化设备坐标系？
一旦你的顶点坐标已经在顶点着色器中处理过，它们就应该是标准化设备坐标（Normalized Device Coordinates，NDC）了，标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪，不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴)：

![](https://vjmap.com/blogimages/image-20240531172323004.png)

### 透视投影原理 
![](https://vjmap.com/blogimages/image-20240531171108503.png)

### 屏幕坐标系、世界坐标系、标准化设备坐标系之间的关系
将二维坐标映射到三维空间时，首先需要将二维坐标转换为 NDC 坐标。这可以通过将二维坐标的值除以视口的尺寸来实现。

 一旦获得了 NDC 坐标，可以使用其他投影技术（如透视投影或正交投影）将其转换为三维坐标。

> 物体坐标系之间的转换关系大致为：
> 
>         局部坐标 -> 世界坐标 -> 观察空间坐标 -> 裁剪空间坐标 -> 屏幕空间坐标

        我们将 **观察空间坐标系** 和 **裁剪空间坐标系** 之间的转换统一处理，最终得到 **标准设备坐标系**     

> 因此坐标转换过程就变成了：
> 
>         局部坐标 -> 世界坐标 -> 标准设备坐标 -> 屏幕空间坐标

原本世界坐标转换到观察空间坐标需要乘上视图矩阵 **CameraMatrixWorldInverse(ViewMatrix)**

  随后，观察空间坐标转换到裁剪空间坐标需要乘上相机投影矩阵：**ProjectMatrix**

  在 ThreeJS 中有一个方法 **Vector3.project(camera)** 综合了这两步：  
 
```js
project( camera ) {    
    return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );
}
```

## 三维场景坐标转换

![](https://vjmap.com/blogimages/image-20240531175209218.png)

```js
// 增加坐标信息
const showCoordInfo = (mesh, isUpdate) => {
    let localPosition = mesh.position.clone();
    let worldPositon = mesh.localToWorld(localPosition.clone());
    let screen = app.project(worldPositon.x, worldPositon.y, worldPositon.z);
    let ndc = app.screenToNdc(screen.x, screen.y)
    let info = `局部坐标: ${localPosition.x.toFixed(1)},${localPosition.y.toFixed(1)},${localPosition.z.toFixed(1)}<br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}<br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}<br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}<br/>`
}
```
<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
    scene: {  // 场景设置
        gridHelper: { visible: true } // 是否显示坐标网格
    }
})
const sun = new THREE.Mesh(
    new THREE.SphereGeometry( 0.5, 16, 16 ),
    new THREE.MeshBasicMaterial( { color: 0xff0000 } )
)
sun.position.set(-1, 0, -1);
app.scene.add( sun );
const earth = new THREE.Mesh(
    new THREE.SphereGeometry( 0.3, 16, 16 ),
    new THREE.MeshBasicMaterial( { color: 0x00ff00 } )
)
earth.position.set(4, 0, 0);
sun.add( earth );
const moon = new THREE.Mesh(
    new THREE.SphereGeometry( 0.1, 16, 16 ),
    new THREE.MeshBasicMaterial( { color: 0x00ffff } )
)
moon.position.set(-1, 0, 0);
earth.add( moon );
// 
let angleSunEarth = 0;
let angleEarthMoon = 0;
app.signal.onAppUpdate.add(() => {
    angleSunEarth += 0.01; // 控制旋转速度
    earth.position.x = 4 * Math.cos(angleSunEarth);
    earth.position.z = 4 * Math.sin(angleSunEarth);
     // 月亮绕地球转
     angleEarthMoon += 0.03; // 月亮旋转速度可以不同于地球以增加真实感
     moon.position.x =  1 * Math.cos(angleEarthMoon);
     moon.position.z = 1 * Math.sin(angleEarthMoon);
     //
     showCoordInfo(moon, true)
})
// 增加坐标信息
const showCoordInfo = (mesh, isUpdate, radius, bkColor) => {
    let localPosition = mesh.position.clone();
    let worldPositon = mesh.localToWorld(localPosition.clone());
    let screen = app.project(worldPositon.x, worldPositon.y, worldPositon.z);
    let ndc = app.screenToNdc(screen.x, screen.y)
    let info = `局部坐标: ${localPosition.x.toFixed(1)},${localPosition.y.toFixed(1)},${localPosition.z.toFixed(1)}&lt;br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}&lt;br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}&lt;br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}&lt;br/>`
    if (isUpdate) {
        mesh.popup.setHTML(info);
        return;
    }
    let popup = new vjmap3d.Popup2D({
        backgroundColor: bkColor,
        closeButton: false,
    })
    popup.setPosition([0, radius, 0]); // 设置位置 
    popup.setHTML(info);
    popup.setOpacity("0.6")
    popup.addTo(mesh, app)
    mesh.popup = popup;
}
showCoordInfo(moon, false, 0.1, "#00ffff")
</pre>
</mapFrame>

## 三维互联网地图做为底图场景坐标转换
![](https://vjmap.com/blogimages/image-20240531204627592.png)

```js
// 增加坐标信息
const showCoordInfo = (pixel) => {
    let screen = pixel;
    let worldPositon = app.unproject(screen.x, screen.y);
    let ndc = app.screenToNdc(screen.x, screen.y);
    let lngLat = mapviewEnt.worldToLngLat(worldPositon);
    let localPositon = mapviewEnt.lngLatToLocal(lngLat);
    // let localPositon = mapviewEnt.node.worldToLocal(worldPositon.clone()); // 与上面的写法等同
    let info = `经纬度: ${lngLat[0].toFixed(5)},${lngLat[1].toFixed(5)}<br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}<br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}<br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}<br/>`
    info += `地图局部坐标: ${localPositon.x.toFixed(6)},${localPositon.y.toFixed(6)},${localPositon.z.toFixed(6)}<br/>`
    app.popup2d.setPosition(worldPositon);
    app.popup2d.setHTML(info);
}
app.signal.onMouseMove.add(e => showCoordInfo(vjmap3d.eventXY(e)))
```
<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    },
    control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
const tdtVecUrl =
    "https://t3.tianditu.gov.cn/DataServer?T=vec_w&X={x}&Y={y}&L={z}&tk=YOUR_TK";
const tdtAnnoUrl = `https://t3.tianditu.gov.cn/DataServer?T=cva_w&X={x}&Y={y}&L={z}&tk=YOUR_TK`;
let provider = new vjmap3d.MapProvider(
    [
        {
            url: tdtVecUrl
        },
        {
            url: tdtAnnoUrl
        }
    ],
    {
        rootTile: [7, 105, 48]
    }
);
let mapviewEnt = new vjmap3d.MapViewEntity({
    provider,
    baseScale: 100,
});
mapviewEnt.addTo(app);
// 增加坐标信息
const showCoordInfo = (pixel) => {
    let screen = pixel;
    let worldPositon = app.unproject(screen.x, screen.y);
    let ndc = app.screenToNdc(screen.x, screen.y);
    let lngLat = mapviewEnt.worldToLngLat(worldPositon);
    let localPositon = mapviewEnt.lngLatToLocal(lngLat);
    // let localPositon = mapviewEnt.node.worldToLocal(worldPositon.clone()); // 与上面的写法等同
    let info = `经纬度: ${lngLat[0].toFixed(5)},${lngLat[1].toFixed(5)}&lt;br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}&lt;br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}&lt;br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}&lt;br/>`
    info += `地图局部坐标: ${localPositon.x.toFixed(6)},${localPositon.y.toFixed(6)},${localPositon.z.toFixed(6)}&lt;br/>`
    app.popup2d.setPosition(worldPositon);
    app.popup2d.setHTML(info);
}
app.signal.onMouseMove.add(e => showCoordInfo(vjmap3d.eventXY(e)))
</pre>
</mapFrame>

## 三维CAD图做为底图场景坐标转换

![](https://vjmap.com/blogimages/image-20240531210937658.png)

::: tip 提示
使用 fromWorld(世界坐标转CAD坐标） 和 toWorld(CAD坐标转世界坐标） 是为了统一 [做为唯杰地图的三维图层坐标转换](#做为唯杰地图的三维图层坐标转换) CAD坐标转换时的写法。

涉及到距离转换时使用：

fromDist(世界坐标距离 转CAD距离）

toDist(CAD距离 转世界坐标距离）
:::

::: warning 注意
`fromWorld`/`toWorld`/`fromDist`/`toDist` 是**包级导出函数**（`vjmap3d.fromWorld(...)`），不在 `app` 实例上；`app` 上的投影方法是 `project`/`unproject`/`screenToNdc`/`ndcToScreen`。

`fromWorld(vec3)` 的入参按**数组**读取（内部判断 `e.length === 3 && e[2] !== undefined` 才保留 z）：
- `toWorld()` 返回 `THREE.Vector3`、`app.unproject()` 也返回 `Vector3` —— 把它们**直接**传给 `fromWorld` 会**静默丢弃 z 分量**（`Vector3.length` 为 undefined）。文档中 `fromWorld(unproject结果)` 的示例只用到 `cadPos[0]/[1]`，故表面无碍；若需要 z，务必先 `position.toArray()`（如 `vjmap3d.fromWorld(position.toArray())`，见下文三维图层示例）。
- 经纬度↔世界坐标的转换（`lngLatToWorld`/`worldToLngLat`/`mapToWorld`/`worldToMap` 等）在 `MapViewEntity` 实例上，`app` 层没有这些方法。
:::

```js
// 增加坐标信息
const showCoordInfo = (pixel) => {
    let screen = pixel;
    let worldPositon = app.unproject(screen.x, screen.y);
    let ndc = app.screenToNdc(screen.x, screen.y);
    let cadPos = vjmap3d.fromWorld(worldPositon); 
    // let cadPos = mapviewEnt.worldToMap(worldPositon);  // 与上面的写法等同
    let localPositon = mapviewEnt.mapToLocal(cadPos);
    // let localPositon = mapviewEnt.node.worldToLocal(worldPositon.clone()); // 与上面的写法等同
    let info = `CAD图: ${cadPos[0].toFixed(2)},${cadPos[1].toFixed(2)}<br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}<br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}<br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}<br/>`
    info += `地图局部坐标: ${localPositon.x.toFixed(6)},${localPositon.y.toFixed(6)},${localPositon.z.toFixed(6)}<br/>`
    app.popup2d.setPosition(worldPositon);
    app.popup2d.setHTML(info);
}
app.signal.onMouseMove.add(e => showCoordInfo(vjmap3d.eventXY(e)))
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    },
    control: { leftButtonPan: true } // 设置为左键用于平移 (同时右键将用于旋转) 和地图2d使用习惯一样
})
let res = await app.svc.openMap({
    mapid: env.exampleMapId, // 地图ID,(请确保此ID已存在，可上传新图形新建ID)
    mapopenway: vjmap3d.MapOpenWay.GeomRender, // 以几何数据渲染方式打开
    style: vjmap3d.openMapDarkStyle()
});
if (res.error) {
    showError(res.error);
    return;
}
let provider = new vjmap3d.MapProvider(
    [
        {
            url: app.svc.rasterTileUrl()
        }
    ],
    {
        transparent: true
    }
);
let mapBounds = vjmap3d.GeoBounds.fromString(res.bounds)
let scale = 100;
let mapviewEnt = new vjmap3d.MapViewEntity({
    provider,
    baseScale: scale,
    mapBounds: mapBounds.toArray()
});
mapviewEnt.addTo(app);
// 增加坐标信息
const showCoordInfo = (pixel) => {
    let screen = pixel;
    let worldPositon = app.unproject(screen.x, screen.y);
    let ndc = app.screenToNdc(screen.x, screen.y);
    let cadPos = vjmap3d.fromWorld(worldPositon); 
    // let cadPos = mapviewEnt.worldToMap(worldPositon);  // 与上面的写法等同
    let localPositon = mapviewEnt.mapToLocal(cadPos);
    // let localPositon = mapviewEnt.node.worldToLocal(worldPositon.clone()); // 与上面的写法等同
    let info = `CAD图: ${cadPos[0].toFixed(2)},${cadPos[1].toFixed(2)}&lt;br/>`
    info += `世界坐标: ${worldPositon.x.toFixed(1)},${worldPositon.y.toFixed(1)},${worldPositon.z.toFixed(1)}&lt;br/>`
    info += `屏幕坐标: ${screen.x.toFixed(1)},${screen.y.toFixed(1)}&lt;br/>`
    info += `NDC坐标: ${ndc.x.toFixed(1)},${ndc.y.toFixed(1)}&lt;br/>`
    info += `地图局部坐标: ${localPositon.x.toFixed(6)},${localPositon.y.toFixed(6)},${localPositon.z.toFixed(6)}&lt;br/>`
    app.popup2d.setPosition(worldPositon);
    app.popup2d.setHTML(info);
}
app.signal.onMouseMove.add(e => showCoordInfo(vjmap3d.eventXY(e)))
</pre>
</mapFrame>

## 做为唯杰地图的三维图层坐标转换

![](https://vjmap.com/blogimages/image-20240531214018468.png)

::: warning
独立 `App` 中 vjmap3D 为 Y 轴向上；而唯杰地图为 Z 轴向上，因此做为唯杰地图三维图层（`MapThreeLayer`，`app.isMapMode === true`）时，vjmap3D 的世界坐标也是 **Z 轴向上**：`vjmap3d.toWorld([x, y, h])` 的高度在 `.z`，地面为 XY 平面；Y-up 的几何/模型需绕 X 轴转 90°（`ResManager.loadModel` 在地图模式下会自动转），旋转轴用 `(0, 0, 1)`；`FillExtrusionsEntity` 的 footprint 坐标直接用 `[w.x, w.y]`（地图模式下 `isXYPlane` 默认 true）。
![](https://vjmap.com/blogimages/image-20240603135621683.png)
::: 

```js
const showCoordInfo = (event) => {
    let lngLat = event.lngLat;
    let cadPos = map.fromLngLat(lngLat);
    let worldPos = vjmap3d.toWorld([cadPos.x, cadPos.y]); 
    let pixel = map.project(lngLat)
    let info = `CAD图: ${cadPos.x.toFixed(2)},${cadPos.y.toFixed(2)}<br/>`
    info += `经纬度: ${lngLat.lng.toFixed(6)},${lngLat.lat.toFixed(6)}<br/>`
    info += `世界坐标: ${worldPos.x.toFixed(2)},${worldPos.y.toFixed(2)},${worldPos.z.toFixed(2)}<br/>`
    info += `屏幕坐标: ${pixel.x.toFixed(0)},${pixel.y.toFixed(0)}<br/>`
    app.popup2d.setPosition(worldPos);
    app.popup2d.setHTML(info);
}
map.on("mousemove", e => showCoordInfo(e))
```

<mapFrame>
<pre>
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// 打开地图
let res = await svc.openMap({
    mapid: env.exampleMapId, // 地图ID
    // @ts-ignore
    mapopenway: vjmap.MapOpenWay.GeomRender, // 以几何数据渲染方式打开
    style: vjmap.openMapLightStyle() // div为深色背景颜色时，这里也传深色背景样式
})
if (res.error) {
    // 如果打开出错
    showError(res.error)
    return;
}
// 获取地图范围
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
// 根据地图范围建立几何投影坐标系
let prj = new vjmap.GeoProjection(mapBounds);
//
// 地图对象
let map = new vjmap.Map({
    container: 'map', // DIV容器ID
    style: svc.rasterStyle(), // 样式，这里是栅格样式
    center: prj.toLngLat(mapBounds.center()), // 设置地图中心点
    zoom: 2, // 设置地图缩放级别
    pitch: 45,
    antialias: true, // 反锯齿
    renderWorldCopies: false // 不显示多屏地图
});
//
// 关联服务对象和投影对象
map.attach(svc, prj);
await map.onLoad();
//
// 创建3d图层
let mapLayer = new vjmap3d.MapThreeLayer(map, {
    stat: {
        show: true,
        left: "0"
    }
});
map.addLayer(new vjmap.ThreeLayer({ context: mapLayer }))
let app = mapLayer.app;
// 在CAD图中心上方创建一个Mesh
let cadLen = mapBounds.width() / 50; // 取cad图长度的1/50
let len = vjmap3d.toDist(cadLen); // cad长度转世界坐标长度
let box = new THREE.BoxGeometry(len, len, len)
const material = new THREE.MeshStandardMaterial({
    color: "#f00",
    side: THREE.DoubleSide
});
let mesh = new THREE.Mesh(box, material);
let cadPos = [mapBounds.center().x, mapBounds.center().y, cadLen]; // mesh在cad中坐标
//
let worldPos = vjmap3d.toWorld(cadPos);// cad坐标转世界坐标
mesh.position.copy(worldPos);
app.scene.add(mesh);
// 增加坐标信息
const showCoordInfo = (event) => {
    let lngLat = event.lngLat;
    let cadPos = map.fromLngLat(lngLat);
    let worldPos = vjmap3d.toWorld([cadPos.x, cadPos.y]); 
    let pixel = map.project(lngLat)
    let info = `CAD图: ${cadPos.x.toFixed(2)},${cadPos.y.toFixed(2)}&lt;br/>`
    info += `经纬度: ${lngLat.lng.toFixed(6)},${lngLat.lat.toFixed(6)}&lt;br/>`
    info += `世界坐标: ${worldPos.x.toFixed(2)},${worldPos.y.toFixed(2)},${worldPos.z.toFixed(2)}&lt;br/>`
    info += `屏幕坐标: ${pixel.x.toFixed(0)},${pixel.y.toFixed(0)}&lt;br/>`
    app.popup2d.setPosition(worldPos);
    app.popup2d.setHTML(info);
}
map.on("mousemove", e => showCoordInfo(e))
</pre>
</mapFrame>

::: tip 提示
涉及到距离转换时使用：

fromDist(世界坐标距离 转CAD距离）

toDist(CAD距离 转世界坐标距离）
:::

::: warning 
当三维做为一个图层加入至二维地图中时，为了保持三维相机与二维地图的相机同步，会在场景的根节点加下面一个子节点Group，三维相机与二维地图所有的坐标转换都会作用在这个子节点上。上面所说的世界坐标是指相对于此子节点的世界坐标。而真正的根节点的世界坐标我们用场景坐标来表示。其转换关系为：

scene2World(三维场景中的场景坐标转世界坐标）

world2Scene(三维场景中的世界坐标转场景坐标）

此转换关系极少用到，除非在`射线拾取`（射线拾取的是场景坐标）或 `shader` 时会用到。
:::

<mapFrame>
<pre>
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// 打开地图
let res = await svc.openMap({
    mapid: env.exampleMapId, // 地图ID
    // @ts-ignore
    mapopenway: vjmap.MapOpenWay.GeomRender, // 以几何数据渲染方式打开
    style: vjmap.openMapLightStyle() // div为深色背景颜色时，这里也传深色背景样式
})
if (res.error) {
    // 如果打开出错
    showError(res.error)
    return;
}
// 获取地图范围
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
// 根据地图范围建立几何投影坐标系
let prj = new vjmap.GeoProjection(mapBounds);
//
// 地图对象
let map = new vjmap.Map({
    container: 'map', // DIV容器ID
    style: svc.rasterStyle(), // 样式，这里是栅格样式
    center: prj.toLngLat(mapBounds.center()), // 设置地图中心点
    zoom: 2, // 设置地图缩放级别
    pitch: 45,
    antialias: true, // 反锯齿
    renderWorldCopies: false // 不显示多屏地图
});
//
// 关联服务对象和投影对象
map.attach(svc, prj);
await map.onLoad();
//
// 创建3d图层
let mapLayer = new vjmap3d.MapThreeLayer(map, {
    stat: {
        show: true,
        left: "0"
    }
});
map.addLayer(new vjmap.ThreeLayer({ context: mapLayer }))
let app = mapLayer.app;
// 
let data = [];
let markers = [];
let color1 = new THREE.Color("#0ff");
let color2 = new THREE.Color("#f00");
let cnt = 6
for(let x = 0; x <= cnt; x++) {
    for(let y = 1; y <= cnt; y++) {
        for(let z = 0; z <= cnt; z++) {
            let px = mapBounds.min.x + x * mapBounds.width() / cnt;
            let py = mapBounds.min.y + y * mapBounds.height() / cnt;
            let pz = z * mapBounds.height() / cnt;
            let worldPos = vjmap3d.toWorld([px, py, pz]);
            let clr = new THREE.Color().copy(color1).lerp(color2, z / cnt);
            let div = vjmap3d.DOM.createStyledDiv(`&lt;div class="markertext${z}">&lt;/div>`, `.markertext${z}{font-weight:600;color:#${clr.getHexString()};text-align:center;display:block;padding-top:10px;font-size:1.3em;}`);
            let marker2d = new vjmap3d.Marker2D({
                element: div,
                anchor: 'bottom',
                fadeDistance: 300,
                allowOverlap: false
            });
            marker2d.setPosition(worldPos);
            marker2d.addTo(app)
            markers.push(marker2d);
            //
            data.push({
                position: worldPos.toArray(),
                color: clr,
                borderColor: clr,
                size: 10
            })
        } 
    } 
}
let symbol = new vjmap3d.SymbolEntity({ //SymbolEntity
    data: data,
    pixelRaycasting: true, // 像素大小拾取
    style: {
        shape: "circle",
        transparent: true,
        //fadeDistance: 500,
        borderWidth: 2,
        sizeAttenuation: false
    }
})
symbol.addTo(app);
const update = () => {
    for(let m of markers) {
        let position = m.position;
        let cad = vjmap3d.fromWorld(position.toArray())
        let lnglat = map.toLngLat(cad)
        let altitude = vjmap3d.fromDist(position.z, true)
        let scene = vjmap3d.map2dUtils.world2Scene(position)
        let posArr = [];
        posArr.push(`CAD坐标: ${cad[0].toFixed(2)}, ${cad[1].toFixed(2)}, ${cad[2].toFixed(2)}`)
        posArr.push(`经纬度坐标: ${lnglat[0].toFixed(2)}, ${lnglat[1].toFixed(2)}, ${altitude.toFixed(2)}`);
        posArr.push(`世界坐标: ${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}`)
        posArr.push(`场景坐标: ${scene.x.toFixed(2)}, ${scene.y.toFixed(2)}, ${scene.z.toFixed(2)}`)
        m.element.children[0].innerHTML = posArr.join("&lt;br/>")
    }
}
update()
setInterval(() => {
    update()
}, 3000)
</pre>
</mapFrame>