【游戏引擎之路】登神长阶(十六)——宁可碎此身,终不起此座——最难的挑战,游戏动画开发

【游戏引擎之路】登神长阶(十六)——宁可碎此身,终不起此座——最难的挑战,游戏动画开发

2024年 5月20日-6月4日:攻克2D物理引擎。
2024年 6月4日-6月13日:攻克《3D数学基础》。
2024年 6月13日-6月20日:攻克《3D图形教程》。
2024年 6月21日-6月22日:攻克《Raycasting游戏教程》。
2024年 6月23日-7月1日:攻克《Windows游戏编程大师技巧》。
2024年 7月2日-7月6日:攻克《雅达利2600汇编游戏开发》。
2024年 7月7日-7月11日:攻克《x86/x64汇编语言》。
2024年 7月11日-7月22日:学习《3D游戏编程大师技巧》(阶段性)。
2024年 7月14日-7月18日:学习《游戏引擎架构》(完成)。
2024年 7月23日-7月30日:攻克Python语言学习。
2024年 7月31日-8月5日:攻克《3D游戏编程大师技巧》。
2024年 9月10日-9月20日:攻克游戏动画绑定
2024年 10月27日-10月31日:攻克《C++大师教程》
2024年 10月21日-11月02日:攻克《DirectX11教程》
2024年 11月02日-11月06日:攻克《CMake教程》
2024年 11月06日-11月10日:攻克《Vulkan教程》
2024年 11月11日-11月13日:攻克《OpenGL教程》
2024年 11月14日-11月29日:攻克《DirectX12龙书》
2024年 11月14日-11月29日:攻克《DirectX12龙书》
2024年 11月29日-2025年1月16日:《心火引擎》基础渲染部分,UI,基础游戏框架
2025年 1月16日-2月12日:载入骨骼部分。能够使蒙皮骨骼,动画载入。但有诸多问题。
2025年 2月19日-3月8日:制作了完整的小哪吒模型。自已的引擎,必须要自已建模适配。
2025年 3月10日-3月23日:制作骨骼动画,面对各种问题,各种崩溃,无法解决。
2025年 3月24日-3月27日:攻克《Lua教程》。把LUA加入引擎解决c++编译时间问题。
2025年 3月28日-4月11日:进行Maya的学习。掌握了Maya软件。
2025年 4月14日-4月19日:宁可碎此身,终不起此座!终于完美解决动画问题。
————————————————

(一)引子


很久没有感受到抑郁症受心态的影响这么大了。
我自从过冬之后,士气一直很低落,虽然不说到了心里面压了一块石头那么严重,但是做游戏也一点动力没有。我把这个归到我的抑郁症,想着开春之后会好些。随着运动起来了,我也渐渐恢复了状态,但是还是没有以前的那种动力。因为我在去年,尤其是搞引擎的时候,曾经有一个时期,真的那种每天晚上睡觉,都期待着自已第二天做什么的时候。我感觉那种情况回不来了。
但是,今天这种情况回来了。为什么?因为我解决了动画的问题,感觉天高任鸟飞,海阔凭鱼跃!我状态一下就来了。
所以说,当一个人走入困境的时候,真的士气会低低落,不停地自我怀疑,这时候真的需要什么东西支撑你。我当时学了Maya,然后再次转回动画制作的时候,我是真想着放弃自已的引擎开发,如果这次尝试再不成功,我就先去做小游戏了。
但是我心里面不甘啊。于是我4月14日的第一次更新里面,提交给github(gitee)的commit就写这一段话:
宁可碎此身,终不起此座!
这是本师释迦摩尼在悟道之前的誓言。最终七七四十九天,终于证得无上菩提。
而我解决问题的过程用了六天,当然,这个六天是从说这句话的时间算起,如果算上整个动画制作的时间,是三个月。这其中每天都遇到各种各样的问题,各种头秃。而且我的头痛非常严重,这种困境加重了头痛。还好之前想起来吃天麻,吃了一周的天麻,感觉状况好多了。
这不管怎么说,终于解决问题,能够完美实现动画的那一天,感觉自已整个人都被解放了!

