尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity雨滴窗户效果:RenderTexture与Shader实现动态雨痕渲染

Unity雨滴窗户效果:RenderTexture与Shader实现动态雨痕渲染 1. 项目概述为什么要在Unity里模拟“雨打窗户”在游戏和交互式体验的开发中环境氛围的营造往往比我们想象的更重要。一个阴雨连绵的场景如果只是简单地播放雨声和切换天空盒玩家或用户很难真正“沉浸”进去。而“雨下在窗户上”这个效果恰恰是连接室内安全感和室外恶劣天气的关键视觉纽带。它不仅仅是几道滑落的水痕更是传递情绪、暗示环境、甚至推动叙事的强大工具。想象一下在恐怖游戏里你躲在一个安全屋窗外暴雨如注雨水在玻璃上扭曲了外面怪物的身影或者在一个温馨的叙事游戏中主角坐在咖啡馆里看着雨水在窗上汇聚、流淌思绪也随之飘远。这种效果能瞬间将玩家拉入场景建立强烈的情感连接。从技术角度看在Unity3D中实现这个效果是一个综合运用渲染管线、着色器编程和粒子系统的经典案例。它不像实现一个复杂的角色动画那样需要庞大的骨骼系统也不像构建开放世界那样需要复杂的地形和流式加载。它更像是一个精致的“小魔术”考验的是开发者对光影、材质和物理现象细微之处的理解与模拟能力。你需要考虑雨滴的形态、流动的轨迹、与玻璃的交互折射、反射、附着以及如何高效地管理成千上万个动态的雨滴实例。这背后涉及的核心技术点包括但不限于Shader Graph或HLSL/CG着色器编写、粒子系统的进阶控制、渲染纹理Render Texture的运用以及对后期处理Post-Processing效果的巧妙结合。对于Unity开发者无论是刚入门的新手想挑战一个有趣的视觉效果还是资深TA技术美术优化场景性能这个项目都是一个绝佳的练手场。它能带你深入理解Unity的渲染流程亲手操控GPU去绘制那些令人信服的细节。接下来我将以一个实际可操作的项目为蓝本拆解从思路到实现的完整过程并分享那些只有踩过坑才知道的“实战经验”。2. 核心效果拆解与实现思路要实现“雨下在窗户上”的效果我们不能把它看作一个单一的特效而应该分解为几个相互关联又相对独立的视觉层。理解这些层次是选择正确技术方案的前提。2.1 视觉元素分解首先我们观察真实世界中雨水打在玻璃上的现象撞击与飞溅雨滴高速撞击玻璃瞬间会形成一个微小的圆形湿润区域并可能伴有细微的飞溅。附着与流动雨滴附着在玻璃上后在重力和风力的作用下开始向下流动。流动的轨迹不是简单的直线会受到玻璃表面细微不平整、灰尘或已有水痕的影响形成蜿蜒曲折的路径。汇聚与分流多条水流路径可能会相遇、合并成更粗的水流也可能遇到障碍如窗框而分流。形态与光学变化水层会改变玻璃的光学属性。它会让透过玻璃看到的室外场景发生折射扭曲同时也会增强玻璃表面的镜面反射特别是夜晚室内有光源时。水珠本身作为一个凸透镜也会轻微放大和扭曲其背后的景象。动态消失水流到玻璃底部或水珠蒸发痕迹会逐渐变淡、消失。2.2 技术方案选型针对以上元素在Unity中我们有几种主流实现方案各有优劣方案一粒子系统 屏幕空间效果适合移动端/性能敏感项目思路使用GPU粒子模拟雨滴撞击和初始水渍。水流轨迹则通过一个简单的屏幕空间UV偏移来实现比如采样一张水流法线贴图动态移动其UV来模拟流动感。折射效果使用一个简单的全屏扭曲Distortion后期效果。优点性能开销相对较低实现快速适合手机游戏或VR应用。缺点效果较“平”缺乏立体感和真实的物理交互水流轨迹固定重复感强。方案二渲染到纹理Render Texture 自定义着色器平衡效果与性能思路这是本次重点讲解的方案。我们创建一个透明的“窗户玻璃”平面为其赋予一个自定义着色器。着色器的核心是使用一张或多张渲染纹理Render Texture作为“画布”。粒子系统或代码生成的雨滴位置信息被实时“绘制”到这张RT上模拟水渍的积累。着色器再根据RT的信息动态计算水流、折射和反射。优点效果真实可动态交互如雨滴击中位置由物理系统决定性能可控是PC和主机游戏的常用方案。缺点需要编写或配置着色器对初学者有一定门槛。方案三流体模拟效果顶级性能要求高思路使用Unity的Visual Effect GraphVFX Graph或第三方流体插件如Obi Fluid进行简化的2D流体模拟。将窗户玻璃视为一个2D模拟平面雨滴作为发射源模拟真实的水流汇聚、分流。优点物理效果最真实动态变化丰富。缺点性能开销巨大通常只用于电影级过场或非实时渲染不适合主流游戏实时运行。我们的选择为了在效果、性能和可学习性之间取得最佳平衡我们将采用方案二作为核心实现路径。这个方案能让我们深入理解Unity渲染的核心机制并且产出的效果足以满足绝大多数项目的需求。3. 核心模块实现详解确定了方案二我们开始搭建核心模块。整个系统可以划分为三个部分动态纹理生成器模拟画布、窗户着色器视觉效果呈现、以及雨滴发射与交互系统。3.1 模块一创建动态雨痕画布Render Texture这个模块是整个效果的数据核心。我们需要一张“白板”来记录雨滴打在哪里、水流动到了哪里。步骤1创建与配置Render Texture在Project窗口中右键 - Create - Render Texture命名为“RainDropRT”。关键参数设置如下Size: 建议512x512或1024x1024。分辨率越高水痕细节越精细但带宽消耗也越大。对于大多数情况512已足够因为最终效果会经过模糊处理。Depth Buffer: 不需要选择“No depth buffer”。Anti-aliasing: 关闭或选择较低级别。因为我们的绘制内容是程序化的噪点和线条抗锯齿意义不大反而可能带来性能开销。Format: 默认的ARGB32即可。如果考虑HDR可以使用R16G16B16A16 SFloat但普通情况不需要。步骤2创建绘制相机与材质新建一个空GameObject命名为“RainCanvasCamera”为其添加一个Camera组件。将此相机的Target Texture设置为刚才创建的“RainDropRT”。这意味着这个相机渲染的内容不会显示在屏幕上而是直接输出到这张RT上。调整相机参数Clear Flags: Solid Color。我们将用纯黑色0,0,0,0作为透明背景。Background: RGBA(0,0,0,0)即完全透明黑色。Culling Mask: 设置为一个自定义的Layer例如“RainCanvas”。我们只希望这个相机渲染用于绘制雨痕的物体。Projection: Orthographic正交。因为我们的“画布”是一个2D平面。调整相机的Size和Position使其正好完整覆盖一个单位平面1x1。通常将相机放在(0,0,-1)看向(0,0,0)Orthographic Size设为0.5。创建一个新的Unlit Shader和Material命名为“RainDraw”。这个材质将用于绘制雨滴和水流。其着色器非常简单核心就是输出一个颜色。我们可以用一个简单的圆形通过计算片元到中心点的距离来模拟雨滴撞击的初始印记。步骤3使用粒子系统进行“绘制”这是将动态数据写入RT的关键。我们不直接渲染3D模型而是使用粒子系统作为“画笔”。创建一个新的Particle System GameObject作为“RainCanvasCamera”的子物体。将其Layer设置为“RainCanvas”。配置粒子系统关键参数Duration: 1单次喷射。Looping: 关闭。Start Lifetime: 0.1非常短粒子一出现就消失只留下一帧的“印记”。Start Speed: 0。Start Size: 根据需求例如0.05代表雨滴大小。Max Particles: 根据雨势调整例如100。Emission: 设置一个合适的Rate over Time如50。Renderer:Render Mode: Mesh实际上我们用Quad。Material: 使用上一步创建的“RainDraw”材质。取消勾选Cast Shadows和Receive Shadows。现在当粒子发射时“RainCanvasCamera”就会将每个粒子一个带有“RainDraw”材质的小方块渲染到“RainDropRT”上。由于相机背景是透明的RT上就会留下白色的圆形印记假设“RainDraw”材质输出白色。这些印记就是我们的“雨滴击中点”。注意这里有一个非常重要的技巧。粒子默认是3D空间中的物体其位置会受透视影响。为了确保粒子被准确地绘制在RT的对应UV位置我们需要确保粒子发射器的局部位置与窗户模型的UV空间对齐。一个常见的做法是将粒子发射器放置在窗户平面世界坐标上然后通过脚本将粒子的世界坐标转换到“RainCanvasCamera”的视口坐标范围0到1再通过一个专门的“绘制器”物体一个始终覆盖RT全屏的Quad的材质属性传递过去。但更简单的方法是直接让“RainCanvasCamera”的投影矩阵与窗户平面匹配然后粒子就在这个相机空间内发射。这需要对粒子发射器的Shape模块进行精细设置通常选择“Box”形状并使其大小与窗户平面一致。3.2 模块二编写窗户玻璃着色器这是视觉效果产生的核心。我们将创建一个Surface Shader或Unlit Shader它负责采样“RainDropRT”并据此计算最终的颜色。着色器核心逻辑采样雨痕纹理在片元着色器中对“RainDropRT”进行采样。采样得到的值假设是灰度图白色代表有水黑色代表无水代表了当前像素位置的水量或水痕强度。我们称这个值为waterMask。模拟水流静态的水渍不够真实。我们需要让水“流”起来。一个简单有效的方法是使用时间变量和噪声纹理。声明一个_FlowSpeed和_FlowDirection一个2D向量属性。声明一张_FlowNoise纹理通常是一张蓝噪声或Perlin噪声图。在着色器中计算一个流动的UV偏移float2 flowUV i.uv _Time.y * _FlowSpeed * _FlowDirection。用这个流动的UV去采样_FlowNoise得到一个噪声值flowNoise。将flowNoise与waterMask结合例如waterMask * (1.0 flowNoise * 0.5)让水痕的强度产生随机变化模拟水流不均匀。更高级的做法是使用两张噪声图以不同的速度和方向采样并混合创造出更复杂的流动效果。实现折射效果水会扭曲背后的景象。首先我们需要获取玻璃背后的场景颜色。在Built-in渲染管线中可以使用GrabPass来捕获屏幕内容。在URP/HDRP中通常使用_CameraOpaqueTexture不透明纹理或通过Renderer Features设置。然后根据waterMask的强度对采样屏幕纹理的UV进行偏移。偏移的方向和大小可以基于waterMask和噪声图计算。// 简化示例 (URP Shader Graph思路) float2 distortionUV i.screenPos.xy (waterMask * _DistortionStrength * flowNoise.xy); float4 sceneColor SampleSceneTexture(distortionUV);实现湿润感与镜面反射玻璃沾水后会变得更光滑镜面反射更强烈。在Surface Shader中我们可以通过修改Smoothness和Metallic如果是非金属则主要影响Smoothness来模拟。Smoothness lerp(_BaseSmoothness, 1.0, waterMask)。对于更自定义的控制可以单独计算一个高光项并让高光强度随waterMask增强。混合最终颜色将折射后的场景颜色、水本身的颜色如果有比如浑浊的雨水、以及高光反射颜色进行混合。float3 finalColor sceneColor.rgb; // 添加水渍颜色例如淡淡的青色 finalColor lerp(finalColor, _WaterTint.rgb, waterMask * _WaterTint.a); // 添加高光 finalColor specularHighlight * waterMask; return float4(finalColor, 1.0);Shader Graph实现要点如果你使用Shader Graph流程会非常直观创建Texture2D属性引用“RainDropRT”。使用Time节点和Vector2属性驱动Tiling And Offset节点对雨痕纹理进行滚动模拟水流。使用Sample Texture 2D节点采样雨痕纹理和噪声纹理。用Multiply、Add、Lerp等节点混合它们得到最终的waterMask。使用Scene Color节点获取背景。使用Normal From Texture节点从噪声图生成法线或者直接用waterMask驱动一个简单的法线扰动。将扰动后的法线输入到Scene Color的UV端口实现折射。将waterMask连接到主材质的Smoothness和Opacity如果需要上。3.3 模块三雨滴发射与物理交互为了让效果更动态我们需要让雨滴的发射与游戏世界互动。基础发射使用一个粒子系统作为雨滴发射源放置在窗户上方或摄像机前方。这个粒子系统就是我们在场景中看到的“雨”。它的渲染就是普通的透明粒子模拟下落的雨丝。交互增强基于碰撞的雨痕生成让上述“雨”粒子系统与窗户平面带碰撞体发生碰撞。在粒子系统的Collision模块中设置碰撞层为窗户所在层。当粒子碰撞时我们可以触发一个事件。事件传递在粒子系统的Collision模块中启用Send Collision Messages。然后在窗户对象上挂载一个C#脚本实现OnParticleCollision(GameObject other)方法。坐标转换与绘制在OnParticleCollision方法中我们可以获取到碰撞点的世界坐标。我们需要将这个世界坐标转换到“RainCanvasCamera”的视口坐标Viewport Space范围0到1。void OnParticleCollision(GameObject other) { ParticleSystem particleSystem other.GetComponentParticleSystem(); ListParticleCollisionEvent collisionEvents new ListParticleCollisionEvent(); int numEvents particleSystem.GetCollisionEvents(gameObject, collisionEvents); foreach (var collisionEvent in collisionEvents) { Vector3 worldPos collisionEvent.intersection; // 将世界坐标转换到RainCanvasCamera的视口坐标 Vector3 viewportPos rainCanvasCamera.WorldToViewportPoint(worldPos); // 确保点在视口内 (0~1) if (viewportPos.x 0 viewportPos.x 1 viewportPos.y 0 viewportPos.y 1) { // 将这个坐标(viewportPos.x, viewportPos.y)传递给绘制系统 // 例如实例化一个临时的“绘制用粒子”在RainCanvasCamera下位置由viewportPos决定 DrawRainDropAt(viewportPos); } } }绘制系统对接DrawRainDropAt函数负责在“RainCanvasCamera”下在对应的UV位置生成一个“RainDraw”材质的粒子或直接通过CommandBuffer绘制一个点。这样物理碰撞点就实时地反映到了我们的动态雨痕纹理上。4. 性能优化与进阶技巧一个好看的效果如果消耗巨大是无法应用到实际项目中的。以下是针对此效果的优化和进阶思路。4.1 性能优化策略降低Render Texture分辨率这是最直接的优化。从1024降到512甚至256性能提升明显。可以通过在着色器中对RT采样后进行小幅度的模糊来弥补精度损失。控制绘制频率不是每一帧都需要往RT上绘制。可以设置一个计时器每0.1秒或0.05秒批量处理一次累积的碰撞点一次性绘制多个雨滴减少Draw Call。简化粒子系统用于在RT上绘制的粒子系统尽可能使用最简单的Shader如Unlit/Color关闭所有不需要的模块如Size over Lifetime, Color over Lifetime将Max Particles控制在必要的最低水平。使用LOD细节层次根据摄像机与窗户的距离动态调整效果精度。例如近距离启用完整的RT绘制、复杂水流模拟、折射。中距离降低RT分辨率简化水流计算。远距离完全关闭RT绘制和折射只保留一个简单的、贴图滚动的湿润感材质。利用Shader的Alpha混合与丢弃在“RainDraw”着色器中雨滴印记的边缘应该是柔和的。确保使用正确的Blend模式如Blend SrcAlpha OneMinusSrcAlpha。同时对于Alpha值极低的像素可以使用clip()函数直接丢弃避免不必要的过度绘制。4.2 效果增强技巧多层纹理混合不要只使用一张RT。可以使用两张RT一张用于当前帧的雨滴撞击高频、清晰另一张用于累积的水流和干燥过程低频、模糊。两相结合效果更有层次感。模拟水珠Condensation除了流动的水玻璃上还会有凝结的水珠。可以在着色器中用噪声函数生成一些随机分布的圆形区域赋予它们更强的折射和凸面镜效果通过不同的法线扰动实现。风的影响将风的方向和强度作为一个Shader属性_Wind。在计算水流方向时不仅考虑重力向下还要加上风力的影响让水流斜着流动。温度与干燥模拟引入一个“干燥速度”参数。在着色器中每一帧让waterMask的值随时间逐渐衰减乘以一个小于1的系数。这样雨停后水痕会慢慢消失。与后处理结合将窗户玻璃的折射效果与全局屏幕空间反射SSR或屏幕空间环境光遮蔽SSAO结合可以让湿润的玻璃反射出室内的模糊倒影真实感大幅提升。5. 常见问题与调试实录在实际操作中你几乎一定会遇到下面这些问题。这里是我踩过坑后的解决方案。5.1 雨痕纹理不显示或显示错误问题描述窗户材质上什么都看不到或者只看到一片纯色。排查步骤检查RT赋值确保“RainDropRT”已经正确赋值给窗户材质的对应纹理属性例如_RainTex。检查绘制相机选中“RainCanvasCamera”在Scene视图的右上角将Display下拉菜单从Shaded切换到Overdraw模式如果可用或者直接查看其预览窗口Camera组件上有预览小图确认粒子是否被正确渲染到了RT上。检查着色器采样在窗户着色器中尝试先将采样的RT颜色直接输出看看是否是预期内容。如果输出是黑色可能是UV不对。检查窗户模型的UV是否展开正确着色器中的UV采样节点是否连接对。检查图层Layer确保绘制粒子的Layer在“RainCanvasCamera”的Culling Mask中并且窗户模型不在这个Layer中以免被意外绘制到RT上。5.2 折射效果边缘闪烁或扭曲过度问题描述折射的边缘出现锯齿状闪烁或者整个背景扭曲得像哈哈镜。解决方案钳制UV偏移在着色器中对用于采样屏幕纹理的扰动后UV进行钳制确保其不超出合理范围通常是0到1。可以使用clamp函数或saturate函数。使用Mipmap确保用于扭曲的噪声纹理启用了Mipmap并且采样时使用tex2Dlod或指定LOD级别避免在远处出现锯齿。控制扭曲强度_DistortionStrength参数不宜过大。通常0.01到0.05就能产生肉眼可见且舒适的扭曲。过大的值会导致采样到完全无关的像素产生混乱。模糊雨痕纹理在着色器中采样雨痕RT后先对其进行一次轻微的高斯模糊或均值模糊。平滑的waterMask会产生平滑的扭曲过渡避免生硬的边界导致闪烁。这可以通过采样RT多次每次UV稍有偏移然后取平均来实现性能换质量。5.3 性能开销过大问题描述游戏帧率FPS明显下降尤其是在移动设备上。优化检查清单Profile分析使用Unity的ProfilerWindow - Analysis - Profiler重点查看Rendering和Scripts部分。确认是GPU负担重RT尺寸大、着色器复杂还是CPU负担重粒子碰撞检测、频繁的脚本调用。降低RT分辨率这是立竿见影的方法。尝试从1024降到512。减少绘制调用检查“RainCanvasCamera”渲染的物体数量。确保只有必要的绘制粒子。考虑使用Graphics.DrawProcedural或CommandBuffer进行更高效的批量绘制替代粒子系统。简化着色器复杂度减少着色器中的复杂计算特别是循环和分支。将一些计算从片元着色器移到顶点着色器。减少纹理采样次数。限制生效范围通过脚本控制只有当摄像机看向窗户或窗户在摄像机视野内时才启用“RainCanvasCamera”的渲染和粒子碰撞检测。5.4 水流效果不自然像贴图滚动问题描述水流看起来是整体平移的重复图案缺乏真实感。解决方案使用高质量噪声图避免使用简单的灰度渐变图。使用Tileable的Perlin噪声或Gabor噪声图。可以混合两张不同尺度和速度的噪声图。引入流向图除了用时间和方向向量控制UV偏移还可以预计算一张流向图Flow Map。流向图存储了每个像素的流动方向编码在RG通道。在着色器中采样流向图用其向量值作为偏移方向这样水流就能沿着预设的复杂路径运动模拟玻璃表面不平整的影响。模拟重力与阻力在着色器中让水流在垂直方向重力方向的流速比水平方向快。同时可以引入一个“粘滞度”参数让水流速度随着水量的减少waterMask值变小而变慢模拟水将流尽时的效果。实现“雨下在窗户上”的效果是一个从观察到分解再从技术重组到艺术调优的完整过程。它没有唯一的正确答案我的这套基于Render Texture和自定义着色器的方案提供了一个在效果、性能和灵活性上都很不错的起点。最关键的是理解其原理用一张动态的纹理作为状态存储用着色器将这种状态解读为视觉现象。掌握了这个核心思路你不仅可以模拟雨水还可以举一反三去实现积雪融化、血迹流淌、墙面受潮等任何需要动态痕迹的效果。在实际项目中多观察多调试从简单的版本开始逐步添加细节你会发现这些看似复杂的视觉奇观其实都是由一个个可理解、可控制的技术模块搭建而成的。
返回列表