Показать сообщение отдельно
Старый 28.06.2009, 00:50   #14
ABTOMAT
Ференька
 
Аватар для ABTOMAT
 
Регистрация: 26.01.2007
Адрес: улица Пушкина дом Колотушкина
Сообщений: 10,741
Написано 5,461 полезных сообщений
(для 15,675 пользователей)
Сообщение Ответ: Учебник по PhysX Wrapper для Blitz3D

Ragdoll


Знакомая картина, правда? Для того, чтобы игрок не наблюдал торчащие из стены ноги, существует технология под названием Ragdoll (рас. англ. - "тряпичная кукла"). Суть её такова: все кости трупа являются физическими объектами, в результетае этого они не станут
свободно проходить сквозь стены (как если использовать анимацию смерти). Плюс к этому мы получаем всегда разные позы трупов
(а не всего лишь 2-3 разных анимации), ну и отскакивание их от пуль, гранат и вообще реалистичное поведение. Хорошие примеры игр,
где используется регдолл: Half-Life 2, Painkiller, ну и вообще почти все современные игры (кроме, разве что, стратегий, хотя и там встречается) не обходятся без регдолла. Другое дело, что частенько регдолл сделан паршиво, чисто "для галочки", как, например, в Сталкере или CoD4, или отключены такие плюшки, как реагирование регдоллов на выстрелы (Crysis). Политкорректность, мать вашу

Давайте вообще делать игры только про пушистых кроликов, причём не менее 50% из них должны быть афроамериканцами, женщинами, людьми с нетрадиционной сексуальной ориентацией, чтобы избежать обвинений в дискриминации, и стрелять они друг в друга должны исключительно водяными пистолетиками? Сумасшедшим американским мамашам очень понравится.

Создаём простейший регдолл

Ну, начнём с простого. Суть данного семпла такова: можно создавать регдоллы в сцене. Пока всё. Как будем делать? Регдолл делается так:
1. Для каждой кости создаётся физическое тело (я буду делать Hull). Можно также делать и сферы, кубы и всё прочее (только не Тримеш), но это всё несерьёзно) ИМХО лучше использовать хуллы. Главное не переборщите с их детализацией, и всё будет ок.
2. Создаются джойнты в тех местах, где кости должны быть соединены. Джойнты настраиваются соответствующим образом. Кроме того, для плечей и голеней лучше использовать Сферические джойнты, т.к. эти сутставы у человека могут крутиться в разные стороны, а вот колени и локти - только в одной плоскости, поэтому для них используем Хиндж.
3. Все кости отпарентиваются (!!!). Это очень важно, т.к. даже если физические тела будут созданы как надо, то представьте, что будет при обновлении (установка кости в позицию и в поворот соотв. тела): обновили чайлд, затем обновили родитель, а т.к. чайлд к нему привязан, то он ушёл с той позиции, где он должен быть, в неизвестное направление, т.к. сдвинулся и повернулся его парент. Ну, а если у того чайлда были тоже чайлды, то понятно, что данный эффект усилится ещё больше и на выходе мы получим не регдолл, а чёрт знает что.

Но для начала нам нужна модель с костями. В качестве подопытного я выбрал спецназовца из Counter-Strike: Condition Zero. Можно было бы взять и из 1.6, но там нет русского спецназа Для того, чтобы упростить тутор, я сделал для него новый скелет, попрошче. Уважаемые про-моделлеры, я знаю, что модель ужасна и скелет тоже, но статья не об этом.



Кроме того, нам понадобится физическая модель для каждой кости:



Обратите внимание: центр каждого объекта совпадает координатами и коротом с центром каждой кости! Это важно (угадайте с одного раза почему) Кроме того, я решил назвать объекты, из которых потом будем делать хуллы, таким образом: его имя - это имя кости, к которой он тоностися + "px". Например, если кость называлась "Bone01", то меш для её физики будет называться "Bone01px". Это нужно для того, чтобы можно было приставив к имени "px" найти нужный меш. В-общем, вооружаемся методом copy-paste (многим из читателей не привыкать ) и дописывам где надо px. И да, экспортил без костей.
З.Ы. Если возникнут трудности с выставлением центров объектов в максе, могу уточнить. Пишите в обсуждении.

