尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Unity Shader水面效果实战:从零构建动态波浪与反射
做水面效果在Unity里是个非常经典的需求。很多人一上来就想着去Asset Store拖一个现成的插件但真到了要调风格、适配项目、优化性能的时候就会发现还是得自己懂点Shader原理才行。这篇博文我会从一个最简单的方案聊起一步一步拆解如何用Shader实现一个“能看、能动、能跑”的基础水面效果。整个过程不依赖任何第三方资源只用Unity内置能力。这个内容适合刚接触Shader、想搞懂水面效果背后原理的Unity开发者也适合那些被网上各种复杂Gerstner波、FFT海洋方案劝退、想先找个轻量方案救急的人。1. 内容整体设计与思路拆解1.1 一个“简单水面”应该包含什么做水面Shader之前先把需求拆清楚。一个能让人一眼看过去觉得“这是水面”的效果至少包含下面几个核心要素波浪起伏水面必须是动态的顶点要随着时间产生位移这是水面的“骨架”。光照响应水面受到光照后要有高光和暗部变化这里依赖法线方向。顶点动了之后法线也要跟着变否则光照就是错的。半透明质感水面不是完全不透明的物体多多少少能看到水下的东西所以要有透明度控制。反射/假反射水面会倒映周围的景物但实时反射非常贵简单水面一般用Cubemap做假反射或者用Unity的反射探针。波光闪烁远处水面那种细碎的高光点是水面“灵气”的来源少了它画面会显得很死。把这些列清楚之后你会发现水面Shader其实就是一个“顶点动画 法线扰动 半透明光照”的组合问题。逐个击破就行。1.2 技术选型表面着色器Surface Shader还是顶点/片元着色器Vertex/FragmentUnity里写Shader主要有三条路表面着色器Surface Shader、顶点/片元着色器Vertex/Fragment、以及Unity 2021之后主推的Shader Graph。对于这个简单水面项目我选择了表面着色器。原因很简单表面着色器是Unity对顶点/片元着色器的一层包装它把光照计算部分自动生成了你只需要关心“这个表面的颜色是什么、法线是什么、光滑度是多少”。这对于水面这种需要复杂光照交互的材质来说能省掉大量样板代码。你不需要自己手写Blinn-Phong不需要手写阴影采样这些Unity都帮你做了。那什么时候用顶点/片元着色器当你需要极致性能、或者需要非PBR风格、或者目标平台是移动端且你对指令数有严格限制时再考虑手写。对于初学入门表面着色器绝对是性价比最高的选择。注意本方案基于Unity内置渲染管线Built-in Render Pipeline。如果你用的是URPSurface Shader依然兼容但URP里更推荐用Shader Graph或手写Lit Shader。后面的代码我会标注兼容性注意事项。1.3 方案优势与可能的坑这个方案的优点很突出代码量少。一个完整的可运行Shader核心代码不到100行。实时性足够。顶点动画只算了几次正弦函数片元里也只多了一次法线采样性能开销非常低。可扩展性强。后面想加波浪、加深浅、加泡沫都可以在这个框架上直接叠。坑也有。最大的坑是“为什么我照着写了效果却不对劲”。大多数学过Shader的人都在这里栽过跟头所以后面我会专门用一节的篇幅来讲排查思路。简单说水面Shader出了诡异效果90%不是代码写错了而是Queue、Cull、ZWrite这些渲染状态没配对。2. 环境准备与Shader框架搭建2.1 工程配置建议在动手写代码之前先把Unity工程准备好。这里我再说下基本要求Unity版本推荐2019.4 LTS及以上。这个方案我在2018、2019、2020、2021几个大版本都验证过老版本也能跑但Inspector面板的呈现会有差异。如果要真实看到水面反射天空和周围物体场景里需要添加一个Reflection Probe反射探针。位置放在水面中心附近Type设为Baked静态场景或Realtime有动态物体时用。Surface Shader里如果我们用了标准光照模型它天然支持反射探针。Render Pipeline用的是内置管线Project Settings的Graphics Settings里确认当前管线是Built-in。如果你已经建了URP工程要么改回内置管线要么新建一个内置管线的工程来练手。2.2 创建Shader和材质的基本流程先在Project窗口里右键 - Create - Shader - Standard Surface Shader得到一个Surface Shader模板文件。然后右键 - Create - Material把Shader拖到材质上或者直接在材质的Inspector里点Shader下拉框选择。我给这个材质起名叫Water_SimpleShader起名叫Water/SimpleSurface。建议单独建一个文件夹存放Shader相关资源比如Assets/Shaders/Water/。这个习惯很重要项目一大Shaders文件夹里几十个文件如果不分组找起来就想骂人。2.3 认识表面着色器的基本骨架先看一下Surface Shader的骨架它长这样Shader Water/SimpleSurface { Properties { _MainTex (Albedo (RGB), 2D) white {} _Glossiness (Smoothness, Range(0,1)) 0.5 _Metallic (Metallic, Range(0,1)) 0.0 } SubShader { Tags { RenderTypeOpaque } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c tex2D (_MainTex, IN.uv_MainTex); o.Albedo c.rgb; o.Metallic _Metallic; o.Smoothness _Glossiness; o.Alpha c.a; } ENDCG } FallBack Diffuse }这段代码里surf函数就是片元着色器的入口它接收一个Input结构体输出一个SurfaceOutputStandard结构体。Unity会基于你在#pragma surface里指定的光照模型自动生成完整的光照计算代码。#pragma target 3.0这个指定了着色器模型的最低版本。3.0及以上的目标支持更多指令数和更复杂操作对于水面这种带顶点动画的效果建议至少3.0。我们的水面Shader就是在这个骨架之上改的。接下来一步步来。3. 核心实现一波浪顶点动画的数学与代码3.1 用正弦波叠加模拟水面起伏波浪的数学基础其实很简单正弦函数。水面任意一点的高度可以表示为height(x, z, t) A * sin(dot(dir, float2(x, z)) * frequency time * speed) ...其中A是振幅dir是波浪传播方向一个二维单位向量frequency是空间频率speed是时间速度。为什么要用多个正弦波叠加因为真实的自然波浪不是单一频率的它是很多不同方向、不同波长的波叠加在一起的结果。两个正弦合成出一个更自然的波形三个就有点意思了五个就足以以假乱真。对于简单水面用三到四个正弦波就够了。太多的话一是增加了计算量二是参数不好调画面容易乱。3.2 顶点位移的代码实现在Surface Shader里有一个神奇的入口叫vert函数。用#pragma surface surf Standard fullforwardshadows vertex:vert就能把顶点函数挂接进去。顶点函数会在顶点阶段被执行我们可以在这里修改顶点的位置和法线。来看代码#pragma surface surf Standard vertex:vert alpha // 波浪参数 float _WaveHeight; float _WaveFrequency; float _WaveSpeed; void vert(inout appdata_full v) { float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float t _Time.y * _WaveSpeed; float wave1 sin(worldPos.x * _WaveFrequency t) * _WaveHeight; float wave2 cos(worldPos.z * _WaveFrequency * 1.2 t * 1.3) * _WaveHeight * 0.7; float wave3 sin((worldPos.x worldPos.z) * _WaveFrequency * 0.8 t * 0.6) * _WaveHeight * 0.4; v.vertex.y wave1 wave2 wave3; }这段代码干了三件事把物体顶点坐标转到世界坐标为了波浪方向和物体旋转无关、计算三个方向/频率不同的正弦波、把结果累加到顶点Y轴上。wave2用cos是因为cos和sin只是相位差90度两个不同步的波形叠加起来会更有随机感。wave3的方向是(1,1)方向也就是沿着场景对角线的方向用来打破x轴和z轴的方向感。注意appdata_full是Unity内置的顶点数据结构包含了顶点坐标、法线、切线、UV、颜色等所有常见数据。只要你需要修改顶点位置就用它不要用精简版的appdata_base。3.3 法线的必要修正顶点位置变了法线呢如果不管法线水面会变成什么样子它会凹凸起伏但光照不会跟着变。就相当于你在黑夜里看海浪——只有轮廓没有层次。更糟的是原来的法线是朝上的(0,1,0)但在斜坡上这张“水面”的反射还是按平面算的看起来就是一张弯曲的塑料布。简单而有效的做法是在顶点函数里对法线做近似的旋转修正。怎么修正数学上精确的做法是计算位移函数的偏导数构造雅可比矩阵来变换法线。但对于一个简单水面有个更轻量的近似方案根据波浪的斜率来倾斜法线。最直观的方法就是采样相邻位置的高度差来近似float3 neighborPos worldPos; neighborPos.x 0.1f; float waveNeighbor sin(neighborPos.x * _WaveFrequency _Time.y * _WaveSpeed) * _WaveHeight; float slopeX ((waveNeighbor wave2 wave3) - (wave1 wave2 wave3)) / 0.1f;然后再对Z方向做同样的操作构造出扰动后的法线。这个做法的缺点是计算量翻倍但思路直观。另一个更简洁的方案也是很多实用水面Shader的做法——直接算偏导。如果波形是wave(x, z) A * sin(k * x t)那么它对x的偏导就是A * k * cos(k * x t)。沿着这个思路可以精确算出法线。但说实话对这种简单水面更容易被接受的一个方案是让片元里的法线贴图来“扛”光照细节顶点阶段只用o.Normal做一个粗略修正就够了。我的做法是给顶点法线混入少量波形斜率然后用TBN矩阵把法线贴图的采样结果转到世界空间这样水面既有大形变的阴影变化又有细节的波光粼粼。让我先把这个“够用”的版本讲透后面会更细讲法线贴图这个步骤。3.4 参数面板设计与调参建议Shader的Properties是给材质调参用的。我把波浪参数全部暴露出来Properties { _Color (Water Color, Color) (0.2, 0.5, 0.7, 0.8) _WaveHeight (Wave Height, Range(0, 1)) 0.1 _WaveFrequency (Wave Frequency, Range(0, 5)) 1.0 _WaveSpeed (Wave Speed, Range(0, 3)) 0.5 _Glossiness (Smoothness, Range(0, 1)) 0.8 _Metallic (Metallic, Range(0, 1)) 0.0 }初次调参建议先把_WaveHeight调到0.05_WaveFrequency调到1.0_WaveSpeed调到0.5这个组合在默认的Unity单位尺度下效果比较自然。如果你的场景比例不一样比如说船模型很大水面单位比较大那你需要同步调大_WaveHeight和调小_WaveFrequency因为大波浪对应低频高振幅。4. 核心实现二法线纹理与光照响应的模拟4.1 为什么需要法线贴图波浪的大形变给了水面“地形”感但真实水面还有大量的细小涟漪。如果这些细节全用顶点来表达你需要极其密集的网格性能完全不可接受。法线贴图就是来解决这个问题的它在不增加顶点的情况下通过修改每个像素的法线方向让光照产生细腻变化。做水面的时候法线贴图一般不需要自己去画。Unity内置资源包Standard Assets里面有一张水的法线贴图网上也有很多免费的水波法线贴图。如果没有现成的也可以自己用噪声工具生成一张法线图。重点是水面是用“两张不同的法线贴图”来采样的两张图的UV各自以不同的速度滚动然后混合水面才会有波光流动的错觉。4.2 两张法线贴图滚动与混合的原理原理非常简单就是让UV随着时间变化float2 uv1 IN.worldPos.xz * _NormalTiling _Time.y * _NormalSpeed; float2 uv2 IN.worldPos.xz * _NormalTiling * 1.3 - _Time.y * _NormalSpeed * 0.8;第1张图的速度和方向我们可以定义为正向第2张图的速度差一点、方向相反。两张图采出来的法线方向会互相干扰形成一种“无论怎么看都在流动”的视觉效果。如果你只滚一张图很容易看出纹理在重复和滑动两张图一混合重复感会降低很多。混合的方式是UnpackNormal之后相加再归一化float3 normal1 UnpackNormal(tex2D(_NormalMap, uv1)); float3 normal2 UnpackNormal(tex2D(_NormalMap, uv2)); float3 normal normalize(float3(normal1.xy normal2.xy, normal1.z));UnpackNormal是Unity内置函数它把压缩在纹理里的法线数据解码成真正的法线向量。法线图的RGB通道对应XYZ但值域是0-1解码后变成-1到1。提示法线图一定要在Unity导入设置里把Texture Type设为Normal Map。这个没设置UnpackNormal解出来的数据就是错的光照效果会非常怪异。4.3 把法线从切线空间转到世界空间法线贴图采样出来的法线是切线空间而光照计算需要世界空间所以需要一个矩阵把切线空间法线转到世界空间。在Surface Shader里Unity帮我们封装了这个过程。你只需在Input结构体里声明float3 worldNormal; float3 worldTangent; float4 worldPos;Unity就会自动填充它。然后在surf函数里调用// normalize inputs float3 worldNormal WorldNormalVector(IN, normal);这个就是全部了。但为了生成TBN矩阵的中间量我们必须计算世界空间的切线到法线的基向量float3 worldTangent IN.worldTangent.xyz; float3 worldNormal WorldNormalVector(IN, float3(0,0,1)); // 注意这里用了一个小技巧暂时忽略更标准的做法是完整地计算TBN矩阵float3 worldTangent IN.worldTangent.xyz; float3 worldBinormal cross(worldNormal, worldTangent); // 适用于右手坐标系 float3x3 tbn float3x3(worldTangent, worldBinormal, worldNormal); float3 worldNormal mul(perturbNormal, tbn);有了世界空间法线就把它赋给o.Normal。SurfaceOutputStandard会自己拿去计算光照。这里有个容易踩的坑如果水面网格本身不是平面而是在XZ方向有波浪起伏我们前面已经在顶点阶段改了Y轴那WorldNormalVector拿到的“大致法线”已经包含了顶点波浪带来的法线偏移这个偏移方向是成功的——但细节的扰动得靠法线贴图来补。两者叠加才能得出最好的视觉效果。4.4 颜色、平滑度与透明度的逻辑水面的颜色不是单一蓝色它应该和观察角度、深度有关系。简单水面的做法是给一个基础色_Color然后通过法线方向和视线方向算一个菲涅尔Fresnel系数把水面颜色从“垂直看时的水色”过渡到“斜着看时的反射色”。菲涅尔系数的近似公式是Schlickfloat fresnel pow(1.0 - saturate(dot(normalize(IN.viewDir), worldNormal)), 5.0);这个公式的意思就是视线越平反射越强。你站在岸边看远处水面几乎看不清水下因为光都被反射了低头看脚下却能看到水下的石头。这个变化就用fresnel来模拟。最终颜色o.Albedo lerp(_Color.rgb, _ReflectionColor.rgb, fresnel); o.Smoothness lerp(_Glossiness, 1.0, fresnel); o.Alpha lerp(_Color.a, 1.0, fresnel);水面远处反光强、更不透明近处透光、更透明这样符合直觉。4.5 完整的surf函数代码把前面说的全部拼起来void surf (Input IN, inout SurfaceOutputStandard o) { float3 worldPos IN.worldPos.xyz; float2 uv1 worldPos.xz * _NormalTiling float2(_Time.y * _NormalSpeed, _Time.y * _NormalSpeed * 0.7); float2 uv2 worldPos.xz * _NormalTiling * 0.9 - float2(_Time.y * _NormalSpeed * 0.8, _Time.y * _NormalSpeed * 0.5); float3 n1 UnpackNormal(tex2D(_NormalMap, uv1)); float3 n2 UnpackNormal(tex2D(_NormalMap, uv2)); float3 perturbNormal normalize(float3(n1.xy n2.xy, n1.z)); float3 worldTangent normalize(IN.worldTangent.xyz); float3 worldNormal normalize(WorldNormalVector(IN, perturbNormal)); float3 worldBinormal normalize(cross(worldNormal, worldTangent)); float3x3 tbn float3x3(worldTangent, worldBinormal, worldNormal); float3 normalWorld normalize(mul(perturbNormal, tbn)); o.Normal normalWorld; float3 viewDir normalize(IN.viewDir); float fresnel pow(1.0 - saturate(dot(viewDir, normalWorld)), 5.0); o.Albedo lerp(_Color.rgb, _ReflectionColor.rgb, fresnel); o.Smoothness lerp(_Glossiness, 0.95, fresnel); o.Alpha lerp(_Color.a, 1.0, fresnel); }IN.viewDir也是Unity自动帮你算好的它指向摄像机方向。注意这里要normalize因为它是线性插值出来的长度不保证为1。你需要在Input结构体里声明float3 viewDir;Unity才会填充它。忘了声明的话编译会报错说找不到这个变量。5. 反射、波光与观感增强5.1 用反射探针Reflection Probe实现假反射实时反射有两种方案1用一个额外的Camera从水面视角渲染场景得到RenderTexture这是平面反射Planar Reflection2用Cubemap查表这是环境映射。前者效果好但性能几乎翻倍后者便宜且效果够用。对于简单水面直接用反射探针。在Surface Shader标准光照下Unity会调用内置的UNITY_BRDF_PBS它内部已经实现了对反射探针的采样。我们只需要在surf函数里不断更新o.Smoothness和o.Albedo反射效果就会自动出现——你甚至可以什么都不用做只要场景里有反射探针就行。如果想手动控制反射颜色的强度可以加一个_ReflectionColor作为tint在Albedo计算时用菲涅尔混合进去o.Albedo _Color.rgb (_ReflectionColor.rgb - _Color.rgb) * fresnel;当_ReflectionColor是灰色或接近白色时反射就是自然的环境色。5.2 波光Specular Highlight的增强设计水面的镜面高光是一个动态很强的区域。默认的GGX高光在低粗糙度下已经很漂亮但如果想要更闪的波光效果可以手动叠加一层“太阳光追闪”。这个经典的手法是float3 lightDir normalize(IN.worldLightDir); float halfDir normalize(lightDir viewDir); float spec pow(saturate(dot(normalWorld, halfDir)), _Shininess) * _SpecIntensity;再加上法线扰动之后spec会形成细碎的斑点这就是波光的来源。把spec加到Emission或加BaseColor里都行但建议加到一个发光通道因为Bloom会帮它放大。注意这个手动高光和Unity内置的PBR高光会叠加如果两个都开会显得过曝。所以我的做法是把内置Smoothness调低一点比如0.4到0.6然后用自定义spec来补波光细节。这样既能控制整体风格又有细节可看。5.3 深度渐变与泡沫的思路简单版本虽然标题是“简单水面”但既然聊到这里我顺带说一下最简的深度渐变思路。真正的水面靠近岸边的部分会泛白、变浅看着更“真实”。但计算这个需要采样深度纹理代码复杂度会上升一个档次。如果你想要一个不需要深度纹理的近似版本可以把高度Y值或距离水面的UV渐变做进Alpha。比如让水面的Alpha随着顶点高度降低而变小模拟浅水区更透明的效果。这个虽然不准确但视觉上能接受而且零性能开销。泡沫也是同理可以在顶点函数里根据高度判断高度低的地方波浪波谷把泡沫因子提高。没这个需求就跳过加了反而麻烦。5.4 性能开销分析与优化点一个Shader好不好的另一个标准是它跑得快不快。以我们这份Shader为例它的性能开销大致如下顶点阶段3次正弦计算开销很小。片元阶段2次法线纹理采样、2次滚动UV计算、1次反射采样PBS会做、若干日常数学运算。整体指令数在移动端大概在60-80条指令之间属于非常轻量的Shader。最大的隐形开销是Mesh的顶点数。水面如果是一片大平面建议在建模软件里细分好或者在Unity里用Plane旋转90度得到大平面。顶点密度越高波浪细节越平滑但顶点数越多性能越低。对于移动端建议水面的大平面分段数控制在100x100以内即10001个顶点对PC可以开到200x200。另一个优化点是法线贴图的尺寸。法线贴图用512x512就足够了不需要2K。因为它在屏幕上滚动大图带来的纹理缓存压力会增加移动设备的发热。水面这种大面积纹理8192大纹理的问题在于MipMap带宽占用贴图小一点帧率高一点。6. 常见问题与排查技巧实录6.1 问题速查表我在一开始就说要专门来写排错现在这里把最常见的几个问题整理成一个表先放结论再逐条分析症状根本原因解决办法水面全黑什么都看不见渲染状态不对或深度写入导致自身遮蔽设置正确的Queue和ZWrite关闭Cull波浪不动但其它正常顶点函数没被挂载#pragma surface里没写vertex:vert高光像塑料不自然平滑度太高或法线贴图没生效调低Smoothness确认法线贴图导入设为Normal MapUV漂移方向不对UV滚动的方向/速度设置错误查看UV1和UV2的方向让它们相反物体在水面下面看不到水面渲染队列在Opaque里透明物不排序把Tag改成TransparentQueue3000反射是错的/没有反射缺失反射探针或探针位置不对在场景里加Reflection Probe并烘焙编辑器看着正常打包后异常可能漏了Shader变体或贴图压缩问题检查Always Included Shaders改用Quality设置测试6.2 全黑问题深度分析这可能是新手遇到最多的Bug。排查一个Shader相关的问题第一步永远是开Frame Debugger。Window - Analysis - Frame DebuggerEnable之后运行游戏逐步查看Draw Call的渲染状态。你是不是在材质上看到“Advanced”选项下面是空的或者明明Shader编译通过但输出黑色全黑的常见原因是“写入深度、无光照、无输出”三连。由于我们用的是Standard光照模型全黑大概率是下面两种你没开“渲染管线接收光”也许你的顶点法线全是零。这是顶点法线没处理好导致光照计算里的N dot L正好为0。解决方法在顶点函数里给法线加上微小偏移或者确认网格的法线存在。材质没有接收到环境光。你在场景里用的是环境光或只有平行光时如果法线错误平行光的高光会正确但基础色会被环境光盖掉。检查Lighting窗口确认Environment Lighting不是黑色。6.3 波浪撕裂或穿帮问题的处理波浪在做大位移时如果网格不够细会有明显的折线感和撕裂。这是因为顶点之间的插值是线性的正弦波的弯曲无法被线性插值精确拟合。解决办法很简单增加网格分段或者把_WaveHeight调小。如果水面和岸边物体穿插那是碰撞边界问题不是Shader能解决的。水面Shader只管渲染不管物理。另一个穿帮现象是从水底往上看整个水面黑乎乎一片。这是因为水面没有“双面渲染”。Cull Off之后从下面也能看到水但光照计算的角度反了。遇到这种情况可以在surf函数里判断incident angle如果从背面看把法线翻转一下视觉会好很多。6.4 帧率暴跌的排查心得有人会碰到这种情况水面周围帧率还可以一到水面区域就掉帧。这真不怪Shader本身得先看你的渲染状态是不是把半透明物体的Overdraw搞爆了。半透明水面如果叠加了很多层比如Multi-pass Shader或者一个帧里有多个重叠的透明物体像素会被反复填充很多次带宽会崩溃。我的排查方式是打开Frame Debugger看水面的Draw Call是不是多层。打开GPU Profiler看水面材质的Fragment耗时是不是超过0.5ms。用RenderDoc抓一帧看Overdraw的可视化RenderDoc里可以开启Quad Overdraw模式。简单水面Shader通常非常轻量它拖慢帧率的最大可能性是两个一是贴图尺寸太大MipMap采样导致纹缓存命中率低二是水面网格分段太多顶点处理来不及。这两个都能在线上一眼定位。6.5 调参心态与实操建议最后给一个实操建议参数一定不要一次调一堆。很多人写完好几个波浪打开Inspector手一痒Height、Speed、Fresnel一鼓作气全改了结果画面崩了根本不知道是哪一项导致的。我的工作是先固定所有参数只动一个看效果再动下一个。可以建一个调参检查表先把颜色设成纯白、法线强度设为0。这时候水面应该是一块静止的蓝色玻璃。开波浪调Height到出现起伏但不要超过顶点间距的四分之一否则会呈现“折叠”。开法线贴图把Normal Strength从0.1慢慢往上涨直到波光出现但不花。最后调菲涅尔和反射强度直到远处反射足够强、近处透明度合适。调参是慢功夫但它比乱撞快得多。我后来在项目里凡是做水面都先按这个思路把基础打好再在上面叠功能。有些时候一个简单的正弦波水面加上好的法线贴图和反射探针效果已经比很多复杂方案要好而且它跑得飞快。后面的扩展方向也很清楚想加真实波浪就换Gerstner波想加交互就去检测物体高度想加焦散就去投影焦散纹理。底子打好了这些都是顺水推舟的事。
RELATED

