
水面shader在Unity3D项目里属于那种“看着不难一写就废”的东西。两年前我第一次尝试直接把水面做成一个半透明蓝片片放在场景里不仅没有“波光粼粼”反而像个蓝色大果冻倒影、浪花、深浅变化全都没有越看越尴尬。后来静下心把水面效果拆解成“顶点起伏、法线扰动、颜色渐变、边缘融合、反射高光”五个层面一个个解决才做出能放进游戏里用、帧率也不太难看的水面。这篇文章就是基于我当时踩坑的过程整理出来的完整实现方案全部用ShaderLab写可以直接拿去做一个能上移动端的水面材质。作者只需要一个Unity3D项目、一块基本的法线贴图就能跟着把代码搭起来。我也会把每个关键参数的选择理由说清楚比如波形为什么要叠加两个方向、深水区和浅水区的颜色为什么要用深度纹理去过渡以及为什么不管多着急都先开深度纹理否则边缘必穿帮。整体方案不是那种吃性能的FFT海洋而是走“常规技巧组合拳”的路线顶点正弦波双法线贴图扰动深度融合反射探针高光渲染量几乎可以忽略不计在低端安卓机上也能跑适合大多数俯视角和第三人称场景。1. 内容整体设计与思路拆解1.1 水面效果本质不是“画”出来而是“骗”出来想清楚水面怎么做先得明白人眼觉得“这是水”到底靠哪几个信号。单纯给一个平面贴蓝色再调成半透明出来的东西谁也不认为是水面因为没有“动态”和“深度”。一片合格的水面至少要同时具备这四类视觉特征表面有随时间变化的起伏即使是远处看起来相对平静的水面放大看也是在动颜色随着水的深浅变化浅滩发青、发透深水区发蓝、发暗与岸边、物体的交界处有融合感不能是一刀切的边缘高光和反射要跟着视角动太阳打在波纹上的那条“光路”是水面身份的重要标识。这些特征加在一起才是“水感”。单独拆开每一项都不难难的是组合在一起还得高效。所以我的方案是顶点Shader负责几何层面的波动片元Shader负责颜色、光照和边缘融合再叠加噪声或双法线贴图增加表面细节。这样分配最符合GPU的工作方式——顶点阶段运算少但改变几何结构片元阶段逐像素处理能创造丰富细节各干各的、互不拖累。1.2 为什么不用复杂的FFT海洋扰动方案网上一搜水面shader很容易看到一堆FFT快速傅里叶变换海面、Gerstner波大规模模拟的教程。那些方案确实效果震撼但是它们的目标场景是《盗贼之海》那种大型海面需要的是海量顶点密度和GPU Tessellation甚至要配合compute shader做频谱合成。普通项目的水面可能只是一个池塘、一条河流或者战斗场景里一片浅滩直接把FFT搬过来性能先崩一半调参又能调一个礼拜。我这篇文章用的是“正弦波叠加法线贴图”的经典组合本质上是让一个平面在顶点层做连续的波浪状偏移再用法线贴图去“欺骗”光照方向。水面网格用不到太多顶点一块20x20的平面顶点密度在合适范围内就够用移动端跑起来几乎没压力。这也是UnityAsset Store里一大堆风格化水面插件的基本原理不是偷懒而是这个方案在“性价比”上确实最优。1.3 整体架构五个模块各司其职从0到1写这个shader我的思路是分模块实现不搞一口气糊一大段代码。每个模块效果可单独开关方便调试定位问题。模块划分如下模块作用实现层级波形起伏平面顶点随正弦波偏移产生几何波动顶点着色器法线扰动让光照计算使用扰动后的法线形成波纹高光细节片元着色器深度融合采深度纹理让水面颜色随深度渐变边缘与岸边融合片元着色器反射与高光反射探针采样Blinn-Phong高光补足立体感和光感片元着色器透明与排序半透明渲染队列让水能透出下面的物体颜色SubShader配置每个模块单独写成一个小函数最后在片元着色器里汇总。这样如果最终颜色发黑或者波纹不动我能立刻知道是哪个模块出了问题而不是对着三百行代码猜。2. 基础水面Shader实现从零到能用2.1 Shader框架与渲染队列的坑代码先从ShaderLab的框架开始。水面是半透明的水底物体透过水层必须被看到所以渲染队列要放在Transparent混合模式用SrcAlpha OneMinusSrcAlpha。这里有个初学者常踩的坑如果仅仅是半透明其实用QueueTransparent和QueueAlphaTest都行但水面往往涉及深度纹理对比为了防止水面自己挡住自己、又得能正常写入深度给后续物体参考建议加上ZWrite Off。单纯把队列设置成Transparent而不关深度写入物体会出现半透明排序错乱远处的桥墩可能会被莫名其妙地挡住。Shader Custom/SimpleWater { Properties { _MainTex(水面法线贴图, 2D) bump {} _WaveStrength(波纹强度, Range(0, 1)) 0.3 _WaveSpeed(波纹流动速度, Range(0, 2)) 0.5 _ColorDeep(深水颜色, Color) (0.1, 0.3, 0.5, 1) _ColorShallow(浅水颜色, Color) (0.3, 0.7, 0.8, 1) _Gloss(高光锐度, Range(8, 256)) 64 _SpecColor(高光颜色, Color) (1, 1, 1, 1) _DepthFactor(深度渐变色强度, Range(0, 2)) 1 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc // ... 具体函数 ... ENDCG } } }Cull Off这个细节容易被忽略水面俯视角看没问题但如果从侧面、底下观察开了背面剔除就会看到水面背面是空的特别假。直接关掉一劳永逸。2.2 顶点波动最简单的正弦波叠加顶点波动是水面动态的基础。最原始的想法是给顶点Y轴加一个sin(_Time.y)高度但这样整片水面同一个频率升降效果跟升降平台一样一点波纹感都没有。正确做法是叠加多个不同方向、不同频率的正弦波。在顶点着色器中这样写float3 vertexWave(float3 pos, float2 uv) { float wave1 sin(uv.x * 6.28 _Time.y * 1.2) * 0.1; float wave2 sin(uv.y * 8.0 _Time.y * 0.9) * 0.08; float wave3 sin((uv.x uv.y) * 4.0 _Time.y * 0.6) * 0.05; pos.y wave1 wave2 wave3; return pos; }三个波叠加的理由很简单单个波是“规律感很强”的重复波动人眼会迅速识别成机械运动三个频率、方向不同的波叠加上去就能得到一个看似无规律、但又流畅自然的水面起伏。实际调整的时候我习惯让一个波幅度大但频率低负责“大浪”一个波幅度小但频率高负责“波纹细节”第三个波斜着穿过平面破坏前两个波形成的规则方格感。注意这里为了简化演示直接用了uv作为波的位置参考它的缺点是当物体scale变化时波纹大小会跟着变。如果希望水面波纹跟世界坐标挂钩可以把uv替换成worldPos.xz这样所有水面拼在一起时波纹尺度统一接缝处不会有尺度突变的问题。2.3 片元函数颜色基础与时间动画片元着色的第一步是把深水色和浅水色按区域混合。选颜色是门学问纯蓝色并不是首选因为自然界的水体颜色受底部和天空影响很大。我常用的两套配色清澈热带海水深水色(0.05, 0.35, 0.45)浅水色(0.25, 0.75, 0.85)适合海岛、泳池周边阴沉湖泊水深水色(0.05, 0.1, 0.15)浅水色(0.25, 0.35, 0.4)适合暗色调恐怖、求生类场景。颜色混合不能直接拿_ColorDeep和_ColorShallow做线性lerp那样会造成明显的硬边。标准做法是拿深度纹理的结果做一个非线性映射让浅水区范围收窄深水区快速过渡到深色。下面这个函数先预留第3节详解float4 frag(v2f i) : SV_Target { float3 normal UnpackNormal(tex2D(_MainTex, i.uv _Time.x * _WaveSpeed)); // 后续会在这里叠加深度、反射、高光等 float4 col lerp(_ColorShallow, _ColorDeep, depthFactor); return col; }法线贴图的uv加上时间偏移是为了让波纹产生“流动感”。单纯让法线贴图静止不动的话波纹虽然存在却像塑料压花给UV加上随时间变化的偏置法线就像有了生命。3. 进阶效果让水面真正“活”起来3.1 从深度纹理拿到水底深度差深度融合是水面效果从“塑料玻璃”升级到“真水”的关键一步。菜鸟做法是给水面一个固定透明度但真实浅水区应该能看到底深水区则越来越不透明。这两者的分界必须依赖“深度纹理”——也就是场景中每个像素距离相机的远近。拍摄深度纹理前需要在相机上允许深度纹理。如果是主相机直接设置Camera.main.depthTextureMode DepthTextureMode.Depth;或者在相机的Inspector面板里把Depth Texture选项设为On。Shader里通过_CameraDepthTexture访问。拿到当前水面像素的深度和它后面物体深度的差值记作depthDelta。深度差值大水就深差值小水就浅。float depth LinearEyeDepth(tex2Dproj(_CameraDepthTexture, i.screenPos).r); float depthDelta depth - i.screenPos.w; float depthFactor saturate(depthDelta * _DepthFactor);这段代码的解释i.screenPos是水面顶点投影后的屏幕坐标i.screenPos.w是水面本身在视图空间里的深度。背景物体的深度减去水面的深度得到的就是“从水面往下看有多深”。这里有一个直接决定效果好坏的经验_DepthFactor不是越大越好。设成0.5到1.5之间比较稳妥。数值太大会导致水面颜色瞬间过渡到深水色浅滩效果完全消失数值太小又会千篇一律全是浅水色深水区不显“深”。3.2 双法线贴图流动打破重复感单张法线贴图无论怎么偏移仔细观察还是能看出波纹循环的规律因为图案在移动但图案本身不变。要打破这种规律性“双法线贴图不同速度、不同方向叠加”是目前综合性价比最高的方案。这是我的核心做法float2 uv1 i.uv * _MainTex_ST.xy float2(_Time.x * 0.3, _Time.y * 0.2); float2 uv2 i.uv * _MainTex_ST.xy float2(_Time.x * -0.2, _Time.y * 0.3); float3 normal1 UnpackNormal(tex2D(_MainTex, uv1)); float3 normal2 UnpackNormal(tex2D(_MainTex, uv2)); float3 normal normalize(float3(normal1.xy normal2.xy, normal1.z * normal2.z));两张法线贴图用同一张图但是让它们一个往右上移、一个往左下移、速度还不一样叠加之后原本规则的重复纹理就变得很自然。如果用两张不同的法线贴图比如一张细小波纹、一张大波浪效果更好但哪怕只有一张只要用双UV的方法观感也能提升一大截。使用法线贴图时的导入设置也很关键必须把Texture Type设为Normal Map否则取样出来的法线方向是错的水面会出现一种“凹凹凸凸”的怪光感而且无法通过调强度修正。3.3 反射探针不用实时反射的Plan B实时反射相机Reflection Probe的Realtime模式在移动端是性能杀手一个反射相机等于把场景又渲染一遍遇到复杂场景帧率直接腰斩。更聪明的做法是使用烘焙反射探针Baked Reflection Probe。具体操作在场景里创建Reflection Probe选择Baked模式调整好影响范围保证水面所在区域能被探针捕捉到。Unity会预计算一个Cubemap包含环境颜色信息。Shader里这样采样float3 reflDir reflect(-viewDir, normal); float3 reflColor texCUBE(unity_SpecCube0, reflDir).rgb;用unity_SpecCube0不需要额外声明变量这是Unity ShaderLab内置的反射探针采样接口光照探针组会自动给它赋值。这样水面就会反射出天空、周围的山和建筑性能开销几乎可以忽略。如果想让反射更“糊”一点像真实水面那样模糊反射可以用texCUBElod(unity_SpecCube0, float4(reflDir, roughness))越大越模糊。这是一个免费模糊不用做单独的模糊渲染靠mipmap的lod就能实现。3.4 高光Blinn-Phong的魔改水面的高光不是普通镜面高光而是“碎碎”的、被波纹切碎的光斑。直接把Blinn-Phong的高光套上去会看到一大块闪亮的亮斑像塑料球。我的做法是把高光项跟法线扰动挂钩因为法线已经被双法线贴图打乱了高光自然就会碎掉float3 halfDir normalize(lightDir viewDir); float spec pow(max(dot(normal, halfDir), 0), _Gloss); float specMask saturate(noiseFactor * 2); float3 specColor _SpecColor.rgb * spec * specMask;specMask可以用法线贴图里的R通道或者另一个噪声值做目的是给高光加一个不规则强度蒙版让有些位置亮、有些位置暗而不是一刀切整片亮。_Gloss设置在32到128之间最有水面感太低是粗糙水面太高就成了镜子。3.5 岸边泡沫和边缘淡化细节决定档次一个人物站在岸边或者小船划过水面水面边缘跟岸接触的地方如果只有一条硬边立刻显得廉价。这里有两个思路思路一用深度差做泡沫Mask深度差小于一定阈值的区域认为是浅滩/岸边混入白色泡沫颜色。这样水面接近岸边的位置会自然形成一圈白边float foam smoothstep(0.05, 0.25, depthDelta); float3 foamColor float3(1, 1, 1); col.rgb lerp(foamColor, col.rgb, foam);思路二用深度差渐变控制Alpha越浅的水越透明越深的水越不透明col.a saturate(1 - depthDelta * 2);这两个效果共用一份深度差数据没有任何额外成本但观感提升是肉眼可见的。如果水面在大面积湖泊中用泡沫阈值建议调到0.2到0.4之间太大会出现一条很宽的白边很怪如果做小溪或浅滩可以适当加大。4. 材质面板、参数调优与性能考量4.1 一张可直接抄作业的完整Shader把前面所有模块串起来生成一个完整、能直接应用于Unity3D项目的Shader。这段代码我实测过是本文的核心资产。把它复制进工程内新建的Shader文件新建材质、指定这个Shader再把法线贴图拖进_MainTex槽位即可运行。Shader Custom/SimpleWater { Properties { _MainTex(法线贴图, 2D) bump {} _WaveStrength(顶点波浪强度, Range(0, 0.5)) 0.15 _WaveSpeed(波浪速度, Range(0, 2)) 0.6 _ColorDeep(深水颜色, Color) (0.05, 0.2, 0.4, 1) _ColorShallow(浅水颜色, Color) (0.3, 0.75, 0.8, 1) _Gloss(高光锐度, Range(8, 256)) 64 _SpecColor(高光颜色, Color) (1, 1, 1, 1) _DepthFactor(深度渐变强度, Range(0, 2)) 0.8 _NormalScale(法线强度, Range(0, 2)) 1 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc #include Lighting.cginc sampler2D _MainTex; float4 _MainTex_ST; float _WaveStrength; float _WaveSpeed; float4 _ColorDeep; float4 _ColorShallow; float _Gloss; float4 _SpecColor; float _DepthFactor; float _NormalScale; sampler2D _CameraDepthTexture; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float4 screenPos : TEXCOORD1; float3 worldPos : TEXCOORD2; }; v2f vert(appdata v) { v2f o; float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float wave1 sin(worldPos.x * 0.3 _Time.y * 1.2) * _WaveStrength; float wave2 sin(worldPos.z * 0.5 _Time.y * 0.8) * _WaveStrength * 0.6; float wave3 sin((worldPos.x worldPos.z) * 0.4 _Time.y * 0.7) * _WaveStrength * 0.3; v.vertex.y wave1 wave2 wave3; o.uv v.uv; o.vertex UnityObjectToClipPos(v.vertex); o.screenPos ComputeScreenPos(o.vertex); o.worldPos worldPos; return o; } fixed4 frag(v2f i) : SV_Target { float2 uv1 i.uv * _MainTex_ST.xy float2(_Time.x * _WaveSpeed * 0.8, _Time.y * _WaveSpeed * 0.4); float2 uv2 i.uv * _MainTex_ST.xy float2(_Time.x * _WaveSpeed * -0.6, _Time.y * _WaveSpeed * 0.7); float3 normal1 UnpackNormal(tex2D(_MainTex, uv1)); float3 normal2 UnpackNormal(tex2D(_MainTex, uv2)); float3 normal normalize(float3((normal1.xy normal2.xy) * _NormalScale, normal1.z * normal2.z)); normal normalize(normal); float depth LinearEyeDepth(tex2Dproj(_CameraDepthTexture, i.screenPos).r); float depthDelta saturate(depth - i.screenPos.w); float depthFactor saturate(depthDelta * _DepthFactor); float4 col lerp(_ColorShallow, _ColorDeep, depthFactor); float3 viewDir normalize(_WorldSpaceCameraPos - i.worldPos); float3 lightDir normalize(_WorldSpaceLightPos0.xyz); float3 halfDir normalize(lightDir viewDir); float spec pow(max(dot(normal, halfDir), 0), _Gloss); float specMask saturate(normal1.r * 2 normal2.r * 1.5); col.rgb _SpecColor.rgb * spec * specMask; float3 reflDir reflect(-viewDir, normal); float3 reflCol texCUBElod(unity_SpecCube0, float4(reflDir, 2)).rgb; col.rgb reflCol * 0.3; float foam smoothstep(0.05, 0.25, depthDelta); col.rgb lerp(float3(0.9, 0.95, 1) * foam * 0.8, col.rgb, saturate(foam 0.2)); col.a saturate(0.6 depthFactor * 0.4); return col; } ENDCG } } }4.2 材质面板参数调优经验参数调优的基准顺序先调颜色再调深度再调法线最后调高光。颜色先确定大基调深水色和浅水色的色相不要相差太远否则会有一条明显的变色分界线。深度渐变强度决定这个变色带的位置把它调到水面刚好能看出深浅过渡、但又不会突然跳色为止。法线强度决定波浪的“锐利感”数值超过1后波纹高光会过曝产生闪烁白噪点。高光锐度控制反射光斑的大小64附近是安全区过小像油面过大像镜面。如果是夜晚场景深水色和浅水色都要压暗且高光颜色改成冷色调比如淡蓝色不要用纯白否则水面在月光下会显得荧光。4.3 移动端与PC端的性能取舍性能方面主要看三块顶点数、法线贴图采样次数、反射探针。顶点数上20x20平面网格在PC上没什么感觉但移动端建议用10x10或者8x8的网格密度。水面波浪的细节由法线贴图补充顶点密度低一点无妨。法线贴图采样我控制在2次再加上一个反射探针采样片元Shader压力很小。反射探针的Cubemap分辨率建议顶到256就够设成512的话在游戏中占据内存和带宽明显增加。值得提醒的是如果水面面积很大千万不要用一个大平面挂这个Shader然后用Realtime反射。替代方案是水面分为近处和远处两块近处用高精度参数远处用简化版没有顶点波动只做颜色渐变。很多成熟项目都是这么处理的。5. 常见问题与排查实录5.1 快速对照表问题、原因、解决问题现象大概率原因解决方案水面全黑相机没有开启深度纹理Camera.main.depthTextureMode DepthTextureMode.Depth;水面没有波动顶点密度太低增加平面网格分段数或检查_WaveStrength是否接近0法线贴图有奇怪凹凸贴图没有设置成Normal Map在导入设置里Texture Type选Normal Map水面排序错乱、挡物体没有ZWrite Off加回ZWrite Off高光过曝闪烁法线强度_NormalScale太大降到0.5~1之间反射内容不正确反射探针没覆盖水面区域调节Reflection Probe的Bounds确保障景覆盖水面半透明看不到水底背景物体不在Transparent前面大概率深度纹理未开或背景物体用Unlit不写入深度5.2 深度纹理不生效的完整排查过程曾经遇到过一个问题水下物体能看见但水面深浅过渡完全失效整片水面都是一种颜色。排查后发现不是Shader的问题而是相机的Depth Texture没有成功生成。DetectionTexutre在部分后处理管线里自带但在纯Shader实现时必须手动开启相机深度纹理模式。排查时可以这样验证用一个临时脚本在Start里输出Camera.main.depthTextureMode看看有没有包含Depth标志。如果没有就执行Camera.main.depthTextureMode | DepthTextureMode.Depth;。另一个隐蔽的坑是URP管线下的_CameraDepthTexture命名和内置管线不同。URP里要用_CameraDepthTexture没问题但Shader要改成HLSL语法整体结构不一样这篇文章的ShaderLab版本主要针对内置渲染管线Built-in RP。如果用URP需要把代码迁移到HLSLLinearEyeDepth的调用参数也要根据URP版本调整。5.3 法线贴图不动波纹像静止贴图法线纹理不动的原因通常是Shader里没有给_MainTex的UV加时间偏置或者_Time被禁用了。_Time.x和_Time.y在Unity内置Shader变量里都有定义只要Shader没有被特殊优化掉默认都能用。还有一个常见错误是直接在采样函数里写_Time.x但没有使用_Time.y导致速度极慢肉眼几乎看不到变化。我实践下来水面流动速度设置在每秒偏移0.2到0.5个UV单位比较合适低于0.1基本肉眼无法察觉高于1则像瀑布一样剧烈。5.4 透明物体排序的终极方案半透明渲染顺序一直是个老大难。水面之间互相重叠、物体一半在水里一半在水上排序很容易乱。Shader层面的常规解法是为模型摆正渲染顺序而不是依赖distance to camera排序。水面可以用QueueTransparent1或更高确保在水下物体之后渲染。如果一个物体需要在水面之上被看到它的材质队列应小于水面如果物体是在水下被看到则要保证它先绘制到深度纹理里再被水面覆盖。这个顺序一旦搞混就会出现明明物体在水下却被水面完全遮挡的效果。6. 从“能用”到“好用”的方向补充6.1 双面波纹的奇偶网格优化大平面网格的顶点波在计算时如果所有顶点用同样的公式会出现对称感很强的重复波浪。优化方法是让每个顶点的波长和相位跟它的坐标挂钩比如float wave4 sin(worldPos.x * 0.7 worldPos.z * 1.1 _Time.y * 0.5);这样平面不同区域相位错开波形更自然。还有一种做法是用顶点色R/G/B通道存储三种相位偏移美术可以在模型上自由刷出波动的强弱这种方法在较大水面时很好用。6.2 用Gerstner波替代正弦波做浪尖如果想做更真实的海浪可以让顶点横纵坐标也跟时间发生偏移这就是Gerstner波的核心思路。Gerstner波能做出“波峰尖锐、波谷平缓”的立体感而正弦波只能做出对称的起伏。这段内容是为那些觉得纯正弦波不够味的朋友准备的实现方式是在顶点函数里加入水平方向偏移float xOff sin(worldPos.x * 0.5 _Time.y * 1.0) * _WaveStrength * 0.5; float zOff cos(worldPos.z * 0.4 _Time.y * 0.8) * _WaveStrength * 0.5; worldPos.x xOff; worldPos.z zOff;加入水平偏移后水面会从上下起伏变成真正的“涌动”尤其在岸边的礁石碰撞处这种运动会更有冲击力。代价是水面网格的密度需要更高否则顶点位移会造成穿插。6.3 风格化水面的极简思路如果项目走低多边形/卡通风格可以放弃真实感路线把所有物理参数替换成颜色分区。做法如下去掉法线贴图用噪声图让颜色沿世界坐标xz流动深度纹理保留但只用来区分两个固定颜色不渐变高光直接做成圆形亮斑跟随视角移动。效果很风格化性能也最好。6.4 后续扩展方向目前实现的Shader是基于内置管线的如果要转URP尤其Unity6的URP 17以上版本需要把CGPROGRAM改成HLSLPROGRAM采样深度纹理的函数从LinearEyeDepth改到LinearEyeDepthURP有自定义实现光照函数也要从Lighting.cginc换成URP的Lighting.hlsl。转换不难但细节很多。更进一步可以加入模拟浪花泡沫贴图、泡沫随时间消散、岸边湿沙效果、以及水面下的焦散投影Caustics。焦散投影的做法是给水下地面单独加一层扰动贴图用水的波形去驱动这个图案可以实现水下光影摇曳的梦幻效果。这篇文章先写到这后面我会专门出一篇URP迁移和焦散实现的进阶教程到时候可以对照着一起改。其实水面shader最有趣的一点就是它把所有基础图形学技巧都串起来用了。顶点变换、纹理采样、法线映射、反射、深度缓冲——全是基础功组合起来却能有很惊艳的结果。做完这个真的能体会到graphics编程的乐趣每次调个参数水面都会给你完全不一样的表情。