Unity引擎源码-物理系统详解-其二

发布于:2025-05-15 ⋅ 阅读:(21) ⋅ 点赞:(0)

继续我们关于Unity的物理系统的源码阅读,不过这一次我们的目标是PhysX引擎——这个Unity写了一堆脚本来调用API的实际用C++写成的底层物理引擎。

Github的地址如下:NVIDIA-Omniverse/PhysX: NVIDIA PhysX SDK (github.com)

下载后发现由三个文件组成:

其中blast主要是负责实现爆破、破坏效果计算的部分,flow则是负责实现流体效果,physX则是主题。

PhysX是一个内容非常繁多的引擎——其本质就是一堆API和封装好的算法,如果生硬地去学那么很快就会不知所措,你上学的时候也没人会叫你去背字典吧。所以我们更重要的是学习其思路与架构,还好作者非常贴心的给了我们许多示例,这样我们光是示例都可以学很久了。

这中间涉及到一个cmake的过程,我们直接一个快进来到PhysX的第一个示例:

HelloWorld

SnippetHelloWorld.cpp

代码如下:

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//  * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//  * Neither the name of NVIDIA CORPORATION nor the names of its
//    contributors may be used to endorse or promote products derived
//    from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved.
// Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved.
// Copyright (c) 2001-2004 NovodeX AG. All rights reserved.  

// ****************************************************************************
// This snippet illustrates simple use of physx
//
// It creates a number of box stacks on a plane, and if rendering, allows the
// user to create new stacks and fire a ball from the camera position
// ****************************************************************************

#include <ctype.h>
#include "PxPhysicsAPI.h"
#include "../snippetcommon/SnippetPrint.h"
#include "../snippetcommon/SnippetPVD.h"
#include "../snippetutils/SnippetUtils.h"

using namespace physx;

static PxDefaultAllocator		gAllocator;
static PxDefaultErrorCallback	gErrorCallback;
static PxFoundation*			gFoundation = NULL;
static PxPhysics*				gPhysics	= NULL;
static PxDefaultCpuDispatcher*	gDispatcher = NULL;
static PxScene*					gScene		= NULL;
static PxMaterial*				gMaterial	= NULL;
static PxPvd*					gPvd        = NULL;

static PxReal stackZ = 10.0f;

static PxRigidDynamic* createDynamic(const PxTransform& t, const PxGeometry& geometry, const PxVec3& velocity=PxVec3(0))
{
	PxRigidDynamic* dynamic = PxCreateDynamic(*gPhysics, t, geometry, *gMaterial, 10.0f);
	dynamic->setAngularDamping(0.5f);
	dynamic->setLinearVelocity(velocity);
	gScene->addActor(*dynamic);
	return dynamic;
}

static void createStack(const PxTransform& t, PxU32 size, PxReal halfExtent)
{
	PxShape* shape = gPhysics->createShape(PxBoxGeometry(halfExtent, halfExtent, halfExtent), *gMaterial);
	for(PxU32 i=0; i<size;i++)
	{
		for(PxU32 j=0;j<size-i;j++)
		{
			PxTransform localTm(PxVec3(PxReal(j*2) - PxReal(size-i), PxReal(i*2+1), 0) * halfExtent);
			PxRigidDynamic* body = gPhysics->createRigidDynamic(t.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			gScene->addActor(*body);
		}
	}
	shape->release();
}

void initPhysics(bool interactive)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);

	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);

	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	gDispatcher = PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher	= gDispatcher;
	sceneDesc.filterShader	= PxDefaultSimulationFilterShader;
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
	}
	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);

	for(PxU32 i=0;i<5;i++)
		createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);

	if(!interactive)
		createDynamic(PxTransform(PxVec3(0,40,100)), PxSphereGeometry(10), PxVec3(0,-50,-100));
}

void stepPhysics(bool /*interactive*/)
{
	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
}
	
void cleanupPhysics(bool /*interactive*/)
{
	PX_RELEASE(gScene);
	PX_RELEASE(gDispatcher);
	PX_RELEASE(gPhysics);
	if(gPvd)
	{
		PxPvdTransport* transport = gPvd->getTransport();
		PX_RELEASE(gPvd);
		PX_RELEASE(transport);
	}
	PX_RELEASE(gFoundation);
	
	printf("SnippetHelloWorld done.\n");
}

void keyPress(unsigned char key, const PxTransform& camera)
{
	switch(toupper(key))
	{
	case 'B':	createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);						break;
	case ' ':	createDynamic(camera, PxSphereGeometry(3.0f), camera.rotate(PxVec3(0,0,-1))*200);	break;
	}
}

int snippetMain(int, const char*const*)
{
#ifdef RENDER_SNIPPET
	extern void renderLoop();
	renderLoop();
#else
	static const PxU32 frameCount = 100;
	initPhysics(false);
	for(PxU32 i=0; i<frameCount; i++)
		stepPhysics(false);
	cleanupPhysics(false);
#endif

	return 0;
}

效果如图:

主要的功能就是实现了一堆盒子,同时允许发射球和生成盒子。

通过这个示例,我们来学一些基本的PhysX内容:

首先是关于PhysX引擎的初始化,使用PhysX引擎进行物理计算的话这一步就不可避免,具体来说初始化的内容包括:

可以看到涉及到了PhysX基础对象Foundation:

gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);

 这里的三个参数分别是:要使用的PhysX SDK版本号,内存分配和释放的回调接口,处理PhysX内部产生的错误和警告。

物理可视化调试:

	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);

PhysX Visual Debugger:可视化调试,参数就是我们刚刚创建的Foundation。

 我们根据之前创建的Foundation对象创建PVD对象的指针,然后定义传输方法并根据定义的传输方法建立连接。

物理引擎实例:

gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);

 PhysX引擎实例创建函数PxCreatePhysics需要五个参数:第一个是PhysX版本号,确保应用与库版本兼容;第二个是Foundation对象引用,提供内存管理和错误处理;第三个是物理单位比例设置,定义物理世界尺度和公差;第四个是布尔值,控制是否跟踪内存分配以便调试内存泄漏;最后一个是PVD指针,用于连接调试工具,可以为空。其中前三个参数是必需的,后两个是可选的。这些参数共同决定了物理引擎的行为特性、精度和调试能力。

创建和配置物理场景:

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	gDispatcher = PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher	= gDispatcher;
	sceneDesc.filterShader	= PxDefaultSimulationFilterShader;
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
	}

创建PhysX物理场景需要配置几个关键组件:首先是场景描述符,它继承物理引擎的尺度设置;然后设置重力向量(这里是标准地球重力);创建CPU分发器处理多线程物理计算;指定碰撞过滤着色器决定哪些物体可以相互碰撞;最后使用这些配置创建实际场景。如果启用了调试功能,还需要获取PVD客户端并配置要传输的数据类型,如约束、接触点和场景查询信息。一个完整的物理场景本质上是物理对象的容器,包含了模拟环境参数和处理物理计算的必要组件。 

这里涉及到了一个CPU分发器的概念:

CPU分发器是PhysX引擎中负责管理和分配物理模拟任务到多个CPU线程的组件。它本质上是一个任务调度器,将物理计算工作(如碰撞检测、约束求解等)分解成较小的任务并分配给不同线程执行,从而利用多核处理器提高模拟性能。

创建地面:

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);

这里可以看到的是我们使用的函数叫做addActor,这个Actor具体来说就是物理场景中的基本单位,代表一个可以参与物理模拟的对象。addActor方法就是将这些物理对象添加到场景中,使它们成为模拟的一部分。

创建盒子:

	for(PxU32 i=0;i<5;i++)
		createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);

有条件地创建球体:

	if(!interactive)
		createDynamic(PxTransform(PxVec3(0,40,100)), PxSphereGeometry(10), PxVec3(0,-50,-100));

这里涉及到了两个函数createStack和createDynamic:

static PxRigidDynamic* createDynamic(const PxTransform& t, const PxGeometry& geometry, const PxVec3& velocity=PxVec3(0))
{
	PxRigidDynamic* dynamic = PxCreateDynamic(*gPhysics, t, geometry, *gMaterial, 10.0f);
	dynamic->setAngularDamping(0.5f);
	dynamic->setLinearVelocity(velocity);
	gScene->addActor(*dynamic);
	return dynamic;
}

static void createStack(const PxTransform& t, PxU32 size, PxReal halfExtent)
{
	PxShape* shape = gPhysics->createShape(PxBoxGeometry(halfExtent, halfExtent, halfExtent), *gMaterial);
	for(PxU32 i=0; i<size;i++)
	{
		for(PxU32 j=0;j<size-i;j++)
		{
			PxTransform localTm(PxVec3(PxReal(j*2) - PxReal(size-i), PxReal(i*2+1), 0) * halfExtent);
			PxRigidDynamic* body = gPhysics->createRigidDynamic(t.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			gScene->addActor(*body);
		}
	}
	shape->release();
}

createDynamic函数用于创建单个动态刚体。它接收物体的变换(位置和旋转)、几何形状和初始速度作为参数,然后创建一个可以自由移动并受重力影响的物理对象。这个函数适合创建如球体、箱子等可移动物体,它设置了一些基本属性如角阻尼,并将创建的刚体添加到场景中。

createStack函数则更复杂,用于创建一个盒子堆叠的塔。它在指定位置创建多层盒子,每层的盒子数量逐渐减少,形成金字塔状结构。函数首先创建一个共享的盒子形状,然后循环创建多个动态刚体,计算每个盒子的位置,并将它们添加到场景中。这个函数展示了如何批量创建和组织物理对象,是物理引擎中常见的测试场景。

PhysX引擎在初始化时比较有特点的一点是:它的内容都是一项接着一项的,也就是后一项会在前一项的基础上初始化。

然后是关于物理模拟步进的函数部分:

void stepPhysics(bool /*interactive*/)
{
	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
}

某个程度上来说,物理模拟步进和Unity里的生命周期函数中的Update有些相似,比如他们都是每一帧执行一次,每次执行都会调用内部的函数等等;不过区别上首先物理模拟的步进的时间间隔其实不是严格意义上的逻辑帧,而是固定时长(具体数值就是simulate中的参数),这一点类似于FixedUpdate;且物理模拟步进并不是类自动调用的(Update只用写好函数内容,不用显式调用就会执行),需要一个手动调用;物理模拟步进必须执行这两个函数:simulate和fetchResults,Update可以为空等等。

然后是资源清理部分,防止内存泄露。

void cleanupPhysics(bool /*interactive*/)
{
	PX_RELEASE(gScene);
	PX_RELEASE(gDispatcher);
	PX_RELEASE(gPhysics);
	if(gPvd)
	{
		PxPvdTransport* transport = gPvd->getTransport();
		PX_RELEASE(gPvd);
		PX_RELEASE(transport);
	}
	PX_RELEASE(gFoundation);
	
	printf("SnippetHelloWorld done.\n");
}

按键检测输入部分:

void keyPress(unsigned char key, const PxTransform& camera)
{
	switch(toupper(key))
	{
	case 'B':	createStack(PxTransform(PxVec3(0,0,stackZ-=10.0f)), 10, 2.0f);						break;
	case ' ':	createDynamic(camera, PxSphereGeometry(3.0f), camera.rotate(PxVec3(0,0,-1))*200);	break;
	}
}

主程序部分:

int snippetMain(int, const char*const*)
{
#ifdef RENDER_SNIPPET
	extern void renderLoop();
	renderLoop();
#else
	static const PxU32 frameCount = 100;
	initPhysics(false);
	for(PxU32 i=0; i<frameCount; i++)
		stepPhysics(false);
	cleanupPhysics(false);
#endif

	return 0;
}

这样我们就大体上实现了这个逻辑的部分了,这个脚本主要负责来实现逻辑,而想让这个过程被我们在屏幕上看见的话就还需要渲染:

SnippetHelloWorldRender.cpp

#ifdef RENDER_SNIPPET

#include "PxPhysicsAPI.h"

#include "../snippetrender/SnippetRender.h"
#include "../snippetrender/SnippetCamera.h"

using namespace physx;

extern void initPhysics(bool interactive);
extern void stepPhysics(bool interactive);	
extern void cleanupPhysics(bool interactive);
extern void keyPress(unsigned char key, const PxTransform& camera);

namespace
{
Snippets::Camera* sCamera;

void renderCallback()
{
	stepPhysics(true);

	Snippets::startRender(sCamera);

	PxScene* scene;
	PxGetPhysics().getScenes(&scene,1);
	PxU32 nbActors = scene->getNbActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC);
	if(nbActors)
	{
		PxArray<PxRigidActor*> actors(nbActors);
		scene->getActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC, reinterpret_cast<PxActor**>(&actors[0]), nbActors);
		Snippets::renderActors(&actors[0], static_cast<PxU32>(actors.size()), true);
	}

	Snippets::finishRender();
}

