// Copyright 2011 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 // // 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. #ifndef WEBGL_LOADER_MESH_H_ #define WEBGL_LOADER_MESH_H_ #include #include #include #include #include #include #include #include #include #include "base.h" #include "bounds.h" #include "stream.h" #include "utf8.h" // A short list of floats, useful for parsing a single vector // attribute. class ShortFloatList { public: // MeshLab can create position attributes with // color coordinates like: v x y z r g b static const size_t kMaxNumFloats = 6; ShortFloatList() : size_(0) { clear(); } void clear() { for (size_t i = 0; i < kMaxNumFloats; ++i) { a_[i] = 0.f; } } // Parse up to kMaxNumFloats from C string. // TODO: this should instead return endptr, since size // is recoverable. size_t ParseLine(const char* line) { for (size_ = 0; size_ != kMaxNumFloats; ++size_) { char* endptr = NULL; a_[size_] = strtof(line, &endptr); if (endptr == NULL || line == endptr) break; line = endptr; } return size_; } float operator[](size_t idx) const { return a_[idx]; } void AppendTo(AttribList* attribs) const { AppendNTo(attribs, size_); } void AppendNTo(AttribList* attribs, const size_t sz) const { attribs->insert(attribs->end(), a_, a_ + sz); } bool empty() const { return size_ == 0; } size_t size() const { return size_; } private: float a_[kMaxNumFloats]; size_t size_; }; class IndexFlattener { public: explicit IndexFlattener(size_t num_positions) : count_(0), table_(num_positions) { } int count() const { return count_; } void reserve(size_t size) { table_.reserve(size); } // Returns a pair of: < flattened index, newly inserted >. std::pair GetFlattenedIndex(int position_index, int texcoord_index, int normal_index) { if (position_index >= static_cast(table_.size())) { table_.resize(position_index + 1); } // First, optimistically look up position_index in the table. IndexType& index = table_[position_index]; if (index.position_or_flat == kIndexUnknown) { // This is the first time we've seen this position in the table, // so fill it. Since the table is indexed by position, we can // use the position_or_flat_index field to store the flat index. const int flat_index = count_++; index.position_or_flat = flat_index; index.texcoord = texcoord_index; index.normal = normal_index; return std::make_pair(flat_index, true); } else if (index.position_or_flat == kIndexNotInTable) { // There are multiple flattened indices at this position index, // so resort to the map. return GetFlattenedIndexFromMap(position_index, texcoord_index, normal_index); } else if (index.texcoord == texcoord_index && index.normal == normal_index) { // The other indices match, so we can use the value cached in // the table. return std::make_pair(index.position_or_flat, false); } // The other indices don't match, so we mark this table entry, // and insert both the old and new indices into the map. const IndexType old_index(position_index, index.texcoord, index.normal); map_.insert(std::make_pair(old_index, index.position_or_flat)); index.position_or_flat = kIndexNotInTable; const IndexType new_index(position_index, texcoord_index, normal_index); const int flat_index = count_++; map_.insert(std::make_pair(new_index, flat_index)); return std::make_pair(flat_index, true); } private: std::pair GetFlattenedIndexFromMap(int position_index, int texcoord_index, int normal_index) { IndexType index(position_index, texcoord_index, normal_index); MapType::iterator iter = map_.lower_bound(index); if (iter == map_.end() || iter->first != index) { const int flat_index = count_++; map_.insert(iter, std::make_pair(index, flat_index)); return std::make_pair(flat_index, true); } else { return std::make_pair(iter->second, false); } } static const int kIndexUnknown = -1; static const int kIndexNotInTable = -2; struct IndexType { IndexType() : position_or_flat(kIndexUnknown), texcoord(kIndexUnknown), normal(kIndexUnknown) { } IndexType(int position_index, int texcoord_index, int normal_index) : position_or_flat(position_index), texcoord(texcoord_index), normal(normal_index) { } // I'm being tricky/lazy here. The table_ stores the flattened // index in the first field, since it is indexed by position. The // map_ stores position and uses this struct as a key to lookup the // flattened index. int position_or_flat; int texcoord; int normal; // An ordering for std::map. bool operator<(const IndexType& that) const { if (position_or_flat == that.position_or_flat) { if (texcoord == that.texcoord) { return normal < that.normal; } else { return texcoord < that.texcoord; } } else { return position_or_flat < that.position_or_flat; } } bool operator==(const IndexType& that) const { return position_or_flat == that.position_or_flat && texcoord == that.texcoord && normal == that.normal; } bool operator!=(const IndexType& that) const { return !operator==(that); } }; typedef std::map MapType; int count_; std::vector table_; MapType map_; }; static inline size_t positionDim() { return 3; } static inline size_t texcoordDim() { return 2; } static inline size_t normalDim() { return 3; } // TODO(wonchun): Make a c'tor to properly initialize. struct GroupStart { size_t offset; // offset into draw_mesh_.indices. unsigned int group_line; int min_index, max_index; // range into attribs. webgl_loader::Bounds bounds; }; class DrawBatch { public: DrawBatch() : flattener_(0), current_group_line_(0xFFFFFFFF) { } const std::vector& group_starts() const { return group_starts_; } void Init(AttribList* positions, AttribList* texcoords, AttribList* normals) { positions_ = positions; texcoords_ = texcoords; normals_ = normals; flattener_.reserve(1024); } void AddTriangle(unsigned int group_line, int* indices) { if (group_line != current_group_line_) { current_group_line_ = group_line; GroupStart group_start; group_start.offset = draw_mesh_.indices.size(); group_start.group_line = group_line; group_start.min_index = INT_MAX; group_start.max_index = INT_MIN; group_start.bounds.Clear(); group_starts_.push_back(group_start); } GroupStart& group = group_starts_.back(); for (size_t i = 0; i < 9; i += 3) { // .OBJ files use 1-based indexing. const int position_index = indices[i + 0] - 1; const int texcoord_index = indices[i + 1] - 1; const int normal_index = indices[i + 2] - 1; const std::pair flattened = flattener_.GetFlattenedIndex( position_index, texcoord_index, normal_index); const int flat_index = flattened.first; CHECK(flat_index >= 0); draw_mesh_.indices.push_back(flat_index); if (flattened.second) { // This is a new index. Keep track of index ranges and vertex // bounds. if (flat_index > group.max_index) { group.max_index = flat_index; } if (flat_index < group.min_index) { group.min_index = flat_index; } const size_t new_loc = draw_mesh_.attribs.size(); CHECK(8*size_t(flat_index) == new_loc); for (size_t i = 0; i < positionDim(); ++i) { draw_mesh_.attribs.push_back( positions_->at(positionDim() * position_index + i)); } if (texcoord_index == -1) { for (size_t i = 0; i < texcoordDim(); ++i) { draw_mesh_.attribs.push_back(0); } } else { for (size_t i = 0; i < texcoordDim(); ++i) { draw_mesh_.attribs.push_back( texcoords_->at(texcoordDim() * texcoord_index + i)); } } if (normal_index == -1) { for (size_t i = 0; i < normalDim(); ++i) { draw_mesh_.attribs.push_back(0); } } else { for (size_t i = 0; i < normalDim(); ++i) { draw_mesh_.attribs.push_back( normals_->at(normalDim() * normal_index + i)); } } // TODO: is the covariance body useful for anything? group.bounds.EncloseAttrib(&draw_mesh_.attribs[new_loc]); } } } const DrawMesh& draw_mesh() const { return draw_mesh_; } private: AttribList* positions_, *texcoords_, *normals_; DrawMesh draw_mesh_; IndexFlattener flattener_; unsigned int current_group_line_; std::vector group_starts_; }; struct Material { std::string name; float Kd[3]; std::string map_Kd; void DumpJson(FILE* out = stdout) const { fprintf(out, " \"%s\": { ", name.c_str()); if (map_Kd.empty()) { fprintf(out, "\"Kd\": [%hu, %hu, %hu] }", Quantize(Kd[0], 0, 1, 255), Quantize(Kd[1], 0, 1, 255), Quantize(Kd[2], 0, 1, 255)); } else { fprintf(out, "\"map_Kd\": \"%s\" }", map_Kd.c_str()); } } }; typedef std::vector MaterialList; class WavefrontMtlFile { public: explicit WavefrontMtlFile(FILE* fp) { ParseFile(fp); } const MaterialList& materials() const { return materials_; } private: // TODO: factor this parsing stuff out. void ParseFile(FILE* fp) { // TODO: don't use a fixed-size buffer. const size_t kLineBufferSize = 256; char buffer[kLineBufferSize]; unsigned int line_num = 1; while (fgets(buffer, kLineBufferSize, fp) != NULL) { char* stripped = StripLeadingWhitespace(buffer); TerminateAtNewlineOrComment(stripped); ParseLine(stripped, line_num++); } } void ParseLine(const char* line, unsigned int line_num) { switch (*line) { case 'K': ParseColor(line + 1, line_num); break; case 'm': if (0 == strncmp(line + 1, "ap_Kd", 5)) { ParseMapKd(line + 6, line_num); } break; case 'n': if (0 == strncmp(line + 1, "ewmtl", 5)) { ParseNewmtl(line + 6, line_num); } default: break; } } void ParseColor(const char* line, unsigned int line_num) { switch (*line) { case 'd': { ShortFloatList floats; floats.ParseLine(line + 1); float* Kd = current_->Kd; Kd[0] = floats[0]; Kd[1] = floats[1]; Kd[2] = floats[2]; break; } default: break; } } void ParseMapKd(const char* line, unsigned int line_num) { current_->map_Kd = StripLeadingWhitespace(line); } void ParseNewmtl(const char* line, unsigned int line_num) { materials_.push_back(Material()); current_ = &materials_.back(); ToLower(StripLeadingWhitespace(line), ¤t_->name); } Material* current_; MaterialList materials_; }; typedef std::map MaterialBatches; // TODO: consider splitting this into a low-level parser and a high-level // object. class WavefrontObjFile { public: explicit WavefrontObjFile(FILE* fp) { current_batch_ = &material_batches_[""]; current_batch_->Init(&positions_, &texcoords_, &normals_); current_group_line_ = 0; line_to_groups_.insert(std::make_pair(0, "default")); ParseFile(fp); } const MaterialList& materials() const { return materials_; } const MaterialBatches& material_batches() const { return material_batches_; } const std::string& LineToGroup(unsigned int line) const { typedef LineToGroups::const_iterator Iterator; typedef std::pair EqualRange; EqualRange equal_range = line_to_groups_.equal_range(line); const std::string* best_group = NULL; int best_count = 0; for (Iterator iter = equal_range.first; iter != equal_range.second; ++iter) { const std::string& group = iter->second; const int count = group_counts_.find(group)->second; if (!best_group || (count < best_count)) { best_group = &group; best_count = count; } } if (!best_group) { ErrorLine("no suitable group found", line); } return *best_group; } void DumpDebug() const { printf("positions size: " PRIuS "\n" "texcoords size: " PRIuS "\n" "normals size: " PRIuS "\n", positions_.size(), texcoords_.size(), normals_.size()); } private: WavefrontObjFile() { } // For testing. void ParseFile(FILE* fp) { // TODO: don't use a fixed-size buffer. const size_t kLineBufferSize = 256; char buffer[kLineBufferSize] = { 0 }; unsigned int line_num = 1; while (fgets(buffer, kLineBufferSize, fp) != NULL) { char* stripped = StripLeadingWhitespace(buffer); TerminateAtNewlineOrComment(stripped); ParseLine(stripped, line_num++); } } void ParseLine(const char* line, unsigned int line_num) { switch (*line) { case 'v': ParseAttrib(line + 1, line_num); break; case 'f': ParseFace(line + 1, line_num); break; case 'g': if (isspace(line[1])) { ParseGroup(line + 2, line_num); } else { goto unknown; } break; case '\0': case '#': break; // Do nothing for comments or blank lines. case 'p': WarnLine("point unsupported", line_num); break; case 'l': WarnLine("line unsupported", line_num); break; case 'u': if (0 == strncmp(line + 1, "semtl", 5)) { ParseUsemtl(line + 6, line_num); } else { goto unknown; } break; case 'm': if (0 == strncmp(line + 1, "tllib", 5)) { ParseMtllib(line + 6, line_num); } else { goto unknown; } break; case 's': ParseSmoothingGroup(line + 1, line_num); break; unknown: default: WarnLine("unknown keyword", line_num); break; } } void ParseAttrib(const char* line, unsigned int line_num) { ShortFloatList floats; floats.ParseLine(line + 1); if (isspace(*line)) { ParsePosition(floats, line_num); } else if (*line == 't') { ParseTexCoord(floats, line_num); } else if (*line == 'n') { ParseNormal(floats, line_num); } else { WarnLine("unknown attribute format", line_num); } } void ParsePosition(const ShortFloatList& floats, unsigned int line_num) { if (floats.size() != positionDim() && floats.size() != 6) { // ignore r g b for now. ErrorLine("bad position", line_num); } floats.AppendNTo(&positions_, positionDim()); } void ParseTexCoord(const ShortFloatList& floats, unsigned int line_num) { if ((floats.size() < 1) || (floats.size() > 3)) { // TODO: correctly handle 3-D texcoords intead of just // truncating. ErrorLine("bad texcoord", line_num); } floats.AppendNTo(&texcoords_, texcoordDim()); } void ParseNormal(const ShortFloatList& floats, unsigned int line_num) { if (floats.size() != normalDim()) { ErrorLine("bad normal", line_num); } // Normalize to avoid out-of-bounds quantization. This should be // optional, in case someone wants to be using the normal magnitude as // something meaningful. const float x = floats[0]; const float y = floats[1]; const float z = floats[2]; const float scale = 1.0/sqrt(x*x + y*y + z*z); if (isfinite(scale)) { normals_.push_back(scale * x); normals_.push_back(scale * y); normals_.push_back(scale * z); } else { normals_.push_back(0); normals_.push_back(0); normals_.push_back(0); } } // Parses faces and converts to triangle fans. This is not a // particularly good tesselation in general case, but it is really // simple, and is perfectly fine for triangles and quads. void ParseFace(const char* line, unsigned int line_num) { // Also handle face outlines as faces. if (*line == 'o') ++line; // TODO: instead of storing these indices as-is, it might make // sense to flatten them right away. This can reduce memory // consumption and improve access locality, especially since .OBJ // face indices are so needlessly large. int indices[9] = { 0 }; // The first index acts as the pivot for the triangle fan. line = ParseIndices(line, line_num, indices + 0, indices + 1, indices + 2); if (line == NULL) { ErrorLine("bad first index", line_num); } line = ParseIndices(line, line_num, indices + 3, indices + 4, indices + 5); if (line == NULL) { ErrorLine("bad second index", line_num); } // After the first two indices, each index introduces a new // triangle to the fan. while ((line = ParseIndices(line, line_num, indices + 6, indices + 7, indices + 8))) { current_batch_->AddTriangle(current_group_line_, indices); // The most recent vertex is reused for the next triangle. indices[3] = indices[6]; indices[4] = indices[7]; indices[5] = indices[8]; indices[6] = indices[7] = indices[8] = 0; } } // Parse a single group of indices, separated by slashes ('/'). // TODO: convert negative indices (that is, relative to the end of // the current vertex positions) to more conventional positive // indices. const char* ParseIndices(const char* line, unsigned int line_num, int* position_index, int* texcoord_index, int* normal_index) { const char* endptr = NULL; *position_index = strtoint(line, &endptr); if (*position_index == 0) { return NULL; } if (endptr != NULL && *endptr == '/') { *texcoord_index = strtoint(endptr + 1, &endptr); } else { *texcoord_index = *normal_index = 0; } if (endptr != NULL && *endptr == '/') { *normal_index = strtoint(endptr + 1, &endptr); } else { *normal_index = 0; } return endptr; } // .OBJ files can specify multiple groups for a set of faces. This // implementation finds the "most unique" group for a set of faces // and uses that for the batch. In the first pass, we use the line // number of the "g" command to tag the faces. Afterwards, after we // collect group populations, we can go back and give them real // names. void ParseGroup(const char* line, unsigned int line_num) { std::string token; while ((line = ConsumeFirstToken(line, &token))) { ToLowerInplace(&token); group_counts_[token]++; line_to_groups_.insert(std::make_pair(line_num, token)); } current_group_line_ = line_num; } void ParseSmoothingGroup(const char* line, unsigned int line_num) { static bool once = true; if (once) { WarnLine("s ignored", line_num); once = false; } } void ParseMtllib(const char* line, unsigned int line_num) { FILE* fp = fopen(StripLeadingWhitespace(line), "r"); if (!fp) { WarnLine("mtllib not found", line_num); return; } WavefrontMtlFile mtlfile(fp); fclose(fp); materials_ = mtlfile.materials(); for (size_t i = 0; i < materials_.size(); ++i) { DrawBatch& draw_batch = material_batches_[materials_[i].name]; draw_batch.Init(&positions_, &texcoords_, &normals_); } } void ParseUsemtl(const char* line, unsigned int line_num) { std::string usemtl; ToLower(StripLeadingWhitespace(line), &usemtl); MaterialBatches::iterator iter = material_batches_.find(usemtl); if (iter == material_batches_.end()) { ErrorLine("material not found", line_num); } current_batch_ = &iter->second; } void WarnLine(const char* why, unsigned int line_num) const { fprintf(stderr, "WARNING: %s at line %u\n", why, line_num); } void ErrorLine(const char* why, unsigned int line_num) const { fprintf(stderr, "ERROR: %s at line %u\n", why, line_num); exit(-1); } AttribList positions_; AttribList texcoords_; AttribList normals_; MaterialList materials_; // Currently, batch by texture (i.e. map_Kd). MaterialBatches material_batches_; DrawBatch* current_batch_; typedef std::multimap LineToGroups; LineToGroups line_to_groups_; std::map group_counts_; unsigned int current_group_line_; }; #endif // WEBGL_LOADER_MESH_H_