Merge branch 'master' into gltf_binary

pull/5154/head
Kim Kulling 2023-07-03 21:31:28 +02:00 committed by GitHub
commit 90ff723b6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1920 additions and 1813 deletions

View File

@ -115,15 +115,12 @@ BlenderImporter::~BlenderImporter() {
delete modifier_cache;
}
static const char * const Tokens[] = { "BLENDER" };
static const char Token[] = "BLENDER";
// ------------------------------------------------------------------------------------------------
// Returns whether the class can handle the format of the given file.
bool BlenderImporter::CanRead(const std::string &pFile, IOSystem *pIOHandler, bool /*checkSig*/) const {
// note: this won't catch compressed files
static const char *tokens[] = { "<BLENDER", "blender" };
return SearchFileHeaderForToken(pIOHandler, pFile, tokens, AI_COUNT_OF(tokens));
return ParseMagicToken(pFile, pIOHandler).error.empty();
}
// ------------------------------------------------------------------------------------------------
@ -142,63 +139,21 @@ void BlenderImporter::SetupProperties(const Importer * /*pImp*/) {
// Imports the given file into the given scene structure.
void BlenderImporter::InternReadFile(const std::string &pFile,
aiScene *pScene, IOSystem *pIOHandler) {
#ifndef ASSIMP_BUILD_NO_COMPRESSED_BLEND
std::vector<char> uncompressed;
#endif
FileDatabase file;
std::shared_ptr<IOStream> stream(pIOHandler->Open(pFile, "rb"));
if (!stream) {
ThrowException("Could not open file for reading");
StreamOrError streamOrError = ParseMagicToken(pFile, pIOHandler);
if (!streamOrError.error.empty()) {
ThrowException(streamOrError.error);
}
std::shared_ptr<IOStream> stream = std::move(streamOrError.stream);
char magic[8] = { 0 };
stream->Read(magic, 7, 1);
if (strcmp(magic, Tokens[0])) {
// Check for presence of the gzip header. If yes, assume it is a
// compressed blend file and try uncompressing it, else fail. This is to
// avoid uncompressing random files which our loader might end up with.
#ifdef ASSIMP_BUILD_NO_COMPRESSED_BLEND
ThrowException("BLENDER magic bytes are missing, is this file compressed (Assimp was built without decompression support)?");
#else
if (magic[0] != 0x1f || static_cast<uint8_t>(magic[1]) != 0x8b) {
ThrowException("BLENDER magic bytes are missing, couldn't find GZIP header either");
}
char version[4] = { 0 };
file.i64bit = (stream->Read(version, 1, 1), version[0] == '-');
file.little = (stream->Read(version, 1, 1), version[0] == 'v');
LogDebug("Found no BLENDER magic word but a GZIP header, might be a compressed file");
if (magic[2] != 8) {
ThrowException("Unsupported GZIP compression method");
}
stream->Read(version, 3, 1);
version[3] = '\0';
// http://www.gzip.org/zlib/rfc-gzip.html#header-trailer
stream->Seek(0L, aiOrigin_SET);
std::shared_ptr<StreamReaderLE> reader = std::shared_ptr<StreamReaderLE>(new StreamReaderLE(stream));
size_t total = 0;
Compression compression;
if (compression.open(Compression::Format::Binary, Compression::FlushMode::NoFlush, 16 + Compression::MaxWBits)) {
total = compression.decompress((unsigned char *)reader->GetPtr(), reader->GetRemainingSize(), uncompressed);
compression.close();
}
// replace the input stream with a memory stream
stream = std::make_shared<MemoryIOStream>(reinterpret_cast<uint8_t *>(uncompressed.data()), total);
// .. and retry
stream->Read(magic, 7, 1);
if (strcmp(magic, "BLENDER")) {
ThrowException("Found no BLENDER magic word in decompressed GZIP file");
}
#endif
}
file.i64bit = (stream->Read(magic, 1, 1), magic[0] == '-');
file.little = (stream->Read(magic, 1, 1), magic[0] == 'v');
stream->Read(magic, 3, 1);
magic[3] = '\0';
LogInfo("Blender version is ", magic[0], ".", magic + 1,
LogInfo("Blender version is ", version[0], ".", version + 1,
" (64bit: ", file.i64bit ? "true" : "false",
", little endian: ", file.little ? "true" : "false", ")");
@ -1338,4 +1293,55 @@ aiNode *BlenderImporter::ConvertNode(const Scene &in, const Object *obj, Convers
return node.release();
}
BlenderImporter::StreamOrError BlenderImporter::ParseMagicToken(const std::string &pFile, IOSystem *pIOHandler) const {
std::shared_ptr<IOStream> stream(pIOHandler->Open(pFile, "rb"));
if (stream == nullptr) {
return {{}, {}, "Could not open file for reading"};
}
char magic[8] = { 0 };
stream->Read(magic, 7, 1);
if (strcmp(magic, Token) == 0) {
return {stream, {}, {}};
}
// Check for presence of the gzip header. If yes, assume it is a
// compressed blend file and try uncompressing it, else fail. This is to
// avoid uncompressing random files which our loader might end up with.
#ifdef ASSIMP_BUILD_NO_COMPRESSED_BLEND
return {{}, {}, "BLENDER magic bytes are missing, is this file compressed (Assimp was built without decompression support)?"};
#else
if (magic[0] != 0x1f || static_cast<uint8_t>(magic[1]) != 0x8b) {
return {{}, {}, "BLENDER magic bytes are missing, couldn't find GZIP header either"};
}
LogDebug("Found no BLENDER magic word but a GZIP header, might be a compressed file");
if (magic[2] != 8) {
return {{}, {}, "Unsupported GZIP compression method"};
}
// http://www.gzip.org/zlib/rfc-gzip.html#header-trailer
stream->Seek(0L, aiOrigin_SET);
std::shared_ptr<StreamReaderLE> reader = std::shared_ptr<StreamReaderLE>(new StreamReaderLE(stream));
size_t total = 0;
Compression compression;
auto uncompressed = std::make_shared<std::vector<char>>();
if (compression.open(Compression::Format::Binary, Compression::FlushMode::NoFlush, 16 + Compression::MaxWBits)) {
total = compression.decompress((unsigned char *)reader->GetPtr(), reader->GetRemainingSize(), *uncompressed);
compression.close();
}
// replace the input stream with a memory stream
stream = std::make_shared<MemoryIOStream>(reinterpret_cast<uint8_t *>(uncompressed->data()), total);
// .. and retry
stream->Read(magic, 7, 1);
if (strcmp(magic, Token) == 0) {
return {stream, uncompressed, {}};
}
return {{}, {}, "Found no BLENDER magic word in decompressed GZIP file"};
#endif
}
#endif // ASSIMP_BUILD_NO_BLEND_IMPORTER

View File

@ -180,6 +180,19 @@ private:
const Blender::MTex *tex,
Blender::ConversionData &conv_data);
// TODO: Move to a std::variant, once c++17 is supported.
struct StreamOrError {
std::shared_ptr<IOStream> stream;
std::shared_ptr<std::vector<char>> input;
std::string error;
};
// Returns either a stream (and optional input data for the stream) or
// an error if it can't parse the magic token.
StreamOrError ParseMagicToken(
const std::string &pFile,
IOSystem *pIOHandler) const;
private: // static stuff, mostly logging and error reporting.
// --------------------
static void CheckActualType(const Blender::ElemBase *dt,

View File

@ -43,6 +43,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* @brief Implementation of the Irr importer class
*/
#include "assimp/Exceptional.h"
#include "assimp/StringComparison.h"
#ifndef ASSIMP_BUILD_NO_IRR_IMPORTER
#include "AssetLib/Irr/IRRLoader.h"
@ -62,8 +64,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <assimp/DefaultLogger.hpp>
#include <assimp/IOSystem.hpp>
#include <memory>
using namespace Assimp;
static const aiImporterDesc desc = {
@ -380,7 +380,6 @@ void IRRImporter::ComputeAnimations(Node *root, aiNode *real, std::vector<aiNode
if (360 == lcm)
break;
// find out how many time units we'll need for the finest
// track (in seconds) - this defines the number of output
// keys (fps * seconds)
@ -836,175 +835,164 @@ void IRRImporter::GenerateGraph(Node *root, aiNode *rootOut, aiScene *scene,
}
}
// ------------------------------------------------------------------------------------------------
// Imports the given file into the given scene structure.
void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) {
std::unique_ptr<IOStream> file(pIOHandler->Open(pFile));
void IRRImporter::ParseNodeAttributes(pugi::xml_node &attributesNode, IRRImporter::Node *nd, BatchLoader &batch) {
ai_assert(!ASSIMP_stricmp(attributesNode.name(), "attributes")); // Node must be <attributes>
ai_assert(nd != nullptr); // dude
// Check whether we can read from the file
if (file == nullptr) {
throw DeadlyImportError("Failed to open IRR file ", pFile);
// Big switch statement that tests for various tags inside <attributes>
// and applies them to nd
// I don't believe nodes have boolean attributes
for (pugi::xml_node &attribute : attributesNode.children()) {
if (attribute.type() != pugi::node_element) continue;
if (!ASSIMP_stricmp(attribute.name(), "vector3d")) { // <vector3d />
VectorProperty prop;
ReadVectorProperty(prop, attribute);
if (prop.name == "Position") {
nd->position = prop.value;
} else if (prop.name == "Rotation") {
nd->rotation = prop.value;
} else if (prop.name == "Scale") {
nd->scaling = prop.value;
} else if (Node::CAMERA == nd->type) {
aiCamera *cam = cameras.back();
if (prop.name == "Target") {
cam->mLookAt = prop.value;
} else if (prop.name == "UpVector") {
cam->mUp = prop.value;
}
// Construct the irrXML parser
XmlParser st;
if (!st.parse( file.get() )) {
throw DeadlyImportError("XML parse error while loading IRR file ", pFile);
}
pugi::xml_node rootElement = st.getRootNode();
// The root node of the scene
Node *root = new Node(Node::DUMMY);
root->parent = nullptr;
root->name = "<IRRSceneRoot>";
// Current node parent
Node *curParent = root;
// Scene-graph node we're currently working on
Node *curNode = nullptr;
// List of output cameras
std::vector<aiCamera *> cameras;
// List of output lights
std::vector<aiLight *> lights;
// Batch loader used to load external models
BatchLoader batch(pIOHandler);
//batch.SetBasePath(pFile);
cameras.reserve(5);
lights.reserve(5);
bool inMaterials = false, inAnimator = false;
unsigned int guessedAnimCnt = 0, guessedMeshCnt = 0, guessedMatCnt = 0;
// Parse the XML file
//while (reader->read()) {
for (pugi::xml_node child : rootElement.children())
switch (child.type()) {
case pugi::node_element:
if (!ASSIMP_stricmp(child.name(), "node")) {
// ***********************************************************************
/* What we're going to do with the node depends
* on its type:
*
* "mesh" - Load a mesh from an external file
* "cube" - Generate a cube
* "skybox" - Generate a skybox
* "light" - A light source
* "sphere" - Generate a sphere mesh
* "animatedMesh" - Load an animated mesh from an external file
* and join its animation channels with ours.
* "empty" - A dummy node
* "camera" - A camera
* "terrain" - a terrain node (data comes from a heightmap)
* "billboard", ""
*
* Each of these nodes can be animated and all can have multiple
* materials assigned (except lights, cameras and dummies, of course).
} else if (!ASSIMP_stricmp(attribute.name(), "float")) { // <float />
FloatProperty prop;
ReadFloatProperty(prop, attribute);
if (prop.name == "FramesPerSecond" && Node::ANIMMESH == nd->type) {
nd->framesPerSecond = prop.value;
} else if (Node::CAMERA == nd->type) {
/* This is the vertical, not the horizontal FOV.
* We need to compute the right FOV from the
* screen aspect which we don't know yet.
*/
// ***********************************************************************
//const char *sz = reader->getAttributeValueSafe("type");
pugi::xml_attribute attrib = child.attribute("type");
Node *nd;
if (!ASSIMP_stricmp(attrib.name(), "mesh") || !ASSIMP_stricmp(attrib.name(), "octTree")) {
// OctTree's and meshes are treated equally
nd = new Node(Node::MESH);
} else if (!ASSIMP_stricmp(attrib.name(), "cube")) {
nd = new Node(Node::CUBE);
++guessedMeshCnt;
} else if (!ASSIMP_stricmp(attrib.name(), "skybox")) {
nd = new Node(Node::SKYBOX);
guessedMeshCnt += 6;
} else if (!ASSIMP_stricmp(attrib.name(), "camera")) {
nd = new Node(Node::CAMERA);
if (prop.name == "Fovy") {
cameras.back()->mHorizontalFOV = prop.value;
} else if (prop.name == "Aspect") {
cameras.back()->mAspect = prop.value;
} else if (prop.name == "ZNear") {
cameras.back()->mClipPlaneNear = prop.value;
} else if (prop.name == "ZFar") {
cameras.back()->mClipPlaneFar = prop.value;
}
} else if (Node::LIGHT == nd->type) {
/* Additional light information
*/
if (prop.name == "Attenuation") {
lights.back()->mAttenuationLinear = prop.value;
} else if (prop.name == "OuterCone") {
lights.back()->mAngleOuterCone = AI_DEG_TO_RAD(prop.value);
} else if (prop.name == "InnerCone") {
lights.back()->mAngleInnerCone = AI_DEG_TO_RAD(prop.value);
}
}
// radius of the sphere to be generated -
// or alternatively, size of the cube
else if ((Node::SPHERE == nd->type && prop.name == "Radius") ||
(Node::CUBE == nd->type && prop.name == "Size")) {
nd->sphereRadius = prop.value;
}
} else if (!ASSIMP_stricmp(attribute.name(), "int")) { // <int />
// Only sphere nodes make use of integer attributes
if (Node::SPHERE == nd->type) {
IntProperty prop;
ReadIntProperty(prop, attribute);
if (prop.name == "PolyCountX") {
nd->spherePolyCountX = prop.value;
} else if (prop.name == "PolyCountY") {
nd->spherePolyCountY = prop.value;
}
}
} else if (!ASSIMP_stricmp(attribute.name(), "string") || !ASSIMP_stricmp(attribute.name(), "enum")) { // <string /> or < enum />
StringProperty prop;
ReadStringProperty(prop, attribute);
if (prop.value.length() == 0) continue; // skip empty strings
if (prop.name == "Name") {
nd->name = prop.value;
// Setup a temporary name for the camera
aiCamera *cam = new aiCamera();
cam->mName.Set(nd->name);
cameras.push_back(cam);
} else if (!ASSIMP_stricmp(attrib.name(), "light")) {
nd = new Node(Node::LIGHT);
/* If we're either a camera or a light source
* we need to update the name in the aiLight/
* aiCamera structure, too.
*/
if (Node::CAMERA == nd->type) {
cameras.back()->mName.Set(prop.value);
} else if (Node::LIGHT == nd->type) {
lights.back()->mName.Set(prop.value);
}
} else if (Node::LIGHT == nd->type && "LightType" == prop.name) {
if (prop.value == "Spot")
lights.back()->mType = aiLightSource_SPOT;
else if (prop.value == "Point")
lights.back()->mType = aiLightSource_POINT;
else if (prop.value == "Directional")
lights.back()->mType = aiLightSource_DIRECTIONAL;
else {
// We won't pass the validation with aiLightSourceType_UNDEFINED,
// so we remove the light and replace it with a silly dummy node
delete lights.back();
lights.pop_back();
nd->type = Node::DUMMY;
// Setup a temporary name for the light
aiLight *cam = new aiLight();
cam->mName.Set(nd->name);
lights.push_back(cam);
} else if (!ASSIMP_stricmp(attrib.name(), "sphere")) {
nd = new Node(Node::SPHERE);
++guessedMeshCnt;
} else if (!ASSIMP_stricmp(attrib.name(), "animatedMesh")) {
nd = new Node(Node::ANIMMESH);
} else if (!ASSIMP_stricmp(attrib.name(), "empty")) {
nd = new Node(Node::DUMMY);
} else if (!ASSIMP_stricmp(attrib.name(), "terrain")) {
nd = new Node(Node::TERRAIN);
} else if (!ASSIMP_stricmp(attrib.name(), "billBoard")) {
// We don't support billboards, so ignore them
ASSIMP_LOG_ERROR("IRR: Billboards are not supported by Assimp");
nd = new Node(Node::DUMMY);
ASSIMP_LOG_ERROR("Ignoring light of unknown type: ", prop.value);
}
} else if ((prop.name == "Mesh" && Node::MESH == nd->type) ||
Node::ANIMMESH == nd->type) {
/* This is the file name of the mesh - either
* animated or not. We need to make sure we setup
* the correct post-processing settings here.
*/
unsigned int pp = 0;
BatchLoader::PropertyMap map;
/* If the mesh is a static one remove all animations from the impor data
*/
if (Node::ANIMMESH != nd->type) {
pp |= aiProcess_RemoveComponent;
SetGenericProperty<int>(map.ints, AI_CONFIG_PP_RVC_FLAGS,
aiComponent_ANIMATIONS | aiComponent_BONEWEIGHTS);
}
/* TODO: maybe implement the protection against recursive
* loading calls directly in BatchLoader? The current
* implementation is not absolutely safe. A LWS and an IRR
* file referencing each other *could* cause the system to
* recurse forever.
*/
const std::string extension = GetExtension(prop.value);
if ("irr" == extension) {
ASSIMP_LOG_ERROR("IRR: Can't load another IRR file recursively");
} else {
ASSIMP_LOG_WARN("IRR: Found unknown node: ", attrib.name());
/* We skip the contents of nodes we don't know.
* We parse the transformation and all animators
* and skip the rest.
*/
nd = new Node(Node::DUMMY);
}
/* Attach the newly created node to the scene-graph
*/
curNode = nd;
nd->parent = curParent;
curParent->children.push_back(nd);
} else if (!ASSIMP_stricmp(child.name(), "materials")) {
inMaterials = true;
} else if (!ASSIMP_stricmp(child.name(), "animators")) {
inAnimator = true;
} else if (!ASSIMP_stricmp(child.name(), "attributes")) {
// We should have a valid node here
// FIX: no ... the scene root node is also contained in an attributes block
if (!curNode) {
continue;
nd->id = batch.AddLoadRequest(prop.value, pp, &map);
nd->meshPath = prop.value;
}
}
}
}
}
void IRRImporter::ParseAnimators(pugi::xml_node &animatorNode, IRRImporter::Node *nd) {
Animator *curAnim = nullptr;
// Materials can occur for nearly any type of node
if (inMaterials && curNode->type != Node::DUMMY) {
// This is a material description - parse it!
curNode->materials.emplace_back();
std::pair<aiMaterial *, unsigned int> &p = curNode->materials.back();
p.first = ParseMaterial(p.second);
++guessedMatCnt;
continue;
} else if (inAnimator) {
// This is an animation path - add a new animator
// to the list.
curNode->animators.emplace_back();
curAnim = &curNode->animators.back();
++guessedAnimCnt;
// Make empty animator
nd->animators.emplace_back();
curAnim = &nd->animators.back(); // Push it back
pugi::xml_node attributes = animatorNode.child("attributes");
if (!attributes) {
ASSIMP_LOG_WARN("Animator node does not contain attributes. ");
return;
}
/* Parse all elements in the attributes block
* and process them.
*/
// while (reader->read()) {
for (pugi::xml_node attrib : child.children()) {
if (attrib.type() == pugi::node_element) {
//if (reader->getNodeType() == EXN_ELEMENT) {
//if (!ASSIMP_stricmp(reader->getNodeName(), "vector3d")) {
for (pugi::xml_node attrib : attributes.children()) {
// XML may contain useless noes like CDATA
if (!ASSIMP_stricmp(attrib.name(), "vector3d")) {
VectorProperty prop;
ReadVectorProperty(prop);
ReadVectorProperty(prop, attrib);
if (inAnimator) {
if (curAnim->type == Animator::ROTATION && prop.name == "Rotation") {
// We store the rotation euler angles in 'direction'
curAnim->direction = prop.value;
@ -1041,36 +1029,20 @@ void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy
curAnim->direction = prop.value;
}
}
} else {
if (prop.name == "Position") {
curNode->position = prop.value;
} else if (prop.name == "Rotation") {
curNode->rotation = prop.value;
} else if (prop.name == "Scale") {
curNode->scaling = prop.value;
} else if (Node::CAMERA == curNode->type) {
aiCamera *cam = cameras.back();
if (prop.name == "Target") {
cam->mLookAt = prop.value;
} else if (prop.name == "UpVector") {
cam->mUp = prop.value;
}
}
}
//} else if (!ASSIMP_stricmp(reader->getNodeName(), "bool")) {
} else if (!ASSIMP_stricmp(attrib.name(), "bool")) {
BoolProperty prop;
ReadBoolProperty(prop);
ReadBoolProperty(prop, attrib);
if (inAnimator && curAnim->type == Animator::FLY_CIRCLE && prop.name == "Loop") {
if (curAnim->type == Animator::FLY_CIRCLE && prop.name == "Loop") {
curAnim->loop = prop.value;
}
//} else if (!ASSIMP_stricmp(reader->getNodeName(), "float")) {
} else if (!ASSIMP_stricmp(attrib.name(), "float")) {
FloatProperty prop;
ReadFloatProperty(prop);
ReadFloatProperty(prop, attrib);
if (inAnimator) {
// The speed property exists for several animators
if (prop.name == "Speed") {
curAnim->speed = prop.value;
@ -1079,126 +1051,20 @@ void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy
} else if (curAnim->type == Animator::FOLLOW_SPLINE && prop.name == "Tightness") {
curAnim->tightness = prop.value;
}
} else {
if (prop.name == "FramesPerSecond" && Node::ANIMMESH == curNode->type) {
curNode->framesPerSecond = prop.value;
} else if (Node::CAMERA == curNode->type) {
/* This is the vertical, not the horizontal FOV.
* We need to compute the right FOV from the
* screen aspect which we don't know yet.
*/
if (prop.name == "Fovy") {
cameras.back()->mHorizontalFOV = prop.value;
} else if (prop.name == "Aspect") {
cameras.back()->mAspect = prop.value;
} else if (prop.name == "ZNear") {
cameras.back()->mClipPlaneNear = prop.value;
} else if (prop.name == "ZFar") {
cameras.back()->mClipPlaneFar = prop.value;
}
} else if (Node::LIGHT == curNode->type) {
/* Additional light information
*/
if (prop.name == "Attenuation") {
lights.back()->mAttenuationLinear = prop.value;
} else if (prop.name == "OuterCone") {
lights.back()->mAngleOuterCone = AI_DEG_TO_RAD(prop.value);
} else if (prop.name == "InnerCone") {
lights.back()->mAngleInnerCone = AI_DEG_TO_RAD(prop.value);
}
}
// radius of the sphere to be generated -
// or alternatively, size of the cube
else if ((Node::SPHERE == curNode->type && prop.name == "Radius") || (Node::CUBE == curNode->type && prop.name == "Size")) {
curNode->sphereRadius = prop.value;
}
}
//} else if (!ASSIMP_stricmp(reader->getNodeName(), "int")) {
} else if (!ASSIMP_stricmp(attrib.name(), "int")) {
IntProperty prop;
ReadIntProperty(prop);
ReadIntProperty(prop, attrib);
if (inAnimator) {
if (curAnim->type == Animator::FLY_STRAIGHT && prop.name == "TimeForWay") {
curAnim->timeForWay = prop.value;
}
} else {
// sphere polygon numbers in each direction
if (Node::SPHERE == curNode->type) {
if (prop.name == "PolyCountX") {
curNode->spherePolyCountX = prop.value;
} else if (prop.name == "PolyCountY") {
curNode->spherePolyCountY = prop.value;
}
}
}
//} else if (!ASSIMP_stricmp(reader->getNodeName(), "string") || !ASSIMP_stricmp(reader->getNodeName(), "enum")) {
} else if (!ASSIMP_stricmp(attrib.name(), "string") || !ASSIMP_stricmp(attrib.name(), "enum")) {
StringProperty prop;
ReadStringProperty(prop);
if (prop.value.length()) {
if (prop.name == "Name") {
curNode->name = prop.value;
ReadStringProperty(prop, attrib);
/* If we're either a camera or a light source
* we need to update the name in the aiLight/
* aiCamera structure, too.
*/
if (Node::CAMERA == curNode->type) {
cameras.back()->mName.Set(prop.value);
} else if (Node::LIGHT == curNode->type) {
lights.back()->mName.Set(prop.value);
}
} else if (Node::LIGHT == curNode->type && "LightType" == prop.name) {
if (prop.value == "Spot")
lights.back()->mType = aiLightSource_SPOT;
else if (prop.value == "Point")
lights.back()->mType = aiLightSource_POINT;
else if (prop.value == "Directional")
lights.back()->mType = aiLightSource_DIRECTIONAL;
else {
// We won't pass the validation with aiLightSourceType_UNDEFINED,
// so we remove the light and replace it with a silly dummy node
delete lights.back();
lights.pop_back();
curNode->type = Node::DUMMY;
ASSIMP_LOG_ERROR("Ignoring light of unknown type: ", prop.value);
}
} else if ((prop.name == "Mesh" && Node::MESH == curNode->type) ||
Node::ANIMMESH == curNode->type) {
/* This is the file name of the mesh - either
* animated or not. We need to make sure we setup
* the correct post-processing settings here.
*/
unsigned int pp = 0;
BatchLoader::PropertyMap map;
/* If the mesh is a static one remove all animations from the impor data
*/
if (Node::ANIMMESH != curNode->type) {
pp |= aiProcess_RemoveComponent;
SetGenericProperty<int>(map.ints, AI_CONFIG_PP_RVC_FLAGS,
aiComponent_ANIMATIONS | aiComponent_BONEWEIGHTS);
}
/* TODO: maybe implement the protection against recursive
* loading calls directly in BatchLoader? The current
* implementation is not absolutely safe. A LWS and an IRR
* file referencing each other *could* cause the system to
* recurse forever.
*/
const std::string extension = GetExtension(prop.value);
if ("irr" == extension) {
ASSIMP_LOG_ERROR("IRR: Can't load another IRR file recursively");
} else {
curNode->id = batch.AddLoadRequest(prop.value, pp, &map);
curNode->meshPath = prop.value;
}
} else if (inAnimator && prop.name == "Type") {
if (prop.name == "Type") {
// type of the animator
if (prop.value == "rotation") {
curAnim->type = Animator::ROTATION;
@ -1216,42 +1082,168 @@ void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy
}
}
}
//} else if (reader->getNodeType() == EXN_ELEMENT_END && !ASSIMP_stricmp(reader->getNodeName(), "attributes")) {
} else if (attrib.type() == pugi::node_null && !ASSIMP_stricmp(attrib.name(), "attributes")) {
break;
}
}
}
break;
/*case EXN_ELEMENT_END:
IRRImporter::Node *IRRImporter::ParseNode(pugi::xml_node &node, BatchLoader &batch) {
// Parse <node> tags.
// <node> tags have various types
// <node> tags can contain <attribute>, <material>
// they can also contain other <node> tags, (and can reference other files as well?)
// ***********************************************************************
/* What we're going to do with the node depends
* on its type:
*
* "mesh" - Load a mesh from an external file
* "cube" - Generate a cube
* "skybox" - Generate a skybox
* "light" - A light source
* "sphere" - Generate a sphere mesh
* "animatedMesh" - Load an animated mesh from an external file
* and join its animation channels with ours.
* "empty" - A dummy node
* "camera" - A camera
* "terrain" - a terrain node (data comes from a heightmap)
* "billboard", ""
*
* Each of these nodes can be animated and all can have multiple
* materials assigned (except lights, cameras and dummies, of course).
* Said materials and animators are all collected at the bottom
*/
// ***********************************************************************
Node *nd;
pugi::xml_attribute nodeTypeAttrib = node.attribute("type");
if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "mesh") || !ASSIMP_stricmp(nodeTypeAttrib.value(), "octTree")) {
// OctTree's and meshes are treated equally
nd = new Node(Node::MESH);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "cube")) {
nd = new Node(Node::CUBE);
guessedMeshCnt += 1; // Cube is only one mesh
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "skybox")) {
nd = new Node(Node::SKYBOX);
guessedMeshCnt += 6; // Skybox is a box, with 6 meshes?
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "camera")) {
nd = new Node(Node::CAMERA);
// Setup a temporary name for the camera
aiCamera *cam = new aiCamera();
cam->mName.Set(nd->name);
cameras.push_back(cam);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "light")) {
nd = new Node(Node::LIGHT);
// Setup a temporary name for the light
aiLight *cam = new aiLight();
cam->mName.Set(nd->name);
lights.push_back(cam);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "sphere")) {
nd = new Node(Node::SPHERE);
guessedMeshCnt += 1;
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "animatedMesh")) {
nd = new Node(Node::ANIMMESH);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "empty")) {
nd = new Node(Node::DUMMY);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "terrain")) {
nd = new Node(Node::TERRAIN);
} else if (!ASSIMP_stricmp(nodeTypeAttrib.value(), "billBoard")) {
// We don't support billboards, so ignore them
ASSIMP_LOG_ERROR("IRR: Billboards are not supported by Assimp");
nd = new Node(Node::DUMMY);
} else {
ASSIMP_LOG_WARN("IRR: Found unknown node: ", nodeTypeAttrib.value());
// If we reached the end of a node, we need to continue processing its parent
if (!ASSIMP_stricmp(reader->getNodeName(), "node")) {
if (!curNode) {
// currently is no node set. We need to go
// back in the node hierarchy
if (!curParent) {
curParent = root;
ASSIMP_LOG_ERROR("IRR: Too many closing <node> elements");
} else
curParent = curParent->parent;
} else
curNode = nullptr;
/* We skip the contents of nodes we don't know.
* We parse the transformation and all animators
* and skip the rest.
*/
nd = new Node(Node::DUMMY);
}
// clear all flags
else if (!ASSIMP_stricmp(reader->getNodeName(), "materials")) {
inMaterials = false;
} else if (!ASSIMP_stricmp(reader->getNodeName(), "animators")) {
inAnimator = false;
}
break;*/
default:
// GCC complains that not all enumeration values are handled
break;
// TODO: consolidate all into one loop
for (pugi::xml_node subNode : node.children()) {
// Collect node attributes first
if (!ASSIMP_stricmp(subNode.name(), "attributes")) {
ParseNodeAttributes(subNode, nd, batch); // Parse attributes into this node
} else if (!ASSIMP_stricmp(subNode.name(), "animators")) {
// Then parse any animators
// All animators should contain an <attributes> tag
// This is an animation path - add a new animator
// to the list.
ParseAnimators(subNode, nd); // Function modifies nd's animator vector
guessedAnimCnt += 1;
}
// Then parse any materials
// Materials are available to almost all node types
if (nd->type != Node::DUMMY) {
if (!ASSIMP_stricmp(subNode.name(), "materials")) {
// Parse material description directly
// Each material should contain an <attributes> node
// with everything specified
nd->materials.emplace_back();
std::pair<aiMaterial *, unsigned int> &p = nd->materials.back();
p.first = ParseMaterial(subNode, p.second);
guessedMatCnt += 1;
}
}
}
// Then parse any child nodes
// Attach the newly created node to the scene-graph
for (pugi::xml_node child : node.children()) {
if (!ASSIMP_stricmp(child.name(), "node")) { // Is a child node
Node *childNd = ParseNode(child, batch); // Repeat this function for all children
nd->children.push_back(childNd);
};
}
// Return fully specified node
return nd;
}
// ------------------------------------------------------------------------------------------------
// Imports the given file into the given scene structure.
void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSystem *pIOHandler) {
std::unique_ptr<IOStream> file(pIOHandler->Open(pFile));
// Check whether we can read from the file
if (file == nullptr) {
throw DeadlyImportError("Failed to open IRR file ", pFile);
}
// Construct the irrXML parser
XmlParser st;
if (!st.parse(file.get())) {
throw DeadlyImportError("XML parse error while loading IRR file ", pFile);
}
pugi::xml_node documentRoot = st.getRootNode();
// The root node of the scene
Node *root = new Node(Node::DUMMY);
root->parent = nullptr;
root->name = "<IRRSceneRoot>";
// Batch loader used to load external models
BatchLoader batch(pIOHandler);
// batch.SetBasePath(pFile);
cameras.reserve(1); // Probably only one camera in entire scene
lights.reserve(5);
this->guessedAnimCnt = 0;
this->guessedMeshCnt = 0;
this->guessedMatCnt = 0;
// Parse the XML
// Find the scene root from document root.
const pugi::xml_node &sceneRoot = documentRoot.child("irr_scene");
if (!sceneRoot) throw new DeadlyImportError("IRR: <irr_scene> not found in file");
for (pugi::xml_node &child : sceneRoot.children()) {
// XML elements are either nodes, animators, attributes, or materials
if (!ASSIMP_stricmp(child.name(), "node")) {
// Recursive collect subtree children
Node *nd = ParseNode(child, batch);
// Attach to root
root->children.push_back(nd);
}
}
//}
// Now iterate through all cameras and compute their final (horizontal) FOV
for (aiCamera *cam : cameras) {
@ -1337,8 +1329,7 @@ void IRRImporter::InternReadFile(const std::string &pFile, aiScene *pScene, IOSy
// Now merge all sub scenes and attach them to the correct
// attachment points in the scenegraph.
SceneCombiner::MergeScenes(&pScene, tempScene, attach,
AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES | (!configSpeedFlag ? (
AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES_IF_NECESSARY | AI_INT_MERGE_SCENE_GEN_UNIQUE_MATNAMES) :
AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES | (!configSpeedFlag ? (AI_INT_MERGE_SCENE_GEN_UNIQUE_NAMES_IF_NECESSARY | AI_INT_MERGE_SCENE_GEN_UNIQUE_MATNAMES) :
0));
// If we have no meshes | no materials now set the INCOMPLETE

View File

@ -94,18 +94,10 @@ private:
} type;
explicit Animator(AT t = UNKNOWN)
: type (t)
, speed ( ai_real( 0.001 ) )
, direction ( ai_real( 0.0 ), ai_real( 1.0 ), ai_real( 0.0 ) )
, circleRadius ( ai_real( 1.0) )
, tightness ( ai_real( 0.5 ) )
, loop (true)
, timeForWay (100)
{
explicit Animator(AT t = UNKNOWN) :
type(t), speed(ai_real(0.001)), direction(ai_real(0.0), ai_real(1.0), ai_real(0.0)), circleRadius(ai_real(1.0)), tightness(ai_real(0.5)), loop(true), timeForWay(100) {
}
// common parameters
ai_real speed;
aiVector3D direction;
@ -128,11 +120,9 @@ private:
/** Data structure for a scene-graph node in an IRR file
*/
struct Node
{
struct Node {
// Type of the node
enum ET
{
enum ET {
LIGHT,
CUBE,
MESH,
@ -144,16 +134,15 @@ private:
ANIMMESH
} type;
explicit Node(ET t)
: type (t)
, scaling (1.0,1.0,1.0) // assume uniform scaling by default
, parent()
, framesPerSecond (0.0)
, id()
, sphereRadius (1.0)
, spherePolyCountX (100)
, spherePolyCountY (100)
{
explicit Node(ET t) :
type(t), scaling(1.0, 1.0, 1.0) // assume uniform scaling by default
,
parent(),
framesPerSecond(0.0),
id(),
sphereRadius(1.0),
spherePolyCountX(100),
spherePolyCountY(100) {
// Generate a default name for the node
char buffer[128];
@ -204,8 +193,7 @@ private:
/** Data structure for a vertex in an IRR skybox
*/
struct SkyboxVertex
{
struct SkyboxVertex {
SkyboxVertex() = default;
//! Construction from single vertex components
@ -213,14 +201,29 @@ private:
ai_real nx, ai_real ny, ai_real nz,
ai_real uvx, ai_real uvy)
: position (px,py,pz)
, normal (nx,ny,nz)
, uv (uvx,uvy,0.0)
{}
:
position(px, py, pz), normal(nx, ny, nz), uv(uvx, uvy, 0.0) {}
aiVector3D position, normal, uv;
};
// -------------------------------------------------------------------
// Parse <node> tag from XML file and extract child node
// @param node XML node
// @param guessedMeshesContained number of extra guessed meshes
IRRImporter::Node *ParseNode(pugi::xml_node &node, BatchLoader& batch);
// -------------------------------------------------------------------
// Parse <attributes> tags within <node> tags and apply to scene node
// @param attributeNode XML child node
// @param nd Attributed scene node
void ParseNodeAttributes(pugi::xml_node &attributeNode, IRRImporter::Node *nd, BatchLoader& batch);
// -------------------------------------------------------------------
// Parse an <animator> node and attach an animator to a node
// @param animatorNode XML animator node
// @param nd Animated scene node
void ParseAnimators(pugi::xml_node &animatorNode, IRRImporter::Node *nd);
// -------------------------------------------------------------------
/// Fill the scene-graph recursively
@ -276,6 +279,12 @@ private:
/// Configuration option: speed flag was set?
bool configSpeedFlag;
std::vector<aiCamera*> cameras;
std::vector<aiLight*> lights;
unsigned int guessedMeshCnt;
unsigned int guessedMatCnt;
unsigned int guessedAnimCnt;
};
} // end of namespace Assimp

View File

@ -133,6 +133,7 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
meshes.reserve(5);
// temporary data - current mesh buffer
// TODO move all these to inside loop
aiMaterial *curMat = nullptr;
aiMesh *curMesh = nullptr;
unsigned int curMatFlags = 0;
@ -142,23 +143,28 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
std::vector<aiVector3D> curUVs, curUV2s;
// some temporary variables
int textMeaning = 0;
int vertexFormat = 0; // 0 = normal; 1 = 2 tcoords, 2 = tangents
// textMeaning is a 15 year old variable, that could've been an enum
// int textMeaning = 0; // 0=none? 1=vertices 2=indices
// int vertexFormat = 0; // 0 = normal; 1 = 2 tcoords, 2 = tangents
bool useColors = false;
/*
** irrmesh files have a top level <mesh> owning multiple <buffer> nodes.
** Each <buffer> contains <material>, <vertices>, and <indices>
** <material> tags here directly owns the material data specs
** <vertices> are a vertex per line, contains position, UV1 coords, maybe UV2, normal, tangent, bitangent
** <boundingbox> is ignored, I think assimp recalculates those?
*/
// Parse the XML file
for (pugi::xml_node child : root.children()) {
if (child.type() == pugi::node_element) {
if (!ASSIMP_stricmp(child.name(), "buffer") && (curMat || curMesh)) {
// end of previous buffer. A material and a mesh should be there
if (!curMat || !curMesh) {
ASSIMP_LOG_ERROR("IRRMESH: A buffer must contain a mesh and a material");
releaseMaterial(&curMat);
releaseMesh(&curMesh);
} else {
materials.push_back(curMat);
meshes.push_back(curMesh);
pugi::xml_node const &meshNode = root.child("mesh");
for (pugi::xml_node bufferNode : meshNode.children()) {
if (ASSIMP_stricmp(bufferNode.name(), "buffer")) {
// Might be a useless warning
ASSIMP_LOG_WARN("IRRMESH: Ignoring non buffer node <", bufferNode.name(), "> in mesh declaration");
continue;
}
curMat = nullptr;
curMesh = nullptr;
@ -169,42 +175,47 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
curUVs.clear();
curTangents.clear();
curBitangents.clear();
}
if (!ASSIMP_stricmp(child.name(), "material")) {
if (curMat) {
// TODO ensure all three nodes are present and populated
// before allocating everything
// Get first material node
pugi::xml_node materialNode = bufferNode.child("material");
if (materialNode) {
curMat = ParseMaterial(materialNode, curMatFlags);
// Warn if there's more materials
if (materialNode.next_sibling("material")) {
ASSIMP_LOG_WARN("IRRMESH: Only one material description per buffer, please");
releaseMaterial(&curMat);
}
curMat = ParseMaterial(curMatFlags);
}
/* no else here! */ if (!ASSIMP_stricmp(child.name(), "vertices")) {
pugi::xml_attribute attr = child.attribute("vertexCount");
int num = attr.as_int();
//int num = reader->getAttributeValueAsInt("vertexCount");
if (!num) {
// This is possible ... remove the mesh from the list and skip further reading
ASSIMP_LOG_WARN("IRRMESH: Found mesh with zero vertices");
releaseMaterial(&curMat);
releaseMesh(&curMesh);
textMeaning = 0;
} else {
ASSIMP_LOG_ERROR("IRRMESH: Buffer must contain one material");
continue;
}
curVertices.reserve(num);
curNormals.reserve(num);
curColors.reserve(num);
curUVs.reserve(num);
// Get first vertices node
pugi::xml_node verticesNode = bufferNode.child("vertices");
if (verticesNode) {
pugi::xml_attribute vertexCountAttrib = verticesNode.attribute("vertexCount");
int vertexCount = vertexCountAttrib.as_int();
if (vertexCount == 0) {
// This is possible ... remove the mesh from the list and skip further reading
ASSIMP_LOG_WARN("IRRMESH: Found mesh with zero vertices");
releaseMaterial(&curMat);
// releaseMesh(&curMesh);
continue; // Bail out early
};
curVertices.reserve(vertexCount);
curNormals.reserve(vertexCount);
curColors.reserve(vertexCount);
curUVs.reserve(vertexCount);
VertexFormat vertexFormat;
// Determine the file format
//const char *t = reader->getAttributeValueSafe("type");
pugi::xml_attribute t = child.attribute("type");
if (!ASSIMP_stricmp("2tcoords", t.name())) {
curUV2s.reserve(num);
vertexFormat = 1;
pugi::xml_attribute typeAttrib = verticesNode.attribute("type");
if (!ASSIMP_stricmp("2tcoords", typeAttrib.value())) {
curUV2s.reserve(vertexCount);
vertexFormat = VertexFormat::t2coord;
if (curMatFlags & AI_IRRMESH_EXTRA_2ND_TEXTURE) {
// *********************************************************
// We have a second texture! So use this UV channel
@ -223,29 +234,38 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
mat->AddProperty(&idx, 1, AI_MATKEY_UVWSRC_DIFFUSE(1));
}
}
} else if (!ASSIMP_stricmp("tangents", t.name())) {
curTangents.reserve(num);
curBitangents.reserve(num);
vertexFormat = 2;
} else if (ASSIMP_stricmp("standard", t.name())) {
} else if (!ASSIMP_stricmp("tangents", typeAttrib.value())) {
curTangents.reserve(vertexCount);
curBitangents.reserve(vertexCount);
vertexFormat = VertexFormat::tangent;
} else if (!ASSIMP_stricmp("standard", typeAttrib.value())) {
vertexFormat = VertexFormat::standard;
} else {
// Unsupported format, discard whole buffer/mesh
// Assuming we have a correct material, then release it
// We don't have a correct mesh for sure here
releaseMaterial(&curMat);
ASSIMP_LOG_WARN("IRRMESH: Unknown vertex format");
} else
vertexFormat = 0;
textMeaning = 1;
} else if (!ASSIMP_stricmp(child.name(), "indices")) {
if (curVertices.empty() && curMat) {
releaseMaterial(&curMat);
throw DeadlyImportError("IRRMESH: indices must come after vertices");
ASSIMP_LOG_ERROR("IRRMESH: Unknown vertex format");
continue; // Skip rest of buffer
};
// We know what format buffer is, collect numbers
ParseBufferVertices(verticesNode.text().get(), vertexFormat,
curVertices, curNormals,
curTangents, curBitangents,
curUVs, curUV2s, curColors, useColors);
}
textMeaning = 2;
// Get indices
// At this point we have some vertices and a valid material
// Collect indices and create aiMesh at the same time
pugi::xml_node indicesNode = bufferNode.child("indices");
if (indicesNode) {
// start a new mesh
curMesh = new aiMesh();
// allocate storage for all faces
pugi::xml_attribute attr = child.attribute("indexCount");
pugi::xml_attribute attr = indicesNode.attribute("indexCount");
curMesh->mNumVertices = attr.as_int();
if (!curMesh->mNumVertices) {
// This is possible ... remove the mesh from the list and skip further reading
@ -256,9 +276,7 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
// material - away
releaseMaterial(&curMat);
textMeaning = 0;
continue;
continue; // Go to next buffer
}
if (curMesh->mNumVertices % 3) {
@ -293,107 +311,6 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
if (curUV2s.size() == curVertices.size()) {
curMesh->mTextureCoords[1] = new aiVector3D[curMesh->mNumVertices];
}
}
//break;
//case EXN_TEXT: {
const char *sz = child.child_value();
if (textMeaning == 1) {
textMeaning = 0;
// read vertices
do {
SkipSpacesAndLineEnd(&sz);
aiVector3D temp;
aiColor4D c;
// Read the vertex position
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
curVertices.push_back(temp);
// Read the vertex normals
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
curNormals.push_back(temp);
// read the vertex colors
uint32_t clr = strtoul16(sz, &sz);
ColorFromARGBPacked(clr, c);
if (!curColors.empty() && c != *(curColors.end() - 1))
useColors = true;
curColors.push_back(c);
SkipSpaces(&sz);
// read the first UV coordinate set
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.z = 0.f;
temp.y = 1.f - temp.y; // DX to OGL
curUVs.push_back(temp);
// read the (optional) second UV coordinate set
if (vertexFormat == 1) {
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
temp.y = 1.f - temp.y; // DX to OGL
curUV2s.push_back(temp);
}
// read optional tangent and bitangent vectors
else if (vertexFormat == 2) {
// tangents
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.y *= -1.0f;
curTangents.push_back(temp);
// bitangents
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.y *= -1.0f;
curBitangents.push_back(temp);
}
}
/* IMPORTANT: We assume that each vertex is specified in one
line. So we can skip the rest of the line - unknown vertex
elements are ignored.
*/
while (SkipLine(&sz));
} else if (textMeaning == 2) {
textMeaning = 0;
// read indices
aiFace *curFace = curMesh->mFaces;
@ -409,24 +326,33 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
unsigned int curIdx = 0;
unsigned int total = 0;
// NOTE this might explode for UTF-16 and wchars
const char *sz = indicesNode.text().get();
// For each index loop over aiMesh faces
while (SkipSpacesAndLineEnd(&sz)) {
if (curFace >= faceEnd) {
ASSIMP_LOG_ERROR("IRRMESH: Too many indices");
break;
}
// if new face
if (!curIdx) {
curFace->mNumIndices = 3;
curFace->mIndices = new unsigned int[3];
}
// Read index base 10
// function advances the pointer
unsigned int idx = strtoul10(sz, &sz);
if (idx >= curVertices.size()) {
ASSIMP_LOG_ERROR("IRRMESH: Index out of range");
idx = 0;
}
// make up our own indices?
curFace->mIndices[curIdx] = total++;
// Copy over data to aiMesh
*pcV++ = curVertices[idx];
if (pcN) *pcN++ = curNormals[idx];
if (pcT) *pcT++ = curTangents[idx];
@ -435,14 +361,16 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
if (pcT0) *pcT0++ = curUVs[idx];
if (pcT1) *pcT1++ = curUV2s[idx];
// start new face
if (++curIdx == 3) {
++curFace;
curIdx = 0;
}
}
// We should be at the end of mFaces
if (curFace != faceEnd)
ASSIMP_LOG_ERROR("IRRMESH: Not enough indices");
}
// Finish processing the mesh - do some small material workarounds
if (curMatFlags & AI_IRRMESH_MAT_trans_vertex_alpha && !useColors) {
@ -451,12 +379,9 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
aiMaterial *mat = (aiMaterial *)curMat;
mat->AddProperty(&curColors[0].a, 1, AI_MATKEY_OPACITY);
}
}
}
}
// textMeaning = 2;
// End of the last buffer. A material and a mesh should be there
if (curMat || curMesh) {
// end of previous buffer. A material and a mesh should be there
if (!curMat || !curMesh) {
ASSIMP_LOG_ERROR("IRRMESH: A buffer must contain a mesh and a material");
releaseMaterial(&curMat);
@ -467,7 +392,8 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
}
}
if (materials.empty()) {
// If one is empty then so is the other
if (materials.empty() || meshes.empty()) {
throw DeadlyImportError("IRRMESH: Unable to read a mesh from this file");
}
@ -492,7 +418,105 @@ void IRRMeshImporter::InternReadFile(const std::string &pFile,
for (unsigned int i = 0; i < pScene->mNumMeshes; ++i) {
pScene->mRootNode->mMeshes[i] = i;
};
}
void IRRMeshImporter::ParseBufferVertices(const char *sz, VertexFormat vertexFormat,
std::vector<aiVector3D> &vertices, std::vector<aiVector3D> &normals,
std::vector<aiVector3D> &tangents, std::vector<aiVector3D> &bitangents,
std::vector<aiVector3D> &UVs, std::vector<aiVector3D> &UV2s,
std::vector<aiColor4D> &colors, bool &useColors) {
// read vertices
do {
SkipSpacesAndLineEnd(&sz);
aiVector3D temp;
aiColor4D c;
// Read the vertex position
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
vertices.push_back(temp);
// Read the vertex normals
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
normals.push_back(temp);
// read the vertex colors
uint32_t clr = strtoul16(sz, &sz);
ColorFromARGBPacked(clr, c);
// If we're pushing more than one distinct color
if (!colors.empty() && c != *(colors.end() - 1))
useColors = true;
colors.push_back(c);
SkipSpaces(&sz);
// read the first UV coordinate set
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.z = 0.f;
temp.y = 1.f - temp.y; // DX to OGL
UVs.push_back(temp);
// NOTE these correspond to specific S3DVertex* structs in irr sourcecode
// So by definition, all buffers have either UV2 or tangents or neither
// read the (optional) second UV coordinate set
if (vertexFormat == VertexFormat::t2coord) {
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
temp.y = 1.f - temp.y; // DX to OGL
UV2s.push_back(temp);
}
// read optional tangent and bitangent vectors
else if (vertexFormat == VertexFormat::tangent) {
// tangents
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.y *= -1.0f;
tangents.push_back(temp);
// bitangents
sz = fast_atoreal_move<float>(sz, (float &)temp.x);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.z);
SkipSpaces(&sz);
sz = fast_atoreal_move<float>(sz, (float &)temp.y);
SkipSpaces(&sz);
temp.y *= -1.0f;
bitangents.push_back(temp);
}
} while (SkipLine(&sz));
/* IMPORTANT: We assume that each vertex is specified in one
line. So we can skip the rest of the line - unknown vertex
elements are ignored.
*/
}
#endif // !! ASSIMP_BUILD_NO_IRRMESH_IMPORTER

View File

@ -85,6 +85,19 @@ protected:
*/
void InternReadFile(const std::string &pFile, aiScene *pScene,
IOSystem *pIOHandler) override;
private:
enum class VertexFormat {
standard = 0, // "standard" - also noted as 'normal' format elsewhere
t2coord = 1, // "2tcoord" - standard + 2 UV maps
tangent = 2, // "tangents" - standard + tangents and bitangents
};
void ParseBufferVertices(const char *sz, VertexFormat vertexFormat,
std::vector<aiVector3D> &vertices, std::vector<aiVector3D> &normals,
std::vector<aiVector3D> &tangents, std::vector<aiVector3D> &bitangents,
std::vector<aiVector3D> &UVs, std::vector<aiVector3D> &UV2s,
std::vector<aiColor4D> &colors, bool &useColors);
};
} // end of namespace Assimp

View File

@ -49,8 +49,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "IRRShared.h"
#include <assimp/ParsingUtils.h>
#include <assimp/fast_atof.h>
#include <assimp/DefaultLogger.hpp>
#include <assimp/material.h>
#include <assimp/DefaultLogger.hpp>
using namespace Assimp;
@ -63,34 +63,34 @@ const aiMatrix4x4 Assimp::AI_TO_IRR_MATRIX = aiMatrix4x4 (
// ------------------------------------------------------------------------------------------------
// read a property in hexadecimal format (i.e. ffffffff)
void IrrlichtBase::ReadHexProperty(HexProperty &out ) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadHexProperty(HexProperty &out, pugi::xml_node& hexnode) {
for (pugi::xml_attribute attrib : hexnode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
// parse the hexadecimal value
out.value = strtoul16(attrib.name());
out.value = strtoul16(attrib.value());
}
}
}
// ------------------------------------------------------------------------------------------------
// read a decimal property
void IrrlichtBase::ReadIntProperty(IntProperty & out) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadIntProperty(IntProperty &out, pugi::xml_node& intnode) {
for (pugi::xml_attribute attrib : intnode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.value(),"value")) {
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
// parse the int value
out.value = strtol10(attrib.name());
out.value = strtol10(attrib.value());
}
}
}
// ------------------------------------------------------------------------------------------------
// read a string property
void IrrlichtBase::ReadStringProperty( StringProperty& out) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadStringProperty(StringProperty &out, pugi::xml_node& stringnode) {
for (pugi::xml_attribute attrib : stringnode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
@ -102,8 +102,8 @@ void IrrlichtBase::ReadStringProperty( StringProperty& out) {
// ------------------------------------------------------------------------------------------------
// read a boolean property
void IrrlichtBase::ReadBoolProperty(BoolProperty &out) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadBoolProperty(BoolProperty &out, pugi::xml_node& boolnode) {
for (pugi::xml_attribute attrib : boolnode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
@ -115,8 +115,8 @@ void IrrlichtBase::ReadBoolProperty(BoolProperty &out) {
// ------------------------------------------------------------------------------------------------
// read a float property
void IrrlichtBase::ReadFloatProperty(FloatProperty &out) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadFloatProperty(FloatProperty &out, pugi::xml_node &floatnode) {
for (pugi::xml_attribute attrib : floatnode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
@ -128,8 +128,8 @@ void IrrlichtBase::ReadFloatProperty(FloatProperty &out) {
// ------------------------------------------------------------------------------------------------
// read a vector property
void IrrlichtBase::ReadVectorProperty( VectorProperty &out ) {
for (pugi::xml_attribute attrib : mNode->attributes()) {
void IrrlichtBase::ReadVectorProperty(VectorProperty &out, pugi::xml_node& vectornode) {
for (pugi::xml_attribute attrib : vectornode.attributes()) {
if (!ASSIMP_stricmp(attrib.name(), "name")) {
out.name = std::string(attrib.value());
} else if (!ASSIMP_stricmp(attrib.name(), "value")) {
@ -170,7 +170,7 @@ int ConvertMappingMode(const std::string& mode) {
// ------------------------------------------------------------------------------------------------
// Parse a material from the XML file
aiMaterial* IrrlichtBase::ParseMaterial(unsigned int& matFlags) {
aiMaterial *IrrlichtBase::ParseMaterial(pugi::xml_node& materialNode, unsigned int &matFlags) {
aiMaterial *mat = new aiMaterial();
aiColor4D clr;
aiString s;
@ -179,10 +179,10 @@ aiMaterial* IrrlichtBase::ParseMaterial(unsigned int& matFlags) {
int cnt = 0; // number of used texture channels
unsigned int nd = 0;
for (pugi::xml_node child : mNode->children()) {
for (pugi::xml_node child : materialNode.children()) {
if (!ASSIMP_stricmp(child.name(), "color")) { // Hex properties
HexProperty prop;
ReadHexProperty(prop);
ReadHexProperty(prop, child);
if (prop.name == "Diffuse") {
ColorFromARGBPacked(prop.value, clr);
mat->AddProperty(&clr, 1, AI_MATKEY_COLOR_DIFFUSE);
@ -206,13 +206,13 @@ aiMaterial* IrrlichtBase::ParseMaterial(unsigned int& matFlags) {
#endif
} else if (!ASSIMP_stricmp(child.name(), "float")) { // Float properties
FloatProperty prop;
ReadFloatProperty(prop);
ReadFloatProperty(prop, child);
if (prop.name == "Shininess") {
mat->AddProperty(&prop.value, 1, AI_MATKEY_SHININESS);
}
} else if (!ASSIMP_stricmp(child.name(), "bool")) { // Bool properties
BoolProperty prop;
ReadBoolProperty(prop);
ReadBoolProperty(prop, child);
if (prop.name == "Wireframe") {
int val = (prop.value ? true : false);
mat->AddProperty(&val, 1, AI_MATKEY_ENABLE_WIREFRAME);
@ -226,7 +226,7 @@ aiMaterial* IrrlichtBase::ParseMaterial(unsigned int& matFlags) {
} else if (!ASSIMP_stricmp(child.name(), "texture") ||
!ASSIMP_stricmp(child.name(), "enum")) { // String properties - textures and texture related properties
StringProperty prop;
ReadStringProperty(prop);
ReadStringProperty(prop, child);
if (prop.value.length()) {
// material type (shader)
if (prop.name == "Type") {
@ -379,7 +379,7 @@ aiMaterial* IrrlichtBase::ParseMaterial(unsigned int& matFlags) {
}
}*/
}
ASSIMP_LOG_ERROR("IRRMESH: Unexpected end of file. Material is not complete");
//ASSIMP_LOG_ERROR("IRRMESH: Unexpected end of file. Material is not complete");
return mat;
}

View File

@ -58,8 +58,7 @@ extern const aiMatrix4x4 AI_TO_IRR_MATRIX;
*/
class IrrlichtBase {
protected:
IrrlichtBase() :
mNode(nullptr) {
IrrlichtBase() {
// empty
}
@ -82,25 +81,25 @@ protected:
/// XML reader instance
XmlParser mParser;
pugi::xml_node *mNode;
// -------------------------------------------------------------------
/** Parse a material description from the XML
* @return The created material
* @param matFlags Receives AI_IRRMESH_MAT_XX flags
*/
aiMaterial *ParseMaterial(unsigned int &matFlags);
aiMaterial *ParseMaterial(pugi::xml_node &materialNode, unsigned int &matFlags);
// -------------------------------------------------------------------
/** Read a property of the specified type from the current XML element.
* @param out Receives output data
* @param node XML attribute element containing data
*/
void ReadHexProperty(HexProperty &out);
void ReadStringProperty(StringProperty &out);
void ReadBoolProperty(BoolProperty &out);
void ReadFloatProperty(FloatProperty &out);
void ReadVectorProperty(VectorProperty &out);
void ReadIntProperty(IntProperty &out);
void ReadHexProperty(HexProperty &out, pugi::xml_node& hexnode);
void ReadStringProperty(StringProperty &out, pugi::xml_node& stringnode);
void ReadBoolProperty(BoolProperty &out, pugi::xml_node& boolnode);
void ReadFloatProperty(FloatProperty &out, pugi::xml_node& floatnode);
void ReadVectorProperty(VectorProperty &out, pugi::xml_node& vectornode);
void ReadIntProperty(IntProperty &out, pugi::xml_node& intnode);
};
// ------------------------------------------------------------------------------------------------

View File

@ -837,7 +837,10 @@ void SMDImporter::ParseNodeInfo(const char* szCurrent, const char** szCurrentOut
unsigned int iBone = 0;
SkipSpacesAndLineEnd(szCurrent,&szCurrent);
if ( !ParseUnsignedInt(szCurrent,&szCurrent,iBone) || !SkipSpaces(szCurrent,&szCurrent)) {
LogErrorNoThrow("Unexpected EOF/EOL while parsing bone index");
throw DeadlyImportError("Unexpected EOF/EOL while parsing bone index");
}
if (iBone == UINT_MAX) {
LogErrorNoThrow("Invalid bone number while parsing bone index");
SMDI_PARSE_RETURN;
}
// add our bone to the list

View File

@ -391,6 +391,19 @@ def export_blob(scene,
raise AssimpError('Could not export scene to blob!')
return exportBlobPtr
def available_formats():
"""
Return a list of file format extensions supported to import.
Returns
---------
A list of upper-case file extensions, e.g. [3DS, OBJ]
"""
from ctypes import byref
extension_list = structs.String()
_assimp_lib.dll.aiGetExtensionList(byref(extension_list))
return [e[2:].upper() for e in str(extension_list.data, sys.getfilesystemencoding()).split(";")]
def _finalize_texture(tex, target):
setattr(target, "achformathint", tex.achFormatHint)
if numpy:

View File

@ -1,41 +0,0 @@
FORMATS = ["CSM",
"LWS",
"B3D",
"COB",
"PLY",
"IFC",
"OFF",
"SMD",
"IRRMESH",
"3D",
"DAE",
"MDL",
"HMP",
"TER",
"WRL",
"XML",
"NFF",
"AC",
"OBJ",
"3DS",
"STL",
"IRR",
"Q3O",
"Q3D",
"MS3D",
"Q3S",
"ZGL",
"MD2",
"X",
"BLEND",
"XGL",
"MD5MESH",
"MAX",
"LXO",
"DXF",
"BVH",
"LWO",
"NDO"]
def available_formats():
return FORMATS

View File

@ -0,0 +1,73 @@
SCEA Shared Source License 1.0
Terms and Conditions:
1. Definitions:
"Software" shall mean the software and related documentation, whether in Source or Object Form, made available under this SCEA Shared Source license ("License"),
that is indicated by a copyright notice file included in the source files or attached or accompanying the source files.
"Licensor" shall mean Sony Computer Entertainment America, Inc. (herein "SCEA")
"Object Code" or "Object Form" shall mean any form that results from translation or transformation of Source Code, including but not limited to compiled object code
or conversions to other forms intended for machine execution. "Source Code" or "Source Form" shall have the plain meaning generally accepted in the software industry,
including but not limited to software source code, documentation source, header and configuration files.
"You" or "Your" shall mean you as an individual or as a company, or whichever form under which you are exercising rights under this License.
2. License Grant.
Licensor hereby grants to You, free of charge subject to the terms and conditions of this License, an irrevocable, non-exclusive, worldwide, perpetual, and royalty-free
license to use, modify, reproduce, distribute, publicly perform or display the Software in Object or Source Form .
4. No Right to File for Patent.
In exchange for the rights that are granted to You free of charge under this License, You agree that You will not file for any patent application, seek copyright
protection or take any other action that might otherwise impair the ownership rights in and to the Software that may belong to SCEA or any of the other contributors/authors
of the Software.
6. Contributions.
SCEA welcomes contributions in form of modifications, optimizations, tools or documentation designed to improve or expand the performance and scope of the Software
(collectively "Contributions"). Per the terms of this License You are free to modify the Software and those modifications would belong to You. You may however wish to
donate Your Contributions to SCEA for consideration for inclusion into the Software. For the avoidance of doubt, if You elect to send Your Contributions to SCEA, You are
doing so voluntarily and are giving the Contributions to SCEA and its parent company Sony Computer Entertainment, Inc., free of charge, to use, modify or distribute in
any form or in any manner. SCEA acknowledges that if You make a donation of Your Contributions to SCEA, such Contributions shall not exclusively belong to SCEA or its
parent company and such donation shall not be to Your exclusion. SCEA, in its sole discretion, shall determine whether or not to include Your donated Contributions into
the Software, in whole, in part, or as modified by SCEA. Should SCEA elect to include any such Contributions into the Software, it shall do so at its own risk and may elect
to give credit or special thanks to any such contributors in the attached copyright notice. However, if any of Your contributions are included into the Software, they will
become part of the Software and will be distributed under the terms and conditions of this License. Further, if Your donated Contributions are integrated into the Software
then Sony Computer Entertainment, Inc. shall become the copyright owner of the Software now containing Your contributions and SCEA would be the Licensor.
8. Redistribution in Source Form
You may redistribute copies of the Software, modifications or derivatives thereof in Source Code Form, provided that You:
a. Include a copy of this License and any copyright notices with source
b. Identify modifications if any were made to the Software
c. Include a copy of all documentation accompanying the Software and modifications made by You
6. Redistribution in Object Form
If You redistribute copies of the Software, modifications or derivatives thereof in Object Form only (as incorporated into finished goods, i.e. end user applications) then
You will not have a duty to include any copies of the code, this License, copyright notices, other attributions or documentation.
7. No Warranty
THE SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT ANY REPRESENTATIONS OR WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY
WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING,
MODIFYING OR REDISTRIBUTING THE SOFTWARE AND ASSUME ANY RISKS ASSOCIATED WITH YOUR EXERCISE OF PERMISSIONS UNDER THIS LICENSE.
9. Limitation of Liability
UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY WILL EITHER PARTY BE LIABLE TO THE OTHER PARTY OR ANY THIRD PARTY FOR ANY DIRECT, INDIRECT, CONSEQUENTIAL, SPECIAL,
INCIDENTAL, OR EXEMPLARY DAMAGES WITH RESPECT TO ANY INJURY, LOSS, OR DAMAGE, ARISING UNDER OR IN CONNECTION WITH THIS LETTER AGREEMENT, WHETHER FORESEEABLE OR
UNFORESEEABLE, EVEN IF SUCH PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH INJURY, LOSS, OR DAMAGE. THE LIMITATIONS OF LIABILITY SET FORTH IN THIS SECTION SHALL
APPLY TO THE FULLEST EXTENT PERMISSIBLE AT LAW OR ANY GOVERMENTAL REGULATIONS.
11. Governing Law and Consent to Jurisdiction
This Agreement shall be governed by and interpreted in accordance with the laws of the State of California, excluding that body of law related to choice of laws,
and of the United States of America. Any action or proceeding brought to enforce the terms of this Agreement or to adjudicate any dispute arising hereunder shall
be brought in the Superior Court of the County of San Mateo, State of California or the United States District Court for the Northern District of California. Each
of the parties hereby submits itself to the exclusive jurisdiction and venue of such courts for purposes of any such action. In addition, each party hereby waives the
right to a jury trial in any action or proceeding related to this Agreement.
13. Copyright Notice for Redistribution of Source Code
Copyright 2005 Sony Computer Entertainment Inc.
Licensed under the SCEA Shared Source License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:
http://research.scea.com/scea_shared_source_license.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -51,7 +51,8 @@ public:
virtual bool importerTest() {
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/IRR/box.irr", aiProcess_ValidateDataStructure);
return nullptr != scene;
// Only one box thus only one mesh
return nullptr != scene && scene->mNumMeshes == 1;
}
};
@ -63,4 +64,7 @@ TEST_F(utIrrImportExport, importSGIrrTest) {
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(ASSIMP_TEST_MODELS_DIR "/IRR/dawfInCellar_SameHierarchy.irr", aiProcess_ValidateDataStructure);
EXPECT_NE(nullptr, scene);
EXPECT_EQ(scene->mNumMeshes, 2);
EXPECT_EQ(scene->mNumMaterials, 2);
EXPECT_GT(scene->mMeshes[0]->mNumVertices, 0);
}