《哈迪斯1》里最让我着迷的风格化渲染对象,不是那些刀光剑影的技能特效,而是整条冥河。作为一个经常和Unity CG Shader打交道的TA,我第一次看到那片深蓝紫的流体时,脑子里冒出的问题特别直接:如果只给我一个Plane和一段UV,我要怎么用Shader复刻出这种暗底亮线的流动感?后来我花了两个版本迭代,把这条河从“动态贴图”逐步调到了“接近原版气质”的程度。这篇博文就是一次完整复盘,包含视觉拆解、代码实现、以及我在工程落地时踩过的那些坑。想搞风格化流体、想做河流渲染、或者单纯想理解Unity CG Shader里UV流动和噪波扭曲怎么组合,都可以参考这套思路。
1. 冥河视觉拆解:先把一张会流动的插画翻译成参数
1.1 从截图里提取颜色分区
《哈迪斯1》的冥河并不是写实流体,更像是一张被赋予生命的高饱和插画。我把它放大看了很多遍,发现颜色其实可以拆成三个层次:最底层的深蓝黑和紫褐色负责给画面压住重量感,中层的青蓝和淡紫负责表现流体的体积,最上层的白色和亮青色细线则承担了轮廓和视觉引导。这三层之间的关系非常明确:暗的地方足够暗,亮的地方足够亮,中间几乎没有复杂的过渡。做风格化渲染最忌讳的就是把颜色调成连续的渐变,看起来像塑料,原版的处理方式是刻意保持明暗跳变。
落到Shader参数上,我最终保留了三个Color属性:_DeepColor(基底暗色)、_MidColor(中层主色)、_GlowColor(高亮描边色)。这三个颜色在运行时并非均匀混合,而是依靠贴图alpha、噪波强度和视角边缘同时参与计算,不同区域会呈现完全不同的色调权重。这样做的目的是让河面看起来有“信息层次”,而不是一个简单的大色块。
1.2 看懂流动形态:螺旋、带状与颗粒的共存关系
除了颜色,《哈迪斯1》的冥河在形态上有一个很重要的设计,就是同时存在三种不同尺度的运动。第一种是大尺度的带状流动,像绸缎被风吹动一样,整条河流有明显的主方向;第二种是中尺度的螺旋涡流,尤其在河面拐弯处和狭窄处,会出现局部回旋;第三种是小尺度的颗粒闪光,像碎星一样漂浮在河面上,逐帧闪烁。
这三层运动在Shader里对应完全不同的技术手段。带状流动我用了最简单的主纹理UV滚动;螺旋涡流用了局部旋转UV加上遮罩;颗粒闪光则是把一张高频噪声贴图叠加到漫反射和高光通道里,让它随时间抖动。三层速率必须拉开差距,否则会糊成一团。原版给我的体感是:主流动速度偏慢,涡流中等,高光颗粒最快,但高光颗粒的透明度又很低,属于“存在但不抢戏”的级别。
1.3 动态强度要服务于游戏阅读性
很多人在复刻这种效果时会犯同一个错误:把水面做得太花。我记得第一版Shader把流速调到很高,涡流强度也特别大,结果放进实际关卡里,玩家完全读不清地形,角色的轮廓也被水流干扰了。后来我把动态强度整体降到60%左右,再配合角色脚下的“跟随波光”粒子,游戏性反而好了很多。
这条经验我想放在最前面说:风格化场景的特效不是越强越好,而是要跟玩法形成一个动态对比。在《哈迪斯1》这种俯视角地下牢关卡里,水面既是视觉主体,也是陷阱和通行区域。它的流动必须能暗示方向,但不能覆盖怪物攻击和玩家操作的信息。所以后面所有参数的调节,我都围绕一个原则:让水面在静态截图上好看,在动态游玩中安静。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与资产准备:为什么是UV Flow而不是粒子系统
2.1 大面积水面用粒子系统是灾难
我最初也考虑过用粒子+Trail来做冥河,因为原版很多细碎的涡流看起来特别像粒子层。但粒子方案在实操中很快就被否掉了:要覆盖一整片关卡地图,需要几千个粒子,排序混乱、Overdraw高、不同分辨率下分布还不可控。更麻烦的是,粒子的运动无法跟场景网格的UV保持连续,一旦边缘不齐就会露出非常明显的穿帮。
UV Flow方案的优势正好相反:性能开销主要来自片元着色器,场景有多大就采样多少片元,开销完全可控;流动方向通过UV速度控制,和地形网格自然贴合;更关键的是,它可以跟顶点色、世界坐标遮罩这些信息自由组合,做出河流中间快、岸边慢的渐变效果。粒子适合做点缀,大范围流体一定要用Mesh + Shader。
2.2 网格与UV的规划直接决定效果上限
这条河的网格我用的是最简单的Plane改。网格尺寸按照关卡地形切成长条形,宽度大概8到12米,长度直接拉伸到几十米。Plane本身不需要太多细分,但要留意UV的V方向不能跟着模型拉伸得过于夸张,否则岸边波纹会被放大成一条条横线。我习惯把UV做两套:一套是模型自带的常规UV,保留了0到1的横纵向比例;另一套是通过脚本或顶点色生成的沿河岸长度方向递增的UV,专门给流动纹理使用。
如果你只是用小尺寸Plane平铺,那V方向的波纹会随着平铺次数反复出现,接缝处特别显眼。推荐的做法是:让材质主纹理Tiling的X方向保持较高值,Y方向保持1左右,配合Wrap Mode设为Repeat,这样纵向波纹会被压扁,横向上反而会出现连绵不断的长条纹路,更接近原版绸带感。
2.3 两张程序化贴图就能拿下大部分资产
很多项目的风格化河流喜欢用FlowMap,但《哈迪斯1》的冥河并不需要严格的FlowMap方向引导,它更像“大体方向一致,局部有扰动”,所以只用两张平铺噪声贴图就够用:一张做主纹理,一张做扰动和颗粒。
主纹理我推荐用1024x512的尺寸,绘制方法可以很简单:在Photoshop里新建文档,用柔边圆画笔横向拉出一些长条曲线,低透明度叠加,然后在通道面板里把R通道和高光细节分开——R通道用来控制流动遮罩,A通道用来控制河流边缘透明度。如果你想做得更可控,还可以再叠加一点细微噪点作为河面颗粒。
噪波纹理可以在Photoshop里用“渲染->纤维”加“极坐标”生成,也可以用网上现成的Perlin Noise无缝贴图。关键是它必须Tileable,而且不能有明显的十字接缝。我自己最终用的是128x128Perlin噪声,R通道和G通道采样结果错开,分别用于大尺度扰动和小尺度闪烁。两张贴图加一个网格体,就能把河流的主干撑起来。
2.4 为什么用Unity CG Shader而不是URP Shader Graph
这里需要特别声明一下:标题里提到的UnityCGshader,指的是内置渲染管线下的Cg/HLSL写法。我在这个项目里选择内置管线的原因主要有三个:第一,目标项目本身是Unity 2019的老项目,团队管线没有迁到URP;第二,CG Shader对半透明、多Pass和内置雾效的支持非常直接,写起来反而比Shader Graph灵活;第三,也是最重要的,你可以通过连接线清楚地看到每一个数学节点在干什么。对想学习风格化渲染原理的TA来说,直接读Cg代码能建立更扎实的底层直觉,而不是被一堆节点连成黑盒。
当然,如果你在URP项目里,核心思路完全一致,只需要把CGPROGRAM切换成HLSLPROGRAM,把UnityCG.cginc换成Core.hlsl或Lighting.hlsl即可。后面我会在工程落地部分专门讲一次迁移要点。
3. 核心Shader实现:让河水“活”起来的三个阶段
3.1 第一阶段:UV滚动制造基础运动
所有风格化流体的第一课都是UV滚动。道理很简单:给采样贴图的UV坐标加上一个随时间线性偏移的量,画面上的纹理会沿着某个方向持续平移,大脑就会把它解读为“流动”。在Cg里最基础的一段代码是这样的:
cg复制float speed = _Time.y * _FlowSpeed;
float2 flowUV = uv;
flowUV.x += speed;
fixed4 flowTex = tex2D(_MainTex, flowUV);
_Time.y是Unity内置的时间变量,单位是秒,乘以速度系数后即可让UV沿X方向前进。这里有个几乎所有人都会忽略的细节:_Time.y会一直增大,当运行时间超过几十秒后,浮点精度会下降,导致UV出现肉眼可见的抖动甚至卡顿。所以正式项目里我会在偏移量外套一层frac(),把时间偏移约束在0到1的周期里循环:
cg复制float2 flowUV = uv + float2(frac(_Time.y * _FlowSpeed), 0.0);
如果你的流动纹理本身是平铺的,frac循环以后看起来和持续增长没有任何区别,但精度问题会彻底消失。这是我在移动设备上复现时踩到的第一个坑,后面还会细说。
3.2 第二阶段:噪波扰动打破规律
只做UV滚动的话,河面就像一条传送带,太规则,缺少风格化手绘的那种“呼吸感”。这时需要引入噪波扰动:用一个透贴噪声的采样结果作为偏移量,加回到主纹理的UV坐标上,让原本直线流动的纹路产生扭曲。扰动代码通常长这样:
cg复制float noise1 = tex2D(_NoiseTex, uv * _NoiseScale + float2(frac(_Time.y * _NoiseSpeed), 0)).r;
float offset = (noise1 - 0.5) * _DistortionStrength;
fixed4 flowTex = tex2D(_MainTex, flowUV + float2(offset, offset * 0.5));
这里的关键参数是_DistortionStrength,我强烈建议控制在0.02到0.1之间。数值过小看不到扭曲,数值过大则会让纹理碎成一块块马赛克。另外,噪声采样自身的UV也必须随时间变化,否则扰动是静止的,跟流动纹路结合起来反而会像“脏东西贴在河面上”。
更进阶一点的做法是用两张噪声叠加,一张频率低、振幅大,控制大波浪;另一张频率高、振幅小,制造颗粒和细碎感。两张噪声的采样频率不要设成整数倍,比如一张用uv * 1.0,另一张用uv * 1.7,这样重复感会明显下降。
3.3 第三阶段:透明度分层与边缘高光
《哈迪斯1》的冥河明显不是一块不透明的色块,岸边的河水几乎透明,只有河中心能看出明显的流体厚度。这种效果我通过两个步骤实现:第一步用UV的V方向做渐变遮罩,让河岸两侧透明度收敛到接近0;第二步用视角方向跟法线方向的夹角做菲涅尔边缘光,让河岸轮廓和角色经过的区域产生一圈亮边,模拟“发光液体”的感觉。
核心代码如下:
cg复制float edgeFade = smoothstep(0.0, 0.3, i.uv.y) * smoothstep(1.0, 0.7, i.uv.y);
float fresnel = pow(1.0 - saturate(dot(worldNormal, viewDir)), 2.0);
float alpha = baseAlpha * edgeFade + fresnel * _EdgeIntensity;
这里edgeFade负责控制平面上的左右边界,fresnel负责在视角低角度掠过河面时增强亮度和透明度,两者叠加后,河面会有一个“液体微光”的轮廓感。原版那种细窄的亮线描边,很大程度就是靠菲涅尔加上一层高亮色叠出来的。
3.4 完整Shader代码(内置管线Cg)
下面是我在项目中使用并调优过的完整Shader,去掉了业务耦合,保留了核心效果。你可以直接复制到Unity里挂到Plane上测试:
cg复制Shader "TA/StylizedRiverStyx"
{
Properties
{
_MainTex ("Flow Texture", 2D) = "white" {}
_NoiseTex ("Noise Texture", 2D) = "white" {}
_DeepColor ("Deep Color", Color) = (0.03, 0.02, 0.10, 1)
_MidColor ("Mid Color", Color) = (0.12, 0.32, 0.55, 1)
_GlowColor ("Glow Color", Color) = (0.45, 0.85, 0.95, 1)
_FlowSpeed ("Flow Speed", Float) = 0.25
_NoiseScale ("Noise Scale", Float) = 1.2
_NoiseSpeed ("Noise Speed", Float) = 0.08
_Distortion ("Distortion", Range(0.0, 0.2)) = 0.05
_EdgeIntensity ("Edge Intensity", Range(0.0, 4.0)) = 1.5
_GlowIntensity ("Glow Intensity", Range(0.0, 2.0)) = 0.8
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" }
Blend SrcAlpha OneMinusSrcAlpha
ZWrite Off
Cull Off
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _NoiseTex;
float4 _NoiseTex_ST;
fixed4 _DeepColor;
fixed4 _MidColor;
fixed4 _GlowColor;
float _FlowSpeed;
float _NoiseScale;
float _NoiseSpeed;
float _Distortion;
float _EdgeIntensity;
float _GlowIntensity;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
v2f vert (appdata v)
{
v2f o;
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.vertex = UnityObjectToClipPos(v.vertex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// 时间精度处理
float timeOffset = frac(_Time.y * _FlowSpeed);
float noiseTime = frac(_Time.y * _NoiseSpeed);
// 大尺度 UV 滚动
float2 flowUV = i.uv + float2(timeOffset, 0.0);
// 噪声扰动
float noise1 = tex2D(_NoiseTex, i.uv * _NoiseScale + float2(noiseTime, 0.0)).r;
float noise2 = tex2D(_NoiseTex, i.uv * _NoiseScale * 1.7 - float2(noiseTime * 1.3, 0.0)).g;
float offset = (noise1 + noise2 - 1.0) * _Distortion;
// 两层不同速度的流动采样,增加层次
fixed4 colA = tex2D(_MainTex, flowUV + float2(offset, offset * 0.5));
fixed4 colB = tex2D(_MainTex, i.uv * 1.4 - float2(timeOffset * 0.6, 0.0) + float2(offset * 0.8, 0.0));
// 河岸纵向淡出
float edgeFade = smoothstep(0.0, 0.3, i.uv.y) * smoothstep(1.0, 0.7, i.uv.y);
// 菲涅尔边缘光
float3 worldViewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
float ndv = saturate(dot(normalize(i.worldNormal), worldViewDir));
float fresnel = pow(1.0 - ndv, 2.0);
// 色彩混合:暗色作为基底,主色通过flowMask占主体,亮色负责描边
float flowMask = saturate(colA.r * 1.2 + colB.g * 0.4);
float glowMask = saturate(colA.a + fresnel * _EdgeIntensity);
fixed3 finalColor = lerp(_DeepColor.rgb, _MidColor.rgb, flowMask);
finalColor += _GlowColor.rgb * glowMask * _GlowIntensity;
finalColor = lerp(finalColor, _DeepColor.rgb, (1.0 - edgeFade) * 0.5);
float alpha = (flowMask * 0.5 + fresnel * _EdgeIntensity * 0.6) * edgeFade;
alpha = saturate(alpha);
return fixed4(finalColor, alpha);
}
ENDCG
}
}
}
这个Shader放到场景里的第一眼效果,通常就已经有六成“哈迪斯味”了。剩下的四成靠的是调整参数和叠加场景元素。
4. 风格化增强:漩涡、波形与多层的组合玩法
4.1 局部漩涡:用旋转UV模拟回旋水流
原版冥河最迷人的地方,是河面上时不时出现的螺旋状小涡流。它们不是粒子,而是区域性的UV旋转。做法是:先定义一个圆心和一个半径,把当前UV坐标换算成相对于圆心的向量,然后用旋转矩阵旋转这个向量,再把旋转后的坐标加回圆心作为新的采样坐标。
cg复制float2 center = float2(0.5, 0.5);
float2 delta = uv - center;
float radius = length(delta);
float angle = _Time.y * _VortexSpeed;
float2x2 rot = float2x2(cos(angle), -sin(angle), sin(angle), cos(angle));
float2 vortexUV = center + mul(rot, delta);
float mask = 1.0 - smoothstep(0.0, _VortexRadius, radius);
float2 mixedUV = lerp(uv, vortexUV, mask * _VortexStrength);
这个方法并不适合全河面使用,而是应该配合世界坐标或者一张控制贴图,把漩涡限定在几个关键位置。比如河水转弯处、地块出口、桥洞旁边。否则满屏都是乱转的小圈,视觉噪音会非常大。
4.2 顶点起伏:什么时候该用,什么时候别用
如果你做的是侧视角河流,比如横版游戏里的瀑布和小溪,那顶点波浪一定必不可少。做法是在顶点阶段给模型Y轴加上一个正弦波偏移:v.vertex.y += sin(v.vertex.x * _WaveFreq + _Time.y * _WaveSpeed) * _WaveHeight。但《哈迪斯1》是俯视角游戏,相机几乎垂直看向地面,顶点在Y轴上的起伏传到屏幕里几乎是不可见的,反而会破坏阴影和碰撞感知。
所以在这个项目里我没有在顶点阶段做起伏,而是把“波浪感”转移到了片元阶段:通过噪声扰动让高亮纹路不断变形,再叠加一个低频的高光扫动,模拟水面起伏反射。如果你确实需要俯视角也能看到的波浪,可以尝试把UV的V方向采样位置也加一点扰动,让每一行亮纹出现轻微的“S形”弯曲,视觉效果会比顶点起伏更自然。
4.3 多层纹理叠加:深色层、发光层、浪花层
《哈迪斯1》的冥河绝不是一张贴图铺到底,而是至少三层信息同时混合。我把它们拆解成下表,并把每个图层对应的Shader控制项写清楚:
| 图层 | 贴图/采样来源 | 主要作用 | 关键参数 | 动画速度 |
|---|---|---|---|---|
| 深色基底层 | 主纹理R通道、暗色Color | 压住画面底部,形成河水的体量感 | _DeepColor、flowMask | 慢 |
| 发光主色层 | 主纹理G通道、中层Color | 表达流体主体颜色与流向 | _MidColor、flowSpeed、_NoiseScale、_Distortion | 中 |
| 高光浪花层 | 主纹理A通道、噪声A通道、菲涅尔 | 边缘亮线、颗粒闪光、描边 | _GlowColor、_GlowIntensity、_EdgeIntensity | 快 |
每一层都要有独立的运动速度。慢层负责让眼睛捕捉到“整体流向”,中层负责拉住视觉停留,快层负责瞬间的视觉刺激。如果没有速度差,三层融合后观感上就是一潭死水。我调试时习惯把三层速度比值先固定在1:2.5:6左右,然后再根据关卡长度微调。
5. 工程落地与性能注意:这效果不是只能活在编辑器里
5.1 半透明队列与Overdraw控制
这个Shader最终一定会挂在透明队列里,所以有几个细节是必须注意的。首先是ZWrite Off,否则半透明物体会把深度缓冲写乱,导致后续其它透明物体排序异常。其次,河流这类大面积半透明物体会产生很高的Overdraw,如果整条河都用全屏多边形而不是沿河岸轮廓制作,那屏幕上有三分之一甚至一半的像素都会被着色器重复绘制。
解决办法有两个:一个是把河面网格尽量贴合河道形状,不要让Plane覆盖到不可见的区域;另一个是在Shader里尽早使用clip或discard剔除alpha接近0的像素,比如在采样完噪声和遮罩后,先判断alpha低于0.03就直接丢弃,能省下后续多次颜色混合的开销。我在移动端实测,这样能降低10%到20%的像素填充压力。
5.2 移动端与URP的迁移要点
如果要在移动端跑这套Shader,关键是减少采样次数。原始版本里我用了三层纹理叠加,在低端机上发热明显。优化时我把三层采样改为两层主纹理采样加一层噪声,同时关闭了高精度浮点要求,改到half精度,帧率立刻上来了许多。
如果项目已经升级到URP,只需要做几处机械替换:把CGPROGRAM换成HLSLPROGRAM,UnityCG.cginc换成Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl,UnityObjectToClipPos换成TransformObjectToHClip,UnityObjectToWorldNormal换成TransformObjectToWorldNormal,_WorldSpaceCameraPos换成GetCameraPositionWS()。其余数学逻辑完全不变。这就是手写Shader的好处,管线迁移时思路好迁移,不像Shader Graph连线扯不清。
5.3 材质实例化和合批的平衡
大量河流块如果共用一个材质,可以做静态合批和动态合批,但如果每个河流块需要不同的颜色或流速,复制材质又会打断合批。我推荐的做法是:颜色和流速差异通过模型顶点色传入_ColorScale和_SpeedScale,而不是实例化材质。在Shader的顶点阶段读取COLOR属性,用顶点色对最终颜色和流速做乘法,这样每个网格体可以长得不一样,但材质还是同一个,合批照常进行。
cg复制float speedScale = v.color.r * 1.8 + 0.2;
float colorTint = v.color.g;
这种把美术表现参数下沉到顶点色的做法,在TA工作流里非常实用,风格化渲染大概率都要用到。
6. 踩坑记录:从“能跑”到“像”要处理的五个问题
6.1 河道边界穿帮
第一版Shader挂上去以后,最明显的问题是河道边界太假。Plane边缘直接切在场景地面上,透明度的突变暴露了多边形轮廓,看起来像一块玻璃碎片插在土里。后来我做了两件事解决:第一,重做了河道网格,让网格边缘距场景可行走区域保留一定宽度;第二,把edgeFade从单纯的UV渐变改成“UV渐变+世界坐标到河道中心线的距离渐变”,并压低边缘alpha到0.1以下。这样即使网格切得不太规整,视觉上也能平滑融入场景地面。
6.2 时间浮点精度造成的画面抖动
这是移动端复现时的老大难问题。Unity内置时间变量_Time.y累积到几分钟后,精度下降,UV坐标出现步进感,整个河面每半秒跳一次。排查了很久,最后在代码里所有时间变量都包上frac(),问题消失。这个坑非常隐蔽,因为PC端编辑器里一小时也看不出明显异常,但在长时间运行的移动端上几乎必然出现。所有涉及_Time的算式中,我都建议保留周期约束,这也是我在这个项目里学到的最实用的一课。
6.3 无缝纹理的平铺可见重复
做长条河道时,平铺次数一旦超过5次,纹理的周期性一眼就能看出来。我换了类似原版的流动笔触纹理后,周期性依然存在,只是不那么刺眼。后来引入了第二层纹理的速度错开,重复感基本被打散。如果你的河道特别长,还可以考虑用世界坐标XZ作为采样UV的基础,而不是模型UV,这样就能让纹理在场景坐标下连续平铺,彻底规避模型拉伸带来的变形。
6.4 高亮过曝与颜色空间差异
初版我在Gamma空间下调色,到了Linear色彩空间项目里,高亮部分严重过曝,白色亮线糊成一片。原因在于pow和saturate在不同色彩空间中的插值结果差异很大。建议这种特效Shader全部在Linear空间下调参,同时把高光色控制在0.8亮度以内,并加上saturate防溢出。如果你在Gamma空间项目里,需要把颜色值预期调低20%到30%再测试。
6.5 水面与角色的交互感
纯Shader的水面跟角色是没有交互的,角色踩进河里不会有涟漪,看起来仍然缺少一点“融入感”。直接做深度检测交互在移动端不现实,我的替代方案是:在角色脚下同步播放一个极小的涟漪粒子,粒子的颜色和Shader的_GlowColor保持一致,再在Shader里给河面加了一点世界坐标驱动的扰动区域,当角色位置传入时该区域的噪波幅度会短暂增加。这样虽然物理上并不是Shader自己检测碰撞,但视觉反馈已经足够自然,成本也低。
写在最后的一点经验
如果你现在正准备在项目里做类似的效果,我会建议不要一开始就追求全功能:先把UV滚动和颜色分层调到满意,再考虑噪声扰动,最后才加漩涡和边缘光。每加一层都要回放到实际游戏视角里跑一跑,特别关注跟角色、地形和UI的互相干扰。这套Shader的成本非常低,哪怕在低端机上也能跑满60帧,真正的难点反而是美术参数的取舍和游戏性的平衡。至于要不要把整条冥河做得和原版一模一样,我觉得没必要,学到原版在视觉分层和动态节奏上的设计逻辑,比复制一张截图有价值得多。