(二)为什么骨骼动画这么难


以前我自已在做游戏引擎的时候,我也去了解了不少单人开发的自研引擎,我发现这些引擎都有一个共同的特点,就是不支持骨骼动画。我当时觉得很奇怪,现在终于明白了。
在最初的时候我没有认识到这个问题,觉得问题会非常简单。比如我在教程中学习M3D动画,只用了2天便使3D动画运行起来了。而且就连我小时候,我学习《windows游戏编程大师》的时候,那时候我用雷神引擎的模型做动画,那是顶点动画,我也做好了。我是实在没有想到动画这么难。
骨骼动画难的地方在于,从载入到运算的每一个环节都可能出错。而且最变态的是,即便是如错了,因为矩阵运算的特殊性,你可能得到一个正确的结果。就比如说,我制作骨骼Pose的时候(即没有动画,仅绑定数据),我就用完全错误的运算,得到了一个完全正确的结果。
这多可怕!
我们程序员里面有一句老话,叫“崩溃是一件好事情”。因为你运行的程序崩溃了,也就意味着你能够准确地定位到BUG。所以在程序员里面也有争论,到底是让程序更健壮更好,比如说,即便是得到了错误的结果,也能够运行,还是使程序易崩溃越好,比如说,只要有一个环节出错,程序就立刻崩溃。
这里没有正确答案,但是我偏向后者。比如说,我的载入程序就没有SafeGuard,只要文件丢失,立刻程序就崩溃。照理来说,就算是一个图片不见了,程序也应该能够运行。不过我让他立刻崩溃,这样就能够立刻找到问题,至少开发的时候这样做最好。


(三)我如何一步一步解决这个问题


下面我将一路解释我如何解决问题,我希望自已的思路和经验也能够对大家带来帮助。以后我会专门写一篇技术文章来讨论骨骼动画中的要点。

1、M3D动画

我学习了M3D的动画载入。这里面包括了骨骼动画的每一个要点,我基本上都了解了。当时感觉没什么难度,尤其是我3D数学现在基础已经很好了,里面的东西都不难理解。骨骼动画就是组合从Root到骨骼的矩阵组合。

2、Assimp系统

我学习Assimp的教程。我这里找的是OpenGL的教程,在网上OpenGL的教程最多,因为这东西最简单,任何其它的引擎,比如DirectX,Vulkan之流,都需要一套复杂的前置机制,导致这些东西不好做教程。然后就是这个教程是OpenGL的,导致我之后被坑出翔。

3、蒙皮骨骼权重问题

我第一步完成骨骼权重的载入,这里不涉及动画。但是很快,我发现自已的骨骼崩溃了。这个BUG尤其严重在一个地方,我最初从Blender中自已建立了一个模型,用于测试,一切都正常。但是到了载入复杂的骨骼的时候,却出现问题了。然后我又去倒过去检查简单的模型,发现一些都正常。
这就是我上面所说的,程序员不怕崩溃,就怕正常。最后我才发现问题出在我的教程里面的一个语法糖上面,因为在教程中,为了展示自已的“技巧”,用于骨骼权重的信息保存只用了3个数据。使用了一个XMFLOAT3来存储。最后一个权重,通过1-x-y-z就能够计算得到,这样就省下一个空间。但是我在重新建立起我的系统的时候,没有发现这个问题,因为程序员有句老话,能够运行的代码,就不要去动。我在测试最初的骨骼的时候,什么问题都没有,所以这段隐藏在代码中的陷阱就被我忽视掉了。
而为什么最初的骨骼被我忽视掉?因为这个骨骼只有三段。如果我开始就做了一个有四段骨骼的动画,也许问题就能被找到。
这是属于我的一个失误导致的,也有经验,就是别太相信能够完好运行的代码。他不出错,可能是因为从来没有遇到其它的情况。