void exitCallback()
{
	delete sCamera;
	cleanupPhysics(true);
}
}

void renderLoop()
{
	sCamera = new Snippets::Camera(PxVec3(50.0f, 50.0f, 50.0f), PxVec3(-0.6f,-0.2f,-0.7f));

	Snippets::setupDefault("PhysX Snippet HelloWorld", sCamera, keyPress, renderCallback, exitCallback);

	initPhysics(true);
	glutMainLoop();
}
#endif

比较简短的代码,分为渲染回调,退出回调以及渲染循环。渲染回调就是在执行渲染时执行的回调函数,我们开启物理模拟的步进,开始渲染并生成一个相机,从场景中获取所有actor并渲染;退出回调就是退出渲染时执行资源清除;渲染循环则是生成相机并设置物理环境之后初始化物理引擎,并开启Glut主循环。

什么是Glut主循环?

那么至此我们的HelloWorld的内容就完成了,从这个HelloWorld可以看出一个PhysX程序需要的东西包括:内部逻辑和渲染,其中内部逻辑首先需要去初始化一个引擎对象,然后由这个引起对象初始化一个PVD(可视化调试器),再由这个引擎对象初始化出物理场景,剩下的我们就去在物理场景中创建刚体actor等,写一些逻辑。我们还需要由物理模拟步进,用来在固定时长内执行物理计算和模拟,最后我们还需要一个资源清理,在退出程序时把所有Actor删除。

PhysX官方给的示例有些太多了,我计划看其中几个示例,大体上学会PhysX引擎如何使用即可,因为我们真正的目标是其底层的计算方法和逻辑。

Joint

SnippetJoint.cpp

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//  * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//  * Neither the name of NVIDIA CORPORATION nor the names of its
//    contributors may be used to endorse or promote products derived
//    from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved.
// Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved.
// Copyright (c) 2001-2004 NovodeX AG. All rights reserved.  

// ****************************************************************************
// This snippet illustrates simple use of joints in physx
//
// It creates a chain of objects joined by limited spherical joints, a chain
// joined by fixed joints which is breakable, and a chain of damped D6 joints
// ****************************************************************************

#include <ctype.h>
#include "PxPhysicsAPI.h"
#include "../snippetcommon/SnippetPrint.h"
#include "../snippetcommon/SnippetPVD.h"
#include "../snippetutils/SnippetUtils.h"

using namespace physx;

static PxDefaultAllocator		gAllocator;
static PxDefaultErrorCallback	gErrorCallback;
static PxFoundation*			gFoundation = NULL;
static PxPhysics*				gPhysics	= NULL;
static PxDefaultCpuDispatcher*	gDispatcher = NULL;
static PxScene*					gScene		= NULL;
static PxMaterial*				gMaterial	= NULL;
static PxPvd*					gPvd        = NULL;

static PxRigidDynamic* createDynamic(const PxTransform& t, const PxGeometry& geometry, const PxVec3& velocity=PxVec3(0))
{
	PxRigidDynamic* dynamic = PxCreateDynamic(*gPhysics, t, geometry, *gMaterial, 10.0f);
	dynamic->setAngularDamping(0.5f);
	dynamic->setLinearVelocity(velocity);
	gScene->addActor(*dynamic);
	return dynamic;
}

// spherical joint limited to an angle of at most pi/4 radians (45 degrees)
static PxJoint* createLimitedSpherical(PxRigidActor* a0, const PxTransform& t0, PxRigidActor* a1, const PxTransform& t1)
{
	PxSphericalJoint* j = PxSphericalJointCreate(*gPhysics, a0, t0, a1, t1);
	j->setLimitCone(PxJointLimitCone(PxPi/4, PxPi/4));
	j->setSphericalJointFlag(PxSphericalJointFlag::eLIMIT_ENABLED, true);
	return j;
}

// fixed, breakable joint
static PxJoint* createBreakableFixed(PxRigidActor* a0, const PxTransform& t0, PxRigidActor* a1, const PxTransform& t1)
{
	PxFixedJoint* j = PxFixedJointCreate(*gPhysics, a0, t0, a1, t1);
	j->setBreakForce(1000, 100000);	
	j->setConstraintFlag(PxConstraintFlag::eDRIVE_LIMITS_ARE_FORCES, true);
	j->setConstraintFlag(PxConstraintFlag::eDISABLE_PREPROCESSING, true);
	return j;
}

// D6 joint with a spring maintaining its position
static PxJoint* createDampedD6(PxRigidActor* a0, const PxTransform& t0, PxRigidActor* a1, const PxTransform& t1)
{
	PxD6Joint* j = PxD6JointCreate(*gPhysics, a0, t0, a1, t1);
	j->setMotion(PxD6Axis::eSWING1, PxD6Motion::eFREE);
	j->setMotion(PxD6Axis::eSWING2, PxD6Motion::eFREE);
	j->setMotion(PxD6Axis::eTWIST, PxD6Motion::eFREE);
	j->setDrive(PxD6Drive::eSLERP, PxD6JointDrive(0, 1000, FLT_MAX, true));
	return j;
}

