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

news2025/1/8 16:22:04

本文将利用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.heightmap
  shader.vertexShader = shader.vertexShader.replace('#include <common>', `
    uniform sampler2D heightmap;
    #include <common>
  `)
  shader.vertexShader = shader.vertexShader.replace('#include <beginnormal_vertex>', `
    // Compute normal from heightmap
    vec2 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 space
    this.pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1
    this.pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1
    this.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.1415926538

uniform 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.coloradmin.cn/o/1663507.html

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

相关文章

护肤升级:如何选择最适合您的AI皮肤技术解决方案?

在不断变化的护肤行业中&#xff0c;人工智能技术的整合已经彻底改变了企业满足个人护肤需求的方式。了解人工智能在美容行业的重要性以及提供的解决方案&#xff0c;是选择最合适的解决方案至关重要的。领先的主要参与者之一是玩美移动&#xff0c;他们提供了一套全面的AI皮肤…

算法day04

第一题 &#xff1a; 209. 长度最小的子数组 有上题可知&#xff0c;我们会采用双指针和单调性的思路来解决 我们本题采用左右双指针从数组的0位置同向前进&#xff0c;所以将此类模型称为滑块&#xff1b; 步骤思路如下&#xff1a; 步骤一&#xff1a; 定义所有双指针都指向…

使用 Kubeadm 搭建个公网 k8s 集群(单控制平面集群)

前言 YY&#xff1a;国庆的时候趁着阿里云和腾讯云的轻量级服务器做促销一不小心剁了个手&#x1f60e;&#x1f622;&#xff0c;2 Cores&#xff0c;4G RAM 还是阔以的&#xff0c;既然买了&#xff0c;那不能不用呀&#x1f6a9;&#xff0c;之前一直想着搭建个 k8s 集群玩…

用HAL库改写江科大的stm32入门例子8-1 DMA数据转运

实验目的&#xff1a;通过DMA把buffer的数据搬运到buffer2当中 //declare a buffer to store the data uint32_t buffer[3] {1,2,3};//declare a buffer to store the data uint32_t buffer2[3] {0,0,0}; DMA&#xff1a;是个搬运数据的小助手。 相关设置&#xff1a; main…

算法练习day7

四数相加II 代码随想录 0454.四数相加II 454. 四数相加 II - 力扣&#xff08;LeetCode&#xff09; &#xff08;用时&#xff1a;0.5小时&#xff09; 思路 本道题是需要在四个数组中&#xff0c;各找一个数&#xff0c;这些数加起来能够等于0&#xff0c;那么就是答案元…

ChatGPT未来可能应用于iPhone?

苹果接即将与OpenAI达成协议 ChatGPT未来应用于iPhone 前言 就在5月11日&#xff0c;苹果公司正与OpenAI进行深入讨论&#xff0c;计划在其最新的iOS操作系统中整合OpenAI的先进技术。这一举措是苹果公司在为其产品线融入更先进的人工智能功能所做努力的一部分。 目前情况双方…

Vue路由拆分

1.在src下建立router&#xff0c;在router中建立文件index 2.将main.js中部分内容复制 App <template> <div><a href"#/friend">朋友</a><br><a href"#/info">信息</a><br><a href"#/music&quo…

源代码防泄密的重要性

​源代码”作为互联网企业的核心资产之一&#xff0c;其安全性至关重要。源代码泄露不仅可能导致企业丧失技术优势&#xff0c;还可能引发知识产权纠纷、增加竞争对手的市场竞争力&#xff0c;甚至可能被用于恶意目的&#xff0c;如开发恶意软件等。因此&#xff0c;保护源代码…

没有公网ip,如何实现外网访问内网?

目前拨号上网是最广泛的上网方式&#xff0c;这种方式优点是价格便宜&#xff0c;缺点是没有固定公网ip&#xff0c;每次重新您拨号ip地址都会变。如果有一台服务器&#xff0c;需要实现外网访问&#xff0c;在没有固定公网ip的环境下&#xff0c;该如何实现呢&#xff1f;使用…

apk反编译修改教程系列-----让你反编译apk中 事半功倍的辅助编译软件列举【十六】

在反编译apk中。排除我们常用的编译软件外。还有很多辅助编译软件。例如用来查看代码 转换语言 加解密算法 文件监听 弹窗定位 activity记录 签名验证 dex文件混淆 签校测试 apk伪加密 apk对齐 拆分dex 合并dex 等等类似的功能 通过此课程了解; 01----反编译软件中列…

哪些软件格式在win跟linux上都能运行?

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「linux的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 有一些软件格式在Windows和Li…

基于yolov8的水果检测系统,系统既支持图像检测,也支持视频和摄像实时检测(pytorch框架)【python源码+UI界面+功能源码详解】

更多目标检测和图像分类识别项目可看我主页其他文章 功能演示&#xff1a; 基于yolov8的水果检测系统&#xff0c;系统既支持图像检测&#xff0c;也支持视频和摄像实时检测_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于yolov8的水果检测系统是在pytorch框架下实…

JAVA毕业设计139—基于Java+Springboot+Vue的农产品销售商城(源代码+数据库+万字论文)

毕设所有选题&#xff1a; https://blog.csdn.net/2303_76227485/article/details/131104075 基于JavaSpringbootVue的农产品销售商城(源代码数据库万字论文)139 一、系统介绍 本项目前后端分离&#xff0c;分为管理员、用户两种角色 1、用户&#xff1a; 注册登录、农产品…

【hana】hana1.0多容器常用命令

基础命令 数据库 连接数据库 hdbsql -u system -p {passwd} -i 02 -d {dbname}查询所有数据库 SELECT DATABASE_NAME, ACTIVE_STATUS FROM M_DATABASES;停止数据库&#xff0c;会修改数据库状态为No ALTER SYSTEM STOP DATABASE testdb; 启动数据库&#xff0c;会修改数据…

租用香港Windows服务器要注意的几种安全防护措施

在网络世界里&#xff0c;永远没有绝对的安全&#xff0c;但我们可以通过采取适当的措施使风险降低。对于选择香港Windows服务器租赁的企业和个人来说&#xff0c;保护数据的安全性与隐私至关重要。下面将介绍几种关键的租用香港Windows服务器时应注意的安全防护措施。 1.使用本…

美颜滤镜SDK解决方案,稳定可靠,易于集成

高质量的视觉体验已成为企业吸引用户、提升品牌形象的关键&#xff0c;美摄科技凭借其领先的美颜滤镜SDK技术&#xff0c;为企业提供了从人像美颜到多元场景处理的全方位解决方案&#xff0c;助力企业轻松实现视觉升级。 一、强大能力&#xff0c;覆盖多场景 美摄科技美颜滤镜…

使用JavaScript日历小部件和DHTMLX Gantt的应用场景(二)

DHTMLX Suite UI 组件库允许您更快地构建跨平台、跨浏览器 Web 和移动应用程序。它包括一组丰富的即用式 HTML5 组件&#xff0c;这些组件可以轻松组合到单个应用程序界面中。 DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表&#xff0c;可满足项目管理应用…

微软如何打造数字零售力航母系列科普09 - 什么是Dynamics 365 Customer Insight 以及如何使用它?

什么是Dynamics 365 Customer Insight(客户见解)以及如何使用它? 新的Dynamics 365 Customer Insights平台在Microsoft Inspire 2023上推出&#xff0c;为CX创新者提供了对组合客户数据平台&#xff08;CDP&#xff09;和旅程编排工具的访问。 更新后的解决方案于2023年9月首…

特产销售|基于Springboot+vue的藏区特产销售平台(源码+数据库+文档)​

目录 基于Springbootvue的藏区特产销售平台 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道…