用Three.js搭建的一个艺术场景

news2024/11/14 2:59:51

本文翻译自于Medium,原作者用 Three.js 创建了一个“Synthwave 场景”,效果还不错,在此加上自己的理解,记录一下。在线Demo.

地形构建

作者想要搭建一个中间平坦、两侧有凹凸山脉效果并且能够一直绵延不断的地形,接下来我们通过三个问题来进行分析。

采用什么样的几何图形

通常情况下,采用PlaneGeometry一般是大多数人的选择,但是PlaneGeometry没有凹凸不平的效果,而且作者寄希望于在地形道路上沿着三角网绘制霓虹灯线条。但是问题是相邻的三角形面,在对角线上着色会发生变化,如果使用普通的PlaneGeometry形状会造成中间的道路上的霓虹灯线条显得不对称。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SPUoDVN-1676727158338)(null)]
普通PlaneGeometry的线框

如果我们想避免对角线问题,有如下3种方法:

  1. 整体旋转PlaneGeometry45度;
  2. 自定义一个BufferGeometry,并旋转正方形的方位;
  3. 在 y 方向上切变PlaneGeometry直到拥有对称三角形的排布。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F38ksNL9-1676727158606)(null)]
左:将平面旋转 45 度的选项 右:构造自定义 BufferGeometry


使用 matrix.makeShear()进行切变,其中 xy 设置为 -0.5

现在我们对这三种方式进行评估,因为我们需要在地形中铺设一条绵延的长路,所以我们可能需要复制并连接平面以使其绵延不断。

  • 对于选项 1,连接多个旋转平面会带来一个明显的问题:它们两侧会有很大的空白空间。将一个指向前方的菱形镶嵌成一条道路将是非常低效的。

  • 对于选项 2,自定义几何看起来不错,但开销太高;我们将不得不计算和设置顶点的位置,并且精确地连接平面,这会更加复杂。

  • 对于选项 3,我们只需要多写2-3行代码就可以对它进行切变,直到三角形对称为止。开销很小。虽然整个平面是沿对角线拉伸的,但仍然很容易将平面连接起来,如果我们正确设置相机和动画循环,用户也不会注意到切变的影响。

所以很显然,我们选用第三种方式。

如何让地形中间平坦,两边凹凸不平?

我们首先需要的是高度图图像。它是一张灰度图,其中白色表示最高,黑色表示最低。作者用了一款名为 Affinity Designer 的绘图软件使用径向渐变和纹理画笔生成了一张灰度图。很明显,中间的垂直矩形区域保持黑色,用来将其渲染成平坦道路。


场景中使用的高度图

下一个问题是使用来自该高度图的高度数据来渲染我们的平面几何图形。作者一开始尝试用TextureLoader加载高度图图像,然后将纹理分配给MeshStandardMaterialdisplacementMap(置换贴图)属性。但是这种方法不适用于稍后创建的霓虹灯线,因为该displacementMap属性仅在运行时更新vertexShader中的顶点位置,我们主js程序中平面几何对象的位置数组不受影响。

因此,我们必须手动从置换贴图图像中提取灰度值,对其进行缩放并将其值直接分配给我们几何对象的每个顶点的z值。

如何为地形制作动画来达到无尽绵延的道路效果?

这个问题其实存在于很多需要某种无尽之路的游戏/动画中,这个问题本身很容易解决。大致就是假设我们有多个平面实例连接在一起形成了道路,我们将它们加速朝向相机,这样看起来我们正在向前移动。一旦道路的头部移动到我们的相机后面,我们就把它的位置放回道路的尾部,这就是让我们的道路看起来无穷无尽的方法。

在我们的例子中,有个额外的问题是我们如何确保平面实例在它们的连接处完美连接而没有间隙。我们可以想象一下,我们采用的heightmap对所有平面实例使用相同的高度图像素,高度图的顶行像素很可能与底行像素不匹配,这样就会产生这样的间隙:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VblO5n6p-1676727160160)(null)]
如果高度图的顶行像素与底行像素不匹配,则平面实例会产生间隙

当然了,解决方案其实很简单。我们可以编辑高度图图像,复制顶行像素并将其粘贴到底行像素的顶部,这样就可以确保完美连接。

编写地形实现代码

首先我们用图片加载函数读取高度图图片,接着我们添加两个DirectionalLight以照亮左侧和右侧的山坡。

然后,我们创建一个canvas对象来存储加载的高度图图像,并通过context.getImageData()将图像数据保存。