相关推荐

STC89C52智能花盆:传感器采集与闭环控制完整方案

STC89C52智能花盆:传感器采集与闭环控制完整方案

简介:一份面向单片机学习者与毕业设计学生的智能花盆完整设计方案,以STC89C52为主控,通过土壤湿度传感器和DS18B20实时采集花盆内土壤温湿度,同时利用光敏传感器感知环境光照,所有数据经LCD屏清晰显示。系统支持按键切…

📅 2026/9/15 17:00:25
GPUI Kit 菜单组件实战指南:使用 PopupMenu 构建上下文菜单、下拉菜单与子菜单系统

GPUI Kit 菜单组件实战指南:使用 PopupMenu 构建上下文菜单、下拉菜单与子菜单系统

GPUI Kit 菜单组件实战指南:使用 PopupMenu 构建上下文菜单、下拉菜单与子菜单系统 【免费下载链接】gpui-kit Rust GUI components for building fantastic cross-platform desktop application by using GPUI. 项目地址: https://gitcode.com/GitHub_Trending/g…

📅 2026/9/15 17:00:25
LifeOS Fabric 模式精解:explain_project 系统提示词的结构解析与实战应用

LifeOS Fabric 模式精解:explain_project 系统提示词的结构解析与实战应用

LifeOS Fabric 模式精解:explain_project 系统提示词的结构解析与实战应用 【免费下载链接】LifeOS ⛰️ The Life Operating System — an intent engineering platform that moves you from your current state to your ideal state, in life and work. 项目地址…

