WebGL 2 实战:单文件移植与扩展 Heartfelt 玻璃雨滴折射效果

📅 发布时间:2026/7/7 5:59:41 👁️ 浏览次数:
WebGL 2 实战:单文件移植与扩展 Heartfelt 玻璃雨滴折射效果
一、项目效果与功能概览这个项目的交付物只有一个index.html不依赖 npm、不依赖 Three.js也不需要额外资源文件。页面打开后主视觉是一个全屏 WebGL 2 canvas背景默认是双色渐变流体前景覆盖大量玻璃雨滴、拖尾水痕、雾气和轻微色散折射。项目同时提供了三个交互层右上角折叠控制面板调节雨势、雾气、折射率、水滴尺度、流速和背景混合比例。背景上传支持图片或视频上传后作为 WebGL 纹理被雨滴折射。悬浮文本框玻璃拟态样式可编辑正文可拖拽位置。这类效果很适合作为个人主页首屏、视觉展示页、互动海报、音乐可视化背景或者 WebGL shader 教学案例。“C:\Users\86182\Desktop\rainnyDay\index.html”二、整体架构DOM 负责交互Shader 负责画面虽然是单文件项目但源码结构非常清晰index.html ├── style 页面布局、玻璃拟态 UI、控制面板、悬浮文本框 ├── canvas idglCanvas WebGL 2 全屏渲染目标 ├── 控制面板 DOM sliders / upload / fullscreen / toggle ├── 悬浮文本框 DOM contenteditable pointer drag └── script ├── WebGL 2 初始化 ├── UI 参数读取与事件绑定 ├── 顶点着色器 ├── 片元着色器 ├── 媒体纹理上传 └── requestAnimationFrame 渲染循环这个架构的关键点是分工明确HTML/CSS 只负责用户界面JavaScript 只负责把用户参数和媒体资源送进 GPU真正复杂的雨滴、折射、流体背景都在片元着色器里完成。三、WebGL 2 初始化为什么使用一个全屏三角形源码中获取 WebGL 2 上下文时开启了高性能偏好同时关闭了深度、模板和 alphaconstglcanvas.getContext(webgl2,{antialias:false,alpha:false,depth:false,stencil:false,powerPreference:high-performance});这里的设计非常适合 shader toy 类型的全屏后处理效果。因为画面不是由复杂几何体组成而是每个像素根据uv、时间、鼠标和纹理计算出来所以不需要传统 3D 场景里的深度测试。顶点着色器也很极简const vec2 pos[3] vec2[3]( vec2(-1.0, -1.0), vec2( 3.0, -1.0), vec2(-1.0, 3.0) );它使用一个覆盖全屏的大三角形而不是两个三角形拼成矩形。这样可以避免屏幕中间对角线处的插值边界也减少一次顶点处理。对于全屏 shader这是非常常见也很优雅的写法。四、Uniform 参数把 UI 控制映射到 GPU控制面板中的滑块并不直接修改画面而是通过readParams()转换成统一参数在每一帧传给 shaderconstreadParams()({rain:Number(controls.rainStrength.value),fog:Number(controls.fogDensity.value),refraction:Number(controls.refraction.value),dropScale:Number(controls.dropScale.value),flowSpeed:Number(controls.flowSpeed.value),mediaMix:Number(controls.mediaMix.value)});这些参数最终对应片元着色器里的 uniformsuniform float uRainStrength; uniform float uFogDensity; uniform float uRefraction; uniform float uDropScale; uniform float uFlowSpeed; uniform float uMediaMix; uniform float uHasMedia; uniform sampler2D uMedia;这种设计有两个好处第一UI 和 shader 解耦。滑块叫什么、放在哪里不影响 shader 内部算法。第二实时性很好。每一帧只需要更新少量 uniform不需要重建纹理、重编译 shader 或重新布局 DOM。五、默认背景用 FBM 做双色流体渐变项目的默认背景不是静态 CSS 渐变而是在 shader 中实时生成的流体渐变vec3 fluidGradient(vec2 uv) { vec2 p uv - 0.5; float t uTime * 0.085 * uFlowSpeed; float f1 fbm(p * 2.2 vec2(t, -t * 0.7)); float f2 fbm(p * 4.0 - vec2(t * 1.6, t)); float swirl atan(p.y, p.x) 1.4 * f1 0.9 * sin(2.2 * length(p) - t * 5.0); ... }这里用到了fbm也就是 fractal brownian motion。简单理解它是多层噪声叠加用来生成比普通 noise 更自然的云雾、液体和纹理变化。背景色由几组颜色混合而来vec3 a vec3(0.025, 0.22, 0.31); vec3 b vec3(0.78, 0.12, 0.36); vec3 c vec3(0.95, 0.58, 0.22); vec3 d vec3(0.09, 0.05, 0.18);青蓝、玫红、暖橙和深紫不是平均铺开而是通过噪声、极坐标角度和距离共同调制所以最终画面会有一种缓慢流动的“液态渐变”质感。这比普通 linear-gradient 更适合作为折射背景因为水滴折射需要背景中有足够的色彩和明暗变化才能看出玻璃感。六、核心算法雨滴不是图片而是法线场玻璃雨滴效果的核心不在于“画出雨滴贴图”而是构造一个局部法线扰动场。片元着色器中的rainNormal()函数负责生成每个像素的雨滴法线vec2 rainNormal(vec2 uv) { vec2 aspect vec2(uResolution.x / max(uResolution.y, 1.0), 1.0); vec2 p (uv - 0.5) * aspect; float rain clamp(uRainStrength, 0.0, 1.6); float scale mix(10.0, 18.0, clamp(rain, 0.0, 1.0)) / max(0.45, uDropScale); ... }注意这里先处理了屏幕宽高比。否则在宽屏上圆形雨滴会被拉伸成椭圆。雨滴分两类静态水珠像玻璃上附着的小水滴。下滑拖尾像雨水沿玻璃向下流动形成的水痕。静态水珠由staticDrop()生成。它通过 hash 函数给每个网格单元一个随机中心和半径float staticDrop(vec2 uv, vec2 id, float sizeBias) { vec2 rnd hash22(id); vec2 center (rnd - 0.5) * vec2(0.82, 0.72); vec2 p uv - center; float radius mix(0.035, 0.14, rnd.y) * sizeBias; float d length(p); float core smoothstep(radius, radius * 0.28, d); ... }这里并没有真正存储任何雨滴对象。每个像素根据自己的坐标反推出所在网格和附近网格里的伪随机水滴。这也是 shader 编程非常迷人的地方看似复杂的粒子系统其实可以完全由数学函数实时生成。拖尾水痕由trailDrop()生成float fall fract(uTime * uFlowSpeed * mix(0.06, 0.22, rnd.x) rnd.y); vec2 center vec2(rnd.x - 0.5, 0.62 - fall * 1.55);fract()让水滴下落循环发生uFlowSpeed控制速度rnd让每条水痕速度和位置不同。后面再用head和tail分别构造水滴头部和拖尾float head smoothstep(0.075, 0.0, length(p / vec2(0.72, 1.0))); float tail smoothstep(0.035, 0.0, abs(p.x)) * smoothstep(0.42, 0.02, -p.y) * step(0.0, -p.y);这段是雨滴真实感的关键如果只有圆点会像玻璃上的水珠加入拖尾后画面才有“雨正在流动”的叙事感。七、折射与色散玻璃感的真正来源拿到雨滴法线n后shader 不直接把水滴画成白色而是用它偏移背景采样坐标vec2 n rainNormal(uv); vec2 chroma n * uRefraction; refracted.r sampleBackground(uv chroma * vec2(1.12, 0.88)).r; refracted.g sampleBackground(uv chroma).g; refracted.b sampleBackground(uv chroma * vec2(0.82, 1.2)).b;这就是折射的核心水滴区域看到的不是原本的背景而是稍微偏移后的背景。更进一步代码对 RGB 三个通道使用了略微不同的偏移量形成轻微色散。真实玻璃、水滴、镜头边缘都会出现类似现象。这个处理非常轻但能显著增强“高级感”。最后再加一点高光float highlight pow( clamp(dot(normalize(vec3(n * 1.8, 0.65)), normalize(vec3(-0.4, 0.55, 0.72))), 0.0, 1.0), 5.0 ); col highlight * vec3(0.7, 0.9, 1.0) * 0.55;折射负责形变高光负责材质两者叠加后雨滴才会像透明玻璃而不是普通噪声。八、媒体背景上传图片和视频都变成 WebGL 纹理项目支持用户上传图片或视频替换背景。核心逻辑是监听文件选择document.getElementById(mediaUpload).addEventListener(change,event{constfileevent.target.filesevent.target.files[0];if(!file)return;consturlURL.createObjectURL(file);mediaIsVideofile.type.startsWith(video/);mediaElementmediaIsVideo?document.createElement(video):newImage();});如果是视频则设置循环、静音和playsInlinemediaElement.looptrue;mediaElement.mutedtrue;mediaElement.playsInlinetrue;真正上传到 GPU 的地方在uploadMediaFrame()gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,mediaElement);图片只需要上传一次视频则在每一帧判断readyState后更新纹理。这样雨滴折射的背景就可以是动态视频视觉表现会比静态图片更强。在 shader 中背景混合由sampleBackground()完成vec3 shaderBg fluidGradient(clampedUv); vec3 mediaBg texture(uMedia, clampedUv).rgb; float mixAmount clamp(uMediaMix * uHasMedia, 0.0, 1.0); vec3 bg mix(shaderBg, mediaBg, mixAmount);这个设计保留了默认 shader 背景也允许用户逐渐混合上传媒体而不是一上传就完全替换。九、控制面板与悬浮文本框WebGL 项目也需要产品化交互很多 shader demo 只停留在“能看”但这个项目进一步做了可用性包装。右上角控制面板通过 CSS 固定定位折叠时只隐藏主体.panel{position:fixed;top:14px;right:14px;z-index:10;backdrop-filter:blur(24px)saturate(145%);}.panel.collapsed .panel-body{display:none;}悬浮文本框则使用contenteditable让用户直接编辑文字divclassfloat-contentcontenteditabletruespellcheckfalse雨夜玻璃small可编辑文字 / 拖拽移动 / 背景可替换/small/div拖拽逻辑基于 Pointer Events兼容鼠标和触摸floatHandle.addEventListener(pointerdown,event{constrectfloatingText.getBoundingClientRect();dragState{pointerId:event.pointerId,offsetX:event.clientX-rect.left,offsetY:event.clientY-rect.top};});移动时根据指针坐标更新left/top并限制在视口内避免拖出屏幕constxMath.min(Math.max(event.clientX-dragState.offsetX,8),window.innerWidth-rect.width-8);constyMath.min(Math.max(event.clientY-dragState.offsetY,8),window.innerHeight-rect.height-8);这部分虽然不是 shader 核心但对实际作品非常重要。视觉特效如果不能被用户操作很容易只是一个 demo加入参数面板和文本框后它就更像一个可发布的互动页面。十、渲染循环与性能策略主循环非常直接functionrender(now){resize();uploadMediaFrame();constpreadParams();gl.useProgram(program);...gl.drawArrays(gl.TRIANGLES,0,3);requestAnimationFrame(render);}这里有几个值得注意的性能点使用全屏三角形减少几何复杂度。DPR 限制在 2 以内避免高分屏上像素数量暴涨。UI 控制只更新 uniform不触发 shader 重编译。视频纹理只有在 readyState 满足条件时才上传。背景、雨滴、拖尾都在 shader 中程序化生成不需要大量贴图资源。不过这个 shader 中有多层循环例如rainNormal()会遍历多层雨滴网格。它的画质很好但如果要适配低端移动设备可以考虑减少 layer 数、降低静态雨滴邻域采样或在移动端降低 canvas DPR。十一、踩坑记录中文源码一定要注意 UTF-8这个项目是中文 UI源码里包含“雨势”“雾气”“折射率”等文本。编辑这类单文件 HTML 时务必保证metacharsetutf-8同时编辑器或脚本写文件时也要使用 UTF-8。否则中文可能变成乱码更严重的是如果乱码出现在 HTML 属性或 JS 字符串里还可能破坏标签结构或导致脚本语法错误。一个简单的建议是涉及中文内容的 HTML 文件尽量用现代编辑器直接保存为 UTF-8如果用脚本批量替换显式指定 UTF-8 读写。十二、总结这个项目的核心价值在于它把一个 Shadertoy 风格的视觉算法包装成了一个真正可交互、可复用、可发布的 Web 页面。从技术角度看它涵盖了 WebGL 2 全屏渲染、GLSL 噪声函数、程序化雨滴法线、折射采样、色散、高光、视频纹理上传和 DOM 交互同步。对于想学习 shader 的前端开发者来说这是一个很好的综合练习。如果继续扩展还可以加入预设主题切换比如霓虹城市、冷色雨夜、暖色室内灯光。截图导出或录制短视频。文本框字体、颜色、阴影可调。移动端自动降采样策略。多个 shader pass实现更真实的玻璃模糊与水痕累积。WebGL 的魅力就在这里只要掌握坐标、噪声、采样和时间你就可以在一个普通网页里造出很有电影感的实时视觉效果。