我们再创建一个具有相同宽度和高度(设置为30)的正方形planeGeometry,为简单起见,宽度/高度分段的值也相同。我们从PlaneGeometryBufferAttribute中提取positionuv数组供以后使用。

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 = cvWidth
  const mapHeight = cvHeight
  const displacementScale = 5
  var x = Math.round(u * (mapWidth - 1))
  var y = Math.round((1 - v) * (mapHeight - 1))
  var index = (y * imageData.width + x) * 4
  var 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)]

在地形上创建明亮/霓虹灯网格线

最初,作者尝试使用WireframeGeometryLineSegments创建网格线,但是因为linewidth总是被限制为 1,所以只能用另一种方法。

threejs官方示例中有fastline的示例可以控制线条粗细,要实现这种方法,首先需要平面几何的顶点位置及其高度图数据,可以从代码中看到主要用了LineGeometryLineMaterialLine2。其中创建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.speed
    this.lineGroup[i].position.z += interval * params.speed
    if (this.meshGroup[i].position.z >= terrainHeight) {
        this.meshGroup[i].position.z -= numOfMeshSets * terrainHeight
        this.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_ES
      precision mediump float;
      #endif
      #define PI 3.14159265359
      #define TWO_PI 6.28318530718
      
      uniform 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)
})

结论

本文特点主要在地形的构建上,采用了一些技巧来完成比较好的视觉效果,在一些无尽场景中应用还是比较多的。

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

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

相关文章

Quartz组件任务调度管理

Quartz什么是Quartzquartz:石英钟的意思是一个当今市面上流行的高效的任务调度管理工具所谓"调度"就是制定好的什么时间做什么事情的计划由OpenSymphony开源组织开发Symphony:交响乐是java编写的,我们使用时需要导入依赖即可为什么需要Quartz所谓"调度"就是…

18:CTK 总结篇(FAQ)

作者: 一去、二三里 个人微信号: iwaleon 微信公众号: 高效程序员 经过了几个月的艰苦奋战,终于到了最后一节啦,是不是和我一样,心里有点儿小激动! 回顾之前的章节,从初级 -> 进阶 -> 高级,我们针对 CTK 做了详细的分类讲解。希望通过这些知识,大家能对模块化…

管理会计报告和财务报告的区别

财务会计报告是给投资人看的&#xff0c;可以反映公司总体的盈利能力。不过&#xff0c;我们回顾一下前面“第一天”里面提到的问题。如果你是公司的产品经理&#xff0c;目前有三个产品在你的管辖范围内。上级给你一笔新的资金&#xff0c;这笔资金应该投到哪个产品上&#xf…

c++容器

1、vector容器 1.1性质 a&#xff09;该容器的数据结构和数组相似&#xff0c;被称为单端数组。 b&#xff09;在存储数据时不是在原有空间上往后拓展&#xff0c;而是找到一个新的空间&#xff0c;将原数据深拷贝到新空间&#xff0c;释放原空间。该过程被称为动态拓展。 vec…

什么是猜疑心理?小猫测试网科普小作文

什么是猜疑心理&#xff1f;猜疑心理是说一个人心中想法偏离了客观事实&#xff0c;牵强附会&#xff0c;往往是指不好的一面&#xff0c;对别人的一言一行都充满了不良的解读&#xff0c;认为这些对自己都有针对性&#xff0c;目的性&#xff0c;对自己都是不利的。猜疑心理重…

算力引领 数“聚”韶关——第二届中国韶关大数据创新创业大赛圆满收官

为进一步促进数字经济领域创新创业发展&#xff0c;推动国家数据中心集群建设&#xff0c;构建大数据领域资源专业平台&#xff0c;促进大湾区大数据科技成果和创新创业人才转化落地&#xff0c;为韶关大数据领域创新型产业集群的打造、大数据科技成果和创新创业人才的转化落地…

如何选择合适的固态继电器?

如何选择合适的固态继电器&#xff1f; 在选择固态继电器&#xff08;SSR&#xff09;时&#xff0c;应根据实际应用条件和SSR性能参数&#xff0c;特别要考虑到使用中的过流和过压条件以及SSR的负载能力&#xff0c;这有助于实现固态继电器的长寿命和高可靠性。然后&#xff0…

九龙证券|最新评级情况出炉,机构扎堆这一板块!聚氨酯龙头获得最多关注

本周算计254家上市公司获组织“买入型”评级。 电子板块评级组织扎堆 证券时报数据宝计算&#xff0c;2月13日至17日&#xff0c;A股市场53家组织算计进行347次评级&#xff0c;254家上市公司获“买入型”评级&#xff08;包含买入、增持、强烈推荐、推荐&#xff09;。 从申…

ONNX yolov5导出 convert error --grid