typedef PxJoint* (*JointCreateFunction)(PxRigidActor* a0, const PxTransform& t0, PxRigidActor* a1, const PxTransform& t1);

// create a chain rooted at the origin and extending along the x-axis, all transformed by the argument t.

static void createChain(const PxTransform& t, PxU32 length, const PxGeometry& g, PxReal separation, JointCreateFunction createJoint)
{
	PxVec3 offset(separation/2, 0, 0);
	PxTransform localTm(offset);
	PxRigidDynamic* prev = NULL;

	for(PxU32 i=0;i<length;i++)
	{
		PxRigidDynamic* current = PxCreateDynamic(*gPhysics, t*localTm, g, *gMaterial, 1.0f);
		(*createJoint)(prev, prev ? PxTransform(offset) : t, current, PxTransform(-offset));
		gScene->addActor(*current);
		prev = current;
		localTm.p.x += separation;
	}
}

void initPhysics(bool /*interactive*/)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);
	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);

	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(),true, gPvd);
	PxInitExtensions(*gPhysics, gPvd);

	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.gravity = PxVec3(0.0f, -9.81f, 0.0f);
	gDispatcher = PxDefaultCpuDispatcherCreate(2);
	sceneDesc.cpuDispatcher	= gDispatcher;
	sceneDesc.filterShader	= PxDefaultSimulationFilterShader;
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
	}

	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);

	createChain(PxTransform(PxVec3(0.0f, 20.0f, 0.0f)), 5, PxBoxGeometry(2.0f, 0.5f, 0.5f), 4.0f, createLimitedSpherical);
	createChain(PxTransform(PxVec3(0.0f, 20.0f, -10.0f)), 5, PxBoxGeometry(2.0f, 0.5f, 0.5f), 4.0f, createBreakableFixed);
	createChain(PxTransform(PxVec3(0.0f, 20.0f, -20.0f)), 5, PxBoxGeometry(2.0f, 0.5f, 0.5f), 4.0f, createDampedD6);
}

void stepPhysics(bool /*interactive*/)
{
	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
}
	
void cleanupPhysics(bool /*interactive*/)
{
	PX_RELEASE(gScene);
	PX_RELEASE(gDispatcher);
	PxCloseExtensions();
	PX_RELEASE(gPhysics);
	if(gPvd)
	{
		PxPvdTransport* transport = gPvd->getTransport();
		PX_RELEASE(gPvd);
		PX_RELEASE(transport);
	}
	PX_RELEASE(gFoundation);
	
	printf("SnippetJoint done.\n");
}

void keyPress(unsigned char key, const PxTransform& camera)
{
	switch(toupper(key))
	{
	case ' ':	createDynamic(camera, PxSphereGeometry(3.0f), camera.rotate(PxVec3(0,0,-1))*200);	break;
	}
}

int snippetMain(int, const char*const*)
{
#ifdef RENDER_SNIPPET
	extern void renderLoop();
	renderLoop();
#else
	static const PxU32 frameCount = 100;
	initPhysics(false);
	for(PxU32 i=0; i<frameCount; i++)
		stepPhysics(false);
	cleanupPhysics(false);
#endif

	return 0;
}

这段代码的作用主要是去实现三种关节的效果:

球形关节其实就是允许连接的一方任意旋转但是不允许平移,所以说类似人类的肩关节,你可以任意旋转你的手臂但是你的手臂可不能飞出去(或者说只能飞出去一次);固定关节大家应该都懂,连接的两个刚体的相对运动被锁死;D6关节则是允许连接的两方在x,y,z轴任意控制是否允许移动和旋转,是自由度最高的关节。

效果如图:

具体到代码来说,我们可以看到和HelloWorld一样的基本架构:初始化引擎,物理模拟步进,资源清理,按键检测输入以及主程序,然后我们都会有一个创建动态刚体的函数,不过这个代码中我们还多了几步创建关节的过程。

SnippetJointRender.cpp

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//  * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//  * Neither the name of NVIDIA CORPORATION nor the names of its
//    contributors may be used to endorse or promote products derived
//    from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved.
// Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved.
// Copyright (c) 2001-2004 NovodeX AG. All rights reserved.  

#ifdef RENDER_SNIPPET

#include "PxPhysicsAPI.h"

#include "../snippetrender/SnippetRender.h"
#include "../snippetrender/SnippetCamera.h"

using namespace physx;

extern void initPhysics(bool interactive);
extern void stepPhysics(bool interactive);	
extern void cleanupPhysics(bool interactive);
extern void keyPress(unsigned char key, const PxTransform& camera);

namespace
{
Snippets::Camera* sCamera;

void renderCallback()
{
	stepPhysics(true);

	Snippets::startRender(sCamera);

	PxScene* scene;
	PxGetPhysics().getScenes(&scene,1);
	PxU32 nbActors = scene->getNbActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC);
	if(nbActors)
	{
		const PxVec3 dynColor(1.0f, 0.5f, 0.25f);

		PxArray<PxRigidActor*> actors(nbActors);
		scene->getActors(PxActorTypeFlag::eRIGID_DYNAMIC | PxActorTypeFlag::eRIGID_STATIC, reinterpret_cast<PxActor**>(&actors[0]), nbActors);
		Snippets::renderActors(&actors[0], static_cast<PxU32>(actors.size()), true, dynColor);
	}

	Snippets::finishRender();
}

void exitCallback()
{
	delete sCamera;
	cleanupPhysics(true);
}
}

void renderLoop()
{
	sCamera = new Snippets::Camera(PxVec3(34.613838f, 27.653027f, 9.363596f), PxVec3(-0.754040f, -0.401930f, -0.519496f));

	Snippets::setupDefault("PhysX Snippet Joint", sCamera, keyPress, renderCallback, exitCallback);

	initPhysics(true);
	glutMainLoop();
}
#endif

