用shader实现流动的水面(webgl)
这段时间一直在看如何用shader绘制一个流动的水面,直接用贴图(高度图、法向贴图)实现的方法,这里就不讨论了。
搜了一大波博客资料,感觉存在如下一些问题:
1⃣️大多数资料都是基于opengl实现(或者是unity里的shader),过多关注点在渲染上面而不是水波的mesh实现上,让人没有看下去的欲望
2⃣️有的就直接是照搬别人的博客,公式大段大段地搬,却没有自己的一丝见解,太过敷衍
3⃣️代码不加注释,对前来学习者不太友好
4⃣️针对webgl的实现,网上的资料太少(虽然已经有了opengl的实现)
所以在查阅了资料之后,决定写一个webgl版本的实现(three.js + shader)
nvidia官方提供的水波实现方程(其实网上大多数博客里的方程式应该都是源于此处):传送门。
对应的,知乎有一篇文章,基本上就是上面网站的中文版,但是作者加入了一点自己思考后的想法,个人觉得很好,推荐一下:GPU Gems 基于物理模型的水面模拟。
-------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------
一、PlaneGeometry + ShaderMaterial + 正弦波方程式
//1.PlaneGeometry this.seaMaterial = new THREE.ShaderMaterial({ uniforms: { time:{type:'f',value:0}, }, vertexShader: seashader.vs, fragmentShader: seashader.fs, side:THREE.DoubleSide, wireframe: true }); this.geometry = new THREE.PlaneGeometry( 500,500,100,20); var plane = new THREE.Mesh( this.geometry, this.seaMaterial ); plane.rotation.x= -Math.PI/2; this.scene.add( plane );
const seashader = { vs:` uniform float time; void main(){ float x = position.x; float y = position.y; float PI = 3.141592653589; float sz = 0.0; float ti = 0.06; float index = 1.0; vec2 dir;//波的方向 //四条正弦波相加 for(int i = 0;i<4;i++){ ti = ti + 0.0005; index = index + 0.1; if(mod(index,2.0)==0.0){ dir = vec2(1.0,ti); }else{ dir = vec2(-1.0,ti); } float l1 = 2.0 * PI / (0.5);//波长 float s1 = 10.0 * 2.0 / l1;//速度 float z1 = 1.0 * sin(dot(normalize(dir),vec2(x,y)) * l1 + time * s1);//正弦波方程式 sz +=z1; } gl_Position = projectionMatrix * modelViewMatrix * vec4(x,y,sin(sz) * 10.0,1.0); } `, fs:` void main(){ gl_FragColor = vec4(90./255.,160./255.,248./255.,1.0); } `, }
//animation if(this.seaMaterial){ this.seaMaterial.uniforms.time.value += 0.01; }
参考的水波方程式:
效果如下:
温馨提示:
调参很重要,水波方向、波长、波的叠加数量,如果取值不当,生成的波将会很诡异(和plane尺寸以及分割的分数有关,因为这些参数将会直接影响vs接收的顶点坐标)。
可以看到上述代码vs中顶点z:sin(sz) * 10.0,在测试中,我发现对sz进行sin处理,得到的水波细节会更多一点,就是凹凸的感觉会多一点。
正弦波具有圆润的外观-这可能正是我们想要一个平静的田园池塘所需要的。
二、Gerstner波方程式
接下来只贴shader部分
const gerstnershader = { vs:` uniform float time; void main(){ float x = position.x; float y = position.y; float PI = 3.141592653589; float sx = 0.0; float sy = 0.0; float sz = 0.0; float ti = 0.0; float index = 1.0; vec2 dir;//水波方向 for(int i = 0;i<3;i++){ ti = ti + 0.0005; index +=1.0; if(mod(index,2.0)==0.0){ dir = vec2(1.0,ti); }else{ dir = vec2(-1.0,ti); } float l1 = 2.0 * PI / (0.5 + ti);//波长 float s1 = 20.0 * 2.0 / l1;//速度 float x1 = 1.0 * dir.x * sin(dot(normalize(dir),vec2(x,y)) * l1 + time * s1); float y1 = 1.0 * dir.y * sin(dot(normalize(dir),vec2(x,y)) * l1 + time * s1); float z1 = 1.0 * sin(dot(normalize(dir),vec2(x,y)) * l1 + time * s1); sx +=x1; sy +=y1; sz +=z1; } sx = x + sx; sy = y + sy; gl_Position = projectionMatrix * modelViewMatrix * vec4(sx,sy,sin(sz) * 10.0,1.0); } `, fs:` void main(){ gl_FragColor = vec4(90./255.,160./255.,248./255.,1.0); } `, }
参考的水波方程式:
gerstner波相较正弦波的特点是:波峰陡峭、波谷平坦。
效果如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构