4、蒙皮骨骼姿势问题

我解决了这个问题之后,然后就着手解决Pose的问题。也就是不带任何骨骼动画,让Pose立起来。这里遇到的第一个问题就是行主序和列主序的问题。最奇葩的事情是,我当时都意识到这个问题了,但是我用一个完全错误的方式,认为我做的事情没有错。这东西怎么来的呢?
第一个阶段,因为用Assimp载入的aiMatrix3d是列主序,而DirectX的矩阵是行主序,所以是不是要做一个转换?于是我做了转化。但是我发现做了转化之后,却得不到正确的结果。因为我当时载入项目,计算矩阵的方式是从OpenGL中抄来的,而这里有一个问题,就是OpenGL变换矩阵的乘法是“从右到左”,即先应用的变换写在最右边,后应用的变换写在最左边。而Direct是反过来的。
想想看,为什么我当时意识到了行主序和列主序,但是为什么没有解决这个问题?就是因为在数据运算中,矩阵乘法是有结合率的,也就是A*(B*C)等于(A*B)*C。你左乘右乘有什么区别呢?
而且最离谱的是,我用了错误的思路,但得到了正确的结果。就是说,当我的核心代码是抄OpenGL的时候,我的骨骼动画是右乘的,也就是说,Root的东西放在左边。但如果你采用了这种乘法,那你一开始将aiMatrix3d转化为XMFLOAT4X4的时候,你就不能使用转置,你就得保持原来的样子,否则就要出错。但是,你其实应该进行转化的。
当时我解决这个问题,我一开始是抄的OpenGL的代码,但是后来出了问题,我靠自已的理解,然后自已重新写了代码,一行一行都是靠自已的理解写的。这虽然花费了更多的功夫,但是让我对骨骼动画的理解也强化了。
正因为对这个理解的强化,所以导致我能够剖析每一个环节,最终能够使Pose得到一个正确的结果。但是,这个“正确的结果”最终导致一个神坑,我直到最后的最后才解决这个问题。
换句话说,我这个阶段的努力,就是实现了“在DirectX中成功进行OpenGL方式的运算”。因为矩阵运算是纯粹的数学,CPU不会管你的行主序还是列主序,A*B*C能运算,A(T)*B(T)*C(T)也能运算((T)表示转置矩阵)。如果你采用了OpenGL的右乘机制,那么你一开始将矩阵的主序调整和OpenGL一样,在数学上就能够得到一个正确的结果。
这个阶段我解决了各种各样的坑,比如说,我一开始用XMMATRIX来保存数据,因为DirectX不知道为什么,使用了一种巨坑爹,巨反人类的矩阵演算方式。他用SIMD的东西,需要用XMMATRIX和XMVECTOR来计算,然后保存数据的时候用XMFLOAT2/3/4,或者XMFLOAT4X4这种。然后你在使用的时候,要用XMStore或者XMLoad这类的函数将其转化之后再计算。一个小小的计算,要倒来倒去很多次。这让我觉得非常痛苦,所以我一开始的时候,很多地方为了省事,直接用XMMATRIX或者XMVECTOR传递数据,我觉得都是数据,传递应该不会有问题的啊?但是也不知道怎么回事,就是各种BUG,最后我没办法,只有严格按照DirectX的规则来。

5、蒙皮骨骼动画问题