说实话,感觉基本一模一样,我都以为我打开错文件了,所以貌似没什么好说的。

这些示例有些没劲,让我们看几个有点水平的示例:

ContactReport

SnippetContactReport.cpp

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//  * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//  * Neither the name of NVIDIA CORPORATION nor the names of its
//    contributors may be used to endorse or promote products derived
//    from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Copyright (c) 2008-2025 NVIDIA Corporation. All rights reserved.
// Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved.
// Copyright (c) 2001-2004 NovodeX AG. All rights reserved.  

// ****************************************************************************
// This snippet illustrates the use of simple contact reports.
//
// It defines a filter shader function that requests touch reports for 
// all pairs, and a contact callback function that saves the contact points.  
// It configures the scene to use this filter and callback, and prints the 
// number of contact reports each frame. If rendering, it renders each 
// contact as a line whose length and direction are defined by the contact 
// impulse.
// 
// ****************************************************************************

#include "PxPhysicsAPI.h"
#include "../snippetutils/SnippetUtils.h"
#include "../snippetcommon/SnippetPrint.h"
#include "../snippetcommon/SnippetPVD.h"

using namespace physx;

static PxDefaultAllocator		gAllocator;
static PxDefaultErrorCallback	gErrorCallback;
static PxFoundation*			gFoundation = NULL;
static PxPhysics*				gPhysics	= NULL;
static PxDefaultCpuDispatcher*	gDispatcher = NULL;
static PxScene*					gScene		= NULL;
static PxMaterial*				gMaterial	= NULL;
static PxPvd*					gPvd        = NULL;

PxArray<PxVec3> gContactPositions;
PxArray<PxVec3> gContactImpulses;

static PxFilterFlags contactReportFilterShader(	PxFilterObjectAttributes attributes0, PxFilterData filterData0, 
												PxFilterObjectAttributes attributes1, PxFilterData filterData1,
												PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize)
{
	PX_UNUSED(attributes0);
	PX_UNUSED(attributes1);
	PX_UNUSED(filterData0);
	PX_UNUSED(filterData1);
	PX_UNUSED(constantBlockSize);
	PX_UNUSED(constantBlock);

	// all initial and persisting reports for everything, with per-point data
	pairFlags = PxPairFlag::eSOLVE_CONTACT | PxPairFlag::eDETECT_DISCRETE_CONTACT
			  |	PxPairFlag::eNOTIFY_TOUCH_FOUND 
			  | PxPairFlag::eNOTIFY_TOUCH_PERSISTS
			  | PxPairFlag::eNOTIFY_CONTACT_POINTS;
	return PxFilterFlag::eDEFAULT;
}

class ContactReportCallback: public PxSimulationEventCallback
{
	void onConstraintBreak(PxConstraintInfo* constraints, PxU32 count)	{ PX_UNUSED(constraints); PX_UNUSED(count); }
	void onWake(PxActor** actors, PxU32 count)							{ PX_UNUSED(actors); PX_UNUSED(count); }
	void onSleep(PxActor** actors, PxU32 count)							{ PX_UNUSED(actors); PX_UNUSED(count); }
	void onTrigger(PxTriggerPair* pairs, PxU32 count)					{ PX_UNUSED(pairs); PX_UNUSED(count); }
	void onAdvance(const PxRigidBody*const*, const PxTransform*, const PxU32) {}
	void onContact(const PxContactPairHeader& pairHeader, const PxContactPair* pairs, PxU32 nbPairs) 
	{
		PX_UNUSED((pairHeader));
		PxArray<PxContactPairPoint> contactPoints;
		
		for(PxU32 i=0;i<nbPairs;i++)
		{
			PxU32 contactCount = pairs[i].contactCount;
			if(contactCount)
			{
				contactPoints.resize(contactCount);
				pairs[i].extractContacts(&contactPoints[0], contactCount);

				for(PxU32 j=0;j<contactCount;j++)
				{
					gContactPositions.pushBack(contactPoints[j].position);
					gContactImpulses.pushBack(contactPoints[j].impulse);
				}
			}
		}
	}
};

ContactReportCallback gContactReportCallback;

static void createStack(const PxTransform& t, PxU32 size, PxReal halfExtent)
{
	PxShape* shape = gPhysics->createShape(PxBoxGeometry(halfExtent, halfExtent, halfExtent), *gMaterial);
	for(PxU32 i=0; i<size;i++)
	{
		for(PxU32 j=0;j<size-i;j++)
		{
			PxTransform localTm(PxVec3(PxReal(j*2) - PxReal(size-i), PxReal(i*2+1), 0) * halfExtent);
			PxRigidDynamic* body = gPhysics->createRigidDynamic(t.transform(localTm));
			body->attachShape(*shape);
			PxRigidBodyExt::updateMassAndInertia(*body, 10.0f);
			gScene->addActor(*body);
		}
	}
	shape->release();
}

void initPhysics(bool /*interactive*/)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);
	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);
	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);
	PxInitExtensions(*gPhysics,gPvd);
	PxU32 numCores = SnippetUtils::getNbPhysicalCores();
	gDispatcher = PxDefaultCpuDispatcherCreate(numCores == 0 ? 0 : numCores - 1);
	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.cpuDispatcher = gDispatcher;
	sceneDesc.gravity = PxVec3(0, -9.81f, 0);
	sceneDesc.filterShader	= contactReportFilterShader;			
	sceneDesc.simulationEventCallback = &gContactReportCallback;	
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
	}
	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);
	createStack(PxTransform(PxVec3(0,3.0f,10.0f)), 5, 2.0f);
}

void stepPhysics(bool /*interactive*/)
{
	gContactPositions.clear();
	gContactImpulses.clear();

	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
	printf("%d contact reports\n", PxU32(gContactPositions.size()));
}
	
