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

news2024/11/26 10:35:50

image.png
接下来我会分步骤讲解,在线示例在数字孪生平台。

先添加一个球体

我们用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)
}

在屏幕上我们就会得到如下的效果。
image.png

添加凹凸贴图

地球上有山脉海洋,本质上是凹凸不平的,在太阳下也会呈现不同的阴影效果。那么为了让我们的地球更加真实,我们可以用 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, 
})

这样我们就能够立即看山脉在阳光照耀下投下的阴影。
image.png
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)

添加云后真个效果就会看起来更加真实。
image.png
接下来我们实现云的投射阴影效果。具体做法:在地球纹理贴图上的任意 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.shader
    if ( shader ) {
      let offset = (interval * 0.005 * params.speedFactor) / (2 * Math.PI)
      shader.uniforms.uv_xOffset.value += offset % 1
    }
  }

完成这些更改后,现在我们得到了微妙但逼真的云阴影。
image.png

让海洋反射阳光

接下来我们可以让海洋看起来更加真实。水是一种很强的光反射体,尤其是当波浪安静或光的入射角较小时。通过利用粗糙度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_ROUGHNESSMAP

          vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv );
          texelRoughness = vec4(1.0) - texelRoughness;

          roughnessFactor *= clamp(texelRoughness.g, 0.5, 1.0);

        #endif
      `);
      ...
  }

因为这里用的海洋贴图其实就只有黑白色,如果海洋区域为白色,我们必须反转黑白值以使粗糙度正确,因为我们希望陆地的粗糙度为“白色”(值:1)。对于水域,我们不希望它的粗糙度为零,因为这会使其看起来太像金属并且反光效果太强,海洋会失去其通常的蓝色,因此我们将粗糙度值限制在最小值 0.5。
image.png

夜景图

如果地球的夜晚是完全黑暗的,那就没有意义了,因为通常情况下都是会有夜景灯光。那我们接下的实现逻辑就是:在白天显示正常地球贴图,在阳光照不到的区域,用夜景图代替。

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_EMISSIVEMAP

          vec4 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

        ...
      `)
      ...
}

添加大气菲涅尔效果

菲涅尔效应可以想象,当我们在湖边看到离我们较远的湖水表面比我们近处的表面更清晰的反射时产生的效果;其实就是理解成,可以以更小的角度来更清晰地看到表面的反射。

虽然我们的海洋已经有了反光效果,但我们仍然缺少大气层。我们将分两步模拟大气效果:

  1. 调整地球纹理的漫反射颜色diffuseColor,使其在观察者观察到的球体边缘附近呈现出更亮的蓝色(由于从较厚的路径积累了更多的光,边缘总是看起来更亮,看看下面的 ba 路径长得多)

image.png

  1. 在云层和地球网格之上添加另一个球体网格作为大气层本身。
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增加到1
    float 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 逐渐指向相机而不是远离相机。
image.png
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 等……这里没有明确的答案,这只是我测试过的组合之一,并且效果很好。
image.png

最后一步:星空背景

背景就是插入任何你认为适合的天空盒,我这里使用等距柱状图像来实现。

const envMap = await loadTexture(GaiaSky)
envMap.mapping = THREE.EquirectangularReflectionMapping
    
scene.background = envMap

来看下最终效果:
image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1092828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年建筑电工(建筑特殊工种)证考试题库及建筑电工(建筑特殊工种)试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年建筑电工(建筑特殊工种)证考试题库及建筑电工(建筑特殊工种)试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人…

搭建Umijs环境并创建一个项目 介绍基本操作

上文 Umijs介绍中 我们简单了解了一下这个东西 这里 我们还是不要关说不练了 直接终端执行 npm install -g umi可能会比较久 因为这个东西还挺大的 我们执行 umi -v这里就会输出版本 然后 我们创建一个文件夹目录 作为项目目录 然后 我们可以 通过 终端输入 mkdir 项目名称…

使用 Go 和 Wails 构建跨平台桌面应用程序

由于多种原因&#xff0c;Electron 曾经&#xff08;并且仍然&#xff09;大受欢迎。首先&#xff0c;其跨平台功能使开发人员能够从单个代码库支持 Linux、Windows 和 macOS。最重要的是&#xff0c;它对于熟悉 Javascript 的开发人员来说有一个精简的学习曲线。 尽管它有其缺…

LENOVO联想笔记本小新 Pro-14 2021AMD处理器ACH版(82MS)原厂Win10系统

下载链接&#xff1a;https://pan.baidu.com/s/1-KZ8Y9NmkS7nDXcMbhZLHw?pwdyrkx 系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、lenovo联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#xff1…

VScode运行C/C++

VScode运行C/C VScode的安装这里不讲 一、mingw64的下载 二、VS code打开文件夹与创建C文件 ----------------这一步给萌新看&#xff0c;有C和VScode的基础可跳过---------------- 1.创建一个文件夹 2.vscode打开刚刚创建的文件夹 3.新建文件&#xff0c;在输入文件名1.c后…

C语言中的文件操作指南

