Hands-on C++ Game Animation Programming阅读笔记(九)

news2024/11/28 22:52:21

Chapter 12: Blending between Animations

This fade is usually short—a quarter of a second or less. (动画之间的fade一般很快,0.5s甚至更短)

本章重点:

  • 两个Pose的Blending
  • 不同Animations的CrossFading,会有一个cross fade controller来处理一个blend queue
  • Additive Blending

Pose blending

也没啥新东西,主要是:

  • Pose Blending很简单,如果没有动画遮罩(AvatarMaks),把所有的Joints的Transform的各个Property按比例,进行插值即可,可以使用线性插值;如果有AvatarMask,那么就只插值那些没有被Mask过的Joints
  • Animation Blending本章上是Pose的Blending,而Pose是Sample Animation得到的结果
  • Blend操作的俩Pose需要是相似的,它们的Joint的hierarchy一般是相同的
  • For blending to work, it must happen in local space

这里声明俩全局函数,放在Blending.h文件里,如下所示:

// root node有点特殊, 它代表Pose b的第root个Joint的subHierarchy会被Blend到Pose a的相关部分
// 也就是说, 这里的Blend函数本身就带了一定程度上的AvatarMask功能(但是它一次性只能mask整个subHierarchy
// 跟Unity的AvatarMask功能比起来还是弱了一点)
void Blend(Pose& a, const Pose& b,float t,int root);
// 如果Pose里的search节点是root节点的子节点, 返回true
bool IsInHierarchy(Pose& pose, unsigned int root, unsigned int search);

函数定义如下,就是各自的LocalTransform的插值,很简单:

// 根据a和b进行插值, 改变原本的pose a, 作为output pose
void Blend(Pose& a, const Pose& b, float t, int blendroot) 
{
	unsigned int numJoints = output.Size();
	// 遍历所有Joints
	for (unsigned int i = 0; i < numJoints; ++i) 
	{
		// 如果blendroot>=0, 则是在pose a的基础上, 在a的的blendroot节点的subTree与B对应的subTree进行Blend
		// 如果blendroot为-1, 则是两个Pose在整个Hierarchy上的blend, 不需要做第一步的check
		if (blendroot >= 0) 
		{
			// 	1. 如果joint不在混合的subTree下, 则不处理joint的数据
			if (!IsInHierarchy(a, (unsigned int)blendroot, i)) 
				continue;	
		}

		// 2. 否则进行混合
		a.SetLocalTransform(i, mix(a.GetLocalTransform(i), b.GetLocalTransform(i), t));
	}
}

// 判断search节点的父节点链表里是否有parent节点
// 这里输入的parent >=0
bool IsInHierarchy(Pose& pose, unsigned int parent, unsigned int search) 
{
	if (search == parent)
		return true;
	
	// 遍历父节点链表
	int p = pose.GetParent(search);
	while (p >= 0) // p为-1时搜寻到了root
	{
		if (p == (int)parent)
			return true;
		
		p = pose.GetParent(p);
	}

	return false;
}

Crossfading animations

A crossfade is a fast blend from one animation to another.

前面设计的Blend函数一般用于动画之间的transition,也就是cross fade,这个过程其实就是在一段较短的时间内,不断改变插值比例,从0变到1,在这个过程里,每帧调用一次Blend函数,返回对应的Pose而已。

由于CrossFade这个操作,不是一帧就能完成的,这是个长期的任务,所以可以先创建一个CrossFadeTask类,代表从Pose A转换到Animation B这个任务,接口如下,由于只是个Data类,所以只有Header文件,没有Source文件:

// 属于一个Helper类, 用于帮助根据时间数据Sampler Clip数据, 得到Pose, 再与原本的mPose进行Blend
struct CrossFadeTask
{
	Pose mPose;					// 本身的Pose
	Clip* mClip;				// 需要Sample的Clip, 其实就是RestPose
	float mTime;				// Sample的相关信息
	float mDuration;			// CrossFade的相关信息
	float mElapsed;				// 已经过渡部分的时间信息(0f<=mElapsed<=mDuration)
	// default ctor
	inline CrossFadeTask() : mClip(0), mTime(0.0f),
							   mDuration(0.0f), mElapsed(0.0f) 
	// ctor with params
	inline CrossFadeTask(Clip* target, Pose& pose, float dur): 
		mClip(target), mTime(target -> GetStartTime()), 
		mPose(pose), mDuration(dur),
		mElapsed(0.0f) { }
};

Declaring the cross-fade task manager

接下来创建一个类,用于实现上面的cross fade task,它应该有一个Input和Output,Input就是CrossFade之前的Pose,Output就是执行Task期间算出的新Pose,类声明如下:

class CrossFadeTaskManager
{
protected:
	// 由于可能存在多个动画的Blend任务, 一个动画同时blend到多个动画, 所以这里设计为数组
	// 比如说, 从run过渡到idle和walk的组合状态(感觉很像BlendSpace)
	std::vector<CrossFadeTask> mTasks;
	Clip* mClip;						// 为啥还有个mClip?
	float mTime;
	Pose mPose;							// 如果存在mSkeleton, 则mPose存储了mSkeleton里的RestPose
	Skeleton mSkeleton;	
	bool mWasSkeletonSet;				// 	mWasSkeletonSet用于标识CrossFadeTaskManager里是否已经设置好了Skeleton
public:
	CrossFadeTaskManager();
	CrossFadeTaskManager(Skeleton& skeleton);
	void SetSkeleton(Skeleton& skeleton);
	void BeginPlay(Clip* targetClip);								// 开始播放targetClip, 停止Blending
	void AddCrossFadeTask(Clip* target, float fadeTime);
	void Update(float dt);
	Pose& GetCurrentPose();
	Clip* GetcurrentClip();
};

相关代码实现如下:

#include "CrossFadeTaskManager.h"
#include "Blending.h"

// 默认ctor
CrossFadeTaskManager::CrossFadeTaskManager() 
{
	mClip = 0;
	mTime = 0.0f;
	mWasSkeletonSet = false;
}

// 带Skeleton参数的ctor
CrossFadeTaskManager::CrossFadeTaskManager(Skeleton& skeleton) 
{
	mClip = 0;
	mTime = 0.0f;
	SetSkeleton(skeleton);
}

void CrossFadeTaskManager::SetSkeleton(Skeleton& skeleton) 
{
	mSkeleton = skeleton;
	mPose = mSkeleton.GetRestPose();
	mWasSkeletonSet = true;
}

// 开始播放targetClip, 停止Blending
void CrossFadeTaskManager::BeginPlay(Clip* targetClip) 
{
	// 只是设置播放状态, 并没有真正开始Play
	mTasks.clear();
	mClip = targetClip;
	mPose = mSkeleton.GetRestPose();
	mTime = targetClip->GetStartTime();
}

