用 Three.js 创建一个酷炫且真实的地球

接下来我会分步骤讲解,在线示例在数字孪生平台。
先添加一个球体
我们用threejs中的SphereGeometry来创建球体,给他贴一张地球纹理。
let earthGeo = new THREE.SphereGeometry(10, 64, 64)
let earthMat = new THREE.MeshStandardMaterial({map: albedoMap,
})
this.earth = new THREE.Mesh(earthGeo, earthMat)
this.group.add(this.earth)
然后写一个函数让这个球体能够进行自转,并且该函数每帧都会调用。
updateScene(interval, elapsed) {this.controls.update()this.stats1.update()this.earth.rotateY(interval * 0.005 * params.speedFactor)
}
在屏幕上我们就会得到如下的效果。

添加凹凸贴图
地球上有山脉海洋,本质上是凹凸不平的,在太阳下也会呈现不同的阴影效果。那么为了让我们的地球更加真实,我们可以用 NASA 对外提供的地球的凹凸贴图,将其应用到地球的MeshStandardMaterial.
this.dirLight = new THREE.DirectionalLight(0xffffff, params.sunIntensity)
this.dirLight.position.set(-50, 0, 30)
scene.add(this.dirLight)let earthMat = new THREE.MeshStandardMaterial({map: albedoMap,bumpMap: bumpMap,bumpScale: 0.03,
})
这样我们就能够立即看山脉在阳光照耀下投下的阴影。

bumpScale这个值要尽量小一点,如果太大的话,会发现即使在地球的阴影面,山脉也会被照亮。
添加云
NASA官方同样提供了云的贴图。我们将云的贴图添加到一个单独的网格球体,并且半径要比地球大。这么做是为了后面模拟云在地球上投射阴影的效果。
const cloudsMap = await loadTexture(Clouds)
...let cloudGeo = new THREE.SphereGeometry(10.05, 64, 64)
let cloudsMat = new THREE.MeshStandardMaterial({alphaMap: cloudsMap,transparent: true,
})
this.clouds = new THREE.Mesh(cloudGeo, cloudsMat)
this.group.add(this.clouds)
添加云后真个效果就会看起来更加真实。

