唯杰地图 logo唯杰地图
VJMAP
EN

坐标转换

vjmap3d/references/graphics/coordTransform.md 21.9 KB546 行
原文

基本概念

什么是屏幕坐标系?

ThreeJS使用了canvas画布绘制图形,屏幕坐标即画布上的像素位置。注意 vjmap3d 的 project/unproject/screenToNdc/ndcToScreen 用的是浏览器视口 client 坐标(如 event.clientX/clientY,内部会加减容器 getBoundingClientRect() 的 left/top),不是 canvas 局部坐标(offsetX/offsetY)。

什么是世界坐标系?

世界坐标系(三维坐标系)分为两种,左手坐标系和右手坐标系。

左手坐标系:BabylonJS

右手坐标系:ThreeJS(下图就是右手坐标系)

什么是标准化设备坐标系?

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

透视投影原理

屏幕坐标系、世界坐标系、标准化设备坐标系之间的关系

将二维坐标映射到三维空间时,首先需要将二维坐标转换为 NDC 坐标。这可以通过将二维坐标的值除以视口的尺寸来实现。

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

物体坐标系之间的转换关系大致为:

        局部坐标 -> 世界坐标 -> 观察空间坐标 -> 裁剪空间坐标 -> 屏幕空间坐标

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

因此坐标转换过程就变成了:

        局部坐标 -> 世界坐标 -> 标准设备坐标 -> 屏幕空间坐标

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

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

  在 ThreeJS 中有一个方法 Vector3.project(camera) 综合了这两步:
 

project( camera ) {    
    return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );
}

三维场景坐标转换

// 增加坐标信息
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/>`
}
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)}<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/>`
    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")

三维互联网地图做为底图场景坐标转换

// 增加坐标信息
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)))
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)}<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)))

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

提示

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

涉及到距离转换时使用:

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

toDist(CAD距离 转世界坐标距离)

注意

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 层没有这些方法。
// 增加坐标信息
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)))
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)}<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)))

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

注意

独立 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)。

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))
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)}<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))

提示

涉及到距离转换时使用:

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

toDist(CAD距离 转世界坐标距离)

注意

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

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

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

此转换关系极少用到,除非在射线拾取(射线拾取的是场景坐标)或 shader 时会用到。

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(`<div class="markertext${z}"></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("<br/>")
    }
}
update()
setInterval(() => {
    update()
}, 3000)