Exemplo n.º 1
0
void NzGenerateBox(const NzVector3f& lengths, const NzVector3ui& subdivision, const NzMatrix4f& matrix, const NzRectf& textureCoords, NzMeshVertex* vertices, NzIndexIterator indices, NzBoxf* aabb, unsigned int indexOffset)
{
	unsigned int xIndexCount, yIndexCount, zIndexCount;
	unsigned int xVertexCount, yVertexCount, zVertexCount;

	NzComputePlaneIndexVertexCount(NzVector2ui(subdivision.y, subdivision.z), &xIndexCount, &xVertexCount);
	NzComputePlaneIndexVertexCount(NzVector2ui(subdivision.x, subdivision.z), &yIndexCount, &yVertexCount);
	NzComputePlaneIndexVertexCount(NzVector2ui(subdivision.x, subdivision.y), &zIndexCount, &zVertexCount);

	NzMatrix4f transform;
	NzVector3f halfLengths = lengths/2.f;

	// Face +X
	transform.MakeTransform(NzVector3f::UnitX() * halfLengths.x, NzEulerAnglesf(-90.f, 0.f, -90.f));
	NzGeneratePlane(NzVector2ui(subdivision.z, subdivision.y), NzVector2f(lengths.z, lengths.y), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += xVertexCount;
	indices += xIndexCount;
	vertices += xVertexCount;

	// Face +Y
	transform.MakeTransform(NzVector3f::UnitY() * halfLengths.y, NzEulerAnglesf(0.f, 0.f, 0.f));
	NzGeneratePlane(NzVector2ui(subdivision.x, subdivision.z), NzVector2f(lengths.x, lengths.z), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += yVertexCount;
	indices += yIndexCount;
	vertices += yVertexCount;

	// Face +Z
	transform.MakeTransform(NzVector3f::UnitZ() * halfLengths.z, NzEulerAnglesf(-90.f, 90.f, 90.f));
	NzGeneratePlane(NzVector2ui(subdivision.x, subdivision.y), NzVector2f(lengths.x, lengths.y), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += zVertexCount;
	indices += zIndexCount;
	vertices += zVertexCount;

	// Face -X
	transform.MakeTransform(-NzVector3f::UnitX() * halfLengths.x, NzEulerAnglesf(-90.f, 0.f, 90.f));
	NzGeneratePlane(NzVector2ui(subdivision.z, subdivision.y), NzVector2f(lengths.z, lengths.y), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += xVertexCount;
	indices += xIndexCount;
	vertices += xVertexCount;

	// Face -Y
	transform.MakeTransform(-NzVector3f::UnitY() * halfLengths.y, NzEulerAnglesf(0.f, 0.f, 180.f));
	NzGeneratePlane(NzVector2ui(subdivision.x, subdivision.z), NzVector2f(lengths.x, lengths.z), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += yVertexCount;
	indices += yIndexCount;
	vertices += yVertexCount;

	// Face -Z
	transform.MakeTransform(-NzVector3f::UnitZ() * halfLengths.z, NzEulerAnglesf(-90.f, -90.f, 90.f));
	NzGeneratePlane(NzVector2ui(subdivision.x, subdivision.y), NzVector2f(lengths.x, lengths.y), NzMatrix4f::ConcatenateAffine(matrix, transform), textureCoords, vertices, indices, nullptr, indexOffset);
	indexOffset += zVertexCount;
	indices += zIndexCount;
	vertices += zVertexCount;

	if (aabb)
	{
		aabb->Set(-halfLengths, halfLengths);
		aabb->Transform(matrix, false);
	}
}
Exemplo n.º 2
0
int main()
{
	// Pour commencer, nous initialisons le module Graphique, celui-ci va provoquer l'initialisation (dans l'ordre),
	// du noyau (Core), Utility, Renderer.
	// NzInitializer est une classe RAII appelant Initialize dans son constructeur et Uninitialize dans son destructeur.
	// Autrement dit, une fois ceci fait nous n'avons plus à nous soucier de la libération du moteur.
	NzInitializer<NzGraphics> nazara;
	if (!nazara)
	{
		// Une erreur s'est produite dans l'initialisation d'un des modules
		std::cout << "Failed to initialize Nazara, see NazaraLog.log for further informations" << std::endl;
		std::getchar(); // On laise le temps de voir l'erreur

		return EXIT_FAILURE;
	}

	// Nazara étant initialisé, nous pouvons créer la scène
	// Une scène représente tout ce qui est visible par une ou plusieurs caméras.
	// La plupart du temps vous n'aurez pas besoin de plus d'une scène, mais cela peut se révéler utile pour mieux
	// organiser et optimiser le rendu.
	// Par exemple, une pièce contenant une télévision, laquelle affichant des images provenant d'une Camera
	// Le rendu sera alors plus efficace en créant deux scènes, une pour la pièce et l'autre pour les images de la télé.
	// Cela diminuera le nombre de SceneNode à gérer pour chaque scène, et vous permettra même de ne pas afficher la scène
	// affichée dans la télé si cette dernière n'est pas visible dans la première scène.
	NzScene scene;

	// La première chose que nous faisons est d'ajouter un background (fond) à la scène.
	// Il en existe plusieurs types, le moteur inclut pour l'instant trois d'entre eux:
	// -ColorBackground: Une couleur unie en fond
	// -SkyboxBackground: Une skybox en fond, un cube à six faces rendu autour de la caméra (En perdant la notion de distance)
	// -TextureBackground: Une texture en fond, celle-ci sera affichée derrière la scène

	// Nous choisirons ici une Skybox, cette dernière étant l'effet le plus réussi et convenant très bien à une scène spatiale
	// Pour commencer il faut charger une texture de type cubemap, certaines images sont assemblées de cette façon,
	// comme celle que nous allons utiliser.
	// En réalité les textures "cubemap" regroupent six faces en une, pour faciliter leur utilisation.
	NzTexture* texture = new NzTexture;
	if (texture->LoadCubemapFromFile("resources/skybox-space.png"))
	{
		// Si la création du cubemap a fonctionné

		// Nous indiquons que la texture est "non-persistente", autrement dit elle sera libérée automatiquement par le moteur
		// à l'instant précis où elle ne sera plus utilisée, dans ce cas-ci, ce sera à la libération de l'objet skybox,
		// ceci arrivant lorsqu'un autre background est affecté à la scène, ou lorsque la scène sera libérée
		texture->SetPersistent(false);

		// Nous créons le background en lui affectant la texture
		NzSkyboxBackground* background = new NzSkyboxBackground(texture);

		// Nous pouvons en profiter pour paramétrer le background.
		// Cependant, nous n'avons rien de spécial à faire ici, nous pouvons donc l'envoyer à la scène.
		scene.SetBackground(background);

		// Comme indiqué plus haut, la scène s'occupera automatiquement de la libération de notre background
	}
	else
	{
		delete texture; // Le chargement a échoué, nous libérons la texture
		std::cout << "Failed to load skybox" << std::endl;
	}

	// Ensuite, nous allons rajouter un modèle à notre scène.
	// Les modèles représentent, globalement, tout ce qui est visible en trois dimensions.
	// Nous choisirons ici un vaisseau spatial (Quoi de mieux pour une scène spatiale ?)
	NzModel spaceship;

	// Une structure permettant de paramétrer le chargement des modèles
	NzModelParameters params;

	// Le format OBJ ne précise aucune échelle pour ses données, contrairement à Nazara (une unité = un mètre).
	// Comme le vaisseau est très grand (Des centaines de mètres de long), nous allons le rendre plus petit
	// pour les besoins de la démo.
	// Ce paramètre sert à indiquer la mise à l'échelle désirée lors du chargement du modèle.
	params.mesh.scale.Set(0.01f); // Un centième de la taille originelle

	// On charge ensuite le modèle depuis son fichier
	// Le moteur va charger le fichier et essayer de retrouver les fichiers associés (comme les matériaux, textures, ...)
	if (!spaceship.LoadFromFile("resources/Spaceship/spaceship.obj", params))
	{
		std::cout << "Failed to load spaceship" << std::endl;
		std::getchar();

		return EXIT_FAILURE;
	}

	// Nous voulons afficher quelques statistiques relatives au modèle, comme le nombre de sommets et de triangles
	// Pour cela, nous devons accéder au mesh (maillage 3D)
	NzMesh* mesh = spaceship.GetMesh();

	std::cout << mesh->GetVertexCount() << " sommets" << std::endl;
	std::cout << mesh->GetTriangleCount() << " triangles" << std::endl;

	// En revanche, le format OBJ ne précise pas l'utilisation d'une normal map, nous devons donc la charger manuellement
	// Pour commencer on récupère le matériau du mesh, celui-ci en possède plusieurs mais celui qui nous intéresse,
	// celui de la coque, est le second (Cela est bien entendu lié au modèle en lui-même)
	NzMaterial* material = spaceship.GetMaterial(1);

	// On lui indique ensuite le chemin vers la normal map
	if (!material->SetNormalMap("resources/Spaceship/Texture/normal.png"))
	{
		// Le chargement a échoué, peut-être le fichier n'existe pas, ou n'est pas reconnu par le moteur
		// Mais ce n'est pas une erreur critique, le rendu peut quand même se faire (Mais sera moins détaillé)
		std::cout << "Failed to load normal map" << std::endl;
	}

	// Il nous reste à attacher le modèle à la scène, ce qui se fait simplement via cet appel
	spaceship.SetParent(scene);
	// Et voilà, à partir de maintenant le modèle fait partie de la hiérarchie de la scène, et sera donc rendu avec cette dernière

	// Nous avons besoin également d'une caméra, pour des raisons évidentes, celle-ci sera à l'écart du modèle
	// regardant dans sa direction.

	// On conserve la rotation à part via des angles d'eulers pour la caméra free-fly
	NzEulerAnglesf camAngles(0.f, -20.f, 0.f);

	NzCamera camera;
	camera.SetPosition(0.f, 0.25f, 2.f); // On place la caméra à l'écart
	camera.SetRotation(camAngles);

	// Et on n'oublie pas de définir les plans délimitant le champs de vision
	// (Seul ce qui se trouvera entre les deux plans sera rendu)

	// La distance entre l'oeil et le plan éloigné
	camera.SetZFar(5000.f);

	// La distance entre l'oeil et le plan rapproché (0 est une valeur interdite car la division par zéro l'est également)
	camera.SetZNear(0.1f);

	// On indique à la scène que le viewer (Le point de vue) sera la caméra
	scene.SetViewer(camera);

	// Attention que le ratio entre les deux (zFar/zNear) doit rester raisonnable, dans le cas contraire vous risquez un phénomène
	// de "Z-Fighting" (Impossibilité de déduire quelle surface devrait apparaître en premier) sur les surfaces éloignées.

	// Il ne nous manque plus maintenant que de l'éclairage, sans quoi la scène sera complètement noire
	// Il existe trois types de lumières:
	// -DirectionalLight: Lumière infinie sans position, envoyant de la lumière dans une direction particulière
	// -PointLight: Lumière située à un endroit précis, envoyant de la lumière finie dans toutes les directions
	// -SpotLight: Lumière située à un endroit précis, envoyant de la lumière vers un endroit donné, avec un angle de diffusion

	// Nous choisissons une lumière directionnelle représentant la nébuleuse de notre skybox
	NzLight nebulaLight(nzLightType_Directional);

	// Il nous faut ensuite configurer la lumière
	// Pour commencer, sa couleur, la nébuleuse étant d'une couleur jaune, j'ai choisi ces valeurs
	nebulaLight.SetColor(NzColor(255, 182, 90));

	// Nous appliquons ensuite une rotation de sorte que la lumière dans la même direction que la nébuleuse
	nebulaLight.SetRotation(NzEulerAnglesf(0.f, 102.f, 0.f));

	// Et nous ajoutons la lumière à la scène
	nebulaLight.SetParent(scene);

	// Nous allons maintenant créer la fenêtre, dans laquelle nous ferons nos rendus
	// Celle-ci demande des paramètres plus complexes

	// Pour commencer le mode vidéo, celui-ci va définir la taille de la zone de rendu et le nombre de bits par pixels
	NzVideoMode mode = NzVideoMode::GetDesktopMode(); // Nous récupérons le mode vidéo du bureau

	// Nous allons prendre les trois quarts de la résolution du bureau pour notre fenêtre
	mode.width *= 3.f/4.f;
	mode.height *= 3.f/4.f;

	// Maintenant le titre, rien de plus simple...
	NzString windowTitle = "Nazara Demo - First scene";

	// Ensuite, le "style" de la fenêtre, possède-t-elle des bordures, peut-on cliquer sur la croix de fermeture,
	// peut-on la redimensionner, ...
	nzWindowStyleFlags style = nzWindowStyle_Default; // Nous prenons le style par défaut, autorisant tout ce que je viens de citer

	// Ensuite, les paramètres du contexte de rendu
	// On peut configurer le niveau d'antialiasing, le nombre de bits du depth buffer et le nombre de bits du stencil buffer
	// Nous désirons avoir un peu d'antialiasing (4x), les valeurs par défaut pour le reste nous conviendrons très bien
	NzRenderTargetParameters parameters;
	parameters.antialiasingLevel = 4;

	NzRenderWindow window(mode, windowTitle, style, parameters);
	if (!window.IsValid())
	{
		std::cout << "Failed to create render window" << std::endl;
		std::getchar();

		return EXIT_FAILURE;
	}

	// On fait disparaître le curseur de la souris
	window.SetCursor(nzWindowCursor_None);

	// On lie la caméra à la fenêtre
	camera.SetTarget(window);

	// Et on créé deux horloges pour gérer le temps
	NzClock secondClock, updateClock;

	// Ainsi qu'un compteur de FPS improvisé
	unsigned int fps = 0;

	// Quelques variables de plus pour notre caméra
	bool smoothMovement = true;
	NzVector3f targetPos = camera.GetPosition();

	// Début de la boucle de rendu du programme
	while (window.IsOpen())
	{
		// Ensuite nous allons traiter les évènements (Étape indispensable pour la fenêtre)
		NzEvent event;
		while (window.PollEvent(&event))
		{
			switch (event.type)
			{
				case nzEventType_MouseMoved: // La souris a bougé
				{
					// Gestion de la caméra free-fly (Rotation)
					float sensitivity = 0.3f; // Sensibilité de la souris

					// On modifie l'angle de la caméra grâce au déplacement relatif sur X de la souris
					camAngles.yaw = NzNormalizeAngle(camAngles.yaw - event.mouseMove.deltaX*sensitivity);

					// Idem, mais pour éviter les problèmes de calcul de la matrice de vue, on restreint les angles
					camAngles.pitch = NzClamp(camAngles.pitch - event.mouseMove.deltaY*sensitivity, -89.f, 89.f);

					// On applique les angles d'Euler à notre caméra
					camera.SetRotation(camAngles);

					// Pour éviter que le curseur ne sorte de l'écran, nous le renvoyons au centre de la fenêtre
					// Cette fonction est codée de sorte à ne pas provoquer d'évènement MouseMoved
					NzMouse::SetPosition(window.GetWidth()/2, window.GetHeight()/2, window);
					break;
				}

				case nzEventType_Quit: // L'utilisateur a cliqué sur la croix, ou l'OS veut terminer notre programme
					window.Close(); // On demande la fermeture de la fenêtre (Qui aura lieu au prochain tour de boucle)
					break;

				case nzEventType_KeyPressed: // Une touche a été pressée !
					if (event.key.code == NzKeyboard::Key::Escape)
						window.Close();
					else if (event.key.code == NzKeyboard::F1)
					{
						if (smoothMovement)
						{
							targetPos = camera.GetPosition();
							smoothMovement = false;
						}
						else
							smoothMovement = true;
					}
					break;

				default:
					break;
			}
		}

		// Mise à jour (Caméra)
		if (updateClock.GetMilliseconds() >= 1000/60) // 60 fois par seconde
		{
			// Le temps écoulé depuis la dernière fois que ce bloc a été exécuté
			float elapsedTime = updateClock.GetSeconds();

			// Vitesse de déplacement de la caméra
			float cameraSpeed = 3.f * elapsedTime; // Trois mètres par seconde

			// Si la touche espace est enfoncée, notre vitesse de déplacement est multipliée par deux
			if (NzKeyboard::IsKeyPressed(NzKeyboard::Space))
				cameraSpeed *= 2.f;

			// Pour que nos déplacement soient liés à la rotation de la caméra, nous allons utiliser
			// les directions locales de la caméra

			// Si la flèche du haut ou la touche Z (vive ZQSD) est pressée, on avance
			if (NzKeyboard::IsKeyPressed(NzKeyboard::Up) || NzKeyboard::IsKeyPressed(NzKeyboard::Z))
				targetPos += camera.GetForward() * cameraSpeed;

			// Si la flèche du bas ou la touche S est pressée, on recule
			if (NzKeyboard::IsKeyPressed(NzKeyboard::Down) || NzKeyboard::IsKeyPressed(NzKeyboard::S))
				targetPos += camera.GetBackward() * cameraSpeed;

			// Etc...
			if (NzKeyboard::IsKeyPressed(NzKeyboard::Left) || NzKeyboard::IsKeyPressed(NzKeyboard::Q))
				targetPos += camera.GetLeft() * cameraSpeed;

			// Etc...
			if (NzKeyboard::IsKeyPressed(NzKeyboard::Right) || NzKeyboard::IsKeyPressed(NzKeyboard::D))
				targetPos += camera.GetRight() * cameraSpeed;

			// Majuscule pour monter, notez l'utilisation d'une direction globale (Non-affectée par la rotation)
			if (NzKeyboard::IsKeyPressed(NzKeyboard::LShift) || NzKeyboard::IsKeyPressed(NzKeyboard::RShift))
				targetPos += NzVector3f::Up() * cameraSpeed;

			// Contrôle (Gauche ou droite) pour descendre dans l'espace global, etc...
			if (NzKeyboard::IsKeyPressed(NzKeyboard::LControl) || NzKeyboard::IsKeyPressed(NzKeyboard::RControl))
				targetPos += NzVector3f::Down() * cameraSpeed;

			camera.SetPosition((smoothMovement) ? DampedString(camera.GetPosition(), targetPos, elapsedTime) : targetPos, nzCoordSys_Global);

			// On relance l'horloge
			updateClock.Restart();
		}

		// Rendu de la scène:
		// On procède maintenant au rendu de la scène en elle-même, celui-ci se décompose en quatre étapes distinctes

		// Pour commencer, on met à jour la scène, ceci appelle la méthode Update de tous les SceneNode enregistrés
		// pour la mise à jour globale (Scene::RegisterForUpdate)
		scene.Update();

		// Ensuite il y a le calcul de visibilité, la scène se sert de la caméra active pour effectuer un test de visibilité
		// afin de faire une liste des SceneNode visibles (Ex: Frustum culling)
		scene.Cull();

		// Ensuite il y a la mise à jour des SceneNode enregistrés pour la mise à jour visible (Exemple: Terrain)
		scene.UpdateVisible();

		// Pour terminer, il y a l'affichage en lui-même, de façon organisée et optimisée (Batching)
		scene.Draw();

		// Après avoir dessiné sur la fenêtre, il faut s'assurer qu'elle affiche cela
		// Cet appel ne fait rien d'autre qu'échanger les buffers de rendu (Double Buffering)
		window.Display();

		// On incrémente le compteur de FPS improvisé
		fps++;

		if (secondClock.GetMilliseconds() >= 1000) // Toutes les secondes
		{
			// Et on insère ces données dans le titre de la fenêtre
			window.SetTitle(windowTitle + " - " + NzString::Number(fps) + " FPS");

			/*
			Note: En C++11 il est possible d'insérer de l'Unicode de façon standard, quel que soit l'encodage du fichier,
			via quelque chose de similaire à u8"Cha\u00CEne de caract\u00E8res".
			Cependant, si le code source est encodé en UTF-8 (Comme c'est le cas dans ce fichier),
			cela fonctionnera aussi comme ceci : "Chaîne de caractères".
			*/

			// Et on réinitialise le compteur de FPS
			fps = 0;

			// Et on relance l'horloge pour refaire ça dans une seconde
			secondClock.Restart();
		}
	}

    return EXIT_SUCCESS;
}
Exemplo n.º 3
0
bool NzMD5AnimParser::Parse(NzAnimation* animation)
{
	while (Advance(false))
	{
		switch (m_currentLine[0])
		{
			#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
			case 'M': // MD5Version
				if (m_currentLine.GetWord(0) != "MD5Version")
					UnrecognizedLine();
				break;
			#endif

			case 'b': // baseframe/bounds
				if (m_currentLine.StartsWith("baseframe {"))
				{
					if (!ParseBaseframe())
					{
						Error("Failed to parse baseframe");
						return false;
					}
				}
				else if (m_currentLine.StartsWith("bounds {"))
				{
					if (!ParseBounds())
					{
						Error("Failed to parse bounds");
						return false;
					}
				}
				#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
				else
					UnrecognizedLine();
				#endif
				break;

			#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
			case 'c': // commandline
				if (m_currentLine.GetWord(0) != "commandline")
					UnrecognizedLine();
				break;
			#endif

			case 'f':
			{
				unsigned int index;
				if (std::sscanf(&m_currentLine[0], "frame %u {", &index) == 1)
				{
					if (m_frameIndex != index)
					{
						Error("Unexpected frame index (expected " + NzString::Number(m_frameIndex) + ", got " + NzString::Number(index) + ')');
						return false;
					}

					if (!ParseFrame())
					{
						Error("Failed to parse frame");
						return false;
					}

					m_frameIndex++;
				}
				else if (std::sscanf(&m_currentLine[0], "frameRate %u", &m_frameRate) != 1)
				{
					#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
					UnrecognizedLine();
					#endif
				}
				break;
			}

			case 'h': // hierarchy
				if (m_currentLine.StartsWith("hierarchy {"))
				{
					if (!ParseHierarchy())
					{
						Error("Failed to parse hierarchy");
						return false;
					}
				}
				#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
				else
					UnrecognizedLine();
				#endif
				break;

			case 'n': // num[Frames/Joints]
			{
				unsigned int count;
				if (std::sscanf(&m_currentLine[0], "numAnimatedComponents %u", &count) == 1)
				{
					#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
					if (!m_animatedComponents.empty())
						Warning("Animated components count is already defined");
					#endif

					m_animatedComponents.resize(count);
				}
				else if (std::sscanf(&m_currentLine[0], "numFrames %u", &count) == 1)
				{
					#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
					if (!m_frames.empty())
						Warning("Frame count is already defined");
					#endif

					m_frames.resize(count);
				}
				else if (std::sscanf(&m_currentLine[0], "numJoints %u", &count) == 1)
				{
					#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
					if (!m_joints.empty())
						Warning("Joint count is already defined");
					#endif

					m_joints.resize(count);
				}
				#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
				else
					UnrecognizedLine();
				#endif
				break;
			}

			default:
				#if NAZARA_UTILITY_STRICT_RESOURCE_PARSING
				UnrecognizedLine();
				#endif
				break;
		}
	}

	unsigned int frameCount = m_frames.size();
	if (frameCount == 0)
	{
		NazaraError("Frame count is invalid or missing");
		return false;
	}

	unsigned int jointCount = m_joints.size();
	if (jointCount == 0)
	{
		NazaraError("Joint count is invalid or missing");
		return false;
	}

	if (m_frameIndex != frameCount)
	{
		NazaraError("Missing frame infos: [" + NzString::Number(m_frameIndex) + ',' + NzString::Number(frameCount) + ']');
		return false;
	}

	if (m_frameRate == 0)
	{
		NazaraWarning("Framerate is either invalid or missing, assuming a default value of 24");
		m_frameRate = 24;
	}

	// À ce stade, nous sommes censés avoir assez d'informations pour créer l'animation
	if (!animation->CreateSkeletal(frameCount, jointCount))
	{
		NazaraError("Failed to create animation");
		return false;
	}

	NzSequence sequence;
	sequence.firstFrame = 0;
	sequence.frameCount = m_frames.size();
	sequence.frameRate = m_frameRate;
	sequence.name = m_stream.GetPath().SubStringFrom(NAZARA_DIRECTORY_SEPARATOR, -1, true);
	if (!animation->AddSequence(sequence))
		NazaraWarning("Failed to add sequence");

	NzSequenceJoint* sequenceJoints = animation->GetSequenceJoints();

	// Pour que le squelette soit correctement aligné, il faut appliquer un quaternion "de correction" aux joints à la base du squelette
	NzQuaternionf rotationQuat = NzEulerAnglesf(-90.f, 90.f, 0.f);
	for (unsigned int i = 0; i < jointCount; ++i)
	{
		int parent = m_joints[i].parent;
		for (unsigned int j = 0; j < frameCount; ++j)
		{
			NzSequenceJoint& sequenceJoint = sequenceJoints[j*jointCount + i];

			if (parent >= 0)
			{
				sequenceJoint.position = m_frames[j].joints[i].pos;
				sequenceJoint.rotation = m_frames[j].joints[i].orient;
			}
			else
			{
				sequenceJoint.position = rotationQuat * m_frames[j].joints[i].pos;
				sequenceJoint.rotation = rotationQuat * m_frames[j].joints[i].orient;
			}

			sequenceJoint.scale = NzVector3f(1.f, 1.f, 1.f);
		}
	}

	return true;
}