You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Three.js地球模型标记仅映射半个球体问题求助

问题:Three.js地球模型标记仅显示半个地球,无法跨越“接缝”

基于Three.js实现带地图纹理的地球模型,标记通过经纬度映射到球面上,但发现标记仅能显示在半个地球上,手动调整标记位置也无法越过EAS标记旁的“接缝”。

核心代码

初始化代码

EarthView3D.Init = function (siteObjects, animateCallback) {

    const EARTH_RADIUS = 225;
    const EARTH_SEGMENTS = 75;
    const EARTH_RINGS = 75;

    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
    camera.position.set(-70, 375, 500);
    scene = new THREE.Scene();

    var light = new THREE.PointLight(0xffffff, 1, Infinity);
    camera.add(light);

    scene.add(camera);

    // Starfield
    var starGeometry = new THREE.SphereGeometry(1000, 50, 50); //basically creates another sphere around the earth's sphere, and covers it in starfield image.
    var starMaterial = new THREE.MeshPhongMaterial({
        map: new THREE.ImageUtils.loadTexture(Global.basePath + 'Content/images/galaxy_starfield.png'),
        side: THREE.DoubleSide,
        shininess: 0
    });
    var starField = new THREE.Mesh(starGeometry, starMaterial);
    scene.add(starField);

    //paths to each of the texture images.
    var earthNoCloudsTex = Global.basePath + 'Content/images/earth_no_clouds_12k.jpg';
    var earthDarkTex = Global.basePath + 'Content/images/earth_dark_12k.jpg';
    var earthDarkInvertTex = Global.basePath + 'Content/images/earth_dark_invert_12k.jpg';
    var earthRedTex = Global.basePath + 'Content/images/earth_dark_red_12k.jpg';

    // Set to default texture, earth with no clouds.
    var currentTexture = earthNoCloudsTex;

    //Create the Earth's sphere and give it an initial texture image
    var earthSphere = new THREE.SphereGeometry(EARTH_RADIUS, EARTH_SEGMENTS, EARTH_RINGS);
    var earthMaterial = new THREE.MeshBasicMaterial({
        map: new THREE.ImageUtils.loadTexture(Global.basePath + 'Content/images/earth_no_clouds_12k.jpg')
    });
    var theEarth = new THREE.Mesh(earthSphere, earthMaterial); //mesh them together, to create the earth.
    scene.add(theEarth); //add earth to the scene.

    //Adding 1st atmosphere, inner
    var innerAtmosphere = new THREE.SphereGeometry(EARTH_RADIUS, EARTH_SEGMENTS, EARTH_RINGS);
    var atmosMaterial = THREEx.createAtmosphereMaterial();
    atmosMaterial.uniforms.glowColor.value.set(0x00b3ff);
    atmosMaterial.uniforms.coeficient.value = 1;
    atmosMaterial.uniforms.power.value = 1.8;
    var atmosMesh = new THREE.Mesh(innerAtmosphere, atmosMaterial);
    atmosMesh.scale.multiplyScalar(1.001);
    scene.add(atmosMesh);

    //Adding 2nd atmosphere, the outer faded looking ring around inner atmosphere. 
    var outerAtmosphere = new THREE.SphereGeometry(EARTH_RADIUS, EARTH_SEGMENTS, EARTH_RINGS);
    var atmosMaterial2 = THREEx.createAtmosphereMaterial();
    atmosMaterial2.side = THREE.BackSide;
    atmosMaterial2.uniforms.glowColor.value.set(0x00b3ff);
    atmosMaterial2.uniforms.coeficient.value = .800;
    atmosMaterial2.uniforms.power.value = 4.0;
    var atmosMesh2 = new THREE.Mesh(outerAtmosphere, atmosMaterial2);
    atmosMesh2.scale.multiplyScalar(1.06);
    scene.add(atmosMesh2);

    //Add toggle click event for changing earth background texture
    $('.texture_change_button').on("click", function (e) {
        //figure out which button was clicked
        var buttonClicked = $(this).attr('id');
        //disable all currently disabled buttons before disabling another
        $(" :disabled").attr('disabled', false);
        //disable button clicked so that it can't be clicked twice.
        $(this).attr('disabled', true);
        //depending on the id of the buttonClicked, update the currentTexture
        switch (buttonClicked) {
            case 'earth_no_clouds_button':
                currentTexture = earthNoCloudsTex;
                break;
            case 'earth_dark_button':
                currentTexture = earthDarkTex;
                break;
            case 'earth_dark_invert_button':
                currentTexture = earthDarkInvertTex;
                break;
            case 'earth_red_button':
                currentTexture = earthRedTex;
        }
        //switch out the image with new currentTexture
        earthMaterial.map = THREE.ImageUtils.loadTexture(currentTexture);
        theEarth.material.needsUpdate = true;
        scene.add(theEarth);
    });

    // Add siteObjects to Earth
    for (var i = 0; i < siteObjects.length; i++) {

        var siteID = siteObjects[i].SiteID;
        var name = siteObjects[i].Name;
        var lat = siteObjects[i].LAT;
        var lon = siteObjects[i].LON;
        var status = siteObjects[i].Status;

        // Create dot for each site
        var siteDiv = document.createElement('div');
        siteDiv.setAttribute("id", name);

        //set data attributes to later use when clicking dot.
        siteDiv.setAttribute("data-siteid", siteID);

        siteDiv.setAttribute("title", name);
        siteDiv.className = status.toLowerCase() == "fmc" ? "site_status fmc" :
            status.toLowerCase() == "pmc" ? "site_status pmc" :
                "site_status nmc";

        //Create label to append to each site's dot
        var siteDivLabel = document.createElement('label');
        siteDivLabel.setAttribute("id", name + '_label');
        siteDivLabel.setAttribute("title", name + ' label');
        siteDivLabel.textContent = name;

        //Setting the label's class (Some labels need to display on different sides of the dot so that all can be seen in initial view.)
        if (siteID == 6 || siteID == 9) { // 6 is BUckley, 9 is COD
            siteDivLabel.className = 'label_top_right earth_view_label';
        } else if (siteID == 12 || siteID == 20 || siteID == 19) { // 12 is FGA, 20 is PAFB, 19 is MDIOC
            siteDivLabel.className = 'label_bottom_right earth_view_label';
        } else if (siteID == 8) { // 8 is CMD
            siteDivLabel.className = 'label_bottom_left earth_view_label';
        } else {
            siteDivLabel.className = 'label_top_left earth_view_label';
        }
        siteDiv.appendChild(siteDivLabel);

        var siteDot = new THREE.CSS2DObject(siteDiv);

        siteDiv.parent = siteDot;

        siteDot.element.style.cursor = "pointer";

        // Add click event for each dot/label that takes them to the site section for the dashboard homepage.
        siteDot.element.onclick = function () {
            var siteSK = $(this).data('siteid');

            EarthView3D.Edit(siteSK);
        };

        var sitePosition = EarthView3D.CalcPosFromLatLonRad(lat, lon, EARTH_RADIUS);
        // Some code to slightly move certain site markers around from their original position so that they don't overlap each other too much.
        if (siteID == 5) { // 5 is boulder
            siteDot.position.set(sitePosition[0] - 1, sitePosition[1] + 1, Math.abs(sitePosition[2]));
        } else if (siteID == 8) { // 8 is CMD
            siteDot.position.set(sitePosition[0] - 1, sitePosition[1] - 1, Math.abs(sitePosition[2]));
        } else if (siteID == 11) { //11 is EAS
            siteDot.position.set(sitePosition[0], sitePosition[1] + 1, Math.abs(sitePosition[2]));
        }else if (siteID == 20) { // 20 is PAFB
            siteDot.position.set(sitePosition[0] + 1, sitePosition[1] - 6, Math.abs(sitePosition[2]));
        } else if (siteID == 19) { // 19 is MDIOC
            siteDot.position.set(sitePosition[0] + 5, sitePosition[1] + 4, Math.abs(sitePosition[2]));
        } else if (siteID == 6) { // 6 is Buckley
            siteDot.position.set(sitePosition[0] + 2, sitePosition[1] + 3, Math.abs(sitePosition[2]));
        } else {
            siteDot.position.set(sitePosition[0], sitePosition[1], Math.abs(sitePosition[2]));
        }
        scene.add(siteDot);
    }

    renderer = new THREE.WebGLRenderer({ alpha: true });
    renderer.setClearColor(0xffffff, 0);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    var earthContainer = document.getElementById('earth_container');
    earthContainer.appendChild(renderer.domElement);

    labelRenderer = new THREE.CSS2DRenderer();
    labelRenderer.setSize(window.innerWidth, window.innerHeight);
    labelRenderer.domElement.style.position = 'absolute';
    labelRenderer.domElement.style.top = 0;
    document.body.appendChild(labelRenderer.domElement);

    // For rotation and mouse controls
    var controls = new THREE.OrbitControls(camera, labelRenderer.domElement);
    controls.minDistance = 400;
    controls.maxDistance = 800;
    animateCallback(siteObjects); //Running Animate right after Init.
};

