1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00
luanti/irr/src/CSceneManager.cpp
Lars Müller 7ac5502fdf
Fix handling of skinned meshes for nodes
Second try after the revert in 8a28339 due to an unexpected regression.

- Rigidly animated models (e.g. the glTF frog node) were not working correctly,
  since cloning the mesh ignored the transformation matrices.
  Note that scaling the mesh needs to occur *after* transforming the vertices.
- Visual scale did not apply to skinned models,
  as resetting the animation overwrote scaled vertex data with static positions & normals.
  For backwards compatibility, we now apply a 10x scale to static, non-glTF models.

We now do scale static meshes, as the bug that caused meshes not to be scaled was limited to skeletally animated meshes,
hence we ought not to reproduce it for skinned meshes that do not take advantage of skeletal animations (e.g. current MTG doors).

However, glTF models (e.g. Wuzzy's eyeballs) up until recently were always affected due to technical reasons
(using skeletal animation for rigid animation).

Thus, to preserve behavior, we:

1. Do not apply 10x scale to glTF models.
2. Apply 10x scale to obj models.
3. Apply 10x scale to static x or b3d models, but not to animated ones.

See also: #16141
2025-05-20 18:37:33 +02:00

775 lines
20 KiB
C++

// Copyright (C) 2002-2012 Nikolaus Gebhardt
// This file is part of the "Irrlicht Engine".
// For conditions of distribution and use, see copyright notice in irrlicht.h
#include <algorithm>
#include <cassert>
#include "CSceneManager.h"
#include "IVideoDriver.h"
#include "IFileSystem.h"
#include "SAnimatedMesh.h"
#include "CMeshCache.h"
#include "IGUIEnvironment.h"
#include "IMaterialRenderer.h"
#include "IReadFile.h"
#include "IWriteFile.h"
#include "os.h"
#include "SkinnedMesh.h"
#include "CXMeshFileLoader.h"
#include "COBJMeshFileLoader.h"
#include "CB3DMeshFileLoader.h"
#include "CGLTFMeshFileLoader.h"
#include "CBillboardSceneNode.h"
#include "CAnimatedMeshSceneNode.h"
#include "CCameraSceneNode.h"
#include "CMeshSceneNode.h"
#include "CDummyTransformationSceneNode.h"
#include "CEmptySceneNode.h"
#include "CSceneCollisionManager.h"
namespace irr
{
namespace scene
{
//! constructor
CSceneManager::CSceneManager(video::IVideoDriver *driver,
gui::ICursorControl *cursorControl, IMeshCache *cache) :
ISceneNode(0, 0),
Driver(driver),
CursorControl(cursorControl),
ActiveCamera(0),
MeshCache(cache), CurrentRenderPass(ESNRP_NONE)
{
// root node's scene manager
SceneManager = this;
if (Driver)
Driver->grab();
if (CursorControl)
CursorControl->grab();
// create mesh cache if not there already
if (!MeshCache)
MeshCache = new CMeshCache();
else
MeshCache->grab();
// create collision manager
CollisionManager = new CSceneCollisionManager(this, Driver);
// add file format loaders. add the least commonly used ones first,
// as these are checked last
// TODO: now that we have multiple scene managers, these should be
// shallow copies from the previous manager if there is one.
MeshLoaderList.push_back(new CXMeshFileLoader(this));
MeshLoaderList.push_back(new COBJMeshFileLoader(this));
MeshLoaderList.push_back(new CB3DMeshFileLoader(this));
MeshLoaderList.push_back(new CGLTFMeshFileLoader());
}
//! destructor
CSceneManager::~CSceneManager()
{
clearDeletionList();
//! force to remove hardwareTextures from the driver
//! because Scenes may hold internally data bounded to sceneNodes
//! which may be destroyed twice
if (Driver)
Driver->removeAllHardwareBuffers();
if (CursorControl)
CursorControl->drop();
if (CollisionManager)
CollisionManager->drop();
for (auto *loader : MeshLoaderList)
loader->drop();
if (ActiveCamera)
ActiveCamera->drop();
ActiveCamera = 0;
if (MeshCache)
MeshCache->drop();
// remove all nodes before dropping the driver
// as render targets may be destroyed twice
removeAll();
if (Driver)
Driver->drop();
}
//! gets an animatable mesh. loads it if needed. returned pointer must not be dropped.
IAnimatedMesh *CSceneManager::getMesh(io::IReadFile *file)
{
if (!file)
return 0;
io::path name = file->getFileName();
IAnimatedMesh *msh = MeshCache->getMeshByName(name);
if (msh)
return msh;
msh = getUncachedMesh(file, name, name);
return msh;
}
// load and create a mesh which we know already isn't in the cache and put it in there
IAnimatedMesh *CSceneManager::getUncachedMesh(io::IReadFile *file, const io::path &filename, const io::path &cachename)
{
// iterate the list in reverse order so user-added loaders can override the built-in ones
bool unsupported = true;
for (auto it = MeshLoaderList.rbegin(); it != MeshLoaderList.rend(); it++) {
if ((*it)->isALoadableFileExtension(filename)) {
unsupported = false;
// reset file to avoid side effects of previous calls to createMesh
file->seek(0);
IAnimatedMesh *msh = (*it)->createMesh(file);
if (msh) {
MeshCache->addMesh(cachename, msh);
msh->drop();
os::Printer::log("Loaded mesh", filename, ELL_DEBUG);
return msh;
}
}
}
os::Printer::log(unsupported
? "Could not load mesh, file format seems to be unsupported"
: "Attempt to load mesh failed",
filename, ELL_ERROR);
return nullptr;
}
//! returns the video driver
video::IVideoDriver *CSceneManager::getVideoDriver()
{
return Driver;
}
//! adds a scene node for rendering a static mesh
//! the returned pointer must not be dropped.
IMeshSceneNode *CSceneManager::addMeshSceneNode(IMesh *mesh, ISceneNode *parent, s32 id,
const core::vector3df &position, const core::vector3df &rotation,
const core::vector3df &scale, bool alsoAddIfMeshPointerZero)
{
if (!alsoAddIfMeshPointerZero && !mesh)
return 0;
if (!parent)
parent = this;
IMeshSceneNode *node = new CMeshSceneNode(mesh, parent, this, id, position, rotation, scale);
node->drop();
return node;
}
//! adds a scene node for rendering an animated mesh model
IAnimatedMeshSceneNode *CSceneManager::addAnimatedMeshSceneNode(IAnimatedMesh *mesh, ISceneNode *parent, s32 id,
const core::vector3df &position, const core::vector3df &rotation,
const core::vector3df &scale, bool alsoAddIfMeshPointerZero)
{
if (!alsoAddIfMeshPointerZero && !mesh)
return 0;
if (!parent)
parent = this;
IAnimatedMeshSceneNode *node =
new CAnimatedMeshSceneNode(mesh, parent, this, id, position, rotation, scale);
node->drop();
return node;
}
//! Adds a camera scene node to the tree and sets it as active camera.
//! \param position: Position of the space relative to its parent where the camera will be placed.
//! \param lookat: Position where the camera will look at. Also known as target.
//! \param parent: Parent scene node of the camera. Can be null. If the parent moves,
//! the camera will move too.
//! \return Returns pointer to interface to camera
ICameraSceneNode *CSceneManager::addCameraSceneNode(ISceneNode *parent,
const core::vector3df &position, const core::vector3df &lookat, s32 id,
bool makeActive)
{
if (!parent)
parent = this;
ICameraSceneNode *node = new CCameraSceneNode(parent, this, id, position, lookat);
if (makeActive)
setActiveCamera(node);
node->drop();
return node;
}
//! Adds a billboard scene node to the scene. A billboard is like a 3d sprite: A 2d element,
//! which always looks to the camera. It is usually used for things like explosions, fire,
//! lensflares and things like that.
IBillboardSceneNode *CSceneManager::addBillboardSceneNode(ISceneNode *parent,
const core::dimension2d<f32> &size, const core::vector3df &position, s32 id,
video::SColor colorTop, video::SColor colorBottom)
{
if (!parent)
parent = this;
IBillboardSceneNode *node = new CBillboardSceneNode(parent, this, id, position, size,
colorTop, colorBottom);
node->drop();
return node;
}
//! Adds an empty scene node.
ISceneNode *CSceneManager::addEmptySceneNode(ISceneNode *parent, s32 id)
{
if (!parent)
parent = this;
ISceneNode *node = new CEmptySceneNode(parent, this, id);
node->drop();
return node;
}
//! Adds a dummy transformation scene node to the scene graph.
IDummyTransformationSceneNode *CSceneManager::addDummyTransformationSceneNode(
ISceneNode *parent, s32 id)
{
if (!parent)
parent = this;
IDummyTransformationSceneNode *node = new CDummyTransformationSceneNode(
parent, this, id);
node->drop();
return node;
}
//! Returns the root scene node. This is the scene node which is parent
//! of all scene nodes. The root scene node is a special scene node which
//! only exists to manage all scene nodes. It is not rendered and cannot
//! be removed from the scene.
//! \return Returns a pointer to the root scene node.
ISceneNode *CSceneManager::getRootSceneNode()
{
return this;
}
//! Returns the current active camera.
//! \return The active camera is returned. Note that this can be NULL, if there
//! was no camera created yet.
ICameraSceneNode *CSceneManager::getActiveCamera() const
{
return ActiveCamera;
}
//! Sets the active camera. The previous active camera will be deactivated.
//! \param camera: The new camera which should be active.
void CSceneManager::setActiveCamera(ICameraSceneNode *camera)
{
if (camera)
camera->grab();
if (ActiveCamera)
ActiveCamera->drop();
ActiveCamera = camera;
}
//! renders the node.
void CSceneManager::render()
{
}
//! returns the axis aligned bounding box of this node
const core::aabbox3d<f32> &CSceneManager::getBoundingBox() const
{
assert(false); // Bounding Box of Scene Manager should never be used.
static const core::aabbox3d<f32> dummy{{0.0f, 0.0f, 0.0f}};
return dummy;
}
//! returns if node is culled
bool CSceneManager::isCulled(const ISceneNode *node) const
{
const ICameraSceneNode *cam = getActiveCamera();
if (!cam) {
return false;
}
bool result = false;
// has occlusion query information
if (node->getAutomaticCulling() & scene::EAC_OCC_QUERY) {
result = (Driver->getOcclusionQueryResult(const_cast<ISceneNode *>(node)) == 0);
}
// can be seen by a bounding box ?
if (!result && (node->getAutomaticCulling() & scene::EAC_BOX)) {
core::aabbox3d<f32> tbox = node->getBoundingBox();
node->getAbsoluteTransformation().transformBoxEx(tbox);
result = !(tbox.intersectsWithBox(cam->getViewFrustum()->getBoundingBox()));
}
// can be seen by a bounding sphere
if (!result && (node->getAutomaticCulling() & scene::EAC_FRUSTUM_SPHERE)) {
const core::aabbox3df nbox = node->getTransformedBoundingBox();
const float rad = nbox.getRadius();
const core::vector3df center = nbox.getCenter();
const float camrad = cam->getViewFrustum()->getBoundingRadius();
const core::vector3df camcenter = cam->getViewFrustum()->getBoundingCenter();
const float dist = (center - camcenter).getLengthSQ();
const float maxdist = (rad + camrad) * (rad + camrad);
result = dist > maxdist;
}
// can be seen by cam pyramid planes ?
if (!result && (node->getAutomaticCulling() & scene::EAC_FRUSTUM_BOX)) {
SViewFrustum frust = *cam->getViewFrustum();
// transform the frustum to the node's current absolute transformation
core::matrix4 invTrans(node->getAbsoluteTransformation(), core::matrix4::EM4CONST_INVERSE);
// invTrans.makeInverse();
frust.transform(invTrans);
core::vector3df edges[8];
node->getBoundingBox().getEdges(edges);
for (s32 i = 0; i < scene::SViewFrustum::VF_PLANE_COUNT; ++i) {
bool boxInFrustum = false;
for (u32 j = 0; j < 8; ++j) {
if (frust.planes[i].classifyPointRelation(edges[j]) != core::ISREL3D_FRONT) {
boxInFrustum = true;
break;
}
}
if (!boxInFrustum) {
result = true;
break;
}
}
}
return result;
}
//! registers a node for rendering it at a specific time.
u32 CSceneManager::registerNodeForRendering(ISceneNode *node, E_SCENE_NODE_RENDER_PASS pass)
{
u32 taken = 0;
switch (pass) {
// take camera if it is not already registered
case ESNRP_CAMERA: {
if (std::find(CameraList.begin(), CameraList.end(), node) == CameraList.end()) {
taken = 1;
CameraList.push_back(node);
}
} break;
case ESNRP_SKY_BOX:
SkyBoxList.push_back(node);
taken = 1;
break;
case ESNRP_SOLID:
if (!isCulled(node)) {
SolidNodeList.emplace_back(node);
taken = 1;
}
break;
case ESNRP_TRANSPARENT:
if (!isCulled(node)) {
TransparentNodeList.emplace_back(node, camWorldPos);
taken = 1;
}
break;
case ESNRP_TRANSPARENT_EFFECT:
if (!isCulled(node)) {
TransparentEffectNodeList.emplace_back(node, camWorldPos);
taken = 1;
}
break;
case ESNRP_AUTOMATIC:
if (!isCulled(node)) {
const u32 count = node->getMaterialCount();
taken = 0;
for (u32 i = 0; i < count; ++i) {
if (Driver->needsTransparentRenderPass(node->getMaterial(i))) {
// register as transparent node
TransparentNodeList.emplace_back(node, camWorldPos);
taken = 1;
break;
}
}
// not transparent, register as solid
if (!taken) {
SolidNodeList.emplace_back(node);
taken = 1;
}
}
break;
case ESNRP_GUI:
if (!isCulled(node)) {
GuiNodeList.push_back(node);
taken = 1;
}
case ESNRP_NONE: // ignore this one
break;
}
return taken;
}
void CSceneManager::clearAllRegisteredNodesForRendering()
{
CameraList.clear();
SkyBoxList.clear();
SolidNodeList.clear();
TransparentNodeList.clear();
TransparentEffectNodeList.clear();
GuiNodeList.clear();
}
//! This method is called just before the rendering process of the whole scene.
//! draws all scene nodes
void CSceneManager::drawAll()
{
if (!Driver)
return;
// reset all transforms
Driver->setMaterial(video::SMaterial());
Driver->setTransform(video::ETS_PROJECTION, core::IdentityMatrix);
Driver->setTransform(video::ETS_VIEW, core::IdentityMatrix);
Driver->setTransform(video::ETS_WORLD, core::IdentityMatrix);
for (u32 i = video::ETS_COUNT - 1; i >= video::ETS_TEXTURE_0; --i)
Driver->setTransform((video::E_TRANSFORMATION_STATE)i, core::IdentityMatrix);
Driver->setAllowZWriteOnTransparent(true);
// do animations and other stuff.
OnAnimate(os::Timer::getTime());
/*!
First Scene Node for prerendering should be the active camera
consistent Camera is needed for culling
*/
camWorldPos.set(0, 0, 0);
if (ActiveCamera) {
ActiveCamera->render();
camWorldPos = ActiveCamera->getAbsolutePosition();
}
// let all nodes register themselves
OnRegisterSceneNode();
const auto &render_node = [this] (ISceneNode *node) {
u32 flags = node->isDebugDataVisible();
node->setDebugDataVisible((flags & DebugDataMask) | DebugDataBits);
node->render();
};
// render camera scenes
{
CurrentRenderPass = ESNRP_CAMERA;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
for (auto *node : CameraList)
render_node(node);
CameraList.clear();
}
// render skyboxes
{
CurrentRenderPass = ESNRP_SKY_BOX;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
for (auto *node : SkyBoxList)
render_node(node);
SkyBoxList.clear();
}
// render default objects
{
CurrentRenderPass = ESNRP_SOLID;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
std::sort(SolidNodeList.begin(), SolidNodeList.end());
for (auto &it : SolidNodeList)
render_node(it.Node);
SolidNodeList.clear();
}
// render transparent objects.
{
CurrentRenderPass = ESNRP_TRANSPARENT;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
std::sort(TransparentNodeList.begin(), TransparentNodeList.end());
for (auto &it : TransparentNodeList)
render_node(it.Node);
TransparentNodeList.clear();
}
// render transparent effect objects.
{
CurrentRenderPass = ESNRP_TRANSPARENT_EFFECT;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
std::sort(TransparentEffectNodeList.begin(), TransparentEffectNodeList.end());
for (auto &it : TransparentEffectNodeList)
render_node(it.Node);
TransparentEffectNodeList.clear();
}
// render custom gui nodes
{
CurrentRenderPass = ESNRP_GUI;
Driver->getOverrideMaterial().Enabled = ((Driver->getOverrideMaterial().EnablePasses & CurrentRenderPass) != 0);
for (auto *node : GuiNodeList)
render_node(node);
GuiNodeList.clear();
}
clearDeletionList();
CurrentRenderPass = ESNRP_NONE;
}
//! Adds an external mesh loader.
void CSceneManager::addExternalMeshLoader(IMeshLoader *externalLoader)
{
if (!externalLoader)
return;
externalLoader->grab();
MeshLoaderList.push_back(externalLoader);
}
//! Returns the number of mesh loaders supported by Irrlicht at this time
u32 CSceneManager::getMeshLoaderCount() const
{
return static_cast<u32>(MeshLoaderList.size());
}
//! Retrieve the given mesh loader
IMeshLoader *CSceneManager::getMeshLoader(u32 index) const
{
if (index < MeshLoaderList.size())
return MeshLoaderList[index];
else
return 0;
}
//! Returns a pointer to the scene collision manager.
ISceneCollisionManager *CSceneManager::getSceneCollisionManager()
{
return CollisionManager;
}
//! Returns a pointer to the mesh manipulator.
IMeshManipulator *CSceneManager::getMeshManipulator()
{
return Driver->getMeshManipulator();
}
//! Adds a scene node to the deletion queue.
void CSceneManager::addToDeletionQueue(ISceneNode *node)
{
if (!node)
return;
node->grab();
DeletionList.push_back(node);
}
//! clears the deletion list
void CSceneManager::clearDeletionList()
{
for (auto *node : DeletionList) {
node->remove();
node->drop();
}
DeletionList.clear();
}
//! Returns the first scene node with the specified name.
ISceneNode *CSceneManager::getSceneNodeFromName(const char *name, ISceneNode *start)
{
if (start == 0)
start = getRootSceneNode();
auto startName = start->getName();
if (startName.has_value() && startName == name)
return start;
ISceneNode *node = 0;
const ISceneNodeList &list = start->getChildren();
ISceneNodeList::const_iterator it = list.begin();
for (; it != list.end(); ++it) {
node = getSceneNodeFromName(name, *it);
if (node)
return node;
}
return 0;
}
//! Returns the first scene node with the specified id.
ISceneNode *CSceneManager::getSceneNodeFromId(s32 id, ISceneNode *start)
{
if (start == 0)
start = getRootSceneNode();
if (start->getID() == id)
return start;
ISceneNode *node = 0;
const ISceneNodeList &list = start->getChildren();
ISceneNodeList::const_iterator it = list.begin();
for (; it != list.end(); ++it) {
node = getSceneNodeFromId(id, *it);
if (node)
return node;
}
return 0;
}
//! Returns the first scene node with the specified type.
ISceneNode *CSceneManager::getSceneNodeFromType(scene::ESCENE_NODE_TYPE type, ISceneNode *start)
{
if (start == 0)
start = getRootSceneNode();
if (start->getType() == type || ESNT_ANY == type)
return start;
ISceneNode *node = 0;
const ISceneNodeList &list = start->getChildren();
ISceneNodeList::const_iterator it = list.begin();
for (; it != list.end(); ++it) {
node = getSceneNodeFromType(type, *it);
if (node)
return node;
}
return 0;
}
//! returns scene nodes by type.
void CSceneManager::getSceneNodesFromType(ESCENE_NODE_TYPE type, core::array<scene::ISceneNode *> &outNodes, ISceneNode *start)
{
if (start == 0)
start = getRootSceneNode();
if (start->getType() == type || ESNT_ANY == type)
outNodes.push_back(start);
const ISceneNodeList &list = start->getChildren();
ISceneNodeList::const_iterator it = list.begin();
for (; it != list.end(); ++it) {
getSceneNodesFromType(type, outNodes, *it);
}
}
//! Posts an input event to the environment. Usually you do not have to
//! use this method, it is used by the internal engine.
bool CSceneManager::postEventFromUser(const SEvent &event)
{
bool ret = false;
ICameraSceneNode *cam = getActiveCamera();
if (cam)
ret = cam->OnEvent(event);
return ret;
}
//! Removes all children of this scene node
void CSceneManager::removeAll()
{
ISceneNode::removeAll();
setActiveCamera(0);
// Make sure the driver is reset, might need a more complex method at some point
if (Driver)
Driver->setMaterial(video::SMaterial());
}
//! Clears the whole scene. All scene nodes are removed.
void CSceneManager::clear()
{
removeAll();
}
//! Returns current render pass.
E_SCENE_NODE_RENDER_PASS CSceneManager::getSceneNodeRenderPass() const
{
return CurrentRenderPass;
}
//! Returns an interface to the mesh cache which is shared between all existing scene managers.
IMeshCache *CSceneManager::getMeshCache()
{
return MeshCache;
}
//! Creates a new scene manager.
ISceneManager *CSceneManager::createNewSceneManager(bool cloneContent)
{
CSceneManager *manager = new CSceneManager(Driver, CursorControl, MeshCache);
if (cloneContent)
manager->cloneMembers(this, manager);
return manager;
}
//! Get a skinned mesh, which is not available as header-only code
SkinnedMesh *CSceneManager::createSkinnedMesh()
{
return new SkinnedMesh(SkinnedMesh::SourceFormat::OTHER);
}
// creates a scenemanager
ISceneManager *createSceneManager(video::IVideoDriver *driver, gui::ICursorControl *cursorcontrol)
{
return new CSceneManager(driver, cursorcontrol, nullptr);
}
} // end namespace scene
} // end namespace irr