// 从当前的mPose开始Fade到targetClip
void CrossFadeTaskManager::AddCrossFadeTask(Clip* targetClip, float fadeTime) 
{
	// 如果当前没有mPose, 则直接设置播放状态到targetClip
	if (mClip == 0) 
	{
		BeginPlay(targetClip);
		return;
	}
	
	// 两种情况下不会添加targetClip对应的CrossFadeTask
	// 1. Task数组最后一个已经是目标Task了
	// 2. 在Task数组为空时, 已经在处理targetClip对应的CrossFadeTask了

	// 如果存在mTask数组, 检查最后一个任务是不是已经为目标Clip
	if (mTasks.size() >= 1) 
	{
		if (mTasks[mTasks.size() - 1].mClip == targetClip)
			return;
	}
	else 
	{
		// mClip应该是代表了CrossFadeManager正在播放的clip
		if (mClip == targetClip) 
			return;// 如果新的要CrossFade到的targetClip也是正在播放的Clip
	}

	mTasks.push_back(CrossFadeTask(targetClip, mSkeleton.GetRestPose(), fadeTime));
}

// 这个函数应该会每帧执行
void CrossFadeTaskManager::Update(float dt) 
{
	if (mClip == 0 || !mWasSkeletonSet)
		return;

	// 遍历CrossFadeTask数组, 根据当前时间顺序判断哪些任务已经执行完了
	// 如果执行完了, 就从mTasks里删除掉, 注意, 一帧只可以删除一个BlendTask
	unsigned int numTasks = mTasks.size();
	for (unsigned int i = 0; i < numTasks ; ++i) 
	{
		// 判断任务如果执行完了, 标识当前帧就是播放的对应动画
		if (mTasks[i].mElapsed >= mTasks[i].mDuration) 
		{
			// 存储被执行完成任务的Clip、mTime和mPose
			// 这代码太奇怪了
			mClip = mTasks[i].mClip;
			mTime = mTasks[i].mTime;
			//mPose = mTasks[i].mPose;// ....?这代码有病吧? 后面反正会Reset的

			mTasks.erase(mTasks.begin() + i);
			break;
		}
	}

	// 
	numTasks = mTasks.size();
	// 每次Blend动画, 基础都是Skeleton里的RestPose
	mPose = mSkeleton.GetRestPose();
	// 正常来说, mTime是随着Sample函
	mTime = mClip->Sample(mPose, mTime + dt);// 这里的mTime会随着Loop参数被取模....

	// 更新剩余的没完成的BlendTask的时间
	for (unsigned int i = 0; i < numTasks ; ++i) 
	{
		// 注意, 这些Task的时间是同时增加的
		CrossFadeTask& task = mTasks[i];
		// Sample函数的结果会存在task.mPose里
		task.mTime = task.mClip->Sample(task.mPose, task.mTime + dt);
		task.mElapsed += dt;
		float t = task.mElapsed / task.mDuration;
		if (t > 1.0f)
			t = 1.0f;

		// 基于Skeleton的RestPose, 不断与BlendTasks里的动画对应的Pose进行Blend
		Blend(mPose, task.mPose, t, -1);// -1代表blend整个skeleton
	}
}

Pose& CrossFadeTaskManager::GetCurrentPose() 
{
	return mPose;
}

Clip* CrossFadeTaskManager::GetcurrentClip() 
{
	return mClip;
}

Additive blending

Additive animations are used to modify an animation by adding in extra joint movements.

通过一些Additive动画,可以通过添加额外的joint movements修改来动画,它的核心是把该Additive动画带来的offset加在原本的动画上。
这种动画一般都很特殊,比如说我有个左倾斜的动画,这个动画仅仅改变了人物的spine,时长为1.0s,0.0s时的人物是直立的,随着时间从0s过渡到1s,动画左倾的程度越来越明显。这种动画在实际应用时,一般不是简单的按时间来进行Sample的,而是经常与Input联系到一起。比如说,人物的左摇杆,向左扭动的幅度越大,这里的动画播放的时长越接近1.0s。

Not all animations are a good fit for additive animations. Additive animations are usually specifically made. Additive animations typically don’t play according to time, but rather, according to some other input. It’s common to sync the playback of additive animations to something other than time.

这里在Blending.h里再声明两个全局函数:

// 这个函数会在load additive animtion clip时被调用, 在time为0的地方Sample
// additive animtion clip, 返回对应的BasePose
Pose MakeAdditivePose(Skeleton& skeleton, Clip& clip);
// result pose = input pose + (additive pose – additive base pose)
void Add(Pose& inAndOutPose, const Pose& addPose, const Pose& additiveBasePose, int blendroot);

实现的代码如下:

// 基于restPose, Sample, 返回新Pose即可
Pose MakeAdditivePose(Skeleton& skeleton, Clip& clip) 
{
	Pose result = skeleton.GetRestPose();
	clip.Sample(result, clip.GetStartTime());
	return result;
}

// 其实就是算出DeltaTransform加在原本的每个Joint上, 很简单
void Add(Pose& inAndOutPose, const Pose& addPose, const Pose& basePose, int blendroot) 
{
	unsigned int numJoints = addPose.Size();
	for (unsigned int i = 0; i < numJoints; ++i) 
	{
		Transform input = inAndOutPose.GetLocalTransform(i);
		Transform additiveBase = basePose.GetLocalTransform(i);
		Transform additiveCur = addPose.GetLocalTransform(i);
		
		// SubTree的mask
		if (blendroot >= 0 && !IsInHierarchy(addPose, blendroot, i)) 
			continue;

		// outPose = inPose + (addPose - basePose)
		Transform result(
			input.position + (additiveCur.position - additiveBase.position),		// Delta Position
			normalized(input.rotation * (inverse(additiveBase.rotation) * additiveCur.rotation)),	// Delta Rotation
			input.scale + (additiveCur.scale - additiveBase.scale));// 这里的Scale居然是减, 为啥不是除
		
		inAndOutPose.SetLocalTransform(i, result);
	}
}

Additive animations are most often used to create new animation variants

Additive动画经常用于创建新的动画variants,这里的Additive不一定非得是动画,也可以是一个Pose,常见的比如说,拿一个蹲着的pose,然后作为Additive Animation(或者说Pose),去跟其他的动画Blend,比如说跟Walking动画进行Additive Blending,就可以得到一个蹲着走的动画(感谢好像是一种程序性动画)


总结

本章主要是以下内容:

  • Blending的本质,就是对相同Skeleton的两个Pose的相同关节进行Transform数据的插值
  • Cross Fade的本质就是在一段较短时间内,逐帧调用Blend函数,然后不断改变之间动画的权重值,也就是插值的权重
  • 还有一种特殊的Blend,叫做Additive Blending,是把一个动画的从第n帧到第0帧的offset,附加在已有的正常播放的动画上

Chapter 13: Implementing Inverse Kinematics

By using IK, you can figure out how to rotate the character’s shoulder, elbow, and wrist in a way that the character’s finger is always touching a specific point.

注意,IK的算法应该只涉及Rotation

常见的IK算法是CCD和FABRIK算法,这章的主要内容:

  • CCD IK算法和实现CCD solver
  • FABRIK IK算法和实现FABRIK solver
  • 实现ball-and-socket constraints
  • 实现hinge constraints
  • 理解IK solvers应该用在动画流程的哪个地方,以及应该怎么使用(Understand where and how IK solvers fit into an animation pipeline)

Creating a CCD solver

CCD: Cyclic Coordinate Descent,算法有三个重要概念:

  • goal: 也就是目标点
  • IK chain:所有的joints的链表
  • end effector:chain最尾部的joint

