// // 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-2021 NVIDIA Corporation. All rights reserved. // Copyright (c) 2004-2008 AGEIA Technologies, Inc. All rights reserved. // Copyright (c) 2001-2004 NovodeX AG. All rights reserved. #include #include "SampleCharacterHelpers.h" #include "AcclaimLoader.h" #include "PsMathUtils.h" #include "SampleAllocatorSDKClasses.h" #include "SampleArray.h" /////////////////////////////////////////////////////////////////////////////// static Acclaim::Bone* getBoneFromName(Acclaim::ASFData &data, const char *name) { // use a simple linear search -> probably we could use hash map if performance is an issue for (PxU32 i = 0; i < data.mNbBones; i++) { if (strcmp(name, data.mBones[i].mName) == 0) return &data.mBones[i]; } return 0; } /////////////////////////////////////////////////////////////////////////////// inline PxQuat EulerAngleToQuat(const PxVec3 &rot) { PxQuat qx(Ps::degToRad(rot.x), PxVec3(1.0f, 0.0f, 0.0f)); PxQuat qy(Ps::degToRad(rot.y), PxVec3(0.0f, 1.0f, 0.0f)); PxQuat qz(Ps::degToRad(rot.z), PxVec3(0.0f, 0.0f, 1.0f)); return qz * qy * qx; } /////////////////////////////////////////////////////////////////////////////// static PxTransform computeBoneTransform(PxTransform &rootTransform, Acclaim::Bone &bone, PxVec3* boneFrameData) { using namespace Acclaim; //PxTransform rootTransform(PxVec3(0.0f), PxQuat(PxIdentity)); PxTransform parentTransform = (bone.mParent) ? computeBoneTransform(rootTransform, *bone.mParent, boneFrameData) : rootTransform; PxQuat qWorld = EulerAngleToQuat(bone.mAxis); PxVec3 offset = bone.mLength * bone.mDirection; PxQuat qDelta = PxQuat(PxIdentity); PxVec3 boneData = boneFrameData[bone.mID-1]; if (bone.mDOF & BoneDOFFlag::eRX) qDelta = PxQuat(Ps::degToRad(boneData.x), PxVec3(1.0f, 0.0f, 0.0f)) * qDelta; if (bone.mDOF & BoneDOFFlag::eRY) qDelta = PxQuat(Ps::degToRad(boneData.y), PxVec3(0.0f, 1.0f, 0.0f)) * qDelta; if (bone.mDOF & BoneDOFFlag::eRZ) qDelta = PxQuat(Ps::degToRad(boneData.z), PxVec3(0.0f, 0.0f, 1.0f)) * qDelta; PxQuat boneOrientation = qWorld * qDelta * qWorld.getConjugate(); PxTransform boneTransform(boneOrientation.rotate(offset), boneOrientation); return parentTransform.transform(boneTransform); } /////////////////////////////////////////////////////////////////////////////// static PxTransform computeBoneTransformRest(Acclaim::Bone &bone) { using namespace Acclaim; PxTransform parentTransform = (bone.mParent) ? computeBoneTransformRest(*bone.mParent) : PxTransform(PxVec3(0.0f), PxQuat(PxIdentity)); PxVec3 offset = bone.mLength * bone.mDirection; PxTransform boneTransform(offset, PxQuat(PxIdentity)); return parentTransform.transform(boneTransform); } /////////////////////////////////////////////////////////////////////////////// Character::Character() : mCurrentMotion(NULL), mTargetMotion(NULL), mBlendCounter(0), mCharacterScale(1.0f), mASFData(NULL), mCharacterPose(PxVec3(0.0f), PxQuat(PxIdentity)), mFrameTime(0.0f) { } /////////////////////////////////////////////////////////////////////////////// int Character::addMotion(const char* amcFileName, PxU32 start, PxU32 end) { Acclaim::AMCData AMCData; if (Acclaim::readAMCData(amcFileName, *mASFData, AMCData) == false) { AMCData.release(); return -1; } if (AMCData.mNbFrames == 0) { AMCData.release(); return -1; } if (end >= AMCData.mNbFrames) end = AMCData.mNbFrames - 1; Motion* motion = SAMPLE_NEW(Motion)(); if (buildMotion(AMCData, *motion, start, end) == false) { SAMPLE_FREE(motion); AMCData.release(); return -1; } mMotions.pushBack(motion); mCurrentMotion = motion; // set the frame counter to 0 mFrameTime = 0.0f; AMCData.release(); return mMotions.size() - 1; } /////////////////////////////////////////////////////////////////////////////// bool Character::buildMotion(Acclaim::AMCData &amcData, Motion &motion, PxU32 start, PxU32 end) { using namespace Acclaim; if (mASFData == NULL) return false; motion.mNbFrames = end - start + 1; motion.mMotionData = SAMPLE_NEW(MotionData)[motion.mNbFrames]; // compute bounds of all the motion data on normalized frame PxBounds3 bounds = PxBounds3::empty(); for (PxU32 i = start; i < end; i++) { Acclaim::FrameData &frameData = amcData.mFrameData[i]; PxTransform rootTransform(PxVec3(0.0f), EulerAngleToQuat(frameData.mRootOrientation)); for (PxU32 j = 0; j < mASFData->mNbBones; j++) { PxTransform t = computeBoneTransform(rootTransform, mASFData->mBones[j], frameData.mBoneFrameData); bounds.include(t.p); } } Acclaim::FrameData& firstFrame = amcData.mFrameData[0]; Acclaim::FrameData& lastFrame = amcData.mFrameData[amcData.mNbFrames-1]; // compute direction vector motion.mDistance = mCharacterScale * (lastFrame.mRootPosition - firstFrame.mRootPosition).magnitude(); PxVec3 firstPosition = firstFrame.mRootPosition; PX_UNUSED(firstPosition); PxVec3 firstAngles = firstFrame.mRootOrientation; PxQuat firstOrientation = EulerAngleToQuat(PxVec3(0, firstAngles.y, 0)); for (PxU32 i = 0; i < motion.mNbFrames; i++) { Acclaim::FrameData& frameData = amcData.mFrameData[i+start]; MotionData &motionData = motion.mMotionData[i]; // normalize y-rot by computing inverse quat from first frame // this makes all the motion aligned in the same (+ z) direction. PxQuat currentOrientation = EulerAngleToQuat(frameData.mRootOrientation); PxQuat qdel = firstOrientation.getConjugate() * currentOrientation; PxTransform rootTransform(PxVec3(0.0f), qdel); for (PxU32 j = 0; j < mNbBones; j++) { PxTransform boneTransform = computeBoneTransform(rootTransform, mASFData->mBones[j], frameData.mBoneFrameData); motionData.mBoneTransform[j] = boneTransform; } //PxReal y = mCharacterScale * (frameData.mRootPosition.y - firstPosition.y) - bounds.minimum.y; motionData.mRootTransform = PxTransform(PxVec3(0.0f, -bounds.minimum.y, 0.0f), PxQuat(PxIdentity)); } // now make the motion cyclic by linear interpolating root position and joint angles const PxU32 windowSize = 10; for (PxU32 i = 0; i <= windowSize; i++) { PxU32 j = motion.mNbFrames - 1 - windowSize + i; PxReal t = PxReal(i) / PxReal(windowSize); MotionData& motion_i = motion.mMotionData[0]; MotionData& motion_j = motion.mMotionData[j]; // lerp root translation PxVec3 blendedRootPos = (1.0f - t) * motion_j.mRootTransform.p + t * motion_i.mRootTransform.p; for (PxU32 k = 0; k < mNbBones; k++) { PxVec3 pj = motion_j.mRootTransform.p + motion_j.mBoneTransform[k].p; PxVec3 pi = motion_i.mRootTransform.p + motion_i.mBoneTransform[k].p; PxVec3 p = (1.0f - t) * pj + t * pi; motion_j.mBoneTransform[k].p = p - blendedRootPos; } motion_j.mRootTransform.p = blendedRootPos; } return true; } /////////////////////////////////////////////////////////////////////////////// // we apply pose blending if there is transition from one motion to another bool Character::computeFramePose() { PxU32 frameNo = PxU32(mFrameTime); if (frameNo >= mCurrentMotion->mNbFrames) return false; MotionData& motionData = mCurrentMotion->mMotionData[frameNo]; // compute blended motion when a target motion is set if (mTargetMotion) { // first pose from target motion data MotionData& targetData = mTargetMotion->mMotionData[0]; PxReal t = PxReal(mBlendCounter) / 10.0f; for (PxU32 i = 0; i < mNbBones; i++) { mCurrentBoneTransform[i].p = t * motionData.mBoneTransform[i].p + (1.0f - t) * targetData.mBoneTransform[i].p; mCurrentBoneTransform[i].q = motionData.mBoneTransform[i].q; } mCurrentRootTransform.p = t * motionData.mRootTransform.p + (1.0f - t) * targetData.mRootTransform.p; mCurrentRootTransform.q = targetData.mRootTransform.q; } else { for (PxU32 i = 0; i < mNbBones; i++) mCurrentBoneTransform[i] = motionData.mBoneTransform[i]; mCurrentRootTransform = motionData.mRootTransform; } return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::create(const char *asfFileName, PxReal scale) { mCharacterScale = scale; if (readSetup(asfFileName) == false) return false; return true; } ////////////////////////////////////////////////////////////////////////////// bool Character::faceToward(const PxVec3& targetDir, PxReal angleLimitPerFrame) { PxVec3 oldDir = mCharacterPose.q.rotate(PxVec3(0,0,1)); PxVec3 up(0,1,0); PxVec3 newDir = PxVec3(targetDir.x, 0, targetDir.z).getNormalized(); PxVec3 right = -1.0f * oldDir.cross(up); PxReal cos = newDir.dot(oldDir); PxReal sin = newDir.dot(right); PxReal angle = atan2(sin, cos); PxReal limit = angleLimitPerFrame * (PxPi / 180.0f); if (angle > limit) angle = limit; else if (angle < -limit) angle = -limit; PxQuat qdel(angle, up); mCharacterPose.q = qdel * mCharacterPose.q; return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::getFramePose(PxTransform &rootTransform, SampleArray &positions, SampleArray &indexBuffers) { if (mCurrentMotion == NULL) return false; PxU32 frameNo = PxU32(mFrameTime); if (frameNo >= mCurrentMotion->mNbFrames) return false; positions.resize(mNbBones+1); // copy precomputed bone position in local space positions[0] = PxVec3(0.0f); // root position for (PxU32 i = 0; i < mNbBones; i++) positions[i+1] = mCurrentBoneTransform[i].p; // copy capsule index data indexBuffers.resize(mASFData->mNbBones * 2); for (PxU32 i = 0; i < mASFData->mNbBones; i++) { Acclaim::Bone& bone = mASFData->mBones[i]; indexBuffers[i*2] = bone.mID; indexBuffers[i*2+1] = (bone.mParent) ? bone.mParent->mID : 0; } // compute root transform rootTransform = mCharacterPose.transform(mCurrentRootTransform); return true; } ////////////////////////////////////////////////////////////////////////////// bool Character::move(PxReal speed, bool jump, bool active ) { if (mCurrentMotion == NULL) return false; if (mBlendCounter > 0) mBlendCounter--; if (mTargetMotion && (mBlendCounter == 0)) { mBlendCounter = 0; mCurrentMotion = mTargetMotion; mFrameTime = 0.0f; mTargetMotion = NULL; } PxU32 nbFrames = mCurrentMotion->mNbFrames; PxReal distance = mCurrentMotion->mDistance; PxReal frameDelta = 0.0f; const PxReal angleLimitPerFrame = 3.0f; if (jump) { frameDelta = 1.0f; } else if (active && (mBlendCounter == 0)) { // compute target orientation PxVec3 dir = mTargetPosition - mCharacterPose.p; dir.y = 0.0f; PxReal curDistance = dir.magnitude(); if (curDistance > 0.01f) faceToward(dir, angleLimitPerFrame); frameDelta = speed * PxReal(nbFrames) * (curDistance / distance); } else { frameDelta = 0; } mCharacterPose.p = mTargetPosition; mFrameTime += frameDelta; PxU32 frameNo = PxU32(mFrameTime); if (frameNo >= nbFrames) { if (jump) mFrameTime = PxReal(nbFrames) - 1.0f; else mFrameTime = 0.0f; } // compute pose of all the bones at current frame (results are used by both getFramePose and Skin) computeFramePose(); return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::readSetup(const char* asfFileName) { if (mASFData) mASFData->release(); mASFData = new Acclaim::ASFData; if (Acclaim::readASFData(asfFileName, *mASFData) == false) { delete mASFData; mASFData = NULL; return false; } mNbBones = mASFData->mNbBones; // scale bone length for (PxU32 i = 0; i < mASFData->mNbBones; i++) { Acclaim::Bone& bone = mASFData->mBones[i]; bone.mLength *= mCharacterScale; } return true; } /////////////////////////////////////////////////////////////////////////////// void Character::release() { for (PxU32 i = 0; i < mMotions.size(); i++) { if (mMotions[i]) mMotions[i]->release(); } if (mASFData) mASFData->release(); } /////////////////////////////////////////////////////////////////////////////// void Character::resetMotion(PxReal firstFrame) { mFrameTime = firstFrame; } /////////////////////////////////////////////////////////////////////////////// bool Character::setGlobalPose(const PxTransform &transform) { mCharacterPose.p = transform.p; return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::setGoalPosition(const PxVec3 pos) { mGoalPosition = pos; return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::setMotion(PxU32 id, bool init) { if (id >= mMotions.size()) return false; if (init) { mCurrentMotion = mMotions[id]; mTargetMotion = NULL; computeFramePose(); return true; } if (mCurrentMotion == NULL) { mCurrentMotion = mMotions[id]; return true; } if (mCurrentMotion == mMotions[id]) return true; if (mTargetMotion == mMotions[id]) return true; mTargetMotion = mMotions[id]; mBlendCounter = 10; return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::setTargetPosition(const PxVec3 pos) { mTargetPosition = pos; return true; } /////////////////////////////////////////////////////////////////////////////// bool Character::setForward(void) { PxVec3 p = mCharacterPose.p; PxVec3 dir = mGoalPosition - p; dir.normalize(); dir = mCharacterPose.q.rotate(dir); PxU32 nbFrames = mCurrentMotion->mNbFrames; PxReal distance = mCurrentMotion->mDistance; PxReal frameDelta = distance / PxReal(nbFrames); mTargetPosition = p + frameDelta * dir; return true; } /////////////////////////////////////////////////////////////////////////////// bool Skin::bindToCharacter(Character &character, SampleArray &positions) { // currently we just bind everything to the 'thorax' (between neck and clavicles). // Modify this if you need to do more elaborate skin binding mBindPos.resize(positions.size()); Acclaim::Bone* bone = getBoneFromName(*character.mASFData, "thorax"); if (bone == NULL) return false; PxTransform boneTransform = computeBoneTransformRest(*bone); PxTransform boneTransformInv = boneTransform.getInverse(); mBoneID = bone->mID - 1; for (PxU32 i = 0; i < positions.size(); i++) { mBindPos[i] = boneTransformInv.transform( reinterpret_cast(positions[i])); } return true; } /////////////////////////////////////////////////////////////////////////////// bool Skin::computeNewPositions(Character &character, SampleArray &particlePositions) { if (character.mCurrentMotion == NULL) return false; PxTransform t = character.mCurrentBoneTransform[mBoneID]; particlePositions.resize(mBindPos.size()); for (PxU32 i = 0; i < mBindPos.size(); i++) particlePositions[i] = t.transform(mBindPos[i]); return true; }