void cleanupPhysics(bool /*interactive*/)
{
	gContactPositions.reset();
	gContactImpulses.reset();
    
	PX_RELEASE(gScene);
	PX_RELEASE(gDispatcher);
	PxCloseExtensions();
	PX_RELEASE(gPhysics);
	if(gPvd)
	{
		PxPvdTransport* transport = gPvd->getTransport();
		PX_RELEASE(gPvd);
		PX_RELEASE(transport);
	}
	PX_RELEASE(gFoundation);
	
	printf("SnippetContactReport done.\n");
}

int snippetMain(int, const char*const*)
{
#ifdef RENDER_SNIPPET
	extern void renderLoop();
	renderLoop();
#else
	initPhysics(false);
	for(PxU32 i=0; i<250; i++)
		stepPhysics(false);
	cleanupPhysics(false);
#endif

	return 0;
}

这段代码主要用来返回碰撞发生时返回碰撞信息,展示了如何使用碰撞报告系统来获取物体之间的接触信息,是一个相对来说更为复杂的代码。

static PxFilterFlags contactReportFilterShader(	PxFilterObjectAttributes attributes0, PxFilterData filterData0, 
												PxFilterObjectAttributes attributes1, PxFilterData filterData1,
												PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize)
{
	PX_UNUSED(attributes0);
	PX_UNUSED(attributes1);
	PX_UNUSED(filterData0);
	PX_UNUSED(filterData1);
	PX_UNUSED(constantBlockSize);
	PX_UNUSED(constantBlock);

	// all initial and persisting reports for everything, with per-point data
	pairFlags = PxPairFlag::eSOLVE_CONTACT | PxPairFlag::eDETECT_DISCRETE_CONTACT
			  |	PxPairFlag::eNOTIFY_TOUCH_FOUND 
			  | PxPairFlag::eNOTIFY_TOUCH_PERSISTS
			  | PxPairFlag::eNOTIFY_CONTACT_POINTS;
	return PxFilterFlag::eDEFAULT;
}

首先是这个碰撞过滤着色器类,我似乎还没有介绍过什么是碰撞过滤着色器:

碰撞过滤着色器是PhysX中的一个关键概念,它是一个用户定义的函数,用于决定哪些物体对需要进行碰撞检测以及如何处理它们的碰撞。这个名字中的"着色器"并不是指图形着色器,而是表示它"着色"或"过滤"了物理世界中的碰撞对。在物理模拟中,检查场景中每对物体之间的碰撞是非常昂贵的操作。在有N个物体的场景中,可能的碰撞对数量为N²/2。为了提高性能,我们需要一种方法来快速排除不需要检测的碰撞对,并为不同类型的碰撞指定不同的处理方式。

碰撞过滤着色器解决了这个问题,它允许我们:完全跳过某些物体对的碰撞检测、为不同的物体对设置不同的碰撞行为、控制哪些碰撞会生成回调通知。

这里又引出了一个物理模拟的宽相阶段的概念:

宽相阶段会涉及到很多算法,不过在后续的学习中我们应该还会遇到这个问题的,此处先不展开吧,回到我们的碰撞过滤着色器。概括地说,这个碰撞过滤着色器就是针对在宽相阶段判断可能会发生碰撞的物体进行检查来决定是否计算这两个物体的碰撞。

回到代码,我们可以看到函数参数列表很长:

然后是:

最后返回过滤标志即可,这段代码中它对所有物体对启用完整的碰撞检测和接触报告,而不考虑它们的属性或过滤数据。在实际游戏中,你可能会根据物体类型、碰撞组或其他标准来决定哪些物体应该碰撞,哪些应该生成报告。

class ContactReportCallback: public PxSimulationEventCallback
{
	void onConstraintBreak(PxConstraintInfo* constraints, PxU32 count)	{ PX_UNUSED(constraints); PX_UNUSED(count); }
	void onWake(PxActor** actors, PxU32 count)							{ PX_UNUSED(actors); PX_UNUSED(count); }
	void onSleep(PxActor** actors, PxU32 count)							{ PX_UNUSED(actors); PX_UNUSED(count); }
	void onTrigger(PxTriggerPair* pairs, PxU32 count)					{ PX_UNUSED(pairs); PX_UNUSED(count); }
	void onAdvance(const PxRigidBody*const*, const PxTransform*, const PxU32) {}
	void onContact(const PxContactPairHeader& pairHeader, const PxContactPair* pairs, PxU32 nbPairs) 
	{
		PX_UNUSED((pairHeader));
		PxArray<PxContactPairPoint> contactPoints;
		
		for(PxU32 i=0;i<nbPairs;i++)
		{
			PxU32 contactCount = pairs[i].contactCount;
			if(contactCount)
			{
				contactPoints.resize(contactCount);
				pairs[i].extractContacts(&contactPoints[0], contactCount);

				for(PxU32 j=0;j<contactCount;j++)
				{
					gContactPositions.pushBack(contactPoints[j].position);
					gContactImpulses.pushBack(contactPoints[j].impulse);
				}
			}
		}
	}
};

我们暂且不管上面哪些杂七杂八的方法,我们真正关心的只有onContact这一个函数,因为只有这个函数提供了具体的实现。我们遍历所有接触对(发生碰撞时可能有多个接触对),从接触对之中获取接触点并把这些接触点存储在一个新建的数组里,之后再从这个接触点数组里提取位置和冲量信息分别存储在对应的数组里。