上面说到,我做骨骼Pose的时候,用了一个“在DirectX中成功进行OpenGL方式的运算”,然后使我得到了非常正确的Pose,任何骨骼载入,Pose都是对的,就是默认的Pose。
但是我那时候还不知道,我的这个实现,完全是属于“瞎猫碰到死耗子”式的成功。
在我看来,一个Pose就已经完整的实现了骨骼所需要的各种要素,而动画只不过改变Pose其中的骨骼矩阵,用动画的矩阵去替换掉NodeTransform这个节点的矩阵而已。
我当时花了大量的时间解决Pose问题,而且从载入节点的时候全部代码都自已写,基础也打牢固了。然后我觉得自已又牛逼起来了。但是我一做动画,完全崩溃,各种动画数据崩溃。
我当时真的觉得游戏引擎太难了,尤其是为什么很多游戏引擎居然不支持骨骼动画,或者要么是自已专有的动画格式,这其中是有道理的。因为一两个数据的影响,就导致各种问题,而你调式这个问题的过程,却无比困难,这真的是大海捞针。
在别的地方,你出现什么问题,然后顺藤摸瓜找就是了,然而骨骼动画一系列的过程,在某些时候你用了错误的东西,却能得到正确的结果。
我当时真的彻底崩溃了,说实话,我自从当程序员以来,从来没有遇到过自已解决不了的问题,从来没有,没有一个。
因为程序说到底就是运算,不存在解不开的东西。但那可能是我以前从来没有涉及这种最底层,最数据性的东西。因为矩阵运算就是最后的数学,在它的下面已经没有封装的东西了,要再下面就是硬件了。
那时候我想着没办法,放弃就放弃吧。尤其是我的抑郁症那时候就又上来了,头痛也很厉害。然后我面对这种情况,我怎么做呢?我就自已骗自已,因为我有抑郁症,抑郁症的人就是这样的,思想就比较消极,我有放弃的想法很正常,我脑子有病,但这不一定是我真实的想法,因为我很不甘,就不甘这么失败。
于是我开始学习Maya。为什么要学习Maya?用Blender不行吗?还真不行。
为什么?
这就得说起Blender坑爹的问题了。我之前学习的软件就是Blender,因为Blender最适合我这种单干的游戏独立开发者。然后我用Blender用得很顺手,尤其是我学习了Maya之后,更觉得Blender顺手了,Maya很多东西简直反人类。
但Blender有一个非常严重的问题,和FBX不太兼容。我不知道是不是Blender故意的,因为Blender的开发者对Autodesk公司有巨大的敌意,他在一次采访中就直说,他搞Blender就是为了干翻Autodesk的。
Blender的模型有什么问题?当他导出一个模型的时候,首先人需要旋转90度,然后核心的骨骼是100倍。如果你把一个Blender导出的FBX拖动到Unity中,你就可以看到,他核心的骨骼是100倍缩放,而且旋转了90度。
Unity虽然能够适应这种问题,但这本身就是一个坑。而且会导致各种问题。就比如说,你如果用DynamicBone插件制作物理骨骼,他里面的各种指示器就会出错,因为DynamicBone根本不适应那种骨骼有100倍缩放的情况。
这里顺便给大家提供一个解决方案,如果你要在Unity中使用Blender制作的模型,那么你别用Blender导出FBX,而是直接将Blender文件拖动到Unity里面,这样,Unity在一开始的时候,就会对Blender模型进行处理。
Blender里面有一个“应用变换”,可以把比例改成1:1,但是它这个功能也是纯坑的,因为他自已就在后面有一个三角警告标志:说这个是实验功能,已知骨骼和动画会崩溃。事实上也是如此,的确会崩溃,根本用不了。
然后我又找到另外一个解决方式,导出glb文件。glb导出的文件用Assimp可以成功载入,没有任何问题。但是,奇怪的是,glb支持动画,但是Blender导出的glb文件里面却没有动画,动画不见了。
所以我只能用FBX导出动画,但问题又回来了,你glb里面是一个正常的,1:1的,没有90度旋转的模型,但是,在FBX的动画里面,里面的数据可都是100倍加90度旋转的模型啊!
所以那个时候,我真的感觉四面楚歌,这也不对,那也不对。因为Blender除了坐标系和Direct不一样之外,他的轴向也有问题,他的Z轴是向上的,我也不知道为什么他要做一个这样奇葩的设计,就是为了和Autodesk对着干吗?
其它我所有知道的工具也好,引擎也好,都是Y轴向上,X左右,Z轴前后。左右手坐标系只是Z轴反转而已,只有Blender这么奇葩。这就导致了他导出的模型,其中就自带了一个90度的旋转。
要知道,我调试东西,最重要的就是看每一步自已有什么变化,而Blender这种一开始就自带旋转,自带缩放的东西,导致我的调试非常困难。
没办法,于是我找一个Maya教程来进行学习,。当然要学习我就学习全套,而且非得学动画不可,所以从建模到骨骼绑定到动画,花了半个月的时间,终于把全套学到了。
然后我再回来进行调试。