Так, модельки нашли. Теперь, наконец, пишем код. Нафиг предыдущие примеры, пишем заново. Итак:

Graphics3D 800,600,32,2
SetBuffer BackBuffer()

cam = CreateCamera()
PositionEntity cam,0,100,-200
CameraClsColor cam,128,128,128
CreateLight()

plane = CreatePlane()
EntityColor plane, 0,128,128

Repeat 
	RenderWorld()
	Flip
Until KeyHit(1)
End
Надеюсь, это объяснять не надо. Подрубаем физикс:
pxCreateWorld(1,"http://forum.boolean.name/")
И рендер физики в цикле:
pxRenderPhysic(30,0)
Так-с, физику подрубили. Теперь нам нужна наша модель. Загрузим её, вернее, их, и тут же скроем, шоб не мешали.

Spetsnaz = LoadAnimMesh("Spetsnaz.b3d")
SpetsnazPX = LoadAnimMesh("SpetsnazPX.b3d")

HideEntity Spetsnaz
HideEntity SpetsnazPX
Теперь надо из этих моделек создавать регдолл. Напишем функцию CreateRagdoll, в которую будем сувать
хендлы самоуцй модели человека и модели для создания физики. Кроме того, ещё нужны координаты, где етот регдолл создавать. Меш человека копируется.

