使用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://xiahunao.cn/news/3032022.html

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

相关文章

T型三电平逆变器的Simulink仿真

1 T型三电平拓扑的开关状态 图1为T字型-三电平电路单相拓扑&#xff0c;拓扑中共有4个IGBT&#xff0c;4个二极管&#xff0c;还有电容组C1和C2&#xff1b;假设正负母线电压均等&#xff0c;都是Vdc。将T1&#xff0c;T2&#xff0c;T3&#xff0c;T4的状态用1和0分别表示&…

AI + Web3 如何打造全新创作者经济模型?

可编程 IP 的兴起&#xff0c;借助人工智能极大提高创作效率和效能&#xff0c;让 Web3 用户体会到了自主创作和产品制作的乐趣。然而&#xff0c;你知道 AI 时代来临的背景下&#xff0c;创作者经济模型又该如何在 Web3 技术的加持下走向更成熟的运作轨道吗&#xff1f;第 43 …

手机恢复出厂设置会怎么样?一切回到了原点?

随着智能手机的普及&#xff0c;我们每天都在与手机紧密互动&#xff0c;里边存储了大量的个人信息和应用数据。然而&#xff0c;有时候我们会遇到手机卡顿、应用崩溃或数据丢失的问题。这时&#xff0c;恢复出厂设置成为了许多人的选择。那么&#xff0c;手机恢复出厂设置会怎…

网站访问提示不安全怎么办??

当网站访问时提示“不安全”&#xff0c;这通常与网站的SSL证书有关&#xff0c;或者是网站本身存在一些安全风险。以下是一些解决步骤和建议&#xff1a; 1、检查URL前缀&#xff1a;首先&#xff0c;检查URL是否以https://开头。如果仍然是http://&#xff0c;则网站没有使用…

详解GaussDB(DWS)中的行执行引擎

1.前言 GaussDB&#xff08;DWS&#xff09;包含三大引擎&#xff0c;一是SQL执行引擎&#xff0c;用来解析用户输入的SQL语句&#xff0c;生成执行计划&#xff0c;供执行引擎来执行&#xff1b;二是执行引擎&#xff0c;其中包含了行执行引擎和列执行引擎&#xff0c;执行引擎…

盘点自动驾驶的技术发展趋势

自动驾驶技术在不断发展变快&#xff0c;我们之前提过算法岗如今越来越卷&#xff0c;从今年的就业局势看&#xff0c;前年还属于蓝海行业的自动驾驶&#xff0c;今年就已经满满关上了招揽之门——呈红海之势。作为在这个行业中摸爬滚打的一以子&#xff0c;我们到底该如何纵观…

agiletc部署

数据库创建及运行 启动命令 cd /AgileTC/case-server&& nohup mvn spring-boot:run &查看是否启动成功 http://192.168.101.:8094/case/caseList/1需要安装java javac等 一、安装java 1 安装java11 sudo yum install java-11-openjdk-devel -y2 切换到java11 …

c++ 获取机器码

看到网上代码代码都没什么好的&#xff0c;自己备用一个 #include <iostream> #include <string> #include <sstream> #include <iomanip> #include <Windows.h> #include <iphlpapi.h> // 包含这个头文件以获取 PIP_ADAPTER_INFO #inclu…

【NodeMCU实时天气时钟温湿度项目 4】通过NTPClient库获取实时网络时间并显示在TFT屏幕上

今天是【实时天气时钟温湿度项目】第四专题&#xff0c;主要内容是&#xff1a;学习导入NTPClient库&#xff0c;通过这个库获取实时网络时间&#xff0c;显示在1.3寸TFT液晶屏幕上。此前三个专题&#xff0c;请选择查看以下链接。 第一专题内容&#xff0c;请参考 【N…

阿里云和AWS负载均衡服务对比分析

在云计算时代,负载均衡作为一种关键的网络基础设施,承担着在多个服务器之间分发网络流量的重要任务。作为全球两大主要的云服务提供商,阿里云和Amazon Web Services(AWS)都提供了强大的负载均衡解决方案。本文将从性能、功能、可用性和成本等方面对两者进行对比分析。我们九河云…

