用Three.js搭建的一个艺术场景
本文翻译自于Medium,原作者用 Three.js 创建了一个“Synthwave 场景”,效果还不错,在此加上自己的理解,记录一下。在线Demo.
地形构建
作者想要搭建一个中间平坦、两侧有凹凸山脉效果并且能够一直绵延不断的地形,接下来我们通过三个问题来进行分析。
采用什么样的几何图形
通常情况下,采用PlaneGeometry
一般是大多数人的选择,但是PlaneGeometry
没有凹凸不平的效果,而且作者寄希望于在地形道路上沿着三角网绘制霓虹灯线条。但是问题是相邻的三角形面,在对角线上着色会发生变化,如果使用普通的PlaneGeometry
形状会造成中间的道路上的霓虹灯线条显得不对称。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SPUoDVN-1676727158338)(null)]
普通PlaneGeometry的线框
如果我们想避免对角线问题,有如下3种方法:
- 整体旋转
PlaneGeometry
45度; - 自定义一个
BufferGeometry
,并旋转正方形的方位; - 在 y 方向上切变
PlaneGeometry
直到拥有对称三角形的排布。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F38ksNL9-1676727158606)(null)]
左:将平面旋转 45 度的选项 右:构造自定义 BufferGeometry
使用 matrix.makeShear()进行切变,其中 xy 设置为 -0.5
现在我们对这三种方式进行评估,因为我们需要在地形中铺设一条绵延的长路,所以我们可能需要复制并连接平面以使其绵延不断。
-
对于选项 1,连接多个旋转平面会带来一个明显的问题:它们两侧会有很大的空白空间。将一个指向前方的菱形镶嵌成一条道路将是非常低效的。
-
对于选项 2,自定义几何看起来不错,但开销太高;我们将不得不计算和设置顶点的位置,并且精确地连接平面,这会更加复杂。
-
对于选项 3,我们只需要多写2-3行代码就可以对它进行切变,直到三角形对称为止。开销很小。虽然整个平面是沿对角线拉伸的,但仍然很容易将平面连接起来,如果我们正确设置相机和动画循环,用户也不会注意到切变的影响。
所以很显然,我们选用第三种方式。
如何让地形中间平坦,两边凹凸不平?
我们首先需要的是高度图图像。它是一张灰度图,其中白色表示最高,黑色表示最低。作者用了一款名为 Affinity Designer 的绘图软件使用径向渐变和纹理画笔生成了一张灰度图。很明显,中间的垂直矩形区域保持黑色,用来将其渲染成平坦道路。
场景中使用的高度图
下一个问题是使用来自该高度图的高度数据来渲染我们的平面几何图形。作者一开始尝试用TextureLoader
加载高度图图像,然后将纹理分配给MeshStandardMaterial
的displacementMap
(置换贴图)属性。但是这种方法不适用于稍后创建的霓虹灯线,因为该displacementMap
属性仅在运行时更新vertexShader
中的顶点位置,我们主js程序中平面几何对象的位置数组不受影响。
因此,我们必须手动从置换贴图图像中提取灰度值,对其进行缩放并将其值直接分配给我们几何对象的每个顶点的z值。
如何为地形制作动画来达到无尽绵延的道路效果?
这个问题其实存在于很多需要某种无尽之路的游戏/动画中,这个问题本身很容易解决。大致就是假设我们有多个平面实例连接在一起形成了道路,我们将它们加速朝向相机,这样看起来我们正在向前移动。一旦道路的头部移动到我们的相机后面,我们就把它的位置放回道路的尾部,这就是让我们的道路看起来无穷无尽的方法。
在我们的例子中,有个额外的问题是我们如何确保平面实例在它们的连接处完美连接而没有间隙。我们可以想象一下,我们采用的heightmap
对所有平面实例使用相同的高度图像素,高度图的顶行像素很可能与底行像素不匹配,这样就会产生这样的间隙:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VblO5n6p-1676727160160)(null)]
如果高度图的顶行像素与底行像素不匹配,则平面实例会产生间隙
当然了,解决方案其实很简单。我们可以编辑高度图图像,复制顶行像素并将其粘贴到底行像素的顶部,这样就可以确保完美连接。
编写地形实现代码
首先我们用图片加载函数读取高度图图片,接着我们添加两个DirectionalLight
以照亮左侧和右侧的山坡。
然后,我们创建一个canvas对象来存储加载的高度图图像,并通过context.getImageData()
将图像数据保存。
我们再创建一个具有相同宽度和高度(设置为30)的正方形planeGeometry
,为简单起见,宽度/高度分段的值也相同。我们从PlaneGeometry
的BufferAttribute
中提取position
和uv
数组供以后使用。
let planeGeometry = new THREE.PlaneGeometry(terrainWidth, terrainHeight, terrainWidth, terrainHeight)
let geometryPositions = planeGeometry.getAttribute("position").array
let geometryUVs = planeGeometry.getAttribute("uv").array
然后我们遍历所有的顶点来设置每个顶点的高度值。我们使用getZFromImageDataPoint()
来获取高度图上每个顶点对应的高度值。
export function getZFromImageDataPoint(imageData, u, v, cvWidth, cvHeight) {const mapWidth = cvWidthconst mapHeight = cvHeightconst displacementScale = 5var x = Math.round(u * (mapWidth - 1))var y = Math.round((1 - v) * (mapHeight - 1))var index = (y * imageData.width + x) * 4var red = imageData.data[index]return red / 255 * displacementScale
}
现在我们为每个顶点正确设置了高度,我们仍然需要沿y方向切变平面,直到图案看起来像一对对称三角形。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lLkp5Ekz-1676727159110)(null)]
虚线代表普通平面几何的一个单位,实线代表切变后的状态
我们创建的原始平面几何体由正方形组成(因为宽度和高度值与宽度和高度分段数相同)。我们想在 y 方向上将其切变为正方形长度的一半l/2
。查看上面简化的2d切变方程,如果我们将切变因子s
设置为 0.5,那么我们得到的坐标将为(x, y + 0.5x)
,由此可得切变量将是l/2
。
对于平面的材质,我们使用MeshStandardMaterial
,这样我们可以调整金属度metalness
和粗糙度roughness
。然后开启flatShading
,因为这种视觉风格更适合低面数多边形对象。
应用所有更改后,场景现在应如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xgq8im7x-1676727159201)(null)]
在地形上创建明亮/霓虹灯网格线
最初,作者尝试使用WireframeGeometry
和LineSegments
创建网格线,但是因为linewidth
总是被限制为 1,所以只能用另一种方法。
threejs官方示例中有fastline的示例可以控制线条粗细,要实现这种方法,首先需要平面几何的顶点位置及其高度图数据,可以从代码中看到主要用了LineGeometry
、LineMaterial
、Line2
。其中创建WebGLRenderer时候要开启logarithmicDepthBuffer
,来修复网格线和平面几何之间的 z 冲突问题。
LineGeometry
需要一个连续的顶点位置数组,然后它会以相同的顺序连接位置数组中指定的点画线。再传输顶点位置数组时候,一定要设置好正确的顺序,因为如果直接用geometryPositions
的话,就会发现线段错乱,因为顶点位置的默认顺序是从左到右逐行排列,但是我们再绘制的时候就需要重新进行排列。
每行的线都从最右边飞到最左边,就会产生混乱的场景
为了防止线从右边缘跳到左边缘,我们必须以特定方式对线位置进行排序。对于第一行,我们应该按照这个顶点顺序画线:
v0 — v1 — v5 — v6,然后是 v1 — v2 — v6 — v7,依此类推。
然而,对于第二行,我们必须从右侧而不是左侧开始,否则,我们将遇到同样的问题,即线从右边缘跳到左边缘。所以基本上,对于偶数行,我们必须按照奇数行的相反顺序对顶点进行排序。
因此对于第二行,我们应该遵循这个顶点顺序:
v14 — v13 — v9 — v8,然后是 v13 — v12 — v8 — v7,依此类推。
通过实现这个特定的顺序,我们可以在我们的地形上得到一个完美的线网格。
创造一个永无止境的地形效果
在开发过程中,作者发现为所有连续的地形克隆同样的高度图在视觉上过于重复,所以做了一个快速修复,使地形看起来不那么重复:通过水平反转高度图“创建”第二个高度图。那就是对“奇数”地形使用高度图,对“偶数”地形反转高度图。
// 在render函数中进行更新
for (let i = 0; i < numOfMeshSets; i++) {this.meshGroup[i].position.z += interval * params.speedthis.lineGroup[i].position.z += interval * params.speedif (this.meshGroup[i].position.z >= terrainHeight) {this.meshGroup[i].position.z -= numOfMeshSets * terrainHeightthis.lineGroup[i].position.z -= numOfMeshSets * terrainHeight}
}
我们定义了一个新的speed
参数来控制地形移动的速度。我们还定义了一个新的numOfMeshSets
变量来控制我们要创建的地形副本的数量。
添加夕阳 ☀ 并制作动画
本文实现的太阳还增加了一些条纹效果,shader代码如下:
// vertexShader for the Sun
export function vertexShader() {return `varying vec2 vUv;varying vec3 vPos;void main() {vUv = uv;vPos = position;gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`
}// fragmentShader for the Sun
export function fragmentShader() {return `#ifdef GL_ESprecision mediump float;#endif#define PI 3.14159265359#define TWO_PI 6.28318530718uniform vec2 u_resolution;uniform vec2 u_mouse;uniform float u_time;uniform vec3 color_main;uniform vec3 color_accent;varying vec2 vUv;varying vec3 vPos;void main() {vec2 st = gl_FragCoord.xy/u_resolution.xy;float x = vPos.y;float osc = ceil(sin((3. - (x - u_time) / 1.5) * 5.) / 2. + 0.4 - floor((3. - x / 1.5) * 5. / TWO_PI) / 10.);vec3 color = mix(color_accent, color_main, smoothstep(0.2, 1., vUv.y));gl_FragColor = vec4(color, osc);}`
}
用Bloom效果✨✨对场景进行后期处理
// Bloom的后处理效果
let bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight),params.bloomStrength,params.bloomRadius,params.bloomThreshold);
let composer = createComposer(renderer, scene, camera, (comp) => {comp.addPass(bloomPass)
})
结论
本文特点主要在地形的构建上,采用了一些技巧来完成比较好的视觉效果,在一些无尽场景中应用还是比较多的。
相关文章:
用Three.js搭建的一个艺术场景
本文翻译自于Medium,原作者用 Three.js 创建了一个“Synthwave 场景”,效果还不错,在此加上自己的理解,记录一下。在线Demo. 地形构建 作者想要搭建一个中间平坦、两侧有凹凸山脉效果并且能够一直绵延不断的地形,接下…...
算法导论【字符串匹配】—朴素算法、Rabin-Karp、有限自动机、KMP
算法导论【字符串匹配】—朴素算法、Rabin Karp、有限自动机、KMP朴素字符串匹配算法Rabin-Karp算法有限自动机KMP算法朴素字符串匹配算法 预处理时间:0匹配时间:O((n-m1)m) Rabin-Karp算法 预处理时间:Θ(m),需要预先算出匹…...
如何在 Python 中验证用户输入
要验证用户输入: 使用 while 循环进行迭代,直到提供的输入值有效。检查输入值在每次迭代中是否有效。如果该值有效,则跳出 while 循环。 # ✅ 验证用户输入的是否是整数num 0while True:try:num int(input("Enter an integer 1-10: …...
JVM详解——类的加载
文章目录类的加载1、Java程序如何运行2、Java字节码文件3、类加载4、类加载的过程5、类加载器6、类的加载方式7、类的加载机制8、双亲委派机制9、破坏双亲委派机制类的加载 1、Java程序如何运行 首先通过Javac命令将.java文件编译生成.class字节码文件。 Javac是Java编译命令&a…...
Ubuntu最新版本(Ubuntu22.04LTS)安装nfs服务器及使用教程
目录 一、概述 二、在Ubuntu搭建nfs服务器 👉2.1 安装nfs服务器 👉2.2 创建nfs服务器共享目录 👉2.3 修改nfs服务器配置文件 👉2.4 重启nfs服务器 三、客户端访问nfs服务器共享目录 🎈3.1 在nfs客户端挂载服…...
Python-第九天 Python异常、模块与包
Python-第九天 Python异常、模块与包一、了解异常1. 什么是异常:2. bug是什么意思:二、异常的捕获方法1. 为什么要捕获异常?2. 捕获异常的语法3. 如何捕获所有异常?三、异常的传递性1.异常是具有传递性的四、Python模块1. 什么是模…...
博彩公司 BetMGM 发生数据泄露,“赌徒”面临网络风险
Bleeping Computer 网站披露,著名体育博彩公司 BetMGM 发生一起数据泄露事件,一名威胁攻击者成功窃取其大量用户个人信息。 据悉,BetMGM 数据泄漏事件中,攻击者盗取了包括用户姓名、联系信息(如邮政地址、电子邮件地址…...
初探Mysql反向读取文件
前言 Mysql反向读取文件感觉蛮有意思的,进行了解过后,简单总结如下,希望能对在学习Mysql反向读取文件的师傅有些许帮助。 前置知识 在Mysql中存在这样一条语句 LOAD DATA INFILE它的作用是读取某个文件中的内容并放置到要求的表中&#x…...
地图坐标系大全:常用地图坐标系详解与转换指南
介绍地图坐标系的基本概念和原理地图坐标系是用于描述地图上位置的数学模型。它可以用来表示地球表面上的任意一个点,使得这个点的位置可以在地图上精确定位。不同的地图坐标系采用不同的基准面和投影方式,因此会有不同的坐标系参数,不同的坐…...
使用 URLSearchParams 解析和管理URL query参数
介绍 首先 URLSearchParams是一个构造函数,会生成一个URLSearchParams对象,参数类型: 不传 | string | object | URLSearchParams, 并且遇到特殊字符它会自动帮我们encode 和 decode const ur…...
一台电脑安装26个操作系统(windows,macos,linux,chromeOS,Android,静待HarmonyOS)
首先看看安装了哪些操作系统1-4: windows系统 四个5.Ubuntu6.deepin7.UOS家庭版8.fydeOS9.macOS10.银河麒麟11.红旗OS12.openSUSE Leap13.openAnolis14.openEuler(未安装桌面UI)15.中标麒麟(NeoKylin)16.centos17.debian Edu18.fedora19.oraclelinux(特别…...
Python配置文件管理之ini和yaml文件读取
1. 引言 当我们设计软件时,我们通常会花费大量精力来编写高质量的代码。但这往往还不够,一个好的软件还应该考虑其整个系统,如测试、部署、网络等。其中最重要的一个方面是配置管理。 良好的配置管理应允许在任何环境中执行软件而不更改代码…...
实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
上一节课,我们做了一些理论知识的铺垫性讲解,讲到了两种开发模式,基于贫血模型的传统开发模式,以及基于充血模型的DDD开发模式。今天,我们正式进入实战环节,看如何分别用这两种开发模式,设计实现一个钱包系统。话不多说,让我们正式…...
webpack当中的代码分割详解
A.代码分割方法一:将原来的单入口文件改为多入口文件 将不同的文件例如js代码文件分为入口文件和测试文件,这个时候打包出来的代码就会根据不同的文件单独打包成属于他们自己的文件 例如以下为单入口文件: entry: ./src/js/index.js 多入口文件:(在输出…...
【SSM】Spring对IoC的实现方式DI详讲
控制反转的一种实现方式——依赖注入一、IoC 控制反转(Overview)依赖注入(DI)- Overview利用 IoC(控制反转)这种思想有什么好处呢?二、依赖注入的方式setter 方式(xml配置中的proper…...
【QT 5 相关实验-示波器-学习笔记-示波器组件练习与使用总结】
【QT 5 相关实验-示波器-学习笔记-示波器组件练习与使用总结】1、概述2、实验环境3、参考资料-致谢4、自我提升实验效果视频演示5、代码练习-学习后拆解-实验步骤(1)头文件部分-"mwaveview.h"(2)cpp文件部分-"mwav…...
二维数组中的查找(两种解法,各有千秋)
凡事都有可能,永远别说永远。——《放牛班的春天》今天一题为再一个行列都有序的二维数组中寻找一个目标值,我们第一时间想到的可能是很暴力的解法,例如从头到尾进行遍历,这样能做出来,但是借用武忠祥老师的一句话&…...
quartz使用及原理解析
quartz简介 Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能: 持久性作业 - 就是保持调度…...
Datawhale组队学习:大数据 D2——分布式文件系统(HDFS)
妙趣横生大数据 Day2三、Hadoop 分布式文件系统(HDFS)1. 分布式文件系统2. HDFS 简介3. HDFS 体系结构4. HDFS存储原理数据冗余存储数据存储策略数据错误与恢复5. HDFS数据读写过程读写过程HDFS故障类型和其检测方法HDFS编程实验1. 本地和集群文件间操作2. 基本文件操作3. Hado…...
CCIE重认证-300-401-拖图题全
拖图 拖图题 编程 snippet;192.168.5.0,mask 255.255.255.0;number是192.168.5.0;mask是255.255.255.0 snippets;edit-config对config,loopback对name 100,address对primary,mask…...
如何动态的创建类?type的其他用法?什么是元类,如何自定义元类?
1、python中一切都是对象,类也不例外,type是object的子类,是创建类的类。 如何动态的创建一个类? 用脚丫子创建 用脑子创建 不会 不知道什么事动态类 大家可能会有一堆的疑惑,是的我也是有很多疑惑那让我们一起来探个…...
XCP实战系列介绍15-XCP故障排查指导
本文框架 1.概述2. 通过调试器排查2.1 打开Det功能2.2 如何确定Det ErrorCode3. 通过XCP应答报文排查3.1 FE报文组成及故障码对应关系3.2 举个例子1.概述 前面几篇文章我们介绍了基于Davinci开发工具的XCP配置指导,配好了,代码也生成了,但是程序一定能正常跑起来吗?就算软…...
吉林大学软件需求分析与规范(Software Requirements Analysis Specification)
chapter0课程简介:◼ 软件工程专业核心课程之一◼ 软件工程课程体系最前端课程◼ 主要内容:需求的基本概念,需求的分类,需求工程的基本过程,需求获取的方法、步骤、技巧,需求分析和建模技术,需求…...
PyTorch - Conv2d 和 MaxPool2d
文章目录Conv2d计算Conv2d 函数解析代码示例MaxPool2d计算函数说明卷积过程动画Transposed convolution animationsTransposed convolution animations参考视频:土堆说 卷积计算 https://www.bilibili.com/video/BV1hE411t7RN 关于 torch.nn 和 torch.nn.function t…...
leetcode Day2(昨天实习有点bug,心态要崩了)
int carry 0;for(int i a.size() - 1, j b.size() - 1; i > 0 || j > 0 || carry; --i, --j) {int x i < 0 ? 0 : a[i] - 0;int y j < 0 ? 0 : b[j] - 0;int sum (x y carry) % 2;carry (x y carry) / 2;str.insert(0, 1, sum 0);}return str;加一&a…...
另一种思考:为什么不选JPA、MyBatis,而选择JDBCTemplate
以下内容转载自:https://segmentfault.com/a/1190000018472572 作者:scherman 因为项目需要选择数据持久化框架,看了一下主要几个流行的和不流行的框架,对于复杂业务系统,最终的结论是,JOOQ是总体上最好的…...
LeetCode 338. 比特位计数
给你一个整数 n ,对于 0 < i < n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n 1 的数组 ans 作为答案。 示例 1: 输入:n 2 输出:[0,1,1] 解释: 0 --> 0 1 --> …...
排序评估指标——NDCG和MAP
在搜索和推荐任务中,系统常返回一个item列表。如何衡量这个返回的列表是否优秀呢? 例如,当我们检索【推荐排序】,网页返回了与推荐排序相关的链接列表。列表可能会是[A,B,C,G,D,E,F],也可能是[C,F,A,E,D],现在问题来了…...
[Android Studio] Android Studio Virtual Device(AVD)虚拟机的功能试用
🟧🟨🟩🟦🟪 Android Debug🟧🟨🟩🟦🟪 Topic 发布安卓学习过程中遇到问题解决过程,希望我的解决方案可以对小伙伴们有帮助。 🚀write…...
kafka-3-kafka应用的核心要点和内外网访问
kafka实战教程(python操作kafka),kafka配置文件详解 Kafka内外网访问的设置 1 kafka简介 根据官网的介绍,ApacheKafka是一个分布式流媒体平台,它主要有3种功能: (1)发布和订阅消息流,这个功能类似于消息队列&#x…...
网站建设有生意吗/搜索引擎优化排名品牌
Kubernetes 是 Google 基于 Borg 开源的容器编排调度,用于管理容器集群自动化部署、扩容以及运维的开源平台。作为云原生计算基金会 CNCF(Cloud Native Computing Foundation)最重要的组件之一(CNCF 另一个毕业项目 Prometheus &a…...
网站代码优化的方法/seo 知乎
bitset的好处很多,尤其是第一次接触到这个结构的人。觉得只需要一个key就可以很简单的处理这个标志位的数据, 甚至说几个亿的offset占用内存也会很小。但在项目实际使用过程中还是要好好算算这笔账的。 bitset占用的内存是用最大的offset来决定的&#x…...
网站备案在哪查/友情链接软件
本书第一章提出了一个看似简单的问题,有最多1000万条不同的整型数据存在于硬盘的文件中,如何在1M内存的情况下对其进行尽可能快的排序。 每个数字用4byte,1M即可存储250 000个数据,显然,只要每次对250 000个数据排序&a…...
织梦做的网站后台登录/网站模板之家免费下载
继承的重写 (Override) class Base { public void function1() //无参数的方法 { 功能1; } public void function1(Datatype var1) //带一个参数的方法 { 功能2; } } class Sub extends Base { public void function1() [ /…...
山西网站设计/快速排名生客seo
对于中断通俗点说:就是让单片机的cpu暂停执行当前任务转去执行引起中断的任务。刚开始学习单片机时非常排斥中断方面的东西因为要记忆很多寄存器相关的东西什么IE,IP等但是仔细一想如果不懂中断就只能写最简单的顺序执行程序而且非常浪费单片机的这些中断…...
网站建设维护需要作假吗/中国国际新闻
传送门(不用再辛苦百度实验报告啦,一键直达呦) oracle实验1 oracle 基本操作 oracle实验2 oracle数据体系结构 oracle实验3 用户、方案的创建与管理 oracle实验4 表的创建与管理 oracle实验5 sql基本查询 oracle实验6 sql高级查询 ora…...