WJsjtu / BabylonSource

1 stars 1 forks source link

源码注解 #1

Open WJsjtu opened 6 years ago

WJsjtu commented 6 years ago

说明

源码注解的部分放到issue是因为这里可以方便地引用代码片段,这一点对于源码的注解非常有用,但是有一点不好的是issue的引用貌似只能是本repository的代码,所以我之前的fork的版本并不能引用,因此这个仓库的代码是手动引入的,具体的Babylon.js的版本为4d21914cba29d3b9671abfb12719490ae59e7e05。

WJsjtu commented 6 years ago

正文一

我的研究源码的方式很暴力,就是随着流程走一遍,Babylon.js的主渲染的函数就是Scenerender方法: https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4490 这里是我源码研究的入口, https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4491-L4493 这一段没什么好说的,就是判断Scene是否析构了。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4494-L4498 _activeParticles_totalVertices_activeIndices_activeBones这些属性都是Scene的私有属性,也都是PerfCounter的实例。PerfCounter的是一种监视器,户主要负责两种监视类型:事件和数量。在这里,这些属性所监视的都是数量,所以每次开始新的计数前需要调用fetchNewFrame开始新的帧监视, 而在之后的更新中调用addCount(newCount: number, fetchResult: boolean)addCountfetchResulttrue时,表示这次的计数结束,PerfCounter会更新一些类似于平均值之类的信息。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4499 _meshesForIntersections的类型是SmartArrayNoDuplicatereset方法继承于模板类SmartArray<AbstractMesh>SmartArray是一个类似于C++ Vector的数据结构,其除了有Array的常用方法外,内部最主要的特性就是当容器内的元素超过设定值的时候,会将容器的上限调整为当前值的2倍,这样的设计本人猜测是因为设定固定长度的数组对于JS的执行速率有提升?但是,无论如何这些内部实现都是对外屏蔽的,所以并不需要太多的关心。SmartArrayNoDuplicate顾名思义就是没有重复元素的数组,值得注意的是:SmartArrayNoDuplicate所存储的类型必须能够被自由地添加属性(如:对象之类的,而number不行),这是 因为其内部会对于已存在与某个SmartArrayNoDuplicate的对象设定一个Map来记录哪些SmartArrayNoDuplicate存储了它,所以在调用pushNoDuplicate方法的时候就能很快地判断这个元素是否在当前的SmartArrayNoDuplicate实例中。当然这是一个空间换时间的算法:

reset方法就是更新内部id,然后将length设为0(避免数组清空导致的结构变化)的过程。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4500-L4501 resetCachedMaterial方法涉及3个属性:_cachedMaterial_cachedEffect_cachedVisibility,它们的类型分别为Nullable<Material>Nullable<Effect>Nullable<number>,这些类型具体是什么的先展开描述,但是从名字可以看出来这是用来比较的缓存值,在将来取值的时候就会通过判断要设置的值和已缓存的值是否相等来区分实现相关的逻辑。这里有个小的JS技巧,就是将null赋值给number类型,其实不只是number类型,null !== null是恒成立的,这个特性对于初始化缓存值相当有用,这会使得缓存在被设定为null之后必定会触发未缓存情况的逻辑。这样的方法也在Three.js中被经常使用。而resetCachedMaterial方法的工作就是将这3个属性设置为null。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4502-L4503 onBeforeAnimationsObservable涉及Babylon.js的ObservableObserver相关的内容,实际上虽然这部分的代码看似很多,但是没有必要去深究这些细节,它们的功能就是提供各种事件的hook,然后通过add/remove函数来添加和移除回调函数,就和用jQuery的on/off差不多。关于ObservableObserver相关的内容之后的源码注解部分都会跳过。

WJsjtu commented 6 years ago

正文二

https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4504-L4508

actionManagerActionManager的实例,解释这个ActionManager并不容易,但是Action系统是Babylon.js的重要的一部分,有必要深入了解一下。 在介绍Action相关的知识之前为了排除理解障碍,先讲一下ConditionActionActionManagerSceneMesh之间的关系。 首先,一个Action表示一个动作,这个动作会有触发条件和执行条件,触发条件由ActionManager管理,而执行条件由Condition来判断。但是ActionManager并不是能够独立存在的,它需要SenceMesh来构建,所以可以将ActionManager理解为SenceMesh的桥梁。这一部分的Babylon.js的设计比较混乱,几个类之间耦合得也比较紧,下面分开来解释。

