实战篇40 # 如何实现3D地球可视化?

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实战篇40 # 如何实现3D地球可视化?相关的知识,希望对你有一定的参考价值。

说明

【跟月影学可视化】学习笔记。

如何实现一个 3D 地球

学习笔记源码实现:https://github.com/kaimo313/visual-learning-demo

整体实现效果如下:

1、绘制一个 3D 球体

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>绘制一个 3D 球体</title>
        <style>
            #container 
                width: 600px;
                height: 600px;
                border: 1px dashed salmon;
            
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="http://unpkg.com/spritejs/dist/spritejs.js"></script>
        <script src="http://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.js"></script>
        <script>
            const  Scene  = spritejs;
            const  Sphere, shaders  = spritejs.ext3d;

            const container = document.getElementById("container");
            // 创建场景对象
            const scene = new Scene(
                container,
            );
            // 添加 Layer,设置透视相机,视角为 35 度,位置为 0, 0, 5
            const layer = scene.layer3d("fglayer", 
                alpha: false,
                camera: 
                    fov: 35,
                    pos: [0, 0, 5],
                ,
            );
            // shaders.GEOMETRY 是一个符合 Phong 反射模型的几何体 Shader
            const program = layer.createProgram(
                ...shaders.GEOMETRY,
                cullFace: null,
            );
            // 创建一个球体
            const globe = new Sphere(program, 
                colors: "skyblue",
                widthSegments: 64,
                heightSegments: 32,
                radius: 1,
            );

            layer.append(globe);
        </script>
    </body>
</html>

2、绘制地图

先绘制一张平面地图,然后把它以纹理的方式添加到我们创建的 3D 球体上。用 d3-geo 模块来创建等角方位投影(Equirectangular Projection)。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>绘制地图</title>
        <style>
            #container 
                width: 960px;
                height: 480px;
                border: 1px dashed salmon;
            
        </style>
    </head>
    <body>
        <canvas id="container"></canvas>
        <script src="https://lib.baomitu.com/topojson/3.0.2/topojson.min.js"></script>
        <script src="https://d3js.org/d3-array.v2.min.js"></script>
        <script src="https://d3js.org/d3-geo.v2.min.js"></script>
        <script>
            const ctx = document.getElementById("container").getContext("2d");
            // d3 的地图投影默认宽高
            const mapWidth = 960;
            const mapHeight = 480;
            // 将投影缩放为 4 倍,也就是将地图绘制为 3480 * 1920 大小。
            const mapScale = 4 / 13;

            // 创建等角方位投影
            const projection = d3.geoEquirectangular();
            // 通过 tanslate 将中心点调整到画布中心
            projection
                .scale(projection.scale() * mapScale)
                .translate([
                    mapWidth * mapScale * 0.5,
                    (mapHeight + 2) * mapScale * 0.5,
                ]);

            // 使用 topoJSON 数据加载地图
            async function loadMap(
                src = topojsonData,
                 strokeColor, fillColor  = 
            ) 
                const data = await (await fetch(src)).json();
                const countries = topojson.feature(
                    data,
                    data.objects.countries
                );
                // 创建一个离屏 Canvas,用加载的数据来绘制地图到离屏 Canvas 上
                const canvas = new OffscreenCanvas(
                    mapScale * mapWidth,
                    mapScale * mapHeight
                );
                const context = canvas.getContext("2d");
                context.imageSmoothingEnabled = false;
                return drawMap( context, countries, strokeColor, fillColor );
            

            // 绘制地图
            function drawMap(
                context,
                countries,
                strokeColor = "#666",
                fillColor = "salmon",
                strokeWidth = 1.5,
             = ) 
                const path = d3.geoPath(projection).context(context);

                context.save();
                context.strokeStyle = strokeColor;
                context.lineWidth = strokeWidth;
                context.fillStyle = fillColor;
                context.beginPath();
                path(countries);
                context.fill();
                context.stroke();
                context.restore();

                return context.canvas;
            
            loadMap("./data/world-topojson.json").then((res) => 
                console.log(res)
                ctx.drawImage(res, 0, 0);
            );
        </script>
    </body>
</html>