void initPhysics(bool /*interactive*/)
{
	gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAllocator, gErrorCallback);
	gPvd = PxCreatePvd(*gFoundation);
	PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
	gPvd->connect(*transport,PxPvdInstrumentationFlag::eALL);
	gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);
	PxInitExtensions(*gPhysics,gPvd);
	PxU32 numCores = SnippetUtils::getNbPhysicalCores();
	gDispatcher = PxDefaultCpuDispatcherCreate(numCores == 0 ? 0 : numCores - 1);
	PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
	sceneDesc.cpuDispatcher = gDispatcher;
	sceneDesc.gravity = PxVec3(0, -9.81f, 0);
	sceneDesc.filterShader	= contactReportFilterShader;			
	sceneDesc.simulationEventCallback = &gContactReportCallback;	
	gScene = gPhysics->createScene(sceneDesc);

	PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
	if(pvdClient)
	{
		pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
	}
	gMaterial = gPhysics->createMaterial(0.5f, 0.5f, 0.6f);

	PxRigidStatic* groundPlane = PxCreatePlane(*gPhysics, PxPlane(0,1,0,0), *gMaterial);
	gScene->addActor(*groundPlane);
	createStack(PxTransform(PxVec3(0,3.0f,10.0f)), 5, 2.0f);
}

这一次的初始化引擎的函数中做了很多的修改,首先是加了一个回调函数gContactReportCallback,然后是场景的过滤着色器使用了contactReportFilterShader这个碰撞过滤着色器,然后还特别启用了接触点数据传输到PhysX Visual Debugger,便于可视化调试接触点。

void stepPhysics(bool /*interactive*/)
{
	gContactPositions.clear();
	gContactImpulses.clear();

	gScene->simulate(1.0f/60.0f);
	gScene->fetchResults(true);
	printf("%d contact reports\n", PxU32(gContactPositions.size()));
}

每次重新执行物理模拟步进时先清空接触点的位置和冲量信息,最后打印接触点位置数组的大小。

效果如图:

这个比较难以捕捉,冲量基本都是竖直向上或者向下的,反正是官方示例,没问题的大火。

SnippetContactReportRender.cpp

这个脚本里与之前的渲染脚本最大的区别是:

   if(gContactPositions.size())
   {
       gContactVertices.clear();
       for(PxU32 i=0;i<gContactPositions.size();i++)
       {
           gContactVertices.pushBack(gContactPositions[i]);
           gContactVertices.pushBack(gContactPositions[i]+gContactImpulses[i]*0.1f);
       }
       glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
       glEnableClientState(GL_VERTEX_ARRAY);
       glVertexPointer(3, GL_FLOAT, 0, &gContactVertices[0]);
       glDrawArrays(GL_LINES, 0, GLint(gContactVertices.size()));
       glDisableClientState(GL_VERTEX_ARRAY);
   }

可以看到我们把接触点的位置和冲量信息也给渲染出来可视化了,这样有更直观的体验。

不过这里我才想起来没有提到OpenGL和PhysX这二者的联系:PhysX内部本身是不包含渲染功能的,这里调用OpenGL只是为了更直观地让我们理解。

因为CSDN要是字数太多就会打字很卡,而代码里的每一个字母都被算作字,所以后续的示例中我只展示比较有特色的、有技术难点的代码,如果大家想要看完整版的话就去下载官方源码来看吧。

MultiThreading

这个脚本的作用就是展示如何在PhysX系统中多线程实现物理模拟计算,我来展示一些关键代码:

struct RaycastThread
{
	SnippetUtils::Sync*		mWorkReadySyncHandle;
	SnippetUtils::Thread*	mThreadHandle;
};

这个结构体是射线检测的线程结构体,这两个成员变量共同提供了线程管理和线程间通信的基础设施,使主线程能够控制射线检测线程的启动时机,并在需要时通知它们退出。 

static PxVec3 randVec3() 
{
	return (PxVec3(float(rand())/float(RAND_MAX),
		float(rand())/float(RAND_MAX), 
		float(rand())/float(RAND_MAX))*2.0f - PxVec3(1.0f)).getNormalized();
}

简单来说,这个函数生成一个指向三维空间中随机方向的单位向量,用于创建随机方向的射线,这个射线将用来验证我们的多线程计算的过程。 

static void threadExecute(void* data)
{
	RaycastThread* raycastThread = static_cast<RaycastThread*>(data);

	// Perform random raycasts against the scene until stop.
	for(;;)
	{
		// Wait here for the sync to be set then reset the sync
		// to ensure that we only perform raycast work after the 
		// sync has been set again.
		SnippetUtils::syncWait(raycastThread->mWorkReadySyncHandle);
		SnippetUtils::syncReset(raycastThread->mWorkReadySyncHandle);

		// If the thread has been signaled to quit then exit this function.
		if (SnippetUtils::threadQuitIsSignalled(raycastThread->mThreadHandle))
			break;

		// Perform a fixed number of random raycasts against the scene
		// and share the work between multiple threads.
		while (SnippetUtils::atomicDecrement(&gRaysAvailable) >= 0)
		{
			PxVec3 dir = randVec3();

			PxRaycastBuffer buf;
			gScene->raycast(PxVec3(0.0f), dir.getNormalized(), 1000.0f, buf, PxHitFlag::eDEFAULT);

			// If this is the last raycast then signal this to the main thread.
			if (SnippetUtils::atomicIncrement(&gRaysCompleted) == gRayCount)
			{
				SnippetUtils::syncSet(gWorkDoneSyncHandle);
			}
		}
	}

	// Quit the current thread.
	SnippetUtils::threadQuit(raycastThread->mThreadHandle);
}

这个代码则是负责执行具体的线程,是射线检测线程的执行函数,它在无限循环中等待主线程的工作信号,收到信号后检查是否需要退出,如不退出则开始执行射线检测任务。线程通过原子操作递减共享计数器来获取工作,确保多线程环境下工作不会重复分配。每次执行一次射线检测后,增加完成计数,当所有射线检测完成时通知主线程。整个过程使用同步原语和原子操作确保线程安全,实现了在物理模拟过程中高效并行执行射线检测的功能。

void createRaycastThreads()
{
	// Create and start threads that will perform raycasts.
	// Create a sync for each thread so that a signal may be sent
	// from the main thread to the raycast thread that it can start 
	// performing raycasts.
	for (PxU32 i=0; i < gNumThreads; ++i)
	{
		//Create a sync.
		gThreads[i].mWorkReadySyncHandle = SnippetUtils::syncCreate();

		//Create and start a thread.
		gThreads[i].mThreadHandle =  SnippetUtils::threadCreate(threadExecute, &gThreads[i]);
	}

	// Create another sync so that the raycast threads can signal to the main 
	// thread that they have finished performing their raycasts.
	gWorkDoneSyncHandle = SnippetUtils::syncCreate();
}

 创建线程的代码。

