// Converts .obj files to .optmesh files // Usage: meshencoder [.obj] [.optmesh] // Data layout: // Header: 64b // Object table: 16b * object_count // Object data // Vertex data // Index data #include "../src/meshoptimizer.h" #include "objparser.h" #include #include #include #include #include #include struct Header { char magic[4]; // OPTM unsigned int group_count; unsigned int vertex_count; unsigned int index_count; unsigned int vertex_data_size; unsigned int index_data_size; float pos_offset[3]; float pos_scale; float uv_offset[2]; float uv_scale[2]; unsigned int reserved[2]; }; struct Object { unsigned int index_offset; unsigned int index_count; unsigned int material_length; unsigned int reserved; }; struct Vertex { unsigned short px, py, pz, pw; // unsigned 16-bit value, use pos_offset/pos_scale to unpack char nx, ny, nz, nw; // normalized signed 8-bit value unsigned short tx, ty; // unsigned 16-bit value, use uv_offset/uv_scale to unpack }; float rcpSafe(float v) { return v == 0.f ? 0.f : 1.f / v; } int main(int argc, char** argv) { if (argc <= 2) { printf("Usage: %s [.obj] [.optmesh]\n", argv[0]); return 1; } const char* input = argv[1]; const char* output = argv[2]; ObjFile file; if (!objParseFile(file, input)) { printf("Error loading %s: file not found\n", input); return 2; } if (!objValidate(file)) { printf("Error loading %s: invalid file data\n", input); return 3; } float pos_offset[3] = { FLT_MAX, FLT_MAX, FLT_MAX }; float pos_scale = 0.f; for (size_t i = 0; i < file.v_size; i += 3) { pos_offset[0] = std::min(pos_offset[0], file.v[i + 0]); pos_offset[1] = std::min(pos_offset[1], file.v[i + 1]); pos_offset[2] = std::min(pos_offset[2], file.v[i + 2]); } for (size_t i = 0; i < file.v_size; i += 3) { pos_scale = std::max(pos_scale, file.v[i + 0] - pos_offset[0]); pos_scale = std::max(pos_scale, file.v[i + 1] - pos_offset[1]); pos_scale = std::max(pos_scale, file.v[i + 2] - pos_offset[2]); } float uv_offset[2] = { FLT_MAX, FLT_MAX }; float uv_scale[2] = { 0, 0 }; for (size_t i = 0; i < file.vt_size; i += 3) { uv_offset[0] = std::min(uv_offset[0], file.vt[i + 0]); uv_offset[1] = std::min(uv_offset[1], file.vt[i + 1]); } for (size_t i = 0; i < file.vt_size; i += 3) { uv_scale[0] = std::max(uv_scale[0], file.vt[i + 0] - uv_offset[0]); uv_scale[1] = std::max(uv_scale[1], file.vt[i + 1] - uv_offset[1]); } float pos_scale_inverse = rcpSafe(pos_scale); float uv_scale_inverse[2] = { rcpSafe(uv_scale[0]), rcpSafe(uv_scale[1]) }; size_t total_indices = file.f_size / 3; std::vector triangles(total_indices); int pos_bits = 14; int uv_bits = 12; for (size_t i = 0; i < total_indices; ++i) { int vi = file.f[i * 3 + 0]; int vti = file.f[i * 3 + 1]; int vni = file.f[i * 3 + 2]; // note: we scale the vertices uniformly; this is not the best option wrt compression quality // however, it means we can scale the mesh uniformly without distorting the normals // this is helpful for backends like ThreeJS that apply mesh scaling to normals float px = (file.v[vi * 3 + 0] - pos_offset[0]) * pos_scale_inverse; float py = (file.v[vi * 3 + 1] - pos_offset[1]) * pos_scale_inverse; float pz = (file.v[vi * 3 + 2] - pos_offset[2]) * pos_scale_inverse; // normal is 0 if absent from the mesh float nx = vni >= 0 ? file.vn[vni * 3 + 0] : 0; float ny = vni >= 0 ? file.vn[vni * 3 + 1] : 0; float nz = vni >= 0 ? file.vn[vni * 3 + 2] : 0; // scale the normal to make sure the largest component is +-1.0 // this reduces the entropy of the normal by ~1.5 bits without losing precision // it's better to use octahedral encoding but that requires special shader support float nm = std::max(fabsf(nx), std::max(fabsf(ny), fabsf(nz))); float ns = nm == 0.f ? 0.f : 1 / nm; nx *= ns; ny *= ns; nz *= ns; // texture coordinates are 0 if absent, and require a texture matrix to decode float tx = vti >= 0 ? (file.vt[vti * 3 + 0] - uv_offset[0]) * uv_scale_inverse[0] : 0; float ty = vti >= 0 ? (file.vt[vti * 3 + 1] - uv_offset[1]) * uv_scale_inverse[1] : 0; Vertex v = { (unsigned short)(meshopt_quantizeUnorm(px, pos_bits)), (unsigned short)(meshopt_quantizeUnorm(py, pos_bits)), (unsigned short)(meshopt_quantizeUnorm(pz, pos_bits)), 0, char(meshopt_quantizeSnorm(nx, 8)), char(meshopt_quantizeSnorm(ny, 8)), char(meshopt_quantizeSnorm(nz, 8)), 0, (unsigned short)(meshopt_quantizeUnorm(tx, uv_bits)), (unsigned short)(meshopt_quantizeUnorm(ty, uv_bits)), }; triangles[i] = v; } std::vector remap(total_indices); size_t total_vertices = meshopt_generateVertexRemap(&remap[0], NULL, total_indices, &triangles[0], total_indices, sizeof(Vertex)); std::vector indices(total_indices); meshopt_remapIndexBuffer(&indices[0], NULL, total_indices, &remap[0]); std::vector vertices(total_vertices); meshopt_remapVertexBuffer(&vertices[0], &triangles[0], total_indices, sizeof(Vertex), &remap[0]); for (size_t i = 0; i < file.g_size; ++i) { ObjGroup& g = file.g[i]; meshopt_optimizeVertexCache(&indices[g.index_offset], &indices[g.index_offset], g.index_count, vertices.size()); } meshopt_optimizeVertexFetch(&vertices[0], &indices[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex)); std::vector vbuf(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(Vertex))); vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &vertices[0], vertices.size(), sizeof(Vertex))); std::vector ibuf(meshopt_encodeIndexBufferBound(indices.size(), vertices.size())); ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), &indices[0], indices.size())); FILE* result = fopen(output, "wb"); if (!result) { printf("Error saving %s: can't open file for writing\n", output); return 4; } Header header = {}; memcpy(header.magic, "OPTM", 4); header.group_count = unsigned(file.g_size); header.vertex_count = unsigned(vertices.size()); header.index_count = unsigned(indices.size()); header.vertex_data_size = unsigned(vbuf.size()); header.index_data_size = unsigned(ibuf.size()); header.pos_offset[0] = pos_offset[0]; header.pos_offset[1] = pos_offset[1]; header.pos_offset[2] = pos_offset[2]; header.pos_scale = pos_scale / float((1 << pos_bits) - 1); header.uv_offset[0] = uv_offset[0]; header.uv_offset[1] = uv_offset[1]; header.uv_scale[0] = uv_scale[0] / float((1 << uv_bits) - 1); header.uv_scale[1] = uv_scale[1] / float((1 << uv_bits) - 1); fwrite(&header, 1, sizeof(header), result); for (size_t i = 0; i < file.g_size; ++i) { ObjGroup& g = file.g[i]; Object object = {}; object.index_offset = unsigned(g.index_offset); object.index_count = unsigned(g.index_count); object.material_length = unsigned(strlen(g.material)); fwrite(&object, 1, sizeof(object), result); } for (size_t i = 0; i < file.g_size; ++i) { ObjGroup& g = file.g[i]; fwrite(g.material, 1, strlen(g.material), result); } fwrite(&vbuf[0], 1, vbuf.size(), result); fwrite(&ibuf[0], 1, ibuf.size(), result); fclose(result); return 0; }