CCD算法的伪代码如下:

// 从倒数第二个Joint开始,反向遍历chain里的所有joints(倒数第一个节点是end effector)
foreach joint in ikchain.reverse() 
{
	// 算出joint指向end effector的向量,也就是当前的实际尾部
	jointToEffector = effector.position - joint.position
	// 算出joint指向goal的向量,也就是当前的预期尾部
	jointToGoal = goal.position - joint.position
	
	// 为了让此时的joint、Effector和Goal三点一线,注意这里算的DeltaRotation是基于WorldPosition
	// 来的,所以应该是GlobalRotation, 乘在左边
	joint.rotation = fromToRotation(jointToEffector, jointToGoal) * joint.rotation
}

思路是这样的
从尾部节点开始逼近Goal,既然Effector的旋转改变也不影响最终的结果,那么直接从倒数第二个节点开始处理,其他的就跟代码里写的一样,简单来说,CCD就是从倒数第二个节点开始逆序遍历,改变遍历的节点的旋转,让该节点、尾部节点和Goal三点一线,遍历一次,视为一次迭代过程,过程如下图所示:
在这里插入图片描述

这里的CCD算法是个迭代算法,注意,这里倒数第二个Joint,算出来的DeltaRotation,会应用到Joint自身上,目的是让该Joint、Effector和Goal在一条直线上,CCD遍历到最后,最Parent的joint、Effector和goal会在一条直线上,但其他的Joint就不一定了。



声明CCD solver类

solver的意思是解决者,所以这个类其实就是用CCD算法解决IK问题的类,类声明如下:

// CCDSolver.h文件
class CCDSolver 
{
protected:
	std::vector<Transform> mIKChain;					// Joints的原始Transform, 除了root joint, 其他都存的Local数据
	unsigned int mNumSteps;								// 算法的迭代次数, 设置它可以有效的防止Chain的错误导致死循环
	float mThreshold;									// 用于浮点数比较的误差
	
public:
	CCDSolver();// default ctor
	unsigned int Size();								// 返回IK Chain的joint个数
	void Resize(unsigned int newSize);
	Transform& operator[](unsigned int index);			// 获取第i个节点的localTrans数据
	Transform GetGlobalTransform(unsigned int index);	// 获取第i个节点的globalTrans数据
	unsigned int GetNumSteps();							// 迭代次数的get函数
	void SetNumSteps(unsigned int numSteps);			// 迭代次数的set函数
	float GetThreshold();
	void SetThreshold(float value);
	// 如果IK Chain问题解决了, 返回true
	bool Solve(vec3 goalPos);							// gloal就是方程的解, IK问题的goal
}

The mNumSteps variable is used to make sure the solver doesn’t fall into an infinite loop. There is no guarantee that the end effector will ever reach the goal.


Implementing the CCD solver

下面是具体类的实现函数:

// 前面都是一些简单的Get, Set函数
CCDSolver::CCDSolver() 
{
	mNumSteps = 15;
	mThreshold = 0.00001f;
}

...//省略一些简单的Get和Set函数

// 算出Ik Chain第x个joint的GlobalTransform
Transform CCDSolver::GetGlobalTransform(unsigned int x) 
{
	unsigned int size = (unsigned int)mIKChain.size();
	Transform world = mIKChain[x];
	// 一级一级的左乘parent的transform
	for (int i = (int) x - 1; i >= 0; --i) 
		world = combine(mIKChain[i], world);
	
	return world;
}

