使用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.rhkb.cn/news/325430.html

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

相关文章

从0开始linux(2)——热键、如何查阅手册

欢迎来到博主的专栏——从0开始linux 博主ID&#xff1a;代码小豪 博主使用的linux发行版&#xff1a;CentOS 7.6 文章目录 热键[Tab]热键Ctrl cshift[pageup]和[pagedown] 查阅手册\-\-help在线手册maninfo手册 热键 我们的命令行模式中存在很多的组合热键&#xff0c;这些按…

神经网络中的算法优化(皮毛讲解)

抛砖引玉 在深度学习中&#xff0c;优化算法是训练神经网络时至关重要的一部分。 优化算法的目标是最小化&#xff08;或最大化&#xff09;一个损失函数&#xff0c;通常通过调整神经网络的参数来实现。 这个过程可以通过梯度下降法来完成&#xff0c;其中梯度指的是损失函数…

利用“AnaTraf“网络流量分析仪轻松诊断和优化网络

网络性能监测和诊断(NPMD)是网络管理和优化的重要环节,准确快速地定位和排除网络故障对于保障业务正常运转至关重要。作为一款专业的网络流量分析设备,AnaTraf网络流量分析仪凭借其强大的流量分析和故障诊断功能,为网络管理者提供了一个高效的网络优化解决方案。 全面掌握网络…

计算机网络实验1:交换机基本配置管理

实验目的和要求 安装Packer Tracer&#xff0c;了解Packer Tracer的基本操作掌握交换机基本命令集实验项目内容 认识Packet Tracer软件 交换机的基本配置与管理 交换机的端口配置与管理 交换机的端口聚合配置 交换机划分Vlan配置 实验环境 硬件&#xff1a;PC机&#x…

体验MouseBoost PRO,让Mac操作更高效

还在为Mac的右键功能而烦恼吗&#xff1f;试试MouseBoost PRO for Mac吧&#xff01;这款强大的鼠标右键增强软件&#xff0c;能让你通过简单操作即可激活多种实用功能&#xff0c;让你的工作变得更加轻松。其高度定制化的设计&#xff0c;更能满足你的个性化需求。赶快下载体验…

【ubuntu】ubuntu-18.04开机卡在Starting User Manager for UID 120....问题解决方案

错误截图 解决方案 启动系统&#xff0c;开机界面单击按键esc键&#xff0c;注意需要将鼠标定位到菜单界面&#xff0c;移动键盘上下键选择Advanced options for Ubuntu 进入如下菜单&#xff0c;选择recovery mode 回车之后会弹出如下界面&#xff0c;选择如下root&#xff0…

瀚高数据库(HighGoDB)Windows安装使用

1.下载 2.安装 瀚高数据库下载与安装&#xff08;Windows版&#xff09;-CSDN博客 3.连接工具 4.建库、建表操作 瀚高数据库管理工具-CSDN博客 *报错Cant access non-default database&#xff0c;需要右键数据库-设为活动对象 5.导入外部数据&#xff08;迁移、对比&…

生信人写程序1. Perl语言模板及配置

生物信息领域常用语言 个人认为&#xff1a;是否能熟悉使用Shell(项目流程搭建)R(数据统计与可视化)Perl/Python/Java…(胶水语言&#xff0c;数据格式转换&#xff0c;软件间衔接)三门语言是一位合格生物信息工程师的标准。 生物信息常用语言非常广泛&#xff0c;我常用的有…

爱普生推出5G基站可用耐高温高稳定性温补晶振

爱普生推出了六款新的温补晶振型号:TG7050CKN&#xff0c;TG7050SKNTG7050CMN&#xff0c;TG7050SMN&#xff0c;TG-5510CA&#xff0c;TG-5511CA。这几款的特点就是耐高温温度可达105℃C高温&#xff0c;而且都是高稳定性温补晶振&#xff0c;而且都是7050尺寸&#xff0c;这个…

AVL 树的理解和简单实现

目录 1. AVL 树 1.1. AVL 树的概念 1.2. AVL 树的性质 2. AVL 树的框架如下 2. AVL树的 插入 2.1. 平衡因子的更新 2.2.1. 平衡因子更新的第一种情况 2.2.2. 平衡因子更新的第二种情况 2.2.3. 平衡因子更新的第三种情况 2.2.4. 平衡因子更新的代码框架如下 2.2. AV…

