今天是六一,先祝你们六一快乐!距上次发文章已通过了快一个月,工做有点忙,因此有点拖更,见谅~c#
咱们此次来研究一下Crawler(爬虫)示例。官方其实还有个示例——Reacher,可是这个示例比较简单,就是模拟一个带两个关节的手臂去跟随目标物体,其核心就是让咱们学会如何利用Configurable Joint来进行训练,相比之下,Crawler就复杂得多,所以咱们跳过Reacher这个示例,感兴趣的童靴能够本身去研究一下。dom
先来看看Crawler示例的效果:ide
能够看到此次小蓝变成了一只四脚爬虫,每只脚上有两个关节,其任务就是经过四肢协调运动找到绿色方块。大概能够预想到,这个示例在开始训练的时候先要解决的问题就是小蓝怎么经过四肢协调站起来不摔倒,而后进行移动,最后才是找到绿色方块。函数
此外,该示例有两个场景:CrawlerDynamicTarget和CrawlerStaticTarget,分别是动态目标物和静态目标物,上图展现的就是动态生成目标物,绿色的方块会产生到随机的位置,然后者是绿色方块就在小蓝的前方。所以咱们直接研究较难的CrawlerDynamicTarget示例,CrawlerStaticTarget也就迎刃而解了。学习
设定:有四只胳膊和四只前臂的生物。测试
目标:Agent必须移动它的身体朝目标方向移动而不摔倒。ui
CrawlerStaticTarget
:目标方向一直在前方。CrawlerDynamicTarget
:目标会随机改变位置。Agents:环境中包含8个相同行为参数的agent。this
Agent奖励设定(独立的):spa
Note:这里来讲一下,为何奖励这样设定。首先要明白点积的概念:从几何意义来说,如有两个向量a、b,则a·b = |a||b|cos(a, b),由此可推出:pwa
①当a·b>0,两向量方向基本相同,夹角在0°到90°之间;
②当a·b=0,两向量正交,相互垂直;
③当a·b<0,两向量方向基本相反,夹角在90°到180°之间。
所以,当小蓝和目标速度或朝向大于90°时,实际上是在奖励负数,由此来迫使小蓝面朝目标物而且向目标物前进,这里的设置其实还比较巧妙,能够注意一下。
行为参数:
可变参数:无。
基准平均奖励:
CrawlerStaticTarget
:2000。
CrawlerDynamicTarget
:400。
这个示例的场景很简单:
其中,CrawlerSettings里有一个AdjustTrainingTimescale脚本,该脚本就是经过数字键来改变Time.timeScale属性的,说是能够在训练的时候用,本次训练的时候我会试一下改变Time.timeScale可否加快训练速度。
而后来详细讲一下训练单元:
训练单元里的Walls、Ground以及Target都如同直译,没啥特别说的,主要看一下Crawler:
Crawler身上有5个脚本,BehaviorParameters是行为参数脚本,只要有继承Agent的脚本,则会自动附加该脚本;CrawlerAgent则是agent训练脚本;DecisionRequester会按期自动为agent请求决策,若是没有该脚本,则须要手动调用Agent.RequestDecision()方法,不过以前的例子其实Agent上都有这个脚本,我之前忘记讲了;JointDriveController控制各个关节,具体还有什么做用一下子代码解析的时候再来看;ModelOverrider这个脚本在0.15.0里是没有的,这个脚本能够先不用管它,这个脚本的做用大概是在训练前,在Console里输入指定的命令,容许在训练期间覆盖代理的.nn模型文件。
除此以外,Crawler的Body还有一个GroundContact脚本,该脚本是用来检测Crawler是否摔倒,即头部触地,此时能够对agent惩罚1,并从新开始新的Episode,这两个都是可选的:
Crawler有四只前臂和四只腿,再加一个Body:
它们是以Configuration Joint(关节组件)两两相连的,以一部分为例:
这里小提一下可配置关节 (Configurable Joint),该组件具备很是强的自定义性,具体能够看下图:
能够配置的参数很是多,但其实不少都是基础的参数,各个参数是如何限制该关节的,建议你们本身下去后再去深刻研究,咱们这里大概能够看一下Crawler的四肢是如何运动的:
首先看前臂:
前臂的绕X、Y角旋转没被锁定(Locked),其他的都被锁定了。
再看一下后臂:
后臂的只有绕X轴是没被锁定的,其他的都被锁定了。
这样组合的结果就是(灵魂画手,能看懂便可。。。):
前臂能够绕自身中心轴转,能够绕body上下转,然后臂只能绕前臂上下。该关节配置构成了Crawler运动的基础。
每一个后臂还有一个脚的子物体(就是那个小圆球),我看官方原本是想在脚接触地面时令小球换个材质,不接触地面时又是另外一种材质,可是示例中最终并无使用该方法,咱们能够来试试这个功能:
创建两个材质球Red和Green,而后分别将这两个材质球拖到Crawler预制体的Agent脚本上,并勾选Use Foot Grounded Visualization选项:
运行的话咱们就会有如下效果:
脚基础地面就变成绿色,不接触地面就是红色。
本示例的代码首先须要理解Agent身上的JointDriveController脚本,该脚本用于设置Crawler的肢体关节协调(其余三个示例Walker、Warm也同样用到该脚本),同时该脚本中还包括BodyPart脚本,用于存储agent中每一个肢体部分的相关信息。
咱们先来看一下BodyPart结构,是如何对Crawler的身体作存储的。
/// <summary> /// 用于存储agent每一个身体部位的行动和学习相关信息 /// </summary> [System.Serializable] public class BodyPart { [Header("Body Part Info")] [Space(10)] public ConfigurableJoint joint;//身体的可配置关节组件 public Rigidbody rb;//刚体 [HideInInspector] public Vector3 startingPos;//起始位置 [HideInInspector] public Quaternion startingRot;//起始角度 [Header("Ground & Target Contact")] [Space(10)] public GroundContact groundContact;//检测地面接触 public TargetContact targetContact;//检测目标接触 [FormerlySerializedAs("thisJDController")] [HideInInspector] public JointDriveController thisJdController;//关节组件Controller [Header("Current Joint Settings")] [Space(10)] public Vector3 currentEularJointRotation;//关节当前欧拉角 [HideInInspector] public float currentStrength;//当前做用力 public float currentXNormalizedRot; public float currentYNormalizedRot; public float currentZNormalizedRot; [Header("Other Debug Info")] [Space(10)] public Vector3 currentJointForce;//当前关节做用力 public float currentJointForceSqrMag;//当前关节做用力大小 public Vector3 currentJointTorque;//当前关节转矩 public float currentJointTorqueSqrMag;//当前关节转矩大小 public AnimationCurve jointForceCurve = new AnimationCurve();//关节做用力曲线 public AnimationCurve jointTorqueCurve = new AnimationCurve();//关节力矩曲线 /// <summary> /// Reset body part to initial configuration. /// 身体关节初始化 /// </summary> public void Reset(BodyPart bp) { bp.rb.transform.position = bp.startingPos;//位置 bp.rb.transform.rotation = bp.startingRot;//角度 bp.rb.velocity = Vector3.zero;//速度 bp.rb.angularVelocity = Vector3.zero;//角速度 if (bp.groundContact) {//地面接触标志置位 bp.groundContact.touchingGround = false; } if (bp.targetContact) {//目标接触标志置位 bp.targetContact.touchingTarget = false; } } /// <summary> /// 根据给定的x,y,z角度和力的大小计算扭矩 /// </summary> public void SetJointTargetRotation(float x, float y, float z) { x = (x + 1f) * 0.5f; y = (y + 1f) * 0.5f; z = (z + 1f) * 0.5f; //Mathf.Lerp(from : float, to : float, t : float) 插值,t=0~1,返回(to-from)*t var xRot = Mathf.Lerp(joint.lowAngularXLimit.limit, joint.highAngularXLimit.limit, x); var yRot = Mathf.Lerp(-joint.angularYLimit.limit, joint.angularYLimit.limit, y); var zRot = Mathf.Lerp(-joint.angularZLimit.limit, joint.angularZLimit.limit, z); //Mathf.InverseLerp(from : float, to : float, value : float)反插值,返回value在from和to之间的比例值 currentXNormalizedRot = Mathf.InverseLerp(joint.lowAngularXLimit.limit, joint.highAngularXLimit.limit, xRot); currentYNormalizedRot = Mathf.InverseLerp(-joint.angularYLimit.limit, joint.angularYLimit.limit, yRot); currentZNormalizedRot = Mathf.InverseLerp(-joint.angularZLimit.limit, joint.angularZLimit.limit, zRot); joint.targetRotation = Quaternion.Euler(xRot, yRot, zRot);//使关节转向目标角度 currentEularJointRotation = new Vector3(xRot, yRot, zRot);//当前关节欧拉角 } /// <summary> /// 设置关节做用力大小 /// </summary> /// <param name="strength"></param> public void SetJointStrength(float strength) { var rawVal = (strength + 1f) * 0.5f * thisJdController.maxJointForceLimit; var jd = new JointDrive { positionSpring = thisJdController.maxJointSpring,//关节最大弹力 positionDamper = thisJdController.jointDampen,//关节弹性大小 maximumForce = rawVal//施加的最大力 }; joint.slerpDrive = jd; currentStrength = jd.maximumForce;//当前施加的力 } }
以上代码说难不难,说简单也不简单。。。主要是涉及到Joint组件的使用,这里面牵扯到一些力学知识,我就不望文生义了,有兴趣的同窗能够深刻研究一下。
/// <summary> /// Joint控制器 /// </summary> public class JointDriveController : MonoBehaviour { [Header("Joint Drive Settings")] [Space(10)] public float maxJointSpring;//关节最大弹力大小 public float jointDampen;//关节抵抗弹力的强度 public float maxJointForceLimit;//最大做用力 //float m_FacingDot;//该变量没用到 //身体部位字典 [HideInInspector] public Dictionary<Transform, BodyPart> bodyPartsDict = new Dictionary<Transform, BodyPart>(); /// <summary> /// 建立BodyPart对象并将其添加到字典中 /// </summary> public void SetupBodyPart(Transform t) { var bp = new BodyPart { rb = t.GetComponent<Rigidbody>(), joint = t.GetComponent<ConfigurableJoint>(), startingPos = t.position, startingRot = t.rotation }; bp.rb.maxAngularVelocity = 100;//最大角速度为100 //添加地面碰撞检测脚本 bp.groundContact = t.GetComponent<GroundContact>(); if (!bp.groundContact) { bp.groundContact = t.gameObject.AddComponent<GroundContact>(); bp.groundContact.agent = gameObject.GetComponent<Agent>(); } else { bp.groundContact.agent = gameObject.GetComponent<Agent>(); } //添加目标碰撞检测脚本 bp.targetContact = t.GetComponent<TargetContact>(); if (!bp.targetContact) { bp.targetContact = t.gameObject.AddComponent<TargetContact>(); } bp.thisJdController = this; bodyPartsDict.Add(t, bp); } /// <summary> /// 更新身体每一部分当前的做用力及转矩 /// </summary> public void GetCurrentJointForces() { foreach (var bodyPart in bodyPartsDict.Values) {//轮询身体每部分 if (bodyPart.joint) { bodyPart.currentJointForce = bodyPart.joint.currentForce;//当前关节做用力 bodyPart.currentJointForceSqrMag = bodyPart.joint.currentForce.magnitude;//当前关节做用力大小 bodyPart.currentJointTorque = bodyPart.joint.currentTorque;//当前关节做用转矩 bodyPart.currentJointTorqueSqrMag = bodyPart.joint.currentTorque.magnitude;//当前关节做用转矩大小 if (Application.isEditor) {//IDE下,建立关节做用力和关节力矩的曲线 if (bodyPart.jointForceCurve.length > 1000) { bodyPart.jointForceCurve = new AnimationCurve(); } if (bodyPart.jointTorqueCurve.length > 1000) { bodyPart.jointTorqueCurve = new AnimationCurve(); } bodyPart.jointForceCurve.AddKey(Time.time, bodyPart.currentJointForceSqrMag); bodyPart.jointTorqueCurve.AddKey(Time.time, bodyPart.currentJointTorqueSqrMag); } } } } }
这个脚本主要是将多个BodyPart进行管理的做用,同时能够实时更新身体每一部分做用力及转矩,用以Agent收集
BodyPart的相关信息。
以上两个脚本我注释的比较粗略,主要是对Joint组件的不熟悉形成的,该组件使用的细节我就不深刻讲解了,咱们主要能弄清楚ml-agents是如何对这种多关节复杂的agent进行训练的就能够了。
/// <summary> /// 该脚本包含了agent可能与地面接触的关节运动的逻辑。经过该脚本,能够设置某些身体部位若是接触地面后作出惩罚 /// </summary> [DisallowMultipleComponent] //不可重复挂载特性 public class GroundContact : MonoBehaviour { [HideInInspector] public Agent agent;//对应的agent //当接触地面时,是否令agent置位 [Header("Ground Check")] public bool agentDoneOnGroundContact; //是否在接触地面时惩罚agent public bool penalizeGroundContact; //接触地面惩罚的数值 public float groundContactPenalty; //接触地面标志 public bool touchingGround; //地面物体的tag const string k_Ground = "ground"; /// <summary> /// 检测碰撞是否为地面 /// </summary> void OnCollisionEnter(Collision col) { if (col.transform.CompareTag(k_Ground)) {//碰撞到地面 touchingGround = true; if (penalizeGroundContact) {//惩罚agent agent.SetReward(groundContactPenalty); } if (agentDoneOnGroundContact) {//使得agent从新开始 agent.EndEpisode(); } } } /// <summary> /// 检查地面碰撞是否结束,并使其标志复位 /// </summary> void OnCollisionExit(Collision other) { if (other.transform.CompareTag(k_Ground)) { touchingGround = false; } } }
该脚本在小蓝的每一个BodyPart上都有挂载,咱们能够来详细看一下:
由上图可知,当agent的leg以及Body接触地面后,会使agent惩罚1,并使得agent从新开始新的一轮训练;而foreLeg接触地面则不作任何惩罚。
同时能够留意一下脚本一开始的[DisallowMultipleComponent]特性,该特性可以使得脚本组件只在同一物体上存在一个。
下面咱们来看一下Crawler的Agent脚本。
/// <summary> /// Crawler的Agent脚本 /// </summary> [RequireComponent(typeof(JointDriveController))]//要求JointDriveController脚本同时存在 public class CrawlerAgent : Agent { [Header("Target To Walk Towards")] [Space(10)] public Transform target;//目标方块 public Transform ground;//地面 public bool detectTargets;//检测目标标志 public bool targetIsStatic;//目标物是不是静态的 public bool respawnTargetWhenTouched;//当为true时,到达目标后,目标会从新随机到其余地方 public float targetSpawnRadius;//目标随机位置半径 //各部分BodyPart [Header("Body Parts")] [Space(10)] public Transform body; public Transform leg0Upper; public Transform leg0Lower; public Transform leg1Upper; public Transform leg1Lower; public Transform leg2Upper; public Transform leg2Lower; public Transform leg3Upper; public Transform leg3Lower; [Header("Joint Settings")] [Space(10)] JointDriveController m_JdController;//Joint控制器 Vector3 m_DirToTarget;//小蓝到目标物的方向 float m_MovingTowardsDot;//小蓝速度方向与目标方向的点积 float m_FacingDot;//小蓝正方向与目标方向的点积 [Header("Reward Functions To Use")] [Space(10)] public bool rewardMovingTowardsTarget;//速度方向与目标方向奖励是否开启 public bool rewardFacingTarget;//小蓝正方向与目标方向点积是否开启 public bool rewardUseTimePenalty;//是否随时间流逝而惩罚 [Header("Foot Grounded Visualization")] [Space(10)] public bool useFootGroundedVisualization;//是否使脚接触地面改变材质 public MeshRenderer foot0; public MeshRenderer foot1; public MeshRenderer foot2; public MeshRenderer foot3; public Material groundedMaterial;//接触地面脚的材质 public Material unGroundedMaterial;//未接触地面脚的材质 Quaternion m_LookRotation;//小蓝到目标方向四元数 Matrix4x4 m_TargetDirMatrix;//目标方向旋转矩阵 /// <summary> /// Agent初始化 /// </summary> public override void Initialize() { m_JdController = GetComponent<JointDriveController>();//得到Joint控制器 m_DirToTarget = target.position - body.position;//小蓝到目标方向向量 //Setup each body part //设置身体每一部分 m_JdController.SetupBodyPart(body); m_JdController.SetupBodyPart(leg0Upper); m_JdController.SetupBodyPart(leg0Lower); m_JdController.SetupBodyPart(leg1Upper); m_JdController.SetupBodyPart(leg1Lower); m_JdController.SetupBodyPart(leg2Upper); m_JdController.SetupBodyPart(leg2Lower); m_JdController.SetupBodyPart(leg3Upper); m_JdController.SetupBodyPart(leg3Lower); } }
这段代码主要注意一下上面将要使用的变量。
/// <summary> /// 观测值收集 /// </summary> /// <param name="sensor"></param> public override void CollectObservations(VectorSensor sensor) { m_JdController.GetCurrentJointForces();//更新身体每一部分当前的做用力及转矩 //更新小蓝到目标的方向 m_DirToTarget = target.position - body.position;//向量agent到target m_LookRotation = Quaternion.LookRotation(m_DirToTarget);//获取小蓝正向到目标向量的四元数 m_TargetDirMatrix = Matrix4x4.TRS(Vector3.zero, m_LookRotation, Vector3.one);//将上述四元数转换为旋转矩阵 //Body到地面的高度(下方测量值) RaycastHit hit; if (Physics.Raycast(body.position, Vector3.down, out hit, 10.0f)) { sensor.AddObservation(hit.distance); } else sensor.AddObservation(10.0f); //前方、上方测量值收集 //获取body的正向到目标方向转换的相对向量 var bodyForwardRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(body.forward); sensor.AddObservation(bodyForwardRelativeToLookRotationToTarget); //获取body的上方到目标方向转换的相对向量 var bodyUpRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(body.up); sensor.AddObservation(bodyUpRelativeToLookRotationToTarget); foreach (var bodyPart in m_JdController.bodyPartsDict.Values) {//收集身体每一部分的测量值 CollectObservationBodyPart(bodyPart, sensor); } } /// <summary> /// 将每一个身体部位的相关信息添加到观察中 /// </summary> public void CollectObservationBodyPart(BodyPart bp, VectorSensor sensor) { var rb = bp.rb; //是否接触地面 sensor.AddObservation(bp.groundContact.touchingGround ? 1 : 0); //bp速度方向相对于目标方向的相对矢量,即速度与目标方向关系 var velocityRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(rb.velocity); sensor.AddObservation(velocityRelativeToLookRotationToTarget); //bp角加速度方向相对于目标方向的相对矢量,即角速度与目标方向关系 var angularVelocityRelativeToLookRotationToTarget = m_TargetDirMatrix.inverse.MultiplyVector(rb.angularVelocity); sensor.AddObservation(angularVelocityRelativeToLookRotationToTarget); if (bp.rb.transform != body) {//除了body以外的部分,获取每一部分(肢体)的相对位置,x、y、z当前角度以及当前做用力 var localPosRelToBody = body.InverseTransformPoint(rb.position); sensor.AddObservation(localPosRelToBody); sensor.AddObservation(bp.currentXNormalizedRot); // Current x rot sensor.AddObservation(bp.currentYNormalizedRot); // Current y rot sensor.AddObservation(bp.currentZNormalizedRot); // Current z rot sensor.AddObservation(bp.currentStrength / m_JdController.maxJointForceLimit); } }
这部分代码我认为是Crawler示例中比较重要的部分,由于从中能够学习到如何对于多关节的复杂问题进行数据收集,这里面又涉及到一些角度转换的问题,例如四元数、转换矩阵操做等。
整体来,这部分数据主要以下:小蓝forward到目标的相对旋转关系,小蓝up到目标的相对关系,每一部分速度方向、角速度相对于目标方向关系,每部分肢节的做用力及旋转角度等。
这里有兴趣的童靴能够仔细研究一下,我这里主要来搞一些四元数的相关用法。
四元数(Quaternion)
网上关于四元数的文章应该不少了,我按我得理解写一下,有错误请指正。
首先说到四元数,主要用到的地方就是三维世界中物体的旋转,对于三维世界中描述物体的旋转,咱们通常有三种方法表示:旋转矩阵、欧拉角、四元数。
以一个点p为例,以上述三种方法旋转获得p',则有:
旋转矩阵
旋转矩阵乘以点p的齐次坐标,获得旋转后的的点p':
另,绕x,y,z轴旋转θ的矩阵为:
欧拉角
欧拉角描述旋转,是咱们一般用的方式,例以下图,能够将其旋转分解为三步(蓝色为起始坐标系,红色为旋转后的坐标系):
先绕z轴旋转α,再绕x轴旋转β,最后绕z轴旋转γ。固然这里的旋转顺序并非规定死的,在Unity中,旋转的顺序是ZXY顺序。
那么对应于以上欧拉角的旋转矩阵为:
不过欧拉角有个解决不了的问题,即“万向节死锁”问题,同时使用欧拉角也不能进行平滑插值。
四元数
四元数实际上是一种高阶复数,它能够很方便的描述物体绕任意轴的旋转,四元数q能够表示为:
其中,i、j、k知足:
同时四元数又能够写成一个向量和一个实数的组合形式:
四元数能够看做是向量和实数的更加通常的形式,咱们普通用的向量能够视为实部为0的四元数,而实数能够视为虚部为0的四元数,由此能够获得一些四元数符合实数或者向量的运算性质(感兴趣的同窗能够本身去查,例如四元数的乘法、共轭四元数、四元数的逆等)。
利用四元数来刻画三维空间中的旋转,令点p绕单位向量(x,y,z)表示的轴旋转θ,则可申明一个四元数q:
再令咱们要旋转的p点写成四元数的形式p(P,0)(至关于虚部为p,实部为0),则旋转后的p'能够用如下公式计算:
固然这个公式的右边能够看到,是三个四元数的乘法,最后获得的p'也是一个四元数。
Unity中四元数的API
上面咱们讲了三种旋转方式,都只是比较简单的讲解,有许多细节其实并无涉及到,其实能够分别利用三种方法对一个点去计算旋转后的位置,这样能够更加深入的加深印象。
在Unity中,咱们大多数使用的是欧拉角来描述物体的旋转,但其实四元数更加方便,功能更增强大。可是四元数的实部和虚部若是你不是很了解,则不要去修改它们,这里咱们只是解析一下Quaternion的一些API用法。
Quaternion.AngleAxis(float angle, Vector3 axis)
这个方法其实就是四元数的原本用法,即绕某轴axis旋转angle角度,例如,使得一个Cube绕Unity中x轴旋转45度,则有
q = Quaternion.AngleAxis(45, Vector3.right);Cube.transform.rotation = q;
上式中q为任意四元数,效果以下图:
初始位置,蓝色线为物体自身z轴,绿色为y轴,红色为x轴。
变换后:
固然,咱们也能够利用该函数使得Cube绕任意轴旋转angle角度,咱们在场景中放置一个轴:
而后让Cube绕这这根轴旋转45度,既有:
q = Quaternion.AngleAxis(45, Axis.transform.up);Cube.transform.rotation = q;
Quaternion.LookRotation(Vector3 forward, Vector3 upwards = Vector3.up)
这个函数其实就是让物体的前方指向forward方向,物体的上方指向upwards方向(可不赋值)。例如,我如今要让Cube的前方指向下,上方指向前,则有:
q = Quaternion.LookRotation(Vector3.down, Vector3.forward);Cube.transform.rotation = q;
固然这个函数就能够衍生出一些还玩的用法,例如同步两个物体的旋转,咱们引入一个Target球体:
如今使得方块的前方与球体的上方一致,方块的上方与球体的后方一致,将代码在Update中执行:
q = Quaternion.LookRotation(Target.transform.up, -Target.transform.forward);Cube.transform.rotation = q;
则有:
能够看到上图中方块的前方(蓝色轴)一直与球体的上方(绿色轴)一致,而方块的上方(绿色轴)一直与球体的后方一致。
除此以外,咱们还能够利用该方法使得方块一直面向球体:
q = Quaternion.LookRotation(Target.transform.position);Cube.transform.rotation = q;
固然这样写有个弊端,就是方块的位置不能移动,若是移动的话,则该方法失效:
能够看到将方块上移一些,则不能看向目标球体了,所以咱们能够对这段代码改造一下:
var vec = Target.transform.position - Cube.transform.position;q = Quaternion.LookRotation(vec);Cube.transform.rotation = q;
这样就能够一直使得方块的前方指向目标球体了。
Quaternion.FromToRotation(Vector3 fromDirection, Vector3 toDirection)
这个函数主要是将某个方向fromDirection指向另外一个方向toDirection,例如将Cube的前方指向Cube的下方,则有:
q = Quaternion.FromToRotation(Cube.transform.forward, -Cube.transform.up);
固然,除此以外,咱们会发现若是使用Quaternion.LookRotation能够令方块前方一直看向目标球体,那若是想让方块的上方一直看向目标球体怎么办呢?这里就须要使用Quaternion.FromToRotation来操做了,一开始你可能会写出如下代码:
q = Quaternion.FromToRotation(Cube.transform.up, Target.transform.position);
可是这段代码是有问题的,会使得Cube产生抖动:
因此这里实际上是应该这么写:
q = Quaternion.FromToRotation(Vector3.up, Target.transform.position);
这样就能实现方块的上方一直指向目标球体,与上面同理,再次改造一下,使得方块移动位置也能够指向球体,则有:
var vec= Target.transform.position - Cube.transform.position;q = Quaternion.FromToRotation(Vector3.up, vec);
OK,到这里咱们对于四元数这几个函数就讲解到这,算是抛砖引玉,若是理解有什么不正确的地方还请留言指正。附上测试的代码,注释能够本身进行撤销去测试,基本都是上面讲解中涉及到的代码:
public class QuaTest : MonoBehaviour { public GameObject Cube; public GameObject Axis; public GameObject Target; private Quaternion q; void Update() { //向右(x) Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.right, Color.red); Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.right, Color.red); //向前(z) Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.forward, Color.blue); Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.forward, Color.blue); //向上(y) Debug.DrawLine(Cube.transform.position, Cube.transform.localPosition + Cube.transform.up, Color.green); Debug.DrawLine(Target.transform.position, Target.transform.localPosition + Target.transform.up, Color.green); if (Input.GetKeyDown(KeyCode.Q)) { //Cube绕x轴旋转45度 //q = Quaternion.AngleAxis(45, Vector3.right); //Cube绕Axis自定义轴旋转45度 //q = Quaternion.AngleAxis(45, Axis.transform.up); //绕x轴旋转90度 //q = Quaternion.Euler(90, 0, 0);//欧拉角实现 //q = Quaternion.LookRotation(Vector3.down, Vector3.forward);//令物体的前方指向下,上方指向前 //q = Quaternion.AngleAxis(90, Vector3.right);//令物体绕右轴(x轴)旋转90度 //q = Quaternion.FromToRotation(Vector3.forward, Vector3.down);//令物体的前方指向物体的下方,不能使用自身坐标系 //q = Quaternion.FromToRotation(Cube.transform.forward, -Cube.transform.up);//令物体的前方指向物体的下方,不能使用自身坐标系 Cube.transform.rotation = q; } //令方块前方与球体上方一致,方块上方与球体后方一致,即令方块的旋转与球的旋转同步 //q = Quaternion.LookRotation(Target.transform.up, -Target.transform.forward); //令方块一直面向目标球体,若Cube自身坐标变了,则失效 //q = Quaternion.LookRotation(Target.transform.position); //令方块一直面向目标球体,Cube自身坐标变也一直面向 //var vec = Target.transform.position - Cube.transform.position; //q = Quaternion.LookRotation(vec); //令方块上方一直看向目标球体 //q = Quaternion.FromToRotation(Cube.transform.up, Target.transform.position);//若是使用自身的向上向量,会产生抖动 //q = Quaternion.FromToRotation(Vector3.up, Target.transform.position); var vec= Target.transform.position - Cube.transform.position; q = Quaternion.FromToRotation(Vector3.up, vec);//注,这里须要使用世界坐标向上,而不能使用自身坐标系的向上向量 Cube.transform.rotation = q; } }
/// <summary> /// 动做反馈 /// </summary> /// <param name="vectorAction"></param> public override void OnActionReceived(float[] vectorAction) { //获取全部部分 var bpDict = m_JdController.bodyPartsDict; var i = -1; //设置每一部分的角度 bpDict[leg0Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0); bpDict[leg1Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0); bpDict[leg2Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0); bpDict[leg3Upper].SetJointTargetRotation(vectorAction[++i], vectorAction[++i], 0); bpDict[leg0Lower].SetJointTargetRotation(vectorAction[++i], 0, 0); bpDict[leg1Lower].SetJointTargetRotation(vectorAction[++i], 0, 0); bpDict[leg2Lower].SetJointTargetRotation(vectorAction[++i], 0, 0); bpDict[leg3Lower].SetJointTargetRotation(vectorAction[++i], 0, 0); //设置每一部分的做用力 bpDict[leg0Upper].SetJointStrength(vectorAction[++i]); bpDict[leg1Upper].SetJointStrength(vectorAction[++i]); bpDict[leg2Upper].SetJointStrength(vectorAction[++i]); bpDict[leg3Upper].SetJointStrength(vectorAction[++i]); bpDict[leg0Lower].SetJointStrength(vectorAction[++i]); bpDict[leg1Lower].SetJointStrength(vectorAction[++i]); bpDict[leg2Lower].SetJointStrength(vectorAction[++i]); bpDict[leg3Lower].SetJointStrength(vectorAction[++i]); }
注意这里Action Space Type为Continuous,且Space Size为20。
/// <summary> /// Agent重置,使得身体各个部分初始化等 /// </summary> public override void OnEpisodeBegin() { if (m_DirToTarget != Vector3.zero) {//让小蓝正向面对目标物 transform.rotation = Quaternion.LookRotation(m_DirToTarget); } transform.Rotate(Vector3.up, Random.Range(0.0f, 360.0f));//使得小蓝随机旋转一个角度 foreach (var bodyPart in m_JdController.bodyPartsDict.Values) {//身体各部分置位 bodyPart.Reset(bodyPart); } if (!targetIsStatic) {//若是开启动态目标物,则随机重置目标物位置 GetRandomTargetPos(); } } /// <summary> /// 使得目标方块位置随机生成 /// </summary> public void GetRandomTargetPos() { //Random.insideUnitSphere:返回半径为1的球体内的一个随机点 var newTargetPos = Random.insideUnitSphere * targetSpawnRadius; newTargetPos.y = 5; target.position = newTargetPos + ground.position; }
在以前的版本中,Agent重置函数为AgentReset(),如今版本改名为OnEpisodeBegin()。
此外,以上代码段中能够看一下随机产生目标物的方法,其使用了UnityEngine.Random.insideUnitSphere属性,该值会返回一个半径为1的球体内的一个随机点,除此以外,该类中还提供onUnitSphere(在球上随机位置),insideUnitCircle(在平面圆内随机位置)两个属性。
void FixedUpdate() { if (detectTargets) {//开启检测碰撞目标奖励 foreach (var bodyPart in m_JdController.bodyPartsDict.Values) {//每帧遍历身体的每一个部分,是否碰撞到目标 if (bodyPart.targetContact && bodyPart.targetContact.touchingTarget) {//碰撞到目标,则奖励1,并根据自选项重置目标位置 TouchedTarget(); } } } if (useFootGroundedVisualization) {//是否开启碰撞地板脚变材质功能 foot0.material = m_JdController.bodyPartsDict[leg0Lower].groundContact.touchingGround ? groundedMaterial : unGroundedMaterial; foot1.material = m_JdController.bodyPartsDict[leg1Lower].groundContact.touchingGround ? groundedMaterial : unGroundedMaterial; foot2.material = m_JdController.bodyPartsDict[leg2Lower].groundContact.touchingGround ? groundedMaterial : unGroundedMaterial; foot3.material = m_JdController.bodyPartsDict[leg3Lower].groundContact.touchingGround ? groundedMaterial : unGroundedMaterial; } if (rewardMovingTowardsTarget) {//是否开启速度方向与目标方向奖励惩罚机制 RewardFunctionMovingTowards(); } if (rewardFacingTarget) {//是否开启小蓝前方与目标方向奖励惩罚机制 RewardFunctionFacingTarget(); } if (rewardUseTimePenalty) {//是否开启随时间流失惩罚机制 RewardFunctionTimePenalty(); } } /// <summary> /// 计算小蓝速度方向与目标方向的点积,以此来奖励或惩罚 /// </summary> void RewardFunctionMovingTowards() { m_MovingTowardsDot = Vector3.Dot(m_JdController.bodyPartsDict[body].rb.velocity, m_DirToTarget.normalized); AddReward(0.03f * m_MovingTowardsDot); } /// <summary> /// 计算小蓝正向与目标方向的点积,以此来惩罚获奖励 /// </summary> void RewardFunctionFacingTarget() { m_FacingDot = Vector3.Dot(m_DirToTarget.normalized, body.forward); AddReward(0.01f * m_FacingDot); } /// <summary> /// 随时间流失,惩罚小蓝,促使其快速完成任务 /// </summary> void RewardFunctionTimePenalty() { AddReward(-0.001f); }
此段代码主要是对各类自选项进行设置判断,主要用途就是设置在何时给予小蓝惩罚或奖励。
至此,咱们将Crawler的主要代码都解析了一遍,可能有一些地方没有解析的很清楚,也算是抛砖引玉,主要借鉴一下里面的用法便可,实现仍是要根据具体需求具体分析,除此以外引入了一些四元数的内容,此次也算是对四元数知识的空缺进行了必定程度的弥补。
在命令行中输入如下命令:
mlagents-learn config/trainer_config.yaml --run-id=crawler_normal --train
进行训练,以下图:
有点像魔鬼的步伐。。。
还记得AdjustTrainingTimescale脚本么,是设置Time.Scale的脚本,此时咱们若是按数字键1~9,会发现Crawler的动做确实能够减慢或加速,就不作动图了,动图帧数必定的,也看不出来加速或减速,因此本身能够去试一下。这个值应该是确实能影响训练速度的,我打印了一下,普通训练的话这个值是20,Time.Scale最大为100,可是也应该不能设置的太大,太大的话会形成Update卡顿,反而影响训练速度,不过这个是我猜的。。。
训练一段时间就发现小蓝以飞快速度奔向目标:
得飘得飘得意的飘~
顺带附上训练后的TesorBoard:
能够看到,最终训练的Cumulative Reward大概在650左右,比官方的数据400还要好不少。
放到Unity中来看一下训练效果:
效果和官方训练的模型同样,没什么问题。
本次的案例主要是展现对于复杂的多关节对象如何训练,而且说起到了一些四元数的知识,欢迎你们点赞留言共同探讨。
写文不易~所以作如下申明:
1.博客中标注原创的文章,版权归原做者 煦阳(本博博主) 全部;
2.未经原做者容许不得转载本文内容,不然将视为侵权;
3.转载或者引用本文内容请注明来源及原做者;
4.对于不遵照此声明或者其余违法使用本文内容者,本人依法保留追究权等。