经纬度转标记位置代码

EarthView3D.CalcPosFromLatLonRad = function (lat, lon, radius) {

    var phi = (90 - lat) * (Math.PI / 180)
    var theta = (lon + 180) * (Math.PI / 180)

    x = -((radius) * Math.sin(phi) * Math.cos(theta))
    z = ((radius) * Math.sin(phi) * Math.sin(theta))
    y = ((radius) * Math.cos(phi))

    return [x, y, z];
};

问题原因

所有标记的z坐标都被强制取了绝对值Math.abs(sitePosition[2])。经纬度转换函数CalcPosFromLatLonRad会根据经度计算出正负不同的z值:当经度在0180度时z为正,经度在-1800度时z为负。但取绝对值后,所有标记的z坐标都变成了正数,相当于把所有标记都“压”到了z轴正方向的半球上,自然只能显示半个地球,且无法越过z=0的边界(也就是你说的“接缝”)。

解决方案

去掉所有设置标记位置时对z坐标的Math.abs()处理,直接使用计算出来的原始sitePosition[2]值。修改后的代码片段如下:

var sitePosition = EarthView3D.CalcPosFromLatLonRad(lat, lon, EARTH_RADIUS);
// Some code to slightly move certain site markers around from their original position so that they don't overlap each other too much.
if (siteID == 5) { // 5 is boulder
    siteDot.position.set(sitePosition[0] - 1, sitePosition[1] + 1, sitePosition[2]);
} else if (siteID == 8) { // 8 is CMD
    siteDot.position.set(sitePosition[0] - 1, sitePosition[1] - 1, sitePosition[2]);
} else if (siteID == 11) { //11 is EAS
    siteDot.position.set(sitePosition[0], sitePosition[1] + 1, sitePosition[2]);
}else if (siteID == 20) { // 20 is PAFB
    siteDot.position.set(sitePosition[0] + 1, sitePosition[1] - 6, sitePosition[2]);
} else if (siteID == 19) { // 19 is MDIOC
    siteDot.position.set(sitePosition[0] + 5, sitePosition[1] + 4, sitePosition[2]);
} else if (siteID == 6) { // 6 is Buckley
    siteDot.position.set(sitePosition[0] + 2, sitePosition[1] + 3, sitePosition[2]);
} else {
    siteDot.position.set(sitePosition[0], sitePosition[1], sitePosition[2]);
}

修改后,标记会根据经纬度正确映射到整个地球的球面上,不再受限于半个地球,也能正常跨越经度±180度的区域。

内容的提问来源于stack exchange,提问作者JakeFromStateFarm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 02:10:32