Open WJsjtu opened 6 years ago
我的研究源码的方式很暴力,就是随着流程走一遍,Babylon.js的主渲染的函数就是Scene
的render
方法:
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)
当addCount
的fetchResult
为true
时,表示这次的计数结束,PerfCounter
会更新一些类似于平均值之类的信息。
https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/babylon.scene.ts#L4499
_meshesForIntersections
的类型是SmartArrayNoDuplicate
,reset
方法继承于模板类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的Observable
和Observer
相关的内容,实际上虽然这部分的代码看似很多,但是没有必要去深究这些细节,它们的功能就是提供各种事件的hook,然后通过add/remove
函数来添加和移除回调函数,就和用jQuery的on/off
差不多。关于Observable
和Observer
相关的内容之后的源码注解部分都会跳过。
actionManager
是ActionManager
的实例,解释这个ActionManager
并不容易,但是Action
系统是Babylon.js的重要的一部分,有必要深入了解一下。
在介绍Action
相关的知识之前为了排除理解障碍,先讲一下Condition
、Action
、ActionManager
、Scene
和Mesh
之间的关系。
首先,一个Action
表示一个动作,这个动作会有触发条件和执行条件,触发条件由ActionManager
管理,而执行条件由Condition
来判断。但是ActionManager
并不是能够独立存在的,它需要Sence
和Mesh
来构建,所以可以将ActionManager
理解为Sence
和Mesh
的桥梁。这一部分的Babylon.js的设计比较混乱,几个类之间耦合得也比较紧,下面分开来解释。
Condition
是Action
的执行条件,话虽如此,但是其构造函数需要一个ActionManager
(ActionManager
需要一个Scene
……ORZ),这个可能是设计失误,为什么我敢这么说?很简单,虽然Condition
用了ActionManager
的方法,但是使用的只是ActionManger.prototype._getProperty
和ActionManger.prototype._getEffectiveTarget
方法,
https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L377-L392
看出来了吗?这两货完全可以是静态方法……所以这些完全是多此一举,而且只会占用scene._actionManagers
。所以,讨论Condition
的时候完全不需要理会ActionManager
。
Condition
的功能是条件管理,实现的是一个比较器的功能,主要的被调用的方法为isValid()
,在基类Condition
中isValid()
永远发返回true
,所以个人认为这个类应该被声明为absctract
更合适。
下面介绍一下一些Condition
的派生类:
最简洁的描述就是
isValid() { return target.property operation value; }
所以其构造函数除了actionManager
之外还有target
(什么对象)、property
(的什么属性)、operation
(进行什么比较?大于、等于、小于?)和value
(比较的值是什么)。
比较方便的一点是property
可以是多级属性字符串,比如:
target = t; property = "a.b.c";
//内部会自动先保存一个t.a.b对象,和一个key字符串"c"。
最简洁的描述就是
isValid() { return predicate(); }
所以要传入的就是一个函数predicate
。
可以理解为property === ‘state’
的特化ValueCondition
ValueCondition
、PredicateCondition
、StateCondition
都可以被序列化。
另外Condition
有_evaluationId
、_currentResult
两个属性,从名字上就知道,这是为了缓存执行判断后的值而存在的,在Action
中会对这两个值进行操作,从而使用缓存值来避免多余的计算。
https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.action.ts#L65-L85
Condition
差不多就解释这么多,不懂没关系Condition
其实只是一个Action的可选项,而且用得很少……
读源码可能要懵上一会儿,这里就直接将结论:一个Action
实际上就是一个循环链表,里面有表头和当前运行节点。
一个Action
的表头就是this
,next是_child
属性,至于整个循环链表下一个要执行的Action
节点是什么在_nextActiveAction
属性中。循环链表的意思就是Action
会一个一个的按顺序循环执行,当有Condition
不通过的时候就什么都不做,直接将_nextActiveAction
指向下一个Action
。这里再补充三个细节:
事实上Action的直接应用并不多,这是因为:
Action
作为基类_prepare
和execute
是空函数Action
的派生类Action派生类主要可以分为两类,体现在文件名字上:babylon.directActions.ts和babylon.interpolateValueAction.ts,directActions
主要指即时执行的Action;interpolateValueAction
是应用插值产生的过程Action
,其实现依赖于Animation
(等到解释动画的时候再解释吧)。具体的,directActions
有:
SwitchBooleanAction
:实现看起来很复杂,实际上就是把某个对象的某个属性(布尔值)取反,执行的语句:effectTarget[property] = !effectTarget[property]
,支持多级属性字符串。SetStateAction
:target = t, property = "state" value = v
的特化的SwitchBooleanAction
,执行的语句:target.state = value
。SetValueAction
:SwitchBooleanAction
和SetStateAction
的结合体, 执行的语句:effectTarget[property] = value
。有个细节:拥有markAsDirty
方法的target
会调用markAsDirty(property)
方法(比如Material
)。IncrementValueAction
:类似SetValueAction
,执行的语句:effectTarget[property] += value
.PlayAnimationAction
、StopAnimationAction
, 执行的语句:scene.beginAnimation(target, from, to, loop);``scene.stopAnimation(target, from, to, loop);
。DoNothingAction
:什么都不做,空节点。CombineAction
: 一次触发执行多个Action
,CombineAction
其实像是一个树的root
。ExecuteCodeAction
:就是执行函数SetParentAction
:这个比较特殊,执行的语句:
if (this._target.parent === this._parent) {
return;
} else {
//更新target的矩阵
}
可以推测出来,这种Action
是针对具有坐标的对象,比如Node
。
PlaySoundAction
、StopSoundAction
:播放、暂停音频interpolateValueAction
:涉及许多动画的东西,这里只需要知道它能够对一些数据类型,比如:颜色、向量、数字、矩阵进行插值动画。值得注意的细节是:interpolateValueAction
只能支持100帧的动画,而且无法更改这个数字, 你只能通过更改帧率(帧/秒)来操纵动画的执行事件,所以当然当帧率很低的时候就变成幻灯片了……另外,构造函数的stopOtherAnimations
参数设为true
还可以使得该事件被触发时停止其他的在指定物体上的动画Action
。截止这里,Action
和Condition
的关系、功能以及相关调用基本解释的差不多了,下面介绍ActionManager
。
之前介绍了Action
有trigger
属性,但是如何触发呢?一般触发一个Action
的顺序为:_prepare()
, execute()
。但是通常基本不会这样直接暴力地使用(如果你熟悉源码的话随便)。Action
的触发一般是通过ActionManager
的processTrigger
来实现的,processTrigger
会触发指定trigger
类型的Action
,那么有哪些触发类型呢?
https://github.com/WJsjtu/BabylonSource/blob/e47d2e7e43b3303373ff66f8d00562da7a87ab7a/src/Actions/babylon.actionManager.ts#L58-L143
以上那么多,所以实际上你还可以扩充自己的类型只要不和这些冲突即可(好吧,这个也需要你了解源码),值得注意的是OnEveryFrameTrigger
的trigger
只能在Scene
的actionManager
触发。
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
的这些事件还做了很多幕后工作,主要有两个部分:
Scene.prototype.pick
)内部进行处理,大大方便了开发。有兴趣的可以深入了解一下这方面的RayCaster
算法。
ActionManager
注册一个Action
链表的方式是registerAction
方法,值得注意的是,由于Action
链表是循环的,如果你要取消循环,记得在链表的最后一个Action
被执行之后用unregisterAction
方法取消注册。最后用一个例子来讲解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...
]));
这一段代码来自于Playground
的scripts/actions.js
,可以看到then
是用于实现向链表追加Action
的方法,但是值得注意的是then
的多次调用会覆盖之前追加的Action
(好像和Promise的区别?)。
看了这段代码的人可能会有疑问:new BABYLON.CombineAction(BABYLON.ActionManager.NothingTrigger...
什么时候回触发?答案是,BABYLON.ActionManager.OnPickTrigger
会触发,很奇怪?对,一个Action
链表的触发条件只取决于表头的trigger
,这一部分中的主体是light
的InterpolateValueAction
,它是整个循环链表的头,所以无论后面如何,这个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都是有益的,不然的话那估计上面的那个例子没多少人能够一下子理解吧?
这里的文档讲得已经比较详细了。内部也就是一个队列和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
参数中设置相关的选项lockstepMaxSteps
和deterministicLockstep
。这样设置之后如果渲染的时候出现某一帧用了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中是以插件的形式使用的,所以并不算主体,这里就先略过。
上述部分的主要还剩下this._animate();
这句,这一看就知道是关于动画的,那么动画在Babylon.js中是如何使用的呢?这里Babylon.js一下子讲了许多,但是看完了会觉得不明所以,动画调用的API也非常多,参数与很多。这部分的注解会好好研究一下动画。首先,需要对整个Babylon.js是如何设计动画系统的有一个大概的了解。首先是动画的创建,正如文档中所说的Animation
是主要的向用户提供创建动画的类,它实际上只是一个配置类,所谓配置类就是除了配置(一些创建动画配置的工具函数,详见Animation
的许多静态方法)什么也不做。那么动画能动起来还需要三个方面的配合:与物体挂钩、循环执行、计算。完成计算的工作由RuntimeAnimation
类完成,RuntimeAnimation
的主要工作有:插值计算、保存运行状态、触发事件等。Animation
与RuntimeAnimation
构成一对多的关系,即一个配置可以有多个动画实例,这些实例之间的运行时机状态可能都不相同,所以这么设计是合理的。体现Animation
与RuntimeAnimation
一对多关系的是:一个Animation
会有_runtimeAnimations
数组属性,而RuntimeAnimation
会有一个_animation
属性。配置和计算完成后需要和物体相关联,这个工作由Animatable
来实现,Animatable
实例有_runtimeAnimations
属性,不过一般不直接想这个属性赋值,而是调用appendAnimations
方法来批量添加动画。Animatable
一般也不会直接使用它,主要原因是:如同官方的例子,Scene
的一些方法:beginWeightedAnimation
、beginAnimation
、beginDirectAnimation
、beginDirectHierarchyAnimation
这些方法都是调用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
有了关系呢?答案是Animatable
,Animatable
的构造函数需要一个Scene
参数,然后在构造的时候将自己压入Scene
的_activeAnimatables
数组中,然后<scene>this._animate();
这句中_activeAnimatables
就会被调用管理(调用_animate
方法后面再说).
所以综合了那么多,实际上最后的结论就是:尽量用Scene
中的动画执行方法和Animation
的配置。那么只是不是唯一的方式呢?答案:不是,Node
也有beginAnimation
方法,而且Node
用AnimationRage
的数据机构抽象了from
、to
参数。所以官方的例子还可以这样改写:
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
即可……。
讲述了动画的使用,但是关于动画的许多参数细节,官方的例子只是简单的带过了,其实里面有不少重要的点,与其说重要倒不如说不提你就不知道。enableBlending
和blendingSpeed
是什么?其实这个过程是发生在动画计算执行的尾声的,在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_CYCLE
、Animation.ANIMATIONLOOPMODE_CONSTANT
和Animation.ANIMATIONLOOPMODE_RELATIVE
,前两种并没有看出来什么区别,有待进一步研究。Animation.ANIMATIONLOOPMODE_RELATIVE
就是累计求和,内部会有一个offset*repeat
的求和器。
https://github.com/WJsjtu/BabylonSource/blob/0f45bd83265500291838afabb4f139c7fcaca7a8/src/babylon.scene.ts#L4571-L4576 这一段代码就不讲了……关于游戏手柄,我并没有手柄,还有XBOX……
大佬还继续更新吗?
说明
源码注解的部分放到issue是因为这里可以方便地引用代码片段,这一点对于源码的注解非常有用,但是有一点不好的是issue的引用貌似只能是本repository的代码,所以我之前的fork的版本并不能引用,因此这个仓库的代码是手动引入的,具体的Babylon.js的版本为4d21914cba29d3b9671abfb12719490ae59e7e05。