你好汉语翻译(译文你好3D)
你好汉语翻译(译文你好3D)image.png数据是二进制格式,所以为了帮助任何想要使用 R 读取数据的人,以下格式和GLOBE 数据手册中的表 3将有所帮助。几年前,互联网上有一个世界人口的地理可视化,它被用作某种地理社会的封面(在完成实际的 3D 实现之前我找不到它,但后来我想起了这个词写这篇文章之前的人口线使它只是一个快速的谷歌搜索)。这是否也适用于 3D 版本(使用标准的数字高程模型,而不是人口数据)还是在视觉上太忙了?坚持使用纬度线和经度是否有意义?除了测试和查看之外别无他法。我们需要掌握高程数据。搜索 DEM(数字高程模型)有一些可以免费使用,但它们很难批量下载,幸运的是,GLOBE 项目将它们的切片压缩下载,合理的组合文件大小为 ~ 300MB。
我最近需要一种自定义方式来显示地理位置数据,这对于 CV 网站来说会很流行。最终结果如下面的 GIF 所示。
本文将引导您完成从灵感、数据准备和使用 WebGL 到 THREE.js 实现的步骤。
示例代码将是我的特定用例所需的地球仪的精简版本,但将呈现相同的几何图形,但不会进入平移和动画 - 允许更连贯和顺序的代码。我仍然会提示哪些属性可以出于性能原因进行动画处理。Github(https://github.com/Tille88/cv_proj/tree/master/globe) 上提供的代码
「第 1 步:灵感」我希望有一些与线框模型有相似感觉的东西,同时可以清楚地识别为地理位置。
几年前,互联网上有一个世界人口的地理可视化,它被用作某种地理社会的封面(在完成实际的 3D 实现之前我找不到它,但后来我想起了这个词写这篇文章之前的人口线使它只是一个快速的谷歌搜索)。
这是否也适用于 3D 版本(使用标准的数字高程模型,而不是人口数据)还是在视觉上太忙了?坚持使用纬度线和经度是否有意义?除了测试和查看之外别无他法。
第 2 步:可行性和数据控制我们需要掌握高程数据。搜索 DEM(数字高程模型)有一些可以免费使用,但它们很难批量下载,幸运的是,GLOBE 项目将它们的切片压缩下载,合理的组合文件大小为 ~ 300MB。
数据是二进制格式,所以为了帮助任何想要使用 R 读取数据的人,以下格式和GLOBE 数据手册中的表 3将有所帮助。
image.png
# Example code for reading in the A10G
connection = file("./all10/a10g" "rb")
N_COL = 10800
N_ROW = 4800# Read in the binary data NOTE data has little endian format
data = readBin(connection integer() n=N_COL*N_ROW size=2 endian = "little")
close(conn)# Set dimensions of data matrix
dim(data) = c(N_COL N_ROW)
之后,出于可行性考虑,我致力于完成静态版本,沿纬度以固定距离对高程数据进行二次采样,并在绘制结果之前对其应用内核。
image.png
斯堪的纳维亚高程平滑线
数据看起来不错,所以是时候切换到 3D 版本了
(「注意:」 当很明显使用原始数据可以工作时,我没有在第一次尝试后使用带有内核的高程数据)
第 3 步:3D 地球仪THREE.js 在 WebGL 之上提供了很多不错的抽象,让我们能够停留在 Javascript 中,避免学习 GLSL 的时间稍微长一点。我们还需要很多东西:
i) 一个场景
ii) 相机
iii)灯光(我们需要看到一些东西)
iv) 场景中的物体/几何图形(我们需要有一些东西可以看到)
v) 渲染器
vi) 控件/动画
在示例代码中,我将初始化变量,然后直接在函数中传递给不同的构造init()函数。任何需要保存对render()循环的引用的东西都将在脚本开始时声明为全局变量。
「注意」:这个“代码结构”很快就变得不可维护,它已经隐藏了一个稍微好一点的方法,并在 repo 中找到的实际项目代码中分成不同的对象。这里按照正文介绍顺序,这对了解 API 来说是件好事。
全局变量渲染循环需要一些东西。
// Globalsvar scene renderer camera controls;
i) 场景
我们现在在init()函数内部。我们在其中获得对 HTML 中声明的 div 元素的引用,该元素将包含 webGL-canvas。
// Get frame referencevar frame = document.getElementById('globe-scene-container');// Init scenescene = new THREE.Scene();
ii) 相机
为了展示选择的值,以及它们是如何为相机 API 排序的,下面的代码片段创建了一个相机。
网上有很多教程会更详细地介绍所有这些值的含义,但请注意使用camera.up.set(0 0 1)哪个设置相机的向上方向,给我们一个更符合 3D 数学的坐标系我个人是在学校学习的,而不是在游戏编程中使用的那种。顺便说一句,它也是维基百科关于球坐标系的文章中使用的一种,无需修改即可轻松使用任何公式。
这些值中的大多数是在知道场景中的对象有多大时设置的,以便可以将大多数事物保持在视图中。
var cameraConfigs = {
FOV: 35
ASPECT: frame.clientWidth/frame.clientHeight
NEAR: 0.1
FAR: 200
INIT_POS: {
x: 2
y: 0.6
z: 4
}
};
**camera** = **new THREE.PerspectiveCamera**(
cameraConfigs.FOV
cameraConfigs.ASPECT
cameraConfigs.NEAR
cameraConfigs.FAR
**)** ;
var pos = cameraConfigs.INIT_POS;
camera.position.set(pos.x pos.y pos.z);
// Setting up direction to make sure coordinate system used is correct
**camera.up.set(0 0 1);**
// Placement of globe will have center at [0 0 0]
camera.lookAt( 0 0 0 );
iii) 灯
「注意:」 对于本例中使用的材质,它们都不受灯光的影响。但是对于几乎任何其他场景,都需要添加灯光来显示它们。因此,最好在场景中有一些东西,以防要添加更多对象。
我们只需要一盏灯。我们不想从镜面光照或类似的东西中获得任何特定的效果——没有阴影的恒定光源就可以了——在 THREE.js 中转换为 AmbientLight
请注意,灯光的颜色是以 HEX 格式传入的,或者使用 THREE.js 方便的Color 构造函数。
//Init light and add to scene
var light = new THREE.AmbientLight( 0x04040c ) ;
scene.add(light)
iv) 对象/几何
对象将分两步添加。
首先,我们将添加一个构成地球的球体对象,并在接下来的几个步骤中确保正确放置和渲染它。之后,我们将向场景中添加其他对象。
我们只想要一种不完全「不透明度」的基本材质——也允许它背面的东西部分可见——以及恒定的颜色(即不受任何灯光影响)。
var globeConfigs = {
WIDTH_SEGS: 16
HEIGHT_SEGS: 16
GLOBE_OPACITY: 0.7
EARTH_RAD: 1
MULTIPLIER: 1E-5
COLOR: 0x04040c
};
var earthGeometry = new THREE.SphereGeometry(
globeConfigs.EARTH_RAD
globeConfigs.WIDTH_SEGS
globeConfigs.HEIGHT_SEGS
);
var earthMaterial = new THREE.MeshPhongMaterial({
transparent: true
opacity: globeConfigs.GLOBE_OPACITY
color: globeConfigs.COLOR
});
var earthMesh = new THREE.Mesh(earthGeometry earthMaterial);
earthMesh.position.set(0 0 0);
scene.add(earthMesh);
v) 渲染器
渲染器绝对需要显示场景中的任何内容。下面的大部分代码在命名上都非常明确,并且使所发生的事情变得相对明显。请注意我们从上面的 i) 场景中保留的帧参考的使用。
// Init renderer
renderer = new THREE.WebGLRenderer({antialiasing : true});
renderer.setSize(frame.clientWidth frame.clientHeight);
renderer.domElement.style.position = 'relative';
// Background set to black
renderer.setClearColor (0x000000 1);
frame.appendChild(renderer.domElement);
renderer.render(scene camera);
调用该init()函数后,我们现在可以看到渲染到屏幕的内容。这是相当多的代码,只是为了得到一些东西——任何东西——在屏幕上显示,如果从头开始可能需要相当多的迭代才能以你想象的方式显示东西。
所有打字的黑色背景上的一个几乎黑色的球体...... 3D 编程的反高潮
vi) 控件/动画我们还希望允许场景中的对象进行一些移动。
我们可以选择自己为旋转设置动画并尝试计算鼠标交互,但这是一项非常常见的任务,因此有一个名为OrbitControls的 THREE.js 扩展。
「注意:」 轨道控制允许摄像机围绕目标进行轨道运行。 然而,没有任何东西可供眼睛参考,它将具有与地球旋转相同的视觉效果。
对函数做一个小的补充init():
// Init controls [inside init() function]
controls = new THREE.OrbitControls( camera );
controls.autoRotate = true;
还需要定期更新和重新渲染场景以查看我们的控件的运行情况,因此我们需要一个在render()函数之后调用的init()函数。这只是更新控件、重新渲染并安排对渲染函数的新调用。任何额外的“绘画”更新都可以在调用renderer.render().
var render = function render(){
controls.update();
renderer.render(scene camera);
requestAnimationFrame(render);
}
init();
render();
我们将稍微调整一下地球,看看更改是否生效。将globeConfig.[WIDTH_SEGS|HEIGHT_SEGS]16 设置为 6 以使其不那么圆并改变球体的颜色。
image.png
是时候将球体的颜色和形状改回我们之前的样子,并在我们的地球仪上添加一些其他的东西了。
「注意:」 我们很幸运地让地球在正确的方向上“旋转”而没有任何改变,一旦我们添加高程就会看到。
iv) 对象/几何(重新访问)首先,我们要为「高程添加」 「线条」。要将其绘制到局部坐标系,我们将需要一些辅助函数,关键函数如下所示。****
对于 Medium 版本的代码,我只是在调用 init() 和 render() 函数之前将它们一一添加到脚本标签中。
/**
* @param {number} lat latitude
* @param {number} lon longitude
* @param {number} rad radius
* @returns {object} object containing x y z fields
*/
var latLonToSphere = function(lat lon rad){
return {
x: rad * Math.cos(radians(lat))*Math.cos(radians(lon))
y: rad * Math.cos(radians(lat))*Math.sin(radians(lon))
z: rad * Math.sin(radians(lat))
};
};
该prepareLineData()函数根据上面的帮助函数从输入格式转换数据。内部细节不太重要,但是有了这些可用的助手,我们可以创建这些线条并将它们添加到场景中,如下面的代码所示。在第一次渲染后我们没有对它们进行任何更改,因此我们将所有内容都填充到 init 函数中,以使代码的阅读尽可能容易。
var convertedLineData = prepareLineData(lineData globeConfigs.EARTH_RAD globeConfigs.MULTIPLIER);
var lineGroup = new THREE.Group();
var materialLine = new THREE.LineBasicMaterial({color: 0x3393B8});
// Loop over converted data
// Each key in the object is a latitude
// Each latitude key has an array each containing array being the data with lat/lon/alt being in the coordinate frame for one single 'line'
// The line is constructed and added to a group.
for(var lat in convertedLineData){
convertedLineData[lat].forEach((arr) => {
var vectorArr = arr.map(p => new THREE.Vector3( p.x p.y p.z ));
if(vectorArr.length <= 1) { return; }
var curve = new THREE.CatmullRomCurve3(vectorArr);
var points = curve.getPoints( vectorArr.length*4 );
var geometry = new THREE.BufferGeometry().setFromPoints( points );
var curveObject = new THREE.Line( geometry materialLine );
lineGroup.add(curveObject);
});
}
// Lastly the whole lineGroup is added to the scene
scene.add(lineGroup);
在这些添加之后,我们完成了可视化的基础,如下面的 GIF 所示,我们终于可以看到降低的不透明度对整体视觉冲击的简洁效果。
image.png
第二步是添加连接两个位置的路径。
在这里,我们将在其中偷工减料
- 它和其他所有东西一样都是静态的,这意味着在初始化之后我们根本不会碰它。
- 我们引入了很多神奇的数字,它们会显示 API 的使用情况,但在实际项目中是有问题的。
latLonToSphere()除了称为 的线性插值函数外,还使用了映射函数lerp()。
init()在通常函数中的行代码之后添加以下代码。我们只计算三个位置——起点、中间点和终点。这些被输入二次贝塞尔曲线构造函数,并插入到场景中。
对于这条曲线,并不严格要求使用如下所示的 BufferGeometry(),但它可以访问geometry.setDrawRange(fromPoint toPoint)方法,根据文档,该方法用于动画(设置geometry.setDrawRange(0 0)和动画到终点或反向)。
var LAT_LONS = {
// Stockholm
ST: {lat: 59.3 lon: 18.0}
// Shanghai
SH: {lat: 31.2 lon: 121.4}
};
// Start middle and end positions of curve
var start = latLonToSphere(LAT_LONS.ST.lat LAT_LONS.ST.lon globeConfigs.EARTH_RAD);
var middle = latLonToSphere(
lerp(LAT_LONS.ST.lat LAT_LONS.SH.lat 0.5)
lerp(LAT_LONS.ST.lon LAT_LONS.SH.lon 0.5)
globeConfigs.EARTH_RAD 0.5
);
var end = latLonToSphere(LAT_LONS.SH.lat LAT_LONS.SH.lon globeConfigs.EARTH_RAD);
var curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3( start.x start.y start.z )
new THREE.Vector3( middle.x middle.y middle.z )
new THREE.Vector3( end.x end.y end.z )
);
var points = curve.getPoints(50);
var geometry = new THREE.BufferGeometry().setFromPoints( points );
var material = new THREE.LineBasicMaterial( { color : 0xff0000 } );
var curveObject = new THREE.Line( geometry material );
scene.add(curveObject);
最终展示
好的,所以我们已经用一个静态但旋转的地球仪达到了这一点,它看起来像这样:
image.png
随着文本的继续添加所有内容——并且以尽可能线性的方式——我们最终得到了一个巨大的init()函数,它完成了所有的工作,只使用了一些辅助函数。
在以任何方式扩展它之前,几乎需要进行重大的重构。我在主项目中的代码结构肯定不是完美的,并且是针对特定用例设置的,但肯定比这更好,可以在Github repo中找到。
「免责声明:」 这是我第一次使用 THREE.js,因此我使用 API 的方式可能效率低下,尽管我在编码时尝试尽可能多地研究它。
翻译自:https://medium.com/@jonas-tillman/hello-3d-world-bf3b79d9f89e