接下来我们实现云的投射阴影效果。具体做法:在地球纹理贴图上的任意 uv 点(X 点)上,找到云图上叠加的 uv 点(Y 点),提取 Y 点的颜色值。然后,我们根据 Y 点的颜色值(即 Y 点云的强度)使 X 点的颜色值变暗。
同时,因为云的旋转速度是地球的两倍,为了在地球的片段着色器中获得正确的阴影(云)位置,我们需要减去地球的坐标 uv_xOffset,这样结果将在 -1 到 1 的范围内,我们还需要为云纹理的wrapS设置RepeatWrapping,以便texture2D仍然适用于-1到0。
云层越浓的地方阴影应该越浓,因此我们用1.0减去cloudsMapValue来获得shadowValue,并将其乘以diffuseColor,我们还将 ShadowValue 限制为最小值 0.2,这样它就不会变得太暗。
shader.uniforms.tClouds = { value: cloudsMap }
shader.uniforms.tClouds.value.wrapS = THREE.RepeatWrapping;
shader.uniforms.uv_xOffset = { value: 0 }
shader.fragmentShader = shader.fragmentShader.replace('#include <emissivemap_fragment>', `// 因为这是在完成照明计算之前以及应用大多数默认颜色/纹理计算之后#include <emissivemap_fragment>float cloudsMapValue = texture2D(tClouds, vec2(vMapUv.x - uv_xOffset, vMapUv.y)).r;diffuseColor.rgb *= max(1.0 - cloudsMapValue, 0.2 );`)updateScene(interval, elapsed) {...this.earth.rotateY(interval * 0.005 * params.speedFactor)this.clouds.rotateY(interval * 0.01 * params.speedFactor)// 计算 uv_xOffset 并将其传递给地球的 MeshStandardMaterial 使用的着色器// X 点每旋转 n 弧度,Y 点就会旋转 2n 弧度// 因此,Y 点的 uv.x 始终等于 X 点的 (uv.x - n / 2π)// n 除以 2π 是将弧度(即 0 到 2π)转换为 uv 空间(即 0 到 1)// 偏移量 n / 2π 将通过 uv_xOffset 传递到着色器程序中// "offset % 1" 是因为 uv.x 的值 1 意味着完整的圆// 每当 uv_xOffset 大于 1 时,偏移 2π 弧度就像根本没有偏移一样const shader = this.earth.material.userData.shaderif ( shader ) {let offset = (interval * 0.005 * params.speedFactor) / (2 * Math.PI)shader.uniforms.uv_xOffset.value += offset % 1}}
完成这些更改后,现在我们得到了微妙但逼真的云阴影。

让海洋反射阳光
接下来我们可以让海洋看起来更加真实。水是一种很强的光反射体,尤其是当波浪安静或光的入射角较小时。通过利用粗糙度roughness和金属度metalness,这对于 MeshStandardMaterial 来说并不难做到。
const oceanMap = await loadTexture("./assets/Ocean.png")
const params = {...metalness: 0.1,
}
let earthMat = new THREE.MeshStandardMaterial({map: albedoMap,bumpMap: bumpMap,bumpScale: 0.03, // 一定要很小,如果太大连背面的凸的地方都会被照亮roughnessMap: oceanMap, // 以海洋贴图代替粗糙度贴图,将在着色器中反转灰度值metalness: params.metalness, // 与金属度贴图的纹理值相乘metalnessMap: oceanMap,
})
earthMat.onBeforeCompile = function( shader ) {...shader.fragmentShader = shader.fragmentShader.replace('#include <roughnessmap_fragment>', `float roughnessFactor = roughness;#ifdef USE_ROUGHNESSMAPvec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );texelRoughness = vec4(1.0) - texelRoughness;roughnessFactor *= clamp(texelRoughness.g, 0.5, 1.0);#endif`);...}
因为这里用的海洋贴图其实就只有黑白色,如果海洋区域为白色,我们必须反转黑白值以使粗糙度正确,因为我们希望陆地的粗糙度为“白色”(值:1)。对于水域,我们不希望它的粗糙度为零,因为这会使其看起来太像金属并且反光效果太强,海洋会失去其通常的蓝色,因此我们将粗糙度值限制在最小值 0.5。

夜景图
如果地球的夜晚是完全黑暗的,那就没有意义了,因为通常情况下都是会有夜景灯光。那我们接下的实现逻辑就是:在白天显示正常地球贴图,在阳光照不到的区域,用夜景图代替。
const lightsMap = await loadTexture(NightLights)
...let earthMat = new THREE.MeshStandardMaterial({...emissiveMap: lightsMap,emissive: new THREE.Color(0xffff88),
})
earthMat.onBeforeCompile = function( shader ) {...shader.fragmentShader = shader.fragmentShader.replace('#include <emissivemap_fragment>', `#ifdef USE_EMISSIVEMAPvec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv );// geometryNormal是视图空间中的标准化法线// 对于地球的夜晚一侧,geometryNormal和方向光之间的点积将为负// 对于地球的光照面,情况相反,因此 emissiveColor 将乘以 0// smoothstep用来平滑昼夜的变化emissiveColor *= 1.0 - smoothstep(-0.02, 0.0, dot(geometryNormal, directionalLights[0].direction));totalEmissiveRadiance *= emissiveColor.rgb;#endif...`)...
}
添加大气菲涅尔效果
菲涅尔效应可以想象,当我们在湖边看到离我们较远的湖水表面比我们近处的表面更清晰的反射时产生的效果;其实就是理解成,可以以更小的角度来更清晰地看到表面的反射。
虽然我们的海洋已经有了反光效果,但我们仍然缺少大气层。我们将分两步模拟大气效果:
- 调整地球纹理的漫反射颜色
diffuseColor,使其在观察者观察到的球体边缘附近呈现出更亮的蓝色(由于从较厚的路径积累了更多的光,边缘总是看起来更亮,看看下面的b比a路径长得多)

- 在云层和地球网格之上添加另一个球体网格作为大气层本身。
shader.fragmentShader = shader.fragmentShader.replace('#include <emissivemap_fragment>', `...计算自发光...计算云影// 添加少量大气菲涅耳效果,使其更加真实// 微调下面的第一个常数以获得更强或更弱的效果float intensity = 1.4 - dot( geometryNormal, vec3( 0.0, 0.0, 1.0 ) );vec3 atmosphere = vec3( 0.3, 0.6, 1.0 ) * pow(intensity, 5.0);diffuseColor.rgb += atmosphere;
`)
geometryNormal 是视图空间中地球表面的法线,视图空间就是以相机为原点的3D空间。所以 vec3( 0.0, 0.0, 1.0 ) 将是一个指向你自己(相机)的归一化向量。因此,在地球表面中心,dot(geometryNormal, vec3(0.0, 0.0, 1.0))的结果将恰好是1.0;从观察者(相机)的角度来看,地球边缘点结果将为 0.0,地球背面中心点的结果将为 -1.0。vec3( 0.3, 0.6, 1.0 )只是浅蓝色,想让效果更真实。

添加大气层
这将是我们要添加到地球上的最后一个效果。我们都看过从太空拍摄的地球的美丽照片,周围总是有一圈浅蓝色的细带,这就是大气层。至于材质,我们将使用 ShaderMaterial,这意味着我们将提供自己的顶点和片段着色器。
varying vec3 vNormal;
varying vec3 eyeVector;void main() {vec4 mvPos = modelViewMatrix * vec4( position, 1.0 );vNormal = normalize( normalMatrix * normal );eyeVector = normalize(mvPos.xyz);gl_Position = projectionMatrix * mvPos;
}
这里的 gl_Position 仍然使用标准公式计算,通常在一行内完成,但我将其分为两步,因为我想将 mvPos 的归一化值分配给 eyeVector。这两个变量将与 vNormal 一起用于片段着色器。
varying vec3 vNormal;
varying vec3 eyeVector;
uniform float atmOpacity;
uniform float atmPowFactor;
uniform float atmMultiplier;void main() {// 从边缘开始到地球背部的中心,dotP会从0增加到1float dotP = dot( vNormal, eyeVector );// 该因子是为了创造真实的大气增厚的效果float factor = pow(dotP, atmPowFactor) * atmMultiplier;// 在颜色中添加一点 dotP,使其更白,同时颜色加深vec3 atmColor = vec3(0.35 + dotP/4.5, 0.35 + dotP/4.5, 1.0);// 使用 atmOpacity 控制大气颜色的整体强度gl_FragColor = vec4(atmColor, atmOpacity) * factor;
}
对于片段着色器中应用的方法,我参考了一个方法,是仅渲染该大气网格的背面,使用附加混合使其看起来透明,并从球体边缘向其中心快速增加着色。这里为什么 dotP 会向中心从 0 增加到 1,是因为我们正在渲染背面,vNormal 逐渐指向相机而不是远离相机。

atmOpacity 用于控制大气着色的不透明度,atmMultiplier 是与着色相乘的因子,可以使用它来使颜色更加强烈或不那么强烈。atmPowFactor 用于控制大气颜色变化的速度。请注意,我还将 dotP 添加到 atmColor 的r和g分量中,以便随着气氛“增强”,颜色变得更白,从而呈现更真实的色彩。
import vertexShader from "./shaders/vertex.glsl"
import fragmentShader from "./shaders/fragment.glsl"
...
const params = {...atmOpacity: { value: 0.7 },atmPowFactor: { value: 4.1 },atmMultiplier: { value: 9.5 },}...let app = {async initScene() {...let atmosGeo = new THREE.SphereGeometry(12.5, 64, 64)let atmosMat = new THREE.ShaderMaterial({vertexShader: vertexShader,fragmentShader: fragmentShader,uniforms: {atmOpacity: params.atmOpacity,atmPowFactor: params.atmPowFactor,atmMultiplier: params.atmMultiplier},// 注意,默认情况下,Three.js 使用 NormalBlending,如果输出颜色的不透明度变低,显示的颜色可能会变白blending: THREE.AdditiveBlending, // 比设置transparent:true效果更好,因为它避免了地球周围奇怪的黑暗边缘side: THREE.BackSide // 使其不会覆盖在地球顶部;这在顶点着色器中指向相反方向的法线})this.atmos = new THREE.Mesh(atmosGeo, atmosMat)this.group.add(this.atmos)...},...
}
这里有很多预定义的数字,例如大气半径是12.5、atmPowFactor是4.1 、atmMultiplier是9.5 等……这里没有明确的答案,这只是我测试过的组合之一,并且效果很好。

最后一步:星空背景
背景就是插入任何你认为适合的天空盒,我这里使用等距柱状图像来实现。
const envMap = await loadTexture(GaiaSky)
envMap.mapping = THREE.EquirectangularReflectionMappingscene.background = envMap
来看下最终效果:

相关文章:
用 Three.js 创建一个酷炫且真实的地球
接下来我会分步骤讲解,在线示例在数字孪生平台。 先添加一个球体 我们用threejs中的SphereGeometry来创建球体,给他贴一张地球纹理。 let earthGeo new THREE.SphereGeometry(10, 64, 64) let earthMat new THREE.MeshStandardMaterial({map: albed…...
【数据结构】线性表与顺序表
⭐ 作者:小胡_不糊涂 🌱 作者主页:小胡_不糊涂的个人主页 📀 收录专栏:浅谈Java 💖 持续更文,关注博主少走弯路,谢谢大家支持 💖 线性表与顺序表 1. 线性表2. 顺序表2.1 …...
ChatGPT
chatgpt使用地址 https://mycaht.top/#/chat 申请内测免费key https://github.com/chatanywhere/GPT_API_free 设置 接口地址设置改成 https://api.chatanywhere.com.cnAPI Key设置成申请出来的免费key 开始聊天...
矿区井下智慧用电安全监测解决方案
一、背景 矿区井下作业具有复杂的环境和较高的危险性,对于用电安全的要求尤为严格。传统的管理模式和监测方法往往无法实时、准确地掌握井下用电情况,对安全隐患的排查与预防存在一定局限性。因此,引入智慧用电安全监测解决方案ÿ…...
网站列表页加密:三次请求后返回内容多\r
一、抓包第一次请求 url aHR0cDovL2N5eHcuY24vQ29sdW1uLmFzcHg/Y29saWQ9MTA抓包,需要清理浏览器cookie,或者无痕模式打开网址,否则返回的包不全,依照下图中的第一个包进行requests请求 第一次请求后返回 <!DOCTYPE html>…...
12.JVM
一.JVM类加载机制:把类从硬盘文件加载到内存中 1.java文件,编写时是一个.java文件,编译后现成一个.class的字节码文件,运行的时候,JVM就会读取.class文件,放到内存中,并且构造类对象. 2.类加载流程: a.加载:找到.class文件,打开文件,读取内容,尝试解析文件内容. b.验证:检查…...
关于网络协议的若干问题(四)
1、QUIC 是一个精巧的协议,它有哪些特性? 答:QUIC 还有其他特性,一个是快速建立连接。另一个是拥塞控制,QUIC 协议当前默认使用了 TCP 协议的 CUBIC(拥塞控制算法)。 CUBIC 进行了不同的设计&…...
opencv图像卷积操作和常用的图像滤波函数
文章目录 opencv图像卷积操作原理,opencv中常用的图像滤波函数一、图像卷积操作原理:1、卷积操作原理图: 二、opencv常用的图像滤波函数:这些函数的主要作用是对图像进行平滑处理或去除噪声(核心目的是减少图像中的噪声࿰…...
习题1. 31
话不多说 先上代码 (defn product [ term a nxt b](defn iter [a result](if (> a b)1 (* (term a) (iter (nxt a) result))))(iter a 1)) 跟习题1.30比较起来,就是两个地方不同 乘法不能乘0 必须是1。难度来讲,跟1.30难度是一样的。增加了迭代过…...
见微知著:从企业售后技术支持看云计算发展
作者:余凯 售后业务中的细微变化 作为阿里云企业容器技术支持的一员,每天会面对全球各地企业级客户提出的关于容器的各种问题,通过这几年的技术支持的经历,逐步发现容器问题客户的一些惯性,哪些是重度用户࿰…...
C++笔记之如何给 `const char*` 类型变量赋值
C笔记之如何给 const char* 类型变量赋值 code review! 文章目录 C笔记之如何给 const char* 类型变量赋值1.在C中,如果你要给一个 const char* 变量赋值,你通常有几种方法来做这件事,具体取决于你的需求。下面是一些常见的方法:…...
9.Linear Maps
线性映射 线性映射是将向量作为输入并产生一些新向量作为输出的转换。 从坐标定义开始(数组),再到2,3,并展示它们是如何关联的 线性映射的坐标表示最终是矩阵, 1.坐标定义(数组) 列向量是向量的坐标表示…...
大数据Doris(十):添加BE步骤
文章目录 添加BE步骤 一、使用mysql连接 二、添加be...
Vue2 +Element UI 表格行合并
如果相邻数据是一致的,则单元格的行合并,指定需要合并的列,下面我是指定合并了分类和类型这两列。 先看效果 Element UI为我们的<el-table>提供了一个属性span-method:合并行或列的计算方法 下面是一个示例: html部分 - 主要是在表上指…...
SuperEdge易学易用系列-一键搭建SuperEdge集群
条件说明: 系统 公网IP 内网IP 服务器所在地 K8S版本 Centos7.9 114.116.101.254 192.168.0.245 北京 v1.22.6 Centos7.9 119.8.1.96 192.168.0.83 香港 v1.22.6 Ubuntu22 94.74.108.152 192.168.0.154 纽约 v1.22.6 1. 开始部署 1.1 两条指令从零搭建一个边缘集…...
农场养殖农产品商城小程序搭建
鸡鸭羊牛鱼养殖用户不少,其规模也有大有小,尤其对一些生态养殖企业,其产品需求度更高,同时他们也有实际的销售需求。 由于具备较为稳定的货源,因此大规模多规格销售属性很足。 通过【雨科】平台搭建农场养殖商城&…...
大语言模型之十七-QA-LoRA
由于基座模型通常需要海量的数据和算力内存,这一巨大的成本往往只有巨头公司会投入,所以一些优秀的大语言模型要么是大公司开源的,要么是背后有大公司身影公司开源的,如何从优秀的开源基座模型针对特定场景fine-tune模型具有广大的…...
UML组件图综合指南:设计清晰、可维护的软件系统
介绍: UML(Unified Modeling Language)组件图是软件系统设计中的重要工具,用于描绘系统的物理结构和组件之间的关系。在软件工程中,通过创建清晰的组件图,团队能够更好地理解系统的模块化结构和组织关系&a…...
深入浅出ThreadPoolExecutor(一)
文章目录 线程池简诉ThreadPoolExecutor详解ThreadPoolExecutor参数详解创建线程池的工具类Executors 线程池简诉 针对各种池子,比如 连接池:用于管理和重复使用数据库连接,避免频繁创建和销毁数据库连接带来的性能开销。对象池:用于管理和重复使用对象…...
网站的常见攻击与防护方法
在互联网时代,几乎每个网站都存在着潜在的安全威胁。这些威胁可能来自人为失误,也可能源自网络犯罪团伙所发起的复杂攻击。无论攻击的本质如何,网络攻击者的主要动机通常是谋求经济利益。这意味着无论您经营的是电子商务项目还是小型商业网站…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
(十)学生端搭建
本次旨在将之前的已完成的部分功能进行拼装到学生端,同时完善学生端的构建。本次工作主要包括: 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...