3、将地图作为纹理

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>将地图作为纹理</title>
        <style>
            #container 
                width: 600px;
                height: 600px;
                border: 1px dashed salmon;
            
        </style>
    </head>
    <body>
        <div id="container"></div>
        <script src="http://unpkg.com/spritejs/dist/spritejs.js"></script>
        <script src="http://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.js"></script>
        <script src="https://lib.baomitu.com/topojson/3.0.2/topojson.min.js"></script>
        <script src="https://d3js.org/d3-array.v2.min.js"></script>
        <script src="https://d3js.org/d3-geo.v2.min.js"></script>
        <script type="module">
            import  vertex, fragment  from './assets/js/40/shader.js';
            const  Scene  = spritejs;
            const  Sphere, shaders  = spritejs.ext3d;

            // d3 的地图投影默认宽高
            const mapWidth = 960;
            const mapHeight = 480;
            // 将投影缩放为 4 倍,也就是将地图绘制为 3480 * 1920 大小。
            const mapScale = 4;

            // 创建等角方位投影
            const projection = d3.geoEquirectangular();
            // 通过 tanslate 将中心点调整到画布中心
            projection
                .scale(projection.scale() * mapScale)
                .translate([
                    mapWidth * mapScale * 0.5,
                    (mapHeight + 2) * mapScale * 0.5,
                ]);

            // 使用 topoJSON 数据加载地图
            async function loadMap(
                src = topojsonData,
                 strokeColor, fillColor  = 
            ) 
                const data = await (await fetch(src)).json();
                const countries = topojson.feature(
                    data,
                    data.objects.countries
                );
                // 创建一个离屏 Canvas,用加载的数据来绘制地图到离屏 Canvas 上
                const canvas = new OffscreenCanvas(
                    mapScale * mapWidth,
                    mapScale * mapHeight
                );
                const context = canvas.getContext("2d");
                context.imageSmoothingEnabled = false;
                return drawMap( context, countries, strokeColor, fillColor );
            

            // 绘制地图
            function drawMap(
                context,
                countries,
                strokeColor = "#666",
                fillColor = "salmon",
                strokeWidth = 1.5,
             = ) 
                const path = d3.geoPath(projection).context(context);

                context.save();
                context.strokeStyle = strokeColor;
                context.lineWidth = strokeWidth;
                context.fillStyle = fillColor;
                context.beginPath();
                path(countries);
                context.fill();
                context.stroke();
                context.restore();

                return context.canvas;
            

            const container = document.getElementById("container");
            // 创建场景对象
            const scene = new Scene(
                container,
            );
            // 添加 Layer,设置透视相机,视角为 35 度,位置为 0, 0, 5
            const layer = scene.layer3d("fglayer", 
                alpha: false,
                camera: 
                    fov: 35,
                    pos: [0, 0, 5],
                ,
            );
            // 创建一个 Texture 对象,将它赋给 Program 对象
            const texture = layer.createTexture();
            // 加载数据
            loadMap("./data/world-topojson.json").then((map) => 
                console.log(map)
                texture.image = map;
                texture.needsUpdate = true;
                layer.forceUpdate();
            );
            // 创建 Program
            const program = layer.createProgram(
                vertex,
                fragment,
                texture,
                cullFace: null,
            );
            // 创建一个球体
            const globe = new Sphere(program, 
                colors: "skyblue",
                widthSegments: 64,
                heightSegments: 32,
                radius: 1,
            );
            layer.append(globe);
            // 开启旋转控制
            layer.setOrbit(autoRotate: true);
        </script>
    </body>
</html>

如何实现星空背景

创建一个天空包围盒,让摄像机处于整个球体内部,使用二维噪声的技巧来实现来其 Shader,通过 step 函数和 vUv 的缩放,将它缩小之后,最终呈现出来星空效果。

注意这里我们需要关闭旋转控制。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>如何实现星空背景</title>
        <style>
            html,
            body 
                width: 100%;
                height: 100%;
                padding: 0;
                margin: 0;
                overflow: hidden;
            
            #container 
                width: 100%;
                height: 100%;
            
        </style>
    </head>
    <body>
        <div id="container"></div>
        简述高斯投影原理

手把手教你做出数据可视化项目3D地球旋转

手把手教你做出数据可视化项目3D地球旋转

手把手教你做出数据可视化项目3D地球旋转

墨卡托投影坐标系(Mercator Projection)原理及实现C代码

视觉高级篇27 # 如何实现简单的3D可视化图表:GitHub贡献图表的3D可视化?