使用Three.js绘制快速而逼真的水

本文将利用GPUComputationRenderer来实现水波纹的绘制,相似的案例可以看threejs官方的GPGPU Water示例。更多精彩内容尽在数字孪生平台。

image.png

什么是 GPGPU

GPGPU代表通用图形处理单元(General-Purpose Graphic Processing Unit),意思是用GPU计算图形以外的计算任务。在 Three.js 里,指的是我们使用片段着色器来计算其他内容(例如粒子的位置或速度)的技术。通过使用 GPU(GLSL 着色器)而不是 CPU(Javascript)来进行这些计算,我们可以从并行计算中获得巨大的性能提升。

但是我们在 Three.js 中并不经常看到这个术语,更常见的是“帧缓冲区对象”或 FBO 一词。我们字面上称其为 FBO 技术,因为它使用额外的渲染目标来保存你希望 GPU 执行的额外计算,而“渲染目标”本质上是一个“帧缓冲区”。帧缓冲区通常是在底层中使用的功能,请参阅文档。

让我们看看 GPUComputationRenderer 实际上做了什么(threejs的源代码)。它是一个帮助类,可让我们创建数据纹理,通过自定义片段着色器将计算结果存储到每个纹素中的 4 个浮点数 (xyzw) 中。换句话说,每个通道 32 位,每个纹素 16 字节。

这里需要注意的是,与每个颜色通道仅存储 8 位的普通图像纹理相比,使用浮点数的数据纹理能够以更高的精度存储更大的数字。

而且 GPUComputationRenderer中定义的数据纹理(称为“变量”)可以依赖其前一帧的结果来计算下一帧;我们甚至可以设置多个相互依赖的数据纹理。

以“GPGPU Water”示例为例,它只定义了一个变量,命名为heightmap,因为它的唯一功能是计算每一帧中水波的高度图。在第一帧中,高度图具有初始状态(我们偏好的随机值),但当片段着色器完成计算后,输出数据纹理将被分配回高度图变量本身以供下一帧使用,并且依此类推。
下一帧沿用上一帧的数据

具体实现

首先我们为场景添加

  • 方向光,用来照亮场景
  • 水的平面mesh,我们将水波高度图应用于此
  • raycaster,可以将鼠标数据传递到 FBO 中的计算片段着色器
  • 创建 FBO 的 GPUComputationRenderer

水面Mesh

首先创建几何和材质。

const plane = new THREE.PlaneGeometry( GEOM_WIDTH, GEOM_HEIGHT, FBO_WIDTH - 1, FBO_HEIGHT - 1 );
this.waterMat = new THREE.MeshPhongMaterial({color: new THREE.Color( 0x0040C0 )
})

然后定义了四个常量:

const FBO_WIDTH = 128
const FBO_HEIGHT = 128
const GEOM_WIDTH = 512
const GEOM_HEIGHT = 512

这里我们将平面几何体的宽度/高度的段数指定为相应的 FBO 的宽度 - 1。这是因为我们希望几何体的顶点数与 FBO 中的纹素数完全相同。

接下来我们通过 onBeforeCompile 扩展 Phong 材质:

this.waterMat.userData.heightmap = { value: null }this.waterMat.onBeforeCompile = (shader) => {shader.uniforms.heightmap = this.waterMat.userData.heightmapshader.vertexShader = shader.vertexShader.replace('#include <common>', `uniform sampler2D heightmap;#include <common>`)shader.vertexShader = shader.vertexShader.replace('#include <beginnormal_vertex>', `// Compute normal from heightmapvec2 cellSize = vec2( 1.0 / (${FBO_WIDTH.toFixed( 1 )}), 1.0 / ${FBO_HEIGHT.toFixed( 1 )} );vec3 objectNormal = vec3(( texture2D( heightmap, uv + vec2( - cellSize.x, 0 ) ).x - texture2D( heightmap, uv + vec2( cellSize.x, 0 ) ).x ) * ${FBO_WIDTH.toFixed( 1 )} / ${GEOM_WIDTH.toFixed( 1 )},( texture2D( heightmap, uv + vec2( 0, - cellSize.y ) ).x - texture2D( heightmap, uv + vec2( 0, cellSize.y ) ).x ) * ${FBO_HEIGHT.toFixed( 1 )} / ${GEOM_HEIGHT.toFixed( 1 )},1.0 );`)shader.vertexShader = shader.vertexShader.replace('#include <begin_vertex>', `float heightValue = texture2D( heightmap, uv ).x;vec3 transformed = vec3( position.x, position.y, heightValue );`)
}

