Euphoria
mesh.cc
Go to the documentation of this file.
1 #include "core/mesh.h"
2 
3 
4 #include <utility>
5 
6 #include "assimp/Importer.hpp"
7 #include "assimp/IOSystem.hpp"
8 #include "assimp/IOStream.hpp"
9 #include "assimp/scene.h"
10 #include "assimp/postprocess.h"
11 
12 #include "base/cint.h"
13 #include "core/texturetypes.h"
14 #include "core/stdutils.h"
15 #include "io/json.h"
16 #include "log/log.h"
17 #include "base/stringmerger.h"
18 #include "io/vfs.h"
19 
20 #include "files/mesh.h"
21 
22 
23 namespace eu::core
24 {
26  (
27  const vec3f& a_vertex,
28  const vec3f& a_normal,
29  const vec2f& a_uv
30  )
31  : vertex(a_vertex)
32  , normal(a_normal)
33  , uv(a_uv)
34  {
35  }
36 
37 
38  MeshFace::MeshFace(int a_a, int a_b, int a_c)
39  : a(a_a)
40  , b(a_b)
41  , c(a_c)
42  {
43  }
44 
45 
47  : material(0)
48  {
49  }
50 
51 
52  Aabb
54  {
55  Aabb aabb = Aabb::create_empty();
56 
57  for(const auto& p: points)
58  {
59  aabb.extend(p.vertex);
60  }
61 
62  return aabb;
63  }
64 
65 
67  : path(p)
68  , type(t)
69  {
70  }
71 
72 
74  : name("unknown_material")
75  , shader(std::nullopt)
76  , ambient(NamedColor::white)
77  , diffuse(NamedColor::white)
78  , specular(NamedColor::white)
79  , shininess(42.0f)
80  , alpha(1.0f)
81  , wrap_s(WrapMode::repeat)
82  , wrap_t(WrapMode::repeat)
83  {
84  }
85 
86 
87  void
89  (
90  const std::string& texture_name,
91  const io::FilePath& texture_path
92  )
93  {
94  DEFINE_ENUM_VALUE(texture_type, texture_type, texture_name);
95  textures.emplace_back(texture_path, texture_type);
96  }
97 
98 
99  Aabb
101  {
102  Aabb aabb = Aabb::create_empty();
103 
104  for(const auto& part: parts)
105  {
106  aabb.extend(part.calc_aabb());
107  }
108 
109  return aabb;
110  }
111 
112 
113  namespace
114  {
115  DEFINE_ENUM_VALUE(texture_type, DiffuseType, "Diffuse"); // NOLINT
116  }
117 
118 
119  namespace
120  {
121  constexpr unsigned int assimp_flags
122  = aiProcess_CalcTangentSpace
123  | aiProcess_Triangulate
124  | aiProcess_SortByPType
125  | aiProcess_FlipUVs
126  | aiProcess_GenUVCoords
127  | aiProcess_TransformUVCoords
128  | aiProcess_OptimizeMeshes
129  | aiProcess_RemoveRedundantMaterials
130  | aiProcess_PreTransformVertices
131  | aiProcess_ImproveCacheLocality
132  | aiProcess_FindDegenerates
133  | aiProcess_JoinIdenticalVertices
134  | aiProcess_ValidateDataStructure
135  | aiProcess_GenSmoothNormals
136  | aiProcess_FindInvalidData
137  ;
138 
139 
140  WrapMode
141  get_texture_wrapping_mode(const int mode)
142  {
143  switch(mode)
144  {
145  case aiTextureMapMode_Wrap: return WrapMode::repeat;
146  case aiTextureMapMode_Clamp: return WrapMode::clamp_to_edge;
147  case aiTextureMapMode_Decal: throw "Unsupported texture wrapping mode: decal";
148  case aiTextureMapMode_Mirror: return WrapMode::mirror_repeat;
149  default: throw "Unhandled texture wrapping mode";
150  }
151  }
152 
153 
154  Rgb
155  con(const aiColor3D c)
156  {
157  return Rgb {c.r, c.g, c.b};
158  }
159 
160 
161  void
162  add_materials(Mesh* ret, const aiScene* scene)
163  {
164  for
165  (
166  unsigned int material_id = 0;
167  material_id < scene->mNumMaterials;
168  material_id += 1
169  )
170  {
171  const aiMaterial* mat = scene->mMaterials[material_id];
172 
173  Material material;
174 
175  if(mat->GetTextureCount(aiTextureType_DIFFUSE) <= 0)
176  {
177  // do nothing
178  }
179  else
180  {
181  aiString texture;
182  mat->GetTexture(aiTextureType_DIFFUSE, 0, &texture);
183  auto path = io::FilePath::from_dirty_source(texture.C_Str());
184  if(path.has_value() == false)
185  {
186  LOG_WARN
187  (
188  "invalid diffuse texture for material {0}: {0}",
189  material_id,
190  texture.C_Str()
191  );
192  }
193  material.textures.emplace_back
194  (
195  path.value_or(io::FilePath{"~/image-plain/blue"}),
196  DiffuseType
197  );
198  }
199 
200  aiString ai_name;
201  mat->Get(AI_MATKEY_NAME, ai_name);
202  material.name = ai_name.C_Str();
203 
204  const bool got_shininess = aiReturn_SUCCESS == mat->Get(AI_MATKEY_SHININESS, material.shininess);
205  const bool got_alpha = aiReturn_SUCCESS == mat->Get(AI_MATKEY_OPACITY, material.alpha);
206 
207  if(!got_shininess)
208  {
209  material.shininess = 0.0f;
210  }
211 
212  if(!got_alpha)
213  {
214  material.alpha = 1.0f;
215  }
216 
217  aiColor3D ai_ambient;
218  aiColor3D ai_diffuse;
219  aiColor3D ai_specular;
220  mat->Get(AI_MATKEY_COLOR_AMBIENT, ai_ambient);
221  mat->Get(AI_MATKEY_COLOR_DIFFUSE, ai_diffuse);
222  mat->Get(AI_MATKEY_COLOR_SPECULAR, ai_specular);
223  material.ambient = con(ai_ambient);
224  material.diffuse = con(ai_diffuse);
225  material.specular = con(ai_specular);
226 
227  int u = 0;
228  int v = 0;
229  mat->Get(AI_MATKEY_MAPPINGMODE_U(aiTextureType_DIFFUSE, 0), u);
230  mat->Get(AI_MATKEY_MAPPINGMODE_V(aiTextureType_DIFFUSE, 0), v);
231  material.wrap_s = get_texture_wrapping_mode(u);
232  material.wrap_t = get_texture_wrapping_mode(v);
233 
234  // todo(Gustav): improve texture detection?
235  material.shader = std::nullopt;
236  ret->materials.push_back(material);
237  }
238  }
239 
240 
241  void
242  add_faces(MeshPart* part, const aiMesh* mesh)
243  {
244  for(unsigned int face_id = 0; face_id < mesh->mNumFaces; face_id += 1)
245  {
246  const aiFace& face = mesh->mFaces[face_id];
247  if(face.mNumIndices<3)
248  {
249  continue;
250  }
251  ASSERTX(face.mNumIndices == 3, face.mNumIndices);
252  part->faces.emplace_back
253  (
254  c_unsigned_int_to_int(face.mIndices[0]),
255  c_unsigned_int_to_int(face.mIndices[1]),
256  c_unsigned_int_to_int(face.mIndices[2])
257  );
258  }
259  }
260 
261 
262  void
263  add_points(MeshPart* part, const aiMesh* mesh)
264  {
265  for(unsigned int index = 0; index < mesh->mNumVertices; index += 1)
266  {
267  const aiVector3D& vertex = mesh->mVertices[index];
268  const aiVector3D& normal = mesh->mNormals[index];
269  float u = 0;
270  float v = 0;
271  if(mesh->HasTextureCoords(0))
272  {
273  const aiVector3D uv = mesh->mTextureCoords[0][index];
274  u = uv.x;
275  v = uv.y;
276  }
277  part->points.push_back
278  (
279  MeshPoint
280  {
281  vec3f{vertex.x, vertex.y, vertex.z},
282  vec3f{normal.x, normal.y, normal.z},
283  vec2f{u, v}
284  }
285  );
286  }
287  }
288 
289 
290  MeshPart
291  convert_mesh(const aiMesh* mesh)
292  {
293  MeshPart part;
294 
295  part.material = mesh->mMaterialIndex;
296  add_points(&part, mesh);
297  add_faces(&part, mesh);
298 
299  return part;
300  }
301 
302 
303  Mesh
304  convert_scene(const aiScene* scene, const std::string& file_name)
305  {
306  Mesh ret;
307 
312  if(scene->HasMeshes())
313  {
314  add_materials(&ret, scene);
315 
316  for(unsigned int meshid = 0; meshid < scene->mNumMeshes; meshid += 1)
317  {
318  const aiMesh* mesh = scene->mMeshes[meshid];
319  const MeshPart part = convert_mesh(mesh);
320  if(part.faces.empty())
321  {
322  const auto& name = mesh->mName;
323  const char* const the_name = name.C_Str() != nullptr
324  ? name.C_Str()
325  : "<no_name>"
326  ;
327  LOG_WARN("No faces in part {0} for {1}", the_name, file_name);
328  }
329  else
330  {
331  ret.parts.push_back(part);
332  }
333  }
334  }
335 
336  return ret;
337  }
338 
339 
340  // http://assimp.sourceforge.net/howtoBasicShapes.html
341  Mesh
342  load_from_string(const std::string& nff, const std::string& format)
343  {
344  Assimp::Importer importer;
345 
346  const aiScene* scene = importer.ReadFileFromMemory
347  (
348  nff.c_str(),
349  nff.length() + 1,
350  assimp_flags,
351  format.c_str()
352  );
353  if(scene == nullptr)
354  {
355  throw std::string {importer.GetErrorString()};
356  }
357  return convert_scene(scene, "<nff_source>");
358  }
359 
360 
361  constexpr char const* const file_format_nff = "nff";
362  constexpr char const* const file_format_obj = "obj";
363 
364 
365  void
366  decorate_mesh_materials
367  (
368  Mesh* mesh,
369  const io::FilePath& json_path,
370  const files::mesh::Mesh& json
371  )
372  {
373  std::map<std::string, Material*> mesh_materials;
374  for(auto& material: mesh->materials)
375  {
376  mesh_materials[material.name] = &material;
377  }
378 
379  for(const auto& material: json.materials)
380  {
381  auto found = mesh_materials.find(material.name);
382  if(found == mesh_materials.end())
383  {
384  LOG_ERROR
385  (
386  "Unable to find {0} in mesh {1} valid names are: {2}",
387  material.name,
388  json_path,
389  string_mergers::english_or.merge(get_keys(mesh_materials))
390  );
391  continue;
392  }
393 
394  auto* other = found->second;
395  for(const auto& src_texture: material.textures)
396  {
397  auto path = io::FilePath::from_script
398  (
399  src_texture.path
400  );
401  if(path.has_value() == false)
402  {
403  LOG_WARN
404  (
405  "Invalid path {0} in mesh {1}",
406  src_texture.path,
407  json_path
408  );
409  continue;
410  }
411  other->set_texture
412  (
413  src_texture.type,
414  path.value()
415  );
416  }
417  }
418  }
419 
420 
421  void
422  decorate_mesh_materials_ignore_ambient(Mesh* mesh)
423  {
424  for(auto& material: mesh->materials)
425  {
426  material.ambient = material.diffuse;
427  }
428  }
429 
430 
431  void
432  fix_extension(io::FilePath* path, const files::mesh::Folder& folder)
433  {
434  const auto ext = path->get_extension();
435  for(auto c: folder.change_extensions)
436  {
437  if(ext == c.old_ext)
438  {
439  const auto new_path = path->set_extension_copy(c.new_ext);
440  *path = new_path;
441  return;
442  }
443  }
444  }
445 
446 
447  void
448  fix_filename(io::FilePath* path, const files::mesh::Folder& folder)
449  {
450  const auto [dir, file] = path->split_directories_and_file();
451  for(auto c: folder.change_filenames)
452  {
453  if(file == c.old_file)
454  {
455  const auto new_path = dir.get_file(c.new_file);
456  *path = new_path;
457  return;
458  }
459  }
460  }
461 
462 
463  void
464  decorate_mesh
465  (
466  io::FileSystem* fs,
467  Mesh* mesh,
468  const io::FilePath& json_path
469  )
470  {
471  // ------------------------------------------------------------------------------------------------
472  // file decoration
473  {
474  files::mesh::Mesh json_file;
475 
476  if (const auto loaded = read_json_file(fs, json_path); loaded == false && loaded.get_error().error == io::JsonErrorType::parse_error)
477  {
478  LOG_ERROR("Failed to load {}: {}", json_path, loaded.get_error().display);
479  }
480  else if (loaded)
481  {
482  const auto& json = loaded.get_value();
483  const auto parsed = files::mesh::parse(log::get_global_logger(), & json_file, json.root, &json.doc);
484  if (!parsed)
485  {
486  json_file = {};
487  }
488  }
489 
490  if (json_file.diffuse_and_ambient_are_same)
491  {
492  decorate_mesh_materials_ignore_ambient(mesh);
493  }
494 
495  if (!json_file.materials.empty())
496  {
497  decorate_mesh_materials(mesh, json_path, json_file);
498  }
499  }
500 
501 
502  // ------------------------------------------------------------------------------------------------
503  // folder decoration
504  {
505  const auto json_dir = json_path.get_directory();
506  const auto folder_path = json_dir.get_file("folder.json");
507 
508  files::mesh::Folder folder;
509 
510  if (const auto loaded = read_json_file(fs, json_path); loaded == false && loaded.get_error().error == io::JsonErrorType::file_error)
511  {
512  return;
513  }
514  else if (loaded == false)
515  {
516  LOG_ERROR("Failed to load {}: {}", json_path, loaded.get_error().display);
517  return;
518  }
519  else if (loaded)
520  {
521  const auto& json = loaded.get_value();
522  const auto parsed = files::mesh::parse(log::get_global_logger(), &folder, json.root, &json.doc);
523  if (!parsed)
524  {
525  return;
526  }
527  }
528 
529  for (auto& p : mesh->parts)
530  {
531  for (auto& v : p.points)
532  {
533  v.vertex = v.vertex * folder.scale;
534  }
535  }
536 
537  if (folder.texture_override.empty())
538  {
539  return;
540  }
541  auto dir = io::DirPath{ folder.texture_override };
542  if (dir.is_relative()) { dir = io::join(json_dir, dir); }
543 
544  for (auto& m : mesh->materials)
545  {
546  for (auto& t : m.textures)
547  {
548  const auto new_file = dir.get_file(t.path.get_file_with_extension());
549  t.path = new_file;
550  fix_extension(&t.path, folder);
551  fix_filename(&t.path, folder);
552  }
553  }
554  }
555  }
556  }
557 
558 
559  namespace meshes
560  {
561  struct FileForAssimp : public Assimp::IOStream
562  {
563  size_t read_index = 0;
564  std::shared_ptr<MemoryChunk> content;
565 
566  size_t Read(void* target_buffer, size_t size, size_t count) override
567  {
568  char* target = static_cast<char*>(target_buffer);
569  size_t objects_read = 0;
570  for(size_t object_index=0; object_index<count; object_index+=1)
571  {
572  if(c_sizet_to_int(read_index + size) > content->get_size())
573  {
574  return objects_read;
575  }
576  memcpy(target, content->get_data() + read_index, size);
577  read_index += size;
578  target += size;
579  objects_read += 1;
580  }
581  return objects_read;
582  }
583 
584  size_t Write(const void* , size_t , size_t ) override
585  {
586  return 0;
587  }
588 
589  aiReturn Seek(size_t offset, aiOrigin origin) override
590  {
591  switch(origin)
592  {
593  case aiOrigin_SET: read_index = offset; return aiReturn_SUCCESS;
594  case aiOrigin_CUR: read_index += offset; return aiReturn_SUCCESS;
595  case aiOrigin_END: DIE("end seek with unsigned int?"); return aiReturn_FAILURE;
596  default: DIE("unknown seek"); return aiReturn_FAILURE;
597  }
598  }
599 
600  [[nodiscard]] size_t Tell() const override
601  {
602  return read_index;
603  }
604 
605  [[nodiscard]] size_t FileSize() const override
606  {
607  return content->get_size();
608  }
609 
610  void Flush() override
611  {
612  }
613  };
614 
615 
616  struct FilesystemForAssimp : public Assimp::IOSystem
617  {
619 
621  : file_system(fs)
622  {
623  }
624 
625  bool Exists(const char* file) const override
626  {
627  auto content = file_system->read_file(io::FilePath{file});
628  return content != nullptr;
629  }
630 
631  [[nodiscard]] char getOsSeparator() const override
632  {
633  return '/';
634  }
635 
636  Assimp::IOStream* Open(const char* file, const char* mode_cstr) override
637  {
638  const std::string mode = mode_cstr;
639  ASSERT(mode.find('w') == std::string::npos);
640  ASSERT(mode.find('r') != std::string::npos);
641  auto content = file_system->read_file(io::FilePath{file});
642  if(content == nullptr)
643  {
644  return nullptr;
645  }
646  auto* data = new FileForAssimp(); // NOLINT
647  data->content = content;
648  return data;
649  }
650 
651  void Close(Assimp::IOStream* file) override
652  {
653  delete file; // NOLINT
654  }
655 
656  bool DeleteFile( const std::string& ) override
657  {
658  return false;
659  }
660  };
661 
662 
665  {
666  Assimp::Importer importer;
667  importer.SetIOHandler(new FilesystemForAssimp{fs}); // NOLINT
668 
669  LoadedMeshOrError res;
670 
671  const aiScene* scene = importer.ReadFile(path.path, assimp_flags);
672  if(scene == nullptr)
673  {
674  res.error = importer.GetErrorString();
675  }
676  else
677  {
678  res.loaded_mesh = convert_scene(scene, path.path);
679  decorate_mesh
680  (
681  fs,
682  &res.loaded_mesh,
683  path.set_extension_copy(path.get_extension()+".json")
684  );
685 
686  if(res.loaded_mesh.parts.empty())
687  {
688  res.error = "No parts(faces) in mesh";
689  }
690  }
691  return res;
692  }
693 
694 
695  Mesh
696  create_cube(float size)
697  {
698  return create_box(size, size, size);
699  }
700 
701 
702  Mesh
703  create_sphere(float size, const std::string& texture)
704  {
705  // todo(Gustav): replace with a custom generator instead of generating source and parsing
706  std::ostringstream ss;
707  ss
708  << "shader " << texture << "\n"
709  << "s 0 0 0 " << size
710  ;
711  return load_from_string(ss.str(), file_format_nff);
712  }
713 
714 
715  Mesh
716  create_box(float width, float height, float depth)
717  {
718  // todo(Gustav): replace with a custom generator instead of generating source and parsing
719  const float x = width / 2;
720  const float y = height / 2;
721  const float z = depth / 2;
722  std::ostringstream ss;
723  ss
724  << "v " << -x << " " << " " << -y << " " << -z << "\n"
725  << "v " << -x << " " << " " << -y << " " << z << "\n"
726  << "v " << -x << " " << " " << y << " " << -z << "\n"
727  << "v " << -x << " " << " " << y << " " << z << "\n"
728  << "v " << x << " " << " " << -y << " " << -z << "\n"
729  << "v " << x << " " << " " << -y << " " << z << "\n"
730  << "v " << x << " " << " " << y << " " << -z << "\n"
731  << "v " << x << " " << " " << y << " " << z << "\n"
732  << "" << "\n"
733  << "vt 0 0" << "\n"
734  << "vt 0 1" << "\n"
735  << "vt 1 1" << "\n"
736  << "vt 1 0" << "\n"
737  << "" << "\n"
738  << "f 3/1 7/2 5/3 1/4" << "\n"
739  << "f 6/1 8/2 4/3 2/4" << "\n"
740  << "f 2/1 4/2 3/3 1/4" << "\n"
741  << "f 7/1 8/2 6/3 5/4" << "\n"
742  << "f 4/1 8/2 7/3 3/4" << "\n"
743  << "f 5/1 6/2 2/3 1/4" << "\n"
744  ;
745 
746  auto box = load_from_string(ss.str(), file_format_obj);
747  return box;
748  }
749  }
750 }
751 
#define ASSERTX(x,...)
Definition: assert.h:48
#define ASSERT(x)
Definition: assert.h:29
#define DIE(message)
Definition: assert.h:67
#define DEFINE_ENUM_VALUE(TYPE, NAME, STRING)
Definition: enum.h:95
#define LOG_ERROR(...)
Definition: log.h:9
#define LOG_WARN(...)
Definition: log.h:8
constexpr ParseResult error
no error occurred
Definition: argparse.h:73
Mesh create_box(float width, float height, float depth)
Definition: mesh.cc:716
LoadedMeshOrError load_mesh(io::FileSystem *fs, const io::FilePath &path)
Definition: mesh.cc:664
Mesh create_sphere(float size, const std::string &texture)
Definition: mesh.cc:703
Mesh create_cube(float size)
Definition: mesh.cc:696
constexpr Rgbi con(unsigned char r, unsigned char g, unsigned char b)
std::vector< K > get_keys(const std::map< K, V > &m)
Definition: stdutils.h:13
WrapMode
Definition: mesh.h:24
JsonResult read_json_file(FileSystem *fs, const FilePath &file_name)
Definition: json.cc:58
DirPath join(const DirPath &lhs, const DirPath &rhs)
Definition: vfs_path.cc:419
Logger * get_global_logger()
Definition: log.cc:34
constexpr ShaderAttribute vertex
constexpr ShaderAttribute normal
constexpr StringMerger english_or
Definition: stringmerger.h:90
int c_sizet_to_int(size_t t)
Definition: cint.cc:11
NamedColor
Definition: colors.h:12
int c_unsigned_int_to_int(unsigned int i)
Definition: cint.cc:19
Definition: aabb.h:15
static Aabb create_empty()
Definition: aabb.cc:49
void extend(const vec3f &vec)
Definition: aabb.cc:31
const Error & get_error() const
Definition: result.h:36
std::string merge(const std::vector< std::string > &strings) const
Definition: stringmerger.cc:11
MaterialTexture(const io::FilePath &p, EnumValue t)
Definition: mesh.cc:66
std::vector< MaterialTexture > textures
Definition: mesh.h:86
void set_texture(const std::string &texture_name, const io::FilePath &texture_path)
Definition: mesh.cc:89
MeshFace(int a_a, int a_b, int a_c)
Definition: mesh.cc:38
std::vector< MeshPoint > points
Definition: mesh.h:59
Aabb calc_aabb() const
Definition: mesh.cc:53
MeshPoint(const vec3f &a_vertex, const vec3f &a_normal, const vec2f &a_uv)
Definition: mesh.cc:26
Aabb calc_aabb() const
Definition: mesh.cc:100
std::vector< MeshPart > parts
Definition: mesh.h:103
size_t Read(void *target_buffer, size_t size, size_t count) override
Definition: mesh.cc:566
std::shared_ptr< MemoryChunk > content
Definition: mesh.cc:564
size_t Tell() const override
Definition: mesh.cc:600
size_t FileSize() const override
Definition: mesh.cc:605
aiReturn Seek(size_t offset, aiOrigin origin) override
Definition: mesh.cc:589
void Flush() override
Definition: mesh.cc:610
size_t Write(const void *, size_t, size_t) override
Definition: mesh.cc:584
FilesystemForAssimp(io::FileSystem *fs)
Definition: mesh.cc:620
Assimp::IOStream * Open(const char *file, const char *mode_cstr) override
Definition: mesh.cc:636
bool Exists(const char *file) const override
Definition: mesh.cc:625
void Close(Assimp::IOStream *file) override
Definition: mesh.cc:651
char getOsSeparator() const override
Definition: mesh.cc:631
bool DeleteFile(const std::string &) override
Definition: mesh.cc:656
FilePath set_extension_copy(const std::string &ext) const
Definition: vfs_path.cc:170
std::string path
contains either .
Definition: vfs_path.h:39
std::string get_extension() const
Definition: vfs_path.cc:161
static std::optional< FilePath > from_script(const std::string &path)
apply only minor changes, return null on invalid
Definition: vfs_path.cc:46
static std::optional< FilePath > from_dirty_source(const std::string &path)
do everything possible to convert from dirty path to valid path
Definition: vfs_path.cc:75
std::shared_ptr< MemoryChunk > read_file(const FilePath &path)
Definition: vfs.cc:86
Definition: vec2.h:33
Definition: vec3.h:48