5、宁可碎此身,终不起此座


其实我在学Maya的时候,每天都在自我质疑,内耗非常严重,所以那段时间非常痛苦。因为我感觉自已走上一条非常长非常长的路,却不知道这路走不走得通。我当时学完Maya,都有些绝望了,我当时给自已一个目标,说如果一周时间再也找不到突破口,做不好动画,那么我就去做我的《星火盟约》,先做一个小游戏再说。总之挺悲观的。
但是我又不服气。于是我14号重新写代码的时候,我更新到github中的commit就写着“宁可碎此身,终不起此座”。我是真不信了。
然后我学习了Maya之后,使用Maya的模型进行调试,Maya的好处是导出的FBX非常纯净,没有什么额外的100倍加90度旋转,而且左右手坐标系转化非常简单,Assimp可以完全处理。
然后我就从最简单的动画开始做起,先平移X,Y,Z。然后旋转。当然,各种各样的问题接着出现,比如说Z轴反转了,旋转出问题了,我在这个开发过程解决各种各样的问题。甚至我最终做到了简单动画完全正确,单根骨骼无论是位移、旋转都不会出现问题。但是不知道为什么,到了多段骨骼的时候,就会出问题。

然后我真的彻底绝望了。打算放弃了。
然后我就去找到原来的M3D代码,然后再重写一遍。我之前学M3D动画是在学教程的时候做的,只是了解其中的东西,并没有认真思考其中的含义。但是这次重写的时候,我发现了一个核心的关键,DirectX的矩阵是左乘的,最先进行变换在最左边,也就是说,Parent应该在右边。
但是,为什么我原来用右乘就是对的呢?最后顺腾摸瓜找到了最初的那个aiMatrix3d矩阵转换。我终于明白了,这一开始就必须进行行列的转置,然后用这个来依次解决每个矩阵的乘法。OpenGL教程中的顺序不能用于DirectX。
当然,还有些其它问题,比如说FBX载入中,会有一些"$AssimpFbx$"这类的奇葩节点,需要合并,不然会导致问题等等。

还有一些看似对,也不会出问题的东西。比如说四元数转欧拉角,我就有两套不同的转换,然后看上去都对,但是结果却不能匹配。这些都属于小BUG。

这些东西都属于小问题。

(四)结语


最终,我解决了这个问题。关于骨骼动画的核心代码差不多只有2000行,可能还不到。但是,花费的功夫却长达
3个月。
在这个期间,我把我的调试工具写得非常好。能够把每一个动画,每一个矩阵都打印得清清楚楚,进行单帧的分析,每个矩阵怎么算出来,是什么结果我都知道,我是做梦都没有想到,我一个游戏开发者,有天也会做这种事情。
因为在我最初的想法中,我怎么说也是一个应用层的开发者,怎么会搞到这种深的底层去呢?尤其是我还自已学习了建模。
所以我真的非常庆幸自已当初认真学了建模,因为如果我不懂自已建模,不能把动画分解为最简单的元素,一个一个去调试结果,我如果只有那些网上下的模型,动画,一开始就是一整套完全的骨骼动画,我根本没有办法解决这个问题。
只有我自已学了建模,把复杂的任务分解为了简单的任务,再加上对于3D数学的理解,最终才能解决这个问题。
凡做难事,必有所得,这里用一句孟子的话与大家共勉吧:
“故天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。人恒过,然后能改;困于心,衡于虑,而后作;征于色,发于声,而后喻。入则无法家拂士,出则无敌国外患者,国恒亡。然后知生于忧患,而死于安乐也。”
附录:
这是我打印的一个动画的数据。最简单的,只有一个骨骼,只有一帧,只有一个旋转:
 