我们在 this.waterMat.userData.heightmapShader.uniforms.heightmap 之间建立了链接,这样每当从 FBO 计算出新的高度图值时,我们就可以将其保存到材质的 userData 中,然后更新shader中的高度图数据uniform。

可以看到,我们将网格上的 z 位置替换为高度图的值。它是 Z 轴而不是 Y 轴,是因为我们将网格旋转了 90 度,因此它水平面朝上。我们从高度图纹理中获取 x 值,这是我们存储每个点的当前高度的槽。然后因为我们改变了高度位置,所以我们还需要重新计算法线。

最后,我们构建网格并将其添加到场景中:

this.waterMesh = new THREE.Mesh( plane, this.waterMat )
this.waterMesh.rotation.x = - Math.PI / 2
// as the mesh is static, we can turn auto update off: https://threejs.org/docs/#manual/en/introduction/Matrix-transformations
this.waterMesh.matrixAutoUpdate = false
this.waterMesh.updateMatrix()
scene.add( this.waterMesh )

Raycasting

如果我们想让鼠标移动触发涟漪效果,则需要Raycasting将鼠标的屏幕坐标投射到场景中,求出相交的世界坐标。

首先,我们初始化一些变量和 Raycaster

this.mouseMoved = false
this.pointer = new THREE.Vector2()
this.raycaster = new THREE.Raycaster()

然后我们定义绑定到pointermove事件的函数:

onPointerMove( event ) {if ( event.isPrimary === false ) return// converting mouse coordinates into -1 to +1 spacethis.pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1this.pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1this.mouseMoved = true
}this.container.addEventListener( 'pointermove', this.onPointerMove.bind(this) )

在每帧调用的更新函数中,我们添加以下代码:

const hmUniforms = this.heightmapVariable.material.uniforms
if ( this.mouseMoved ) {this.raycaster.setFromCamera( this.pointer, camera )const intersects = this.raycaster.intersectObject( this.waterMesh )if ( intersects.length > 0 ) {const point = intersects[ 0 ].point// point是世界坐标hmUniforms[ 'mousePos' ].value.set( point.x, point.z )} else {hmUniforms[ 'mousePos' ].value.set( 10000, 10000 )}this.mouseMoved = false
} else {hmUniforms[ 'mousePos' ].value.set( 10000, 10000 )
}

hmUniforms 是 FBO 使用的计算片段着色器中的uniform变量。当鼠标移动时,我们计算鼠标和水网格的交点,并将该点的世界坐标保存到 hmUniforms[‘mousePos’] 中。由于计算片段着色器负责计算波高,因此我们可以很自然地将鼠标与水的交点传递给它。当没有鼠标移动时,我们只需将其设置到画布外的某个位置即可。

设置 FBO

最后是这个示例的核心部分。我们将计算片段着色器中的波浪计算位留到最后。
首先引入文件:

import { GPUComputationRenderer } from "three/examples/jsm/misc/GPUComputationRenderer"
import { SimplexNoise } from "three/examples/jsm/math/SimplexNoise"
import HeightmapFragment from "./shaders/heightmapFragment.glsl"

我们需要 SimplexNoise 来生成水网格的初始高度位置。

接下来我们定义一些用户可更改的参数:

const params = {mouseSize: 20.0,viscosity: 0.98,waveHeight: 0.3,
}

为了初始化 GPUComputationRenderer,我们需要传入用于存储计算的高度图的数据纹理的尺寸,以及我们用于场景绘制的渲染器。

this.gpuCompute = new GPUComputationRenderer( FBO_WIDTH, FBO_HEIGHT, renderer )
if ( renderer.capabilities.isWebGL2 === false ) {this.gpuCompute.setDataType( THREE.HalfFloatType )
}

我们的数据纹理的分辨率为 128 x 128,这样每个纹素都存储水网格上相应顶点的高度值。这也是为什么之前我们将宽度/高度段数设置为 127,因为总顶点数将为 128 x 128。

接下来我们需要创建真正的 FBO,即 this.heightmapVariable

const heightmap0 = this.gpuCompute.createTexture()
this.fillTexture( heightmap0 )
this.heightmapVariable = this.gpuCompute.addVariable( 'heightmap', HeightmapFragment, heightmap0 )
this.gpuCompute.setVariableDependencies( this.heightmapVariable, [ this.heightmapVariable ] )

this.heightmapVariable主要存储3个东西:

  • 使用计算片段着色器渲染的虚拟/离屏网格的材质
  • 此离屏渲染的渲染目标
  • 对其他 FBO 的依赖

这里我们将 FBO 设置为依赖于自身,这样我们就可以将最后一帧的高度图位置作为输入来计算下一帧的高度图位置。

我们调用 this.fillTexture 方法使用 Simplex Noise 生成初始高度数据:

fillTexture( texture ) {const waterMaxHeight = 2;const simplex = new SimplexNoise()function layeredNoise( x, y ) {let multR = waterMaxHeight;let mult = 0.025;let r = 0;for ( let i = 0; i < 10; i ++ ) {r += multR * simplex.noise( x * mult, y * mult );multR *= 0.5;mult *= 2;}return r;}const pixels = texture.image.data;let p = 0;for ( let j = 0; j < FBO_HEIGHT; j ++ ) {for ( let i = 0; i < FBO_WIDTH; i ++ ) {const x = i * 128 / FBO_WIDTH;const y = j * 128 / FBO_HEIGHT;pixels[ p + 0 ] = layeredNoise( x, y );pixels[ p + 1 ] = 0;pixels[ p + 2 ] = 0;pixels[ p + 3 ] = 1;p += 4;}}
}

然后我们设置要在计算片段着色器中使用的uniform和define:

this.heightmapVariable.material.uniforms[ 'mousePos' ] = { value: new THREE.Vector2( 10000, 10000 ) }
this.heightmapVariable.material.uniforms[ 'mouseSize' ] = { value: params.mouseSize }
this.heightmapVariable.material.uniforms[ 'viscosityConstant' ] = { value: params.viscosity }
this.heightmapVariable.material.uniforms[ 'waveheightMultiplier' ] = { value: params.waveHeight }
this.heightmapVariable.material.defines.GEOM_WIDTH = GEOM_WIDTH.toFixed( 1 )
this.heightmapVariable.material.defines.GEOM_HEIGHT = GEOM_HEIGHT.toFixed( 1 )

调用init来完成初始化!

const error = this.gpuCompute.init()
if ( error !== null ) {console.error( error )
}

添加可调整参数的 GUI 控件:

const gui = new dat.GUI()
gui.add(params, "mouseSize", 1.0, 100.0, 1.0 ).onChange((newVal) => {this.heightmapVariable.material.uniforms[ 'mouseSize' ].value = newVal
})
gui.add(params, "viscosity", 0.9, 0.999, 0.001 ).onChange((newVal) => {this.heightmapVariable.material.uniforms[ 'viscosityConstant' ].value = newVal
})
gui.add(params, "waveHeight", 0.1, 2.0, 0.05 ).onChange((newVal) => {this.heightmapVariable.material.uniforms[ 'waveheightMultiplier' ].value = newVal
})

最后,在每帧调用的更新函数中,我们必须手动告诉 this.gpuCompute 计算新的高度图数据。然后我们可以立即将计算结果传递给水网格的着色器:

this.gpuCompute.compute()
this.waterMat.userData.heightmap.value = this.gpuCompute.getCurrentRenderTarget( this.heightmapVariable ).texture

如何模拟波浪

现在我们进入本教程的最后部分,先来看下 heightmapFragment.glsl 里有什么。

#define PI 3.1415926538uniform vec2 mousePos;
uniform float mouseSize;
uniform float viscosityConstant;
uniform float waveheightMultiplier;void main() {vec2 cellSize = 1.0 / resolution.xy;vec2 uv = gl_FragCoord.xy * cellSize;// heightmapValue.x 为倒数第一帧的高度// heightmapValue.y 为倒数第二帧的高度vec4 heightmapValue = texture2D( heightmap, uv );// 获取相邻值vec4 north = texture2D( heightmap, uv + vec2( 0.0, cellSize.y ) );vec4 south = texture2D( heightmap, uv + vec2( 0.0, - cellSize.y ) );vec4 east = texture2D( heightmap, uv + vec2( cellSize.x, 0.0 ) );vec4 west = texture2D( heightmap, uv + vec2( - cellSize.x, 0.0 ) );float newHeight = ( ( north.x + south.x + east.x + west.x ) * 0.5 - heightmapValue.y ) * viscosityConstant;// 鼠标影响float mousePhase = clamp( length( ( uv - vec2( 0.5 ) ) * vec2(GEOM_WIDTH, GEOM_HEIGHT) - vec2( mousePos.x, - mousePos.y ) ) * PI / mouseSize, 0.0, PI );newHeight += ( cos( mousePhase ) + 1.0 ) * waveheightMultiplier;heightmapValue.y = heightmapValue.x;heightmapValue.x = newHeight;gl_FragColor = heightmapValue;}
  • resolution 变量定义为数据纹理的分辨率,来自我们构建 GPUComputationRenderer 时传入的宽度和高度参数。
  • 在我们定义了 this.heightmapVariable 的自依赖之后,heightmap 变量也是在这里自动定义的。它应该与 this.heightmapVariable.name 具有相同的名称。

至于水波算法暂时先不讲述了。

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

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

相关文章

1146 -Table ‘performance schema.session variables‘ doesn‘t exist的错误解决

一、问题出现 今天在本地连数据库的时候&#xff0c;发现这个问题&#xff0c;哎呦我擦&#xff0c;差点吓死了 二、解决办法 1&#xff09;找文件 用everything搜一下MySQL Server 5.7 然后去Windows服务找一下MySQL配置文件的具体路径 如果知道那最好&#xff0c;不知道那…

宝塔8.1.0去除绑定用户

非要绑定手机号&#xff0c;确实很烦 1&#xff0c;/www/server/panel/BTPanel __init__.py if not public.is_bind():return redirect(/bind, 302) 将is_bind的路由全部注释 2&#xff0c;/www/server/panel/class下 panelPlugin.py 注释异常&#xff0c; 新增 softLis…

SSL协议

SSL 安全传输协议&#xff08;安全套接层&#xff09; 也叫TLS ---- 传输层安全协议 SSL的工作原理&#xff1a;SSL协议因为是基于TCP协议工作的&#xff0c;通信双方需要先建立TCP会话。因为SSL协议需要进行安全保证&#xff0c;需要协商安全参数&#xff0c;所以也需要建立…

【MySQL】7.MySQL性能优化的六大核心策略

数据库的性能对整个应用的响应速度和用户体验起着至关重要的作用。MySQL&#xff0c;作为广泛使用的开源关系型数据库&#xff0c;提供了丰富的性能优化手段。从资源优化、查询优化到结构、配置、代码乃至架构优化&#xff0c;每一个层面的调整都可能带来性能的飞跃。本文将深入…

springboot房屋租赁系统

摘要 房屋租赁系统&#xff1b;为用户提供了一个房屋租赁系统平台&#xff0c;方便管理员查看及维护&#xff0c;并且可以通过需求进行设备信息内容的编辑及维护等&#xff1b;对于用户而言&#xff0c;可以随时进行查看房屋信息和合同信息&#xff0c;并且可以进行报修、评价…

清理缓存简单功能实现

在程序开发中&#xff0c;经常会用到缓存&#xff0c;最常用的后端缓存技术有Redis、MongoDB、Memcache等。 而有时候我们希望能够手动清理缓存&#xff0c;点一下按钮就把当前Redis的缓存和前端缓存都清空。 功能非常简单&#xff0c;创建一个控制器类CacheController&#xf…

SpringBoot PowerMockito 私有/静态/方法/属性

SpringBoot PowerMockito 私有/静态/方法/属性 1 PrepareForTest2 待测试类3 测试类 1 PrepareForTest PrepareForTest 是 PowerMockito 提供的一个注解&#xff0c;用于告诉 PowerMockito 哪些类需要被修改以允许使用 PowerMockito 的功能。 PowerMockito 主要用于修改 Java…

【计算机毕业设计】基于SSM+Vue的线上旅行信息管理系统【源码+lw+部署文档+讲解】

目录 1 绪论 1.1 研究背景 1.2 设计原则 1.3 论文组织结构 2 系统关键技术 2.1JSP技术 2.2 JAVA技术 2.3 B/S结构 2.4 MYSQL数据库 3 系统分析 3.1 可行性分析 3.1.1 技术可行性 3.1.2 操作可行性 3.1.3 经济可行性 3.1.4 法律可行性 3.2系统功能分析 3.2.1管理员功能分析 3.2.…

JavaScript精粹(一)

JavaScript&#xff08;简称为JS&#xff09;是一种广泛应用于网页开发的脚本语言&#xff0c;具有以下几个主要作用&#xff1a; 网页交互&#xff1a;JavaScript 可以用于创建动态的网页效果&#xff0c;例如响应用户的操作&#xff0c;实现页面内容的动态更新&#xff0c;以…

Java SE vs Java EE:深入剖析及面试指南

Java 平台提供了多个版本来满足不同应用场景的需求&#xff0c;其中最常用的是 Java SE&#xff08;Standard Edition&#xff09;和 Java EE&#xff08;Enterprise Edition&#xff09;。理解这两个版本的区别对于任何 Java 开发者都是至关重要的&#xff0c;尤其是在面试过程…

C++字符串细节,面试题06

文章目录 22. 字符串22.1. 字符数组 vs 字符指针 vs 常量字符指针 vs string22.2. strcpy vs sprintf vs memcpy22.3. strlen vs length vs size vs sizeof22.4. 字符串之间的转换22.5 其他数据类型与字符串之间的转换22.6 字符串分割 22. 字符串 22.1. 字符数组 vs 字符指针 …

Spring整合其他技术

文章目录 Spring整合mybatis思路分析Mybatis程序核心对象分析整合Mybatis 代码实现 Spring整合Junit修改成警告 Spring整合mybatis 思路分析 Mybatis程序核心对象分析 上面图片是mybatis的代码&#xff0c;上述有三个对象&#xff0c;分别是sqlSessionFactory&#xff0c;sqlS…

Linux:配置客户端自定义autofs服务

Linux&#xff1a;配置客户端自定义autofs服务 修改autofs的主策略文件 [rootserver200 data]# vim /etc/auto.master# 修改内容如下 /misc /etc/auto.misc # 挂载目录的上级目录 /mnt /etc/auto.timinglee --timeout3修改autofs的自定义策略文件 [rootserver200 data]…

数据库SQL编写规范-SQL书写规范整理(SQL语句书写规范全解-Word原件)

编写本文档的目的是保证在开发过程中产出高效、格式统一、易阅读、易维护的SQL代码。 1 编写目 2 SQL书写规范 3 SQL编写原则 软件全套精华资料包清单部分文件列表&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说…

鸿蒙布局Column/Row/Stack

鸿蒙布局Column/Row/Stack 简介我们以Column为例进行讲解1. Column({space: 10}) 这里的space: 10&#xff0c;表示Column里面每个元素之间的间距为102. width(100%)&#xff0c;height(100%) 表示宽高占比3. backgroundColor(0xffeeeeee) 设置背景颜色4. padding({top: 50}) 设…

【ARM 嵌入式 C 文件操作系列 20.4 -- 打印 uint64_t 类型的数值】

文章目录 C代码中 打印 uint64_t 类型的数值测试效果 C代码中 打印 uint64_t 类型的数值 为了以16进制方式打印uint64_t类型的数值&#xff0c;可以使用printf函数&#xff0c;配合<inttypes.h>头文件中定义的宏PRIX64或PRIx64。这些宏确保了无论在哪个平台上&#xff0…

keepalived双机热备超详细入门介绍

keepalived 一、keepalived入门介绍 1.keepalived简介 2.keepalived服务的三个重要功能 2.1.管理LVS负载均衡软件 2.2.实现对LVS集群节点健康检查功能 2.3.作为系统网络服务的高可用功能 3.keepalived高可用故障切换转移原理 4.keepalived安装及主配置文件介绍 …

如何用Rust获取本机CPU、内存在Web网页中显示?

目录 一、需求描述 二、具体操作步骤 三、知识点 1、systemstat 2、Actix 一、需求描述 需求&#xff1a; 1、需要使用Rust进行后端开发获取本机CPU和内存信息&#xff1b; 2、使用WEB框架发布API&#xff1b; 3、然后使用HTML/CSS/JavaScript进行前端开发&#xff0…

MySQL表的增删查改【基础部分】

数据表的操作 新增 普通插入 insert into 表名 values(值,值...)注意&#xff1a; 此处的值要和表中的列相匹配 使用’‘单引号或者”“双引号来表示字符串 mysql> insert into student values(123,zhangsan); Query OK, 1 row affected (0.02 sec)指定列插入 insert …

2024年3月 电子学会 青少年等级考试机器人理论真题五级

202403 青少年等级考试机器人理论真题五级 第 1 题 下图程序运行后&#xff0c;串口监视器显示的结果是&#xff1f;&#xff08; &#xff09; A&#xff1a;0 B&#xff1a;1 C&#xff1a;3 D&#xff1a;4 第 2 题 下列选项中&#xff0c;关于74HC595移位寄存器芯片的…