Centos 中如何汉化man命令

刚学Linux&#xff0c;记不住命令和选项&#xff0c;很依赖里面的 man 查看命令&#xff0c;但因为着实看不懂&#xff0c;有没有什么办法把man查看命令的信息改成中文 在CentOS 7中&#xff0c;你可以通过安装man-pages-zh包来获取中文的man手册。以下是具体的步骤&#xff1a…

Java入门基础学习笔记1——初识java

1、为什么学习java&#xff1f; 几乎统治了服务端的开发&#xff1b;几乎所有的互联网企业都使用&#xff1b;100%国内大中型企业都用&#xff1b;全球100亿的设备运行java。开发岗位薪资高。 Java的流行度很高&#xff0c;商用占有率很高。 可移植性。 2、Java的背景知识 …

(动画详解)LeetCode面试题 02.04.分割链表

&#x1f496;&#x1f496;&#x1f496;欢迎来到我的博客&#xff0c;我是anmory&#x1f496;&#x1f496;&#x1f496; 又和大家见面了 欢迎来到动画详解LeetCode系列 用通俗易懂的动画的动画使leetcode算法题可视化 先来自我推荐一波 个人网站欢迎访问以及捐款 推荐阅读…

Jsp+Servlet实现图片上传和点击放大预览功能(提供Gitee源码)

前言&#xff1a;在最近老项目的开发中&#xff0c;需要做一个图片上传和点击放大的功能&#xff0c;在Vue和SpringBoot框架都有现成封装好的组件和工具类&#xff0c;对于一些上世纪的项目就没这么方便了&#xff0c;所以需要自己用原生的代码去编写&#xff0c;这里分享一下我…

中国当代最具影响力的人物颜廷利:死神(死亡)并不可怕,可怕的是…

中国当代最具影响力的人物颜廷利&#xff1a;死神&#xff08;死亡&#xff09;并不可怕&#xff0c;可怕的是… 在中国优秀传统文化之中&#xff0c;汉语‘巳’字与‘四’同音&#xff0c;在阿拉伯数字里面&#xff0c;通常用‘4’来表示&#xff1b; 作为汉语‘九’字&#x…

第三步->手撕spring源码之基于Cglib实现实例化策略

为什么深入研究spring源码&#xff1f; 其实每一个程序员每天的工作都是一贯的CRUD 实现业务和需求完成的操作。几年这样的操作让我感觉在这方面要提神能力 光靠CRUD是绝对不可能的事情 CRUD只是满足你作为一个搬砖人而已。编程能力提升&#xff1f;其实更多的编程能力的提升是…

【408真题】2009-03

“接”是针对题目进行必要的分析&#xff0c;比较简略&#xff1b; “化”是对题目中所涉及到的知识点进行详细解释&#xff1b; “发”是对此题型的解题套路总结&#xff0c;并结合历年真题或者典型例题进行运用。 涉及到的知识全部来源于王道各科教材&#xff08;2025版&…

如何远程控制另一部手机:远程控制使用方法

在现今高科技的社会中&#xff0c;远程控制手机的需求在某些情境下变得越来越重要。不论是为了协助远在他乡的家人解决问题&#xff0c;还是为了确保孩子的在线安全&#xff0c;了解如何实现这一功能都是有益的。本文将为您简要介绍几种远程控制手机的方法及其使用要点。 KKVi…

第五届电子通讯与人工智能学术会议(ICECAI 2024, 5/31-6/2)

目录 1. 会议官方2. 会议新闻中华人民共和国教育部新闻 3. 出版历史4. 大会简介5. 主办单位与嘉宾主办单位承办单位主讲嘉宾组委会 6. 征稿主题7. 论文出版8. 参会说明 1. 会议官方 2024 5th International Conference on Electronic communication and Artificial Intelligenc…

算法提高之字串变换

算法提高之字串变换 核心思想&#xff1a;双向广搜 双向bfs 建立两个队列 一起bfs到中间态 #include <iostream>#include <cstring>#include <algorithm>#include <queue>#include <unordered_map>using namespace std;const int N 6;int n;…