实战篇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>
简述高斯投影原理