---------------------------------------------
Print AnimationClip: Take 001
Channels:
[0]: Spine
[1]: Spine_end
---------------------------------------------
-- Spine[0]: --
000f: P[0.0 0.0 -0.0] R[-0.0 -0.0 -90.0] S[1.0 1.0 1.0]
010f: P[0.0 0.0 -0.0] R[-26.6 -90.0 -90.0] S[1.0 1.0 1.0]
-- Spine_end[1]: --
010f: P[1.0 0.0 -0.0] R[-90.0 -0.0 -0.0] S[1.0 1.0 1.0]

-- END --

userdata(0000025924DA7628) userdata(0000025924DA7E48) userdata(0000025924DA87A8)
nil nil nil
nil nil nil
Amount: 0.000000
PlayAnime false


== Caluclate Hierarchy ==

-- RootNode --
Current Mat:P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
Node Trans :P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
Parent Mat :P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
    
-- Cube --
    Current Mat:P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
    Node Trans :P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
    Parent Mat :P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
    
-- Spine_$AssimpFbx$_Rotation --
    Current Mat:P[0.0 0.0 0.0] R[90.0 0.0 -135.0] S[1.0 1.0 1.0]
    Node Trans :P[0.0 0.0 0.0] R[90.0 0.0 -135.0] S[1.0 1.0 1.0]
    Parent Mat :P[0.0 0.0 0.0] R[-0.0 0.0 -0.0] S[1.0 1.0 1.0]
        
-- Spine --
        Calc VEC: P[0.0 0.0 0.0] R[-14.0 -90.0 -90.0] S[1.0 1.0 1.0]
        Calc MAT: P[0.0 0.0 0.0] R[-14.0 -90.0 -90.0] S[1.0 1.0 1.0]
        Current Mat:P[0.0 0.0 0.0] R[0.0 0.0 0.0] S[1.0 1.0 1.0]
        Node Trans :P[0.0 0.0 0.0] R[-14.0 -90.0 -90.0] S[1.0 1.0 1.0]
        Parent Mat :P[0.0 0.0 0.0] R[90.0 0.0 -135.0] S[1.0 1.0 1.0]
        -- Bone: Spine --
        Bone Offset:P[0.0 0.0 -0.0] R[0.0 -0.0 -90.0] S[1.0 1.0 1.0]
        Bone Final :P[0.0 0.0 0.0] R[0.0 0.0 0.0] S[1.0 1.0 1.0]
            
-- Spine_end --
            Calc VEC: P[1.0 0.0 -0.0] R[-90.0 -0.0 -0.0] S[1.0 1.0 1.0]
            Calc MAT: P[1.0 0.0 0.0] R[-90.0 0.0 -0.0] S[1.0 1.0 1.0]
            Current Mat:P[1.0 0.0 0.0] R[-90.0 0.0 0.0] S[1.0 1.0 1.0]
            Node Trans :P[1.0 0.0 0.0] R[-90.0 0.0 -0.0] S[1.0 1.0 1.0]
            Parent Mat :P[0.0 0.0 0.0] R[0.0 0.0 0.0] S[1.0 1.0 1.0]


== Set Transforms ==
--Spine--
P[0.0 0.0 0.0] R[0.0 0.0 0.0] S[1.0 1.0 1.0]
Load Scene: Anime
Load Time: 19.52s

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值