代码改变世界

Improved collision detection and response简要分析与小的改进

2012-07-22 22:57  风恋残雪  阅读(1102)  评论(0编辑  收藏  举报

游戏中角色控制是不可或缺的一部分,无论是第一人称还是第三人称游戏,而角色控制有分为Kinematic 和dynamic两种,前者使用碰撞检测方法来实现,一些物理特性如重力、阻力等可以模拟出来,而后者则是基于物体的物理特性控制的,两者各有优缺点,具体可参考

There are at least two ways to implement a character controller for a 3D game: Kinematic and Dynamic Character Controllers. In this post we discuss the difference and which solution is better.
A short reminder: 
In this context a character controller is the piece of code that computes the movement of the player character in the game. It takes care that the user does not run through walls, can step up/down, jump etc. The character is often modeled as an upright capsule.
Related posts:  3D XNA Character Controller Example and Character Controller Requirements
Kinematic Character Controller:

The character controller is a dedicated piece of code that uses collision detection routines but no physics library. Physical behavior like gravity, non-penetration of solid objects is implemented explicitly in the character controller module. This type is also called Proxy Character Controller in Havok Physics. Bullet, PhysX and Havok have a Kinematic Character Controller implementation. Our 3D XNA Character Controller Example is another example of this category.
Dynamic Character Controller:

A physics library is used and the character is a special rigid body. The physics engine takes care of non-penetration, gravity etc. This type is also called Rigid Body Character Controller. Havok has a Dynamic Character Controller implementation.
Which solution is better?

Let's here why PhysX is against Dynamic Character Controllers:
(Lack Of) Continuous Collision Detection: Typical physics engines use discrete collision checks, leading to the famous tunneling effect that has plagued various commercial & non-commercial physics packages for years, which leads to three main problems:
The tunneling effect itself - if the character goes too fast, it might tunnel through a wall.
As a consequence, the maximum velocity of the character might be limited (hence also limiting the game-play possibilities).
Even without tunnel, the character might jitter when pushed forward in a corner. For example, the engine keeps moving it back and forth to slightly different positions.
No Direct Control: A rigid body is typically controlled with impulses or forces. It is nearly impossible to move it directly to its final position until you have converted the delta position vector to impulses or forces and applied them in hopes that the character will be where you wanted it to be as a result. Usually it doesn't work very well, in particular when the physics engine uses an imperfect linear solver.
Trouble with Friction: When the character is standing on a ramp, you don't want it to slide. Infinite friction is desired. When the character is moving forward on that same ramp, or sliding against a wall, you don't want it to slow down, thus a null friction is needed. These are usually defined with either 0 or infinite. However, the friction model might not be perfect, and what you actually get is very little friction, so you can still feel the character slowing down, or a high-but-not-infinite friction, so the character slides very slowly on that ramp no matter how artificially high the friction parameters are. The conflicting requirements for ramps mean that usually there is simply no way to perfectly model desired behavior.
Trouble with Restitution: Basically you don't want any restitution, ever. When the character moves fast and collides with a wall, you don't want it to bump against it. When the character falls from a height and lands on the ground, flexing his legs, you definitely don't want any bumps, which would visually look terrible. But once again, even when the restitution is exactly zero, you sometimes get a small bump nonetheless. This is not only related to the non-perfect nature of the linear solver, but also has to do with how typical penetration-depth-based engines recover from overlap situations, sometimes applying too high a force that repels objects more than desired.
Undesired Jumps: It is often important that a character stick to the ground, no matter what the physical behavior should be. For example, characters in action games tend to move fast, at unrealistic speeds. When they reach the top of a ramp, the physics engine often makes them jump a bit, in the same way a fast car would jump in the streets of San Francisco. But that is often not desired: the character should stick to the ground regardless of its current velocity. This is sometimes implemented using fixed joints, which is a terrible, terrible solution to a very simple problem that has been solved for years without requiring all the modern complexity of a physics engine.
Undesired Rotations: Finally, a character is always standing up and never rotating. However, a physics engine often has poor support for that kind of constraint, and a great deal of effort is put into just preventing a capsule around the character from falling (it should always stand up on its tip). This too is sometimes implemented using artificial joints, and the resulting system is neither very robust nor very fast.
(Excerpt from PhysX SDK manual v2.7.2 - please note that this is an older version of the PhysX SDK!)
Valid arguments - although nowadays most physics engines support continuous collision detection, so the first argument is partially valid today. Havok has a Dynamic Character Controller. Havok, what do you say in your defense?
The rigid body character controller has several advantages over the proxy character controller. These include:
Efficiency. The rigid body simulation is much more efficient than the phantom version. This is because of many internal optimizations in the rigid body simulation calculations.
Multithreading. On multithreaded platforms, the rigid body character controller multithreads naturally, as opposed to the proxy version which does not.
[...]
Character - rigid body and character - character interactions will be handled better by the rigid body character controller.
With these factors in mind we recommend using the rigid body character controller over using the proxy controller. However there are a number of caveats to be aware of in this choice. The behavior of the rigid body character controller may not be as smooth as the proxy controller, particularly for stair climbing.
(Excerpt from Havok Physics manual v5.5.0 - please note that this is an older version of Havok Physics!)
Points taken. Still, we prefer Kinematic Character Controllers for several reasons:
Many games or game engines do not use a physics engine, so the Dynamic Character Controller is not even an option. Integrating a physics engine only for the character controller does not sound reasonable.
Character Controller takes precedence over game physics. You can make a first person shooter without game physics, but you cannot make a first person shooter without a character controller. If game physics, e.g. a rolling barrel, behaves slightly odd, I do not care. But if the character controller behaves weird, the gameplay experience suffers. The character controller must be very precise, stable and tweakable.
Most games use custom character controller implementations. So the character controller code must be touched even if a standard physics engine is used. If a game developer has little physics experience, modifying a Kinematic Character Controller is easier. The kinematic controller deals with collisions, penetration depths and positions - whereas a dynamic controller deals with forces, impulses, friction, restitution, constraints etc.
If you already use a physics engine, it sounds tempting to use it for the character controller because many things come for free: non-penetration, gravity, pushing objects and being pushed. But: physics engines compute "rigid body dynamics". And a moving human does not behave like a rigid body! 
Rigid body dynamics supports restitution, friction, inertia - things that have a different meaning for a character. And a character controller supports arbitrary slope limits, stepping up/down, etc. - things that have no natural counterpart in rigid body dynamics. 
In the end, in a Dynamic Character Controller a lot of time is spent disabling features of the physics library and adding artificial non-physical behavior.
We feel Kinematic Character Controllers are the way to go. What do you think? If you have any arguments to add to the discussion, please let us know in the comments.
源地址:http://www.digitalrune.com/Support/Blog/tabid/719/EntryId/40/Kinematic-vs-Dynamic-Character-Controllers.aspx

我现在要说的也就是前面一种即kinematic character controoler,实现角色控制的方法可以用包围盒,胶囊,还有本篇所说的方法。用胶囊实现的角色控制方法可以参照此篇文章:http://www.cnblogs.com/fishboy82/archive/2008/03/01/1087553.html

本篇文章所说方法的原文可以从此处下载:http://www.peroxide.dk/papers/collision/collision.pdf

具体实现方法已经讲解得非常细致,而且还有源代码,实现起来应该也很容易,此处还有一篇中文翻译:http://blog.csdn.net/anluzsj/article/details/4595145

实现起来还算容易,但是当在场景中实验时发现,玩家可能会卡在某处不能移动,而作者却说他在自己的引擎中实现很完美,于是便上网查资料并仔细研究了下算法,看了下Irrlicht中的实现,跟原文中的基本一样,没有什么大的区别,所以也存在这个问题,有兴趣的可以看下这个地址:http://irrlicht.sourceforge.net/forum/viewtopic.php?p=197819。后来才知道之所以会卡住是因为用于实现玩家控制的单位球已经于其他物体相交了,虽然相互渗透的距离很小,但问题就出在这里,所以能解决这个问题就可以实现完整的碰撞检测和响应了。最后在此处找到了答案:http://www.gamedev.net/topic/571118-solved-fauerbys-improved-collision-detectionresponse/papalazaru 的帖子,但是他实现的方法有点小错误,你使用他的方法会发现有时候玩家会掉入地下或者进入封闭物体的内部,他的做法是把当前的包围球放大一点(比如1.01倍),然后查找与之相交的所有物体,找到一个相交三角形后求出相交的距离然后把球体沿法线的方向向后移动一定的距离,但是这样会有一个严重的问题,举个例子来说,如果当前玩家在一个由两面墙组成的角落里,假设墙的角度小于90度,并且于两面墙均略微相交,如果你先根据墙A的法线移动,这时可能包围球与墙B的相交比先前更严重,然后再根据另墙B的法线移动,这时你发现包围球已经与A相交更严重了,所以会出现前面所说的掉入地面或者卡在模型里面,就是这个问题。贴点代码看下,如想仔细了解还要下载他的源码看看吧。

bool IntersectionDetection::intersectTriangle(const Vector& p1, const Vector& p2, const Vector& p3, const Vector& n, IntersectionPacket* packet) const
{
	Vector closestPoint;
	float u, v;
	float dist2 = pointTriangleDistanceSquared(packet->m_position, p1, p2, p3, closestPoint, u, v);
	
	if(dist2 > packet->m_radius * packet->m_radius)
		return false;

	packet->m_intersectionFound = true;
	
	Vector delta = (packet->m_position - closestPoint);
	float dist = sqrt(dist2);

	if(dist > 0.0001f)
		packet->m_position += delta * (packet->m_radius - dist) / dist;
	
	return true;
}

正确的方法是找出所有相交的法线信息,根据相交的深度与相应的法线相乘,最后累加到一块,等所有模型都检测完相交以后再对包围球的位置进行移动,这样就不会出现上面的问题了。还有我觉得作者的算法中有个地方不太正确,当然这是我自己的看法,如果不对还请指正。贴下代码说容易:

// Set this to match application scale..
const float unitsPerMeter = 100.0f;
VECTOR CharacterEntity::collideWithWorld(const VECTOR& pos,
										 const VECTOR& vel)
{
	// All hard-coded distances in this function is
	// scaled to fit the setting above..
	float unitScale = unitsPerMeter / 100.0f;
	float veryCloseDistance = 0.005f * unitScale;
	// do we need to worry?
	if (collisionRecursionDepth>5)
		return pos;
	// Ok, we need to worry:
	collisionPackage->velocity = vel;
	45collisionPackage->normalizedVelocity = vel;
	collisionPackage->normalizedVelocity.normalize();
	collisionPackage->basePoint = pos;
	collisionPackage->foundCollision = false;
	// Check for collision (calls the collision routines)
	// Application specific!!
	world->checkCollision(collisionPackage);
	// If no collision we just move along the velocity
	if (collisionPackage->foundCollision == false) {
		return pos + vel;
	}
	// *** Collision occured ***
	// The original destination point
	VECTOR destinationPoint = pos + vel;
	VECTOR newBasePoint = pos;
	// only update if we are not already very close
	// and if so we only move very close to intersection..not
	// to the exact spot.
	if (collisionPackage->nearestDistance>=veryCloseDistance)
	{
		VECTOR V = vel;
		V.SetLength(collisionPackage->nearestDistance);
		newBasePoint = collisionPackage->basePoint + V;
		// Adjust polygon intersection point (so sliding
		// plane will be unaffected by the fact that we
		// move slightly less than collision tells us)
		V.normalize();
		// veryCloseDistance 在我理解就是用来阻止物体之间相交的,即物理引擎中
		// 习惯使用的空白(margin),所以此处的代码应该为 
		// collisionPackage->intersectionPoint -= min (collisionPackage->nearestDistance, veryCloseDistance) * V;
		// 而不是下面的代码所示,如果分析得不正确还请指正。
		collisionPackage->intersectionPoint -= veryCloseDistance * V;
	}
	// Determine the sliding plane
	VECTOR slidePlaneOrigin = collisionPackage->intersectionPoint;
	VECTOR slidePlaneNormal = newBasePoint-collisionPackage->intersectionPoint;
	slidePlaneNormal.normalize();
	PLANE slidingPlane(slidePlaneOrigin,slidePlaneNormal);
	// Again, sorry about formatting.. but look carefully ;)
	VECTOR newDestinationPoint = destinationPoint -
		slidingPlane.signedDistanceTo(destinationPoint)*
		slidePlaneNormal;
	// Generate the slide vector, which will become our new
	// velocity vector for the next iteration
	VECTOR newVelocityVector = newDestinationPoint - collisionPackage->intersectionPoint;
	// Recurse:
	// dont recurse if the new velocity is very small
	if (newVelocityVector.length() < veryCloseDistance)
	{
		return newBasePoint;
	}
	collisionRecursionDepth++;
	return collideWithWorld(newBasePoint,newVelocityVector);
}

好了,基本的问题解决了,加入重力和限制爬坡就好做了。更新:此方法是原生支持爬楼梯效果的,原来的说法有错误(带删除线的既为原话)。(还可以实现爬楼梯的效果,具体就不说了,爬楼梯的效果可以参照bullet和physx里面的character controller)。上面的方面在物理引擎里面专业的说法应该是recover from penetration,最近在研究bullet物理引擎,过程中遇到了不少问题,以后有机会会把使用bullet的心得写下来,毕竟国内的资料挺少的。如果有什么不对的地方请指正,以免误人子弟。Ps:目前基本上述算法实现的方法还是很稳定的,可以在简单的游戏中(不需要太多的物理特性)使用,如果没有更好的方法的话。 

参考文章:

1.http://www.digitalrune.com/Support/Blog/tabid/719/EntryId/40/Kinematic-vs-Dynamic-Character-Controllers.aspx

2.http://www.peroxide.dk/papers/collision/collision.pdf

3.http://blog.csdn.net/anluzsj/article/details/4595145

4.http://irrlicht.sourceforge.net/forum/viewtopic.php?p=197819

5.http://www.gamedev.net/topic/571118-solved-fauerbys-improved-collision-detectionresponse/