a9一直显示27号,不能显示当天,a9闪退是怎么回事事

我用的移动4G突然变2G了,手机没问题,换个4G卡又显示4G,再换回来还是2G,正常4G已用了两个月,而且刚充过钱,今天就不能用了,显示还有4.3G流量。这是怎么回事?
按时间排序
突然问题就是移动的信号覆盖强度和程度的问题了,一般等些时间或换个地点应该就会好许多的。
您可以邀请下面用户,快速获得回答
擅长领域:&&&&
在手机数码分类下共有80654个回答
擅长领域:&&&&
在手机数码分类下共有22998个回答
擅长领域:&&
在手机数码分类下共有16818个回答
擅长领域:
在手机数码分类下共有8885个回答
擅长领域:&&&&
在手机数码分类下共有7794个回答
weixin_3r654129
擅长领域:
在手机数码分类下共有7269个回答
加载更多答主
感谢您为社区的和谐贡献力量请选择举报类型
经过核实后将会做出处理感谢您为社区和谐做出贡献
点击可定位违规字符位置
确定要取消此次报名,退出该活动?
请输入私信内容:&figure&&img src=&https://pic4.zhimg.com/v2-aa55d25b08f0264ead19_b.jpg& data-rawwidth=&403& data-rawheight=&266& class=&content_image& width=&403&&&/figure&&p&
终于要写帧同步这块了,这块很难讲清楚,细枝末节有很多优化点,也有一些不同的优化方向,根据不同项目类型,对操作手感的要求,对联机玩家的个数等,会有不同的难点和痛点。不同的优化方向,优化手法的差异,可能导致一些争论。并且,帧同步,本身也有很多变种,以应对不同的需求。所以,我一切都是基于我们的项目类型(ACT)来做的方案和优化,并不一定适合其它也需要帧同步的游戏,故在此提前说一下,以免引起一些不必要的误解。&/p&&p&
有些东西,可能会写得详细些,有些可能会简略些,不可能照顾到所有程度的读者,见谅。&/p&&h2&&b&帧同步的几个难点&/b&&/h2&&p&
帧同步的基础原理,以及和状态同步的区别,已经有很多文章介绍,我就不再赘述,大家可以自行google。以下只说几个难点。&/p&&ul&&li&&b&保证客户端独自计算的正确,即一致性&/b&&/li&&/ul&&p&
帧同步的基础,是不同的客户端,基于相同的操作指令顺序,各自执行逻辑,能得到相同的效果。就如大家所知道的,在unity下,不同的调用顺序,时序,浮点数计算的偏差,容器的排序不确定性,coroutine内写逻辑带来的不确定性,物理浮点数,随机数值带来的不确定性等等。&/p&&p&
有些比较好解决,比如随机数值,只需要做随机种子即可。&/p&&p&
有些注意代码规范,比如在帧同步的战斗中,逻辑部分不使用Coroutine,不依赖类似Dictionary等不确定顺序的容器的循环等。&/p&&p&
还有最基础的,要通过一个统一的逻辑tick入口,来更新整个战斗逻辑,而不是每个逻辑自己去Update。保证每次tick都从上到下,每次执行的顺序一致。&/p&&p&
物理方面,因为我们战斗逻辑不需要物理,碰撞都是自己做的碰撞逻辑,所以,跳过不说,这块可以参考别的文章。&/p&&p&
最后,说一下,浮点数计算无法保证一致性,我们需要转换为定点数。关于定点数的实现,比较简单的方式是,在原来浮点数的基础上乘,对应地方除以,这种做法最为简单,再辅以三角函数查表,能解决一些问题,减少计算不一致的概率,但是,这种做法是治标不治本的方式,存在一些隐患(举个例子,例如一个int和一个float做乘法,如果原数值就要*1000,那最后算出来的数值,可能会非常大,有越界的风险。)。&/p&&p&
最佳的解决办法,是使用实现更加精确和严谨,并经过验证的定点数数学库,在c#上,有一个定点数的实现,&a href=&http://link.zhihu.com/?target=https%3A//www.photonengine.com/en-US/Photon& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Photon网络&/a&的早期版本,&a href=&http://link.zhihu.com/?target=https%3A//github.com/Caizc/learn-photon-truesync/tree/master/Assets/TrueSync/Engine/Math& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Truesync有一个很不错的定点数实现&/a&。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-54df1a2a2c4db77fdab09e9_b.jpg& data-size=&normal& data-rawwidth=&669& data-rawheight=&852& class=&origin_image zh-lightbox-thumb& width=&669& data-original=&https://pic2.zhimg.com/v2-54df1a2a2c4db77fdab09e9_r.jpg&&&figcaption&定点数的实现&/figcaption&&/figure&&p&
其中FP,就可以完全代替float,我们只需要将我们自己的逻辑部分,float等改造为FP,就可以轻松解决。并且,能够很好的和我们protobuf的序列化方式集成(注意代码中的Attribute,如下图),保证我们的配置文件,也是定点数的。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-ae47c66a1dbb7ce73207c7_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&437& data-rawheight=&383& class=&origin_image zh-lightbox-thumb& width=&437& data-original=&https://pic4.zhimg.com/v2-ae47c66a1dbb7ce73207c7_r.jpg&&&/figure&&p&TSVector对应Vector3,只要基于FP,可以自己扩展自己的数据结构。(当然,如果用到了复杂的插件,并且不开源,那么对于定点数的改造,就会困难很多)&/p&&figure&&img src=&https://pic3.zhimg.com/v2-de61f21d96bf38aad9a499ee30becd42_b.jpg& data-size=&normal& data-rawwidth=&973& data-rawheight=&288& class=&origin_image zh-lightbox-thumb& width=&973& data-original=&https://pic3.zhimg.com/v2-de61f21d96bf38aad9a499ee30becd42_r.jpg&&&figcaption&三角函数通过查表方式实现,保证了定点数的准确&/figcaption&&/figure&&p&我个人认为,这一套的实现,是优于简单的乘10000,除10000的方式。带来的坏处,可能就是计算性能略差一点点,但是我们大量测试下来,对计算性能的影响很小,应该是能胜任绝大部分项目的需求。&/p&&p&
对于计算的不确定性,我们也有一些小的隐患,就是,我们用到了Physics.Raycast来检测地面和围墙,让人物可以上下坡,走楼梯等高低不平的路,也可以有形状不规则的墙。这里会获得一个浮点数的位置,可能会导致不确定性,这里,我们用了数值截断等方式,尽量规避,经过反复测试,没有出现过不一致。但是这种方式,毕竟在逻辑上,存在隐患,更好的方式,是实现一套基于定点数的raycast机制,我们人力有限,就没时间精力去做了。这块,有篇文章讲得更细致一些,大家可以参看 &a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&帧同步:浮点精度测试&/a&。&/p&&ul&&li&&b&帧同步网络协议的实现&/b&&/li&&/ul&&p&在处理好了基础的计算一致性问题后,我们就要考虑网络如何通信。这里,我不谈p2p方式了,我们以下谈的,都是多client,一个server的模式,server负责统一tick,并转发client的指令,通知其他client,可以参看文章&a href=&http://link.zhihu.com/?target=http%3A//www.10tiao.com/html/255/.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&网游流畅基础:帧同步游戏开发&/a&。&/p&&p&首先,是网络协议的选择。TCP和UDP的选择,我就不多说了,帧同步肯定要基于UDP才能保证更低的延迟。在UDP的选择上,我看网上有些文章,容易导入一个误区,即,我们是要用可靠传输的UDP,还是冗余信息的UDP。&/p&&p&&b&基于可靠传输的UDP&/b&,是指在UDP上加一层封装,自己去实现丢包处理,消息序列,重传等类似TCP的消息处理方式,保证上层逻辑在处理数据包的时候,不需要考虑包的顺序,丢包等。类似的实现有Enet,KCP等。&/p&&p&&b&冗余信息的UDP&/b&,是指需要上层逻辑自己处理丢包,乱序,重传等问题,底层直接用原始的UDP,或者用类似Enet的Unsequenced模式。常见的处理方式,就是两端的消息里面,带有确认帧信息,比如客户端(C)通知服务器(S)第100帧的数据,S收到后通知C,已收到C的第100帧,如果C一直没收到S的通知(丢包,乱序等原因),就会继续发送第100帧的数据给S,直到收到S的确认信息。&/p&&p&
有些文章介绍的时候,没有明确这两者的区别,但是这两种方式,区别是巨大的。可靠传输的UDP,在帧同步中,个人认为是不合适的,因为他为了保证包的顺序和处理丢包重传等,在网络不佳的情况下,delay很大,将导致收发包处理都会变成类似tcp的效果,只是比TCP会好一些。必须要用冗余信息的UDP的方式,才能获得好的效果。并且实现并不复杂,只要和服务器商议好确认帧和如何重传即可,自己实现,有很大的优化空间。例如,我们的协议定义类似如下:&/p&&figure&&img src=&https://pic2.zhimg.com/v2-a79eca856ce184fd70cd80d_b.jpg& data-size=&normal& data-rawwidth=&607& data-rawheight=&499& class=&origin_image zh-lightbox-thumb& width=&607& data-original=&https://pic2.zhimg.com/v2-a79eca856ce184fd70cd80d_r.jpg&&&figcaption&双方都要通知对方,已经接受哪一帧的通知了,并通过cmd list重发没有收到的指令&/figcaption&&/figure&&p&这里简单说一下,对于这种收发频繁的消息,如果使用protobuf,会造成每个逻辑帧的GC,这是非常不好的,解决方案,要么对&a href=&http://link.zhihu.com/?target=https%3A//www.cnblogs.com/SChivas/p/7898166.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&protobuf做无GC改造&/a&,要么就自己实现一个简单的byte[]读写。无GC改造工程太大,感觉无必要,我们只是在战斗的几个频繁发送的消息,需要自己处理一下byte[]读写即可。&/p&&p&-----------------------------------&/p&&p&此处补充一下,看到KCP作者&a href=&https://www.zhihu.com/people/skywind3000/activities& class=&internal&&韦易笑&/a&在评论区的留言,提到&a href=&http://link.zhihu.com/?target=https%3A//github.com/skywind3000/kcp/wiki/Network-Layer& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&kcp+fec的模式&/a&,可以比冗余方式,有更好的效果,我之前并没有仔细研究过这个模式,不过可以推荐大家看一下,如果有用过朋友分享下结论就更好了。&/p&&p&因为我们项目早期,服务器定下了使用enet,我评估了一下,反正使用冗余包的方式,所以没有纠结enet或kcp,后续其实想改成kcp,服务器不想再动,也就放下了。&/p&&p&enet麻烦的地方是,&a href=&http://link.zhihu.com/?target=https%3A//github.com/lsalzman/enet/pull/73& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&enet的ipv6版本&/a&,是一个不成熟的pull request,enet作者没有merge(并且存在好几个ipv6的pull request),我不确定稳定性,还好看了下commit,加上测试下来,没有太大问题。KCP我没有评估过ipv6的问题,不过github上有C#版本,改一下ipv6支持应该很简单。&/p&&p&------------------------------------&/p&&ul&&li&&b&逻辑和显示的分离&/b&&/li&&/ul&&p&这块很多讲帧同步的文章都提过了。我在前面讲&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&技能编辑器&/a&的时候,也提过,配置的数据和显示要分离,在战斗中,战斗的逻辑,也要和显示做到分离。&/p&&p&
例如,最基本,我们动作切换的逻辑,是基于自己抽象的逻辑帧,而不是基于animator中一个clip的播放。比如一个攻击动作,当第10帧的时候,开始出现攻击框,并开始检测和敌人受击框的碰撞,这个时候的第10帧,必须是独立的逻辑,不能依赖于animator播放的时间,或者AnimatorStateInfo的normalizedTime等。甚至,当我们不加载角色的模型,一样可以跑战斗的逻辑。如果抽离得好,还可以放到服务器跑,做为战斗的验证程序,王者荣耀就是这样做的。&/p&&ul&&li&&b&联机如何做到流畅战斗&/b&&/li&&/ul&&p&
前面所有的准备,最终的目的,都是为了战斗的流畅。特别是我们这种Act游戏,或者格斗类游戏,对按键以后操作反馈的即时性,要求非常高,一点点延迟,都会影响玩家的手感,导致玩家的连招操作打断,非常影响体验。我们对延迟的敏感性,甚至比MOBA类游戏还要高,我们要做到好的操作手感,还要联机战斗(PVP,组队PVE),都需要把帧同步做到极致,不能因为延迟卡住或者操作反馈出现变化。&/p&&p&
因为这个原因,我们不能用lockstep的方式,lockstep更适合网络环境良好的内网,或者对操作延迟不敏感的类型(例如我听过还有项目用来做卡牌类的帧同步)。&/p&&p&
我们也不能用缓存服务器确认操作的方式,也就是一些游戏做的指令buffer。具体描述,&a href=&http://link.zhihu.com/?target=http%3A//youxiputao.com/articles/11842& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&王者荣耀的分析文章&/a&,讲得很具体了。这也是他们说的模式,这个模式能解决一些小的网络波动,对一些操作反馈不需要太高的游戏,例如有些游戏攻击前会有一个比较长的前摇动作,这类游戏,用这种方式,应该就能解决大部分问题。但是这种方式还是存在隐患,即使通过策略能很好地动态调整buffer,也还是难以解决高延迟下的卡顿和不流畅。王者荣耀优化得很好,他们说能让buffer长度为0,文章只提到通过平滑插值和逻辑表现分离来优化,更细节的没有提到,我不确定他们是否只是基于这个方式来优化的。目前也没有看到更具体的分析。&/p&&p&
指令buffer的方式,也不能满足我们的需求,或者说,我没有找到基于此方式,能优化到王者荣耀的效果的办法。我也测试过其他moba和act,arpg类游戏的联机,在高延迟,网络波动情况下,没有比王者表现更好的了。&/p&&p&
最后,在仔细研究了我们的需求后,找到一篇指导性的文章,非常适合我们。&/p&&p&
就是&a href=&http://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a&,这篇文章非常详细地介绍了各种方式,最终回滚逻辑(rollback)是终极的解决方案,国内也有文章提到过,即&a href=&http://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a&里面提到的Time Warp方式,我理解回滚逻辑,和Time Warp是一个概念。&/p&&ul&&li&&b&游戏逻辑的回滚&/b&&/li&&/ul&&p&
回滚逻辑,就是我们解决问题的方案。可以这样理解,客户端的时间,领先服务器,客户端不需要服务器确认帧返回才执行指令,而是玩家输入,立刻执行(其他玩家的输入,按照其最近一个输入做预测,或者其他更优化的预测方案),然后将指令发送给服务器,服务器收到后给客户端确认,客户端收到确认后,如果服务确认的操作,和之前执行的一样(自己和其他玩家预测的操作),将不做任何改变,如果不一样(预测错误),就会将游戏整体逻辑回滚到最后一次服务器确认的正确帧,然后再追上当前客户端的帧。&/p&&p&
此处逻辑较为复杂,我尝试举个例子说明下。&/p&&p&
当前客户端(A,B)执行到100帧,服务器执行到97帧。在100帧的时候,A执行了移动,B执行了攻击,A和B都通知服务器:我已经执行到100帧,我的操作是移动(A),攻击(B)。服务器在自己的98帧或99帧收到了A,B的消息,存在对应帧的操作数据中,等服务器执行到100帧的时候(或提前),将这个数据广播给AB。&/p&&p&
然后A和B立刻开始执行100帧,A执行移动,预测B不执行操作。而B执行攻击,预测A执行攻击(可能A的99帧也是攻击),A和B各自预测对方的操作。&/p&&p&
在A和B执行完100帧后,他们会各自保存100帧的状态快照,以及100帧各自的操作(包括预测的操作),以备万一预测错误,做逻辑回滚。&/p&&p&
执行几帧后,A,B来到了103帧,服务器到了100帧,他开始广播数据给AB,在一定延迟后,AB收到了服务器确认的100帧的数据,这时候,AB可能已经执行到104了。A和B各自去核对服务器的数据和自己预测的数据是否相同。例如A核对后,100帧的操作,和自己预测的一样,A不做任何处理,继续往前。而B核对后,发现在100帧,B对A的预测,和服务器确认的A的操作,是不一样的(B预测的是攻击,而实际A的操作是移动),B就回滚到上一个确认一样的帧,即99帧,然后根据确认的100帧操作去执行100帧,然后快速执行101~103的帧逻辑,之后继续执行104帧,其中(101~104)还是预测的逻辑帧。&/p&&p&
因为客户端对当前操作的立刻执行,这个操作手感,是完全和pve(不联网状态)是一样的,不存在任何delay。所以,能做到绝佳的操作手感。当预测不一样的时候,做逻辑回滚,快速追回当前操作。&/p&&p&
这样,对于网络好的玩家,和网络不好的玩家,都不会互相影响,不会像lockstep一样,网络好的玩家,会被网络不好的玩家lock住。也不会被网络延迟lock住,客户端可以一直往前预测。&/p&&p&
对于网络好的玩家(A),可以动态调整(根据动态的latency),让客户端领先服务器少一些,尽量减少预测量,就会尽量减少回滚,例如网络好的,可能客户端只领先2~3帧。&/p&&p&
对于网络不好的玩家(B),动态调整,领先服务器多一些,根据latency调整,例如领先5帧。&/p&&p&
那么,A可能预测错的情况,只有2~3帧,而网络不好的B,可能预测错误的帧有5帧。通过优化的预测技术,和消息通知的优化,可以进一步减少A和B的预测错误率。对于A而言,战斗是顺畅的,手感很好,少数情况的回滚,优化好了,并不会带来卡顿和延迟感。&/p&&p&
重点优化的是B,即网络不好的玩家,他的操作体验。因为客户端不等待服务器确认,就执行操作,所以B的操作手感,和A是一致的,区别只在于,B因为延迟,预测了比较多的帧,可能导致预测错,回滚会多一些。比如按照B的预测,应该在100帧击中A,但是因为预测错误A的操作,回滚重新执行后,B可能在100帧不会击中A。这对于B来说,通过插值和一些平滑方式,B的感受是不会有太大区别的,因为B看自己,操作自己都是及时反馈的,他感觉自己是平滑的。&/p&&p&
这种方式,保证了网络不好的B的操作手感,和A一致。回滚导致的一些轻微的抖动,都是B看A的抖动,通过优化(插值,平滑等),进一步减少这些后,B的感受是很好的。我们测试在200~300毫秒随机延迟的情况下,B的操作手感良好。&/p&&p&
这里,客户端提前服务器的方式,并且在延迟增大的情况下,客户端将加速,和&a href=&http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&守望先锋的处理方式&/a&是一样的。当然,他们肯定比我做得好很多。&/p&&p&
希望我已经大致讲清楚了这个逻辑,大家参看几篇链接的文章,能体会更深。&/p&&p&
这里,我要强调的一点是,我们这里的预测执行,是真实逻辑的预测,和很多介绍帧同步文章提到的预测是不同的。有些文章介绍的预测执行,只是view层面的预测,例如前摇动作和位移,但是逻辑是不会提前执行的,还是要等服务器的返回。这两种预测执行(View的预测执行,和真实逻辑的预测执行)是完全不是一个概念的,这里需要仔细地区分。&/p&&p&
这里有很多的可以优化的点,我就不一一介绍了,以后可能零散地再谈。&/p&&ul&&li&&b&游戏逻辑的快照(snapshot)&/b&&/li&&/ul&&p&我们的逻辑之所以能回滚,都是基于对每一帧状态可以处理快照,存储下每一帧的状态,并可以回滚到任何一帧的状态。在&a href=&http://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a& 文章和&a href=&http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&守望先锋网络&/a& 文章中,都一笔带过了快照的说明。他们说的快照,可能略有不同,但是思路,都是能保存下每一帧的状态。如果去处理快照(Understanding那篇文章做的是模拟器游戏,可以方便地以内存快照的方式来做),是一个难点,这也是我&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&前面文章&/a&提到ECS在这个方式下的应用,云风的解释:&/p&&figure&&img src=&https://pic2.zhimg.com/v2-dd9ed3cab5_b.jpg& data-size=&normal& data-rawwidth=&737& data-rawheight=&87& class=&origin_image zh-lightbox-thumb& width=&737& data-original=&https://pic2.zhimg.com/v2-dd9ed3cab5_r.jpg&&&figcaption&云风博客截图,地址https://blog.codingnow.com/2017/06/overwatch_ecs.html&/figcaption&&/figure&&p&ECS是一个好的处理方式,并且我找到&a href=&http://link.zhihu.com/?target=https%3A//www.kisence.com//guan-yu-zheng-tong-bu-de-xie-xin-de/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&一篇文章&/a&,也这样做了(我看过他开源的demo,做得还不够好,应该还是demo阶段,不太像是一个成型的项目)。这篇文章的思路是很清晰的,并且也点到了一些实实在在的痛点,解决思路也基本是正确的,可以参看。&/p&&p&这块,我做得比较早了,当时守望先锋的文章还没出,我的战斗也没有基于ECS,所以,在处理快照上,只有自己理顺逻辑来做了。&/p&&p&我的思路是,通过一个回滚接口,需要数据回滚的部分,实现接口,各自处理自己的保存快照和回滚。就像我们序列化一个复杂的配置,每个配置各自序列化自己的部分,最终合并成一个序列化好的文件。&/p&&p&首先,定义接口,和快照数据的reader和writer&/p&&figure&&img src=&https://pic2.zhimg.com/v2-c12b7e87d2f452b29fdef61_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&388& data-rawheight=&185& class=&content_image& width=&388&&&/figure&&figure&&img src=&https://pic4.zhimg.com/v2-b666f214abd2a804ebcc119b_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&407& data-rawheight=&499& class=&content_image& width=&407&&&/figure&&figure&&img src=&https://pic4.zhimg.com/v2-2f49a68d6fdbdf3ffdfb283_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&470& data-rawheight=&483& class=&origin_image zh-lightbox-thumb& width=&470& data-original=&https://pic4.zhimg.com/v2-2f49a68d6fdbdf3ffdfb283_r.jpg&&&/figure&&p&然后,就是每个模块,自己去处理自己的takeSnapshot和rollback,例如:&/p&&figure&&img src=&https://pic2.zhimg.com/v2-96bbeff404d_b.jpg& data-size=&normal& data-rawwidth=&400& data-rawheight=&254& class=&content_image& width=&400&&&figcaption&简单的数值回滚&/figcaption&&/figure&&figure&&img src=&https://pic4.zhimg.com/v2-eeec5b5d8e7e663a8e06f487_b.jpg& data-size=&normal& data-rawwidth=&700& data-rawheight=&597& class=&origin_image zh-lightbox-thumb& width=&700& data-original=&https://pic4.zhimg.com/v2-eeec5b5d8e7e663a8e06f487_r.jpg&&&figcaption&复制的列表回滚和调用子模块回滚&/figcaption&&/figure&&p&思路理顺以后,就可以很方便地处理了,注意write和read的顺序,注意处理好list,就解决了大部分问题。当然,在实现逻辑的过程中,时刻要注意,一个模块如何回滚(例如获取随机数也需要回滚)。&/p&&p&有一个更简单的方式,就是给属性打Attribute,然后写通用的方法。例如,我早&b&期的实现方案&/b&:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-05fee97a51c743b54d8dcaf_b.jpg& data-size=&normal& data-rawwidth=&471& data-rawheight=&340& class=&origin_image zh-lightbox-thumb& width=&471& data-original=&https://pic4.zhimg.com/v2-05fee97a51c743b54d8dcaf_r.jpg&&&figcaption&给属性打标签&/figcaption&&/figure&&p&根据标签,通用的读写方法,通过反射来读写,就不需要每个模块自己去实现自己的方法了:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-c2adc282fe621f2357e3_b.jpg& data-size=&normal& data-rawwidth=&649& data-rawheight=&375& class=&origin_image zh-lightbox-thumb& width=&649& data-original=&https://pic4.zhimg.com/v2-c2adc282fe621f2357e3_r.jpg&&&figcaption&部分代码&/figcaption&&/figure&&p&这种方法,能很好地解决大部分问题,甚至前面提到的&a href=&http://link.zhihu.com/?target=https%3A//github.com/suzuke/TrueSync/tree/master/Assets/TrueSync/Engine/Math& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Truesync&/a&,也是用的这种方式来做。&/p&&p&但是这种方法有个难以回避的问题,就是GC,因为基于反射,当我们调用field的GetValue和SetValue的时候,GC难以避免。并且,因为全自动,不方便处理一些特殊逻辑,调试优化也不方便,最后改成了现有的方式,虽然看起来笨重一些,但是可控性更强,我后续做的很多优化,都方便很多。&/p&&p&关于快照,也有很多可以优化的点,无论是GC内存上的,还是运行效率上的,都需要优化好,否则,可能带来性能问题。这块优化,有空另辟文章再细谈吧。&/p&&p&当我们有了快照,就可以支持回滚,甚至跳转。例如我们要看战斗录像,如果没有快照,我们要跳到1000帧,就需要从第一帧,根据保存的操作指令,一直快速执行到1000帧,而有了快照,可以直接跳到1000帧,不需要执行中间的过程,如果需要在不同的帧之间切换,只需要跳转即可,这将带来巨大的帮助。&/p&&ul&&li&&b&自动测试&/b&&/li&&/ul&&p&由于帧同步需要测试一致性的问题,对我们来说,回滚也是需要大量测试的问题。自动测试是必须要做的一步,这块没有什么特别的点,主要就是保存好操作,快照,log,然后对不同客户端的数据做比对,找到不同的地方,查错改正。&/p&&p&我们现在做到,一步操作,自动循环战斗,将每一盘战斗数据上传内网log服务器。&/p&&p&当有很多盘战斗的数据后,通过工具自动解析比对数据,找到不同步的点。也是还可以优化得更好,只是现在感觉已经够用了。经过大量的内部自动测试,目前战斗的一致性,是很好的。&/p&&ul&&li&&b&总结&/b&&/li&&/ul&&p&我们现在的帧同步方案,总结下来,就是预测,快照,回滚。当把这些有机地结合起来,优化好,就有了非常不错的帧同步联网效果,无论网络速度如何,只要不是延迟大到变态,都保证了非常好的操作手感。&/p&&p&快照回滚的方式,也不是所有游戏都适用,例如&a href=&http://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a&文章中对此模式(Time warp或Rollback)的缺点,也说明了。&/p&&figure&&img src=&https://pic3.zhimg.com/v2-fb6abdadfb1ce4ce31ec6_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&651& data-rawheight=&106& class=&origin_image zh-lightbox-thumb& width=&651& data-original=&https://pic3.zhimg.com/v2-fb6abdadfb1ce4ce31ec6_r.jpg&&&/figure&&p&如图所述,这种模式不适合太多人的联网玩法,例如MOBA,可能就不太适用。我们最多三人联机,目前优化测试下来,效果也没有太大问题。但是联机人数越多,预测操作的错误可能性越大,导致的回滚也会越多。&/p&&p&&br&&/p&&p&一篇文章,难以讲得面面俱到,很多地方可能描述也不一定明确,并且,个人能力有限,团队人员有限(3个客户端)的情况下,必定有很多设计实现不够好的地方,大家见谅。后续有空,再补充一些细节和优化等,这里同样是希望抛砖引玉,希望看到更多好的方案。&/p&&p&最后,也要感谢我们项目的服务器哥们的技术支持和帮助。&/p&&p&一些有帮助的文章再列一下:&/p&&ul&&li&&a href=&http://link.zhihu.com/?target=http%3A//mauve.mizuumi.net//understanding-fighting-game-networking/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Understanding Fighting Game Networking&/a& &/li&&li&&a href=&http://link.zhihu.com/?target=http%3A//www.skywind.me/blog/archives/1343%23more-1343& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Skywind Inside & 再谈网游同步技术&/a& &/li&&li&&a href=&http://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s/cOGn8-rHWLIxdDz-R3pXDg& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&《守望先锋》回放技术:阵亡镜头、全场最佳和亮眼表现&/a& &/li&&li&&a href=&http://link.zhihu.com/?target=http%3A//youxiputao.com/articles/11842& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&《王者荣耀》技术总监复盘回炉历程&/a& &/li&&li&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&帧同步:浮点精度测试&/a&&/li&&li&&a href=&http://link.zhihu.com/?target=https%3A//www.gamereplays.org/overwatch/portals.php%3Fshow%3Dpage%26name%3Doverwatch-a-guide-to-understanding-netcode& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&A guide to understanding netcode&/a& &/li&&li&&a href=&http://link.zhihu.com/?target=http%3A//www.10tiao.com/html/255/.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&网游流畅基础:帧同步游戏开发&/a& &/li&&/ul&&p&最后啰嗦一句,如最开始所述,帧同步有很多变种和实现方式,优化方向。有时候,可能不同文章提到帧同步这个术语的时候,里面的意思,可能都有区别,大家需要仔细理清和区分。&/p&
终于要写帧同步这块了,这块很难讲清楚,细枝末节有很多优化点,也有一些不同的优化方向,根据不同项目类型,对操作手感的要求,对联机玩家的个数等,会有不同的难点和痛点。不同的优化方向,优化手法的差异,可能导致一些争论。并且,帧同步,本身也有很多…
&figure&&img src=&https://pic2.zhimg.com/v2-69faec9c4fb409df09d261c5e2b748b5_b.jpg& data-rawwidth=&489& data-rawheight=&291& class=&origin_image zh-lightbox-thumb& width=&489& data-original=&https://pic2.zhimg.com/v2-69faec9c4fb409df09d261c5e2b748b5_r.jpg&&&/figure&&p&正则表达式,一个十分古老而又强大的文本处理工具,仅仅用一段非常简短的表达式语句,便能够快速实现一个非常复杂的业务逻辑。熟练地掌握正则表达式的话,能够使你的开发效率得到极大的提升。&/p&&p&正则表达式经常被用于字段或任意字符串的校验,如下面这段校验基本日期格式的JavaScript代码:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&var reg = /^(\\d{1,4})(-|\\/)(\\d{1,2})\\2(\\d{1,2})$/;
var r = fieldValue.match(reg);
if(r==null)alert('Date format error!');
&/code&&/pre&&/div&&p&下面是&b&技匠&/b&整理的,在前端开发中经常使用到的&b&20&/b&个正则表达式。&/p&&p&&b&1 . 校验密码强度&/b&&/p&&p&密码的强度必须是包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
&/code&&/pre&&/div&&p&&b&2. 校验中文&/b&&/p&&p&字符串仅能是中文。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^[\\u4e00-\\u9fa5]{0,}$
&/code&&/pre&&/div&&p&&b&3. 由数字、26个英文字母或下划线组成的字符串&/b&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^\\w+$
&/code&&/pre&&/div&&p&&b&4. 校验E-Mail 地址&/b&&/p&&p&同密码一样,下面是E-mail地址合规性的正则检查语句。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?
&/code&&/pre&&/div&&p&&b&5. 校验身份证号码&/b&&/p&&p&下面是身份证号码的正则校验。15 或 18位。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&15位:
^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$
^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$
&/code&&/pre&&/div&&p&&b&6. 校验日期&/b&&/p&&p&“yyyy-mm-dd“ 格式的日期校验,已考虑平闰年。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^(?:(?!]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[1])-31)|(?:[0-9]{2}(?:0[48]|[]|[13579][26])|(?:0[48]|[]|[1)-02-29)$
&/code&&/pre&&/div&&p&&b&7. 校验金额&/b&&/p&&p&金额校验,精确到2位小数。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^[0-9]+(.[0-9]{2})?$
&/code&&/pre&&/div&&p&&b&8. 校验手机号&/b&&/p&&p&下面是国内 13、15、18开头的手机号正则表达式。(可根据目前国内收集号扩展前两位开头号码)&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$
&/code&&/pre&&/div&&p&&b&9. 判断IE的版本&/b&&/p&&p&IE目前还没被完全取代,很多页面还是需要做版本兼容,下面是IE版本检查的表达式。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^.*MSIE [5-8](?:\\.[0-9]+)?(?!.*Trident\\/[5-9]\\.0).*$
&/code&&/pre&&/div&&p&&b&10. 校验IP-v4地址&/b&&/p&&p&IP4 正则语句。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b
&/code&&/pre&&/div&&p&&b&11. 校验IP-v6地址&/b&&/p&&p&IP6 正则语句。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))
&/code&&/pre&&/div&&p&&b&12. 检查URL的前缀&/b&&/p&&p&应用开发中很多时候需要区分请求是HTTPS还是HTTP,通过下面的表达式可以取出一个url的前缀然后再逻辑判断。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&if (!s.match(/^[a-zA-Z]+:\\/\\//))
s = 'http://' +
&/code&&/pre&&/div&&p&&b&13. 提取URL链接&/b&&/p&&p&下面的这个表达式可以筛选出一段文本中的URL。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^(f|ht){1}(tp|tps):\\/\\/([\\w-]+\\.)+[\\w-]+(\\/[\\w- ./?%&=]*)?
&/code&&/pre&&/div&&p&&b&14. 文件路径及扩展名校验&/b&&/p&&p&验证windows下文件路径和扩展名(下面的例子中为.txt文件) &/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^([a-zA-Z]\\:|\\\\)\\\\([^\\\\]+\\\\)*[^\\/:*?&&&|]+\\.txt(l)?$
&/code&&/pre&&/div&&p&&b&15. 提取Color Hex
Codes&/b&&/p&&p&有时需要抽取网页中的颜色代码,可以使用下面的表达式。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$
&/code&&/pre&&/div&&p&&b&16. 提取网页图片&/b&&/p&&p&假若你想提取网页中所有图片信息,可以利用下面的表达式。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&\\& *[img][^\\\\&]*[src] *= *[\\&\\']{0,1}([^\\&\\'\\ &]*)
&/code&&/pre&&/div&&p&&b&17. 提取页面超链接&/b&&/p&&p&提取html中的超链接。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&(&a\\s*(?!.*\\brel=)[^&]*)(href=&https?:\\/\\/)((?!(?:(?:www\\.)?'.implode('|(?:www\\.)?', $follow_list).'))[^&]+)&((?!.*\\brel=)[^&]*)(?:[^&]*)&
&/code&&/pre&&/div&&p&&b&18. 查找CSS属性&/b&&/p&&p&通过下面的表达式,可以搜索到相匹配的CSS属性。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&^\\s*[a-zA-Z\\-]+\\s*[:]{1}\\s[a-zA-Z0-9\\s.#]+[;]{1}
&/code&&/pre&&/div&&p&&b&19. 抽取注释&/b&&/p&&p&如果你需要移除HMTL中的注释,可以使用如下的表达式。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&&!--(.*?)--&
&/code&&/pre&&/div&&p&&b&20. 匹配HTML标签&/b&&/p&&p&通过下面的表达式可以匹配出HTML中的标签属性。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&&\\/?\\w+((\\s+\\w+(\\s*=\\s*(?:&.*?&|'.*?'|[\\^'&&\\s]+))?)+\\s*|\\s*)\\/?&
&/code&&/pre&&/div&&h2&&b&正则表达式的相关语法&/b&&/h2&&p&下面是我找到的一张非常不错的正则表达式 Cheat Sheet,可以用来快速查找相关语法。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-2f43be36dd2dd45848e28ddfe430f333_b.jpg& data-caption=&& data-size=&normal& data-rawwidth=&1024& data-rawheight=&768& class=&origin_image zh-lightbox-thumb& width=&1024& data-original=&https://pic4.zhimg.com/v2-2f43be36dd2dd45848e28ddfe430f333_r.jpg&&&/figure&&h2&&b&学习正则表达式&/b&&/h2&&p&我在网上看到了一篇相当不错的正则表达式&a href=&http://link.zhihu.com/?target=https%3A//msdn.microsoft.com/en-us/library/az24scfc%28v%3Dvs.110%29.aspx& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&快速学习指南&/a&,有兴趣继续深入学习的同学可以参考。&/p&&figure&&img src=&https://pic1.zhimg.com/v2-99cdd76a777a8a2b3767cbe3566dc45c_b.png& data-caption=&& data-size=&normal& data-rawwidth=&705& data-rawheight=&413& class=&origin_image zh-lightbox-thumb& width=&705& data-original=&https://pic1.zhimg.com/v2-99cdd76a777a8a2b3767cbe3566dc45c_r.jpg&&&/figure&&h2&&b&正则表达式在线测试工具&/b&&/h2&&p&&a href=&http://link.zhihu.com/?target=https%3A//regex101.com/%23javascript& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&regex101&/a&是一个非常不错的正则表达式在线测试工具,你可以直接在线测试你的正则表达式哦。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-0c8bf3fc322f9824b0cbd_b.png& data-caption=&& data-size=&normal& data-rawwidth=&746& data-rawheight=&423& class=&origin_image zh-lightbox-thumb& width=&746& data-original=&https://pic2.zhimg.com/v2-0c8bf3fc322f9824b0cbd_r.jpg&&&/figure&&p&另外,我也网上找到几本不错的正则表达式方面的教程和书籍,并将它们分享到了&a href=&https://zhuanlan.zhihu.com/p//jijiangshe.com& class=&internal&&技匠社jijiangshe.com&/a&,如果你有兴趣学习欢迎访问获取。^_^&/p&&p&作者:技匠&/p&&p&&a href=&http://link.zhihu.com/?target=http%3A//www.jianshu.com/p/e7bb& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://www.&/span&&span class=&visible&&jianshu.com/p/e7bb97218&/span&&span class=&invisible&&946&/span&&span class=&ellipsis&&&/span&&/a&&/p&&p&——————————————————————————————————————&/p&&p&你想更深入了解学习Python知识体系,你可以看一下我们花费了一个多月整理了上百小时的几百个知识点体系内容:&/p&&p&&a href=&http://link.zhihu.com/?target=http%3A//www.magedu.com/73198.html%3FPython_wenzhang_zhihu_jinke_zhidaozhe20gezhengzebiaodashinengrangnishaoxie1000hangdaima_& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&【超全整理】《Python自动化全能开发从入门到精通》笔记全放送&/a&&/p&
正则表达式,一个十分古老而又强大的文本处理工具,仅仅用一段非常简短的表达式语句,便能够快速实现一个非常复杂的业务逻辑。熟练地掌握正则表达式的话,能够使你的开发效率得到极大的提升。正则表达式经常被用于字段或任意字符串的校验,如下面这段校验基…
&figure&&img src=&https://pic2.zhimg.com/v2-1ee21ef61c88e737797dc_b.jpg& data-rawwidth=&1280& data-rawheight=&720& class=&origin_image zh-lightbox-thumb& width=&1280& data-original=&https://pic2.zhimg.com/v2-1ee21ef61c88e737797dc_r.jpg&&&/figure&&h2&&b&前言&/b&&/h2&&p&卡通渲染是图形学中一个有趣的话题,属于&b&非真实感计算机图形学(NPR)&/b&的范畴,在NPR领域中也最多地被应用到实际游戏中,近年来流行的《守望先锋》,《英雄联盟》,《DOTA2》,《崩坏3》等游戏中都或多或少地出现过卡通渲染的身影,恰好最近对这个领域的内容作了一些了解和探索,所以就对其中涉及的一些经典技术做一个概述。&/p&&h2&&b&卡通渲染的分类&/b&&/h2&&p&在具体讨论技术手段之前,先就卡通渲染做一个分类。卡通渲染最关键的特征包括不同于真实感渲染的&b&艺术化光影效果&/b&和&b&描边&/b&。以这两个关键的特征为卡通渲染分类的话,可以将近年来游戏中常用的卡通渲染分为&b&美式卡通风格&/b&和&b&日式卡通风格&/b&。美式卡通风格在色彩上比较连续,有渐变色,着色风格很大程度上依赖于艺术家定义的色调(tone),而在阴影和高光方面常常采取夸张和变形的做法,比较典型的是《军团要塞2》;日式卡通风格往往角色造型更写实,但在着色方面,则趋向于大片大片纯色色块,并有的明暗交界,例如《崩坏3》。虽然这样的分类并没有清晰界限,但易于描述,接下来我们就按照美式卡通和日式卡通的分类,从光影和描边两个维度上分别列举各类技术实现。&/p&&p&&figure&&img src=&https://pic4.zhimg.com/v2-55d6e225bd5cdad2fd23b0a_b.jpg& data-rawwidth=&1189& data-rawheight=&704& class=&origin_image zh-lightbox-thumb& width=&1189& data-original=&https://pic4.zhimg.com/v2-55d6e225bd5cdad2fd23b0a_r.jpg&&&/figure&军团要塞2的卡通渲染,人物造型夸张,但着色连续,接近真实感光照&/p&&p&&figure&&img src=&https://pic2.zhimg.com/v2-b39b32c0a115b416e438_b.jpg& data-rawwidth=&574& data-rawheight=&639& class=&origin_image zh-lightbox-thumb& width=&574& data-original=&https://pic2.zhimg.com/v2-b39b32c0a115b416e438_r.jpg&&&/figure&崩坏3游戏截图,着色以单色色块为主,有明显的明暗交界&/p&&h2&&b&描边&/b&&/h2&&p&描边是一个比较常用的技术,在《Real Time Rendering》中有相当篇幅的综述,大致来说包含了三类:&/p&&p&(1)&b&基于视角的勾边&/b&,这部分的计算依赖于我们的一个直觉观察:当我们的视线和某个表面相切时,这个表面上的像素点往往就是模型的边缘,基于这个观察,我们可以用&img src=&https://www.zhihu.com/equation?tex=dot%28viewDir%2C+normal%29%5E%7Bk%7D+& alt=&dot(viewDir, normal)^{k} & eeimg=&1&& 来估计一个像素的“边缘程度”,当然,这个值也可以用来作为纹理坐标去采样一张预定义的“轮廓纹理”&/p&&p&&figure&&img src=&https://pic2.zhimg.com/v2-c76d8febd2e33bb45c57_b.jpg& data-rawwidth=&626& data-rawheight=&595& class=&origin_image zh-lightbox-thumb& width=&626& data-original=&https://pic2.zhimg.com/v2-c76d8febd2e33bb45c57_r.jpg&&&/figure&基于视角的描边,最大的缺点是线宽粗细差别较大,不易控制&/p&&p&(2)&b&基于几何生成方法的描边&/b&,这类方法的特点是描边本身是一个单独的几何体,通过特殊的方法绘制出来,比较常见的做法是&b&shell method&/b&,原理和实现都比较简单:首先在绘制结束正常的模型后,将需要描边的物体&b&改用正面剔除&/b&再绘制一遍,在VS中将顶点沿着法线方向膨胀一定距离,然后在FS中将模型用纯色输出。另外一种叫做&b&z-bias&/b&的方法,也是绘制背面,但不膨胀,而是把背面顶点的Z值稍微向前偏移一点点,使得背面的些许部分显示出来形成描边效果。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-5c1e98261fac2ea11ce4fdb_b.jpg& data-rawwidth=&747& data-rawheight=&494& class=&origin_image zh-lightbox-thumb& width=&747& data-original=&https://pic4.zhimg.com/v2-5c1e98261fac2ea11ce4fdb_r.jpg&&&/figure&&p&基于shell method的绘制方法,实现简单,线宽较为均匀&/p&&p&(3)&b&基于图像处理的描边&/b&,这类方法的实现可以说更接近于“边缘”这一概念的本质定义,什么是“边缘”呢?&b&边缘就是在深度或者法线上不连续的位置&/b&。因此为了获取边缘,我们只需要在图片上找到深度或者法线不连续的位置即可,因此,我们需要将深度信息和法线信息以贴图的形式传入,运用边缘检测算法去寻找这些像素。这类方法的优点是描边的线宽一致,缺点是需要额外的法线和深度信息,当然,由于近年来流行的&b&延迟渲染&/b&框架,法线和深度本来就是G-Buffer的一部分,因此往往不需要额外绘制法线和深度的信息。&/p&&figure&&img src=&https://pic2.zhimg.com/v2-ef62abd9e04db2bc951e32dacac9e901_b.jpg& data-rawwidth=&481& data-rawheight=&691& class=&origin_image zh-lightbox-thumb& width=&481& data-original=&https://pic2.zhimg.com/v2-ef62abd9e04db2bc951e32dacac9e901_r.jpg&&&/figure&&p&基于边缘检测的描边方法,分别用深度信息和法线信息进行单独的边缘检测,而后合并起来成为最终的描边&/p&&p&&b&美式卡通中的做法&/b&&/p&&p&美式卡通中往往倾向于使用基于图像处理的描边方法来生成均匀一致的描边效果。在《英雄联盟》[1]中小兵和英雄的勾边效果就是&b&用Sobel算子对深度信息进行边缘检测来获得&/b&的。由于游戏中只需要针对小兵和英雄勾边而不需要对场景地图进行勾边,因此在LOL中,&b&勾边的计算并非全屏后处理,而是逐物体进行的&/b&,这样的好处是可以随意控制哪些物体描边,每个物体可以单独指定描边颜色,缺点是当物体较多时(尤其是skinned mesh较多时)计算量会增大。一个折衷的方案是,在进行正常绘制的阶段用stencil buffer标记出需要描边的物体,然后用一个全屏的后处理,对stencil buffer标记的像素进行边缘检测,当然这样的话,就很难给每个物体单独指定描边颜色了。&/p&&p&实际上,在LOL中有两种类型的描边,一种是小兵和英雄的固定描边,另一种是防御塔发出攻击警报或者某个单位被点选时才产生的红色描边,这两种描边在处理上略有差别,前者直接使用边缘检测的结果作为最终描边,而&b&后者则是对边缘检测结果再进行一次模糊&/b&,借此来扩大和柔化描边效果。&/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-608be38de54c5bb29c9e38_b.jpg& data-rawwidth=&425& data-rawheight=&407& class=&origin_image zh-lightbox-thumb& width=&425& data-original=&https://pic3.zhimg.com/v2-608be38de54c5bb29c9e38_r.jpg&&&/figure&&figure&&img src=&https://pic2.zhimg.com/v2-3f8a1a536a191b6b91b13640c2bfd6d1_b.jpg& data-rawwidth=&1200& data-rawheight=&768& class=&origin_image zh-lightbox-thumb& width=&1200& data-original=&https://pic2.zhimg.com/v2-3f8a1a536a191b6b91b13640c2bfd6d1_r.jpg&&&/figure&LOL中两种类型的描边,可以看出第二类描边的线宽更宽,并且有明显的过度效果&/p&&p&&b&日式卡通的做法&/b&&/p&&p&日式卡通中往往倾向于使用基于几何体生成的方法去描边,这类描边方法相较于另两类方法的好处在于线宽更容易为美术所控制,而在日式卡通中,往往需要粗细有变化的描边去体现角色不同部位的特征,例如在《GUILTY GEAR Xrd》[2][3]中,角色的描边就是通过几何体生成的方法,结合了shell method和z-bias method,并引入了逐物体的顶点色来控制描边细节,同时也是为了保证描边粗细不会随着摄像机视距发生变化,具体来说,顶点色存储的信息包括:&/p&&ul&&li&R通道:控制toon shading的阈值,和描边无关,和着色有关,这个我们后面描述&/li&&li&G通道:控制顶点根据视距膨胀的强度(这个部分具体操作我也没有完全弄清楚,希望了解的朋友来补充)&/li&&li&B通道:控制描边的z-bias,越大则描边越不可见&/li&&li&A通道:控制描边的粗细&/li&&/ul&&br&上述做法中比较直观的理解是,通过引入逐顶点的线宽系数,使得整个描边的细节更易为美术控制,但是从我的理解来看,线宽控制只需要一个值即可,视距无关的粗细可以通过给偏移值offset.xy乘以当前顶点的z值来实现,似乎并不需要三个值来控制。&p&&figure&&img src=&https://pic1.zhimg.com/v2-d5ea50ce999c6ca22460_b.jpg& data-rawwidth=&615& data-rawheight=&500& class=&origin_image zh-lightbox-thumb& width=&615& data-original=&https://pic1.zhimg.com/v2-d5ea50ce999c6ca22460_r.jpg&&&/figure&没有vertex color,轮廓线宽没有粗细变化&/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-d00bfaf3_b.jpg& data-rawwidth=&615& data-rawheight=&500& class=&origin_image zh-lightbox-thumb& width=&615& data-original=&https://pic3.zhimg.com/v2-d00bfaf3_r.jpg&&&/figure&有vertex color, 轮廓线可以按照美术的需要去设定逐顶点粗细变化&/p&&h2&&b&着色&/b&&/h2&&p&&b&Cel Shading和Tone Based Shading &/b&&/p&&p&先来描述两种经典的NPR着色方法,分别是&b&Cel Shading&/b&[4]和&b&Tone Based Shading&/b&[5]。&/p&&p&&b&Cel Shading&/b&的基本思想是把色彩从多色阶降到低色阶,减少色阶的丰富程度,从而实现类似手工着色的效果,具体来说,可以用如下计算方法:&/p&&img src=&https://www.zhihu.com/equation?tex=celCoord+%3D+dot%28normal%2C+lightDir%29& alt=&celCoord = dot(normal, lightDir)& eeimg=&1&&&br&&img src=&https://www.zhihu.com/equation?tex=I+%3D+tex%28paletteTex%2C++celCoord%29.rgb+%2A+lightColor.rgb+%2A+k_%7Bd%7D+& alt=&I = tex(paletteTex,
celCoord).rgb * lightColor.rgb * k_{d} & eeimg=&1&&&p&其中,Kd表示模型自身的贴图颜色,celCoord表示法线和光照方向的点积,用作一维色彩表的查找坐标,而paletteTex则是由美术绘制的一维色阶表,一般来说是由几个纯色色块组成的,如下图:&/p&&figure&&img src=&https://pic4.zhimg.com/v2-328ebfe55fd14144babce61_b.jpg& data-rawwidth=&464& data-rawheight=&545& class=&origin_image zh-lightbox-thumb& width=&464& data-original=&https://pic4.zhimg.com/v2-328ebfe55fd14144babce61_r.jpg&&&/figure&&p&上述做法可以用于模拟卡通渲染的漫反射分量,却并&b&没有考虑到视角相关的光照分量的模拟&/b&,因此很难实现类似菲涅尔效果的卡通渲染。实际上,也可以用类似的查找表的思路来视角相关光照分量的色阶离散化[6],只需要将一维查找表扩展到二维即可:&/p&&img src=&https://www.zhihu.com/equation?tex=celCoord+%3D+vec2%28dot%28normal%2C+lightDir%29%2C+dot%28normal%2C+viewDir%29%5E%7Br%7D+%29& alt=&celCoord = vec2(dot(normal, lightDir), dot(normal, viewDir)^{r} )& eeimg=&1&&&img src=&https://www.zhihu.com/equation?tex=I+%3D+tex%28paletteTex%2C++celCoord%29.rgb+%2A+lightColor.rgb+%2A+k_%7Bd%7D+& alt=&I = tex(paletteTex,
celCoord).rgb * lightColor.rgb * k_{d} & eeimg=&1&&&br&&p&相应地,查找坐标也扩展到了二维。&/p&&figure&&img src=&https://pic4.zhimg.com/v2-59116ffedce792c562deed_b.jpg& data-rawwidth=&559& data-rawheight=&646& class=&origin_image zh-lightbox-thumb& width=&559& data-original=&https://pic4.zhimg.com/v2-59116ffedce792c562deed_r.jpg&&&/figure&&br&&p&不同于Cel Shading,&b&Tone Based Shading&/b&的风格化是基于美术指定的色调插值,并且插值得到的色阶是连续的。首先需要由美术指定&b&冷色调和暖色调&/b&,而最终模型的着色将根据法线和光照方向的夹角,在这两个色调的基础上进行插值,具体算法如下:&/p&&img src=&https://www.zhihu.com/equation?tex=I+%3D+%5Cfrac%7B1+%2B+dot%28normal%2C+lightDir%29%7D%7B2%7D+k_%7Bcool%7D+%2B%281-%5Cfrac%7B1+%2B+dot%28normal%2C+lightDir%29%7D%7B2%7D%29+k_%7Bwarm%7D+& alt=&I = \frac{1 + dot(normal, lightDir)}{2} k_{cool} +(1-\frac{1 + dot(normal, lightDir)}{2}) k_{warm} & eeimg=&1&&&img src=&https://www.zhihu.com/equation?tex=k_%7Bcool%7D+%3Dk_%7Bblue%7D+%2B+%5Calpha+k_%7Bd%7D& alt=&k_{cool} =k_{blue} + \alpha k_{d}& eeimg=&1&&&img src=&https://www.zhihu.com/equation?tex=k_%7Bwarm%7D+%3Dk_%7Byellow%7D+%2B+%5Cbeta+k_%7Bd%7D& alt=&k_{warm} =k_{yellow} + \beta k_{d}& eeimg=&1&&&p&其中,Kd仍是模型自身色彩贴图,Kblue,Kyellow和alpha,beta则均是自定义的参数。&/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-4c95a2de2ea_b.jpg& data-rawwidth=&841& data-rawheight=&97& class=&origin_image zh-lightbox-thumb& width=&841& data-original=&https://pic3.zhimg.com/v2-4c95a2de2ea_r.jpg&&&/figure&基于tone based shading绘制的球体&/p&&br&&p&&b&日式卡通的着色&/b&&/p&&p&前面已经描述过,日式卡通在着色方面比较典型的特点是以大量纯色为主,进一步说,&b&往往只有“明暗”或者“冷暖”两个色阶&/b&,因此光照计算往往最后也要映射到离散的色彩表上。 仍然以《GUILTY GEAR Xrd》为例,它也一定程度上包含了Cel Shading和Tone Based Shading的部分思想,将色阶离散成为“明暗”两个色调,并由美术指定冷暖色调的颜色:&/p&&img src=&https://www.zhihu.com/equation?tex=I%3D+%28%28darkness+%3C+threshold%29+%3F+k_%7Bcool%7D+%2A+k_%7BSSS%7D+%3A+k_%7Bwarm%7D%29+%2A+lightColor.rgb+%2A+k_%7Bd%7D& alt=&I= ((darkness & threshold) ? k_{cool} * k_{SSS} : k_{warm}) * lightColor.rgb * k_{d}& eeimg=&1&&&img src=&https://www.zhihu.com/equation?tex=darkness+%3D+dot%28normal%2C+lightDir%29+%2A+AO& alt=&darkness = dot(normal, lightDir) * AO& eeimg=&1&&&br&&p&上述公式表示了这个卡通渲染的漫反射部分,其中threshold表示&b&明暗交界的阈值&/b&,在游戏中通过顶点色的R通道来实现逐顶点的控制。&b&Kcool和Kwarm由美术逐物体地指定&/b&,&b&Ksss是对模型次表面散射效果的模拟&/b&,对皮肤而言一般呈粉红色,通过美术绘制的SSS贴图来实现逐像素控制,并且只有暗部的像素才会受SSS贴图的影响。Kd是模型自身的颜色贴图。&b&darkness表示了某个像素的明暗程度&/b&,用于确定色调的冷暖。除了正常的dot(normal, lightDir)项,游戏中还加入了&b&由美术绘制的AO贴图&/b&,来实现一些边角缝隙的暗部效果。我在实现时又引入了动态的阴影部分,最终darkness的计算公式为:&/p&&img src=&https://www.zhihu.com/equation?tex=darkness+%3D+dot%28normal%2C+lightDir%29+%2A+AO+%2A+shadow& alt=&darkness = dot(normal, lightDir) * AO * shadow& eeimg=&1&&&p&其中shadow是由shadowMap的算法计算得来的。&/p&&p&高光的计算更简单一些:&/p&&img src=&https://www.zhihu.com/equation?tex=I_%7Bspec%7D%3D%28spec+%3C+threshold_%7Bspec%7D+%29%3F+vec3%280.0%29%3AspecColor& alt=&I_{spec}=(spec & threshold_{spec} )? vec3(0.0):specColor& eeimg=&1&&&img src=&https://www.zhihu.com/equation?tex=spec+%3D+specMask+%2A+dot%28normal%2C+halfVec%29%5E%7BspecPower%7D+& alt=&spec = specMask * dot(normal, halfVec)^{specPower} & eeimg=&1&&&p&其中,spec表示高光的强度,threshold可以由美术逐物体或逐顶点指定,specMask和specPower由美术绘制的贴图来逐像素控制,&b&类似于phong着色中的specular和glossiness的作用&/b&。specColor可以由美术逐物体地指定,也可以把AO,shadow和明暗色调作为影响因素添加进去。最终的着色结果将漫反射和高光叠加即可。&/p&&p&在实际游戏中使用时,上述方法往往还需要配合美术针对具体模型进行&b&法线修正&/b&。 根据模型顶点位置和拓扑关系计算出的法线往往细节过度,表现在上述卡通渲染的结果上就是往往会出现许多不需要的暗部细节,修正的方法是使用&b&模型法线转印&/b&,给精细的模型一个近似的低精度proxy(比如用一个球形代表模型的头部,用一个圆柱形代表模型的胳膊或者腿),然后用proxy上附近顶点的法线作为模型的法线来使用。此外,还需要考虑到明暗交界处反走样的问题,这里不做展开。&br&&/p&&p&根据我的观察和研究,《崩坏3》应该是沿用了《GUILTY GEAR Xrd》中的卡通着色方法和美术工艺,因此在效果上和后者非常相似。&/p&&p&&figure&&img src=&https://pic4.zhimg.com/v2-79eb2d8c5de8ba688f4671_b.jpg& data-rawwidth=&1785& data-rawheight=&635& class=&origin_image zh-lightbox-thumb& width=&1785& data-original=&https://pic4.zhimg.com/v2-79eb2d8c5de8ba688f4671_r.jpg&&&/figure&基于不同的冷暖色调设定值得到的卡通渲染结果&/p&&p&&b&美式卡通的着色&/b&&/p&&p&Valve在其游戏《军团要塞2》[7]中描述了他们的卡通渲染方案,这个卡通渲染算法也在后来影响了《DOTA2》的卡通渲染的实现。他们将卡通渲染着色分为了&b&view dependent term&/b&和&b&view independent term&/b&。两部分的计算分别如下:&/p&&img src=&https://www.zhihu.com/equation?tex=I%3DI_%7BviewIndependent%7D+%2B+I_%7BviewDependent%7D+& alt=&I=I_{viewIndependent} + I_{viewDependent} & eeimg=&1&&&br&&img src=&https://www.zhihu.com/equation?tex=I_%7BviewIndependent%7D%3Dk_%7Bd%7D+%5Ba%28%5Cbar%7Bn%7D%29+%2B+%5Csum_%7Bi%3D1%7D%5E%7BL%7D%7Bc_%7Bi%7Dw%28%28%5Calpha+%7D++%28%5Cbar%7Bn%7D%5Ccdot+%5Cbar%7Bl%7D%29%2B%5Cbeta%29%5E%7Br%7D%29%5D& alt=&I_{viewIndependent}=k_{d} [a(\bar{n}) + \sum_{i=1}^{L}{c_{i}w((\alpha }
(\bar{n}\cdot \bar{l})+\beta)^{r})]& eeimg=&1&&&br&&img src=&https://www.zhihu.com/equation?tex=I_%7BviewDependent%7D%3D%5Csum_%7Bi%3D1%7D%5E%7BL%7D%7B%5Bc_%7Bi%7Dk_%7Bs%7Dmax%28f_%7Bs%7D%28%5Cbar%7Bv%7D%5Ccdot%5Cbar%7Br_%7Bi%7D%7D%29%5E%7Bk_%7Bspec%7D%7D%2Cf_%7Br%7Dk_%7Br%7D%28%5Cbar%7Bv%7D%5Ccdot%5Cbar%7Br_%7Bi%7D%7D%29%5E%7Bk_%7Brim%7D%7D%29%5D%7D+%2B%28%5Cbar%7Bn%7D%5Ccdot+%5Cbar%7Bu%7D%29f_%7Br%7Dk_%7Br%7Da%28%5Cbar%7Bv%7D%29& alt=&I_{viewDependent}=\sum_{i=1}^{L}{[c_{i}k_{s}max(f_{s}(\bar{v}\cdot\bar{r_{i}})^{k_{spec}},f_{r}k_{r}(\bar{v}\cdot\bar{r_{i}})^{k_{rim}})]} +(\bar{n}\cdot \bar{u})f_{r}k_{r}a(\bar{v})& eeimg=&1&&&p&这部分的实现在&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&其他知乎专栏文章&/a&[8]中有详细的描述和实现,这里不再做详细的解释。直观地来说,在&b&视角无关的照明部分&/b&,《军团要塞2》中除了考虑了一般的漫反射部分外,还加入了&b&基于模型法线方向的环境光分量&/b&,此外,通常的漫反射分量改为了&b&wrapped diffuse&/b&;而在&b&视角相关的照明部分&/b&,《军团要塞2》除了考虑了一般的镜面反射外,还&b&基于菲涅尔现象实现了类似边缘光的效果&/b&。实际上,类似&b&ambient cube&/b&和&b&warpped diffuse&/b&的做法也被Valve应用在《Half Life》等其他游戏中[9],在早期3D游戏中用以模拟全局照明。虽然这些方法都是一些纯粹的trick,但是能够以很小的开销实现不错的效果。&/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-8fce8d6ff44d31e5b887c7ffcd885a98_b.jpg& data-rawwidth=&575& data-rawheight=&433& class=&origin_image zh-lightbox-thumb& width=&575& data-original=&https://pic3.zhimg.com/v2-8fce8d6ff44d31e5b887c7ffcd885a98_r.jpg&&&/figure&《军团要塞2》的最终着色结果,可以看出明暗交界处有明显的泛红(warpped diffuse的效果), 模型边缘可以看到边缘光&/p&&p&&figure&&img src=&https://pic2.zhimg.com/v2-8477470ada_b.jpg& data-rawwidth=&506& data-rawheight=&318& class=&origin_image zh-lightbox-thumb& width=&506& data-original=&https://pic2.zhimg.com/v2-8477470ada_r.jpg&&&/figure&经典的half-lambert方法也算是warpped diffuse的一种变体&/p&&h2&&b&风格化高光和阴影&/b&&/h2&&p&在[7]的Future Work里,还提到了可变形状的高光[10]和风格化阴影[11],这两个风格化渲染算法的思路都比较有趣,这里简单就其实现原理进行一个概述。&/p&&p&&b&可变形状的高光&/b&&/p&&p&我们在日式卡通渲染的着色部分描述了一个相对较为简单的高光计算方式,从计算方法可以看出,该方法和经典的Blin-Phong模型有很多相似之处,尤其是对高光强度的计算上,都采用了这个计算项:&/p&&img src=&https://www.zhihu.com/equation?tex=specMask+%2A+dot%28normal%2C+halfVec%29%5E%7BspecPower%7D& alt=&specMask * dot(normal, halfVec)^{specPower}& eeimg=&1&&&br&&p&这个halfVec也就是我们常说的&b&半角向量&/b&,计算方法是:&/p&&img src=&https://www.zhihu.com/equation?tex=%5Cbar%7BH%7D+%3D+%5Cfrac%7B%5Cbar%7BL%7D+%2B+%5Cbar%7BV%7D%7D%7B%5Cleft%7C+%5Cbar%7BL%7D+%2B+%5Cbar%7BV%7D%5Cright%7C%7D& alt=&\bar{H} = \frac{\bar{L} + \bar{V}}{\left| \bar{L} + \bar{V}\right|}& eeimg=&1&&&br&&p&其中,L和V分别是光源方向和视线方向。&/p&&p&从我们上面描述的卡通渲染高光算法可以看出来,&b&改变卡通渲染高光形状的关键就在于改变这个半角向量&/b&。因此文章中就针对半角向量定义了一系列的修改操作,这些修改操作可以叠加使用,也可以单独使用,每个操作对高光形状的影响均不同,具体有以下几个操作:&/p&&p&(1)&b&平移&/b&,改变高光的位置:&/p&&img src=&https://www.zhihu.com/equation?tex=%5Cbar%7BH%27%7D+%3D+normalize%28%5Cbar%7BH%7D+%2B+%5Calpha%5Cbar%7Bdu%7D+%2B+%5Cbeta%5Cbar%7Bdv%7D%29& alt=&\bar{H'} = normalize(\bar{H} + \alpha\bar{du} + \beta\bar{dv})& eeimg=&1&&&br&&p&这里,du和dv表示的是&b&切线空间中的x轴和y轴&/b&,也就是切线和副法线,alpha和beta是自定义平移参数,最终偏移后的向量需要进行归一化处理。&/p&&p&(2)&b&有方向的缩放&/b&,沿着切线空间的某个轴缩放高光形状:&/p&&img src=&https://www.zhihu.com/equation?tex=%5Cbar%7BH%27%7D+%3D+normalize%28%5Cbar%7BH%7D+-+%5Csigma%28%5Cbar%7BH%7D+%5Ccdot+%5Cbar%7Bdu%7D%29%5Cbar%7Bdu%7D%29& alt=&\bar{H'} = normalize(\bar{H} - \sigma(\bar{H} \cdot \bar{du})\bar{du})& eeimg=&1&&&br&&p&sigma是自定义参数,范围是(0, 1],上式将使高光沿着切线空间的X轴缩放。&/p&&p&(3)&b&分割&/b&,将一块连续的高光切分成两块:&/p&&img src=&https://www.zhihu.com/equation?tex=%5Cbar%7BH%7D+%3D+normalize%28%5Cbar%7BH%7D+-+%5Cgamma_%7B1%7Dsgn%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdu%7D%29%5Cbar%7Bdu%7D+-+%5Cgamma_%7B2%7Dsgn%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdv%7D%29%5Cbar%7Bdv%7D%29& alt=&\bar{H} = normalize(\bar{H} - \gamma_{1}sgn(\bar{H}\cdot\bar{du})\bar{du} - \gamma_{2}sgn(\bar{H}\cdot\bar{dv})\bar{dv})& eeimg=&1&&&br&&p&其中,sgn是符号函数,负数返回-1,否则返回1,gamma1和gamma2分别是自定义参数,若其中一个为0,则只沿着另一个方向将高光切为两部分,若两个参数均不为0,则高光被切成四块。&/p&&p&(4)&b&方块化&/b&,将趋于圆形的高光变成方形:&/p&&img src=&https://www.zhihu.com/equation?tex=%5Ctheta+%3D+min%28cos%5E%7B-1%7D%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdu%7D%29%2C+cos%5E%7B-1%7D%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdv%7D%29%29& alt=&\theta = min(cos^{-1}(\bar{H}\cdot\bar{du}), cos^{-1}(\bar{H}\cdot\bar{dv}))& eeimg=&1&&&br&&img src=&https://www.zhihu.com/equation?tex=sqrnorm+%3D+sin%282%5Ctheta%29%5E%7Bn%7D& alt=&sqrnorm = sin(2\theta)^{n}& eeimg=&1&&&br&&img src=&https://www.zhihu.com/equation?tex=%5Cbar%7BH%27%7D+%3D+normalize%28%5Cbar%7BH%7D+-+%5Csigma%2Asqrnorm%2A%28%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdu%7D%29%5Cbar%7Bdu%7D%2B%28%5Cbar%7BH%7D%5Ccdot%5Cbar%7Bdv%7D%29%5Cbar%7Bdv%7D%29%29& alt=&\bar{H'} = normalize(\bar{H} - \sigma*sqrnorm*((\bar{H}\cdot\bar{du})\bar{du}+(\bar{H}\cdot\bar{dv})\bar{dv}))& eeimg=&1&&&br&&p&其中,n是自定义整数,n越大高光形状越方,sigma则定义了方形高光区域的大小,范围是[0, 1]。&/p&&p&上述四个操作的具体实现可参见&a href=&https://link.zhihu.com/?target=http%3A//blog.csdn.net/candycat1992/article/details/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&这篇文章&/a&[12]。&/p&&p&&figure&&img src=&https://pic3.zhimg.com/v2-469eb130cc86266ade9474eca080e182_b.jpg& data-rawwidth=&582& data-rawheight=&616& class=&origin_image zh-lightbox-thumb& width=&582& data-original=&https://pic3.zhimg.com/v2-469eb130cc86266ade9474eca080e182_r.jpg&&&/figure&四个基本的操作符&/p&&br&&p&&b&风格化阴影&/b&&/p&&br&类似于风格化的高光,风格化的阴影也是在标准的阴影计算流程之后,定义了一系列针对标准阴影的操作,通过这些操作,配合用户自定义的参数,便可以达到风格化阴影的效果,总的来说,共有四类操作:&p&(1)&b&膨胀/腐蚀(Inflation)&/b&:扩大或者缩小阴影范围,用参数&b&i&/b&来控制&/p&&p&(2)&b&亮度(Brightness)&/b&:阴影区域的亮度,可以用于模拟半影区的效果,用参数&b&b&/b&控制&br&&/p&&br&&p&(3)&b&柔度(Softness)&/b&:阴影边界处的柔和程度,用参数&b&s&/b&控制&/p&&p&(4)&b&抽象度(Abstraction)&/b&:阴影形状的抽象程度,用参数&b&alpha&/b&控制&/p&&figure&&img src=&https://pic3.zhimg.com/v2-ecbca32a38f41db1e7e7bc_b.jpg& data-rawwidth=&583& data-rawheight=&161& class=&origin_image zh-lightbox-thumb& width=&583& data-original=&https://pic3.zhimg.com/v2-ecbca32a38f41db1e7e7bc_r.jpg&&&/figure&&p&几种操作和相应的效果&/p&&p&整个风格化阴影的生成是&b&基于图像空间的&/b&,&b&从一个已经生成的精确阴影图开始&/b&。可以分成五个阶段:&/p&&p&(1)&b&精确阴影的生成&/b&,由于是基于图像空间的,因此对精确阴影图的生成方法没有特别要求,可以是shadow volume,shadow map,ray tracing或者其他阴影生成技术,但必须要注意的是这里的阴影值&b&一定是二值化的&/b&&/p&&p&(2)&b&有向距离场的生成&/b&,基于图像空间的精确阴影,计算每个像素距离最近阴影边界的有向距离,这是文中算法的核心,也是后面风格化的基础,在文中给出了一种有向距离场的计算方法,当然也可以采用其他方案&/p&&p&(3)&b&有向距离场的高斯模糊&/b&,这一步是抽象阴影生成的关键&/p&&p&(4)&b&过滤&/b&,通过一个转移函数,将模糊后的有向距离场重新映射为阴影图&/p&&p&(5)&b&使用过滤后的阴影进行光照计算&/b&&/p&&p&&figure&&img src=&https://pic2.zhimg.com/v2-310ae42bf5bdd250b1d68385b8adaa43_b.jpg& data-rawwidth=&1198& data-rawheight=&314& class=&origin_image zh-lightbox-thumb& width=&1198& data-original=&https://pic2.zhimg.com/v2-310ae42bf5bdd250b1d68385b8adaa43_r.jpg&&&/figure&整个算法的流程,图3,4中红色部分表示阴影内部,蓝色表示阴影外部&/p&&p&可以清楚地看出整个算法流程的核心是(2)(3)(4),其中(2)是在整个图像空间计算有向距离场,文中给出的有向距离场公式[13]如下:&/p&&img src=&https://www.zhihu.com/equation?tex=D_%7Bp%7D%28V%28x%29%29%3D%28%5Cint_%7BC%7D%5E%7B%7D%5Cfrac%7B1%7D%7B%5Cleft%7C+x-y+%5Cright%7C%5E%7Bp%7D+dy%7D+%29%5E%7B-1%2Fp%7D%2F%28%5Cint_%7BC%7Ddy%5E%7B%7D+%29%5E%7B-1%2Fp%7D& alt=&D_{p}(V(x))=(\int_{C}^{}\frac{1}{\left| x-y \right|^{p} dy} )^{-1/p}/(\int_{C}dy^{} )^{-1/p}& eeimg=&1&&&br&&p&文中p的取值为8,按照文中的说法,这个距离计算方法相较于欧几里得距离,在精确性(accuracy)和平滑度(smoothness)上有一个比较好的折衷(trade off)。这里C表示的是所有&b&阴影的边界像素的合集&/b&(边界就是黑白发生变化的位置),分母上的积分项表示的是整个边界的长度,是一个归一化参数,离散化来看,就是&b&屏幕上所有边界像素的个数&/b&。&/p&&p&从上面这个计算公式就可以知道,如果要精确计算每个像素的有向距离,则需要针对每个像素遍历整个图像空间中的其他像素,找到所有是边界的像素,并代入上述积分中进行求和运算,这个计算是比较低效的,因此&b&文中采用的方法是在当前像素周围随机取一些像素参与到有向距离的计算,然后用计算结果去估计精确的有向距离的值,也就是所谓的蒙特卡洛方法&/b&。此外&img src=&https://www.zhihu.com/equation?tex=%5Cleft%7C+x+-+y+%5Cright%7C+& alt=&\left| x - y \right| & eeimg=&1&&在文中使用的是一个三维的欧几里得距离,因此实际上计算这个有向距离时还需要一张深度图。&/p&&p&计算出有向距离场后,接着要做的就是对这张图进行模糊,可以想见,如果直接针对visibility图进行模糊的话,得到的实际上是柔化的软阴影,而不是我们想要的抽象阴影,所谓抽象阴影就是把精确阴影中的一些细节给略去,恰好就对应了模糊有向距离的值。这一步是用一个标准的高斯模糊去完成的,&b&参数alpha表示高斯模糊的方差,这个值越大,则模糊程度越高,细节丢失越多,抽象程度越高&/b&。在文中的也使用了蒙特卡洛方法来减少高斯模糊的采样次数。&/p&&p&在得到了经过模糊的有向距离场后,接下来就是如何把模糊后的有向距离场重新映射回阴影值,这里用了一个很巧妙的转移函数,一次性完成了边界膨胀/腐蚀,亮度和柔和度的操作:&/p&&img src=&https://www.zhihu.com/equation?tex=f%28D%29%3D%281-+b%29%5E%7B-1%7Dsmoothstep%28D%2C+i+-+s%2F2%2C+i%2Bs%2F2%29+%2B+b& alt=&f(D)=(1- b)^{-1}smoothstep(D, i - s/2, i+s/2) + b& eeimg=&1&&&br&&p&其中,b是亮度,表示阴影区域的亮度值(非阴影区域亮度值是1),D是经过模糊处理的有向距离值,s表示柔和度,换句话说表示了亮度从b到1过渡的区域宽度,也就是软阴影的宽度,i表示了膨胀或者腐蚀的强度,正值表示膨胀,负值表示腐蚀,0表示不膨胀也不腐蚀。&/p&&br&&p&&figure&&img src=&https://pic2.zhimg.com/v2-085bbb1a14e2bf0508fdba64b8451c78_b.jpg& data-rawwidth=&274& data-rawheight=&171& class=&content_image& width=&274&&&/figure&上图是这个转移函数的图像,结合有向距离场的定义再来直观地看这个转移函数其实很好理解,i可以理解为等高线的值,我们认为有向距离值小于i的都是阴影区域,b作为最低亮度很好理解,smoothstep的功能是让阴影边界不再是跃变(如果是step就变成了跃变)而是有一定过渡,过渡的区间中点由i决定,区间长度则由s来决定。&/p&&p&上述算法中计算量最大的部分是有向距离场的生成,因为最终效果和采样数量关系密切,因此很难做到完全实时,这大概也是《军团要塞2》最终没有集成这个算法的原因。&/p&&h2&&b&总结&/b&&/h2&&p&本来是想简单的就卡通渲染做一个概述,写着写着就扯了一堆相干和不相干的技术。总的来说这篇文章偏综述性质,没有涉及到太多具体的实现,实现部分可以参考我在引用中列出的一些文章。卡通渲染这个领域从我的理解来看,是一个经验大于理论,美术大于算法的领域,因此应该主动地接受更多地trick,不要过度地思考“为什么”,毕竟效果好看,直观上好理解即可。关于卡通渲染的在本文中的分类,可能不是一个准确的描述,但是也提供了一种思考角度:&b&有那么多的NPR相关技术,在一个具体的项目中我究竟要使用其中的哪些技术?这时候具体的画风要求就变得格外重要,因为画风决定了具体技术的选择和创新&/b&。文中的许多内容属于我自己对一些资料的解读,可能有许多理解上的错误,欢迎指正,另外今后我也会尽量避免写这么长的内容。。。因为码字真的不轻松:(&/p&&h2&&b&引用&/b&&/h2&&p&[1] &a href=&https://link.zhihu.com/?target=https%3A//engineering.riotgames.com/news/trip-down-lol-graphics-pipeline& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&A Trip Down The LOL Graphics Pipeline&/a&&/p&&p&&a href=&https://link.zhihu.com/?target=https%3A//engineering.riotgames.com/news/trip-down-lol-graphics-pipeline& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&[&/a&2] &a href=&https://link.zhihu.com/?target=http%3A//www.opengpu.org/forum.php%3Fmod%3Dviewthread%26tid%3D17071& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&西川善司[GUILTY GEAR Xrd -SIGN-]的[纯卡通动画的实时3D图形]前篇&/a&&/p&&p&[3] &a href=&https://link.zhihu.com/?target=http%3A//www.opengpu.org/bbs/forum.php%3Fmod%3Dviewthread%26tid%3D17072& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&西川善司[GUILTY GEAR Xrd -SIGN-]的[纯卡通动画的实时3D图形]后篇 &/a&&/p&&p&[4] &a href=&https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Cel_shading& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Cel Shading - Wikipedia&/a& &/p&&p&[5] Amy Gooch, Bruce Gooch, Peter Shirley, Elaine Cohen. A non-photorealistic lighting model for automatic technical illustration&/p&&p&[6] Pascal Barla, Jo?lle Thollot,
Lee Markosian. X-toon: an extended toon shader&/p&&p&[7] Jason Mitchell, Moby Francke, Dhabih Eng. Illustrative Rendering in Team Fortress 2&/p&&p&[8] &a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&风格化角色渲染实践 - 拳四郎&/a&&/p&&p&[9] Jason Mitchell, Gary McTaggart, Chris Green. Shading in Valve’s Source Engine&/p&&p&[10] Ken-ichi Anjyo, Katsuaki Hiramitsu. Stylized Highlights for Cartoon Rendering and Animation&/p&&p&[11] Christopher DeCoro, Forrester Cole, Adam Finkelstein, Szymon Rusinkiewicz. Stylized Shadows&/p&&p&[12] &a href=&https://link.zhihu.com/?target=http%3A//blog.csdn.net/candycat1992/article/details/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&【NPR】卡通渲染 &/a&&br&&/p&&p&[13] Jianbo Peng, Daniel Kristjansson, Denis Zorin. Interactive Modeling of Topologically Complex Geometric Detail&/p&&p&[14] Drew Card, Jason L. Mitchell. Non-Photorealistic Rendering with Pixel and Vertex Shaders&/p&
前言卡通渲染是图形学中一个有趣的话题,属于非真实感计算机图形学(NPR)的范畴,在NPR领域中也最多地被应用到实际游戏中,近年来流行的《守望先锋》,《英雄联盟》,《DOTA2》,《崩坏3》等游戏中都或多或少地出现过卡通渲染的身影,恰好最近对这个领域的…
&b&1.渲染&/b&&br&&ul&&li&利用reflect probe代替反射、折射,尽量不用RTT、GrabPass、RenderWithShader、CommandBuffer.Blit (BuiltinRenderTextureType.CurrentActive...)&/li&&li&建立统一后处理框架(bloom、hdr、DOF等)代替多后处理,可以共用模糊函数,减少多次blit;另外要注意RTT的尺寸。&/li&&li&空气折射、热浪扭曲等使用GrabPass不是所有硬件都支持,改为RTT或者后处理来优化。&/li&&li&建立统一shader材质代替单一shader,充分利用shader_feature、multi_compile,并将宏开关显示于界面。&/li&&li&图像混合代替多通道纹理,阴影投射、阴影接收、MetaPass、forwardadd 等pass不需要时要剔除。&/li&&li&少用alpha test、discard、clip、Alpha Converage等,因为会影响Early-Z Culling、HSR的优化。&/li&&li&避免Alpha Blend穿透问题(权重混合、深度剥离等透明排序方法代价太大了)。&/li&&li&光照贴图代替动态阴影、尽量不用实时光;阴影贴图、环境贴图用16位代替32位;利用projector+rtt或者光圈代替实时阴影。&/li&&li&将环境参数(风、雨、太阳)等shader全局参数统一管理。&/li&&li&非主角可以用matcap代替pbr、无金属不一定要用pbr,仔细选择物理渲染所用的FDG(F:schlick、cook-torrance、lerp、要求不高用4次方,D:blinn-phong、beckmann、GGX、GGX Anisotropic,G:neumann、cook-torrance、Kelemen、SmithGGX;standard shader要注意选择BRDF1-BRDF3),渲染要求不高时不用GGX;可以用LH来优化GGX。&/li&&li&用fixed、half代替float,建立shader统一类型(fixed效率是float的4倍,half是float的2倍),小心选择shader变量的修饰(uniform、static、全局),选择Mobile或Unlit目录下shader&/li&&li&使用高低配渲染,内存足够时可以考虑开启mipmap&/li&&li&使用surface shader注意关掉不用的功能,比如:noshadow、noambient、novertexlights、nolightmap、nodynlightmap、nodirlightmap、nofog、nometa、noforwardadd等&/li&&li&standard shader的变体太多(3万多),导致编译时间较长,内存占用也很惊人(接近1G),如果使用要关掉没用的shader_feature,比如:_PARALLAXMAP、SHADOWS_SOFT、DIRLIGHTMAP_COMBINED DIRLIGHTMAP_SEPARATE、_DETAIL_MULX2、_ALPHAPREMULTIPLY_ON;另外要去掉多余的pass&/li&&li&shaderforge、Amplify Shader Editor生成的shader有多余代码要程序专门优化,Amplify Shader Editor功能更强大一些,而且开源,建议学习。&/li&&li&不要用unity自带terrian,因为即使只用3张splat图,shader也是对应4个的,建议T4M或者转为mesh。&/li&&li&模型和材质相同且数量巨大时用Instance来优化,比如草。&/li&&li&利用查找纹理(LUT)来优化复杂的光照渲染,比如:皮肤、头发、喷漆等。&/li&&li&尽量不要使用Procedural Sky,计算瑞丽散射和米氏散射效率比较低。&/li&&li&尽量不要使用speedtree,改为模型加简单树叶动画,不过SpeedTreeWind.cginc里面的动画函数很丰富,TerrianEngine中的SmoothTriangleWave很好用。&/li&&li&多用调试工具检查shader性能,常用工具有:FrameDebug、Nsight、RenderDoc 、AMD GPU ShaderAnalyzer / PVRShaderEditor、Adreno Profiler 、腾讯Cube、UWA等;另外可以内置GM界面,比如开关阴影,批量替换shader等方便真机调试。&/li&&/ul&&b&2.脚本&/b&&br&&ul&&li&减少GetComponent、find等查找函数在Update等循环函数中的调用、go.CompareTag代替go.tag 、&/li&&li&减少SendMessage等同步函数调用;减少字符串连接;for代替foreach,5.5以后版本foreach已经优化过了;少用linq;&/li&&li&大资源改为异步加载&/li&&li&合理处理协程调用&/li&&li&将AI、网络等放在单独线程&/li&&li&发布优化:关闭log、剔除代码&/li&&li&伪随机&/li&&li&脚本挂载类改为Manager等全局类实现&/li&&li&lua中尽量不实现update、fixedupdate等循环函数,lua和csharp互调用的效率比较低。&/li&&/ul&&b&3.内存管理&/b&&br&&ul&&li&池子管理粒子、float UI等小资源,频繁地GC会造成卡顿&/li&&li&必要时主动调用GC.Collect()&/li&&li&按照不同资源、不同设备管理资源生命周期,Resources.Load和Assetbundle统一接口,利用引用计数来管理生命周期,并打印和观察生命周期。保证资源随场景而卸载,不常驻内存,确定哪些是预加载,哪些泄漏。&/li&&li&内存泄漏(减少驻留内存):Container内资源不remove掉用Resources.UnloadUnusedAssets是卸载不掉的;对于这种情况,建议直接通过Profiler Memory中的Take Sample来对其进行检测,通过直接查看WebStream或SerializedFile中的AssetBundle名称,即可判

我要回帖

更多关于 hpva7a9阳性怎么回事 的文章

 

随机推荐