阅读导航 前言一、文件类型1. 程序文件2. 数据文件PS.文件名 二、文件的打开和关闭1. 文件指针2. 文件的打开和关闭 三、文件的顺序读写四、文件的随机读写1. fseek() 函数2. ftell() 函数3. rewind() 函数 总结 前言 在C语言中&#xff0c;文件操作是一项重要的任务。通过文件…

20和遍历以及迭代器有关的一些东西

知识点有点散&#xff0c;只能这样记录了 1、这边是和遍历有关的&#xff1a; class Person:def __init__(self):self.result 1def __getitem__(self, item):self.result 1if self.result > 6:raise StopIteration(停止遍历)return self.resultpassp Person() for i in…

Unity中Shader光照模型Phong的实现

文章目录 前言一、对主平行光实现高光效果1、在属性面板定义高光颜色2、在属性面板定义高光系数3、在属性面板定义高光范围系数4、获取 V (模型顶点的世界坐标 指到 相机世界坐标 的单位向量)5、由上一篇推理出的公式得到  R 向量6、由公式计算得出高光效果 二、使用已有的数…

常用的软件项目管理工具一览

软件项目管理工具是帮助团队成功管理和完成软件开发项目的软件程序和应用程序。根据项目及其规模和复杂性&#xff0c;可以使用各种各样的这些工具来协助完成任务&#xff0c;从任务跟踪和调度&#xff0c;到项目报告&#xff0c;到版本控制和协作。 项目经理对软件项目的整体成…

Git 回退代码的两种方法对比

Git 回退代码版本 在项目的开发中&#xff0c;有时候还是会出现&#xff0c;一些误提交了一些代码&#xff0c;这时候就会想撤回提交的代码&#xff0c;在Git中有两种方法可以使用&#xff0c;现在通过对比方法比较这两种方法的区别&#xff0c;分别适用于哪些情况&#xff1f…

Google zxing 生成带logo的二维码图片

环境准备 开发环境 JDK 1.8SpringBoot2.2.1Maven 3.2 开发工具 IntelliJ IDEAsmartGitNavicat15 添加maven配置 <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.4.0</version> </…

Stm32_标准库_12_串口_发送数据

波特率&#xff1a;约定的传输速率&#xff0c;1000bps,1s发1000位 引脚 结构 数据帧的传输特点 代码&#xff1a; #include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h"GPIO_InitTypeDef GPIO_InitStruct; USART…

计算机毕业设计选什么题目好?springboot 学习笔记系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【互联网】实习一个月感受

说明&#xff1a;岗位&#xff1a;golang开发实习生&#xff0c;实习已经一个月多点了&#xff0c;这篇文章谈谈实习应该注意些什么点&#xff0c;以及该做些什么事情 说实话这不是我第一次实习了&#xff0c;但是感受很深 注意点&#xff1a; 1、没事找话说 比如中午和同事吃…

12.JVM

一.JVM类加载机制:把类从硬盘文件加载到内存中 1.java文件,编写时是一个.java文件,编译后现成一个.class的字节码文件,运行的时候,JVM就会读取.class文件,放到内存中,并且构造类对象. 2.类加载流程: a.加载:找到.class文件,打开文件,读取内容,尝试解析文件内容. b.验证:检查…

【Python学习笔记】字符串

1. 字符串定义 可以用双引号 、 单三引号、双三引号&#xff0c;下面的定义都是正确的 "你好" 你好 """你好"""其中三引号可以 直接写内容有多行 的字符串。如下 letter 刘总&#xff1a;您好&#xff01;您发的货我们已经收到&am…

百度开放平台第三方代小程序开发,授权事件、消息与事件通知总结

大家好&#xff0c;我是小悟 关于百度开放平台第三方代小程序开发的两个事件接收推送通知&#xff0c;是开放平台代小程序实现业务的重要功能。 授权事件推送和消息与事件推送类型都以event的值判断。 授权事件推送通知 授权事件推送包括&#xff1a;推送票据、授权成功、取…

【C++进阶】:特殊类的设计

特殊类的设计 一.设计一个类不能被拷贝二.设计一个类只能在堆上创建对象三.设计一个类只能在栈上创建4.设计一个类不能被继承五.设计一个类只能有一个对象(单例模式) 一.设计一个类不能被拷贝 拷贝只会放生在两个场景中&#xff1a;拷贝构造函数以及赋值运算符重载&#xff0c…

蓝桥杯(跳跃 C++)

思路&#xff1a; 1、根据题目很容易知道可以用深度搜索、广度搜索、动态规划的思想解题。 2、这里利用深度搜素&#xff0c;由题目可知&#xff0c;可以往九个方向走。 3、这里的判断边界就是走到终点。 #include<iostream> using namespace std; int max1 0; int …

Vue3中ref创建的引用为什么一定要用.value来获取值

Vue3中ref创建的引用为什么一定要用.value来获取值 回顾jsjs中修改复制变量,改变原始变量vue3中使用ref 回顾js js中一个变量赋值给另一个变量,改变另一个变量,原始变量不变 let a 0; let b a; b 2 console.log("a: "a);js中修改复制变量,改变原始变量 let obj…