Function CreateRagdoll(man, px,x#,y#,z#)
	man = CopyEntity(man)
	PositionEntity man,x,y,z	
End Function
При нажатии на пробел создаём регдолл в рандомной точке перед камерой.

If KeyHit(57) Then CreateRagdoll(Spetsnaz, SpetsnazPX,Rand(-100,100),50,Rand(0,100))
Запускаем, давим на кнопку. Челы создаются в рандомной точке, что собсно, от них и требовалось. Продолжаем разговор.
Думаю, нам потребуется тип для регдолла. Описываем его:

Type Ragdoll
Field Mesh ; Моделька
Field Joints[100] ; Хендлы джойнтов
Field Bodies[100] ; Хендлы тел
Field Bones[100] ; Хендлы костей
End Type

Собсно, так как регдолл - это несколько тел, а не одно, как в предыдущих примерах, то мы создаём уже массив для хендлов тел и ентитей. И плюсь ещё храним хендлы джойнтов, на случай, если нам придётся удалять "куклу" (а джойнты тоже надо удалить). Теперь при создании регдолла создаём новый объект типа Регдолл и пихаем сразу туда хендл модельки.

R.Ragdoll = New Ragdoll
R\Mesh = Man
А вот со всем остальным сложнее. Хитрый план таков: перебираем все чайлды, читаем ихние имена, прибавляем к ним "px" и ищем объект с таким именем в модельке для физики. Если он такой есть, то создаём из него хулл. Функцию создания хулла я уже приводил в четвёртом посте, вот она:

Function BodyCreateHull%(mesh%, mass#)

    Local nsurf = CountSurfaces(mesh)
    Local nvert = 0
    For ns = 1 To nsurf
        Local surf = GetSurface(mesh,ns)
        nvert = nvert + CountVertices(surf)
    Next
         vbank = CreateBank(nvert*4*3)
    nv = 0
    For ns = 1 To nsurf
        surf = GetSurface(mesh,ns)
        nvv = CountVertices(surf)
        For nvc = 0 To nvv - 1
             PokeFloat vbank,nv*12+0,VertexX(surf,nvc)
             PokeFloat vbank,nv*12+4,VertexY(surf,nvc)
             PokeFloat vbank,nv*12+8,VertexZ(surf,nvc)
            nv = nv+1
        Next
    Next
    Local bbb%= pxBodyCreateHull(vbank, nvert, mass)
    FreeBank vbank
    Return bbb
End Function
Стоп, но ведь у костей тоже есть свои чайлды? Значит, без рекурсии не обойтись. Делаем ещё одну функцию.

Function CreateHullsForAllChilds(mesh, pxmesh)
	HullMesh = FindChild(pxmesh, EntityName(mesh)+"px")
	If HullMesh Then
		Hull = BodyCreateHull(HullMesh, 10)
		pxBodySetPosition Hull, EntityX(Mesh,1), EntityY(Mesh,1), EntityZ(Mesh,1)
		pxBodySetRotation Hull, EntityPitch(Mesh,1), EntityYaw(Mesh,1), EntityRoll(Mesh,1)
	End If
	For i=1 To CountChildren(mesh)
		CreateHullsForAllChilds(GetChild(mesh,i), pxmesh)
	Next
End Function
Вот, создали для каждой кости нужный хулл с массой 10. Конечно, каждая часть тела весит по-разному, но мы делаем лишь основу, так что пока не станем забивать себе этим голову.
А как же с джойнтами? Давайте разбираться. Если у кости был парент (другая кость), то,
создав для такой кости хулл, его нужно присоединить к хуллу парента. Отсюда видно, что нужно передавать ещё и хендл родительского хулла, если таковой есть. Назовём его batya
Переделываем функцию, чтобы создавала нам и джойнты:

Function CreateHullsForAllChilds(mesh, pxmesh, batya=0)
	HullMesh = FindChild(pxmesh, EntityName(mesh)+"px")
	If HullMesh Then
		Hull = BodyCreateHull(HullMesh, 10)
		pxBodySetPosition Hull, EntityX(Mesh,1), EntityY(Mesh,1), EntityZ(Mesh,1)
		pxBodySetRotation Hull, EntityPitch(Mesh,1), EntityYaw(Mesh,1), EntityRoll(Mesh,1)
		If Batya Then
			Joint = pxJointCreateSpherical (batya,Hull,pxBodyGetPositionX(Hull),pxBodyGetPositionY(Hull),pxBodyGetPositionZ(Hull),0,1,0)
			pxJointSphericalSetLimitAngle(Joint, 30,1, 0)
			pxJointSphericalSetLimitTwist(Joint,-10,10,10,1,0)
			pxJointSphericalSetLimitSpring(Joint, 10, 1, 0)
		End If
	End If
	For i=1 To CountChildren(mesh)
		CreateHullsForAllChilds(GetChild(mesh,i), pxmesh, R, Hull)
	Next
End Function
На ведь нам надо ещё потом обновлять кости в соответствии с их телами. Т.е. хранить хендлы костей, джойнтов и хуллов, для чего мы и создали ранее соответствующие поля.

В таком случае в функцию передаём и хендл регдолла. При создании хулла ищем свободную ячейку массива и суём под этим номером тело и кость. Поскольку джойнтов меньше, чем тел, то и искать ячейку опять надо заново:
Function CreateHullsForAllChilds(mesh, pxmesh, R.Ragdoll, batya=0)
	HullMesh = FindChild(pxmesh, EntityName(mesh)+"px")
	If HullMesh Then
		Hull = BodyCreateHull(HullMesh, 10)
		pxBodySetPosition Hull, EntityX(Mesh,1), EntityY(Mesh,1), EntityZ(Mesh,1)
		pxBodySetRotation Hull, EntityPitch(Mesh,1), EntityYaw(Mesh,1), EntityRoll(Mesh,1)
		
		For i=0 To 100
			If R\Bodies[i] = 0 Then 
				R\Bodies[i] = Hull
				R\Bones[i] = Mesh
				Exit
			End If
		Next		
		If Batya Then
			Joint = pxJointCreateSpherical (batya,Hull,pxBodyGetPositionX(Hull),pxBodyGetPositionY(Hull),pxBodyGetPositionZ(Hull),0,1,0)
			pxJointSphericalSetLimitAngle(Joint, 30,1, 0)
			pxJointSphericalSetLimitTwist(Joint,-10,10,10,1,0)
			pxJointSphericalSetLimitSpring(Joint, 10, 1, 0)
			For i=0 To 100
			If R\Joints[i] = 0 Then 
				R\Joints[i] = Joint
				Exit
			End If
		Next	
		End If
	End If
	For i=1 To CountChildren(mesh)
		CreateHullsForAllChilds(GetChild(mesh,i), pxmesh, R, Hull)
	Next
End Function
Ну, и при создании регдолла юзаем эту функцию:

Function CreateRagdoll(man, px,x#,y#,z#)
	man = CopyEntity(man)
	PositionEntity man,x,y,z	
	R.Ragdoll = New Ragdoll
	R\Mesh = Man	
	CreateHullsForAllChilds(man, px, R)
End Function
Что там у нас дальше по списку? Ага, отпарентивание всех чилдов.
Ну, это уже к физиксу не относится Но для полноты картины примеду и эту функцию:

Function DeparentAllChilds(mesh)
	Repeat
		If CountChildren(mesh) = 0 Then Exit
		DeparentAllChilds(GetChild(mesh,1))	
	Forever 
	EntityParent mesh,0	
End Function
Почему GetChild (mesh,1) а не GetChild (mesh,i) ? Потому что когда в блице удаляется (отпарентивается) чайлд, то нумерация чайлдов обновляется заново. То есть, например, если мы возьмём колоду карт и будем вытаскивать всё время самую нижнюю, (первую) карту, то в конце концов вытащим их все. А если мы, начиная с нижней, будем вытаскивать 1-ю, 2-ю, 3-ю и т.д., то у нас ничего не получится, схватим еррор (когда будем пытаться вытащить 11-ю карту, когда осталось только 10). Так же и тут.

Применяем при создании регдолла.

Function CreateRagdoll(man, px,x#,y#,z#)
	man = CopyEntity(man)
	PositionEntity man,x,y,z	
	R.Ragdoll = New Ragdoll
	R\Mesh = Man	
	CreateHullsForAllChilds(man, px, R)
	DeparentAllChilds(man)
End Function
И да, посмотрите внимательно: мы передаём в R\Mesh хендл модели спецназовца, но ведь сам скин спецназовца является парентом этого объекта. Или не является, смотря как экспортить (виновата галочка Scene Root) Как же сделать это дело поуниверсальнее? Я решил делать так. Scene Root - это Pivot, а Скин - это Mesh. Значит, проверяем, если man у нас - это пивот, то ищем в нём первый же чайлд (который является в таком случае скином, а сам man удаляем. Здесь нам нужна блицовская команда EntityClass$(ent%), которая возвращает тип ентити, который в неё передали. Кстати, нет, чтобы возвращать целочисленную константу, дак нет, возвращает строчку (в рот Марку ноги!). Вот что она может возвращать:

Pivot
Light
Camera
Mirror
Listener
Sprite
Terrain
Plane
Mesh
MD2
BSP
Итак, сделаем проверку:

Function CreateRagdoll(man, px,x#,y#,z#)
man = CopyEntity(man)
PositionEntity man,x,y,z
R.Ragdoll = New Ragdoll
If EntityClass (man) <> "Mesh" Then
R\Mesh = GetChild(man,1)
EntityParent R\Mesh,0
FreeEntity man
Else
R\Mesh = Man
End If
CreateHullsForAllChilds(R\Mesh, px, R)
DeparentAllChilds(R\Mesh)
End Function

Так, а как быть с обновлением?
Ну, тут уже намного всё проще. Если вы внимательно читали всё предыдущее, то и сами догадаетесь, как это сделать. В-общем, перебираем все регдоллы:

Function UpdateRagdolls()
	For R.Ragdoll = Each Ragdoll
	
	Next
End Function
И затем перебираем ихние массивы с костями и бодями, ставя их на место. Если наткнулись на 0, то всё, выход из цикла:

Function UpdateRagdolls()
	For R.Ragdoll = Each Ragdoll
		For i= 0 To 100
			If R\Bodies[i] = 0 Then Exit
			pxBodySetEntity R\Bones[i], R\Bodies[i]
		Next
	Next
End Function
Так, ну вроде всё, можно запускать. Не забудьте только поставить в цикл обновление регдоллов.
Смотри: надо бы им и повороты задать рандомныя. Делаем:

	If KeyHit(57) Then CreateRagdoll(Spetsnaz, SpetsnazPX,Rand(-100,100),50,Rand(0,100), Rand(0,360), Rand(0,360), Rand(0,360))
Ну и соотв. изменения в фукнции создания:
Function CreateRagdoll(man, px,x#,y#,z#, pitch#,yaw#,roll#)
...
	RotateEntity man,Pitch,Yaw,Roll
Ну вот, вполне себе бодренько дёргают ручками-ножками:

Вообще, конечно, для более реалистичного регдолла нужно настраивать отдельно массы для каждой кости и использовать разные виды сочленений для разных суставов, но об этом я расскажу в ближайших постах. Но пок крайней мере мы получили вполне себе физический спецназ, который ни за что не станет торчать ногами из стены

Полный код примера а также все необходимые для запуска ресурсы вы найдёте в аттаче "PhysXRagdollExample1.zip"

PhysX Remote Debugger

Недавно Рендер познакомил меня со штукой с тким названием. Очень полезная вещь, скажу я вам, а именно она визуализирует всё, что творится в физическом мире. Пользоваться так: запускаем сначала дебаггер, а затем вашу игру с PhysX'ом, можно прямо из Блица. Настройки там элементарные, а польза впечатляет: если вы раньше недоумевали, почему ваш персонаж натыкается на невидимую стену, то теперь вы без турда её увидите. И да, иногда наблюдать забавно.
Весёлые картинки:




Сам дебаггер лежит в аттаче "RemoteDebugger.zip"

Ragdoll и анимация

Прочитав (надеюсь) внимательно этот пост, вы спросите: а нафига мы отпарентиваем кости в блице? Не проще ли это сделать в Максе?
Ответ прост: сохранение привязок даёт нам возможность использовать анимацию! Давайте сейчас и попробуем. После загрузки меша загрузим в него анимацию:

Walk = LoadAnimSeq(Spetsnaz, "Walk.b3d")
Ногами не пинать, анимировал сам за 10 минут.
И попробуем сделать так, чтобы человеки некоторое время анимировались (т.е. бегали, стреляли в игре). Для этого я завёл новый тип для "живых" спецназовцев.

Type Man
	Field Mesh
	Field pxMesh
End Type
Из него же вытекает и создание новых человеков (тут, надеюсь, всё понятно):
Function CreateMan(Mesh, pxMesh, x#,y#,z#)
	M.Man = New Man
	M\Mesh = CopyEntity(Mesh)
	PositionEntity M\Mesh,x,y,z
	Animate M\Mesh,1,1,1
	M\PXMesh = PXMesh
End Function
Не забудьте UpdateWorld в главном цикле!
Теперь при нажатии на пробел создаём не сами регдоллы, а Man'ов:

If KeyHit(57) Then CreateMan(Spetsnaz, SpetsnazPX,Rand(-100,100),50,Rand(0,100))

Вот они, кросавчеги, шлёпають:



Теперь надо переработать функцию создания регдолла, чтобы она не копировала меш, а создавала регдолл прямо из него. И да, нудно отключить анимацию меша, при физическом взаимодействии она нам не нужна. Думаю, вы и сами с этим справитесь, вот так выглядит переделанная функция:

Function CreateRagdoll(man, px)
	Animate man,0,0,1
	R.Ragdoll = New Ragdoll
	If EntityClass (man) <> "Mesh" Then	
		R\Mesh = GetChild(man,1)
		EntityParent R\Mesh,0
		FreeEntity man
	Else
		R\Mesh = Man
	End If
	CreateHullsForAllChilds(R\Mesh, px, R)
	DeparentAllChilds(R\Mesh)
End Function
Н, теперь можно сделать, чтобы спецназовцы становились регдолловцами при нажатии, скажем, на левый шифт. Создадим функцию "Обрегдолливания" всех спецов:

Function RagDollAllMen()
For M.Man = Each Man
CreateRagdoll(M\Mesh, M\pxMesh)
Delete M
Next
End Function

Ну, и вызываем по левому шифту.
If KeyHit(42) Then RagDollAllMen()
Челочвечки исправно падают. Но давайте для пушчего веселья сделаем обрегдолливание по клику мышкой. Для этого Man'ам надо добавить сферу для пика. Вообще, можно было сделать и прямо для анимированных моделек, но центр у них находится в ногах, и потом пик будет крайне неточным. Добавляем новый филд для пикательного пивота:
Field PickPivot
Теперь надо как-то найти радиус и позицию пикательного пивота. Как это сделать наглядно? А давайте вместо пивота поместим прозрачную сферу и отмасштабируем её так же, как задаём EntityRadius, так мы увидим, куда будет пикаться.
	M\PickPivot = CreateSphere()
	EntityColor M\PickPivot,0,0,255
	EntityAlpha M\PickPivot,0.3
	ScaleEntity M\PickPivot,15,35,15
	
	PositionEntity M\PickPivot,x,y+35,z
	EntityPickMode M\PickPivot, 1
	EntityRadius M\PickPivot,15,40
При значениях, как в этом куске кода пикательная сфера садится наиболее удачно. Убираем сферу, заменяя её пивотом, колор и альфу тоже фтопку.
При удалении Man'ов не забываем удалять и пивот:
FreeEntity M\PickPivot
Теперь готовим функцию Shoot:

Function Shoot(cam, x,y)
	CameraPick cam, x,y	
	If PickedEntity() Then
		For M.Man = Each Man
			If M\PickPivot = PickedEntity() Then
				CreateRagdoll(M\Mesh, M\pxMesh)
				FreeEntity M\PickPivot
				Delete M
				Exit
			End If
		Next
	End If
End Function
Ну, тут, думаю, всё ясно.
Вызываем в цикле по нажатию мышки:
If MouseHit(1) Then Shoot(cam, MouseX(), MouseY())
Ну, ещё можно украсить всякими шутками типа звука выстрела - и вот вам осовремененный аналог "Курошлёпа".
Полный код примера и ресурсы для его запуска вы найдёте в аттаче "PhysXExample2.zip"
Для тех, кто просто мимо проходил, есть откомпиленная версия в аттаче "PhysXExample2bin.zip" - оп! Чегой-то в аттач загружать наотрез отказывается: Загрузка файла прошла неудачно. Админы, ау! Пришлось положить сюда.

На этом всё. По всем вопросам пишите в обсуждение. Надеюсь, кому-то этот пост оказался полезен. В ближайших постах я расскажу, как настроить регдолл получше, а также, как делаь "капсюльную" физику игрока.
Вложения
Тип файла: zip PhysXRagdollExample1.zip (233.8 Кб, 1894 просмотров)
Тип файла: zip RemoteDebugger.zip (873.5 Кб, 1782 просмотров)
Тип файла: zip PhysXRagdollExample2.zip (370.8 Кб, 1970 просмотров)
__________________
Мои проекты:
Анальное Рабство
Зелёный Слоник
Дмитрий Маслов*
Различие**
Клюква**

* — в стадии разработки
** — в стадии проектирования
Для проектов в стадии проектирования приведены кодовые имена

(Offline)
 
Ответить с цитированием
Эти 63 пользователя(ей) сказали Спасибо ABTOMAT за это полезное сообщение:
3dr1aN (28.06.2009), Alex_Noc (29.06.2009), Android (08.07.2010), Arles (25.08.2009), baton4ik (02.02.2010), Blender (17.01.2010), Brain (15.01.2010), cahekp (01.07.2009), cancel (20.01.2010), Coks (05.04.2010), Coover (01.03.2012), CRASHER (30.06.2009), Damp (27.10.2009), DeeJex (24.02.2010), den (09.07.2010), Diablomania (14.08.2009), Diffuse13 (28.12.2010), Dream (20.02.2010), Dzirt (28.06.2009), Ичигорь (23.08.2010), falcon (28.06.2009), fanblitz (25.07.2009), FireOwl (08.10.2009), FrankH (17.08.2009), h1dd3n (04.07.2009), H@NON (28.06.2009), Harter (08.10.2009), HolyDel (27.07.2010), Hurrit (16.05.2010), Мик Данди (21.11.2009), impersonalis (28.06.2009), Leowey (02.01.2011), Main Cry (28.06.2009), MaxEDn2 (21.08.2009), Mhyhr (28.06.2009), MisterAlex (06.08.2010), Mr.Death (04.10.2010), MrFrosT1 (15.02.2011), m_512 (28.06.2009), Nex (28.06.2009), NitE (28.06.2009), PackegerX (03.02.2010), Randomize (03.07.2009), Reizel (30.12.2009), Reks888 (08.07.2010), robox (23.08.2009), rr333 (28.06.2009), Sashka007 (10.08.2009), SBJoker (28.06.2009), Slavik (28.06.2009), Spiderman (03.03.2010), St.AnGer (08.07.2009), strayhnd (30.06.2010), St_AnGer (24.08.2012), Tadeus (28.06.2009), Taugeshtu (28.06.2009), tirarex (25.11.2011), tormoz (28.06.2009), Townboy (05.11.2009), TxN (20.09.2009), viper86 (16.07.2009), WhiteBlack (27.07.2010), ІГРОГРАЙКО (02.07.2009)