diff --git a/src/r_data/models/models_obj.cpp b/src/r_data/models/models_obj.cpp index e754d2daa..65abfd7c2 100644 --- a/src/r_data/models/models_obj.cpp +++ b/src/r_data/models/models_obj.cpp @@ -23,322 +23,485 @@ #include "cmdlib.h" #include "r_data/models/models_obj.h" +/** + * Load an OBJ model + * + * @param fn The path to the model file + * @param lumpnum The lump index in the wad collection + * @param buffer The contents of the model file + * @param length The size of the model file + */ bool FOBJModel::Load(const char* fn, int lumpnum, const char* buffer, int length) { - FString objName = Wads.GetLumpFullPath(lumpnum); - sc.OpenMem(objName, buffer, length); - Printf("Parsing %s\n", objName.GetChars()); - while(sc.GetString()) - { - if (sc.Compare("#")) // Line comment - { - sc.Line += 1; // I don't think this does anything, though... - } - else if (sc.Compare("v")) // Vertex - { - ParseVector3(this->verts); - } - else if (sc.Compare("vn")) // Vertex normal - { - ParseVector3(this->norms); - } - else if (sc.Compare("vt")) // UV Coordinates - { - ParseVector2(this->uvs); - } - else if (sc.Compare("usemtl")) - { - // Get material name and try to load it - sc.MustGetString(); + FString objName = Wads.GetLumpFullPath(lumpnum); + FString objBuf(buffer, length); - curMtl = LoadSkin("", sc.String); - if (!curMtl.isValid()) - { - // Relative to model file path? - curMtl = LoadSkin(fn, sc.String); - } + // Do some replacements before we parse the OBJ string + { + // Ensure usemtl statements remain intact + TArray mtlUsages; + TArray mtlUsageIdxs; + long bpos = 0, nlpos = 0, slashpos = 0; + while (1) + { + bpos = objBuf.IndexOf("\nusemtl", bpos); + if (bpos == -1) break; + slashpos = objBuf.IndexOf('/', bpos); + nlpos = objBuf.IndexOf('\n', ++bpos); + if (slashpos > nlpos || slashpos == -1) + { + continue; + } + if (nlpos == -1) + { + nlpos = objBuf.Len(); + } + FString lineStr(objBuf.GetChars() + bpos, nlpos - bpos); + mtlUsages.Push(lineStr); + mtlUsageIdxs.Push(bpos); + } - if (!curMtl.isValid()) - { - // Don't use materials that don't exist - continue; - } - // Most OBJs are sorted by material, but just in case the one we're loading isn't... - // Build surface... - if (curSurface == nullptr) - { - // First surface - curSurface = new OBJSurface(curMtl); - } - else - { - if (curSurfFaceCount > 0) - { - // Search for existing surface with current material - /* - for(size_t i = 0; i < surfaces.Size(); i++) - { - if (surfaces[i].skin == curMtl) - { - curSurface = &surfaces[i]; - surfExists = true; - } - } - */ - // Add previous surface - curSurface->numFaces = curSurfFaceCount; - curSurface->faceStart = aggSurfFaceCount; - surfaces.Push(*curSurface); - delete curSurface; - // Go to next surface - curSurface = new OBJSurface(curMtl); - aggSurfFaceCount += curSurfFaceCount; - } - else - { - curSurface->skin = curMtl; - } - } - curSurfFaceCount = 0; - } - else if (sc.Compare("f")) - { - FString sides[4]; - OBJFace face; - for (int i = 0; i < 3; i++) - { - // A face must have at least 3 sides - sc.MustGetString(); - sides[i] = sc.String; - ParseFaceSide(sides[i], face, i); - } - face.sideCount = 3; - if (sc.GetString()) - { - if (!sc.Compare("f")) - { - sides[3] = sc.String; - face.sideCount += 1; - ParseFaceSide(sides[3], face, 3); - } - else - { - sc.UnGet(); // No 4th side, move back - } - } - faces.Push(face); - curSurfFaceCount += 1; - } - } - sc.Close(); + // Replace forward slashes with percent signs so they aren't parsed as line comments + objBuf.ReplaceChars('/', *newSideSep); - // No valid materials detected - if (curSurface == nullptr) - { - FTextureID dummyMtl = LoadSkin("", "-NOFLAT-"); // Built-in to GZDoom - curSurface = new OBJSurface(dummyMtl); - } - curSurface->numFaces = curSurfFaceCount; - curSurface->faceStart = aggSurfFaceCount; - surfaces.Push(*curSurface); - delete curSurface; + // Substitute broken usemtl statements with old ones + bpos = 0, nlpos = 0; + for (size_t i = 0; i < mtlUsages.Size(); i++) + { + bpos = mtlUsageIdxs[i]; + nlpos = objBuf.IndexOf('\n', bpos); + if (nlpos == -1) + { + nlpos = objBuf.Len(); + } + FString lineStr(objBuf.GetChars() + bpos, nlpos - bpos); + objBuf.Substitute(lineStr, mtlUsages[i]); + } - /* - Printf("%d vertices\n", verts.Size()); - Printf("%d normals\n", norms.Size()); - Printf("%d UVs\n", uvs.Size()); - Printf("%d faces\n", faces.Size()); - Printf("%d surfaces\n", surfaces.Size()); - */ + // Find each OBJ line comment, and convert each to a C-style line comment + while (1) + { + bpos = objBuf.IndexOf('#'); + if (bpos == -1) break; + objBuf.Remove(bpos, 1); + objBuf.Insert(bpos, "//", 2); + } + } + sc.OpenString(objName, objBuf); + //Printf("Parsing %s\n", objName.GetChars()); - mLumpNum = lumpnum; - for (size_t i = 0; i < surfaces.Size(); i++) - { - ConstructSurfaceTris(&surfaces[i]); - } - return true; + FTextureID curMtl = FNullTextureID(); + OBJSurface *curSurface = nullptr; + int aggSurfFaceCount = 0; + int curSurfFaceCount = 0; + + while(sc.GetString()) + { + if /*(sc.Compare("#")) // Line comment + { + sc.Line += 1; // I don't think this does anything, though... + } + else if*/ (sc.Compare("v")) // Vertex + { + ParseVector3(this->verts); + } + else if (sc.Compare("vn")) // Vertex normal + { + ParseVector3(this->norms); + } + else if (sc.Compare("vt")) // UV Coordinates + { + ParseVector2(this->uvs); + } + else if (sc.Compare("usemtl")) + { + // Get material name and try to load it + sc.MustGetString(); + + curMtl = LoadSkin("", sc.String); + if (!curMtl.isValid()) + { + // Relative to model file path? + curMtl = LoadSkin(fn, sc.String); + } + + // Build surface... + if (curSurface == nullptr) + { + // First surface + curSurface = new OBJSurface(curMtl); + } + else + { + if (curSurfFaceCount > 0) + { + // Add previous surface + curSurface->numFaces = curSurfFaceCount; + curSurface->faceStart = aggSurfFaceCount; + surfaces.Push(*curSurface); + delete curSurface; + // Go to next surface + curSurface = new OBJSurface(curMtl); + aggSurfFaceCount += curSurfFaceCount; + } + else + { + curSurface->skin = curMtl; + } + } + curSurfFaceCount = 0; + } + else if (sc.Compare("f")) + { + FString sides[4]; + OBJFace face; + for (int i = 0; i < 3; i++) + { + // A face must have at least 3 sides + sc.MustGetString(); + sides[i] = sc.String; + ParseFaceSide(sides[i], face, i); + } + face.sideCount = 3; + if (sc.GetString()) + { + if (!sc.Compare("f") && FString(sc.String).IndexOfAny("-0123456789") == 0) + { + sides[3] = sc.String; + face.sideCount += 1; + ParseFaceSide(sides[3], face, 3); + } + else + { + sc.UnGet(); // No 4th side, move back + } + } + faces.Push(face); + curSurfFaceCount += 1; + } + } + sc.Close(); + + if (curSurface == nullptr) + { // No valid materials detected + FTextureID dummyMtl = LoadSkin("", "-NOFLAT-"); // Built-in to GZDoom + curSurface = new OBJSurface(dummyMtl); + } + curSurface->numFaces = curSurfFaceCount; + curSurface->faceStart = aggSurfFaceCount; + surfaces.Push(*curSurface); + delete curSurface; + + if (uvs.Size() == 0) + { // Needed so that OBJs without UVs can work + uvs.Push(FVector2(0.0, 0.0)); + } + + /* + Printf("%d vertices\n", verts.Size()); + Printf("%d normals\n", norms.Size()); + Printf("%d UVs\n", uvs.Size()); + Printf("%d faces\n", faces.Size()); + Printf("%d surfaces\n", surfaces.Size()); + */ + + mLumpNum = lumpnum; + return true; } /** - * Parse a 2D vector - * - * @param start The buffer to parse from - * @param array The array to append the parsed vector to - */ + * Parse a 2D vector + * + * @param start The buffer to parse from + * @param array The array to append the parsed vector to + */ void FOBJModel::ParseVector2(TArray &array) { - float coord[2]; - for (int axis = 0; axis < 2; axis++) - { - sc.MustGetFloat(); - coord[axis] = (float)sc.Float; - } - FVector2 vec(coord); - array.Push(vec); + float coord[2]; + for (int axis = 0; axis < 2; axis++) + { + sc.MustGetFloat(); + coord[axis] = (float)sc.Float; + } + FVector2 vec(coord); + array.Push(vec); } /** - * Parse a 3D vector - * - * @param start The buffer to parse from - * @param array The array to append the parsed vector to - */ + * Parse a 3D vector + * + * @param start The buffer to parse from + * @param array The array to append the parsed vector to + */ void FOBJModel::ParseVector3(TArray &array) { - float coord[3]; - for (int axis = 0; axis < 3; axis++) - { - sc.MustGetFloat(); - coord[axis] = (float)sc.Float; - } - FVector3 vec(coord); - array.Push(vec); + float coord[3]; + for (int axis = 0; axis < 3; axis++) + { + sc.MustGetFloat(); + coord[axis] = (float)sc.Float; + } + FVector3 vec(coord); + array.Push(vec); } void FOBJModel::ParseFaceSide(const FString &sideStr, OBJFace &face, int sidx) { - OBJFaceSide side; - int origIdx; - if (sideStr.IndexOf("/") >= 0) - { - TArray sides = sideStr.Split("/"); - if (sides[0].Len() > 0) - { - origIdx = atoi(sides[0].GetChars()); - side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex); - } - if (sides[1].Len() > 0) - { - origIdx = atoi(sides[1].GetChars()); - side.uvref = ResolveIndex(origIdx, FaceElement::UVIndex); - } - if (sides.Size() > 2) - { - if (sides[2].Len() > 0) - { - origIdx = atoi(sides[2].GetChars()); - side.normref = ResolveIndex(origIdx, FaceElement::VNormalIndex); - } - } - } - else - { - origIdx = atoi(sideStr.GetChars()); - side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex); - side.normref = 0; - side.uvref = 0; - } - face.sides[sidx] = side; + OBJFaceSide side; + int origIdx; + if (sideStr.IndexOf(newSideSep) >= 0) + { + TArray sides = sideStr.Split(newSideSep, FString::TOK_KEEPEMPTY); + + if (sides[0].Len() > 0) + { + origIdx = atoi(sides[0].GetChars()); + side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex); + } + else + { + sc.ScriptError("Vertex reference is not optional!"); + } + + if (sides[1].Len() > 0) + { + origIdx = atoi(sides[1].GetChars()); + side.uvref = ResolveIndex(origIdx, FaceElement::UVIndex); + } + else + { + side.uvref = -1; + } + + if (sides.Size() > 2) + { + if (sides[2].Len() > 0) + { + origIdx = atoi(sides[2].GetChars()); + side.normref = ResolveIndex(origIdx, FaceElement::VNormalIndex); + } + else + { + side.normref = -1; + } + } + else + { + side.normref = -1; + } + } + else + { + origIdx = atoi(sideStr.GetChars()); + side.vertref = ResolveIndex(origIdx, FaceElement::VertexIndex); + side.normref = -1; + side.uvref = -1; + } + face.sides[sidx] = side; } int FOBJModel::ResolveIndex(int origIndex, FaceElement el) { - if (origIndex > 0) - { - return origIndex - 1; // OBJ indices start at 1 - } - else if (origIndex < 0) - { - if (el == FaceElement::VertexIndex) - { - return this->verts.Size() + origIndex; // origIndex is negative - } - else if (el == FaceElement::UVIndex) - { - return this->uvs.Size() + origIndex; - } - else if (el == FaceElement::VNormalIndex) - { - return this->norms.Size() + origIndex; - } - } - return 0; -} - -void FOBJModel::TriangulateQuad(const OBJFace &quad, OBJFace *tris) -{ - // if (quad.sides < 3 || quad.sides > 4) exception - // if (quad.sides == 3) return &quad; - tris[0].sideCount = 3; - tris[1].sideCount = 3; - - for (int i = 0; i < 3; i++) - { - tris[0].sides[i].vertref = quad.sides[i].vertref; - tris[1].sides[i].vertref = quad.sides[i+1].vertref; - tris[0].sides[i].uvref = quad.sides[i].uvref; - tris[1].sides[i].uvref = quad.sides[i+1].uvref; - tris[0].sides[i].normref = quad.sides[i].normref; - tris[1].sides[i].normref = quad.sides[i+1].normref; - } -} - -void FOBJModel::ConstructSurfaceTris(OBJSurface *surf) -{ - int triCount = 0; - - size_t start = surf->faceStart; - size_t end = start + surf->numFaces; - for (size_t i = start; i < end; i++) - { - triCount += (faces[i].sideCount > 3) ? 2 : 1; - } - - surf->numTris = triCount; - surf->tris = new OBJFace[triCount]; - - int quadCount = 0; - for (size_t i = start; i < end; i++) - { - surf->tris[i+quadCount].sideCount = 3; - if (faces[i].sideCount == 3) - { - memcpy(surf->tris[i+quadCount].sides, faces[i].sides, sizeof(OBJFaceSide) * 3); - } - else if (faces[i].sideCount == 4) - { - OBJFace *triangulated = new OBJFace[2]; - TriangulateQuad(faces[i], triangulated); - memcpy(surf->tris[i+quadCount].sides, triangulated->sides, sizeof(OBJFaceSide) * 3); - memcpy(surf->tris[i+quadCount+1].sides, (triangulated+1)->sides, sizeof(OBJFaceSide) * 3); - delete[] triangulated; - quadCount += 1; - } - } -} - -int FOBJModel::FindFrame(const char* name) -{ - return 0; // OBJs are not animated. -} - -// Stubs - I don't know what these do exactly -void FOBJModel::RenderFrame(FModelRenderer *renderer, FTexture * skin, int frameno, int frameno2, double inter, int translation) -{ - /* - for (int i = 0; i < numMtls; i++) - { - renderer->SetMaterial(skin, false, translation); - GetVertexBuffer(renderer)->SetupFrame(renderer, surf->vindex + frameno * surf->numVertices, surf->vindex + frameno2 * surf->numVertices, surf->numVertices); - renderer->DrawElements(surf->numTriangles * 3, surf->iindex * sizeof(unsigned int)); - } - */ + if (origIndex > 0) + { + return origIndex - 1; // OBJ indices start at 1 + } + else if (origIndex < 0) + { + if (el == FaceElement::VertexIndex) + { + return verts.Size() + origIndex; // origIndex is negative + } + else if (el == FaceElement::UVIndex) + { + return uvs.Size() + origIndex; + } + else if (el == FaceElement::VNormalIndex) + { + return norms.Size() + origIndex; + } + } + return -1; } void FOBJModel::BuildVertexBuffer(FModelRenderer *renderer) { + if (GetVertexBuffer(renderer)) + { + return; + } + unsigned int vbufsize = 0; + + for (size_t i = 0; i < surfaces.Size(); i++) + { + ConstructSurfaceTris(surfaces[i]); + surfaces[i].vbStart = vbufsize; + vbufsize += surfaces[i].numTris * 3; + } + + auto vbuf = renderer->CreateVertexBuffer(false,true); + SetVertexBuffer(renderer, vbuf); + + FModelVertex *vertptr = vbuf->LockVertexBuffer(vbufsize); + + for (size_t i = 0; i < surfaces.Size(); i++) + { + for (size_t j = 0; j < surfaces[i].numTris; j++) + { + for (size_t side = 0; side < 3; side++) + { + FModelVertex *mdv = vertptr + + side + j * 3 + // Current surface and previous triangles + surfaces[i].vbStart; // Previous surfaces + + OBJFaceSide &curSide = surfaces[i].tris[j].sides[side]; + + int vidx = curSide.vertref; + int uvidx = (curSide.uvref >= 0 && curSide.uvref < uvs.Size()) ? curSide.uvref : 0; + int nidx = curSide.normref; + + mdv->Set(verts[vidx].X, verts[vidx].Y, verts[vidx].Z, uvs[uvidx].X, uvs[uvidx].Y * -1); + + if (nidx >= 0 && nidx < norms.Size()) + { + mdv->SetNormal(norms[nidx].X, norms[nidx].Y, norms[nidx].Z); + } + else + { + // https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal + // Find other sides of triangle + int nextSidx = side + 1; + if (nextSidx >= 3) nextSidx -= 3; + + int lastSidx = side + 2; + if (lastSidx >= 3) lastSidx -= 3; + + OBJFaceSide &nextSide = surfaces[i].tris[j].sides[nextSidx]; + OBJFaceSide &lastSide = surfaces[i].tris[j].sides[lastSidx]; + + // Cross-multiply the U-vector and V-vector + FVector3 uvec = verts[nextSide.vertref] - verts[curSide.vertref]; + FVector3 vvec = verts[lastSide.vertref] - verts[curSide.vertref]; + + FVector3 nvec = uvec ^ vvec; + mdv->SetNormal(nvec.X, nvec.Y, nvec.Z); + } + } + } + } + + vbuf->UnlockVertexBuffer(); +} + +void FOBJModel::ConstructSurfaceTris(OBJSurface &surf) +{ + int triCount = 0; + + size_t start = surf.faceStart; + size_t end = start + surf.numFaces; + for (size_t i = start; i < end; i++) + { + triCount += faces[i].sideCount - 2; + } + + surf.numTris = triCount; + surf.tris = new OBJFace[triCount]; + + for (size_t i = start, triIdx = 0; i < end; i++, triIdx++) + { + surf.tris[triIdx].sideCount = 3; + if (faces[i].sideCount == 3) + { + memcpy(surf.tris[triIdx].sides, faces[i].sides, sizeof(OBJFaceSide) * 3); + } + else if (faces[i].sideCount == 4) // Triangulate face + { + OBJFace *triangulated = new OBJFace[2]; + TriangulateQuad(faces[i], triangulated); + memcpy(surf.tris[triIdx].sides, triangulated[0].sides, sizeof(OBJFaceSide) * 3); + memcpy(surf.tris[triIdx+1].sides, triangulated[1].sides, sizeof(OBJFaceSide) * 3); + delete[] triangulated; + triIdx += 1; // Filling out two faces + } + } +} + +void FOBJModel::TriangulateQuad(const OBJFace &quad, OBJFace *tris) +{ + tris[0].sideCount = 3; + tris[1].sideCount = 3; + + int tsidx[2][3] = {{0, 1, 3}, {1, 2, 3}}; + + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 2; j++) + { + tris[j].sides[i].vertref = quad.sides[tsidx[j][i]].vertref; + tris[j].sides[i].uvref = quad.sides[tsidx[j][i]].uvref; + tris[j].sides[i].normref = quad.sides[tsidx[j][i]].normref; + } + } +} + +int FOBJModel::FindFrame(const char* name) +{ + return 0; // OBJs are not animated. +} + +void FOBJModel::RenderFrame(FModelRenderer *renderer, FTexture * skin, int frameno, int frameno2, double inter, int translation) +{ + for (unsigned int i = 0; i < surfaces.Size(); i++) + { + OBJSurface *surf = &surfaces[i]; + + FTexture *userSkin = skin; + if (!userSkin) + { + if (curSpriteMDLFrame->surfaceskinIDs[curMDLIndex][i].isValid()) + { + userSkin = TexMan(curSpriteMDLFrame->surfaceskinIDs[curMDLIndex][i]); + } + else if (surf->skin.isValid()) + { + userSkin = TexMan(surf->skin); + } + } + if (!userSkin) return; + + renderer->SetMaterial(userSkin, false, translation); + GetVertexBuffer(renderer)->SetupFrame(renderer, 0, 0, surf->numTris * 3); + renderer->DrawArrays(surf->vbStart, surf->numTris * 3); + } } void FOBJModel::AddSkins(uint8_t* hitlist) { + for (size_t i = 0; i < surfaces.Size(); i++) + { + if (curSpriteMDLFrame->surfaceskinIDs[curMDLIndex][i].isValid()) + { + hitlist[curSpriteMDLFrame->surfaceskinIDs[curMDLIndex][i].GetIndex()] |= FTextureManager::HIT_Flat; + } + OBJSurface * surf = &surfaces[i]; + if (surf->skin.isValid()) + { + hitlist[surf->skin.GetIndex()] |= FTextureManager::HIT_Flat; + } + } } FOBJModel::~FOBJModel() { - for (size_t i = 0; i < surfaces.Size(); i++) - { - delete[] surfaces[i].tris; - } + verts.Clear(); + norms.Clear(); + uvs.Clear(); + faces.Clear(); + for (size_t i = 0; i < surfaces.Size(); i++) + { + delete[] surfaces[i].tris; + } + surfaces.Clear(); } diff --git a/src/r_data/models/models_obj.h b/src/r_data/models/models_obj.h index 6fa2034c2..76af2e649 100644 --- a/src/r_data/models/models_obj.h +++ b/src/r_data/models/models_obj.h @@ -29,66 +29,59 @@ class FOBJModel : public FModel { private: - int mLumpNum; - int numMtls; + int mLumpNum; + const char *newSideSep = "$"; // OBJ side separator is /, which is parsed as a line comment by FScanner if two of them are next to each other. - enum class FaceElement - { - VertexIndex, - UVIndex, - VNormalIndex - }; + enum class FaceElement + { + VertexIndex, + UVIndex, + VNormalIndex + }; - struct OBJFaceSide - { - int vertref; - int normref; - int uvref; - }; - struct OBJFace - { - int sideCount; - OBJFaceSide sides[4]; - }; - struct OBJTriangle - { - int vertref[3]; - }; - struct OBJSurface // 1 surface per 'usemtl' - { - int numTris; // Number of triangulated faces - int numFaces; // Number of faces - int faceStart; // Index of first face in faces array - OBJFace* tris; // Triangles - FTextureID skin; - OBJSurface(FTextureID skin): numTris(0), numFaces(0), faceStart(0), tris(nullptr), skin(skin) {} - }; + struct OBJFaceSide + { + int vertref; + int normref; + int uvref; + }; + struct OBJFace + { + int sideCount; + OBJFaceSide sides[4]; + }; + struct OBJSurface // 1 surface per 'usemtl' + { + unsigned int numTris; // Number of triangulated faces + unsigned int numFaces; // Number of faces + unsigned int vbStart; // First index in vertex buffer + unsigned int faceStart; // Index of first face in faces array + OBJFace* tris; // Triangles + FTextureID skin; + OBJSurface(FTextureID skin): numTris(0), numFaces(0), vbStart(0), faceStart(0), tris(nullptr), skin(skin) {} + }; - TArray verts; - TArray norms; - TArray uvs; - TArray faces; - TArray surfaces; - FTextureID curMtl; - OBJSurface* curSurface; - int aggSurfFaceCount; - int curSurfFaceCount; - FScanner sc; + TArray verts; + TArray norms; + TArray uvs; + TArray faces; + TArray surfaces; + FScanner sc; - void ParseVector2(TArray &array); - void ParseVector3(TArray &array); - void ParseFaceSide(const FString &side, OBJFace &face, int sidx); - void ConstructSurfaceTris(OBJSurface *surf); - int ResolveIndex(int origIndex, FaceElement el); - void TriangulateQuad(const OBJFace &quad, OBJFace *tris); + void ParseVector2(TArray &array); + void ParseVector3(TArray &array); + void ParseFaceSide(const FString &side, OBJFace &face, int sidx); + void ConstructSurfaceTris(OBJSurface &surf); + int ResolveIndex(int origIndex, FaceElement el); + void TriangulateQuad(const OBJFace &quad, OBJFace *tris); public: - FOBJModel(): curSurface(nullptr), aggSurfFaceCount(0), curSurfFaceCount(0) {} - ~FOBJModel(); - bool Load(const char* fn, int lumpnum, const char* buffer, int length) override; - int FindFrame(const char* name) override; - void RenderFrame(FModelRenderer* renderer, FTexture* skin, int frame, int frame2, double inter, int translation=0) override; - void BuildVertexBuffer(FModelRenderer* renderer) override; - void AddSkins(uint8_t* hitlist) override; + FOBJModel() {} + ~FOBJModel(); + bool Load(const char* fn, int lumpnum, const char* buffer, int length) override; + int FindFrame(const char* name) override; + void RenderFrame(FModelRenderer* renderer, FTexture* skin, int frame, int frame2, double inter, int translation=0) override; + void BuildVertexBuffer(FModelRenderer* renderer) override; + void AddSkins(uint8_t* hitlist) override; }; #endif