使用的版本 https://github.com/ultralytics/yolov5/tree/v5.0 安装onnx torch.onnx.export(model, img, f, verboseFalse, opset_version12, input_names[images],output_names[classes, boxes] if y is None else [output],dynamic_axes{images: {0: batch, 2: height, 3:…

智慧校园:校务助手微信小程序端源码

校园校务助手-智慧校园教师移动端校园校务助手微信小程序端也指智慧校园教师端微信小程序 它包括哪些功能呢&#xff1f;我来介绍一下。 智慧校园教师端微信小程序是基于原生开发 教师端登录界面①.设备管理、通知管理、图片管理、班级考勤 ②.综合素质评价、视频管理、请假…

SQL Server 2008新特性——更改跟踪

在大型的数据库应用中&#xff0c;经常会遇到部分数据的脱机和多个数据库的合并问题。比如现在有一个全省范围使用的应用程序&#xff0c;每个市都部署了单独的相同的应用程序服务器和数据库服务器&#xff0c;每个月需要将全省所有市的数据全部汇总起来用于出全省的报表&#…

微搭使用笔记(三) 数据模型介绍及初步使用

基于数据模型实现表单页面的生成和数据的保存、查看 表单应用是微搭的一个重要的使用场景&#xff0c;我们举下面一个简单的问卷调查的例子: 基于以上问卷&#xff0c;本文我们采取数据模型的方式生成表单页面并完成数据的保存及查看。 数据模型概述 先看下官方文档对于数据…

使用 TypeScript 的 CheckJS 为你的陈旧 JavaScript 项目续命

&#x1f64b; Why CheckJS? 让 JavaScript 项目也能享受到 TS 的类型推导等诸多好处。* 和直迁 TypeScript 相比&#xff0c;大大降低成本和风险&#xff0c;例如&#xff1a;&#x1f6a5; 使用方法 安装依赖、追加配置 # 为你的项目安装 TypeScript npm install typescri…

冰蝎4.0特征分析及流量检测思路

0 1、 冰蝎4.0介绍 冰蝎是一款基于Java开发的动态加密通信流量的新型Webshell客户端。老牌Webshell管理神器——中国菜刀的攻击流量特征明显&#xff0c;容易被各类安全设备检测&#xff0c;实际场景中越来越少使用&#xff0c;加密 Webshell 正变得日趋流行。 由于通信流量被…

JAVA时间类及JAVA8新时间类

文章目录Java旧时间类关系图![在这里插入图片描述](https://img-blog.csdnimg.cn/e2c2c26c841e40bdb9cc85d0fc4bc1df.png)GMT、时间戳、统一标准时间、时区Java时间类创建时间类示例java.text.DateFormat时间格式转换java.util.Calendar总结Java时间类Java8新时间类InstantCloc…

Vulnhub-DC-2实战靶场

Vulnhub-DC-2实战靶场 https://blog.csdn.net/ierciyuan/article/details/127560871 这次试试DC-2&#xff0c;目标是找到官方设置的5个flag。 一. 环境搭建 1. 准备工具 虚拟机Kali&#xff1a; 自备&#xff0c;我的kali的IP为192.168.3.129 靶场机&#xff1a; https…

接口和抽象类

接口(Interface)和抽象类(Abstract Class)是支持抽象类定义的两种机制。 1.抽象类 (1)说明 在Java中被abstract关键字修饰的类称为抽象类&#xff0c;被abstract关键字修饰的方法称为抽象方法&#xff0c;抽象方法只有方法的声明&#xff0c;没有方法体。抽象类是用来捕捉子…

CCNP350-401学习笔记(151-200题)

151、Which two LISP infrastructure elements are needed to support LISP to non-LISP internetworking? (Choose two.)A. PETR B. PITRC. MR D. MS E. ALT 152、In an SD-WAN deployment, which action in the vSmart controller responsible for? A. handle, maintain, …

一文搞懂C/C++内存管理原理与实现

C 语言内存管理指对系统内存的分配、创建、使用这一系列操作。在内存管理中&#xff0c;由于是操作系统内存&#xff0c;使用不当会造成毕竟麻烦的结果。本文将从系统内存的分配、创建出发&#xff0c;并且使用例子来举例说明内存管理不当会出现的情况及解决办法。 一、内存 …

[python入门㊽] - 自定义异常 raise 关键字

目录 ❤ 自定义抛出异常关键字 - raise ❤ 使用raise主动引发异常 ❤ raise 关键字的用法 ❤ 触发异常 ❤ 自定义异常类 在前面我们学过异常三个关键字分别是try、except 以及 finally 在编程过程中合理的使用异常可以使得程序正常的执行。有直接抛出异常的形式&…