1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
|
#include "bulletnifloader.hpp"
#include <cassert>
#include <sstream>
#include <tuple>
#include <variant>
#include <vector>
#include <components/debug/debuglog.hpp>
#include <components/files/conversion.hpp>
#include <components/misc/convert.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <components/nif/extra.hpp>
#include <components/nif/nifstream.hpp>
#include <components/nif/node.hpp>
#include <components/nif/parent.hpp>
namespace
{
bool pathFileNameStartsWithX(const std::string& path)
{
const std::size_t slashpos = path.find_last_of("/\\");
const std::size_t letterPos = slashpos == std::string::npos ? 0 : slashpos + 1;
return letterPos < path.size() && (path[letterPos] == 'x' || path[letterPos] == 'X');
}
}
namespace NifBullet
{
osg::ref_ptr<Resource::BulletShape> BulletNifLoader::load(Nif::FileView nif)
{
mShape = new Resource::BulletShape;
mCompoundShape.reset();
mAvoidCompoundShape.reset();
mShape->mFileHash = nif.getHash();
const size_t numRoots = nif.numRoots();
std::vector<const Nif::NiAVObject*> roots;
for (size_t i = 0; i < numRoots; ++i)
{
const Nif::Record* r = nif.getRoot(i);
if (!r)
continue;
const Nif::NiAVObject* node = dynamic_cast<const Nif::NiAVObject*>(r);
if (node)
roots.emplace_back(node);
}
mShape->mFileName = nif.getFilename();
if (roots.empty())
{
warn("Found no root nodes in NIF file " + mShape->mFileName.value());
return mShape;
}
for (const Nif::NiAVObject* node : roots)
if (findBoundingBox(*node))
break;
HandleNodeArgs args;
// files with the name convention xmodel.nif usually have keyframes stored in a separate file xmodel.kf (see
// Animation::addAnimSource). assume all nodes in the file will be animated
// TODO: investigate whether this should and could be optimized.
args.mAnimated = pathFileNameStartsWithX(mShape->mFileName);
for (const Nif::NiAVObject* node : roots)
handleRoot(nif, *node, args);
if (mCompoundShape)
mShape->mCollisionShape = std::move(mCompoundShape);
if (mAvoidCompoundShape)
mShape->mAvoidCollisionShape = std::move(mAvoidCompoundShape);
return mShape;
}
// Find a bounding box in the node hierarchy to use for actor collision
bool BulletNifLoader::findBoundingBox(const Nif::NiAVObject& node)
{
if (Misc::StringUtils::ciEqual(node.mName, "Bounding Box"))
{
if (node.mBounds.mType == Nif::BoundingVolume::Type::BOX_BV
&& std::ranges::all_of(node.mBounds.mBox.mExtents._v, [](float extent) { return extent > 0.f; }))
{
mShape->mCollisionBox.mExtents = node.mBounds.mBox.mExtents;
mShape->mCollisionBox.mCenter = node.mBounds.mBox.mCenter;
}
else
{
warn("Invalid Bounding Box node bounds in file " + mShape->mFileName.value());
}
return true;
}
if (auto ninode = dynamic_cast<const Nif::NiNode*>(&node))
for (const auto& child : ninode->mChildren)
if (!child.empty() && findBoundingBox(child.get()))
return true;
return false;
}
void BulletNifLoader::handleRoot(Nif::FileView nif, const Nif::NiAVObject& node, HandleNodeArgs args)
{
// Gamebryo/Bethbryo meshes
if (nif.getVersion() >= Nif::NIFStream::generateVersion(10, 0, 1, 0))
{
// Handle BSXFlags
const Nif::NiIntegerExtraData* bsxFlags = nullptr;
for (const auto& e : node.getExtraList())
{
if (e->recType == Nif::RC_BSXFlags)
{
bsxFlags = static_cast<const Nif::NiIntegerExtraData*>(e.getPtr());
break;
}
}
// Collision flag
if (!bsxFlags || !(bsxFlags->mData & 2))
return;
// Editor marker flag
if (bsxFlags->mData & 32)
args.mHasMarkers = true;
// FIXME: hack, using rendered geometry instead of Bethesda Havok data
args.mAutogenerated = true;
}
// Pre-Gamebryo meshes
else
{
// Handle RootCollisionNode
const Nif::NiNode* colNode = nullptr;
if (const Nif::NiNode* ninode = dynamic_cast<const Nif::NiNode*>(&node))
{
for (const auto& child : ninode->mChildren)
{
if (!child.empty() && child.getPtr()->recType == Nif::RC_RootCollisionNode)
{
colNode = static_cast<const Nif::NiNode*>(child.getPtr());
break;
}
}
}
args.mAutogenerated = colNode == nullptr;
// Check for extra data
for (const auto& e : node.getExtraList())
{
if (e->recType == Nif::RC_NiStringExtraData)
{
// String markers may contain important information
// affecting the entire subtree of this node
auto sd = static_cast<const Nif::NiStringExtraData*>(e.getPtr());
// Editor marker flag
if (sd->mData == "MRK")
args.mHasTriMarkers = true;
else if (Misc::StringUtils::ciStartsWith(sd->mData, "NC"))
{
// NC prefix is case-insensitive but the second C in NCC flag needs be uppercase.
// Collide only with camera.
if (sd->mData.length() > 2 && sd->mData[2] == 'C')
mShape->mVisualCollisionType = Resource::VisualCollisionType::Camera;
// No collision.
else
mShape->mVisualCollisionType = Resource::VisualCollisionType::Default;
}
}
}
// FIXME: BulletNifLoader should never have to provide rendered geometry for camera collision
if (colNode && colNode->mChildren.empty())
{
args.mAutogenerated = true;
mShape->mVisualCollisionType = Resource::VisualCollisionType::Camera;
}
}
handleNode(node, nullptr, args);
}
void BulletNifLoader::handleNode(const Nif::NiAVObject& node, const Nif::Parent* parent, HandleNodeArgs args)
{
// TODO: allow on-the fly collision switching via toggling this flag
if (node.recType == Nif::RC_NiCollisionSwitch && !node.collisionActive())
return;
for (Nif::NiTimeControllerPtr ctrl = node.mController; !ctrl.empty(); ctrl = ctrl->mNext)
{
if (args.mAnimated)
break;
if (!ctrl->isActive())
continue;
switch (ctrl->recType)
{
case Nif::RC_NiKeyframeController:
case Nif::RC_NiPathController:
case Nif::RC_NiRollController:
args.mAnimated = true;
break;
default:
continue;
}
}
if (node.recType == Nif::RC_RootCollisionNode)
{
if (args.mAutogenerated)
{
// Encountered a RootCollisionNode inside an autogenerated mesh.
// We treat empty RootCollisionNodes as NCC flag (set collisionType to `Camera`)
// and generate the camera collision shape based on rendered geometry.
if (mShape->mVisualCollisionType == Resource::VisualCollisionType::Camera)
return;
// Otherwise we'll want to notify the user.
Log(Debug::Info) << "BulletNifLoader: RootCollisionNode is not attached to the root node in "
<< mShape->mFileName << ". Treating it as a NiNode.";
}
else
{
args.mIsCollisionNode = true;
}
}
// Don't collide with AvoidNode shapes
if (node.recType == Nif::RC_AvoidNode)
args.mAvoid = true;
if (args.mAutogenerated || args.mIsCollisionNode)
{
auto geometry = dynamic_cast<const Nif::NiGeometry*>(&node);
if (geometry)
handleGeometry(*geometry, parent, args);
}
// For NiNodes, loop through children
if (const Nif::NiNode* ninode = dynamic_cast<const Nif::NiNode*>(&node))
{
const Nif::Parent currentParent{ *ninode, parent };
for (const auto& child : ninode->mChildren)
{
if (!child.empty())
{
assert(std::find(child->mParents.begin(), child->mParents.end(), ninode) != child->mParents.end());
handleNode(child.get(), ¤tParent, args);
}
// For NiSwitchNodes and NiFltAnimationNodes, only use the first child
// TODO: must synchronize with the rendering scene graph somehow
// Doing this for NiLODNodes is unsafe (the first level might not be the closest)
if (node.recType == Nif::RC_NiSwitchNode || node.recType == Nif::RC_NiFltAnimationNode)
break;
}
}
}
void BulletNifLoader::handleGeometry(
const Nif::NiGeometry& niGeometry, const Nif::Parent* nodeParent, HandleNodeArgs args)
{
// This flag comes from BSXFlags
if (args.mHasMarkers && Misc::StringUtils::ciStartsWith(niGeometry.mName, "EditorMarker"))
return;
// This flag comes from Morrowind
if (args.mHasTriMarkers && Misc::StringUtils::ciStartsWith(niGeometry.mName, "Tri EditorMarker"))
return;
if (!niGeometry.mSkin.empty())
args.mAnimated = false;
std::unique_ptr<btCollisionShape> childShape = niGeometry.getCollisionShape();
if (childShape == nullptr)
return;
osg::Matrixf transform = niGeometry.mTransform.toMatrix();
for (const Nif::Parent* parent = nodeParent; parent != nullptr; parent = parent->mParent)
transform *= parent->mNiNode.mTransform.toMatrix();
if (childShape->getShapeType() == TRIANGLE_MESH_SHAPE_PROXYTYPE)
{
auto scaledShape = std::make_unique<Resource::ScaledTriangleMeshShape>(
static_cast<btBvhTriangleMeshShape*>(childShape.get()), Misc::Convert::toBullet(transform.getScale()));
std::ignore = childShape.release();
childShape = std::move(scaledShape);
}
else
{
childShape->setLocalScaling(Misc::Convert::toBullet(transform.getScale()));
}
transform.orthoNormalize(transform);
btTransform trans;
trans.setOrigin(Misc::Convert::toBullet(transform.getTrans()));
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
trans.getBasis()[i][j] = transform(j, i);
if (!args.mAvoid)
{
if (!mCompoundShape)
mCompoundShape.reset(new btCompoundShape);
if (args.mAnimated)
mShape->mAnimatedShapes.emplace(niGeometry.recIndex, mCompoundShape->getNumChildShapes());
mCompoundShape->addChildShape(trans, childShape.get());
}
else
{
if (!mAvoidCompoundShape)
mAvoidCompoundShape.reset(new btCompoundShape);
mAvoidCompoundShape->addChildShape(trans, childShape.get());
}
std::ignore = childShape.release();
}
} // namespace NifBullet
|