Condition

ConditionAction的执行条件,话虽如此,但是其构造函数需要一个ActionManagerActionManager需要一个Scene……ORZ),这个可能是设计失误,为什么我敢这么说?很简单,虽然Condition用了ActionManager的方法,但是使用的只是ActionManger.prototype._getPropertyActionManger.prototype._getEffectiveTarget方法, https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L377-L392 看出来了吗?这两货完全可以是静态方法……所以这些完全是多此一举,而且只会占用scene._actionManagers。所以,讨论Condition的时候完全不需要理会ActionManagerCondition的功能是条件管理,实现的是一个比较器的功能,主要的被调用的方法为isValid(),在基类ConditionisValid()永远发返回true,所以个人认为这个类应该被声明为absctract更合适。 下面介绍一下一些Condition的派生类:

ValueCondition

最简洁的描述就是

isValid() { return target.property operation value; }

所以其构造函数除了actionManager之外还有target(什么对象)、property(的什么属性)、operation(进行什么比较?大于、等于、小于?)和value(比较的值是什么)。 比较方便的一点是property可以是多级属性字符串,比如:

target = t; property = "a.b.c";
//内部会自动先保存一个t.a.b对象,和一个key字符串"c"。
PredicateCondition

最简洁的描述就是

isValid() { return predicate(); }

所以要传入的就是一个函数predicate

StateCondition

可以理解为property === ‘state’的特化ValueCondition

ValueConditionPredicateConditionStateCondition都可以被序列化。 另外Condition_evaluationId_currentResult两个属性,从名字上就知道,这是为了缓存执行判断后的值而存在的,在Action中会对这两个值进行操作,从而使用缓存值来避免多余的计算。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.action.ts#L65-L85

Condition差不多就解释这么多,不懂没关系Condition其实只是一个Action的可选项,而且用得很少……

Action

读源码可能要懵上一会儿,这里就直接将结论:一个Action实际上就是一个循环链表,里面有表头和当前运行节点。 一个Action的表头就是this,next是_child属性,至于整个循环链表下一个要执行的Action节点是什么在_nextActiveAction属性中。循环链表的意思就是Action会一个一个的按顺序循环执行,当有Condition不通过的时候就什么都不做,直接将_nextActiveAction指向下一个Action。这里再补充三个细节:

事实上Action的直接应用并不多,这是因为:

Action派生类主要可以分为两类,体现在文件名字上:babylon.directActions.ts和babylon.interpolateValueAction.ts,directActions主要指即时执行的Action;interpolateValueAction是应用插值产生的过程Action,其实现依赖于Animation(等到解释动画的时候再解释吧)。具体的,directActions有:

截止这里,ActionCondition的关系、功能以及相关调用基本解释的差不多了,下面介绍ActionManager

ActionManager

之前介绍了Actiontrigger属性,但是如何触发呢?一般触发一个Action的顺序为:_prepare(), execute()。但是通常基本不会这样直接暴力地使用(如果你熟悉源码的话随便)。Action的触发一般是通过ActionManagerprocessTrigger来实现的,processTrigger会触发指定trigger类型的Action,那么有哪些触发类型呢? https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L58-L143 以上那么多,所以实际上你还可以扩充自己的类型只要不和这些冲突即可(好吧,这个也需要你了解源码),值得注意的是OnEveryFrameTriggertrigger只能在SceneactionManager触发。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L289-L304 那么,之前有一个问题,就是为什么ActionManager一定需要一个Scene呢?其实,从触发的类型就可以发现端倪了,Babylon.js中的鼠标、键盘事件都是Scene中完成的,这一部分的代码占了Scene代码的相当一部分,全部贴出来不合适,大致的调用关系:

Scene.prototype.attachControl()
    _onPointerUp()
        _initClickEvent()
            _processPointerUp()
                mesh.actionManagerprocessTrigger(ActionManager.OnPickTrigger, ...)

和用户输入相关的这些常用的拾取事件都需要scene._engine.getRenderingCanvas()来获取canvas,Babylon.js这么设计的原因是为了方便用户处理,Scene的这些事件还做了很多幕后工作,主要有两个部分:

最后用一个例子来讲解Action的使用:

var goToColorAction = new BABYLON.InterpolateValueAction(BABYLON.ActionManager.OnPickTrigger, light, "diffuse", color, 1000, null, true);
mesh.actionManager = new BABYLON.ActionManager(scene);
mesh.actionManager.registerAction(
    new BABYLON.InterpolateValueAction(BABYLON.ActionManager.OnPickTrigger, light, "diffuse", BABYLON.Color3.Black(), 1000))
    .then(new BABYLON.CombineAction(BABYLON.ActionManager.NothingTrigger, [ 
    // Then is used to add a child action used alternatively with the root action. 
        goToColorAction,
        new BABYLON.SetValueAction(BABYLON.ActionManager.NothingTrigger, mesh.material, "wireframe", false) 
        // First click: root action. Second click: child action. Third click: going back to root action and so on... 
]));

这一段代码来自于Playgroundscripts/actions.js,可以看到then是用于实现向链表追加Action的方法,但是值得注意的是then的多次调用会覆盖之前追加的Action(好像和Promise的区别?)。 看了这段代码的人可能会有疑问:new BABYLON.CombineAction(BABYLON.ActionManager.NothingTrigger...什么时候回触发?答案是,BABYLON.ActionManager.OnPickTrigger会触发,很奇怪?对,一个Action链表的触发条件只取决于表头的trigger,这一部分中的主体是lightInterpolateValueAction,它是整个循环链表的头,所以无论后面如何,这个action链表的触发条件只能是点击事件。 所以后面用一个CombineAction来同时完成goToColorAction和SetValueAction两个Action的触发条件也是OnPickTrigger,根NothingTrigger无关,同理goToColorAction中的OnPickTrigger也是无关紧要的,将它换成NothingTrigger是没有任何影响的。是不是很诡异?没错,谁叫processTrigger是依据表头来判断的呢? https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L349

回到最开始的scene.actionManager,它其实就是触发一个特定类型的Action而已,只不过scene.actionManager默认没有加Action,如果你想在Scene被渲染的每一帧做点事情的话就用它吧!

这一部分主要延伸出去讲了Action先关的东西,虽然和最开始的代码片段相距甚远,但是作为源码的注解,还是在必要的时候讲清楚一些重要的概念,这对于使用和理解Babylon.js都是有益的,不然的话那估计上面的那个例子没多少人能够一下子理解吧?

WJsjtu commented 6 years ago

正文三

https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4509-L4513

这里的文档讲得已经比较详细了。内部也就是一个队列和AsyncLoop的组合使用,最核心的就是对Mesh的LOD的处理,具体的在源码的原始的注释中也有论文的地址,有想详细了解这方面算法的可以去读相关文献。 https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4514-L4570 首先在了解这段代码前有必要跳出循环来了解一下游戏循环的一些特性。FPS是大家最为熟悉的一个名词,代表每秒渲染的帧数,通常情况下Web端动画的标准(最高)帧率是60FPS,换而言之就是1000/60毫秒/帧。在Babylon.js中循环有两种:固定间隔更新的DeterministicLockStep和普通更新。要了解两者的区别就必须提到Babylon.js的循环是怎么实现的,答:基于RAF。RAF的帧率不固定,那么开发的时候就势必不能假设每两帧之间的间隔相同,不然一个动画就会在不同的机器上以不同的速度播放,达不到同步的效果。而对于这种情况,我们通常的做法就是自己实现一个时间计数器,然后根据实际时间来处理相关的逻辑,而Babylon.js提供了DeterministicLockStep的更新策略,它实现的就是一个跟可能平均离散的触发器。比如你的逻辑代码中某件事情每16毫秒数就要执行一次(可以是简单的重力模拟,也可以是网络数据队列的处理,比如帧同步)那么你就需要在创建一个Engine的时候在options?: EngineOptions参数中设置相关的选项lockstepMaxStepsdeterministicLockstep。这样设置之后如果渲染的时候出现某一帧用了32ms,那么Babylon.js就会知道应该有2次帧的逻辑处理,如果是31ms,那么多余的15ms会在下一次渲染中计算,所以总结起来就是Babylon.js的deterministicLockstep使得开发者有能力在每16ms处理一个逻辑,处理的方法是使用hook:

newScene.onBeforeStepObservable.add(function(theScene){
  console.log("Performing game logic, BEFORE animations and physics for stepId: "+theScene.getStepId());
});

newScene.onAfterStepObservable.add(function(theScene){
  console.log("Performing game logic, AFTER animations and physics for stepId: "+theScene.getStepId());
});

可以看到,这个方法的缺陷也很大,它默认假设逻辑的执行频率为固定的16ms,这个值无法改变,而且动画相关参数比如scene._animationRatio会和具体的物理引擎有影响,物理引擎的更新频率也必须设置为1/60ms,而且无力引擎如果自带计时器还必须关闭。这就是为什么这里的CannonJS要这么设置的原因了。个人建议还是不要用了,自己实现一个比起里面的更加方便(除非你必须帧同步之类的逻辑?貌似自己实现也不复杂?)……

第二种就是最为常见的循环了,这个也是最常用的,要说有个鸡肋的设定就是Scene居然有一个useConstantAnimationDeltaTime属性,这货是用来欺骗动画和无力引擎的,无论每一帧是怎么渲染的Babylon.js都告诉他们它经过了一个固定的时间间隔,有什么用我也不太清楚……

说了这么多,好像就一个结论尽量不要去用deterministicLockstep?或许吧,但是至少也清楚了关于游戏循环的一些问题。 剥离开循环相关的,剩下的主要就几行代码的问题需要解释:

this._animate();
if (this._physicsEngine) {
    this.onBeforePhysicsObservable.notifyObservers(this); 
    this._physicsEngine._step(deltaTime / 1000.0); 
    this.onAfterPhysicsObservable.notifyObservers(this); 
} 

这些代码涉及动画和物理引擎,物理引擎在Babylon.js中是以插件的形式使用的,所以并不算主体,这里就先略过。

Animation

上述部分的主要还剩下this._animate();这句,这一看就知道是关于动画的,那么动画在Babylon.js中是如何使用的呢?这里Babylon.js一下子讲了许多,但是看完了会觉得不明所以,动画调用的API也非常多,参数与很多。这部分的注解会好好研究一下动画。首先,需要对整个Babylon.js是如何设计动画系统的有一个大概的了解。首先是动画的创建,正如文档中所说的Animation是主要的向用户提供创建动画的类,它实际上只是一个配置类,所谓配置类就是除了配置(一些创建动画配置的工具函数,详见Animation的许多静态方法)什么也不做。那么动画能动起来还需要三个方面的配合:与物体挂钩、循环执行、计算。完成计算的工作由RuntimeAnimation类完成,RuntimeAnimation的主要工作有:插值计算、保存运行状态、触发事件等。AnimationRuntimeAnimation构成一对多的关系,即一个配置可以有多个动画实例,这些实例之间的运行时机状态可能都不相同,所以这么设计是合理的。体现AnimationRuntimeAnimation一对多关系的是:一个Animation会有_runtimeAnimations数组属性,而RuntimeAnimation会有一个_animation属性。配置和计算完成后需要和物体相关联,这个工作由Animatable来实现,Animatable实例有_runtimeAnimations属性,不过一般不直接想这个属性赋值,而是调用appendAnimations方法来批量添加动画。Animatable一般也不会直接使用它,主要原因是:如同官方的例子,Scene的一些方法:beginWeightedAnimationbeginAnimationbeginDirectAnimationbeginDirectHierarchyAnimation这些方法都是调用Animatable的构造函数,并调用appendAnimations的处理流程,使用这些方法就用不到去处理。那么问题来了:

box1.animations = [];
box1.animations.push(animationBox);
scene.beginAnimation(box1, 0, 100, true);

这些语句是什么?很显然是将动画和物体挂钩,之前将Animation为配置类的时候没有深入,其实Babylon.js的动画配置都是基于属性的,比如官方例子中的scaling.x,这就意味着只要物体的scaling.x属性存在,那么都可以使用动画。我们知道上面这段代码中box1是一个Mesh,深入其继承关系:Mesh => AbstractMesh => TransformNode => Node,而Node本身的animations就是一个空数组,所以box1.animations = [];这一句让人感到有外挂感的语句是不需要的。最后,动画的循环执行是射来做的,还记得之前讲的引擎的循环吗?没错,最终所有的循环逻辑基本都是交给Scene来做的,就是<scene>this._animate();这句,那么什么时候动画和Scene有了关系呢?答案是AnimatableAnimatable的构造函数需要一个Scene参数,然后在构造的时候将自己压入Scene_activeAnimatables数组中,然后<scene>this._animate();这句中_activeAnimatables就会被调用管理(调用_animate方法后面再说). 所以综合了那么多,实际上最后的结论就是:尽量用Scene中的动画执行方法和Animation的配置。那么只是不是唯一的方式呢?答案:不是,Node也有beginAnimation方法,而且NodeAnimationRage的数据机构抽象了fromto参数。所以官方的例子还可以这样改写:

box1.animations.push(animationBox);
box1.createAnimationRange("myAnimation", 0 ,100);
box1.beginAnimation("myAnimation", true);

或者

box1.animations.push(animationBox);
box1.createAnimationRange(animationBox.name, 0 ,100);
box1.beginAnimation(animationBox.name, true);

貌似这样逻辑更加清楚而且符合对象编程?(按个人喜好用吧!)

和动画效果最为紧密的就是关键帧之间的插值计算,前面提到了计算工作主要由RuntimeAnimation来实现,所以通过阅读它的源码就能发现许多问题。插值计算主要集中在RuntimeAnimation.prototype._interpolate函数中。例子中提到了一种spline interpolations也就是浮点数、向量、颜色、四元组也就是旋转)的样条插值,相比起默认的线性插值,样条插值允许通过顺滑的曲线来实现插值,这种对于属性的某个值的变化程度在某几帧当中剧烈变化的时候可以起到平滑的作用,具体的设置规则是当前关键帧的outTangent和下一个关键帧的inTangent为参数,同时设置才生效,具体的样条插值可以自行学习。但是有的时候我们不洗碗时基于线性插值的动画比如弹簧动画这些,那么easingFunction选项可以改变插值计算的函数。另外一个值得注意的是矩阵插值需要开启Animation.AllowMatricesInterpolation选项(默认是不开启的)。如果你想要的不是插值动画,而只是几帧PPT动画,那么在某一帧上设置interpolation属性为AnimationKeyInterpolation.STEP即可……。