// 唯一有点价值的函数, 就是把上面的伪代码实现了出来
bool CCDSolver::Solve(vec3 goalPos) 
{
	unsigned int size = Size();
	if (size == 0)
		return false;
		
	unsigned int effectorId = size - 1;
	float thresholdSq = mThreshold * mThreshold;
	// 注意, CCD是把这个IK循环过程迭代了很多次, 每次迭代都会逆序遍历一次Chain
	for (unsigned int i = 0; i < mNumSteps; ++i) 
	{
		// 在每次迭代的开始, 判断是否已经解决了IK问题
		vec3 effector = GetGlobalTransform(effectorId).position;
		
		// goalPose和effector几乎相同时, IK问题得到了解决, 返回true
		if (lenSq(goalPos - effector) < thresholdSq) 
			return true;
		
		for (int j = (int)size - 2; j >= 0; --j) 
		{
			// 算出effector的GlobalPos
			effector = GetGlobalTransform(effectorId).position;

			// 算出当前Joint对应的GlobalPosition和GlobalRotation
			Transform world = GetGlobalTransform(j);
			vec3 worldPosition = world.position;
			quat worldRotation = world.rotation;

			// 算出deltaRotation, 但是这个deltaRotation属于GLobalDeltaRotation
			vec3 toEffector = effector - worldPosition;// 这里都是Global的Pos
			vec3 toGoal = goalPos - worldPosition;
			quat deltaRot;
			if (lenSq(toGoal) > 0.00001f)
				deltaRot = fromTo(toEffector, toGoal);

			// 正常的写法
			mIKChain[j].rotation = deltaRot * mIKChain[j].rotation;

			// 书中的写法(我不明白为什么这么写)????????
			//quat newWorldRotation = worldRotation * deltaRot;
			//quat localRotation = newWorldRotation * inverse(worldRotation);
			//mIKChain[j].rotation = localRotation * mIKChain[j].rotation;

			effector = GetGlobalTransform(effectorId).position;
			if (lenSq(goalPos - effector) < thresholdSq)
				return true;
	}	
}

我对书里写的CCD算法,感到很疑惑,不过我看了下别人的CCD算法,感觉我写的没啥问题,为了防止错误,举几个别人代码里的CCD算法:

Unity官方给的CCD算法代码

参考:https://www.youtube.com/watch?v=MA1nT9RAF3k&ab_channel=Unity

他这里的代码加了点小优化,传统的CCD算法每次迭代是逆序遍历所有的Joint(除了Effector),这里改进后,一次不会遍历所有的Joint,它第一次迭代只会逆序遍历两个Joint,然后不断递增。举个例子,假设一个Chain里,从Effector开始倒数,ID分别为0、1、2…,那么遍历顺序前后的变化如下图所示:
在这里插入图片描述
代码如下:

void Solve()
{
	Vector3 goalposition = goal.position;
	Vector3 effectorPosition = m_Bones[0].position;

	Vector3 targetPosition = Vector3.Lerp(effectorPosition, goalPosition, weight);
	float sqrDistance;
	int iterationcount = 0;

	do
	{
		for (int i = 0; i < m_Bones.count - 2; i++)
		{
			// 初始遍历两个Joint, 然后不断增加
			for (int j = 1; j < i + 3 && j < m_Bones.count; j++)
			{
				RotateBone(m_Bones[0], m_Bones[j], targetPosition);
				sqrDistance = (m_Bones[0].position - targetPosition).sqrMagnitude;
				if (sqrDistance <= sqrDistError)
					return;
			}
		}

		sqrDistance = (m_Bones[0].position - targetPosition).sqrMagnitude;
		iterationCount++;
	}
	while (sqrDistance > sqrDistError && iterationCount <= maxIterationCount);

}

public static void RotateBone(Transform effector, Transform bone, Vector3 goalPosition)
{
	// 先算DeltaGlobalRotation
	Vector3 effectorPosition = effector.position;
	Vector3 bonePosition = bone.position;
	Vector3 boneToEffector = effectorPosition - bonePosition;
	Vector3 boneToGoal = goalPosition - bonePosition;
	Quaternion fromToRotation = Quaternion.FromToRotation(boneToEffector, boneToGoal);

	bone.rotation = fromToRotation * bone.rotation;
}

Unity Final IK插件里的CCD算法代码

这个CCD IK算法加了个Joint的权重,以及Joint旋转的Limitations

// 位于IKSolverCCD.cs文件内
private void Solve(Vector3 targetPosition)
{
	// 2D
	if (XY)
	{
		...// 省略2D部分
	}
	else
	{
		for (int i = bones.Length - 2; i > -1; i--)
		{
			// 这里joint有个权重, 当权重为1时, 跟前面的IK算法是一样的
			// Slerp if weight is < 0
			//CCD tends to overemphasise the rotations of the bones closer to the target position. Reducing bone weight down the hierarchy will compensate for this effect.
			float w = bones[i].weight * IKPositionWeight;

			if (w > 0f)
			{
				Vector3 toLastBone = bones[bones.Length - 1].transform.position - bones[i].transform.position;
				Vector3 toTarget = targetPosition - bones[i].transform.position;

				// Get the rotation to direct the last bone to the target
				Quaternion targetRotation = Quaternion.FromToRotation(toLastBone, toTarget) * bones[i].transform.rotation;

				if (w >= 1) 
					bones[i].transform.rotation = targetRotation;
				// 如果权重值在0,1之间, 则需要对原本的rotation和新的rotation进行插值
				else 
					bones[i].transform.rotation = Quaternion.Lerp(bones[i].transform.rotation, targetRotation, w);
			}

			// 计算完了新的LocalRotation后, 施加rotationLimit
			if (useRotationLimits && bones[i].rotationLimit != null) 
				bones[i].rotationLimit.Apply();
		}
	}
}

还有很多类似的CCD IK算法,就不贴出来了,我都没有看到书里这么奇怪的转换代码,具体为啥这么写,好像是为了平稳过渡IK结果把,我也不是特别清楚,Remain。


FABRIK

FABRIK (Forward And Backward Reaching Inverse Kinematics): 这种IK算法看上去效果更好,收敛结果更接近人类的IK效果,与CCD IK算法一样,也是分为Goal、Effector和IK Chain三个内容。CCD IK是通过Rotation起效的,而FABRIK是通过position起效的,这种算法更容易理解。

FABRIK算法思路,分为两个Pass:

  1. 第一个Pass,从Effector倒数往Root开始遍历,不断让遍历的Joint逼近Goal
  2. 计算每个Joint相对于Effector的相对Pos
  3. 第二个Pass,从Root再顺序遍历到Effector,其他的步骤与第一个Pass相同

算法思路如下图所示,第一个Pass,可以理解为像铁臂阿童木的手臂一样,从最外部手臂节点开始,依次发射自己一节节的Bone。这样Bone两端对应的Joints,一端的坐标就已经是目标点了,只需要求出另外一端joint的坐标即可。如下图a、b、c所示,相当于把p3p4旋转对转goal目标后,发射给goal点。这里具体的计算方法,就不需要用到旋转了,其实就是单纯的点的坐标计算。Goal点坐标是已知的,p3的坐标点也是已知的,p3p4这段Bone的长度也是已知的,假设为l3,那么新的p3’点的坐标为:

boneOldHead = p3;
boneTail = goal;// 即p4'点
// 计算方向, 对应的向量, 平移加上去即可
direction = normalize(boneOldHead - boneTail);// p4'p3对应的方向
offset = direction * boneLength;// 算出对应的offset
boneNewHead = boneTail + offset;// 新的p3点的位置,即p3'
bones[bones.size() - 2] = boneNewHead;

第一个Pass接下来的操作,跟上面介绍的差不多了,无非是Goal点坐标变了。在第二个Pass里,又会反向收回一节节的手臂,如图e和f所示:
在这里插入图片描述

伪代码大概是这样:

// 代表一次迭代过程
void Iterate(const Transform& goal) 
{
	startPosition = chain[0];
	// 从End向Root遍历, 这里不用管方向, 只需要计算每个Joint的Global位置即可
	// 遍历之前, 最后的joint的位置已经计算好了, 就是goal
	chain[size - 1] = goal.position;
	for (i = size - 2; i >= 0; --i) 
	{
		// 每次遍历的目的, 是为了计算chain[i]节点的位置, 从倒数第二个节点位置开始计算
		
		// 每次取两个连续的joint, 代表一块Bone
		head = chain[i];
		tail = chain[i + 1];
		// 计算方向, 对应的向量, 平移加上去即可
		direction = normalize(head - tail);
		offset = direction * length[i + 1];
		chain[i] = tail + offset;
	}

	// Iterate forwards
	chain[0] = startPosition;
	for (i = 1; i < size; ++i)
	{
		current = chain[i];
		prev = chain[i - 1];
		direction = normalize(current - prev);
		offset = direction * length[i];
		chain[i] = prev + offset;
	}
}

类声明如下:

class FABRIKSolver 
{
protected:
	std::vector<Transform> mIKChain;
	unsigned int mNumSteps;
	float mThreshold;
	
	// 与CCDIKReSolver相比, 多了俩数组
	std::vector<vec3> mWorldChain;	//这里的计算都是用的WorldPosition, mWorldChain与mIKChain里的Joints一一对应
	std::vector<float> mLengths;	// 记录所有Bone的长度, 就是Joint到其Parent Joint的距离, mLengths[0] = 0f
protected:
	void CalcWorldChainAndBoneLengthsFromIKChain();
	void IterateForward(const vec3& goal);
	void IterateBackward(const vec3& base);
	void WorldToIKChain();
public:
	FABRIKSolver();

	unsigned int Size();
	void Resize(unsigned int newSize);

	Transform GetLocalTransform(unsigned int index);
	void SetLocalTransform(unsigned int index, const Transform& t);
	Transform GetGlobalTransform(unsigned int index);

	unsigned int GetNumSteps();
	void SetNumSteps(unsigned int numSteps);

	float GetThreshold();
	void SetThreshold(float value);

	bool Solve(const Transform& target);
};

类实现如下:

#include "FABRIKSolver.h"

// 省略了一些简单的Get和Set函数
FABRIKSolver::FABRIKSolver() 
{
	mNumSteps = 15;
	mThreshold = 0.00001f;
}

unsigned int FABRIKSolver::Size() 
{
	return mIKChain.size();
}

void FABRIKSolver::Resize(unsigned int newSize) 
{
	mIKChain.resize(newSize);
	mWorldChain.resize(newSize);
	mLengths.resize(newSize);
}

Transform FABRIKSolver::GetLocalTransform(unsigned int index) 
{
	return mIKChain[index];
}

void FABRIKSolver::SetLocalTransform(unsigned int index, const Transform& t) 
{
	mIKChain[index] = t;
}

Transform FABRIKSolver::GetGlobalTransform(unsigned int index) 
{
	unsigned int size = (unsigned int)mIKChain.size();
	Transform world = mIKChain[index];
	for (int i = (int)index - 1; i >= 0; --i) 
		world = combine(mIKChain[i], world);
	
	return world;
}

// 很简单, 根据mIKChain算出WorldChain和每个joint与其parent的距离
void FABRIKSolver::CalcWorldChainAndBoneLengthsFromChainList() 
{
	unsigned int size = Size();
	for (unsigned int i = 0; i < size; ++i) 
	{
		// 遍历joint, 算出每个joint的GlobalTransform
		Transform world = GetGlobalTransform(i);
		mWorldChain[i] = world.position;

		// mLengths记录的是每个joint与其parent的距离
		if (i >= 1) 
		{
			vec3 prev = mWorldChain[i - 1];
			mLengths[i] = len(world.position - prev);
		}
	}
	if (size > 0) 
		mLengths[0] = 0.0f;
}


// 这个函数复杂一些, 它是根据WorldChain反过来推导IKChain
// WorldToIKChain这个函数作者写的有bug
void FABRIKSolver::WorldToIKChain() 
{
	unsigned int size = Size();
	if (size == 0)
		return; 

	// 顺序根据一系列Joints的Global坐标算出各自Local的Transform
	for (unsigned int i = 0; i < size - 1; ++i) 
	{
		// 作者是这么写的, 感觉很离谱, 有bug
		//Transform world = GetGlobalTransform(i);
		//Transform next = GetGlobalTransform(i + 1);
		// 应该这么写
		Transform world = mWorldChain[i];
		Transform next = mWorldChain[i + 1];
		// 获取当前节点的GlobalPos和GlobalRot
		vec3 position = world.position;
		quat rotation = world.rotation;

		// 把这个DeltaGlobalPos变换到LocalSpace下
		// toNext是原本的LocalPos
		vec3 toNext = next.position - position;
		toNext = inverse(rotation) * toNext;

		// toDesired是新的LocalPos
		vec3 toDesired = mWorldChain[i + 1] - position;
		toDesired = inverse(rotation) * toDesired;

		// 为啥又是左乘? 因为这个DeltaRot是用于curJoint的LocalPos上的, 所以应该是更Parent的一级
		quat delta = fromTo(toNext, toDesired);
		mIKChain[i].rotation = delta * mIKChain[i].rotation;
	}
}

void FABRIKSolver::IterateBackward(const vec3& goal)
{
	int size = (int)Size();
	if (size > 0) 
		mWorldChain[size - 1] = goal;
	

	for (int i = size - 2; i >= 0; --i) 
	{
		vec3 direction = normalized(mWorldChain[i] - mWorldChain[i + 1]);
		vec3 offset = direction * mLengths[i + 1];
		mWorldChain[i] = mWorldChain[i + 1] + offset;
	}
}

void FABRIKSolver::IterateForward(const vec3& base) 
{
	unsigned int size = Size();
	if (size > 0) 
		mWorldChain[0] = base;

	for (int i = 1; i < size; ++i) 
	{
		vec3 direction = normalized(mWorldChain[i] - mWorldChain[i - 1]);
		vec3 offset = direction * mLengths[i];
		mWorldChain[i] = mWorldChain[i - 1] + offset;
	}
}

bool FABRIKSolver::Solve(const Transform& target) 
{
	unsigned int size = Size();
	if (size == 0)
		return false;
	unsigned int last = size - 1;
	float thresholdSq = mThreshold * mThreshold;

	// 根据IKChain算出WorldChain和BoneLength数组
	IKChainToWorld();
	vec3 goal = target.position;
	vec3 base = mWorldChain[0];

	// TODO: 其实应该判断一下总长度, 是否小于base到goal的距离


	for (unsigned int i = 0; i < mNumSteps; ++i) 
	{
		vec3 effector = mWorldChain[last];
		if (lenSq(goal - effector) < thresholdSq) 
		{
			WorldToIKChain();
			return true;
		}

		IterateBackward(goal);
		IterateForward(base);
	}

	WorldToIKChain();
	vec3 effector = GetGlobalTransform(last).position;
	if (lenSq(goal - effector) < thresholdSq)
		return true;

	return false;
}

别人写的FabricIK算法

参考:https://www.youtube.com/watch?v=qqOAzn05fvk&t=1217s&ab_channel=DitzelGames


别人写的FabricIK算法

Final IK插件里的:

using UnityEngine;
using System.Collections;
using System;

namespace RootMotion.FinalIK 
{
	[System.Serializable]
	public class IKSolverFABRIK : IKSolverHeuristic 
	{
		// 算法阶段一
		public void SolveForward(Vector3 position) 
		{
			OnPreSolve();	
			ForwardReach(position);
		}
		
		// 算法阶段二
		public void SolveBackward(Vector3 position) 
		{
			BackwardReach(position);
			OnPostSolve();
		}

		public override Vector3 GetIKPosition() 
		{
			if (target != null) 
				return target.position;
			
			return IKPosition;
		}

		// Called before each iteration of the solver.
		public IterationDelegate OnPreIteration;

		private bool[] limitedBones = new bool[0];
		private Vector3[] solverLocalPositions = new Vector3[0];

		protected override void OnInitiate() 
		{
			if (firstInitiation || !Application.isPlaying) IKPosition = bones[bones.Length - 1].transform.position;

			for (int i = 0; i < bones.Length; i++) 
			{
				bones[i].solverPosition = bones[i].transform.position;
				bones[i].solverRotation = bones[i].transform.rotation;
			}
			
			limitedBones = new bool[bones.Length];
			solverLocalPositions = new Vector3[bones.Length];
			
			InitiateBones();

			for (int i = 0; i < bones.Length; i++) 
			{
				solverLocalPositions[i] = Quaternion.Inverse(GetParentSolverRotation(i)) * (bones[i].transform.position - GetParentSolverPosition(i));
			}
		}
		
		protected override void OnUpdate() 
		{
			if (IKPositionWeight <= 0) return;
			IKPositionWeight = Mathf.Clamp(IKPositionWeight, 0f, 1f);
			
			OnPreSolve();

			if (target != null) IKPosition = target.position;
			if (XY) IKPosition.z = bones[0].transform.position.z;

			Vector3 singularityOffset = maxIterations > 1? GetSingularityOffset(): Vector3.zero;

			// Iterating the solver
			for (int i = 0; i < maxIterations; i++) {
				// Optimizations
				if (singularityOffset == Vector3.zero && i >= 1 && tolerance > 0 && positionOffset < tolerance * tolerance) break;
				lastLocalDirection = localDirection;

				if (OnPreIteration != null) OnPreIteration(i);
				
				Solve(IKPosition + (i == 0? singularityOffset: Vector3.zero));
			}
			
			OnPostSolve();
		}
		
		/*
		 * If true, the solver will work with 0 length bones
		 * */
		protected override bool boneLengthCanBeZero { get { return false; }} // Returning false here also ensures that the bone lengths will be calculated

		/*
		 * Interpolates the joint position to match the bone's length
		*/
		private Vector3 SolveJoint(Vector3 pos1, Vector3 pos2, float length) 
		{
			if (XY) pos1.z = pos2.z;
			
			return pos2 + (pos1 - pos2).normalized * length;
		}

		/*
		 * Check if bones have moved from last solved positions
		 * */
		private void OnPreSolve() 
		{
			chainLength = 0;

			for (int i = 0; i < bones.Length; i++) 
			{
				bones[i].solverPosition = bones[i].transform.position;
				bones[i].solverRotation = bones[i].transform.rotation;

				if (i < bones.Length - 1) {
					bones[i].length = (bones[i].transform.position - bones[i + 1].transform.position).magnitude;
					bones[i].axis = Quaternion.Inverse(bones[i].transform.rotation) * (bones[i + 1].transform.position - bones[i].transform.position);

					chainLength += bones[i].length;
				}

				if (useRotationLimits) solverLocalPositions[i] = Quaternion.Inverse(GetParentSolverRotation(i)) * (bones[i].transform.position - GetParentSolverPosition(i));
			}
		}
		

		// After solving the chain
		
		private void OnPostSolve() 
		{
			// Rotating bones to match the solver positions
			if (!useRotationLimits) 
				MapToSolverPositions();
			else 
				MapToSolverPositionsLimited();

			lastLocalDirection = localDirection;
		}
		
		private void Solve(Vector3 targetPosition) 
		{
			// Forward reaching
			ForwardReach(targetPosition);

			// Backward reaching
			BackwardReach(bones[0].transform.position);
		}
		
		/*
		 * Stage 1 of FABRIK algorithm
		 * */
		private void ForwardReach(Vector3 position) 
		{
			// Lerp last bone's solverPosition to position
			bones[bones.Length - 1].solverPosition = Vector3.Lerp(bones[bones.Length - 1].solverPosition, position, IKPositionWeight);

			for (int i = 0; i < limitedBones.Length; i++) limitedBones[i] = false;
			
			for (int i = bones.Length - 2; i > -1; i--) {
				// Finding joint positions
				bones[i].solverPosition = SolveJoint(bones[i].solverPosition, bones[i + 1].solverPosition, bones[i].length);
				
				// Limiting bone rotation forward
				LimitForward(i, i + 1);
			}
			
			// Limiting the first bone's rotation
			LimitForward(0, 0);
		}

		private void SolverMove(int index, Vector3 offset) 
		{
			for (int i = index; i < bones.Length; i++) 
				bones[i].solverPosition += offset;
		}

		private void SolverRotate(int index, Quaternion rotation, bool recursive) 
		{
			for (int i = index; i < bones.Length; i++) 
			{
				bones[i].solverRotation = rotation * bones[i].solverRotation;
				if (!recursive)
					return;
			}
		}

		private void SolverRotateChildren(int index, Quaternion rotation) {
			for (int i = index + 1; i < bones.Length; i++) {
				bones[i].solverRotation = rotation * bones[i].solverRotation;
			}
		}

		private void SolverMoveChildrenAroundPoint(int index, Quaternion rotation) {
			for (int i = index + 1; i < bones.Length; i++) {
				Vector3 dir = bones[i].solverPosition - bones[index].solverPosition;
				bones[i].solverPosition = bones[index].solverPosition + rotation * dir;
			}
		}

		private Quaternion GetParentSolverRotation(int index) {
			if (index > 0) return bones[index - 1].solverRotation;
			if (bones[0].transform.parent == null) return Quaternion.identity;
			return bones[0].transform.parent.rotation;
		}

		private Vector3 GetParentSolverPosition(int index) {
			if (index > 0) return bones[index - 1].solverPosition;
			if (bones[0].transform.parent == null) return Vector3.zero;
			return bones[0].transform.parent.position;
		}

		private Quaternion GetLimitedRotation(int index, Quaternion q, out bool changed) {
			changed = false;
			
			Quaternion parentRotation = GetParentSolverRotation(index);
			Quaternion localRotation = Quaternion.Inverse(parentRotation) * q;
			
			Quaternion limitedLocalRotation = bones[index].rotationLimit.GetLimitedLocalRotation(localRotation, out changed);
			
			if (!changed) return q;
			
			return parentRotation * limitedLocalRotation;
		}

		/*
		 * Applying rotation limit to a bone in stage 1 in a more stable way
		 * */
		private void LimitForward(int rotateBone, int limitBone) {
			if (!useRotationLimits) return;
			if (bones[limitBone].rotationLimit == null) return;

			// Storing last bone's position before applying the limit
			Vector3 lastBoneBeforeLimit = bones[bones.Length - 1].solverPosition;

			// Moving and rotating this bone and all its children to their solver positions
			for (int i = rotateBone; i < bones.Length - 1; i++) {
				if (limitedBones[i]) break;

				Quaternion fromTo = Quaternion.FromToRotation(bones[i].solverRotation * bones[i].axis, bones[i + 1].solverPosition - bones[i].solverPosition);
				SolverRotate(i, fromTo, false);
			}

			// Limit the bone's rotation
			bool changed = false;
			Quaternion afterLimit = GetLimitedRotation(limitBone, bones[limitBone].solverRotation, out changed);

			if (changed) {
				// Rotating and positioning the hierarchy so that the last bone's position is maintained
				if (limitBone < bones.Length - 1) {
					Quaternion change = QuaTools.FromToRotation(bones[limitBone].solverRotation, afterLimit);
					bones[limitBone].solverRotation = afterLimit;
					SolverRotateChildren(limitBone, change);
					SolverMoveChildrenAroundPoint(limitBone, change);

					// Rotating to compensate for the limit
					Quaternion fromTo = Quaternion.FromToRotation(bones[bones.Length - 1].solverPosition - bones[rotateBone].solverPosition, lastBoneBeforeLimit - bones[rotateBone].solverPosition);

					SolverRotate(rotateBone, fromTo, true);
					SolverMoveChildrenAroundPoint(rotateBone, fromTo);

					// Moving the bone so that last bone maintains its initial position
					SolverMove(rotateBone, lastBoneBeforeLimit - bones[bones.Length - 1].solverPosition);
				} else {
					// last bone
					bones[limitBone].solverRotation = afterLimit;
				}
			}

			limitedBones[limitBone] = true;
		}
		
		/*
		 * Stage 2 of FABRIK algorithm
		 * */
		private void BackwardReach(Vector3 position) {
			if (useRotationLimits) BackwardReachLimited(position);
			else BackwardReachUnlimited(position);
		}
		
		/*
		 * Stage 2 of FABRIK algorithm without rotation limits
		 * */
		private void BackwardReachUnlimited(Vector3 position) {
			// Move first bone to position
			bones[0].solverPosition = position;
			
			// Finding joint positions
			for (int i = 1; i < bones.Length; i++) {
				bones[i].solverPosition = SolveJoint(bones[i].solverPosition, bones[i - 1].solverPosition, bones[i - 1].length);
			}
		}
		
		/*
		 * Stage 2 of FABRIK algorithm with limited rotations
		 * */
		private void BackwardReachLimited(Vector3 position) {
			// Move first bone to position
			bones[0].solverPosition = position;

			// Applying rotation limits bone by bone
			for (int i = 0; i < bones.Length - 1; i++) {
				// Rotating bone to look at the solved joint position
				Vector3 nextPosition = SolveJoint(bones[i + 1].solverPosition, bones[i].solverPosition, bones[i].length);

				Quaternion swing = Quaternion.FromToRotation(bones[i].solverRotation * bones[i].axis, nextPosition - bones[i].solverPosition);
				Quaternion targetRotation = swing * bones[i].solverRotation;

				// Rotation Constraints
				if (bones[i].rotationLimit != null) {
					bool changed = false;
					targetRotation = GetLimitedRotation(i, targetRotation, out changed);
				}

				Quaternion fromTo = QuaTools.FromToRotation(bones[i].solverRotation, targetRotation);
				bones[i].solverRotation = targetRotation;
				SolverRotateChildren(i, fromTo);

				// Positioning the next bone to its default local position
				bones[i + 1].solverPosition = bones[i].solverPosition + bones[i].solverRotation * solverLocalPositions[i + 1];
			}

			// Reconstruct solver rotations to protect from invalid Quaternions
			for (int i = 0; i < bones.Length; i++) {
				bones[i].solverRotation = Quaternion.LookRotation(bones[i].solverRotation * Vector3.forward, bones[i].solverRotation * Vector3.up);
			}
		}

		/*
		 * Rotate bones to match the solver positions when not using Rotation Limits
		 * */
		private void MapToSolverPositions() {
			bones[0].transform.position = bones[0].solverPosition;
			
			for (int i = 0; i < bones.Length - 1; i++) {
				if (XY) {
					bones[i].Swing2D(bones[i + 1].solverPosition);
				} else {
					bones[i].Swing(bones[i + 1].solverPosition);
				}
			}
		}

		/*
		 * Rotate bones to match the solver positions when using Rotation Limits
		 * */
		private void MapToSolverPositionsLimited() {
            bones[0].transform.position = bones[0].solverPosition;

			for (int i = 0; i < bones.Length; i++) {
				if (i < bones.Length - 1) bones[i].transform.rotation = bones[i].solverRotation;
			}
		}
	}
}

参考:https://forum.unity.com/threads/ik-chain-constraints-fabrik-algorithm.209306/

using UnityEngine;
using System.Collections;
 
/**
 * FABRIK Solver based on paper found here - www.andreasaristidou.com/publications/FABRIK.pdf  
 *
 * http://forum.unity3d.com/threads/187838-INVERSE-KINEMATICS-Scripting-Tutorial-Searching?p=1283005&viewfull=1#post1283005
*/
 
public class FABRIK : MonoBehaviour
{
    public int maxSolverIterations = 20; // 15 iterations is average solve time
    public float solveAccuracy = 0.001f;
   
    public IKChain myChain;
   
    void Start()
    {
        this.myChain.Init();
    }
   
    void Update()
    {
        if (this.myChain.target != null)
        {
            this.Solve(this.myChain);
        }
    }
   
    void Solve(IKChain chain)
    {
        var joints = chain.joints;
        if (joints.Length < 2)
            return;
       
        var rootToTargetDist = Vector3.Distance(joints[0].position, chain.target.position);
        var lambda = 0f;
       
        // Target unreachable, chain.length记录了chain的总长度
        if (rootToTargetDist > chain.length)
        {
        	// 遍历每段Bone
            for (int i = 0; i < joints.Length - 1; i++)
            {
            	// 计算这段Bone的长度, 与剩余的长度(包含Bone)的比例
                lambda = chain.segLengths[i] / Vector3.Distance(joints[i].position, chain.target.position);
                // 按照长度比例进行累加
                joints[i+1].position = (1 - lambda) * joints[i].position + lambda * chain.target.position;
            }
        }
        else // Target within reach
        {
            chain.Reset();
           
            var rootInitial = joints[0].position;
            var tries = 0;
            var targetDelta = Vector3.Distance(joints[joints.Length-1].position, chain.target.position);
           
            while (targetDelta > this.solveAccuracy  tries < this.maxSolverIterations)
            {
                // Forward reaching phase
               
                joints[joints.Length-1].position = chain.target.position;
               
                for (int i = joints.Length - 2; i > 0; i--)
                {
                    lambda = chain.segLengths[i] / Vector3.Distance(joints[i+1].position, joints[i].position);
                    var pos = (1 - lambda) * joints[i+1].position + lambda * joints[i].position;
                    joints[i].position = pos;
                    joints[i].position = this.Constraints(joints[i+1], joints[i]);
                }
               
                // Backward reaching phase
               
                joints[0].position = rootInitial;
               
                for (int i = 0; i < joints.Length - 1; i++)
                {
                    lambda = chain.segLengths[i] / Vector3.Distance(joints[i+1].position, joints[i].position);
                    var pos = (1 - lambda) * joints[i].position + lambda * joints[i+1].position;
                    joints[i+1].position = pos;
                    joints[i+1].position = this.Constraints(joints[i], joints[i+1]);
                }
 
                targetDelta = Vector3.Distance(joints[joints.Length-1].position, chain.target.position);
                tries++;
            }
        }
    }
   
    Vector3 Constraints(IKJoint j, IKJoint j_1)
    {
        return j_1.position;
    }
}
 

附录

Joint的WorldRotation与LocalRotation的互换

假设有Joint X,其Parent是A,A的Parent是Root R,他们的LocalRotation都知道,那么X的WorldRotation很好算:

WorldRotationX = LocalRotationR * LocalRotationA * LocalRotationX;

但假设我知道X、A和R的WorldRotation,如何求X的LocalRotation呢?

可以写个公式先看看:

WorldRotationA * LocalRotationX = WorldRotationX;

那么结果很明显:

LocalRotationX = WorldRotationA.Inverse() * WorldRotationX;

所以说,要想求某个Joint的Local旋转矩阵,用其Parent的世界矩阵的逆左乘该Joint的世界旋转矩阵即可


根据Joint的DeltaGlobalRotation获取其newLocalRotation

其实就是需要改变Joint原本的LocalRotation而已,Global的DeltaRotation是左乘,所以有

newLocalRotation = DeltaGlobalRotation * LocalRotation;

别想太复杂了


旋转矩阵之间的乘法满足交换律吗

Are rotation matrices commutative?
The two-dimensional case is the only non-trivial (i.e. not one-dimensional) case where the rotation matrices group is commutative, so that it does not matter in which order multiple rotations are performed.

只有2D的旋转矩阵的乘法满足交换律,其他维度的不满足


如何通过WorldChain获得LocalChain

其实可以拆分为一个个子问题,假设A和B的WorldTrasform知道, 那么如何算B在A的LocalTransform?

这个很简单,WorldA * LocalB = WorldB,所以LocalB = WorldA.Inverse() * WorldB

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402119.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenCV入门(六)快速学会OpenCV5图像处理基础(二)像素处理

OpenCV入门&#xff08;六&#xff09;快速学会OpenCV5图像处理基础&#xff08;二&#xff09; 像素是图像构成的基本单位&#xff0c;像素处理是图像处理的基本操作&#xff0c;可以通过位置索引的形式对图像内的元素进行访问、处理。 1.二值化操作 需要说明的是&#xff…

2023年上半年北京杭州/广州深圳软考中/高级报名入口

软考是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资格考试。 系统集成…

【人脸识别】ssd + opencv Eigenfaces 和 LBPH算法进行人脸监测和识别

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言ssd opencv Eigenfaces 和 LBPH算法进行人脸监测和识别1. ssd 目标监测2.opencv的三种人脸识别方法2.1 Eigenfaces2.2 LBPH前言 ssd opencv Eigenfaces 和 LB…

hadoop的补充配置与常用脚本

目录 历史服务器的配置&#xff1a; 添加配置&#xff1a; 分发集群&#xff1a; 日志聚集功能的配置&#xff1a; 添加配置&#xff1a; 分发配置给集群其他服务器&#xff1a; 集群的启动与停止&#xff1a; 整体启动和停止hdfs&#xff1a; 整体启动/停止YARN 启动…

微服务为什么要用到 API 网关?

本文介绍了 API 网关日志的价值&#xff0c;并以知名网关 Apache APISIX 为例&#xff0c;展示如何集成 API 网关日志。 作者程小兰&#xff0c;API7.ai 技术工程师&#xff0c;Apache APISIX Contributor。 原文链接 什么是微服务 微服务架构&#xff08;通常简称为微服务&a…

Promise.all、Promise.race、Promise.allSettled、Promise.any区别

1.Promise.all Promise.all()方法用于将多个 Promise 实例&#xff0c;包装成一个新的 Promise 实例。 const p Promise.all([p1, p2, p3]); p的状态由p1,p2,p3 决定&#xff0c;分成两种情况。 &#xff08;1&#xff09;只有p1、p2、p3的状态都变成fulfilled&#xff0c…

LeetCode - 42 接雨水

目录 题目来源 题目描述 示例 提示 题目解析 算法源码 题目来源 42. 接雨水 - 力扣&#xff08;LeetCode&#xff09; 题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例1 输入&…

金融交易行为监测方法——利用 CNN 模型实现行为识别

背景介绍在证劵交易所领域&#xff0c;曾经发生用户证券账号被盗事件&#xff0c;导致客户资产损失&#xff0c;例如&#xff1a;黑客获得了用户A的证券账号密码&#xff0c;利用多次的低买高卖将资产转移。本文中针对此类对敲欺诈的场景&#xff0c;采用将用户交易数据转换为图…

头脑风暴(一):Controller层前端传参接收;在Service层实现类中?为何要build相关构建器?添加套餐业务分析

文章目录1 MyBatis中Controller层List集合接收数据&#xff0c;泛型添加与否1.1 案例场景1.2 应该用什么接收1.3 是否可以用其他方式接收&#xff1f;1.4 LIst集合接收可否不指定泛型1.5 mybatis中使用基本类型接收数据&#xff1f;resultType是集合中的元素的类型&#xff0c;…

论文阅读笔记《GAMnet: Robust Feature Matching via Graph Adversarial-Matching Network》

核心思想 本文提出一种基于图对抗神经网络的图匹配算法&#xff08;GAMnet&#xff09;,使用图神经网络作为生成器分别生成源图和目标图的节点的特征&#xff0c;并用一个多层感知机作为辨别器来区分两个特征是否来自同一个图&#xff0c;通过对抗训练的办法提高生成器特征提取…

uniCloud基础使用-好文

云函数可以看做java或者php&#xff0c;作为后端服务cloudfunctions/myCloud/index.jsexports.main async (event, context) > {const { name, age } eventreturn 我是${name},今年${age} };pages/index/index.vue//callFunction方法 在前端和云端都可以调用另一个云函数 …

三十而立却被裁,打工人要如何应对职场危机?

又到金三银四就业季&#xff0c;对于部分职场人来说&#xff0c;年龄成为了他们找工作的最大限制。 因为绝大部分企业招聘中层干部以下岗位的时候&#xff0c;都会要求年龄不超过35周岁&#xff0c;再加上每年千万毕业生涌入社会&#xff0c;竞争程度相当激烈&#xff0c;这就导…

QML 模型(ListModel)

LIstModel&#xff08;列表模型&#xff09; ListModel 是ListElement定义的简单容器&#xff0c;每个定义都包含数据角色。内容可以在 QML 中动态定义或显式定义。 属性&#xff1a; count模型中数据条目的数量dynamic动态角色&#xff0c;默认情况下&#xff0c;角色的类型…

Android 进程间通信机制(一) IPC概念和模型

一. 前言 一直想把Binder机制认识清楚, 但是它涉及Android系统的Framework, Native, kernel层, 就需要你要有 C C基础阅读底层源码的能力, 目前笔者的水平,对Binder 在Native 和kernel层的实现原理和机制也是懵逼状态, 真的是博大精深, 故现阶段先把看懂和理解清楚的整理出来…

内核链表分析

内核链表 文章目录内核链表list_head创建链表添加节点1. list_add2. list_add_tail 接口删除节点宿主结构1.找出宿主结构 list_entry(ptr, type, member)2 container_of3. 宿主结构的遍历list_head 在 Linux 内核中&#xff0c;提供了一个用来创建双向循环链表的结构 list_hea…

海康摄像头使用RTSP

1.协议格式。海康威视IP摄像头rtsp协议地址如下&#xff1a;rtsp://[username]:[passwd][ip]:[port]/[codec]/[channel]/[subtype]/av_stream主码流&#xff1a;rtsp://admin:12345192.168.1.64:554/h264/ch1/main/av_streamrtsp://admin:12345192.168.1.64:554/MPEG-4/ch1/mai…

SpringCloud微服务保护

微服务保护微服务保护1.初识Sentinel1.1.雪崩问题及解决方案1.1.1.雪崩问题1.1.2.超时处理1.1.3.仓壁模式1.1.4.断路器1.1.5.限流1.1.6.总结1.2.服务保护技术对比1.3.Sentinel介绍和安装1.3.1.初识Sentinel1.3.2.安装Sentinel1.4.微服务整合Sentinel2.流量控制2.1.簇点链路2.1.…

java基础学习 day51 (匿名内部类)

1. 什么是匿名内部类&#xff1f; 隐藏了名字的内部类&#xff0c;实际名字为&#xff1a;外部类名$序号可以写在成员位置&#xff0c;为没有名字的成员内部类也可以写在局部位置&#xff0c;为没有名字的局部内部类 2. 匿名内部类的格式&#xff1f; new 类名/接口名() { 重…

深入理解AQS

概念设计初衷&#xff1a;该类利用 状态队列 实现了一个同步器&#xff0c;更多的是提供一些模板方法&#xff08;子类必须重写&#xff0c;不然会抛错&#xff09;。 设计功能&#xff1a;独占、共享模式两个核心&#xff0c;state、Queue2.1 statesetState、compareAndSetSta…

SpringMVC简单仿写

之前我分享过SpringMVC的基本原理与配置&#xff08;原文链接&#xff1a;https://blog.csdn.net/L170311/article/details/129339120&#xff09;,为了更深层次的学习&#xff0c;精益求精&#xff0c;手动仿写了一个MVC原理实现demo&#xff0c;一起学习一下吧 结构目录&…