最近工作在研究如何在大型世界的植被(主要是草)的渲染,主要考虑下面几个问题:
1.半自动化procedual生成,密度分布,画刷
2.受环境的的影响(风,角色等)
3.LOD和动态加载释放
由于睡觉后突发奇想,想到了一些idea,趁着没有忘记先记下来:
基本思路:billboard+instancing+分区域管理
1.自动生成一般根据地形的高度/坡度来选择和过滤,加上一些随机化方法
密度分布可以:每1x1 (1平方)用一个8位来保存密度,2048x2048的世界,最大需要4M数据。可以压缩存储。如果支持不同的类型,可以用4444,支持四种,最大8M数据。2048x2048这么大的地图,只有四种显然不够,可以分区域,每个区域有不同的palette。在分区域的情况下,很多区域是没有草的,不需要这些数据,那么预计runtime数据在2M~4M之间。 使用密度分布是为了减少保存的数据(数据包大小),否则每个草都保存位置,即便用uint8nx4数据量是原来的4N倍。
过程化生成的可能不是最好,需要美术手动修,用画刷。画刷实现不难,略。
2.环境影响:目前的目标平台,不考虑geometry shader,可以在vertexshader里面做顶点动画。草受环境的影响,unity asset store里有一个package,是用垂直向上的相机渲染displacement normal,来实现区域风(风扇类的,或技能特效),以及和角色的互交
3.动态加载和释放:和问题1一样,需要分区域。指定可视范围,然后加载一定的区块。用加载队列和释放队列管理这些块,每个块可能在两个队列里切换, 更新很快。
数量LOD:前两天一直没想清楚怎么在LOD切换时,高校合并批次(instancing的buffer), 因为如果没有LOD的话,就不需更改数量,动态切换渲染的物体(草)
实际上可以这么做:
instancing用vertex buffer保存位置,假设有两级LOD,LOD0画10颗草,LOD1画5颗。 那么,instancing buffer里前5个元素,是LOD1的五颗,位置均匀随机分布,后5个元素,是LOD0时,与LOD1相差的5颗,位置也要均匀随机分布,且不跟LOD1的重合。
也就是说,低LOD的草,也包含在高LOD级别里面。所以低LOD的buffer,包含在高LOD的buffer里。
这样一来,只要把低LOD的instancing buffer放在最前,按LOD级别依次往后放, 就不需要动态合并批次。用同一个buffer,只不过draw call时的vertex count/instance count不一样,几乎没有runtime update开销,支持任意LOD级别。
最大的开销在加载后的处理,根据密度生成instancing position。比如16mx16m的块,按8位密度值,实际密度比例1:1无缩放,最大密度16x16x256,64k颗草,需要随机化位置, 可能比较耗时,不过可以在IO以后异步处理。
instancing position 的顶点格式可以用int8nX4,因为每个块的位置是共享的,只需要生成块内偏移,加上可见块数量不多,所以显存耗费不大,按上面的数据,单块最大极限64k x 4字节,实际可能要小很多。
后面有时间准备实现一下(目前预研,全是空想,还没写一行代码 囧rz)
可视剔除是按区域做,只要区域可见,就画全部的草,为了避免浪费overdraw,区块的大小可以更小。
看了一下WiiU塞尔达荒野之息的草,方式如下:
1.和场景人物互交,用的是上面提到的一样的方法,渲染normal作为placment。
2.一个块大概1x1或2x2,但没有instancing,而是预先建立好的mesh,这样的mesh每个叶子的属性都可以由美术建模是确立
3.大部分草叶没有用alphatest, 其他的带形状的有,但看起来PS里没有任何采样,是在VS里采样(VTF)后穿给PS做test和discard
4.草的高度也是在vertex shader里采样得到的,大部分纹理采样都在vertex shader里
更新 2017.08.19 今天想到的问题点都备忘一下,省得后面实现的时候忘记:
首先看了一下在Unity下如何实现:
Unity的terrain自带了detail物体,然而不支持自定义shader,所以只能pass。如果支持自定义shader,那么需求折衷一下或许可以更简单的利用terrain的detail来实现
这里有几个问题参考: 基本都是要做定义shader的,比如tesselation, toon shading:
http://answers.unity3d.com/questions/414458/tessellation-shaders-on-terrain-detail-object.html
http://answers.unity3d.com/questions/583378/unity-terrain-detail-mesh-toon-shading.html
http://answers.unity3d.com/questions/1173078/is-it-possible-to-change-shader-of-terrain-details.html
答案里的方法没有试过。因为即便可以,也没有官方文档说明和支持,可能会坑。。
不使用terrain detail的实现:
1.如果用Unity的static batch自动合并,需要GameObject,对于大量的草来说,就要大量的gameobject。这种方式太浪费。所以最好用DrawMeshInstanced直接画。
2.用DrawMeshInstanced可以,但是每次传入的矩阵数组/列表,估计每次draw call在native端都要创建/更新vertex buffer,至少需要一次memcpy,如果是PC平台,还需要system memory走PCI到video memory。
理论上只需要创建一次vertex buffer,后面只更新instance count就可以。但是unity没有提供这样的功能,而且vertex format不能自定义,前面分析的某些优化无法实现。好在DrawMeshInstancedIndirect是类似的方式,创建好buffer后面就不用修改,看了一下这个方法metal/GLES3是支持的。可以考虑试一下效果。
---更新:unity使用了constant/uniform buffer来设置instance matrices,而没有用instainced顶点数据。所以每次不同的instanced draw call都会绑定uniform。
3.不管是DrawMeshInstanced 还是DrawMeshInstancedIndirect, Unity文档都说了不支持可见性剔除(culling),Unity提供的方案是在DrawMeshInstancedIndirect里用GPU剔除。。个人认为目前最好的方法是自己管理。用四叉树是很简单也很快的(unity的地形detail应该直接用了terrain的quadtree,然而前面分析了不可用)。可能存在的问题是还是比较浪费,比如2048x2048的地形,如果按最小cell为8x8,那么quad tree的叶子节点就有256x256=64K个。如果是超大地形范围,由多个2048的terrain拼接,那么更加浪费。
可以考虑的一种方式是:只创建可见范围的quadtree,把他作为view/window,随着角色的移动把内容填充给quad tree。看起来不好实现,因为要实时更新每个quad tree节点,事实上因为草块的规则分布,可以用(二维)数组来做windowing,quadtree里只保存索引。这样只需要更新数组的内容就可以,只要loading的范围比可见范围稍大,double buffer+async填充数组应该没有问题。这种方式quadtree跟streaming和terrain解耦,不需要每个地形一个quadtree,只需要在填充的时候读取对应terrain的草信息就可以。 ---更新:因为quadtree的包围盒也更新,所以需要两份quadtree,这比起大范围全部创建还是小得可以忽略。 ---更新2:这个agressive optimization放在最后再考虑要不要做 ---更新2:现在在运行时更新quadtree,每次递归只需要两次比较就能定位到子节点,所以效率不是问题。
4.关于密度的问题,如果密度值的粒度太大,比如16x16,那么画刷的精度太小,一次只能刷16x16的范围,所以粒度要尽量小。4x4或许可以接受,再小的话,占用的(package)资源会变大
目前第1点和第3点的部分已经实现了。后面会根据分析做进一步的改进。第2点相对比较简单,可以方后面。下面主要完成第3点和第4点。等整个系统差不多了,在完善shading。
如果在Blade上实现(目前只是分析):
1.使用完整quadtree没有问题。因为Blade的空间管理(space subdivision/management)已经是跟streaming和地形解耦的,可以管理(主要负责culling和space querying)地形,和模型,等等。因为利用率的提高,加上草也没有关系。从设计上来讲,streaming不应该和quadtree/octree耦合,同样,把quadtree内置到地形里面,虽然是最常见的实现方法,但是个人认为利用率不高。
2.instancing用的vertex buffer,可以像前面分析的那样,用最优的vertex format,并且不用动态更新instancing buffer。
备忘LOD矩阵的生成方式:
所有LOD共用同一个矩阵数组,每一级LOD的矩阵都包含前面的内容:
LOD3 绘制前8个实例,
LOD2绘制前8+15个,LOD1绘制前8+15+30个,LOD0绘制全部
LOD3 |
LOD2 |
LOD1 |
LOD0 |
|
矩阵数组 |
8 |
15 |
30 |
60 |
目前在Unity上的运行结果:
PC上耗时0.3~0.5ms,即使可见距离为128性能也没有太大损失。
在IPHONE7S上测试,20米的可见距离,耗时5ms (60FPS=>45FPS)。为了避免LOD数量跳变导致的不平滑,本来每级LOD数量减半,改为每级减少1/4。现在可能要改回去了。 需要继续优化效率,同时调整距离和密度分布。对于更远处的草,可能要像塞尔达一样用换一种方式绘制了
细节更新: 2017/09/14
关于草的模型,目前还没有优化,有1000个面,后期会做优化。
模型max在导入到unity会有node旋转,可能还会有缩放。考虑直接使用mesh,不适用导入的gameobject的矩阵,这样就只需要一个位置float4。
但是因为Unity的接口是matrix4x4,所以会有浪费和效率损失,不过多出来的3个float4后期会做切割和燃烧等per Instance效果。
遇到一个坑是,unity的matrix是column major,shader里面想直接取行相加,即 + matrix[3], 那么就要在script设置matrix的row 3, SetRow(3, xxx), 但是发现只有原点附近的可见时,草才可见。怀疑是Unity在绘制前拿instance matrices的位置做了简单的culling。但是Unity文档里说,DrawMeshInstanced没有culling,WTF? https://docs.unity3d.com/2017.2/Documentation/ScriptReference/Graphics.DrawMeshInstanced.html
Use this function in situations where you want to draw the same mesh for a particular amount of times using an instanced shader. Meshes are not further culled by the view frustum or baked occluders, nor sorted for transparency or z efficiency.
目前只能SetColumn(3, xxx) 同时SetRow(3, xxx), shader里面只用第3行 ( [0,3] )就可以, matrix multiply 变为加法,减少3个指令。。
2017/09/15
因为instance matrix只用了偏移,这时候里面可以填其他数据,而不占用额外的per instance uniform array。比如草的割断,matrix[0][0]存放一个标记,vertex shader里用分支判断,修改顶点位置就可以了。。
整体效果和切割效果还有点丑,后面慢慢改进。。。
更新:
四叉树的内存问题: 使用按需动态创建的方式,只跟草的可视范围相关。 因为四叉树结点频繁创建/销毁,需要使用object pool避免频繁的gc alloc,本身草的处理也是这样。
instance matrix里的位置坐标问题: 发现所有unity shader汇编都用的column major,所以pos + matrix[3], 反而是需要transpose的(多了三条mov指令),因此用列存储就可以
草的边缘过渡:在有草和没有草的地方,显得非常突兀。根据临接块的密度,计算一个渐变值,在shader里面做linear scale。 linear scale的参考中心,未必在块的中央,有可能在边缘,是根据邻接块的密度差来算的。
这是边缘的过渡效果
明暗效果是在instance matrix里存一个随机值,但是那样只能一个instance一种明暗,所以在顶点色里也存储了一个随机值,两个随机值叠加得到最终的亮度
另外,草的根部颜色会变暗,这个可以根据模型空间顶点的高度计算
更新:
草的高光
边缘高光希望实现类似塞尔达的效果。同时边缘高光跟是视角和光照方向相关,比如光从左边照时边缘的高光在叶片左边,相反时则在右边;
左边边缘
右边边缘
一开始想到的方法就是用顶点切线,并不是法线贴图的TBN切线,而是手刷或者脚本刷的边缘朝外的切线, 当然也可以用TBN的切线,但是因为要根据UV算,所以UV要布成水平/垂直的网格。
但是目前的模型根本没有贴图,就是纯计算的颜色,所以使用的方法是刷了边缘的方向信息,可以理解为边缘的mask。
而上面的图实际上是计算了普通的高光,然后再用顶点色作为mask在pixel shader里mask掉。备忘note:为了效率,所有的光照都在vertex里算,类似vertex lit,但这个mask因为需要渐变所以只能插值后在pixel shader里用
计算mask的模型的顶点色,占一个通道,可以刷在顶点的alpha通道里。可以用maxscript来刷,左边刷0, 右边刷1。或者左边刷1,右边刷0这样子 ,左、右以正面(法线)为参考
普通高光
只加了叶片的边缘高光以后,效果很丑,很散,没有成片成范围的高光效果。所以普通的高光也要加上。所以最终结果是mask后的边缘高光 和另一个高光混合(lerp)。两个高光的gloss和强度可以分开调整。
遗留问题
目前两个方向的边缘高光是跳变(瞬间切换)的,虽然不仔细看是注意不到的。。后面会考虑修正
压倒效果
做法和塞尔达类似,用一个proxy mesh渲染法线然后用VTF采样偏移顶点。这种方式的好处是各种效果都可以统一做,比如塞尔达里武器挥动对草的影响,或者那个芭蕉扇扇风的效果。
为了优化,rendertarget的格式为5551 (unity的ARGB1555),大小为256x256,有效范围为50米,这样以来带宽的开销就比较低了,法线的精度也可以接受。 还可以考虑继续降低分辨率。
因为渲染的是world space normal (0~1),所以clear color 设置为(0.5,1,0.5)即对应的vector为(0,1,0)。需要注意的问题是,直接设置为这个值是不对的,结果错了。。unity会把他当作sRGB空间的颜色值,正确的做法是:
cam.backgroundColor = (new Color(0.5f, 1, 0.5f, 1)).gamma; //convert vector to color (linear to sRGB)
然后渲染法线时就可以直接把世界空间法线从[-1,1]映射到[0,1]写入rendertarget。 这个方式和dx11 geometry shader的demo (https://www.assetstore.unity3d.com/en/#!/content/36335)略微不同,但原理类似。
暂时没有其他的问题,结果如下
对了,vertex里面采样法线的时候,模型上每片叶子上的所有顶点需要采样同一个位置,否则叶片上的每个顶点的偏移程度不一样,叶子会被拉伸变形。
目前使用的是每片叶子的根部来采样,根部的坐标刷在顶点色里,只需要xz,可以认为固定值(0),因为草叶的缩放/切割已经用到了(根部相当于每片叶子的pivot,可以实现每片叶子的单独缩放而不是整体模型的缩放),所以这里直接用。
根部坐标的顶点色在max脚本里刷,需要每个叶片是一个element,这样按每个element取到最低的顶点为根部。
后面可能加上脚印压痕的残留效果。
更新
ARGB1555格式的兼容性并不好,现在改为ARGB32
即便在win10 + dx11的系统上,发现GTX1080显卡支持该格式(驱动版本388.13,D3D feature level 12_1),另外一台GTX1080TI (驱动版本388.00, d3d feature level 11_1) 的机器竟然不支持。。。可能是驱动太旧了。。。
报错如下“RenderTexture.Create failed: format unspported 6”
这里有关于1555格式的兼容性文档: https://msdn.microsoft.com/en-us/library/windows/desktop/ff471325(v=vs.85).aspx
DXGI_FORMAT_B5G5R5A1_UNORM
Requires DXGI 1.2 or later. DXGI 1.2 types are only supported on systems with Direct3D 11.1 or later.
这个格式以前在GLES3.0的手机(高通adreno330)上测试过,作为普通texture(GPU read)没遇到问题,不过没有测试过作为rendertarget的情况。
RGB565 也可以考虑,但是alpha通道可能会用来做强度的时间渐变,为了兼容性还是暂时选择ARGB32,优化的话后面再考虑
细节更新:
草压倒的buffer用的是法线,草的vertex shader里采样(VTF)采样法线并做偏移。对应的mesh是这样(从上面unity dx11 grass的package里面拿的,塞尔达也是类似的方式)
可以看出越靠近中心点,发现越往外偏,那么草的倒幅就越强。边缘的法线基本朝上,草就不怎么倒了。
buffer的preview如下:
改进: 倒下的草为辐射状,太整齐和规律,希望能够随机一点
想到的方法有两种:
1. 给上面的mesh加上normal map
2.使用noise texture,给xz方向加上随机编译
目前选用的方法2。因为方法1需要美术给mesh要加uv和法线贴图,使制作流程更加复杂。更重要的的随机性是固定的,不管角色走到哪里都一样。 而方法2没有这样的问题,比如目前的noisetexture 是32x32,直接在脚本里生成,shader里面tiling的距离是4米,也就是说在4m内走动,扰动的方向都不一样。
加了扰动(-90~90的随机旋转)后的buffer preview如下:
改进:草倒下后起来的延迟(脚步痕迹)
unity那个dx11 grass demo里面是手动创建的轨迹mesh,跟随移动物体的脚步,随着物体移动而补充三角形,同时根据时间删除掉超时的三角形。用类似的方法把法线追加到buffer里。
我认为这个方法不是很好,因为它需要动态修改vertex buffer。 而且这个方法和移动物体的数量相关。物体越多,效率越低。而且demo里删除三角形时是立即删除,并没有过渡,看起来被压倒的草,过一会儿突然恢复原状了
想到的方法是把normal buffer做blend,blend权重随着时间变小,草的倒伏就弱。把这个权重写入normal buffer的alpha 通道就可以控制。
遇到的问题
1.一开始尝试直接使用alpha blend,但发现直接用alpha blend 是不行的(虽然所有的结果都可通过blend op和blend factor来完美控制),但是。。。渲染normal buffer的相机也在移动,上一帧的结果无法直接用alpha blend到这一帧。需要两个buffer,一个保存上一帧的结果,在绘制移动物体物体的normal之前,先用reprojection,将当前帧每个像素对应的位置,reproject到上一帧的对的位置,采样上一帧后写入到当前帧里。(类似TAA的history buffer及其采样), 然后再把当前帧里物体的法线的draw 上去
reprojection shader大致如下
const half4 normalUp = half4(0, 0, 1, 0) * 0.5 + 0.5;
float4 prevPos = mul(_GrassDisplacementFadeReprojection, i.clipPos);
//prevPos /= prevPos.w; //orthographic projection doesn't need persepctive divide
#if UNITY_UV_STARTS_AT_TOP
prevPos.y *= -1;
#endifhalf2 uv = prevPos.xy;
uv = uv * 0.5 + 0.5;half4 n = tex2D(_MainTex, uv.xy); //sample previous frame
//blend / fade
half3 blendNormal = lerp(normalUp, n.rgb, n.a);return half4(blendNormal, _oneMinusFadeSpeed * n.a);
其中_oneMinusFadeSpeed 是一个小于1的tweak值,所以随着每帧的迭代,alpha会越来越小并趋近于0,也就是草倒下的幅度越来越小
2.上面的方法发现效果并不明显,因为每两帧里物体移动的偏移量很小,当前帧的内容覆盖掉了上一阵的绝大部分内容。虽然上一帧的内容有一丝残留,但是残留的法线都是mesh的边缘,注意“边缘的法线基本朝上”,所以上一帧残留下来的法线强度太太弱,肉眼看不出什么效果。
两个圆为两帧之间的位置,B部分为重叠部分,但是因为当前帧的法线后画上去,所以覆盖掉了上一帧的内容。红色的A部分为上一帧的残留(位置在边缘,偏移强度很低),所以在角色移动时,拖尾的部分一直都是上一帧的边缘部分,都是很弱的,看不到效果。
目前的解决方法:
将重叠的B部分做blend合并,blend 方法为max。上图中重叠的黄色部分,取两帧之间偏移强度最大的值。这么做相当于把当前帧里物体边缘较弱的法线偏移给去掉了,而采用上一帧较强的偏移。
然而仍然不能用alpha blend 和对应的max op,因为存储的向量,可以为负,所以max比较的是绝对值。
具体方法:再追加一个buffer, 把reprojection 和 当前帧用到的两个buffer分开,在shader里合并。 代码大致如下: (x,y,z对应世界空间法线的xzy)
half4 n0 = tex2D(_MainTex, uv.xy);
half4 n1 = tex2D(_LastTex, uv.xy);half fade = max(n0.a, n1.a);
n0 = n0 * 2 - 1;
n1 = n1 * 2 - 1;half3 n;
if (abs(n0.x) >= abs(n1.x))
n.x = n0.x;
else
n.x = n1.x;if (abs(n0.y) >= abs(n1.y))
n.y = n0.y;
else
n.y = n1.y;if (abs(n0.z) <= abs(n1.z))
n.z = n0.z;
else
n.z = n1.z;return half4(n * 0.5 + 0.5, fade);
最终效果已经达到预期,被压下去的草在角色离开后开始慢慢恢复原状,速度可调控。目前感觉不是最高效的方法,后面会考虑改进。
更新: 三个轴独立的max结果是不对的,会出现类似(n0.x,n1.y,n0.z)的向量。直接使用水平面上投影长度(的平方)比较就可以,另外比较时还要乘上现在的强度(alpha)
half4 n0 = tex2D(_MainTex, uv.xy);
half4 n1 = tex2D(_LastTex, uv.xy);half fade = max(n0.a, n1.a);
half2 n02 = (n0 * 2 - 1) * n0.a;
half2 n12 = (n1 * 2 - 1) * n1.a;half3 n;
if (dot(n02, n02) >= dot(n12.xy, n12.xy))
n = n0;
else
n = n1;return half4(n, fade);
更新:上面的方式草的回弹效果不好,太快。调节速度以后,拖尾又觉得太长太慢,原因是max()方式会导致历史脚印的中心(强度最大)只有脱离了当前mesh范围才开始渐变,而实际上mesh本身就有曲线,max方法会把mesh自身的渐变去掉。 目前的方法是不使用max方法,而把模型最边缘的没有偏移(法线朝上)的部分去掉。这样可以得到需要的效果。
高度问题:
上面的方法没有加入高度,也就是说,即便角色在空中,地面的草也会倒。解决方法是用角色位置向下做raycast,得到的h,除以草的高度H,即strength = clamp(h/H, 0, 1)作为强度调节系数,为1时强度最弱,0时强度最强。当然这种效果是对草的直接影响,反应过快,不像上面可以有延迟效果,不过可以对strength这个参数做一些hack,使他看上去好像有延迟。
主相机对草的碾压:
因为相机贴地时,视线会被草挡住,所以为了更好的效果,可以给相机加一个displacment mesh:
(突起部位靠近相机)
从这个模型的法线可以看到,离相机近的地方草被拨开,然后随着距离变大,强度慢慢减弱。顶视图上看拨动草的范围是一个尖三角形
更新:相机用的displacement mesh改用其他模型了,但是大致原理如此。角色的模型也改回去了,因为有其他问题,但是改进一下算法就可以了。
Fresnel
给草的高光加上fresnel (类似rim light)之后,平视的时候更有层次感(只要地形稍微有点起伏,就能看到层次差别),不过像塞尔达那样,只有草的尖部是亮的。另外,为了使相机稍微往下看的时候有也有效果,可以把向量稍微偏一下。
草的摆动
摆动可以参考这个:GPU Gems3 Chapter 16. Vegetation Procedural Animation and Shading in Crysis
如果查看Unity的builtin shader,TerrainEngine.cginc 可以发现里面的某些实现也是参考上面的文章
更新:
假定波函数为wave( a*t + bx + c),其中a为频率(或频率相关的系数,b为波长倒数,c为初相参数
在频率改变的时候,会导致强烈抖动,因为相位发生跳动, 如果频率改变量是Δf, wave( (a+Δf)*t + bx + c) 相位的参数跳动为:Δf*t0, 其中t0为改变频率那一刻的时间
修正方法:把每次的相位跳动抵消值,-Δf*t0 累加到参数c中
更新2:
远处billboard的波动,假定billboard在viewspace,那么把world space的wave offest转到view space,叠加就可以,当然为了效率可以简化,因为billboard离得比较远:
//note: the more correct way should preserve original length: //float3 vdiff = vpos - vpivot; //float len = length(vdiff); //vpos = vpipot + normalize(vdiff + wave)*len; //but since it's far low LOD, some stretch won't be a big problem vpos.xyz += mul((float3x3)UNITY_MATRIX_V, wind.xyz);
草和地形shading的匹配
由于效率的原因,草的shading是在vertex shader里面完成的custom lighting,所以和地形的光照并不匹配,比如向光面或者背光面,两者亮度的差异。
为了表现效果,目前的terrain用的Unity的standrad specular,blinn phong效果太差。为了提高光照的匹配度,原则上讲,需要草和地形的shading使用相同或者近似的shading model,问题就可以解决。
以上思路很简单,大部分都是体力活,简单备忘一下调试流程:
纯色调整(关掉lighting,ambient可选择性打开 先对颜色)--> diffuse 调整 (在shader里关掉specular、ambient,调整shading function以达到diffuse匹配) --> specular调整(关闭/注释掉ambient、diffuse)
最后再把光照打开,lighting应该对应的差不多了
另外,割草时飞的草叶(例如particle),理论上也需要一定程度的匹配(比如使用相同的shader/shader header/shading),避免色差太大。
2018.06.21:
由于反馈的结果太像塞尔达(笑哭), 抄袭痕迹太重(基本就是照着它的效果做的), 后面会加上自己的风格.
因为工作太忙, 暂时不更了