Josh Posted April 9 Share Posted April 9 I am considering implementing our own file format and using a pipeline more like Leadwerks, where glTF and FBX models get automatically converted by the editor. You can still use glTF files for your final game files if you wish, and the converter can be disabled in the program settings if you wish. The file format is designed to be very simple to read and write, while loading fast enough for game use. Features: LODs Skeletal animation External material files Vertex morphs User-defined entity properties Embedded collider Embedded picking structure I think it is easiest to understand the file format just by looking at the loading code: #include "UltraEngine.h" using namespace UltraEngine; namespace UltraEngine::Core { String G3DModelLoader::ReadText(shared_ptr<Stream> stream) { int len = stream->ReadInt(); auto pos = stream->GetPosition(); String s; if (len) { s = stream->ReadString(len); stream->Seek(pos + len); } return s; } bool G3DModelLoader::Reload(shared_ptr<Stream> stream, shared_ptr<Object> o, const LoadFlags flags) { auto modelbase = o->As<ModelBase>(); if (modelbase == NULL) return false; modelbase->model = CreateModel(NULL); auto model = modelbase->model->As<Model>(); if (stream->ReadString(4) != "G3D") return false; int version = stream->ReadInt(); if (version != 100) { Print("Error: G3D version " + String(version) + " not supported"); return false; } return LoadNode(stream, model, flags); } bool G3DModelLoader::LoadNode(shared_ptr<Stream> stream, shared_ptr<Model> model, const LoadFlags flags) { Vec3 pos, scale; Quat rot; String s; if (stream->ReadString(4) != "NODE") return false; model->name = ReadText(stream); model->properties = ParseJson(ReadText(stream)); ParseJson(ReadText(stream)); pos.x = stream->ReadFloat(); pos.y = stream->ReadFloat(); pos.z = stream->ReadFloat(); rot.x = stream->ReadFloat(); rot.y = stream->ReadFloat(); rot.z = stream->ReadFloat(); rot.w = stream->ReadFloat(); scale.x = stream->ReadFloat(); scale.y = stream->ReadFloat(); scale.z = stream->ReadFloat(); model->SetPosition(pos); model->SetRotation(rot); model->SetScale(scale); int countlods = stream->ReadInt(); for (int level = 0; level < countlods; ++level) { if (not LoadLod(stream, model, level, flags)) return false; } int countkids = stream->ReadInt(); for (int n = 0; n < countkids; ++n) { auto child = CreateModel(NULL); child->SetParent(model); if (not LoadNode(stream, child, flags)) return false; } // Animations int animcount = stream->ReadInt(); for (int n = 0; n < animcount; ++n) { stream->ReadInt();// length stream->ReadFloat();// speed auto name = ReadText(stream); } // Skeleton int bones = stream->ReadInt(); if (bones) { if (model->GetParent()) { Print("Error: Skeleton can only appear in the model root node"); return false; } if (bones != 1) { Print("Error: Skeleton root must have one bone"); return false; } auto skeleton = CreateSkeleton(nullptr); model->SetSkeleton(skeleton); for (int n = 0; n < bones; ++n) { auto bone = std::make_shared<Bone>(nullptr, skeleton); LoadBone(stream, skeleton, bone, animcount, flags); } skeleton->UpdateSkinning(); model->SetSkeleton(skeleton); } // Collider int partscount = stream->ReadInt(); model->UpdateBounds(); return true; } bool G3DModelLoader::LoadLod(shared_ptr<Stream> stream, shared_ptr<Model> model, const int level, const LoadFlags flags) { if (stream->ReadString(4) != "LOD ") return false; if (level >= model->lods.size()) model->AddLod(); float loddistance = stream->ReadFloat(); int countmeshes = stream->ReadInt(); for (int m = 0; m < countmeshes; ++m) { if (not LoadMesh(stream, model, level, flags)) return false; } return true; } bool G3DModelLoader::LoadMesh(shared_ptr<Stream> stream, shared_ptr<Model> model, const int level, const LoadFlags flags) { if (stream->ReadString(4) != "MESH") return false; MeshPrimitives type = MeshPrimitives(stream->ReadInt()); if (type < 1 or type > 4) { Print("Error: Mesh type must be between one and four"); return false; } auto mesh = model->AddMesh(type, level); mesh->name = ReadText(stream); WString mtlpath = ReadText(stream); if (not mtlpath.empty()) { if (mtlpath.Left(2) == "./" and not stream->path.empty()) { mtlpath = ExtractDir(stream->path) + "/" + mtlpath; } auto mtl = LoadMaterial(mtlpath); if (mtl) mesh->SetMaterial(mtl); } int vertexstride = stream->ReadInt(); if (vertexstride != 84) return false; int vertexcount = stream->ReadInt(); mesh->m_vertices.resize(vertexcount); for (int v = 0; v < vertexcount; ++v) { mesh->m_vertices[v].position.x = stream->ReadFloat(); mesh->m_vertices[v].position.y = stream->ReadFloat(); mesh->m_vertices[v].position.z = stream->ReadFloat(); mesh->m_vertices[v].normal.x = stream->ReadFloat(); mesh->m_vertices[v].normal.y = stream->ReadFloat(); mesh->m_vertices[v].normal.z = stream->ReadFloat(); mesh->m_vertices[v].texcoords.x = stream->ReadFloat(); mesh->m_vertices[v].texcoords.y = stream->ReadFloat(); mesh->m_vertices[v].texcoords.z = stream->ReadFloat(); mesh->m_vertices[v].texcoords.w = stream->ReadFloat(); mesh->m_vertices[v].color.r = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].color.g = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].color.b = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].color.a = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].displacement = stream->ReadFloat(); mesh->m_vertices[v].tangent.x = stream->ReadFloat(); mesh->m_vertices[v].tangent.y = stream->ReadFloat(); mesh->m_vertices[v].tangent.z = stream->ReadFloat(); mesh->m_vertices[v].bitangent.x = stream->ReadFloat(); mesh->m_vertices[v].bitangent.y = stream->ReadFloat(); mesh->m_vertices[v].bitangent.z = stream->ReadFloat(); mesh->m_vertices[v].boneindices[0] = stream->ReadShort(); mesh->m_vertices[v].boneindices[1] = stream->ReadShort(); mesh->m_vertices[v].boneindices[2] = stream->ReadShort(); mesh->m_vertices[v].boneindices[3] = stream->ReadShort(); mesh->m_vertices[v].boneweights.x = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].boneweights.y = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].boneweights.z = float(stream->ReadByte()) / 255.0f; mesh->m_vertices[v].boneweights.w = float(stream->ReadByte()) / 255.0f; } int indicesize = stream->ReadInt(); int indicecount = stream->ReadInt(); uint32_t index; switch (indicesize) { case 2: mesh->m_indices.reserve(indicecount); for (int i = 0; i < indicecount; ++i) mesh->AddIndice(stream->ReadShort()); break; case 4: mesh->m_indices.resize(indicecount); stream->Read(mesh->m_indices.data(), indicecount * sizeof(mesh->indices[0])); break; default: return false; } // Pick structure cache int pickcachesize = stream->ReadInt(); if (pickcachesize) stream->Seek(stream->GetPosition() + pickcachesize); //Vertex morphs int morphcount = stream->ReadInt(); for (int m = 0; m < morphcount; ++m) { if (stream->ReadString(4) != "MORP") return false; if (stream->ReadInt() != 48) return false; for (int v = 0; v < vertexcount; ++v) { // Position stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); // Normal stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); // Tangent stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); // Bitangent stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); } } mesh->UpdateBounds(); return true; } bool G3DModelLoader::LoadBone(shared_ptr<Stream> stream, shared_ptr<Skeleton> skeleton, shared_ptr<Bone> bone, const int animcount, const LoadFlags flags) { bone->name = ReadText(stream); bone->position.x = stream->ReadFloat(); bone->position.y = stream->ReadFloat(); bone->position.z = stream->ReadFloat(); bone->quaternion.x = stream->ReadFloat(); bone->quaternion.y = stream->ReadFloat(); bone->quaternion.z = stream->ReadFloat(); bone->quaternion.w = stream->ReadFloat(); bone->scale = stream->ReadFloat(); stream->ReadFloat();// scale y stream->ReadFloat();// scale z int count = stream->ReadInt(); if (count != animcount) { Print("Error: Bone animation count must match that of the root node"); return false; } for (int anim = 0; anim < count; ++anim) { if (stream->ReadString(4) != "ANIM") return false; int keyflags = stream->ReadInt(); int keyframes = stream->ReadInt(); if (keyflags) { for (int k = 0; k < keyframes; ++k) { if ((1 & keyflags) != 0) { stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); } if ((2 & keyflags) != 0) { stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); } if ((4 & keyflags) != 0) { stream->ReadFloat(); stream->ReadFloat(); stream->ReadFloat(); } } } } return true; } } 2 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 9 Share Posted April 9 I think this is a great idea. 🙂 Quote Link to comment Share on other sites More sharing options...
Josh Posted April 9 Author Share Posted April 9 How hard would it be to export to this format from Blender? Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 9 Share Posted April 9 I dare say it's not hard, its just time consuming. The main issue I had was finding out the right stuff to access in blender from python, but google was the answer there. There's also the Leadwerks mdl exporter for blender. Maybe that could be converted to suit this rather than starting again? Quote Link to comment Share on other sites More sharing options...
Josh Posted April 9 Author Share Posted April 9 I have it working now: https://github.com/UltraEngine/G3D/blob/main/README.md 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
Josh Posted April 9 Author Share Posted April 9 15 hours ago, SpiderPig said: I dare say it's not hard, its just time consuming. The main issue I had was finding out the right stuff to access in blender from python, but google was the answer there. There's also the Leadwerks mdl exporter for blender. Maybe that could be converted to suit this rather than starting again? I thought you had a working exporter your wrote yourself? Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 9 Share Posted April 9 Oh I do, but it only does static geometry right now. I don't know how hard animation will be. Quote Link to comment Share on other sites More sharing options...
Josh Posted April 9 Author Share Posted April 9 Yeah, but if we got a static exporter set up, that would cover 95% of use cases. Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 9 Share Posted April 9 That's right. I've pretty much got that then. All I'd have to do is change the extension and add the missing data structures. Seeing as Blender doesn't support BC7 I was thinking of auto converting any png textures to the correct dds format on export with a small program made with ultra. Quote Link to comment Share on other sites More sharing options...
Josh Posted April 9 Author Share Posted April 9 I guess just go without whatever format is in use. The fact that nobody understand or even knows what BC7 is probably means it isn't that important in the grand scheme of things. If someone wants BC7, we can do an auto-converter in the editor from an image file. It would be better to have something simple and reliable that works now than to have something that produces perfect results but breaks easily. 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
Josh Posted April 15 Author Share Posted April 15 Changes: Animations block will be preceded with the tag "ASET" "COLL" tag renamed to "PHYS" Vertex stride must be 88 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
Josh Posted April 15 Author Share Posted April 15 Here is my loading code. I think this is final. A new section was added for node attachments to bones ("ATCH"): G3DModelLoader.cpp 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 15 Share Posted April 15 Just a minor error in the loader. If the stride is wrong it gives the wrong info. if (vertexstride != 88) { Print("Vertext stride must be 84"); return false; } Quote Link to comment Share on other sites More sharing options...
SpiderPig Posted April 15 Share Posted April 15 Why is the stride now 88? I don't see any extra information saved, is it for future development? Quote Link to comment Share on other sites More sharing options...
SpiderPig Posted April 16 Share Posted April 16 @Josh I'm giving animation a go and just need to confirm a few things to help me debug; In LoadBone(), is the bone position; Global coords Local to skeleton Or, Local to the parent bone Is bone->quaternion; The rotation of the bone toward the position of it's child? For example, blender only stores the tail and head positions of bones when they are created. No matter the direction the bones are facing when they are created, they have a rotation of 0 on all axis. E.g. All the bones in this shot have a rotation of 0 an all axis; Do I need to calculate what these rotations would be for Ultra? You are correct. Animation is hard. Quote Link to comment Share on other sites More sharing options...
Josh Posted April 16 Author Share Posted April 16 Bone rotations are quaternions in local space (relative to the parent). 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 16 Share Posted April 16 Thanks. Bone positions the same? Local to parent? Quote Link to comment Share on other sites More sharing options...
Josh Posted April 16 Author Share Posted April 16 Yes. 1 Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
Josh Posted April 17 Author Share Posted April 17 Here is how colliders are stored. For tolerance and optimization, if you don't know just write 0.0 / 0: // Collider if (stream->ReadString(4) != "PHYS") { auto pos = stream->GetPosition() - 4; Print("Error: Expected PHYS tag at position " + String(pos)); return false; } int colliderdatasize = stream->ReadInt(); auto colliderstartposition = stream->GetPosition(); if (colliderdatasize) { Vec3 position, scale, euler; Quat rotation; std::vector<shared_ptr<Collider> > parts; int partcount = stream->ReadInt(); for (int n = 0; n < partcount; ++n) { if (stream->ReadString(4) != "PART") { auto pos = stream->GetPosition() - 4; Print("Error: Expected PART tag at position " + String(pos)); return false; } shared_ptr<Collider> part; auto tag = stream->ReadString(4); if (tag == "HULL") { float tol = stream->ReadFloat();// tolerance std::vector<Vec3> points; Vec3 p; int count = stream->ReadInt(); for (int n = 0; n < count; ++n) { p.x = stream->ReadFloat(); p.y = stream->ReadFloat(); p.z = stream->ReadFloat(); points.push_back(p); } part = CreateConvexHullCollider(points); part->tolerance = tol; } else if (tag == "MESH") { int opt = stream->ReadInt();// optimize flag int count = stream->ReadInt(); int vcount; std::vector<Vec3> face; Vec3 p; std::vector<std::vector<Vec3> > meshfaces; for (int n = 0; n < count; ++n) { vcount = stream->ReadInt(); face.clear(); for (int v = 0; v < vcount; ++v) { p.x = stream->ReadFloat(); p.y = stream->ReadFloat(); p.z = stream->ReadFloat(); face.push_back(p); } meshfaces.push_back(face); } part = CreateMeshCollider(meshfaces); part->optimizemesh = opt; } else { position.x = stream->ReadFloat(); position.y = stream->ReadFloat(); position.z = stream->ReadFloat(); rotation.x = stream->ReadFloat(); rotation.y = stream->ReadFloat(); rotation.z = stream->ReadFloat(); rotation.w = stream->ReadFloat(); scale.x = stream->ReadFloat(); scale.y = stream->ReadFloat(); scale.z = stream->ReadFloat(); if (tag == "BOX_") { part = CreateBoxCollider(scale, position, rotation.Euler()); } else if (tag == "CYLI") { part = CreateCylinderCollider(scale.x, scale.y, position, rotation.Euler()); } else if (tag == "CONE") { part = CreateConeCollider(scale.x, scale.y, position, rotation.Euler()); } else if (tag == "CCYL") { part = CreateChamferCylinderCollider(scale.x, scale.y, position, rotation.Euler()); } else if (tag == "CAPS") { part = CreateCapsuleCollider(scale.x, scale.y, position, rotation.Euler()); } else if (tag == "SPHE") { part = CreateSphereCollider(scale.x, position); } else { Print("Error: Unknown collider type \"" + tag + "\""); return false; } } if (part) parts.push_back(part); } if (parts.size()) { if (parts.size() == 1) { model->SetCollider(parts[0]); } else { auto c = CreateCompoundCollider(parts); model->SetCollider(c); } } stream->Seek(colliderstartposition + colliderdatasize); } Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 19 Share Posted April 19 I don't quite understand the exactly what's needed in the bone data. I think that's the only issue I have left. This is going to be hard to explain, so bare with me. I understand that both bone position and rotation should be local to their parent. What I was doing is getting the local bone position like this. bone_local_pos = bone.global_position - bone.parent.global_postition However that only works if the quaternions are left as this. I knew from the start this probably wasn't correct. quat.x = 0.0 quat.y = 0.0 quat.z = 0.0 quat.w = 1.0 I think what I need to is rather than setting the bone position to the "yellow dot" relative to the "red dot." I need to calculate the "blue dot", and then the "quaternion will be the "orange angle". The "blue dot" will basically be the normal the parent bone is pointing * the length of the child bone. This is the only way I see Ultra being able to give the correct bone position with bone rotation. Am I on the right track or have I utterly confused you? Quote Link to comment Share on other sites More sharing options...
Josh Posted April 19 Author Share Posted April 19 So blender only gives you a bone's orientation in global space? You have to multiply that by the inverse of the parent's 4x4 matrix to get the local orientation. Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
Josh Posted April 19 Author Share Posted April 19 There is nothing like bone.local_position or bone.position? Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 19 Share Posted April 19 There is a local matrix that has the bones position local to its parent. I've since disabled converting the matrices to Y up so I'm left with raw blender data. I think I understand what's going on now.... we'll see. Quote Link to comment Share on other sites More sharing options...
Josh Posted April 19 Author Share Posted April 19 Can you get the local rotation as a quaternion? You might look at the old Leadwerks exporter and see what the author did there. Quote My job is to make tools you love, with the features you want, and performance you can't live without. Link to comment Share on other sites More sharing options...
SpiderPig Posted April 19 Share Posted April 19 I can convert the local matrix to a quaternion. I've been looking over the Leadwerks mdl exporter too, although half the stuff no longer works the same in blender 4.0. Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.