大体上来说,我们初始就创建好线程(不是线程池但是类似于线程池但没有线程回收和复用机制),然后在执行物理计算的时候同时通知子线程让其执行一个随机方向的射线检测,检测完成后再通知主线程,这就是整个代码的大体思路。

这个实例没有设置可视化,最后只会打印一个表示完成的信息。

customgeometry

SnippetCustomGeometry.cpp

这个脚本主要就是用来生成体素地形并初始化PhysX引擎,然后搭建物理场景并进行渲染可视化的脚本。

PX_INLINE void cookVoxelFace(bool reverseWinding) {
	for (int i = 0; i < 6; ++i) {
		gIndices[gIndexCount + i] = gVertexCount + gVertexOrder[i + (reverseWinding ? 6 : 0)];
	}
	gVertexCount += 4;
	gIndexCount += 6;
}

 这个函数用来生成体素表面。

void cookVoxelMesh() {
    // 1. 计算需要生成多少个面(只在体素与空气相邻处生成)
    // 2. 为每个可见的体素面生成四个顶点和六个索引(两个三角形)
    // 3. 使用PhysX的烘焙功能创建三角形网格
}

生成(其实是烘焙,烘焙和生成差不多)体素网格。

这里是烘焙和生成的区别:

void initVoxelMap() {
    // 创建体素地图
    // 设置尺寸和分辨率
    // 生成波浪形地形
}

 初始化体素地图,我们设置到尺寸和分辨率并生成一个波浪地形。

SnippetCustomGeometryRender.cpp

作为渲染的脚本,与其他示例中的渲染脚本几乎没有不同,都是取场景中的刚体逐个渲染。

VoxelMap.cpp

虽然很想把代码放上来,但是客观地说这个脚本非常的长,我在这里用AI生成的总结图大体展示一下其用途:
VoxelMap.cpp是实现自定义体素几何体的核心文件:

总的来说,这个文件实现了一个完整的体素地形系统,不仅能存储和管理体素数据,还能与PhysX引擎无缝集成,使体素地形能够参与物理模拟并与其他物体交互。它展示了如何将自定义数据结构转化为物理引擎可用的几何体。

效果如图:

pbdcloth

这个示例包含了PhysX中基于位置的动力学(Position-Based Dynamics, PBD)实现的布料模拟示例,展示了如何使用粒子系统创建一块布料,并模拟它掉落在一个旋转球体上的物理效果。比起之前的内容涉及的领域较为不同。

这里简单介绍一下PBD的概念:基于位置的动力学(PBD)是一种用于物理模拟的算法框架,与传统的基于力的方法不同,PBD直接操作物体的位置而不是通过力和加速度,这使得模拟更加稳定和可控。

尤其是布料和软体的计算方法中大都使用PBD来计算。

SnippetPBDCloth.cpp

static void initObstacles()
{
	//创建一个球状障碍物
}
static void initScene()
{
    // 初始化物理场景,启用GPU加速
}

这里涉及到一个GPU加速的问题:

 

这里涉及到一个求解器的概念:

关于物理的约束方程,求解器的内容,包括之前提到的宽相检测,都是物理引擎更底层的内容,等我们真正接触到的时候再细说。

GPU加速是基于CUDA实现的,之所以需要启动GPU加速是因为布料由大量粒子组成,而大量粒子的计算强度很大。

static void initCloth(const PxU32 numX, const PxU32 numZ, ...)
{
    // 创建布料粒子系统
    // 设置粒子属性和位置
    // 创建粒子间的弹簧连接
    // 定义布料的三角形网格
}

这里是我们实现布料模拟的核心函数,这里可以看到我们的布料其实是由大量的粒子组成的,这些粒子之间是一种弹簧的连接状态:想象一下我们现实生活中的布料,是不是你去扯布料时其本质也是布料的一个个组成部分互相牵扯。

这里还定义了布料的三角形网格,这个网格的具体作用是什么呢?

 

SnippetPBDClothRender.cpp

void onBeforeRenderParticles()
{
    // 从GPU内存复制粒子数据到CPU内存
}

 可能会有人很疑惑:为什么要把粒子的数据复制到CPU内存?这里涉及到一个比较复杂的过程,因为我们的代码要同时用到CUDA——用于GPU加速以及OpenGL——用于渲染,而这是两个完全不同的API。整个渲染过程总的来说可以这样概括:

物理模拟在GPU的CUDA内存中计算布料粒子位置,但OpenGL渲染系统无法直接访问这些数据,所以必须先将粒子位置从GPU的CUDA内存区域复制到CPU内存,然后再传输到GPU的OpenGL顶点缓冲区中,最后通过OpenGL的绘制命令将粒子渲染为可见的点,也就是一个GPU-CPU-GPU的过程。

非常遗憾因为我的电脑上内存已经严重不足了所以没有安装CUDA,所以我这里没法展示自己的图片了,大家可以自行运行查看效果。

内容已经很多了,PhysX给出的示例都非常的不错,更多示例我也就不费口舌了,建议大家都运行一下看看实现了什么效果,下一章节我将会深入PhysX的真正底层,也就是物理计算的底层实现。不过会涉及大量的数学和物理知识,也就是大量的理论,我会尽量简单概括,然后也没有必要把这些底层的东西看得非常重要,因为其实除非专门去研究算法的,不然对我们来说也就是拿来用而已,不用太放心上。下一章节我会大概介绍一下这个底层原理,可能会带着点GAMES201的内容讲解,然后再下一章节我会尝试在PhysX引擎的基础上尝试自己实现一些物理效果,只有能用这些API实现自己需求的效果才能算会用这些API。