讲述了动画的使用,但是关于动画的许多参数细节,官方的例子只是简单的带过了,其实里面有不少重要的点,与其说重要倒不如说不提你就不知道。enableBlendingblendingSpeed是什么?其实这个过程是发生在动画计算执行的尾声的,在RuntimeAnimation.prototype.animate/goToFrame的最后部分在计算出插值的值之后,会进行RuntimeAnimation.prototype.setValue操作,在setValue中用一种好理解的方式就是:

this._currentValue = this._originalBlendValue * (1.0 - this._blendingFactor) + this._blendingFactor * currentValue;

_blendingFactor是随着时间按照blendSpeed增加的变量,最大值为1。当_blendingFactor为1时,_currentValue将就是插值的值,直接赋给对象的属性。这一过程中,动画将从_originalBlendValue逐渐变回设定的动画。但是这个版本的源码存在着错误:https://github.com/WJsjtu/BabylonSource/blob/0f45bd83265500291838afabb4f139c7fcaca7a8/src/Animations/babylon.runtimeAnimation.ts#L302 显然不应该用prototype来判断因为_originalBlendValue不是函数……,而截至目前为止https://github.com/BabylonJS/Babylon.js/blob/54bd6963463f988ac6980420f43d577a5562536f/src/Animations/babylon.runtimeAnimation.ts#L330-L342 仍然是错的,因为只有null的派生类(如:Object.create(null))才没有constructor。另外,对于有weight的动画,RuntimeAnimation会调用Scene_registerTargetForLateAnimationBinding方法最后合成属性上的动画,这也就是<scene>this._animate()函数体内的最后部分所处理的_processLateAnimationBindings

至于动画类型:Animation.ANIMATIONLOOPMODE_CYCLEAnimation.ANIMATIONLOOPMODE_CONSTANTAnimation.ANIMATIONLOOPMODE_RELATIVE,前两种并没有看出来什么区别,有待进一步研究。Animation.ANIMATIONLOOPMODE_RELATIVE就是累计求和,内部会有一个offset*repeat的求和器。

WJsjtu commented 6 years ago

正文四

https://github.com/WJsjtu/BabylonSource/blob/0f45bd83265500291838afabb4f139c7fcaca7a8/src/babylon.scene.ts#L4571-L4576 这一段代码就不讲了……关于游戏手柄,我并没有手柄,还有XBOX……

itancc commented 1 year ago

大佬还继续更新吗?