this指针详解

目录 this指针this指针的引出this指针的特性this指针相关例题例题1例题2 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &#x1f978;&#x1f978;&#x1f978; C语言 &#x1f43f;️&#x1f43f;️…

【UE Niagara】在UI上生成粒子

效果 步骤 1. 在虚幻商城中将“Niagara UI Render”插件安装到引擎 2. 打开虚幻编辑器&#xff0c;勾选插件“Niagara UI Renderer”&#xff0c;然后重启编辑器 3. 先创建一个控件蓝图&#xff0c;该控件蓝图只包含一个按钮 这里设置尺寸框尺寸为200*50 4. 显示该控件 5. 新…

3月笔记本电脑行业线上市场销售数据分析

笔记本电脑市场在过去几年中经历了起伏&#xff0c;但总体上呈现出稳定增长的态势。特别是随着远程办公、在线学习等需求的增加&#xff0c;以及消费者对于便携性、高性能等方面的追求&#xff0c;笔记本电脑市场得到了进一步的发展。 据鲸参谋数据统计&#xff0c;线上平台&a…

杰理AC632N 通过写flash修改蓝牙名字

杰理修改蓝牙名字一般有他自己的一个工具的,如下图,在编译前修改 现在用写flash的方式更改: 主要看杰理CFG_BT_NAME这个宏里面的.c文件 杰理给这里的flash都定义好每个宏的信息,你需要读写就行,修改后记得重启上电才能生效.如果你要自定义数据写进去flash断电不丢失,估计就是在…

企业微信集成H5授权登录相关知识(二)

流程&#xff1a; 1.前端请求企业微信获取code&#xff1a;官网网页授权链接 2.企业微信返回的code请求后端判断是否已绑定系统账户 3.后端根据企业微信code&#xff0c;accessToken获得userId 4.userId获取user进行oauth2授权方式进行免密登录 相关知识&#xff1a; 一&a…

QT——tableWidget-跳变之舞V1.0-记录学习【1】

QT——tableWidget-跳变之舞V1.0-记录学习【1】 文章目录 QT——tableWidget-跳变之舞V1.0-记录学习【1】前言一、利用QT创建项目文件1.1 完整项目文件如下图所示:1.2 演示&#xff1a; 二、声明文件&#xff1a;2.1 主界面声明文件:mainwindow.h&#xff1b;2.2 控制窗口声明文…

【详细教程】基于pyEchart的封装(附代码)

目 录 一、项目结构 二、文件封装 2.1echart.py 2.2pyechartMock.py 三、结果 3.1柱状图 3.2折线图 3.3饼状图 最近在学习pyechart&#xff0c;老师要我们画几个简单的图&#xff0c;比如折线图&#xff0c;柱状图&#xff0c;饼状图&#xff0c;我这里在参考pyechart…

答辩PPT设计无从下手?哪些AI工具可以提供帮助

本科毕业论文答辩PPT该怎么做&#xff1f; 在我原本的认知里面&#xff0c;答辩PPT是要包含论文各个章节的&#xff0c;在答辩时需要方方面面都讲到的&#xff0c;什么摘要、文献综述、实证分析、研究结果样样不落。但是&#xff0c;这大错特错&#xff01; 答辩PPT环节时长…

luceda ipkiss教程 70:合并GDS版图

通过代码拼版&#xff1a; 所有代码如下&#xff1a; from si_fab import all as pdk from ipkiss3 import all as i3class Design1(i3.GDSCell):def _default_filename(self):return "Ring_Test.gds"def _default_name(self):return "Design1"class Des…

大数据基础工程技术团队4篇论文入选ICLR,ICDE,WWW

近日&#xff0c;由阿里云计算平台大数据基础工程技术团队主导的四篇时间序列相关论文分别被国际顶会ICLR2024、ICDE2024和WWW2024接收。 论文成果是阿里云与华东师范大学、浙江大学、南京大学等高校共同研发&#xff0c;涉及时间序列与智能运维结合的多个应用场景。包括基于P…