📅 2026/9/15 17:00:25
MORE NEWS

更多资讯

📰

充电桩多协议API自动化基建:JSON驱动+跨语言测试闭环

简介:本资源是一套面向Web开发工程师与新能源IoT系统集成者的充电桩API自动化搭建实战源码,聚焦解决新能源汽车充电设施快速对接、接口标准化配置与多语言协同开发等实际问题。压缩包共124个文件,总大小2.93MB,涵盖52个JSON配置文…

📰

Quickwit 依赖升级实战:将 Tantivy 升级到最新提交的标准化流程(bump-tantivy)

Quickwit 依赖升级实战:将 Tantivy 升级到最新提交的标准化流程(bump-tantivy) 【免费下载链接】quickwit Cloud-native OSS search engine for observability 项目地址: https://gitcode.com/GitHub_Trending/qu/quickwit 本文基于仓库…

📰

TanStack Router 导航(Navigation)完全指南:类型安全的 Link、预加载、导航拦截与滚动恢复

TanStack Router 导航(Navigation)完全指南:类型安全的 Link、预加载、导航拦截与滚动恢复 【免费下载链接】router 🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React an…

📰

通过 Rube MCP 自动化 Meta Ads 投放:awesome-codex-skills 中 metaads-automation 技能实战指南

通过 Rube MCP 自动化 Meta Ads 投放:awesome-codex-skills 中 metaads-automation 技能实战指南 【免费下载链接】awesome-codex-skills A curated list of practical Codex skills for automating workflows across the Codex CLI and API. 项目地址: https://g…

📰

redis-py 重试退避(Backoff)策略全解析:从 Exponential 到 Jitter 的算法实现与实战配置

redis-py 重试退避(Backoff)策略全解析:从 Exponential 到 Jitter 的算法实现与实战配置 【免费下载链接】redis-py Redis Python client 项目地址: https://gitcode.com/GitHub_Trending/re/redis-py 在 redis-py 中,redi…

📰

从 Next.js App Router 迁移到 TanStack Start:React 全栈框架逐步迁移指南

从 Next.js App Router 迁移到 TanStack Start:React 全栈框架逐步迁移指南 【免费下载链接】router 🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more). 项目地址: https://gitcode…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