From 8bfc1e21343c32638d6ee33705f52cf96872bf28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Thu, 26 Dec 2024 23:19:20 -0500 Subject: [PATCH] 3d: put the threedim addon into the fold --- .gitmodules | 12 + 3rdparty/eigen | 1 + 3rdparty/libssynth | 1 + 3rdparty/miniply | 1 + 3rdparty/tiny_obj_loader.h | 3452 +++++++++++++++++ 3rdparty/vcglib | 1 + ci/common.deps.sh | 1 - cmake/Configurations/all-plugins.cmake | 1 + .../score-plugin-threedim/CMakeLists.txt | 115 + .../Threedim/ArrayToGeometry.cpp | 150 + .../Threedim/ArrayToGeometry.hpp | 41 + .../Threedim/MeshHelpers.hpp | 46 + .../Threedim/ModelDisplay/Executor.cpp | 84 + .../Threedim/ModelDisplay/Executor.hpp | 24 + .../Threedim/ModelDisplay/Layer.hpp | 0 .../Threedim/ModelDisplay/Metadata.hpp | 23 + .../ModelDisplay/ModelDisplayNode.cpp | 1201 ++++++ .../ModelDisplay/ModelDisplayNode.hpp | 37 + .../Threedim/ModelDisplay/Process.cpp | 118 + .../Threedim/ModelDisplay/Process.hpp | 41 + .../score-plugin-threedim/Threedim/Noise.cpp | 137 + .../score-plugin-threedim/Threedim/Noise.hpp | 106 + .../Threedim/ObjLoader.cpp | 189 + .../Threedim/ObjLoader.hpp | 53 + .../Threedim/PCLToGeometry.cpp | 46 + .../Threedim/PCLToGeometry.hpp | 66 + .../score-plugin-threedim/Threedim/Ply.cpp | 216 ++ .../score-plugin-threedim/Threedim/Ply.hpp | 7 + .../Threedim/Primitive.cpp | 182 + .../Threedim/Primitive.hpp | 200 + .../Threedim/StructureSynth.cpp | 101 + .../Threedim/StructureSynth.hpp | 67 + .../Threedim/TinyObj.cpp | 194 + .../Threedim/TinyObj.hpp | 92 + .../score_plugin_threedim.cpp | 208 + .../score_plugin_threedim.hpp | 24 + 36 files changed, 7237 insertions(+), 1 deletion(-) create mode 160000 3rdparty/eigen create mode 160000 3rdparty/libssynth create mode 160000 3rdparty/miniply create mode 100644 3rdparty/tiny_obj_loader.h create mode 160000 3rdparty/vcglib create mode 100644 src/plugins/score-plugin-threedim/CMakeLists.txt create mode 100644 src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/MeshHelpers.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Layer.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Metadata.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Noise.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Noise.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ObjLoader.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/ObjLoader.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Ply.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Ply.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Primitive.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/Primitive.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/StructureSynth.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/StructureSynth.hpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/TinyObj.cpp create mode 100644 src/plugins/score-plugin-threedim/Threedim/TinyObj.hpp create mode 100644 src/plugins/score-plugin-threedim/score_plugin_threedim.cpp create mode 100644 src/plugins/score-plugin-threedim/score_plugin_threedim.hpp diff --git a/.gitmodules b/.gitmodules index aad1613f85..734052a667 100755 --- a/.gitmodules +++ b/.gitmodules @@ -85,3 +85,15 @@ [submodule "3rdparty/snmalloc"] path = 3rdparty/snmalloc url = https://github.com/microsoft/snmalloc/ +[submodule "3rdparty/libssynth"] + path = 3rdparty/libssynth + url = https://github.com/jcelerier/libssynth +[submodule "3rdparty/vcglib"] + path = 3rdparty/vcglib + url = https://github.com/cnr-isti-vclab/vcglib +[submodule "3rdparty/eigen"] + path = 3rdparty/eigen + url = https://gitlab.com/libeigen/eigen +[submodule "3rdparty/miniply"] + path = 3rdparty/miniply + url = https://github.com/vilya/miniply diff --git a/3rdparty/eigen b/3rdparty/eigen new file mode 160000 index 0000000000..24e0c2a125 --- /dev/null +++ b/3rdparty/eigen @@ -0,0 +1 @@ +Subproject commit 24e0c2a125d2b37b35719124d1f758777c150ca8 diff --git a/3rdparty/libssynth b/3rdparty/libssynth new file mode 160000 index 0000000000..b7b2ca5317 --- /dev/null +++ b/3rdparty/libssynth @@ -0,0 +1 @@ +Subproject commit b7b2ca5317de822baf9c6e80cdc0883a333c8520 diff --git a/3rdparty/miniply b/3rdparty/miniply new file mode 160000 index 0000000000..1a235c7039 --- /dev/null +++ b/3rdparty/miniply @@ -0,0 +1 @@ +Subproject commit 1a235c70390fadf789695c9ccbf285ae712416b3 diff --git a/3rdparty/tiny_obj_loader.h b/3rdparty/tiny_obj_loader.h new file mode 100644 index 0000000000..cb763ed1f1 --- /dev/null +++ b/3rdparty/tiny_obj_loader.h @@ -0,0 +1,3452 @@ +#pragma once +/* +The MIT License (MIT) + +Copyright (c) 2012-Present, Syoyo Fujita and many contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// +// version 2.0.0 : Add new object oriented API. 1.x API is still provided. +// * Support line primitive. +// * Support points primitive. +// * Support multiple search path for .mtl(v1 API). +// * Support vertex weight `vw`(as an tinyobj extension) +// * Support escaped whitespece in mtllib +// * Add robust triangulation using Mapbox earcut(TINYOBJLOADER_USE_MAPBOX_EARCUT). +// version 1.4.0 : Modifed ParseTextureNameAndOption API +// version 1.3.1 : Make ParseTextureNameAndOption API public +// version 1.3.0 : Separate warning and error message(breaking API of LoadObj) +// version 1.2.3 : Added color space extension('-colorspace') to tex opts. +// version 1.2.2 : Parse multiple group names. +// version 1.2.1 : Added initial support for line('l') primitive(PR #178) +// version 1.2.0 : Hardened implementation(#175) +// version 1.1.1 : Support smoothing groups(#162) +// version 1.1.0 : Support parsing vertex color(#144) +// version 1.0.8 : Fix parsing `g` tag just after `usemtl`(#138) +// version 1.0.7 : Support multiple tex options(#126) +// version 1.0.6 : Add TINYOBJLOADER_USE_DOUBLE option(#124) +// version 1.0.5 : Ignore `Tr` when `d` exists in MTL(#43) +// version 1.0.4 : Support multiple filenames for 'mtllib'(#112) +// version 1.0.3 : Support parsing texture options(#85) +// version 1.0.2 : Improve parsing speed by about a factor of 2 for large +// files(#105) +// version 1.0.1 : Fixes a shape is lost if obj ends with a 'usemtl'(#104) +// version 1.0.0 : Change data structure. Change license from BSD to MIT. +// + +// +// Use this in *one* .cc +// #define TINYOBJLOADER_IMPLEMENTATION +// #include "tiny_obj_loader.h" +// + +#ifndef TINY_OBJ_LOADER_H_ +#define TINY_OBJ_LOADER_H_ + +#include +#include +#include + +namespace tinyobj { + +// TODO(syoyo): Better C++11 detection for older compiler +#if __cplusplus > 199711L +#define TINYOBJ_OVERRIDE override +#else +#define TINYOBJ_OVERRIDE +#endif + +#ifdef __clang__ +#pragma clang diagnostic push +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif + +#pragma clang diagnostic ignored "-Wpadded" + +#endif + +// https://en.wikipedia.org/wiki/Wavefront_.obj_file says ... +// +// -blendu on | off # set horizontal texture blending +// (default on) +// -blendv on | off # set vertical texture blending +// (default on) +// -boost real_value # boost mip-map sharpness +// -mm base_value gain_value # modify texture map values (default +// 0 1) +// # base_value = brightness, +// gain_value = contrast +// -o u [v [w]] # Origin offset (default +// 0 0 0) +// -s u [v [w]] # Scale (default +// 1 1 1) +// -t u [v [w]] # Turbulence (default +// 0 0 0) +// -texres resolution # texture resolution to create +// -clamp on | off # only render texels in the clamped +// 0-1 range (default off) +// # When unclamped, textures are +// repeated across a surface, +// # when clamped, only texels which +// fall within the 0-1 +// # range are rendered. +// -bm mult_value # bump multiplier (for bump maps +// only) +// +// -imfchan r | g | b | m | l | z # specifies which channel of the file +// is used to +// # create a scalar or bump texture. +// r:red, g:green, +// # b:blue, m:matte, l:luminance, +// z:z-depth.. +// # (the default for bump is 'l' and +// for decal is 'm') +// bump -imfchan r bumpmap.tga # says to use the red channel of +// bumpmap.tga as the bumpmap +// +// For reflection maps... +// +// -type sphere # specifies a sphere for a "refl" +// reflection map +// -type cube_top | cube_bottom | # when using a cube map, the texture +// file for each +// cube_front | cube_back | # side of the cube is specified +// separately +// cube_left | cube_right +// +// TinyObjLoader extension. +// +// -colorspace SPACE # Color space of the texture. e.g. +// 'sRGB` or 'linear' +// + +#ifdef TINYOBJLOADER_USE_DOUBLE +//#pragma message "using double" +typedef double real_t; +#else +//#pragma message "using float" +typedef float real_t; +#endif + +typedef enum { + TEXTURE_TYPE_NONE, // default + TEXTURE_TYPE_SPHERE, + TEXTURE_TYPE_CUBE_TOP, + TEXTURE_TYPE_CUBE_BOTTOM, + TEXTURE_TYPE_CUBE_FRONT, + TEXTURE_TYPE_CUBE_BACK, + TEXTURE_TYPE_CUBE_LEFT, + TEXTURE_TYPE_CUBE_RIGHT +} texture_type_t; + +struct texture_option_t { + texture_type_t type; // -type (default TEXTURE_TYPE_NONE) + real_t sharpness; // -boost (default 1.0?) + real_t brightness; // base_value in -mm option (default 0) + real_t contrast; // gain_value in -mm option (default 1) + real_t origin_offset[3]; // -o u [v [w]] (default 0 0 0) + real_t scale[3]; // -s u [v [w]] (default 1 1 1) + real_t turbulence[3]; // -t u [v [w]] (default 0 0 0) + int texture_resolution; // -texres resolution (No default value in the spec. + // We'll use -1) + bool clamp; // -clamp (default false) + char imfchan; // -imfchan (the default for bump is 'l' and for decal is 'm') + bool blendu; // -blendu (default on) + bool blendv; // -blendv (default on) + real_t bump_multiplier; // -bm (for bump maps only, default 1.0) + + // extension + std::string colorspace; // Explicitly specify color space of stored texel + // value. Usually `sRGB` or `linear` (default empty). +}; + +struct material_t { + std::string name; + + real_t ambient[3]; + real_t diffuse[3]; + real_t specular[3]; + real_t transmittance[3]; + real_t emission[3]; + real_t shininess; + real_t ior; // index of refraction + real_t dissolve; // 1 == opaque; 0 == fully transparent + // illumination model (see http://www.fileformat.info/format/material/) + int illum; + + int dummy; // Suppress padding warning. + + std::string ambient_texname; // map_Ka + std::string diffuse_texname; // map_Kd + std::string specular_texname; // map_Ks + std::string specular_highlight_texname; // map_Ns + std::string bump_texname; // map_bump, map_Bump, bump + std::string displacement_texname; // disp + std::string alpha_texname; // map_d + std::string reflection_texname; // refl + + texture_option_t ambient_texopt; + texture_option_t diffuse_texopt; + texture_option_t specular_texopt; + texture_option_t specular_highlight_texopt; + texture_option_t bump_texopt; + texture_option_t displacement_texopt; + texture_option_t alpha_texopt; + texture_option_t reflection_texopt; + + // PBR extension + // http://exocortex.com/blog/extending_wavefront_mtl_to_support_pbr + real_t roughness; // [0, 1] default 0 + real_t metallic; // [0, 1] default 0 + real_t sheen; // [0, 1] default 0 + real_t clearcoat_thickness; // [0, 1] default 0 + real_t clearcoat_roughness; // [0, 1] default 0 + real_t anisotropy; // aniso. [0, 1] default 0 + real_t anisotropy_rotation; // anisor. [0, 1] default 0 + real_t pad0; + std::string roughness_texname; // map_Pr + std::string metallic_texname; // map_Pm + std::string sheen_texname; // map_Ps + std::string emissive_texname; // map_Ke + std::string normal_texname; // norm. For normal mapping. + + texture_option_t roughness_texopt; + texture_option_t metallic_texopt; + texture_option_t sheen_texopt; + texture_option_t emissive_texopt; + texture_option_t normal_texopt; + + int pad2; + + std::map unknown_parameter; + +#ifdef TINY_OBJ_LOADER_PYTHON_BINDING + // For pybind11 + std::array GetDiffuse() { + std::array values; + values[0] = double(diffuse[0]); + values[1] = double(diffuse[1]); + values[2] = double(diffuse[2]); + + return values; + } + + std::array GetSpecular() { + std::array values; + values[0] = double(specular[0]); + values[1] = double(specular[1]); + values[2] = double(specular[2]); + + return values; + } + + std::array GetTransmittance() { + std::array values; + values[0] = double(transmittance[0]); + values[1] = double(transmittance[1]); + values[2] = double(transmittance[2]); + + return values; + } + + std::array GetEmission() { + std::array values; + values[0] = double(emission[0]); + values[1] = double(emission[1]); + values[2] = double(emission[2]); + + return values; + } + + std::array GetAmbient() { + std::array values; + values[0] = double(ambient[0]); + values[1] = double(ambient[1]); + values[2] = double(ambient[2]); + + return values; + } + + void SetDiffuse(std::array &a) { + diffuse[0] = real_t(a[0]); + diffuse[1] = real_t(a[1]); + diffuse[2] = real_t(a[2]); + } + + void SetAmbient(std::array &a) { + ambient[0] = real_t(a[0]); + ambient[1] = real_t(a[1]); + ambient[2] = real_t(a[2]); + } + + void SetSpecular(std::array &a) { + specular[0] = real_t(a[0]); + specular[1] = real_t(a[1]); + specular[2] = real_t(a[2]); + } + + void SetTransmittance(std::array &a) { + transmittance[0] = real_t(a[0]); + transmittance[1] = real_t(a[1]); + transmittance[2] = real_t(a[2]); + } + + std::string GetCustomParameter(const std::string &key) { + std::map::const_iterator it = + unknown_parameter.find(key); + + if (it != unknown_parameter.end()) { + return it->second; + } + return std::string(); + } + +#endif +}; + +struct tag_t { + std::string name; + + std::vector intValues; + std::vector floatValues; + std::vector stringValues; +}; + +struct joint_and_weight_t { + int joint_id; + real_t weight; +}; + +struct skin_weight_t { + int vertex_id; // Corresponding vertex index in `attrib_t::vertices`. + // Compared to `index_t`, this index must be positive and + // start with 0(does not allow relative indexing) + std::vector weightValues; +}; + +// Index struct to support different indices for vtx/normal/texcoord. +// -1 means not used. +struct index_t { + int vertex_index; + int normal_index; + int texcoord_index; +}; + +struct mesh_t { + std::vector indices; + std::vector + num_face_vertices; // The number of vertices per + // face. 3 = triangle, 4 = quad, + // ... Up to 255 vertices per face. + std::vector material_ids; // per-face material ID + std::vector smoothing_group_ids; // per-face smoothing group + // ID(0 = off. positive value + // = group id) + std::vector tags; // SubD tag +}; + +// struct path_t { +// std::vector indices; // pairs of indices for lines +//}; + +struct lines_t { + // Linear flattened indices. + std::vector indices; // indices for vertices(poly lines) + std::vector num_line_vertices; // The number of vertices per line. +}; + +struct points_t { + std::vector indices; // indices for points +}; + +struct shape_t { + std::string name; + mesh_t mesh; + lines_t lines; + points_t points; +}; + +// Vertex attributes +struct attrib_t { + std::vector vertices; // 'v'(xyz) + + // For backward compatibility, we store vertex weight in separate array. + std::vector vertex_weights; // 'v'(w) + std::vector normals; // 'vn' + std::vector texcoords; // 'vt'(uv) + + // For backward compatibility, we store texture coordinate 'w' in separate + // array. + std::vector texcoord_ws; // 'vt'(w) + std::vector colors; // extension: vertex colors + + // + // TinyObj extension. + // + + // NOTE(syoyo): array index is based on the appearance order. + // To get a corresponding skin weight for a specific vertex id `vid`, + // Need to reconstruct a look up table: `skin_weight_t::vertex_id` == `vid` + // (e.g. using std::map, std::unordered_map) + std::vector skin_weights; + + attrib_t() {} + + // + // For pybind11 + // + const std::vector &GetVertices() const { return vertices; } + + const std::vector &GetVertexWeights() const { return vertex_weights; } +}; + +struct callback_t { + // W is optional and set to 1 if there is no `w` item in `v` line + void (*vertex_cb)(void *user_data, real_t x, real_t y, real_t z, real_t w); + void (*normal_cb)(void *user_data, real_t x, real_t y, real_t z); + + // y and z are optional and set to 0 if there is no `y` and/or `z` item(s) in + // `vt` line. + void (*texcoord_cb)(void *user_data, real_t x, real_t y, real_t z); + + // called per 'f' line. num_indices is the number of face indices(e.g. 3 for + // triangle, 4 for quad) + // 0 will be passed for undefined index in index_t members. + void (*index_cb)(void *user_data, index_t *indices, int num_indices); + // `name` material name, `material_id` = the array index of material_t[]. -1 + // if + // a material not found in .mtl + void (*usemtl_cb)(void *user_data, const char *name, int material_id); + // `materials` = parsed material data. + void (*mtllib_cb)(void *user_data, const material_t *materials, + int num_materials); + // There may be multiple group names + void (*group_cb)(void *user_data, const char **names, int num_names); + void (*object_cb)(void *user_data, const char *name); + + callback_t() + : vertex_cb(NULL), + normal_cb(NULL), + texcoord_cb(NULL), + index_cb(NULL), + usemtl_cb(NULL), + mtllib_cb(NULL), + group_cb(NULL), + object_cb(NULL) {} +}; + +class MaterialReader { + public: + MaterialReader() {} + virtual ~MaterialReader(); + + virtual bool operator()(const std::string &matId, + std::vector *materials, + std::map *matMap, std::string *warn, + std::string *err) = 0; +}; + +/// +/// Read .mtl from a file. +/// +class MaterialFileReader : public MaterialReader { + public: + // Path could contain separator(';' in Windows, ':' in Posix) + explicit MaterialFileReader(const std::string &mtl_basedir) + : m_mtlBaseDir(mtl_basedir) {} + virtual ~MaterialFileReader() TINYOBJ_OVERRIDE {} + virtual bool operator()(const std::string &matId, + std::vector *materials, + std::map *matMap, std::string *warn, + std::string *err) TINYOBJ_OVERRIDE; + + private: + std::string m_mtlBaseDir; +}; + +/// +/// Read .mtl from a stream. +/// +class MaterialStreamReader : public MaterialReader { + public: + explicit MaterialStreamReader(std::istream &inStream) + : m_inStream(inStream) {} + virtual ~MaterialStreamReader() TINYOBJ_OVERRIDE {} + virtual bool operator()(const std::string &matId, + std::vector *materials, + std::map *matMap, std::string *warn, + std::string *err) TINYOBJ_OVERRIDE; + + private: + std::istream &m_inStream; +}; + +// v2 API +struct ObjReaderConfig { + bool triangulate; // triangulate polygon? + + // Currently not used. + // "simple" or empty: Create triangle fan + // "earcut": Use the algorithm based on Ear clipping + std::string triangulation_method; + + /// Parse vertex color. + /// If vertex color is not present, its filled with default value. + /// false = no vertex color + /// This will increase memory of parsed .obj + bool vertex_color; + + /// + /// Search path to .mtl file. + /// Default = "" = search from the same directory of .obj file. + /// Valid only when loading .obj from a file. + /// + std::string mtl_search_path; + + ObjReaderConfig() + : triangulate(true), triangulation_method("simple"), vertex_color(true) {} +}; + +/// +/// Wavefront .obj reader class(v2 API) +/// +class ObjReader { + public: + ObjReader() : valid_(false) {} + + /// + /// Load .obj and .mtl from a file. + /// + /// @param[in] filename wavefront .obj filename + /// @param[in] config Reader configuration + /// + bool ParseFromFile(const std::string &filename, + const ObjReaderConfig &config = ObjReaderConfig()); + + /// + /// Parse .obj from a text string. + /// Need to supply .mtl text string by `mtl_text`. + /// This function ignores `mtllib` line in .obj text. + /// + /// @param[in] obj_text wavefront .obj filename + /// @param[in] mtl_text wavefront .mtl filename + /// @param[in] config Reader configuration + /// + bool ParseFromString(std::string_view obj_text, std::string_view mtl_text, + const ObjReaderConfig &config = ObjReaderConfig()); + + /// + /// .obj was loaded or parsed correctly. + /// + bool Valid() const { return valid_; } + + const attrib_t &GetAttrib() const { return attrib_; } + + const std::vector &GetShapes() const { return shapes_; } + + const std::vector &GetMaterials() const { return materials_; } + + /// + /// Warning message(may be filled after `Load` or `Parse`) + /// + const std::string &Warning() const { return warning_; } + + /// + /// Error message(filled when `Load` or `Parse` failed) + /// + const std::string &Error() const { return error_; } + + private: + bool valid_; + + attrib_t attrib_; + std::vector shapes_; + std::vector materials_; + + std::string warning_; + std::string error_; +}; + +/// ==>>========= Legacy v1 API ============================================= + +/// Loads .obj from a file. +/// 'attrib', 'shapes' and 'materials' will be filled with parsed shape data +/// 'shapes' will be filled with parsed shape data +/// Returns true when loading .obj become success. +/// Returns warning message into `warn`, and error message into `err` +/// 'mtl_basedir' is optional, and used for base directory for .mtl file. +/// In default(`NULL'), .mtl file is searched from an application's working +/// directory. +/// 'triangulate' is optional, and used whether triangulate polygon face in .obj +/// or not. +/// Option 'default_vcols_fallback' specifies whether vertex colors should +/// always be defined, even if no colors are given (fallback to white). +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, const char *filename, + const char *mtl_basedir = NULL, bool triangulate = true, + bool default_vcols_fallback = true); + +/// Loads .obj from a file with custom user callback. +/// .mtl is loaded as usual and parsed material_t data will be passed to +/// `callback.mtllib_cb`. +/// Returns true when loading .obj/.mtl become success. +/// Returns warning message into `warn`, and error message into `err` +/// See `examples/callback_api/` for how to use this function. +bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, + void *user_data = NULL, + MaterialReader *readMatFn = NULL, + std::string *warn = NULL, std::string *err = NULL); + +/// Loads object from a std::istream, uses `readMatFn` to retrieve +/// std::istream for materials. +/// Returns true when loading .obj become success. +/// Returns warning and error message into `err` +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, std::istream *inStream, + MaterialReader *readMatFn = NULL, bool triangulate = true, + bool default_vcols_fallback = true); + +/// Loads materials into std::map +void LoadMtl(std::map *material_map, + std::vector *materials, std::istream *inStream, + std::string *warning, std::string *err); + +/// +/// Parse texture name and texture option for custom texture parameter through +/// material::unknown_parameter +/// +/// @param[out] texname Parsed texture name +/// @param[out] texopt Parsed texopt +/// @param[in] linebuf Input string +/// +bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, + const char *linebuf); + +/// =<<========== Legacy v1 API ============================================= + +} // namespace tinyobj + +#endif // TINY_OBJ_LOADER_H_ + +#ifdef TINYOBJLOADER_IMPLEMENTATION +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT + +#ifdef TINYOBJLOADER_DONOT_INCLUDE_MAPBOX_EARCUT +// Assume earcut.hpp is included outside of tiny_obj_loader.h +#else + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Weverything" +#endif + +#include +#include "mapbox/earcut.hpp" + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif + +#endif // TINYOBJLOADER_USE_MAPBOX_EARCUT + +namespace tinyobj { + +MaterialReader::~MaterialReader() {} + +struct vertex_index_t { + int v_idx, vt_idx, vn_idx; + vertex_index_t() : v_idx(-1), vt_idx(-1), vn_idx(-1) {} + explicit vertex_index_t(int idx) : v_idx(idx), vt_idx(idx), vn_idx(idx) {} + vertex_index_t(int vidx, int vtidx, int vnidx) + : v_idx(vidx), vt_idx(vtidx), vn_idx(vnidx) {} +}; + +// Internal data structure for face representation +// index + smoothing group. +struct face_t { + unsigned int + smoothing_group_id; // smoothing group id. 0 = smoothing groupd is off. + int pad_; + std::vector vertex_indices; // face vertex indices. + + face_t() : smoothing_group_id(0), pad_(0) {} +}; + +// Internal data structure for line representation +struct __line_t { + // l v1/vt1 v2/vt2 ... + // In the specification, line primitrive does not have normal index, but + // TinyObjLoader allow it + std::vector vertex_indices; +}; + +// Internal data structure for points representation +struct __points_t { + // p v1 v2 ... + // In the specification, point primitrive does not have normal index and + // texture coord index, but TinyObjLoader allow it. + std::vector vertex_indices; +}; + +struct tag_sizes { + tag_sizes() : num_ints(0), num_reals(0), num_strings(0) {} + int num_ints; + int num_reals; + int num_strings; +}; + +struct obj_shape { + std::vector v; + std::vector vn; + std::vector vt; +}; + +// +// Manages group of primitives(face, line, points, ...) +struct PrimGroup { + std::vector faceGroup; + std::vector<__line_t> lineGroup; + std::vector<__points_t> pointsGroup; + + void clear() { + faceGroup.clear(); + lineGroup.clear(); + pointsGroup.clear(); + } + + bool IsEmpty() const { + return faceGroup.empty() && lineGroup.empty() && pointsGroup.empty(); + } + + // TODO(syoyo): bspline, surface, ... +}; + +// See +// http://stackoverflow.com/questions/6089231/getting-std-ifstream-to-handle-lf-cr-and-crlf +static std::istream &safeGetline(std::istream &is, std::string &t) { + t.clear(); + + // The characters in the stream are read one-by-one using a std::streambuf. + // That is faster than reading them one-by-one using the std::istream. + // Code that uses streambuf this way must be guarded by a sentry object. + // The sentry object performs various tasks, + // such as thread synchronization and updating the stream state. + + std::istream::sentry se(is, true); + std::streambuf *sb = is.rdbuf(); + + if (se) { + for (;;) { + int c = sb->sbumpc(); + switch (c) { + case '\n': + return is; + case '\r': + if (sb->sgetc() == '\n') sb->sbumpc(); + return is; + case EOF: + // Also handle the case when the last line has no line ending + if (t.empty()) is.setstate(std::ios::eofbit); + return is; + default: + t += static_cast(c); + } + } + } + + return is; +} + +#define IS_SPACE(x) (((x) == ' ') || ((x) == '\t')) +#define IS_DIGIT(x) \ + (static_cast((x) - '0') < static_cast(10)) +#define IS_NEW_LINE(x) (((x) == '\r') || ((x) == '\n') || ((x) == '\0')) + +// Make index zero-base, and also support relative index. +static inline bool fixIndex(int idx, int n, int *ret) { + if (!ret) { + return false; + } + + if (idx > 0) { + (*ret) = idx - 1; + return true; + } + + if (idx == 0) { + // zero is not allowed according to the spec. + return false; + } + + if (idx < 0) { + (*ret) = n + idx; // negative value = relative + return true; + } + + return false; // never reach here. +} + +static inline std::string parseString(const char **token) { + std::string s; + (*token) += strspn((*token), " \t"); + size_t e = strcspn((*token), " \t\r"); + s = std::string((*token), &(*token)[e]); + (*token) += e; + return s; +} + +static inline int parseInt(const char **token) { + (*token) += strspn((*token), " \t"); + int i = atoi((*token)); + (*token) += strcspn((*token), " \t\r"); + return i; +} + +// Tries to parse a floating point number located at s. +// +// s_end should be a location in the string where reading should absolutely +// stop. For example at the end of the string, to prevent buffer overflows. +// +// Parses the following EBNF grammar: +// sign = "+" | "-" ; +// END = ? anything not in digit ? +// digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; +// integer = [sign] , digit , {digit} ; +// decimal = integer , ["." , integer] ; +// float = ( decimal , END ) | ( decimal , ("E" | "e") , integer , END ) ; +// +// Valid strings are for example: +// -0 +3.1417e+2 -0.0E-3 1.0324 -1.41 11e2 +// +// If the parsing is a success, result is set to the parsed value and true +// is returned. +// +// The function is greedy and will parse until any of the following happens: +// - a non-conforming character is encountered. +// - s_end is reached. +// +// The following situations triggers a failure: +// - s >= s_end. +// - parse failure. +// +static bool tryParseDouble(const char *s, const char *s_end, double *result) { + + if (s >= s_end) { + return false; + } + + double mantissa = 0.0; + // This exponent is base 2 rather than 10. + // However the exponent we parse is supposed to be one of ten, + // thus we must take care to convert the exponent/and or the + // mantissa to a * 2^E, where a is the mantissa and E is the + // exponent. + // To get the final double we will use ldexp, it requires the + // exponent to be in base 2. + int exponent = 0; + + // NOTE: THESE MUST BE DECLARED HERE SINCE WE ARE NOT ALLOWED + // TO JUMP OVER DEFINITIONS. + char sign = '+'; + char exp_sign = '+'; + char const *curr = s; + + // How many characters were read in a loop. + int read = 0; + // Tells whether a loop terminated due to reaching s_end. + bool end_not_reached = false; + bool leading_decimal_dots = false; + + /* + BEGIN PARSING. + */ + + // Find out what sign we've got. + if (*curr == '+' || *curr == '-') { + sign = *curr; + curr++; + if ((curr != s_end) && (*curr == '.')) { + // accept. Somethig like `.7e+2`, `-.5234` + leading_decimal_dots = true; + } + } else if (IS_DIGIT(*curr)) { /* Pass through. */ + } else if (*curr == '.') { + // accept. Somethig like `.7e+2`, `-.5234` + leading_decimal_dots = true; + } else { + goto fail; + } + + // Read the integer part. + end_not_reached = (curr != s_end); + if (!leading_decimal_dots) { + while (end_not_reached && IS_DIGIT(*curr)) { + mantissa *= 10; + mantissa += static_cast(*curr - 0x30); + curr++; + read++; + end_not_reached = (curr != s_end); + } + + // We must make sure we actually got something. + if (read == 0) goto fail; + } + + // We allow numbers of form "#", "###" etc. + if (!end_not_reached) goto assemble; + + // Read the decimal part. + if (*curr == '.') { + curr++; + read = 1; + end_not_reached = (curr != s_end); + while (end_not_reached && IS_DIGIT(*curr)) { + static const double pow_lut[] = { + 1.0, 0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001, + }; + const int lut_entries = sizeof pow_lut / sizeof pow_lut[0]; + + // NOTE: Don't use powf here, it will absolutely murder precision. + mantissa += static_cast(*curr - 0x30) * + (read < lut_entries ? pow_lut[read] : std::pow(10.0, -read)); + read++; + curr++; + end_not_reached = (curr != s_end); + } + } else if (*curr == 'e' || *curr == 'E') { + } else { + goto assemble; + } + + if (!end_not_reached) goto assemble; + + // Read the exponent part. + if (*curr == 'e' || *curr == 'E') { + curr++; + // Figure out if a sign is present and if it is. + end_not_reached = (curr != s_end); + if (end_not_reached && (*curr == '+' || *curr == '-')) { + exp_sign = *curr; + curr++; + } else if (IS_DIGIT(*curr)) { /* Pass through. */ + } else { + // Empty E is not allowed. + goto fail; + } + + read = 0; + end_not_reached = (curr != s_end); + while (end_not_reached && IS_DIGIT(*curr)) { + // To avoid annoying MSVC's min/max macro definiton, + // Use hardcoded int max value + if (exponent > (2147483647/10)) { // 2147483647 = std::numeric_limits::max() + // Integer overflow + goto fail; + } + exponent *= 10; + exponent += static_cast(*curr - 0x30); + curr++; + read++; + end_not_reached = (curr != s_end); + } + exponent *= (exp_sign == '+' ? 1 : -1); + if (read == 0) goto fail; + } + +assemble: + *result = (sign == '+' ? 1 : -1) * + (exponent ? std::ldexp(mantissa * std::pow(5.0, exponent), exponent) + : mantissa); + return true; +fail: + return false; +} + +static inline real_t parseReal(const char **token, double default_value = 0.0) { + (*token) += strspn((*token), " \t"); + const char *end = (*token) + strcspn((*token), " \t\r"); + double val = default_value; + tryParseDouble((*token), end, &val); + real_t f = static_cast(val); + (*token) = end; + return f; +} + +static inline bool parseReal(const char **token, real_t *out) { + (*token) += strspn((*token), " \t"); + const char *end = (*token) + strcspn((*token), " \t\r"); + double val; + bool ret = tryParseDouble((*token), end, &val); + if (ret) { + real_t f = static_cast(val); + (*out) = f; + } + (*token) = end; + return ret; +} + +static inline void parseReal2(real_t *x, real_t *y, const char **token, + const double default_x = 0.0, + const double default_y = 0.0) { + (*x) = parseReal(token, default_x); + (*y) = parseReal(token, default_y); +} + +static inline void parseReal3(real_t *x, real_t *y, real_t *z, + const char **token, const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + (*x) = parseReal(token, default_x); + (*y) = parseReal(token, default_y); + (*z) = parseReal(token, default_z); +} + +static inline void parseV(real_t *x, real_t *y, real_t *z, real_t *w, + const char **token, const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0, + const double default_w = 1.0) { + (*x) = parseReal(token, default_x); + (*y) = parseReal(token, default_y); + (*z) = parseReal(token, default_z); + (*w) = parseReal(token, default_w); +} + +// Extension: parse vertex with colors(6 items) +static inline bool parseVertexWithColor(real_t *x, real_t *y, real_t *z, + real_t *r, real_t *g, real_t *b, + const char **token, + const double default_x = 0.0, + const double default_y = 0.0, + const double default_z = 0.0) { + (*x) = parseReal(token, default_x); + (*y) = parseReal(token, default_y); + (*z) = parseReal(token, default_z); + + const bool found_color = + parseReal(token, r) && parseReal(token, g) && parseReal(token, b); + + if (!found_color) { + (*r) = (*g) = (*b) = 1.0; + } + + return found_color; +} + +static inline bool parseOnOff(const char **token, bool default_value = true) { + (*token) += strspn((*token), " \t"); + const char *end = (*token) + strcspn((*token), " \t\r"); + + bool ret = default_value; + if ((0 == strncmp((*token), "on", 2))) { + ret = true; + } else if ((0 == strncmp((*token), "off", 3))) { + ret = false; + } + + (*token) = end; + return ret; +} + +static inline texture_type_t parseTextureType( + const char **token, texture_type_t default_value = TEXTURE_TYPE_NONE) { + (*token) += strspn((*token), " \t"); + const char *end = (*token) + strcspn((*token), " \t\r"); + texture_type_t ty = default_value; + + if ((0 == strncmp((*token), "cube_top", strlen("cube_top")))) { + ty = TEXTURE_TYPE_CUBE_TOP; + } else if ((0 == strncmp((*token), "cube_bottom", strlen("cube_bottom")))) { + ty = TEXTURE_TYPE_CUBE_BOTTOM; + } else if ((0 == strncmp((*token), "cube_left", strlen("cube_left")))) { + ty = TEXTURE_TYPE_CUBE_LEFT; + } else if ((0 == strncmp((*token), "cube_right", strlen("cube_right")))) { + ty = TEXTURE_TYPE_CUBE_RIGHT; + } else if ((0 == strncmp((*token), "cube_front", strlen("cube_front")))) { + ty = TEXTURE_TYPE_CUBE_FRONT; + } else if ((0 == strncmp((*token), "cube_back", strlen("cube_back")))) { + ty = TEXTURE_TYPE_CUBE_BACK; + } else if ((0 == strncmp((*token), "sphere", strlen("sphere")))) { + ty = TEXTURE_TYPE_SPHERE; + } + + (*token) = end; + return ty; +} + +static tag_sizes parseTagTriple(const char **token) { + tag_sizes ts; + + (*token) += strspn((*token), " \t"); + ts.num_ints = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + return ts; + } + + (*token)++; // Skip '/' + + (*token) += strspn((*token), " \t"); + ts.num_reals = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + return ts; + } + (*token)++; // Skip '/' + + ts.num_strings = parseInt(token); + + return ts; +} + +// Parse triples with index offsets: i, i/j/k, i//k, i/j +static bool parseTriple(const char **token, int vsize, int vnsize, int vtsize, + vertex_index_t *ret) { + if (!ret) { + return false; + } + + vertex_index_t vi(-1); + + if (!fixIndex(atoi((*token)), vsize, &(vi.v_idx))) { + return false; + } + + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + (*ret) = vi; + return true; + } + (*token)++; + + // i//k + if ((*token)[0] == '/') { + (*token)++; + if (!fixIndex(atoi((*token)), vnsize, &(vi.vn_idx))) { + return false; + } + (*token) += strcspn((*token), "/ \t\r"); + (*ret) = vi; + return true; + } + + // i/j/k or i/j + if (!fixIndex(atoi((*token)), vtsize, &(vi.vt_idx))) { + return false; + } + + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + (*ret) = vi; + return true; + } + + // i/j/k + (*token)++; // skip '/' + if (!fixIndex(atoi((*token)), vnsize, &(vi.vn_idx))) { + return false; + } + (*token) += strcspn((*token), "/ \t\r"); + + (*ret) = vi; + + return true; +} + +// Parse raw triples: i, i/j/k, i//k, i/j +static vertex_index_t parseRawTriple(const char **token) { + vertex_index_t vi(static_cast(0)); // 0 is an invalid index in OBJ + + vi.v_idx = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + return vi; + } + (*token)++; + + // i//k + if ((*token)[0] == '/') { + (*token)++; + vi.vn_idx = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + return vi; + } + + // i/j/k or i/j + vi.vt_idx = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + if ((*token)[0] != '/') { + return vi; + } + + // i/j/k + (*token)++; // skip '/' + vi.vn_idx = atoi((*token)); + (*token) += strcspn((*token), "/ \t\r"); + return vi; +} + +bool ParseTextureNameAndOption(std::string *texname, texture_option_t *texopt, + const char *linebuf) { + // @todo { write more robust lexer and parser. } + bool found_texname = false; + std::string texture_name; + + const char *token = linebuf; // Assume line ends with NULL + + while (!IS_NEW_LINE((*token))) { + token += strspn(token, " \t"); // skip space + if ((0 == strncmp(token, "-blendu", 7)) && IS_SPACE((token[7]))) { + token += 8; + texopt->blendu = parseOnOff(&token, /* default */ true); + } else if ((0 == strncmp(token, "-blendv", 7)) && IS_SPACE((token[7]))) { + token += 8; + texopt->blendv = parseOnOff(&token, /* default */ true); + } else if ((0 == strncmp(token, "-clamp", 6)) && IS_SPACE((token[6]))) { + token += 7; + texopt->clamp = parseOnOff(&token, /* default */ true); + } else if ((0 == strncmp(token, "-boost", 6)) && IS_SPACE((token[6]))) { + token += 7; + texopt->sharpness = parseReal(&token, 1.0); + } else if ((0 == strncmp(token, "-bm", 3)) && IS_SPACE((token[3]))) { + token += 4; + texopt->bump_multiplier = parseReal(&token, 1.0); + } else if ((0 == strncmp(token, "-o", 2)) && IS_SPACE((token[2]))) { + token += 3; + parseReal3(&(texopt->origin_offset[0]), &(texopt->origin_offset[1]), + &(texopt->origin_offset[2]), &token); + } else if ((0 == strncmp(token, "-s", 2)) && IS_SPACE((token[2]))) { + token += 3; + parseReal3(&(texopt->scale[0]), &(texopt->scale[1]), &(texopt->scale[2]), + &token, 1.0, 1.0, 1.0); + } else if ((0 == strncmp(token, "-t", 2)) && IS_SPACE((token[2]))) { + token += 3; + parseReal3(&(texopt->turbulence[0]), &(texopt->turbulence[1]), + &(texopt->turbulence[2]), &token); + } else if ((0 == strncmp(token, "-type", 5)) && IS_SPACE((token[5]))) { + token += 5; + texopt->type = parseTextureType((&token), TEXTURE_TYPE_NONE); + } else if ((0 == strncmp(token, "-texres", 7)) && IS_SPACE((token[7]))) { + token += 7; + // TODO(syoyo): Check if arg is int type. + texopt->texture_resolution = parseInt(&token); + } else if ((0 == strncmp(token, "-imfchan", 8)) && IS_SPACE((token[8]))) { + token += 9; + token += strspn(token, " \t"); + const char *end = token + strcspn(token, " \t\r"); + if ((end - token) == 1) { // Assume one char for -imfchan + texopt->imfchan = (*token); + } + token = end; + } else if ((0 == strncmp(token, "-mm", 3)) && IS_SPACE((token[3]))) { + token += 4; + parseReal2(&(texopt->brightness), &(texopt->contrast), &token, 0.0, 1.0); + } else if ((0 == strncmp(token, "-colorspace", 11)) && + IS_SPACE((token[11]))) { + token += 12; + texopt->colorspace = parseString(&token); + } else { +// Assume texture filename +#if 0 + size_t len = strcspn(token, " \t\r"); // untile next space + texture_name = std::string(token, token + len); + token += len; + + token += strspn(token, " \t"); // skip space +#else + // Read filename until line end to parse filename containing whitespace + // TODO(syoyo): Support parsing texture option flag after the filename. + texture_name = std::string(token); + token += texture_name.length(); +#endif + + found_texname = true; + } + } + + if (found_texname) { + (*texname) = texture_name; + return true; + } else { + return false; + } +} + +static void InitTexOpt(texture_option_t *texopt, const bool is_bump) { + if (is_bump) { + texopt->imfchan = 'l'; + } else { + texopt->imfchan = 'm'; + } + texopt->bump_multiplier = static_cast(1.0); + texopt->clamp = false; + texopt->blendu = true; + texopt->blendv = true; + texopt->sharpness = static_cast(1.0); + texopt->brightness = static_cast(0.0); + texopt->contrast = static_cast(1.0); + texopt->origin_offset[0] = static_cast(0.0); + texopt->origin_offset[1] = static_cast(0.0); + texopt->origin_offset[2] = static_cast(0.0); + texopt->scale[0] = static_cast(1.0); + texopt->scale[1] = static_cast(1.0); + texopt->scale[2] = static_cast(1.0); + texopt->turbulence[0] = static_cast(0.0); + texopt->turbulence[1] = static_cast(0.0); + texopt->turbulence[2] = static_cast(0.0); + texopt->texture_resolution = -1; + texopt->type = TEXTURE_TYPE_NONE; +} + +static void InitMaterial(material_t *material) { + InitTexOpt(&material->ambient_texopt, /* is_bump */ false); + InitTexOpt(&material->diffuse_texopt, /* is_bump */ false); + InitTexOpt(&material->specular_texopt, /* is_bump */ false); + InitTexOpt(&material->specular_highlight_texopt, /* is_bump */ false); + InitTexOpt(&material->bump_texopt, /* is_bump */ true); + InitTexOpt(&material->displacement_texopt, /* is_bump */ false); + InitTexOpt(&material->alpha_texopt, /* is_bump */ false); + InitTexOpt(&material->reflection_texopt, /* is_bump */ false); + InitTexOpt(&material->roughness_texopt, /* is_bump */ false); + InitTexOpt(&material->metallic_texopt, /* is_bump */ false); + InitTexOpt(&material->sheen_texopt, /* is_bump */ false); + InitTexOpt(&material->emissive_texopt, /* is_bump */ false); + InitTexOpt(&material->normal_texopt, + /* is_bump */ false); // @fixme { is_bump will be true? } + material->name = ""; + material->ambient_texname = ""; + material->diffuse_texname = ""; + material->specular_texname = ""; + material->specular_highlight_texname = ""; + material->bump_texname = ""; + material->displacement_texname = ""; + material->reflection_texname = ""; + material->alpha_texname = ""; + for (int i = 0; i < 3; i++) { + material->ambient[i] = static_cast(0.0); + material->diffuse[i] = static_cast(0.0); + material->specular[i] = static_cast(0.0); + material->transmittance[i] = static_cast(0.0); + material->emission[i] = static_cast(0.0); + } + material->illum = 0; + material->dissolve = static_cast(1.0); + material->shininess = static_cast(1.0); + material->ior = static_cast(1.0); + + material->roughness = static_cast(0.0); + material->metallic = static_cast(0.0); + material->sheen = static_cast(0.0); + material->clearcoat_thickness = static_cast(0.0); + material->clearcoat_roughness = static_cast(0.0); + material->anisotropy_rotation = static_cast(0.0); + material->anisotropy = static_cast(0.0); + material->roughness_texname = ""; + material->metallic_texname = ""; + material->sheen_texname = ""; + material->emissive_texname = ""; + material->normal_texname = ""; + + material->unknown_parameter.clear(); +} + +// code from https://wrf.ecse.rpi.edu//Research/Short_Notes/pnpoly.html +template +static int pnpoly(int nvert, T *vertx, T *verty, T testx, T testy) { + int i, j, c = 0; + for (i = 0, j = nvert - 1; i < nvert; j = i++) { + if (((verty[i] > testy) != (verty[j] > testy)) && + (testx < + (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + + vertx[i])) + c = !c; + } + return c; +} + +// TODO(syoyo): refactor function. +static bool exportGroupsToShape(shape_t *shape, const PrimGroup &prim_group, + const std::vector &tags, + const int material_id, const std::string &name, + bool triangulate, const std::vector &v, + std::string *warn) { + if (prim_group.IsEmpty()) { + return false; + } + + shape->name = name; + + // polygon + if (!prim_group.faceGroup.empty()) { + // Flatten vertices and indices + for (size_t i = 0; i < prim_group.faceGroup.size(); i++) { + const face_t &face = prim_group.faceGroup[i]; + + size_t npolys = face.vertex_indices.size(); + + if (npolys < 3) { + // Face must have 3+ vertices. + if (warn) { + (*warn) += "Degenerated face found\n."; + } + continue; + } + + if (triangulate) { + if (npolys == 4) { + vertex_index_t i0 = face.vertex_indices[0]; + vertex_index_t i1 = face.vertex_indices[1]; + vertex_index_t i2 = face.vertex_indices[2]; + vertex_index_t i3 = face.vertex_indices[3]; + + size_t vi0 = size_t(i0.v_idx); + size_t vi1 = size_t(i1.v_idx); + size_t vi2 = size_t(i2.v_idx); + size_t vi3 = size_t(i3.v_idx); + + if (((3 * vi0 + 2) >= v.size()) || ((3 * vi1 + 2) >= v.size()) || + ((3 * vi2 + 2) >= v.size()) || ((3 * vi3 + 2) >= v.size())) { + // Invalid triangle. + // FIXME(syoyo): Is it ok to simply skip this invalid triangle? + if (warn) { + (*warn) += "Face with invalid vertex index found.\n"; + } + continue; + } + + real_t v0x = v[vi0 * 3 + 0]; + real_t v0y = v[vi0 * 3 + 1]; + real_t v0z = v[vi0 * 3 + 2]; + real_t v1x = v[vi1 * 3 + 0]; + real_t v1y = v[vi1 * 3 + 1]; + real_t v1z = v[vi1 * 3 + 2]; + real_t v2x = v[vi2 * 3 + 0]; + real_t v2y = v[vi2 * 3 + 1]; + real_t v2z = v[vi2 * 3 + 2]; + real_t v3x = v[vi3 * 3 + 0]; + real_t v3y = v[vi3 * 3 + 1]; + real_t v3z = v[vi3 * 3 + 2]; + + // There are two candidates to split the quad into two triangles. + // + // Choose the shortest edge. + // TODO: Is it better to determine the edge to split by calculating + // the area of each triangle? + // + // +---+ + // |\ | + // | \ | + // | \| + // +---+ + // + // +---+ + // | /| + // | / | + // |/ | + // +---+ + + real_t e02x = v2x - v0x; + real_t e02y = v2y - v0y; + real_t e02z = v2z - v0z; + real_t e13x = v3x - v1x; + real_t e13y = v3y - v1y; + real_t e13z = v3z - v1z; + + real_t sqr02 = e02x * e02x + e02y * e02y + e02z * e02z; + real_t sqr13 = e13x * e13x + e13y * e13y + e13z * e13z; + + index_t idx0, idx1, idx2, idx3; + + idx0.vertex_index = i0.v_idx; + idx0.normal_index = i0.vn_idx; + idx0.texcoord_index = i0.vt_idx; + idx1.vertex_index = i1.v_idx; + idx1.normal_index = i1.vn_idx; + idx1.texcoord_index = i1.vt_idx; + idx2.vertex_index = i2.v_idx; + idx2.normal_index = i2.vn_idx; + idx2.texcoord_index = i2.vt_idx; + idx3.vertex_index = i3.v_idx; + idx3.normal_index = i3.vn_idx; + idx3.texcoord_index = i3.vt_idx; + + if (sqr02 < sqr13) { + // [0, 1, 2], [0, 2, 3] + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx2); + + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx2); + shape->mesh.indices.push_back(idx3); + } else { + // [0, 1, 3], [1, 2, 3] + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx3); + + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx2); + shape->mesh.indices.push_back(idx3); + } + + // Two triangle faces + shape->mesh.num_face_vertices.push_back(3); + shape->mesh.num_face_vertices.push_back(3); + + shape->mesh.material_ids.push_back(material_id); + shape->mesh.material_ids.push_back(material_id); + + shape->mesh.smoothing_group_ids.push_back(face.smoothing_group_id); + shape->mesh.smoothing_group_ids.push_back(face.smoothing_group_id); + + } else { + vertex_index_t i0 = face.vertex_indices[0]; + vertex_index_t i1(-1); + vertex_index_t i2 = face.vertex_indices[1]; + + // find the two axes to work in + size_t axes[2] = {1, 2}; + for (size_t k = 0; k < npolys; ++k) { + i0 = face.vertex_indices[(k + 0) % npolys]; + i1 = face.vertex_indices[(k + 1) % npolys]; + i2 = face.vertex_indices[(k + 2) % npolys]; + size_t vi0 = size_t(i0.v_idx); + size_t vi1 = size_t(i1.v_idx); + size_t vi2 = size_t(i2.v_idx); + + if (((3 * vi0 + 2) >= v.size()) || ((3 * vi1 + 2) >= v.size()) || + ((3 * vi2 + 2) >= v.size())) { + // Invalid triangle. + // FIXME(syoyo): Is it ok to simply skip this invalid triangle? + continue; + } + real_t v0x = v[vi0 * 3 + 0]; + real_t v0y = v[vi0 * 3 + 1]; + real_t v0z = v[vi0 * 3 + 2]; + real_t v1x = v[vi1 * 3 + 0]; + real_t v1y = v[vi1 * 3 + 1]; + real_t v1z = v[vi1 * 3 + 2]; + real_t v2x = v[vi2 * 3 + 0]; + real_t v2y = v[vi2 * 3 + 1]; + real_t v2z = v[vi2 * 3 + 2]; + real_t e0x = v1x - v0x; + real_t e0y = v1y - v0y; + real_t e0z = v1z - v0z; + real_t e1x = v2x - v1x; + real_t e1y = v2y - v1y; + real_t e1z = v2z - v1z; + real_t cx = std::fabs(e0y * e1z - e0z * e1y); + real_t cy = std::fabs(e0z * e1x - e0x * e1z); + real_t cz = std::fabs(e0x * e1y - e0y * e1x); + const real_t epsilon = std::numeric_limits::epsilon(); + // std::cout << "cx " << cx << ", cy " << cy << ", cz " << cz << + // "\n"; + if (cx > epsilon || cy > epsilon || cz > epsilon) { + // std::cout << "corner\n"; + // found a corner + if (cx > cy && cx > cz) { + // std::cout << "pattern0\n"; + } else { + // std::cout << "axes[0] = 0\n"; + axes[0] = 0; + if (cz > cx && cz > cy) { + // std::cout << "axes[1] = 1\n"; + axes[1] = 1; + } + } + break; + } + } + +#ifdef TINYOBJLOADER_USE_MAPBOX_EARCUT + using Point = std::array; + + // first polyline define the main polygon. + // following polylines define holes(not used in tinyobj). + std::vector > polygon; + + std::vector polyline; + + // Fill polygon data(facevarying vertices). + for (size_t k = 0; k < npolys; k++) { + i0 = face.vertex_indices[k]; + size_t vi0 = size_t(i0.v_idx); + + assert(((3 * vi0 + 2) < v.size())); + + real_t v0x = v[vi0 * 3 + axes[0]]; + real_t v0y = v[vi0 * 3 + axes[1]]; + + polyline.push_back({v0x, v0y}); + } + + polygon.push_back(polyline); + std::vector indices = mapbox::earcut(polygon); + // => result = 3 * faces, clockwise + + assert(indices.size() % 3 == 0); + + // Reconstruct vertex_index_t + for (size_t k = 0; k < indices.size() / 3; k++) { + { + index_t idx0, idx1, idx2; + idx0.vertex_index = face.vertex_indices[indices[3 * k + 0]].v_idx; + idx0.normal_index = + face.vertex_indices[indices[3 * k + 0]].vn_idx; + idx0.texcoord_index = + face.vertex_indices[indices[3 * k + 0]].vt_idx; + idx1.vertex_index = face.vertex_indices[indices[3 * k + 1]].v_idx; + idx1.normal_index = + face.vertex_indices[indices[3 * k + 1]].vn_idx; + idx1.texcoord_index = + face.vertex_indices[indices[3 * k + 1]].vt_idx; + idx2.vertex_index = face.vertex_indices[indices[3 * k + 2]].v_idx; + idx2.normal_index = + face.vertex_indices[indices[3 * k + 2]].vn_idx; + idx2.texcoord_index = + face.vertex_indices[indices[3 * k + 2]].vt_idx; + + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx2); + + shape->mesh.num_face_vertices.push_back(3); + shape->mesh.material_ids.push_back(material_id); + shape->mesh.smoothing_group_ids.push_back( + face.smoothing_group_id); + } + } + +#else // Built-in ear clipping triangulation + + + face_t remainingFace = face; // copy + size_t guess_vert = 0; + vertex_index_t ind[3]; + real_t vx[3]; + real_t vy[3]; + + // How many iterations can we do without decreasing the remaining + // vertices. + size_t remainingIterations = face.vertex_indices.size(); + size_t previousRemainingVertices = + remainingFace.vertex_indices.size(); + + while (remainingFace.vertex_indices.size() > 3 && + remainingIterations > 0) { + // std::cout << "remainingIterations " << remainingIterations << + // "\n"; + + npolys = remainingFace.vertex_indices.size(); + if (guess_vert >= npolys) { + guess_vert -= npolys; + } + + if (previousRemainingVertices != npolys) { + // The number of remaining vertices decreased. Reset counters. + previousRemainingVertices = npolys; + remainingIterations = npolys; + } else { + // We didn't consume a vertex on previous iteration, reduce the + // available iterations. + remainingIterations--; + } + + for (size_t k = 0; k < 3; k++) { + ind[k] = remainingFace.vertex_indices[(guess_vert + k) % npolys]; + size_t vi = size_t(ind[k].v_idx); + if (((vi * 3 + axes[0]) >= v.size()) || + ((vi * 3 + axes[1]) >= v.size())) { + // ??? + vx[k] = static_cast(0.0); + vy[k] = static_cast(0.0); + } else { + vx[k] = v[vi * 3 + axes[0]]; + vy[k] = v[vi * 3 + axes[1]]; + } + } + + // + // area is calculated per face + // + real_t e0x = vx[1] - vx[0]; + real_t e0y = vy[1] - vy[0]; + real_t e1x = vx[2] - vx[1]; + real_t e1y = vy[2] - vy[1]; + real_t cross = e0x * e1y - e0y * e1x; + // std::cout << "axes = " << axes[0] << ", " << axes[1] << "\n"; + // std::cout << "e0x, e0y, e1x, e1y " << e0x << ", " << e0y << ", " + // << e1x << ", " << e1y << "\n"; + + real_t area = (vx[0] * vy[1] - vy[0] * vx[1]) * static_cast(0.5); + // std::cout << "cross " << cross << ", area " << area << "\n"; + // if an internal angle + if (cross * area < static_cast(0.0)) { + // std::cout << "internal \n"; + guess_vert += 1; + // std::cout << "guess vert : " << guess_vert << "\n"; + continue; + } + + // check all other verts in case they are inside this triangle + bool overlap = false; + for (size_t otherVert = 3; otherVert < npolys; ++otherVert) { + size_t idx = (guess_vert + otherVert) % npolys; + + if (idx >= remainingFace.vertex_indices.size()) { + // std::cout << "???0\n"; + // ??? + continue; + } + + size_t ovi = size_t(remainingFace.vertex_indices[idx].v_idx); + + if (((ovi * 3 + axes[0]) >= v.size()) || + ((ovi * 3 + axes[1]) >= v.size())) { + // std::cout << "???1\n"; + // ??? + continue; + } + real_t tx = v[ovi * 3 + axes[0]]; + real_t ty = v[ovi * 3 + axes[1]]; + if (pnpoly(3, vx, vy, tx, ty)) { + // std::cout << "overlap\n"; + overlap = true; + break; + } + } + + if (overlap) { + // std::cout << "overlap2\n"; + guess_vert += 1; + continue; + } + + // this triangle is an ear + { + index_t idx0, idx1, idx2; + idx0.vertex_index = ind[0].v_idx; + idx0.normal_index = ind[0].vn_idx; + idx0.texcoord_index = ind[0].vt_idx; + idx1.vertex_index = ind[1].v_idx; + idx1.normal_index = ind[1].vn_idx; + idx1.texcoord_index = ind[1].vt_idx; + idx2.vertex_index = ind[2].v_idx; + idx2.normal_index = ind[2].vn_idx; + idx2.texcoord_index = ind[2].vt_idx; + + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx2); + + shape->mesh.num_face_vertices.push_back(3); + shape->mesh.material_ids.push_back(material_id); + shape->mesh.smoothing_group_ids.push_back( + face.smoothing_group_id); + } + + // remove v1 from the list + size_t removed_vert_index = (guess_vert + 1) % npolys; + while (removed_vert_index + 1 < npolys) { + remainingFace.vertex_indices[removed_vert_index] = + remainingFace.vertex_indices[removed_vert_index + 1]; + removed_vert_index += 1; + } + remainingFace.vertex_indices.pop_back(); + } + + // std::cout << "remainingFace.vi.size = " << + // remainingFace.vertex_indices.size() << "\n"; + if (remainingFace.vertex_indices.size() == 3) { + i0 = remainingFace.vertex_indices[0]; + i1 = remainingFace.vertex_indices[1]; + i2 = remainingFace.vertex_indices[2]; + { + index_t idx0, idx1, idx2; + idx0.vertex_index = i0.v_idx; + idx0.normal_index = i0.vn_idx; + idx0.texcoord_index = i0.vt_idx; + idx1.vertex_index = i1.v_idx; + idx1.normal_index = i1.vn_idx; + idx1.texcoord_index = i1.vt_idx; + idx2.vertex_index = i2.v_idx; + idx2.normal_index = i2.vn_idx; + idx2.texcoord_index = i2.vt_idx; + + shape->mesh.indices.push_back(idx0); + shape->mesh.indices.push_back(idx1); + shape->mesh.indices.push_back(idx2); + + shape->mesh.num_face_vertices.push_back(3); + shape->mesh.material_ids.push_back(material_id); + shape->mesh.smoothing_group_ids.push_back( + face.smoothing_group_id); + } + } +#endif + } // npolys + } else { + for (size_t k = 0; k < npolys; k++) { + index_t idx; + idx.vertex_index = face.vertex_indices[k].v_idx; + idx.normal_index = face.vertex_indices[k].vn_idx; + idx.texcoord_index = face.vertex_indices[k].vt_idx; + shape->mesh.indices.push_back(idx); + } + + shape->mesh.num_face_vertices.push_back( + static_cast(npolys)); + shape->mesh.material_ids.push_back(material_id); // per face + shape->mesh.smoothing_group_ids.push_back( + face.smoothing_group_id); // per face + } + } + + shape->mesh.tags = tags; + } + + // line + if (!prim_group.lineGroup.empty()) { + // Flatten indices + for (size_t i = 0; i < prim_group.lineGroup.size(); i++) { + for (size_t j = 0; j < prim_group.lineGroup[i].vertex_indices.size(); + j++) { + const vertex_index_t &vi = prim_group.lineGroup[i].vertex_indices[j]; + + index_t idx; + idx.vertex_index = vi.v_idx; + idx.normal_index = vi.vn_idx; + idx.texcoord_index = vi.vt_idx; + + shape->lines.indices.push_back(idx); + } + + shape->lines.num_line_vertices.push_back( + int(prim_group.lineGroup[i].vertex_indices.size())); + } + } + + // points + if (!prim_group.pointsGroup.empty()) { + // Flatten & convert indices + for (size_t i = 0; i < prim_group.pointsGroup.size(); i++) { + for (size_t j = 0; j < prim_group.pointsGroup[i].vertex_indices.size(); + j++) { + const vertex_index_t &vi = prim_group.pointsGroup[i].vertex_indices[j]; + + index_t idx; + idx.vertex_index = vi.v_idx; + idx.normal_index = vi.vn_idx; + idx.texcoord_index = vi.vt_idx; + + shape->points.indices.push_back(idx); + } + } + } + + return true; +} + +// Split a string with specified delimiter character and escape character. +// https://rosettacode.org/wiki/Tokenize_a_string_with_escaping#C.2B.2B +static void SplitString(const std::string &s, char delim, char escape, + std::vector &elems) { + std::string token; + + bool escaping = false; + for (size_t i = 0; i < s.size(); ++i) { + char ch = s[i]; + if (escaping) { + escaping = false; + } else if (ch == escape) { + escaping = true; + continue; + } else if (ch == delim) { + if (!token.empty()) { + elems.push_back(token); + } + token.clear(); + continue; + } + token += ch; + } + + elems.push_back(token); +} + +static std::string JoinPath(const std::string &dir, + const std::string &filename) { + if (dir.empty()) { + return filename; + } else { + // check '/' + char lastChar = *dir.rbegin(); + if (lastChar != '/') { + return dir + std::string("/") + filename; + } else { + return dir + filename; + } + } +} + +void LoadMtl(std::map *material_map, + std::vector *materials, std::istream *inStream, + std::string *warning, std::string *err) { + (void)err; + + // Create a default material anyway. + material_t material; + InitMaterial(&material); + + // Issue 43. `d` wins against `Tr` since `Tr` is not in the MTL specification. + bool has_d = false; + bool has_tr = false; + + // has_kd is used to set a default diffuse value when map_Kd is present + // and Kd is not. + bool has_kd = false; + + std::stringstream warn_ss; + + size_t line_no = 0; + std::string linebuf; + while (inStream->peek() != -1) { + safeGetline(*inStream, linebuf); + line_no++; + + // Trim trailing whitespace. + if (linebuf.size() > 0) { + linebuf = linebuf.substr(0, linebuf.find_last_not_of(" \t") + 1); + } + + // Trim newline '\r\n' or '\n' + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\n') + linebuf.erase(linebuf.size() - 1); + } + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\r') + linebuf.erase(linebuf.size() - 1); + } + + // Skip if empty line. + if (linebuf.empty()) { + continue; + } + + // Skip leading space. + const char *token = linebuf.c_str(); + token += strspn(token, " \t"); + + assert(token); + if (token[0] == '\0') continue; // empty line + + if (token[0] == '#') continue; // comment line + + // new mtl + if ((0 == strncmp(token, "newmtl", 6)) && IS_SPACE((token[6]))) { + // flush previous material. + if (!material.name.empty()) { + material_map->insert(std::pair( + material.name, static_cast(materials->size()))); + materials->push_back(material); + } + + // initial temporary material + InitMaterial(&material); + + has_d = false; + has_tr = false; + + // set new mtl name + token += 7; + { + std::stringstream sstr; + sstr << token; + material.name = sstr.str(); + } + continue; + } + + // ambient + if (token[0] == 'K' && token[1] == 'a' && IS_SPACE((token[2]))) { + token += 2; + real_t r, g, b; + parseReal3(&r, &g, &b, &token); + material.ambient[0] = r; + material.ambient[1] = g; + material.ambient[2] = b; + continue; + } + + // diffuse + if (token[0] == 'K' && token[1] == 'd' && IS_SPACE((token[2]))) { + token += 2; + real_t r, g, b; + parseReal3(&r, &g, &b, &token); + material.diffuse[0] = r; + material.diffuse[1] = g; + material.diffuse[2] = b; + has_kd = true; + continue; + } + + // specular + if (token[0] == 'K' && token[1] == 's' && IS_SPACE((token[2]))) { + token += 2; + real_t r, g, b; + parseReal3(&r, &g, &b, &token); + material.specular[0] = r; + material.specular[1] = g; + material.specular[2] = b; + continue; + } + + // transmittance + if ((token[0] == 'K' && token[1] == 't' && IS_SPACE((token[2]))) || + (token[0] == 'T' && token[1] == 'f' && IS_SPACE((token[2])))) { + token += 2; + real_t r, g, b; + parseReal3(&r, &g, &b, &token); + material.transmittance[0] = r; + material.transmittance[1] = g; + material.transmittance[2] = b; + continue; + } + + // ior(index of refraction) + if (token[0] == 'N' && token[1] == 'i' && IS_SPACE((token[2]))) { + token += 2; + material.ior = parseReal(&token); + continue; + } + + // emission + if (token[0] == 'K' && token[1] == 'e' && IS_SPACE(token[2])) { + token += 2; + real_t r, g, b; + parseReal3(&r, &g, &b, &token); + material.emission[0] = r; + material.emission[1] = g; + material.emission[2] = b; + continue; + } + + // shininess + if (token[0] == 'N' && token[1] == 's' && IS_SPACE(token[2])) { + token += 2; + material.shininess = parseReal(&token); + continue; + } + + // illum model + if (0 == strncmp(token, "illum", 5) && IS_SPACE(token[5])) { + token += 6; + material.illum = parseInt(&token); + continue; + } + + // dissolve + if ((token[0] == 'd' && IS_SPACE(token[1]))) { + token += 1; + material.dissolve = parseReal(&token); + + if (has_tr) { + warn_ss << "Both `d` and `Tr` parameters defined for \"" + << material.name + << "\". Use the value of `d` for dissolve (line " << line_no + << " in .mtl.)\n"; + } + has_d = true; + continue; + } + if (token[0] == 'T' && token[1] == 'r' && IS_SPACE(token[2])) { + token += 2; + if (has_d) { + // `d` wins. Ignore `Tr` value. + warn_ss << "Both `d` and `Tr` parameters defined for \"" + << material.name + << "\". Use the value of `d` for dissolve (line " << line_no + << " in .mtl.)\n"; + } else { + // We invert value of Tr(assume Tr is in range [0, 1]) + // NOTE: Interpretation of Tr is application(exporter) dependent. For + // some application(e.g. 3ds max obj exporter), Tr = d(Issue 43) + material.dissolve = static_cast(1.0) - parseReal(&token); + } + has_tr = true; + continue; + } + + // PBR: roughness + if (token[0] == 'P' && token[1] == 'r' && IS_SPACE(token[2])) { + token += 2; + material.roughness = parseReal(&token); + continue; + } + + // PBR: metallic + if (token[0] == 'P' && token[1] == 'm' && IS_SPACE(token[2])) { + token += 2; + material.metallic = parseReal(&token); + continue; + } + + // PBR: sheen + if (token[0] == 'P' && token[1] == 's' && IS_SPACE(token[2])) { + token += 2; + material.sheen = parseReal(&token); + continue; + } + + // PBR: clearcoat thickness + if (token[0] == 'P' && token[1] == 'c' && IS_SPACE(token[2])) { + token += 2; + material.clearcoat_thickness = parseReal(&token); + continue; + } + + // PBR: clearcoat roughness + if ((0 == strncmp(token, "Pcr", 3)) && IS_SPACE(token[3])) { + token += 4; + material.clearcoat_roughness = parseReal(&token); + continue; + } + + // PBR: anisotropy + if ((0 == strncmp(token, "aniso", 5)) && IS_SPACE(token[5])) { + token += 6; + material.anisotropy = parseReal(&token); + continue; + } + + // PBR: anisotropy rotation + if ((0 == strncmp(token, "anisor", 6)) && IS_SPACE(token[6])) { + token += 7; + material.anisotropy_rotation = parseReal(&token); + continue; + } + + // ambient texture + if ((0 == strncmp(token, "map_Ka", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.ambient_texname), + &(material.ambient_texopt), token); + continue; + } + + // diffuse texture + if ((0 == strncmp(token, "map_Kd", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.diffuse_texname), + &(material.diffuse_texopt), token); + + // Set a decent diffuse default value if a diffuse texture is specified + // without a matching Kd value. + if (!has_kd) { + material.diffuse[0] = static_cast(0.6); + material.diffuse[1] = static_cast(0.6); + material.diffuse[2] = static_cast(0.6); + } + + continue; + } + + // specular texture + if ((0 == strncmp(token, "map_Ks", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.specular_texname), + &(material.specular_texopt), token); + continue; + } + + // specular highlight texture + if ((0 == strncmp(token, "map_Ns", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.specular_highlight_texname), + &(material.specular_highlight_texopt), token); + continue; + } + + // bump texture + if ((0 == strncmp(token, "map_bump", 8)) && IS_SPACE(token[8])) { + token += 9; + ParseTextureNameAndOption(&(material.bump_texname), + &(material.bump_texopt), token); + continue; + } + + // bump texture + if ((0 == strncmp(token, "map_Bump", 8)) && IS_SPACE(token[8])) { + token += 9; + ParseTextureNameAndOption(&(material.bump_texname), + &(material.bump_texopt), token); + continue; + } + + // bump texture + if ((0 == strncmp(token, "bump", 4)) && IS_SPACE(token[4])) { + token += 5; + ParseTextureNameAndOption(&(material.bump_texname), + &(material.bump_texopt), token); + continue; + } + + // alpha texture + if ((0 == strncmp(token, "map_d", 5)) && IS_SPACE(token[5])) { + token += 6; + material.alpha_texname = token; + ParseTextureNameAndOption(&(material.alpha_texname), + &(material.alpha_texopt), token); + continue; + } + + // displacement texture + if ((0 == strncmp(token, "disp", 4)) && IS_SPACE(token[4])) { + token += 5; + ParseTextureNameAndOption(&(material.displacement_texname), + &(material.displacement_texopt), token); + continue; + } + + // reflection map + if ((0 == strncmp(token, "refl", 4)) && IS_SPACE(token[4])) { + token += 5; + ParseTextureNameAndOption(&(material.reflection_texname), + &(material.reflection_texopt), token); + continue; + } + + // PBR: roughness texture + if ((0 == strncmp(token, "map_Pr", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.roughness_texname), + &(material.roughness_texopt), token); + continue; + } + + // PBR: metallic texture + if ((0 == strncmp(token, "map_Pm", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.metallic_texname), + &(material.metallic_texopt), token); + continue; + } + + // PBR: sheen texture + if ((0 == strncmp(token, "map_Ps", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.sheen_texname), + &(material.sheen_texopt), token); + continue; + } + + // PBR: emissive texture + if ((0 == strncmp(token, "map_Ke", 6)) && IS_SPACE(token[6])) { + token += 7; + ParseTextureNameAndOption(&(material.emissive_texname), + &(material.emissive_texopt), token); + continue; + } + + // PBR: normal map texture + if ((0 == strncmp(token, "norm", 4)) && IS_SPACE(token[4])) { + token += 5; + ParseTextureNameAndOption(&(material.normal_texname), + &(material.normal_texopt), token); + continue; + } + + // unknown parameter + const char *_space = strchr(token, ' '); + if (!_space) { + _space = strchr(token, '\t'); + } + if (_space) { + std::ptrdiff_t len = _space - token; + std::string key(token, static_cast(len)); + std::string value = _space + 1; + material.unknown_parameter.insert( + std::pair(key, value)); + } + } + // flush last material. + material_map->insert(std::pair( + material.name, static_cast(materials->size()))); + materials->push_back(material); + + if (warning) { + (*warning) = warn_ss.str(); + } +} + +bool MaterialFileReader::operator()(const std::string &matId, + std::vector *materials, + std::map *matMap, + std::string *warn, std::string *err) { + if (!m_mtlBaseDir.empty()) { +#ifdef _WIN32 + char sep = ';'; +#else + char sep = ':'; +#endif + + // https://stackoverflow.com/questions/5167625/splitting-a-c-stdstring-using-tokens-e-g + std::vector paths; + std::istringstream f(m_mtlBaseDir); + + std::string s; + while (getline(f, s, sep)) { + paths.push_back(s); + } + + for (size_t i = 0; i < paths.size(); i++) { + std::string filepath = JoinPath(paths[i], matId); + + std::ifstream matIStream(filepath.c_str()); + if (matIStream) { + LoadMtl(matMap, materials, &matIStream, warn, err); + + return true; + } + } + + std::stringstream ss; + ss << "Material file [ " << matId + << " ] not found in a path : " << m_mtlBaseDir << "\n"; + if (warn) { + (*warn) += ss.str(); + } + return false; + + } else { + std::string filepath = matId; + std::ifstream matIStream(filepath.c_str()); + if (matIStream) { + LoadMtl(matMap, materials, &matIStream, warn, err); + + return true; + } + + std::stringstream ss; + ss << "Material file [ " << filepath + << " ] not found in a path : " << m_mtlBaseDir << "\n"; + if (warn) { + (*warn) += ss.str(); + } + + return false; + } +} + +bool MaterialStreamReader::operator()(const std::string &matId, + std::vector *materials, + std::map *matMap, + std::string *warn, std::string *err) { + (void)err; + (void)matId; + if (!m_inStream) { + std::stringstream ss; + ss << "Material stream in error state. \n"; + if (warn) { + (*warn) += ss.str(); + } + return false; + } + + LoadMtl(matMap, materials, &m_inStream, warn, err); + + return true; +} + +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, const char *filename, const char *mtl_basedir, + bool triangulate, bool default_vcols_fallback) { + attrib->vertices.clear(); + attrib->normals.clear(); + attrib->texcoords.clear(); + attrib->colors.clear(); + shapes->clear(); + + std::stringstream errss; + + std::ifstream ifs(filename); + if (!ifs) { + errss << "Cannot open file [" << filename << "]\n"; + if (err) { + (*err) = errss.str(); + } + return false; + } + + std::string baseDir = mtl_basedir ? mtl_basedir : ""; + if (!baseDir.empty()) { +#ifndef _WIN32 + const char dirsep = '/'; +#else + const char dirsep = '\\'; +#endif + if (baseDir[baseDir.length() - 1] != dirsep) baseDir += dirsep; + } + MaterialFileReader matFileReader(baseDir); + + return LoadObj(attrib, shapes, materials, warn, err, &ifs, &matFileReader, + triangulate, default_vcols_fallback); +} + +bool LoadObj(attrib_t *attrib, std::vector *shapes, + std::vector *materials, std::string *warn, + std::string *err, std::istream *inStream, + MaterialReader *readMatFn /*= NULL*/, bool triangulate, + bool default_vcols_fallback) { + std::stringstream errss; + + std::vector v; + std::vector vn; + std::vector vt; + std::vector vc; + std::vector vw; + std::vector tags; + PrimGroup prim_group; + std::string name; + + // material + std::set material_filenames; + std::map material_map; + int material = -1; + + // smoothing group id + unsigned int current_smoothing_id = + 0; // Initial value. 0 means no smoothing. + + int greatest_v_idx = -1; + int greatest_vn_idx = -1; + int greatest_vt_idx = -1; + + shape_t shape; + + bool found_all_colors = true; + + size_t line_num = 0; + std::string linebuf; + while (inStream->peek() != -1) { + safeGetline(*inStream, linebuf); + + line_num++; + + // Trim newline '\r\n' or '\n' + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\n') + linebuf.erase(linebuf.size() - 1); + } + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\r') + linebuf.erase(linebuf.size() - 1); + } + + // Skip if empty line. + if (linebuf.empty()) { + continue; + } + + // Skip leading space. + const char *token = linebuf.c_str(); + token += strspn(token, " \t"); + + assert(token); + if (token[0] == '\0') continue; // empty line + + if (token[0] == '#') continue; // comment line + + // vertex + if (token[0] == 'v' && IS_SPACE((token[1]))) { + token += 2; + real_t x, y, z; + real_t r, g, b; + + found_all_colors &= parseVertexWithColor(&x, &y, &z, &r, &g, &b, &token); + + v.push_back(x); + v.push_back(y); + v.push_back(z); + + if (found_all_colors || default_vcols_fallback) { + vc.push_back(r); + vc.push_back(g); + vc.push_back(b); + } + + continue; + } + + // normal + if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { + token += 3; + real_t x, y, z; + parseReal3(&x, &y, &z, &token); + vn.push_back(x); + vn.push_back(y); + vn.push_back(z); + continue; + } + + // texcoord + if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { + token += 3; + real_t x, y; + parseReal2(&x, &y, &token); + vt.push_back(x); + vt.push_back(y); + continue; + } + + // skin weight. tinyobj extension + if (token[0] == 'v' && token[1] == 'w' && IS_SPACE((token[2]))) { + token += 3; + + // vw ... + // example: + // vw 0 0 0.25 1 0.25 2 0.5 + + // TODO(syoyo): Add syntax check + int vid = 0; + vid = parseInt(&token); + + skin_weight_t sw; + + sw.vertex_id = vid; + + while (!IS_NEW_LINE(token[0])) { + real_t j, w; + // joint_id should not be negative, weight may be negative + // TODO(syoyo): # of elements check + parseReal2(&j, &w, &token, -1.0); + + if (j < static_cast(0)) { + if (err) { + std::stringstream ss; + ss << "Failed parse `vw' line. joint_id is negative. " + "line " + << line_num << ".)\n"; + (*err) += ss.str(); + } + return false; + } + + joint_and_weight_t jw; + + jw.joint_id = int(j); + jw.weight = w; + + sw.weightValues.push_back(jw); + + size_t n = strspn(token, " \t\r"); + token += n; + } + + vw.push_back(sw); + } + + // line + if (token[0] == 'l' && IS_SPACE((token[1]))) { + token += 2; + + __line_t line; + + while (!IS_NEW_LINE(token[0])) { + vertex_index_t vi; + if (!parseTriple(&token, static_cast(v.size() / 3), + static_cast(vn.size() / 3), + static_cast(vt.size() / 2), &vi)) { + if (err) { + std::stringstream ss; + ss << "Failed parse `l' line(e.g. zero value for vertex index. " + "line " + << line_num << ".)\n"; + (*err) += ss.str(); + } + return false; + } + + line.vertex_indices.push_back(vi); + + size_t n = strspn(token, " \t\r"); + token += n; + } + + prim_group.lineGroup.push_back(line); + + continue; + } + + // points + if (token[0] == 'p' && IS_SPACE((token[1]))) { + token += 2; + + __points_t pts; + + while (!IS_NEW_LINE(token[0])) { + vertex_index_t vi; + if (!parseTriple(&token, static_cast(v.size() / 3), + static_cast(vn.size() / 3), + static_cast(vt.size() / 2), &vi)) { + if (err) { + std::stringstream ss; + ss << "Failed parse `p' line(e.g. zero value for vertex index. " + "line " + << line_num << ".)\n"; + (*err) += ss.str(); + } + return false; + } + + pts.vertex_indices.push_back(vi); + + size_t n = strspn(token, " \t\r"); + token += n; + } + + prim_group.pointsGroup.push_back(pts); + + continue; + } + + // face + if (token[0] == 'f' && IS_SPACE((token[1]))) { + token += 2; + token += strspn(token, " \t"); + + face_t face; + + face.smoothing_group_id = current_smoothing_id; + face.vertex_indices.reserve(3); + + while (!IS_NEW_LINE(token[0])) { + vertex_index_t vi; + if (!parseTriple(&token, static_cast(v.size() / 3), + static_cast(vn.size() / 3), + static_cast(vt.size() / 2), &vi)) { + if (err) { + std::stringstream ss; + ss << "Failed parse `f' line(e.g. zero value for face index. line " + << line_num << ".)\n"; + (*err) += ss.str(); + } + return false; + } + + greatest_v_idx = greatest_v_idx > vi.v_idx ? greatest_v_idx : vi.v_idx; + greatest_vn_idx = + greatest_vn_idx > vi.vn_idx ? greatest_vn_idx : vi.vn_idx; + greatest_vt_idx = + greatest_vt_idx > vi.vt_idx ? greatest_vt_idx : vi.vt_idx; + + face.vertex_indices.push_back(vi); + size_t n = strspn(token, " \t\r"); + token += n; + } + + // replace with emplace_back + std::move on C++11 + prim_group.faceGroup.push_back(face); + + continue; + } + + // use mtl + if ((0 == strncmp(token, "usemtl", 6))) { + token += 6; + std::string namebuf = parseString(&token); + + int newMaterialId = -1; + std::map::const_iterator it = + material_map.find(namebuf); + if (it != material_map.end()) { + newMaterialId = it->second; + } else { + // { error!! material not found } + if (warn) { + (*warn) += "material [ '" + namebuf + "' ] not found in .mtl\n"; + } + } + + if (newMaterialId != material) { + // Create per-face material. Thus we don't add `shape` to `shapes` at + // this time. + // just clear `faceGroup` after `exportGroupsToShape()` call. + exportGroupsToShape(&shape, prim_group, tags, material, name, + triangulate, v, warn); + prim_group.faceGroup.clear(); + material = newMaterialId; + } + + continue; + } + + // load mtl + if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { + if (readMatFn) { + token += 7; + + std::vector filenames; + SplitString(std::string(token), ' ', '\\', filenames); + + if (filenames.empty()) { + if (warn) { + std::stringstream ss; + ss << "Looks like empty filename for mtllib. Use default " + "material (line " + << line_num << ".)\n"; + + (*warn) += ss.str(); + } + } else { + bool found = false; + for (size_t s = 0; s < filenames.size(); s++) { + if (material_filenames.count(filenames[s]) > 0) { + found = true; + continue; + } + + std::string warn_mtl; + std::string err_mtl; + bool ok = (*readMatFn)(filenames[s].c_str(), materials, + &material_map, &warn_mtl, &err_mtl); + if (warn && (!warn_mtl.empty())) { + (*warn) += warn_mtl; + } + + if (err && (!err_mtl.empty())) { + (*err) += err_mtl; + } + + if (ok) { + found = true; + material_filenames.insert(filenames[s]); + break; + } + } + + if (!found) { + if (warn) { + (*warn) += + "Failed to load material file(s). Use default " + "material.\n"; + } + } + } + } + + continue; + } + + // group name + if (token[0] == 'g' && IS_SPACE((token[1]))) { + // flush previous face group. + bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, + triangulate, v, warn); + (void)ret; // return value not used. + + if (shape.mesh.indices.size() > 0) { + shapes->push_back(shape); + } + + shape = shape_t(); + + // material = -1; + prim_group.clear(); + + std::vector names; + + while (!IS_NEW_LINE(token[0])) { + std::string str = parseString(&token); + names.push_back(str); + token += strspn(token, " \t\r"); // skip tag + } + + // names[0] must be 'g' + + if (names.size() < 2) { + // 'g' with empty names + if (warn) { + std::stringstream ss; + ss << "Empty group name. line: " << line_num << "\n"; + (*warn) += ss.str(); + name = ""; + } + } else { + std::stringstream ss; + ss << names[1]; + + // tinyobjloader does not support multiple groups for a primitive. + // Currently we concatinate multiple group names with a space to get + // single group name. + + for (size_t i = 2; i < names.size(); i++) { + ss << " " << names[i]; + } + + name = ss.str(); + } + + continue; + } + + // object name + if (token[0] == 'o' && IS_SPACE((token[1]))) { + // flush previous face group. + bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, + triangulate, v, warn); + (void)ret; // return value not used. + + if (shape.mesh.indices.size() > 0 || shape.lines.indices.size() > 0 || + shape.points.indices.size() > 0) { + shapes->push_back(shape); + } + + // material = -1; + prim_group.clear(); + shape = shape_t(); + + // @todo { multiple object name? } + token += 2; + std::stringstream ss; + ss << token; + name = ss.str(); + + continue; + } + + if (token[0] == 't' && IS_SPACE(token[1])) { + const int max_tag_nums = 8192; // FIXME(syoyo): Parameterize. + tag_t tag; + + token += 2; + + tag.name = parseString(&token); + + tag_sizes ts = parseTagTriple(&token); + + if (ts.num_ints < 0) { + ts.num_ints = 0; + } + if (ts.num_ints > max_tag_nums) { + ts.num_ints = max_tag_nums; + } + + if (ts.num_reals < 0) { + ts.num_reals = 0; + } + if (ts.num_reals > max_tag_nums) { + ts.num_reals = max_tag_nums; + } + + if (ts.num_strings < 0) { + ts.num_strings = 0; + } + if (ts.num_strings > max_tag_nums) { + ts.num_strings = max_tag_nums; + } + + tag.intValues.resize(static_cast(ts.num_ints)); + + for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { + tag.intValues[i] = parseInt(&token); + } + + tag.floatValues.resize(static_cast(ts.num_reals)); + for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { + tag.floatValues[i] = parseReal(&token); + } + + tag.stringValues.resize(static_cast(ts.num_strings)); + for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { + tag.stringValues[i] = parseString(&token); + } + + tags.push_back(tag); + + continue; + } + + if (token[0] == 's' && IS_SPACE(token[1])) { + // smoothing group id + token += 2; + + // skip space. + token += strspn(token, " \t"); // skip space + + if (token[0] == '\0') { + continue; + } + + if (token[0] == '\r' || token[1] == '\n') { + continue; + } + + if (strlen(token) >= 3 && token[0] == 'o' && token[1] == 'f' && + token[2] == 'f') { + current_smoothing_id = 0; + } else { + // assume number + int smGroupId = parseInt(&token); + if (smGroupId < 0) { + // parse error. force set to 0. + // FIXME(syoyo): Report warning. + current_smoothing_id = 0; + } else { + current_smoothing_id = static_cast(smGroupId); + } + } + + continue; + } // smoothing group id + + // Ignore unknown command. + } + + // not all vertices have colors, no default colors desired? -> clear colors + if (!found_all_colors && !default_vcols_fallback) { + vc.clear(); + } + + if (greatest_v_idx >= static_cast(v.size() / 3)) { + if (warn) { + std::stringstream ss; + ss << "Vertex indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + } + if (greatest_vn_idx >= static_cast(vn.size() / 3)) { + if (warn) { + std::stringstream ss; + ss << "Vertex normal indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + } + if (greatest_vt_idx >= static_cast(vt.size() / 2)) { + if (warn) { + std::stringstream ss; + ss << "Vertex texcoord indices out of bounds (line " << line_num << ".)\n\n"; + (*warn) += ss.str(); + } + } + + bool ret = exportGroupsToShape(&shape, prim_group, tags, material, name, + triangulate, v, warn); + // exportGroupsToShape return false when `usemtl` is called in the last + // line. + // we also add `shape` to `shapes` when `shape.mesh` has already some + // faces(indices) + if (ret || shape.mesh.indices + .size()) { // FIXME(syoyo): Support other prims(e.g. lines) + shapes->push_back(shape); + } + prim_group.clear(); // for safety + + if (err) { + (*err) += errss.str(); + } + + attrib->vertices.swap(v); + attrib->vertex_weights.swap(v); + attrib->normals.swap(vn); + attrib->texcoords.swap(vt); + attrib->texcoord_ws.swap(vt); + attrib->colors.swap(vc); + attrib->skin_weights.swap(vw); + + return true; +} + +bool LoadObjWithCallback(std::istream &inStream, const callback_t &callback, + void *user_data /*= NULL*/, + MaterialReader *readMatFn /*= NULL*/, + std::string *warn, /* = NULL*/ + std::string *err /*= NULL*/) { + std::stringstream errss; + + // material + std::set material_filenames; + std::map material_map; + int material_id = -1; // -1 = invalid + + std::vector indices; + std::vector materials; + std::vector names; + names.reserve(2); + std::vector names_out; + + std::string linebuf; + while (inStream.peek() != -1) { + safeGetline(inStream, linebuf); + + // Trim newline '\r\n' or '\n' + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\n') + linebuf.erase(linebuf.size() - 1); + } + if (linebuf.size() > 0) { + if (linebuf[linebuf.size() - 1] == '\r') + linebuf.erase(linebuf.size() - 1); + } + + // Skip if empty line. + if (linebuf.empty()) { + continue; + } + + // Skip leading space. + const char *token = linebuf.c_str(); + token += strspn(token, " \t"); + + assert(token); + if (token[0] == '\0') continue; // empty line + + if (token[0] == '#') continue; // comment line + + // vertex + if (token[0] == 'v' && IS_SPACE((token[1]))) { + token += 2; + // TODO(syoyo): Support parsing vertex color extension. + real_t x, y, z, w; // w is optional. default = 1.0 + parseV(&x, &y, &z, &w, &token); + if (callback.vertex_cb) { + callback.vertex_cb(user_data, x, y, z, w); + } + continue; + } + + // normal + if (token[0] == 'v' && token[1] == 'n' && IS_SPACE((token[2]))) { + token += 3; + real_t x, y, z; + parseReal3(&x, &y, &z, &token); + if (callback.normal_cb) { + callback.normal_cb(user_data, x, y, z); + } + continue; + } + + // texcoord + if (token[0] == 'v' && token[1] == 't' && IS_SPACE((token[2]))) { + token += 3; + real_t x, y, z; // y and z are optional. default = 0.0 + parseReal3(&x, &y, &z, &token); + if (callback.texcoord_cb) { + callback.texcoord_cb(user_data, x, y, z); + } + continue; + } + + // face + if (token[0] == 'f' && IS_SPACE((token[1]))) { + token += 2; + token += strspn(token, " \t"); + + indices.clear(); + while (!IS_NEW_LINE(token[0])) { + vertex_index_t vi = parseRawTriple(&token); + + index_t idx; + idx.vertex_index = vi.v_idx; + idx.normal_index = vi.vn_idx; + idx.texcoord_index = vi.vt_idx; + + indices.push_back(idx); + size_t n = strspn(token, " \t\r"); + token += n; + } + + if (callback.index_cb && indices.size() > 0) { + callback.index_cb(user_data, &indices.at(0), + static_cast(indices.size())); + } + + continue; + } + + // use mtl + if ((0 == strncmp(token, "usemtl", 6)) && IS_SPACE((token[6]))) { + token += 7; + std::stringstream ss; + ss << token; + std::string namebuf = ss.str(); + + int newMaterialId = -1; + std::map::const_iterator it = + material_map.find(namebuf); + if (it != material_map.end()) { + newMaterialId = it->second; + } else { + // { warn!! material not found } + if (warn && (!callback.usemtl_cb)) { + (*warn) += "material [ " + namebuf + " ] not found in .mtl\n"; + } + } + + if (newMaterialId != material_id) { + material_id = newMaterialId; + } + + if (callback.usemtl_cb) { + callback.usemtl_cb(user_data, namebuf.c_str(), material_id); + } + + continue; + } + + // load mtl + if ((0 == strncmp(token, "mtllib", 6)) && IS_SPACE((token[6]))) { + if (readMatFn) { + token += 7; + + std::vector filenames; + SplitString(std::string(token), ' ', '\\', filenames); + + if (filenames.empty()) { + if (warn) { + (*warn) += + "Looks like empty filename for mtllib. Use default " + "material. \n"; + } + } else { + bool found = false; + for (size_t s = 0; s < filenames.size(); s++) { + if (material_filenames.count(filenames[s]) > 0) { + found = true; + continue; + } + + std::string warn_mtl; + std::string err_mtl; + bool ok = (*readMatFn)(filenames[s].c_str(), &materials, + &material_map, &warn_mtl, &err_mtl); + + if (warn && (!warn_mtl.empty())) { + (*warn) += warn_mtl; // This should be warn message. + } + + if (err && (!err_mtl.empty())) { + (*err) += err_mtl; + } + + if (ok) { + found = true; + material_filenames.insert(filenames[s]); + break; + } + } + + if (!found) { + if (warn) { + (*warn) += + "Failed to load material file(s). Use default " + "material.\n"; + } + } else { + if (callback.mtllib_cb) { + callback.mtllib_cb(user_data, &materials.at(0), + static_cast(materials.size())); + } + } + } + } + + continue; + } + + // group name + if (token[0] == 'g' && IS_SPACE((token[1]))) { + names.clear(); + + while (!IS_NEW_LINE(token[0])) { + std::string str = parseString(&token); + names.push_back(str); + token += strspn(token, " \t\r"); // skip tag + } + + assert(names.size() > 0); + + if (callback.group_cb) { + if (names.size() > 1) { + // create const char* array. + names_out.resize(names.size() - 1); + for (size_t j = 0; j < names_out.size(); j++) { + names_out[j] = names[j + 1].c_str(); + } + callback.group_cb(user_data, &names_out.at(0), + static_cast(names_out.size())); + + } else { + callback.group_cb(user_data, NULL, 0); + } + } + + continue; + } + + // object name + if (token[0] == 'o' && IS_SPACE((token[1]))) { + // @todo { multiple object name? } + token += 2; + + std::stringstream ss; + ss << token; + std::string object_name = ss.str(); + + if (callback.object_cb) { + callback.object_cb(user_data, object_name.c_str()); + } + + continue; + } + +#if 0 // @todo + if (token[0] == 't' && IS_SPACE(token[1])) { + tag_t tag; + + token += 2; + std::stringstream ss; + ss << token; + tag.name = ss.str(); + + token += tag.name.size() + 1; + + tag_sizes ts = parseTagTriple(&token); + + tag.intValues.resize(static_cast(ts.num_ints)); + + for (size_t i = 0; i < static_cast(ts.num_ints); ++i) { + tag.intValues[i] = atoi(token); + token += strcspn(token, "/ \t\r") + 1; + } + + tag.floatValues.resize(static_cast(ts.num_reals)); + for (size_t i = 0; i < static_cast(ts.num_reals); ++i) { + tag.floatValues[i] = parseReal(&token); + token += strcspn(token, "/ \t\r") + 1; + } + + tag.stringValues.resize(static_cast(ts.num_strings)); + for (size_t i = 0; i < static_cast(ts.num_strings); ++i) { + std::stringstream ss; + ss << token; + tag.stringValues[i] = ss.str(); + token += tag.stringValues[i].size() + 1; + } + + tags.push_back(tag); + } +#endif + + // Ignore unknown command. + } + + if (err) { + (*err) += errss.str(); + } + + return true; +} + +bool ObjReader::ParseFromFile(const std::string &filename, + const ObjReaderConfig &config) { + std::string mtl_search_path; + + if (config.mtl_search_path.empty()) { + // + // split at last '/'(for unixish system) or '\\'(for windows) to get + // the base directory of .obj file + // + size_t pos = filename.find_last_of("/\\"); + if (pos != std::string::npos) { + mtl_search_path = filename.substr(0, pos); + } + } else { + mtl_search_path = config.mtl_search_path; + } + + valid_ = LoadObj(&attrib_, &shapes_, &materials_, &warning_, &error_, + filename.c_str(), mtl_search_path.c_str(), + config.triangulate, config.vertex_color); + + return valid_; +} + + + + + +namespace +{ +// https://stackoverflow.com/questions/58524805/is-there-a-way-to-create-a-stringstream-from-a-string-view-without-copying-data +template +class view_streambuf final: public std::basic_streambuf<__char_type, __traits_type > { +private: + typedef std::basic_streambuf<__char_type, __traits_type > super_type; + typedef view_streambuf<__char_type, __traits_type> self_type; +public: + + /** + * These are standard types. They permit a standardized way of + * referring to names of (or names dependent on) the template + * parameters, which are specific to the implementation. + */ + typedef typename super_type::char_type char_type; + typedef typename super_type::traits_type traits_type; + typedef typename traits_type::int_type int_type; + typedef typename traits_type::pos_type pos_type; + typedef typename traits_type::off_type off_type; + + typedef typename std::basic_string_view source_view; + + view_streambuf(const source_view& src) noexcept: + super_type(), + src_( src ) + { + char_type *buff = const_cast( src_.data() ); + this->setg( buff , buff, buff + src_.length() ); + } + + virtual std::streamsize xsgetn(char_type* __s, std::streamsize __n) override + { + if(0 == __n) + return 0; + if( (this->gptr() + __n) >= this->egptr() ) { + __n = this->egptr() - this->gptr(); + if(0 == __n && !traits_type::not_eof( this->underflow() ) ) + return -1; + } + std::memmove( static_cast(__s), this->gptr(), __n); + this->gbump( static_cast(__n) ); + return __n; + } + + virtual int_type pbackfail(int_type __c) override + { + char_type *pos = this->gptr() - 1; + *pos = traits_type::to_char_type( __c ); + this->pbump(-1); + return 1; + } + + virtual int_type underflow() override + { + return traits_type::eof(); + } + + virtual std::streamsize showmanyc() override + { + return static_cast( this->egptr() - this->gptr() ); + } + + virtual ~view_streambuf() override + {} +private: + const source_view& src_; +}; + +template +class view_istream final:public std::basic_istream<_char_type, std::char_traits<_char_type> > { + view_istream(const view_istream&) = delete; + view_istream& operator=(const view_istream&) = delete; +private: + typedef std::basic_istream<_char_type, std::char_traits<_char_type> > super_type; + typedef view_streambuf<_char_type, std::char_traits<_char_type> > streambuf_type; +public: + typedef _char_type char_type; + typedef typename super_type::int_type int_type; + typedef typename super_type::pos_type pos_type; + typedef typename super_type::off_type off_type; + typedef typename super_type::traits_type traits_type; + typedef typename streambuf_type::source_view source_view; + + view_istream(const source_view& src): + super_type( nullptr ), + sb_(nullptr) + { + sb_ = new streambuf_type(src); + this->init( sb_ ); + } + + + view_istream(view_istream&& other) noexcept: + super_type( std::forward(other) ), + sb_( std::move( other.sb_ ) ) + {} + + view_istream& operator=(view_istream&& rhs) noexcept + { + view_istream( std::forward(rhs) ).swap( *this ); + return *this; + } + + virtual ~view_istream() override { + delete sb_; + } + +private: + streambuf_type *sb_; +}; + +} + + +bool ObjReader::ParseFromString(std::string_view obj_text, + std::string_view mtl_text, + const ObjReaderConfig &config) { + + view_istream obj_ifs( obj_text ); + view_istream mtl_ifs( mtl_text ); + + MaterialStreamReader mtl_ss(mtl_ifs); + + valid_ = LoadObj(&attrib_, &shapes_, &materials_, &warning_, &error_, + &obj_ifs, &mtl_ss, config.triangulate, config.vertex_color); + + return valid_; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +} // namespace tinyobj + +#endif diff --git a/3rdparty/vcglib b/3rdparty/vcglib new file mode 160000 index 0000000000..88c881d839 --- /dev/null +++ b/3rdparty/vcglib @@ -0,0 +1 @@ +Subproject commit 88c881d8393929c8e09b9df765ce8582bf386499 diff --git a/ci/common.deps.sh b/ci/common.deps.sh index 3da72416c6..082c6f8bf2 100755 --- a/ci/common.deps.sh +++ b/ci/common.deps.sh @@ -33,7 +33,6 @@ CI_PLATFORM="${1:-DEFAULT}" if [[ "$CI_PLATFORM" != "WASM" ]]; then clone_addon https://github.com/ossia/score-addon-ltc - clone_addon https://github.com/ossia/score-addon-threedim clone_addon https://github.com/ossia/score-addon-ndi clone_addon https://github.com/bltzr/score-avnd-granola clone_addon https://github.com/ossia/score-addon-ultraleap diff --git a/cmake/Configurations/all-plugins.cmake b/cmake/Configurations/all-plugins.cmake index 3b1f840a19..12037d44c4 100644 --- a/cmake/Configurations/all-plugins.cmake +++ b/cmake/Configurations/all-plugins.cmake @@ -101,5 +101,6 @@ else() score-plugin-fx score-plugin-ui score-plugin-analysis + score-plugin-threedim ) endif() diff --git a/src/plugins/score-plugin-threedim/CMakeLists.txt b/src/plugins/score-plugin-threedim/CMakeLists.txt new file mode 100644 index 0000000000..1b77ae4c30 --- /dev/null +++ b/src/plugins/score-plugin-threedim/CMakeLists.txt @@ -0,0 +1,115 @@ +cmake_minimum_required(VERSION 3.24 FATAL_ERROR) +project(score_plugin_threedim LANGUAGES CXX) + +if(NOT TARGET score_lib_base) + include(ScoreExternalAddon) +endif() + +if(NOT TARGET score_plugin_avnd) + return() +endif() +if(NOT TARGET score_plugin_gfx) + return() +endif() + +find_package(${QT_VERSION} REQUIRED COMPONENTS Xml) + +# libssynth +add_library(ssynth STATIC + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Parser/EisenParser.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Parser/Preprocessor.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Parser/Tokenizer.cpp" + + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/Action.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/AmbiguousRule.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/Builder.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/CustomRule.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/PrimitiveRule.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/RuleSet.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/State.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/Transformation.cpp" + + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/Rendering/TemplateRenderer.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Model/Rendering/ObjRenderer.cpp" + + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/ColorPool.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/ColorUtils.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/Logging.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/MiniParser.cpp" + "${3RDPARTY_FOLDER}/libssynth/src/ssynth/RandomStreams.cpp" +) +target_include_directories(ssynth + SYSTEM PRIVATE + "${3RDPARTY_FOLDER}/libssynth/src" +) + +target_link_libraries(ssynth + PRIVATE + "${QT_PREFIX}::Core" + "${QT_PREFIX}::Gui" + "${QT_PREFIX}::Xml" + score_lib_base +) + +if(NOT MSVC) + target_compile_options(ssynth PRIVATE -w) +endif() + +# threedim addon +add_library( + score_plugin_threedim + + Threedim/TinyObj.hpp + Threedim/TinyObj.cpp + Threedim/Ply.hpp + Threedim/Ply.cpp + + Threedim/ArrayToGeometry.hpp + Threedim/ArrayToGeometry.cpp + + Threedim/StructureSynth.hpp + Threedim/StructureSynth.cpp + + Threedim/ObjLoader.hpp + Threedim/ObjLoader.cpp + + Threedim/Primitive.hpp + Threedim/Primitive.cpp + + Threedim/Noise.hpp + Threedim/Noise.cpp + + Threedim/ModelDisplay/ModelDisplayNode.hpp + Threedim/ModelDisplay/ModelDisplayNode.cpp + Threedim/ModelDisplay/Executor.hpp + Threedim/ModelDisplay/Executor.cpp + Threedim/ModelDisplay/Metadata.hpp + Threedim/ModelDisplay/Process.hpp + Threedim/ModelDisplay/Process.cpp + Threedim/ModelDisplay/Layer.hpp + + "${3RDPARTY_FOLDER}/miniply/miniply.cpp" + + score_plugin_threedim.hpp + score_plugin_threedim.cpp +) + +set_property(TARGET score_plugin_threedim PROPERTY SCORE_CUSTOM_PCH 1) +setup_score_plugin(score_plugin_threedim) + +target_include_directories(score_plugin_threedim + SYSTEM PRIVATE + "${3RDPARTY_FOLDER}/libssynth/src" + "${3RDPARTY_FOLDER}/vcglib" + "${3RDPARTY_FOLDER}/eigen" + "${3RDPARTY_FOLDER}/miniply" +) + +target_link_libraries(score_plugin_threedim + PRIVATE + score_plugin_engine + score_plugin_avnd + score_plugin_gfx + fmt::fmt + ssynth +) diff --git a/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.cpp b/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.cpp new file mode 100644 index 0000000000..dec80130a9 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.cpp @@ -0,0 +1,150 @@ +#include "ArrayToGeometry.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace Threedim +{ +static thread_local TMesh array_to_mesh; + +void loadPoints(TMesh& mesh, std::vector& complete, PrimitiveOutputs& outputs) +{ + vcg::tri::Clean::RemoveZeroAreaFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::UpdateNormal::PerVertexNormalized(mesh); + vcg::tri::UpdateTexture::WedgeTexFromPlane( + mesh, vcg::Point3f{0., 0., 0.}, vcg::Point3f{1., 1., 1.}, 1.); + + vcg::tri::RequirePerVertexNormal(mesh); + vcg::tri::RequirePerVertexTexCoord(mesh); + + complete.clear(); + const auto vertices = mesh.vert.size(); + const auto floats + = vertices * (3 + 3 + 2); // 3 float for pos, 3 float for normal, 2 float for UV + complete.resize(floats); + float* pos_start = complete.data(); + float* norm_start = complete.data() + vertices * 3; + float* uv_start = complete.data() + vertices * 3 + vertices * 3; + + for (auto& fi : mesh.face) + { // iterate each faces + + auto v0 = fi.V(0); + auto v1 = fi.V(1); + auto v2 = fi.V(2); + + auto p0 = v0->P(); + (*pos_start++) = p0.X(); + (*pos_start++) = p0.Y(); + (*pos_start++) = p0.Z(); + + auto p1 = v1->P(); + (*pos_start++) = p1.X(); + (*pos_start++) = p1.Y(); + (*pos_start++) = p1.Z(); + + auto p2 = v2->P(); + (*pos_start++) = p2.X(); + (*pos_start++) = p2.Y(); + (*pos_start++) = p2.Z(); + + auto n0 = v0->N(); + (*norm_start++) = n0.X(); + (*norm_start++) = n0.Y(); + (*norm_start++) = n0.Z(); + + auto n1 = v1->N(); + (*norm_start++) = n1.X(); + (*norm_start++) = n1.Y(); + (*norm_start++) = n1.Z(); + + auto n2 = v2->N(); + (*norm_start++) = n2.X(); + (*norm_start++) = n2.Y(); + (*norm_start++) = n2.Z(); + +#if 0 + auto uv0 = fi.WT(0); + (*uv_start++) = uv0.U(); + (*uv_start++) = uv0.V(); + + auto uv1 = fi.WT(1); + (*uv_start++) = uv1.U(); + (*uv_start++) = uv1.V(); + + auto uv2 = fi.WT(2); + (*uv_start++) = uv2.U(); + (*uv_start++) = uv2.V(); +#endif + (*uv_start++) = p0.X(); + (*uv_start++) = p0.Y(); + + (*uv_start++) = p1.X(); + (*uv_start++) = p1.Y(); + + (*uv_start++) = p2.X(); + (*uv_start++) = p2.Y(); + } + outputs.geometry.mesh.buffers.main_buffer.data = complete.data(); + outputs.geometry.mesh.buffers.main_buffer.size = complete.size(); + outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + outputs.geometry.mesh.input.input0.offset = 0; + outputs.geometry.mesh.input.input1.offset = sizeof(float) * vertices * 3; + outputs.geometry.mesh.input.input2.offset = sizeof(float) * vertices * (3 + 3); + outputs.geometry.mesh.vertices = vertices; + outputs.geometry.dirty_mesh = true; +} +void ArrayToMesh::create_mesh(std::span v) +{ + if (v.size() < 3) + return; + + if (inputs.triangulate) + { + auto& m = array_to_mesh; + m.Clear(); + vcg::tri::Allocator::AddVertices(m, v.size() / 3); + + for (std::size_t i = 0; i < v.size(); i += 3) + { + auto& vtx = m.vert[i / 3].P(); + vtx.X() = v[i + 0]; + vtx.Y() = v[i + 1]; + vtx.Z() = v[i + 2]; + } + vcg::tri::UpdateBounding::Box(m); + vcg::tri::UpdateNormal::PerFace(m); + + vcg::tri::BallPivoting pivot(m, 0.01, 0.05); + pivot.BuildMesh(); + loadTriMesh(m, complete, outputs); + } + else + { + std::size_t vertices = v.size() / 3; + + this->complete.clear(); + this->complete.resize(std::ceil((v.size() / 3.) * (3 + 3 + 2))); + std::copy_n(v.begin(), v.size(), complete.begin()); + + outputs.geometry.mesh.buffers.main_buffer.data = complete.data(); + outputs.geometry.mesh.buffers.main_buffer.size = complete.size(); + outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + outputs.geometry.mesh.input.input0.offset = 0; + outputs.geometry.mesh.input.input1.offset = sizeof(float) * vertices * 3; + outputs.geometry.mesh.input.input2.offset = sizeof(float) * vertices * (3 + 3); + outputs.geometry.mesh.vertices = vertices; + outputs.geometry.dirty_mesh = true; + } +} +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.hpp b/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.hpp new file mode 100644 index 0000000000..18d3807849 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ArrayToGeometry.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Threedim +{ + +class ArrayToMesh +{ +public: + halp_meta(name, "Array to mesh") + halp_meta(category, "Visuals/3D") + halp_meta(c_name, "array_to_mesh") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/array-to-mesh.html") + halp_meta(uuid, "dfc5bae9-c75c-4180-b4e8-be3063c8d8f2") + + struct ins + { + struct : halp::val_port<"Input", std::vector> + { + void update(ArrayToMesh& self) { self.create_mesh(value); } + } in; + PositionControl position; + RotationControl rotation; + ScaleControl scale; + + halp::toggle<"Triangulate"> triangulate; + } inputs; + + PrimitiveOutputs outputs; + void create_mesh(std::span v); + + std::vector complete; +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/MeshHelpers.hpp b/src/plugins/score-plugin-threedim/Threedim/MeshHelpers.hpp new file mode 100644 index 0000000000..2ead716072 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/MeshHelpers.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace Threedim +{ +using namespace vcg; +class TFace; +class TVertex; + +struct TUsedTypes + : public vcg::UsedTypes::AsVertexType, vcg::Use::AsFaceType> +{ +}; + +class TVertex + : public Vertex< + TUsedTypes, + vertex::BitFlags, + vertex::Coord3f, + vertex::Normal3f, + vertex::TexCoord2f, + vertex::Mark> +{ +}; + +class TFace + : public Face< + TUsedTypes, + face::VertexRef, + face::Normal3f, + face::WedgeTexCoord2f, + face::BitFlags, + face::FFAdj> +{ +}; + +class TMesh : public vcg::tri::TriMesh, std::vector> +{ +}; + +void loadTriMesh(TMesh& mesh, std::vector& complete, PrimitiveOutputs& outputs); +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.cpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.cpp new file mode 100644 index 0000000000..38b6b458ed --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.cpp @@ -0,0 +1,84 @@ +#include "Executor.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Gfx::ModelDisplay +{ +class model_display_node final : public gfx_exec_node +{ +public: + model_display_node(GfxExecutionAction& ctx) + : gfx_exec_node{ctx} + { + id = exec_context->ui->register_node( + std::make_unique()); + } + + ~model_display_node() + { + if (id >= 0) + exec_context->ui->unregister_node(id); + } + + std::string label() const noexcept override { return "Gfx::ModelDisplay_node"; } +}; + +ProcessExecutorComponent::ProcessExecutorComponent( + Gfx::ModelDisplay::Model& element, + const Execution::Context& ctx, + QObject* parent) + : ProcessComponent_T{element, ctx, "modelComponent", parent} +{ + auto n = ossia::make_node( + *ctx.execState, ctx.doc.plugin().exec); + + for (auto* outlet : element.outlets()) + { + if (auto out = qobject_cast(outlet)) + { + out->nodeId = n->id; + } + } + + n->root_inputs().push_back(new ossia::texture_inlet); + n->root_inputs().push_back(new ossia::geometry_inlet); + + for (std::size_t i = 2; i <= 8; i++) + { + auto ctrl = qobject_cast(element.inlets()[i]); + auto& p = n->add_control(); + p->value = ctrl->value(); + + QObject::connect( + ctrl, + &Process::ControlInlet::valueChanged, + this, + con_unvalidated{ctx, i - 2, 0, n}); + } + + n->root_outputs().push_back(new ossia::texture_outlet); + + this->node = n; + m_ossia_process = std::make_shared(n); +} + +void ProcessExecutorComponent::cleanup() +{ + for (auto* outlet : this->process().outlets()) + { + if (auto out = qobject_cast(outlet)) + { + out->nodeId = -1; + } + } +} +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.hpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.hpp new file mode 100644 index 0000000000..a1884d34ba --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Executor.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +#include + +namespace Gfx::ModelDisplay +{ +class Model; +class ProcessExecutorComponent final + : public Execution:: + ProcessComponent_T +{ + COMPONENT_METADATA("f72d3fcd-a4d2-42cb-8758-94c0d72ffe61") +public: + ProcessExecutorComponent( + Model& element, + const Execution::Context& ctx, + QObject* parent); + void cleanup() override; +}; + +using ProcessExecutorComponentFactory + = Execution::ProcessComponentFactory_T; +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Layer.hpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Layer.hpp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Metadata.hpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Metadata.hpp new file mode 100644 index 0000000000..6543757388 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Metadata.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace Gfx::ModelDisplay +{ +class Model; +} + +PROCESS_METADATA(, + Gfx::ModelDisplay::Model, + "9ce44e4b-eeb6-4042-bb7f-9d0b28190daf", + "modeldisplay", // Internal name + "Model Display", // Pretty name + Process::ProcessCategory::Visual, // Category + "Visuals", // Category + "Display input geometry", // Description + "ossia team", // Author + (QStringList{"gfx", "model", "3d"}), // Tags + {}, // Inputs + {}, // Outputs + QUrl{}, // Doc url + Process::ProcessFlags::SupportsAll | Process::ProcessFlags::ControlSurface // Flags +) diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.cpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.cpp new file mode 100644 index 0000000000..84a31dd4a3 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.cpp @@ -0,0 +1,1201 @@ +#include "ModelDisplayNode.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(near) +#undef near +#undef far +#endif + +namespace score::gfx +{ + +#define model_display_default_uniforms \ + "\ +layout(std140, binding = 0) uniform renderer_t { \n\ + mat4 clipSpaceCorrMatrix; \n\ + vec2 renderSize; \n\ +} renderer; \n\ + \n\ +// Time-dependent uniforms, only relevant during execution \n\ +layout(std140, binding = 1) uniform process_t { \n\ + float TIME; \n\ + float TIMEDELTA; \n\ + float PROGRESS; \n\ + \n\ + int PASSINDEX; \n\ + int FRAMEINDEX; \n\ + \n\ + vec4 DATE; \n\ + vec4 MOUSE; \n\ + vec4 CHANNELTIME; \n\ + \n\ + float SAMPLERATE; \n\ +} isf_process_uniforms; \n\ + \n\ +layout(std140, binding = 2) uniform material_t { \n\ + mat4 matrixModelViewProjection; \n\ + mat4 matrixModelView; \n\ + mat4 matrixModel; \n\ + mat4 matrixView; \n\ + mat4 matrixProjection; \n\ + mat3 matrixNormal; \n\ +} mat; \n\ + \n\ +float TIME = isf_process_uniforms.TIME; \n\ +float TIMEDELTA = isf_process_uniforms.TIMEDELTA; \n\ +float PROGRESS = isf_process_uniforms.PROGRESS; \n\ +int PASSINDEX = isf_process_uniforms.PASSINDEX; \n\ +int FRAMEINDEX = isf_process_uniforms.FRAMEINDEX; \n\ +vec4 DATE = isf_process_uniforms.DATE; \n\ +" + +const constexpr auto vtx_output_triangle = R"_( +out gl_PerVertex { +vec4 gl_Position; +}; +)_"; +const constexpr auto vtx_output_point = R"_( +out gl_PerVertex { +vec4 gl_Position; +float gl_PointSize; +}; +)_"; +const constexpr auto vtx_output_process_triangle = R"_()_"; +const constexpr auto vtx_output_process_point = R"_( + gl_PointSize = 1.0f; +)_"; + +const constexpr auto model_display_vertex_shader_phong = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 texcoord; +layout(location = 3) in vec3 normal; + +layout(location = 0) out vec3 esVertex; +layout(location = 1) out vec3 esNormal; +layout(location = 2) out vec2 v_texcoord; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = normal; + vec2 in_uv = texcoord; + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + esVertex = in_position; + esNormal = in_normal; + v_texcoord = in_uv; + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_phong = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding=3) uniform sampler2D y_tex; + +layout(location = 0) in vec3 esVertex; +layout(location = 1) in vec3 esNormal; +layout(location = 2) in vec2 v_texcoord; +layout(location = 0) out vec4 fragColor; + +vec4 lightPosition = vec4(100, 10, 10, 0.); // should be in the eye space +vec4 lightAmbient = vec4(0.1, 0.1, 0.1, 1); // light ambient color +vec4 lightDiffuse = vec4(0.0, 0.2, 0.7, 1); // light diffuse color +vec4 lightSpecular = vec4(0.9, 0.9, 0.9, 1); // light specular color +vec4 materialAmbient= vec4(0.1, 0.4, 0, 1); // material ambient color +vec4 materialDiffuse= vec4(0.2, 0.8, 0, 1); // material diffuse color +vec4 materialSpecular= vec4(0, 0, 1, 1); // material specular color +float materialShininess = 0.5; // material specular shininess + +void main () +{ + vec3 normal = normalize(esNormal); + vec3 light; + lightPosition.y = sin(TIME) * 20.; + lightPosition.z = cos(TIME) * 50.; + if(lightPosition.w == 0.0) + { + light = normalize(lightPosition.xyz); + } + else + { + light = normalize(lightPosition.xyz - esVertex); + } + vec3 view = normalize(-esVertex); + vec3 halfv = normalize(light + view); + + vec3 color = lightAmbient.rgb * materialAmbient.rgb; // begin with ambient + float dotNL = max(dot(normal, light), 0.0); + color += lightDiffuse.rgb * materialDiffuse.rgb * dotNL; // add diffuse + // color *= texture2D(map0, texCoord0).rgb; // modulate texture map + float dotNH = max(dot(normal, halfv), 0.0); + color += pow(dotNH, materialShininess) * lightSpecular.rgb * materialSpecular.rgb; // add specular + + + vec4 tex = texture(y_tex, v_texcoord); + fragColor = vec4(mix(color, tex.rgb, 0.5), materialDiffuse.a); +} +)_"; + +const constexpr auto model_display_vertex_shader_texcoord = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 1) in vec2 texcoord; + +layout(location = 0) out vec2 v_texcoord; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = vec3(0); + vec2 in_uv = texcoord; + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + v_texcoord = in_uv; + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_texcoord = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding=3) uniform sampler2D y_tex; + +layout(location = 0) in vec2 v_texcoord; +layout(location = 0) out vec4 fragColor; + +void main () +{ + fragColor = texture(y_tex, v_texcoord); +} +)_"; + +// See also: +// https://www.pbr-book.org/3ed-2018/Texture/Texture_Coordinate_Generation#fragment-TextureMethodDefinitions-3 +// https://gamedevelopment.tutsplus.com/articles/use-tri-planar-texture-mapping-for-better-terrain--gamedev-13821 + +const constexpr auto model_display_vertex_shader_triplanar = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 1) in vec3 normal; + +layout(location = 0) out vec3 v_normal; +layout(location = 1) out vec3 v_coords; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = normal; + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + v_normal = in_normal; + v_coords = (mat.matrixModel * vec4(in_position.xyz, 1.0)).xyz; + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_triplanar = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +layout(location = 0) in vec3 v_normal; +layout(location = 1) in vec3 v_coords; +layout(location = 0) out vec4 fragColor; + +void main () +{ + vec3 blending = abs( v_normal ); + blending = normalize(max(blending, 0.00001)); // Force weights to sum to 1.0 + float b = (blending.x + blending.y + blending.z); + blending /= vec3(b, b, b); + + float scale = 0.1; + + vec4 xaxis = texture(y_tex, v_coords.yz * scale); + vec4 yaxis = texture(y_tex, v_coords.xz * scale); + vec4 zaxis = texture(y_tex, v_coords.xy * scale); + vec4 tex = xaxis * blending.x + xaxis * blending.y + zaxis * blending.z; + + fragColor = tex; +} +)_"; + +const constexpr auto model_display_vertex_shader_spherical = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 3) in vec3 normal; + +layout(location = 0) out vec3 v_e; +layout(location = 1) out vec3 v_n; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = normal; + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + // https://www.clicktorelease.com/blog/creating-spherical-environment-mapping-shader.html + vec4 p = vec4( in_position, 1. ); + v_e = normalize( vec3( mat.matrixModelView * p ) ); + v_n = normal; //normalize( mat.matrixNormal * in_normal ); + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; +const constexpr auto model_display_fragment_shader_spherical = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +layout(location = 0) in vec3 v_e; +layout(location = 1) in vec3 v_n; +layout(location = 0) out vec4 fragColor; + +const float pi = 3.14159265359; + +float atan2(in float y, in float x) +{ + bool s = (abs(x) > abs(y)); + return mix(3.141596/2.0 - atan(x,y), atan(y,x), s); +} +void main () +{ + vec2 uv = vec2(atan2(v_n.z, v_n.x), asin(v_n.y)); + uv = uv * vec2(1. / 2. * pi, 1. / pi) + 0.5; + fragColor = texture(y_tex, uv); +} +)_"; + +const constexpr auto model_display_vertex_shader_spherical2 = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 3) in vec3 normal; + +layout(location = 0) out vec3 v_e; +layout(location = 1) out vec3 v_n; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +float atan2(in float y, in float x) +{ + bool s = (abs(x) > abs(y)); + return mix(3.141596/2.0 - atan(x,y), atan(y,x), s); +} + +void main() +{ + vec3 in_position = position; + vec3 in_normal = normal; + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + // https://www.clicktorelease.com/blog/creating-spherical-environment-mapping-shader.html + vec4 p = vec4( in_position, 1. ); + v_e = normalize( vec3( mat.matrixModelView * p ) ); + v_n = normalize( mat.matrixNormal * in_normal ); + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; +const constexpr auto model_display_fragment_shader_spherical2 = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +layout(location = 0) in vec3 v_e; +layout(location = 1) in vec3 v_n; +layout(location = 0) out vec4 fragColor; + +void main () +{ + vec3 r = reflect( v_e, v_n ); + float m = 2. * sqrt( pow( r.x, 2. ) + pow( r.y, 2. ) + pow( r.z + 1., 2. ) ); + vec2 vN = r.xy / m + .5; + + fragColor = texture(y_tex, vN.xy); +} +)_"; + +const constexpr auto model_display_vertex_shader_viewspace = R"_(#version 450 +layout(location = 0) in vec3 position; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = vec3(0); + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_viewspace = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +layout(location = 0) out vec4 fragColor; + +void main () +{ + fragColor = texture(y_tex, gl_FragCoord.xy / renderer.renderSize.xy); +} +)_"; + +const constexpr auto model_display_vertex_shader_barycentric = R"_(#version 450 +layout(location = 0) in vec3 position; + +layout(location = 1) out vec2 v_bary; + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = vec3(0); + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(1); + + %vtx_do_filters% + + if(gl_VertexIndex % 3 == 0) v_bary = vec2(0, 0); + else if(gl_VertexIndex % 3 == 1) v_bary = vec2(0, 1); + else if(gl_VertexIndex % 3 == 2) v_bary = vec2(1, 0); + + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_barycentric = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(binding = 3) uniform sampler2D y_tex; + +layout(location = 1) in vec2 v_bary; +layout(location = 0) out vec4 fragColor; + +void main () +{ + fragColor = texture(y_tex, v_bary); +} +)_"; + +const constexpr auto model_display_vertex_shader_color = R"_(#version 450 +layout(location = 0) in vec3 position; +layout(location = 2) in vec3 color; + +layout(location = 0) out vec3 v_color; + +)_" model_display_default_uniforms R"_( + +%vtx_define_filters% + +%vtx_output% + +void main() +{ + vec3 in_position = position; + vec3 in_normal = vec3(0); + vec2 in_uv = vec2(0); + vec3 in_tangent = vec3(0); + vec4 in_color = vec4(color.rgb, 1.); + + %vtx_do_filters% + + v_color = in_color.rgb; + gl_Position = renderer.clipSpaceCorrMatrix * mat.matrixModelViewProjection * vec4(in_position.xyz, 1.0); + + %vtx_output_process% +} +)_"; + +const constexpr auto model_display_fragment_shader_color = R"_(#version 450 + +)_" model_display_default_uniforms R"_( + +layout(location = 0) in vec3 v_color; +layout(location = 0) out vec4 fragColor; + +void main () +{ + fragColor.rgb = v_color; + fragColor.a = 1.; +} +)_"; + +ModelDisplayNode::ModelDisplayNode() +{ + input.push_back(new Port{this, nullptr, Types::Image, {}}); + input.push_back(new Port{this, nullptr, Types::Geometry, {}}); + input.push_back(new Port{this, nullptr, Types::Camera, {}}); + output.push_back(new Port{this, {}, Types::Image, {}}); + + m_materialData.reset((char*)&ubo); +} + +ModelDisplayNode::~ModelDisplayNode() +{ + m_materialData.release(); +} + +#include // clang-format: keep +class ModelDisplayNode::Renderer : public GenericNodeRenderer +{ +public: + using GenericNodeRenderer::GenericNodeRenderer; + + struct RenderShaders + { + QShader phongVS, phongFS; + QShader texCoordVS, texCoordFS; + QShader triplanarVS, triplanarFS; + QShader sphericalVS, sphericalFS; + QShader spherical2VS, spherical2FS; + QShader viewspaceVS, viewspaceFS; + QShader barycentricVS, barycentricFS; + QShader colorVS, colorFS; + } triangle, point; + + int m_curShader{0}; + int m_draw_mode{0}; + int64_t materialChangedIndex{-1}; + int64_t geometryChangedIndex{-1}; + int64_t meshChangedIndex{-1}; + +private: + ~Renderer() = default; + + score::gfx::TextureRenderTarget m_inputTarget; + TextureRenderTarget renderTargetForInput(const Port& p) override + { + return m_inputTarget; + } + + void initPasses_impl(RenderList& renderer, const Mesh& mesh, RenderShaders& shaders) + { + auto& n = (ModelDisplayNode&)node; + bool has_texcoord = mesh.flags() & Mesh::HasTexCoord; + bool has_normals = mesh.flags() & Mesh::HasNormals; + bool has_colors = mesh.flags() & Mesh::HasColor; + + int cur_binding = 4; + std::vector ubos; + std::vector additional_bindings; + + if (mesh.filters) + { + if (!mesh.filters->filters.empty()) + { + for (auto& f : mesh.filters->filters) + { + for (auto n : renderer.renderers) + { + if (n->id == f.node_id) + { + if (auto c = safe_cast(n)) + { + additional_bindings.push_back(QRhiShaderResourceBinding::uniformBuffer( + cur_binding, QRhiShaderResourceBinding::VertexStage, c->material())); + + cur_binding++; + break; + } + } + } + } + } + } + + if (has_colors && n.wantedProjection == 7) + { + defaultPassesInit( + renderer, mesh, shaders.colorVS, shaders.colorFS, additional_bindings); + return; + } + + if (has_texcoord && has_normals) + { + switch (n.wantedProjection) + { + default: + case 0: // Needs TCoord + defaultPassesInit( + renderer, + mesh, + shaders.texCoordVS, + shaders.texCoordFS, + additional_bindings); + break; + case 1: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.triplanarVS, + shaders.triplanarFS, + additional_bindings); + break; + case 2: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.sphericalVS, + shaders.sphericalFS, + additional_bindings); + break; + case 3: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.spherical2VS, + shaders.spherical2FS, + additional_bindings); + break; + case 4: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.viewspaceVS, + shaders.viewspaceFS, + additional_bindings); + break; + case 5: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.barycentricVS, + shaders.barycentricFS, + additional_bindings); + break; + case 6: // Needs TCoord + Normals + defaultPassesInit( + renderer, mesh, shaders.phongVS, shaders.phongFS, additional_bindings); + break; + } + } + else if (has_texcoord && !has_normals) + { + switch (n.wantedProjection) + { + default: + case 0: + case 1: + case 2: + case 3: + case 6: + defaultPassesInit( + renderer, + mesh, + shaders.texCoordVS, + shaders.texCoordFS, + additional_bindings); + break; + case 4: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.viewspaceVS, + shaders.viewspaceFS, + additional_bindings); + break; + case 5: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.barycentricVS, + shaders.barycentricFS, + additional_bindings); + break; + } + } + else if (has_normals && !has_texcoord) + { + switch (n.wantedProjection) + { + default: + case 0: + case 6: + case 1: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.triplanarVS, + shaders.triplanarFS, + additional_bindings); + break; + case 2: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.sphericalVS, + shaders.sphericalFS, + additional_bindings); + break; + case 3: // Needs Normals + defaultPassesInit( + renderer, + mesh, + shaders.spherical2VS, + shaders.spherical2FS, + additional_bindings); + break; + case 4: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.viewspaceVS, + shaders.viewspaceFS, + additional_bindings); + break; + case 5: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.barycentricVS, + shaders.barycentricFS, + additional_bindings); + break; + } + } + else if (!has_texcoord && !has_normals) + { + switch (n.wantedProjection) + { + default: + case 0: + case 1: + case 2: + case 3: + case 6: + case 4: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.viewspaceVS, + shaders.viewspaceFS, + additional_bindings); + break; + case 5: // Needs just position + defaultPassesInit( + renderer, + mesh, + shaders.barycentricVS, + shaders.barycentricFS, + additional_bindings); + break; + } + } + } + + void initPasses(RenderList& renderer, const Mesh& mesh) + { + auto& n = (ModelDisplayNode&)node; + + createShaders(renderer, mesh); + m_curShader = n.wantedProjection; + m_draw_mode = n.draw_mode; + switch (m_draw_mode) + { + case 0: + initPasses_impl(renderer, mesh, triangle); + for (auto& [e, pass] : this->m_p) + { + pass.pipeline->destroy(); + pass.pipeline->setTopology(QRhiGraphicsPipeline::Triangles); + pass.pipeline->create(); + } + break; + case 1: + initPasses_impl(renderer, mesh, point); + for (auto& [e, pass] : this->m_p) + { + pass.pipeline->destroy(); + pass.pipeline->setTopology(QRhiGraphicsPipeline::Points); + pass.pipeline->create(); + } + break; + case 2: + initPasses_impl(renderer, mesh, triangle); + for (auto& [e, pass] : this->m_p) + { + pass.pipeline->destroy(); + pass.pipeline->setTopology(QRhiGraphicsPipeline::Lines); + pass.pipeline->create(); + } + break; + } + } + + QString processVertexShader( + QString init, + std::string_view out, + std::string_view proc, + const score::gfx::Mesh& mesh) + { + + std::string vtx_define_filters; + std::string vtx_do_filters; + // Add additional bindings. + // 0: renderer + // 1: processUBO + // 2: materialUBO + // 3: input texture + // 4: it starts here :) + int cur_binding = 4; + + if (mesh.filters) + { + if (!mesh.filters->filters.empty()) + { + for (auto& f : mesh.filters->filters) + { + auto shader = f.shader; + boost::algorithm::replace_first( + shader, "%next%", std::to_string(cur_binding++)); + vtx_define_filters += shader; + vtx_do_filters += fmt::format( + "process_vertex_{}(in_position, in_normal, in_uv, in_tangent, " + "in_color);", + f.filter_id); + } + } + } + + init.replace("%vtx_define_filters%", vtx_define_filters.data()); + init.replace("%vtx_do_filters%", vtx_do_filters.data()); + init.replace("%vtx_output%", out.data()); + init.replace("%vtx_output_process%", proc.data()); + return init; + } + + void createShaders(RenderList& renderer, const score::gfx::Mesh& mesh) + { + const QString triangle_phongVS = processVertexShader( + model_display_vertex_shader_phong, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_texcoordVS = processVertexShader( + model_display_vertex_shader_texcoord, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_triplanarVS = processVertexShader( + model_display_vertex_shader_triplanar, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_sphericalVS = processVertexShader( + model_display_vertex_shader_spherical, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_spherical2VS = processVertexShader( + model_display_vertex_shader_spherical2, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_viewspaceVS = processVertexShader( + model_display_vertex_shader_viewspace, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_barycentricVS = processVertexShader( + model_display_vertex_shader_barycentric, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + const QString triangle_colorVS = processVertexShader( + model_display_vertex_shader_color, + vtx_output_triangle, + vtx_output_process_triangle, + mesh); + + std::tie(triangle.phongVS, triangle.phongFS) = score::gfx::makeShaders( + renderer.state, triangle_phongVS, model_display_fragment_shader_phong); + std::tie(triangle.texCoordVS, triangle.texCoordFS) = score::gfx::makeShaders( + renderer.state, triangle_texcoordVS, model_display_fragment_shader_texcoord); + std::tie(triangle.triplanarVS, triangle.triplanarFS) = score::gfx::makeShaders( + renderer.state, triangle_triplanarVS, model_display_fragment_shader_triplanar); + std::tie(triangle.sphericalVS, triangle.sphericalFS) = score::gfx::makeShaders( + renderer.state, triangle_sphericalVS, model_display_fragment_shader_spherical); + std::tie(triangle.spherical2VS, triangle.spherical2FS) = score::gfx::makeShaders( + renderer.state, triangle_spherical2VS, model_display_fragment_shader_spherical2); + std::tie(triangle.viewspaceVS, triangle.viewspaceFS) = score::gfx::makeShaders( + renderer.state, triangle_viewspaceVS, model_display_fragment_shader_viewspace); + std::tie(triangle.barycentricVS, triangle.barycentricFS) = score::gfx::makeShaders( + renderer.state, + triangle_barycentricVS, + model_display_fragment_shader_barycentric); + std::tie(triangle.colorVS, triangle.colorFS) = score::gfx::makeShaders( + renderer.state, triangle_colorVS, model_display_fragment_shader_color); + + const QString point_phongVS = processVertexShader( + model_display_vertex_shader_phong, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_texcoordVS = processVertexShader( + model_display_vertex_shader_texcoord, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_triplanarVS = processVertexShader( + model_display_vertex_shader_triplanar, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_sphericalVS = processVertexShader( + model_display_vertex_shader_spherical, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_spherical2VS = processVertexShader( + model_display_vertex_shader_spherical2, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_viewspaceVS = processVertexShader( + model_display_vertex_shader_viewspace, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_barycentricVS = processVertexShader( + model_display_vertex_shader_barycentric, + vtx_output_point, + vtx_output_process_point, + mesh); + const QString point_colorVS = processVertexShader( + model_display_vertex_shader_color, + vtx_output_point, + vtx_output_process_point, + mesh); + + std::tie(point.phongVS, point.phongFS) = score::gfx::makeShaders( + renderer.state, point_phongVS, model_display_fragment_shader_phong); + std::tie(point.texCoordVS, point.texCoordFS) = score::gfx::makeShaders( + renderer.state, point_texcoordVS, model_display_fragment_shader_texcoord); + std::tie(point.triplanarVS, point.triplanarFS) = score::gfx::makeShaders( + renderer.state, point_triplanarVS, model_display_fragment_shader_triplanar); + std::tie(point.sphericalVS, point.sphericalFS) = score::gfx::makeShaders( + renderer.state, point_sphericalVS, model_display_fragment_shader_spherical); + std::tie(point.spherical2VS, point.spherical2FS) = score::gfx::makeShaders( + renderer.state, point_spherical2VS, model_display_fragment_shader_spherical2); + std::tie(point.viewspaceVS, point.viewspaceFS) = score::gfx::makeShaders( + renderer.state, point_viewspaceVS, model_display_fragment_shader_viewspace); + std::tie(point.barycentricVS, point.barycentricFS) = score::gfx::makeShaders( + renderer.state, point_barycentricVS, model_display_fragment_shader_barycentric); + std::tie(point.colorVS, point.colorFS) = score::gfx::makeShaders( + renderer.state, point_colorVS, model_display_fragment_shader_color); + } + void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override + { + auto& rhi = *renderer.state.rhi; + // Sampler for input texture + + auto sampler = rhi.newSampler( + QRhiSampler::Linear, + QRhiSampler::Linear, + QRhiSampler::Linear, + QRhiSampler::Mirror, + QRhiSampler::Mirror); + sampler->setName("ModelDisplayNode::init::sampler"); + SCORE_ASSERT(sampler->create()); + + m_inputTarget = score::gfx::createRenderTarget( + renderer.state, + QRhiTexture::RGBA8, + renderer.state.renderSize, + renderer.samples(), + QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips); + + auto texture = m_inputTarget.texture; + m_samplers.push_back({sampler, texture}); + + const auto& mesh = m_mesh ? *m_mesh : renderer.defaultQuad(); + defaultMeshInit(renderer, mesh, res); + processUBOInit(renderer); + m_material.init(renderer, node.input, m_samplers); + + initPasses(renderer, mesh); + } + + template + static void fromGL(float (&from)[N], auto& to) + { + memcpy(to.data(), from, sizeof(float[N])); + } + template + static void toGL(auto& from, float (&to)[N]) + { + memcpy(to, from.data(), sizeof(float[N])); + } + + void update(RenderList& renderer, QRhiResourceUpdateBatch& res) override + { + auto& n = (ModelDisplayNode&)node; + + bool mustRecreatePasses = false; + if (n.hasMaterialChanged(materialChangedIndex)) + { + QMatrix4x4 model{}; + fromGL(n.ubo.model, model); + + QMatrix4x4 projection; + // FIXME should use the size of the target instead + // Since we can render on multiple target, this means that we must have one UBO for each + projection.perspective( + n.fov, + qreal(renderer.state.renderSize.width()) / renderer.state.renderSize.height(), + n.near, + n.far); + QMatrix4x4 view; + + view.lookAt( + QVector3D{n.position[0], n.position[1], n.position[2]}, + QVector3D{n.center[0], n.center[1], n.center[2]}, + QVector3D{0, 1, 0}); + QMatrix4x4 mv = view * model; + QMatrix4x4 mvp = projection * view * model; + QMatrix3x3 norm = model.normalMatrix(); + + ModelCameraUBO* mc = &n.ubo; + std::fill_n((char*)mc, sizeof(ModelCameraUBO), 0); + toGL(model, mc->model); + toGL(projection, mc->projection); + toGL(view, mc->view); + toGL(mv, mc->mv); + toGL(mvp, mc->mvp); + toGL(norm, mc->modelNormal); + + res.updateDynamicBuffer(m_material.buffer, 0, sizeof(ModelCameraUBO), mc); + + if (m_curShader != n.wantedProjection) + mustRecreatePasses = true; + if (m_draw_mode != n.draw_mode) + mustRecreatePasses = true; + } + + res.updateDynamicBuffer( + m_processUBO, 0, sizeof(ProcessUBO), &this->node.standardUBO); + + if (node.hasGeometryChanged(geometryChangedIndex)) + { + if (node.geometry.meshes) + { + std::tie(m_mesh, m_meshbufs) + = renderer.acquireMesh(node.geometry, res, m_mesh, m_meshbufs); + SCORE_ASSERT(m_mesh); + this->meshChangedIndex = this->m_mesh->dirtyGeometryIndex; + } + mustRecreatePasses = true; + } + + const auto& mesh = m_mesh ? *m_mesh : renderer.defaultQuad(); + if (mesh.hasGeometryChanged(meshChangedIndex)) + mustRecreatePasses = true; + + if (mustRecreatePasses) + { + for (auto& pass : m_p) + pass.second.release(); + m_p.clear(); + + const auto& mesh = m_mesh ? *m_mesh : renderer.defaultQuad(); + initPasses(renderer, mesh); + } + + res.generateMips(this->m_inputTarget.texture); + } + + void runRenderPass(RenderList& renderer, QRhiCommandBuffer& cb, Edge& edge) override + { + const auto& mesh = m_mesh ? *m_mesh : renderer.defaultQuad(); + defaultRenderPass(renderer, mesh, cb, edge); + } + + void release(RenderList& r) override + { + m_inputTarget.release(); + defaultRelease(r); + } +}; +#include // clang-format: keep + +NodeRenderer* ModelDisplayNode::createRenderer(RenderList& r) const noexcept +{ + return new Renderer{*this}; +} + +void ModelDisplayNode::process(Message&& msg) +{ + ProcessNode::process(msg.token); + + int32_t p = 0; + for (const gfx_input& m : msg.input) + { + if (auto val = ossia::get_if(&m)) + { + switch (p) + { + case 2: + { + this->position = ossia::convert(*val); + this->materialChange(); + break; + } + case 3: + { + this->center = ossia::convert(*val); + this->materialChange(); + break; + } + case 4: + { + this->fov = ossia::convert(*val); + this->materialChange(); + break; + } + case 5: + { + this->near = ossia::convert(*val); + this->materialChange(); + break; + } + case 6: + { + this->far = ossia::convert(*val); + this->materialChange(); + break; + } + case 7: + { + this->wantedProjection = ossia::convert(*val); + this->materialChange(); + break; + } + case 8: + { + this->draw_mode = ossia::convert(*val); + this->materialChange(); + break; + } + } + + p++; + } + else if (auto val = ossia::get_if(&m)) + { + ProcessNode::process(p, *val); + + p++; + } + else if (auto val = ossia::get_if(&m)) + { + memcpy(this->ubo.model, val->matrix, sizeof(val->matrix)); + this->materialChange(); + } + else + { + p++; + } + } +} +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.hpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.hpp new file mode 100644 index 0000000000..2339c5eba5 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/ModelDisplayNode.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include +#include + +#if defined(near) +#undef near +#undef far +#endif + +namespace score::gfx +{ +/** + * @brief A node that renders a model to screen. + */ +struct ModelDisplayNode : NodeModel +{ +public: + explicit ModelDisplayNode(); + virtual ~ModelDisplayNode(); + + score::gfx::NodeRenderer* createRenderer(RenderList& r) const noexcept override; + + void process(Message&& msg) override; + class Renderer; + ModelCameraUBO ubo; + + ossia::vec3f position, center; + float fov{90.f}, near{0.001f}, far{10000.f}; + + int wantedProjection{}; + int draw_mode{}; +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.cpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.cpp new file mode 100644 index 0000000000..909b84386c --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.cpp @@ -0,0 +1,118 @@ +#include "Process.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +W_OBJECT_IMPL(Gfx::ModelDisplay::Model) +namespace Gfx::ModelDisplay +{ + +Model::Model( + const TimeVal& duration, + const Id& id, + QObject* parent) + : Process::ProcessModel{duration, id, "gfxProcess", parent} +{ + metadata().setInstanceName(*this); + + m_inlets.push_back(new TextureInlet{Id(0), this}); + m_inlets.push_back(new GeometryInlet{Id(1), this}); + + m_inlets.push_back(new Process::XYZSpinboxes{ + ossia::vec3f{-10000., -10000., -10000.}, + ossia::vec3f{10000., 10000., 10000.}, + ossia::vec3f{-1., -1., -1.}, + "Position", + Id(2), + this}); + m_inlets.push_back(new Process::XYZSpinboxes{ + ossia::vec3f{-10000., -10000., -10000.}, + ossia::vec3f{10000., 10000., 10000.}, + ossia::vec3f{}, + "Center", + Id(3), + this}); + + m_inlets.push_back( + new Process::FloatSlider{0.01, 179.9, 90., "FOV", Id(4), this}); + m_inlets.push_back( + new Process::FloatSlider{0.001, 1000., 0.001, "Near", Id(5), this}); + m_inlets.push_back(new Process::FloatSlider{ + 0.001, 10000., 100000., "Far", Id(6), this}); + + std::vector> projs{ + {"Texture coordinates", 0}, + {"Spherical", 2}, + {"View-space", 4}, + {"Barycentric", 5}, + {"Funky A", 1}, + {"Funky B", 3}, + {"Light", 6}, + {"Color", 7}, + }; + + m_inlets.push_back( + new Process::ComboBox{projs, 0, "Projection", Id(7), this}); + + std::vector> modes{ + {"Triangles", 0}, + {"Points", 1}, + {"Lines", 2}, + }; + + m_inlets.push_back( + new Process::ComboBox{modes, 0, "Mode", Id(8), this}); + m_outlets.push_back(new TextureOutlet{Id(0), this}); +} + +Model::~Model() = default; + +QString Model::prettyName() const noexcept +{ + return tr("Model Display"); +} + +} +template <> +void DataStreamReader::read(const Gfx::ModelDisplay::Model& proc) +{ + readPorts(*this, proc.m_inlets, proc.m_outlets); + + insertDelimiter(); +} + +template <> +void DataStreamWriter::write(Gfx::ModelDisplay::Model& proc) +{ + writePorts( + *this, + components.interfaces(), + proc.m_inlets, + proc.m_outlets, + &proc); + + checkDelimiter(); +} + +template <> +void JSONReader::read(const Gfx::ModelDisplay::Model& proc) +{ + readPorts(*this, proc.m_inlets, proc.m_outlets); +} + +template <> +void JSONWriter::write(Gfx::ModelDisplay::Model& proc) +{ + writePorts( + *this, + components.interfaces(), + proc.m_inlets, + proc.m_outlets, + &proc); +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.hpp b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.hpp new file mode 100644 index 0000000000..bde6436bf0 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ModelDisplay/Process.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#include +namespace Gfx::ModelDisplay +{ +class Model final : public Process::ProcessModel +{ + SCORE_SERIALIZE_FRIENDS + PROCESS_METADATA_IMPL(Gfx::ModelDisplay::Model) + W_OBJECT(Model) + +public: + constexpr bool hasExternalUI() { return false; } + Model( + const TimeVal& duration, + const Id& id, + QObject* parent); + + template + Model(Impl& vis, QObject* parent) + : Process::ProcessModel{vis, parent} + { + vis.writeTo(*this); + } + + ~Model() override; + +private: + QString prettyName() const noexcept override; +}; + +using ProcessFactory = Process::ProcessFactory_T; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Noise.cpp b/src/plugins/score-plugin-threedim/Threedim/Noise.cpp new file mode 100644 index 0000000000..3f5622bc73 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Noise.cpp @@ -0,0 +1,137 @@ +#include "Noise.hpp" + +#include + +#include + +#include +namespace Threedim +{ +Noise::Noise() { } + +Noise::~Noise() +{ + if (!outputs.geometry.mesh.buffers.empty()) + { + auto& b = outputs.geometry.mesh.buffers[0]; + delete[] (float*)b.data; + } +} + +const siv::BasicPerlinNoise engine{4u}; // chosen by fair dice roll +void Noise::operator()(tick tt) +{ + auto old_bufs = outputs.geometry.mesh.buffers; + (GeometryPort&)outputs.geometry = (const GeometryPort&)inputs.geometry; + + if (!outputs.geometry.mesh.buffers.empty()) + { + int old_buf_idx = 0; + for (auto& buf : outputs.geometry.mesh.buffers) + { + if (old_buf_idx < old_bufs.size()) + { + auto old = old_bufs[old_buf_idx]; + auto cur = (float*)buf.data; + void* newb{}; + if (buf.size == old.size) + { + newb = old.data; + } + else + { + delete[] (float*)old.data; + newb = new float[buf.size]; + } + memcpy(newb, cur, buf.size); + buf.data = newb; + } + else + { + auto cur = (float*)buf.data; + buf.data = new float[buf.size]; + memcpy(buf.data, cur, buf.size); + } + old_buf_idx++; + } + } + else + { + for (auto buf : old_bufs) + { + delete (float*)buf.data; + } + } + outputs.geometry.dirty_mesh = true; + outputs.geometry.dirty_transform = true; + + outputs.geometry.dirty_mesh = true; + //outputs.geometry.dirty = true; + auto& mesh = outputs.geometry.mesh; + if (mesh.buffers.empty()) + return; + + // Find the position attribute: + auto& attr = mesh.attributes; + auto it + = std::find_if(attr.begin(), attr.end(), [](auto& a) { return a.location == 0; }); + if (it == attr.end()) + return; + + const int binding = it->binding; + auto& ins = mesh.input; + assert(binding >= 0); + assert(binding < ins.size()); + + const int buffer = ins[binding].buffer; + + auto& bufs = mesh.buffers; + assert(buffer >= 0); + assert(buffer < bufs.size()); + + auto& buf = bufs[buffer]; + + // Cheat a bit for now... and assume that position comes first, + // and that things aren't interleaved, + // and that we have float[3]s, ... + using type = float[3]; + std::span vertices((type*)buf.data, mesh.vertices); + + using f_type = double (*)(double x, double intens, double t) noexcept; + auto func = [&](DeformationControl::enum_type c) noexcept -> f_type + { + switch (c) + { + default: + case DeformationControl::None: + return [](double x, double intens, double t) noexcept { return x; }; + break; + case DeformationControl::Noise: + return [](double x, double intens, double t) noexcept + { return x + intens * 100. * engine.noise1D(x + t); }; + break; + case DeformationControl::Sine: + return [](double x, double intens, double t) noexcept + { return x + intens * 100. * std::sin(x + t); }; + break; + } + }; + + const f_type fs[3] = {func(inputs.dx), func(inputs.dy), func(inputs.dz)}; + + const double t = tt.position_in_frames / 44100.; + + const double ix = inputs.ix.value; + const double iy = inputs.iy.value; + const double iz = inputs.iz.value; + + for (float(&v)[3] : vertices) + { + v[0] = fs[0](v[0], ix, t + v[1] + v[2]); + v[1] = fs[1](v[1], iy, t + v[0] + v[2]); + v[2] = fs[2](v[2], iz, t + v[0] + v[1]); + } + mesh.buffers[0].dirty = true; +} + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Noise.hpp b/src/plugins/score-plugin-threedim/Threedim/Noise.hpp new file mode 100644 index 0000000000..db5265c039 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Noise.hpp @@ -0,0 +1,106 @@ +#pragma once + +/* SPDX-License-Identifier: GPL-3.0-or-later */ + +#include +#include +#include + +#include +#include +#include +#include + +namespace Threedim +{ +struct GeometryPort +{ + halp::dynamic_geometry mesh; + float transform[16]{}; + bool dirty_mesh = false; + bool dirty_transform = false; +}; +struct DeformationControl +{ + enum enum_type + { + None, + Noise, + Sine + } value{}; + + enum widget + { + enumeration, + list, + combobox + }; + + struct range + { + std::string_view values[3]{"None", "Noise", "Sine"}; + enum_type init = enum_type::Noise; + }; + + operator enum_type&() noexcept { return value; } + operator const enum_type&() const noexcept { return value; } + auto& operator=(enum_type t) noexcept + { + value = t; + return *this; + } +}; + +struct Noise +{ + halp_meta(name, "Mesh Noise") + halp_meta(c_name, "mesh_noise") + halp_meta(uuid, "4f493663-3739-43df-94b5-20a31c4dc8aa") + halp_meta(category, "Visuals/3D/Modifiers") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/meshes.html#noise") + halp_meta(author, "Jean-Michaël Celerier") + + struct + { + struct : GeometryPort + { + halp_meta(name, "Geometry"); + } geometry; + struct : DeformationControl + { + halp_meta(name, "Deformation X"); + } dx; + struct : DeformationControl + { + halp_meta(name, "Deformation Y"); + } dy; + struct : DeformationControl + { + halp_meta(name, "Deformation Z"); + } dz; + halp::hslider_f32<"Intensity X"> ix; + halp::hslider_f32<"Intensity Y"> iy; + halp::hslider_f32<"Intensity Z"> iz; + + } inputs; + + struct + { + struct : GeometryPort + { + halp_meta(name, "Geometry"); + } geometry; + } outputs; + + Noise(); + ~Noise(); + + struct tick + { + int frames; + int64_t position_in_frames; + }; + + void operator()(tick); +}; +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ObjLoader.cpp b/src/plugins/score-plugin-threedim/Threedim/ObjLoader.cpp new file mode 100644 index 0000000000..01358c593a --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ObjLoader.cpp @@ -0,0 +1,189 @@ +#include "ObjLoader.hpp" + +#include "Ply.hpp" + +#include +#include + +namespace Threedim +{ + +void ObjLoader::operator()() { } + +void ObjLoader::rebuild_geometry() +{ + std::vector& new_meshes = this->meshinfo; + + if (!outputs.geometry.mesh.empty()) + { + outputs.geometry.mesh.clear(); + } + + for (auto& m : new_meshes) + { + if (m.vertices <= 0) + continue; + + halp::dynamic_geometry geom; + + geom.buffers.clear(); + geom.bindings.clear(); + geom.attributes.clear(); + geom.input.clear(); + if (m.points) + { + geom.topology = halp::dynamic_geometry::points; + geom.cull_mode = halp::dynamic_geometry::none; + geom.front_face = halp::dynamic_geometry::counter_clockwise; + } + else + { + geom.topology = halp::dynamic_geometry::triangles; + geom.cull_mode = halp::dynamic_geometry::back; + geom.front_face = halp::dynamic_geometry::counter_clockwise; + } + geom.index = {}; + + geom.vertices = m.vertices; + + geom.buffers.push_back(halp::dynamic_geometry::buffer{ + .data = this->complete.data(), + .size = int64_t(this->complete.size() * sizeof(float)), + .dirty = true}); + + // Bindings + geom.bindings.push_back(halp::dynamic_geometry::binding{ + .stride = 3 * sizeof(float), + .step_rate = 1, + .classification = halp::dynamic_geometry::binding::per_vertex}); + + if (m.texcoord) + { + geom.bindings.push_back(halp::dynamic_geometry::binding{ + .stride = 2 * sizeof(float), + .step_rate = 1, + .classification = halp::dynamic_geometry::binding::per_vertex}); + } + + if (m.normals) + { + geom.bindings.push_back(halp::dynamic_geometry::binding{ + .stride = 3 * sizeof(float), + .step_rate = 1, + .classification = halp::dynamic_geometry::binding::per_vertex}); + } + + if (m.colors) + { + geom.bindings.push_back(halp::dynamic_geometry::binding{ + .stride = 3 * sizeof(float), + .step_rate = 1, + .classification = halp::dynamic_geometry::binding::per_vertex}); + } + + // Attributes + geom.attributes.push_back(halp::dynamic_geometry::attribute{ + .binding = 0, + .location = halp::dynamic_geometry::attribute::position, + .format = halp::dynamic_geometry::attribute::float3, + .offset = 0}); + + if (m.texcoord) + { + geom.attributes.push_back(halp::dynamic_geometry::attribute{ + .binding = geom.attributes.back().binding + 1, + .location = halp::dynamic_geometry::attribute::tex_coord, + .format = halp::dynamic_geometry::attribute::float2, + .offset = 0}); + } + + if (m.normals) + { + geom.attributes.push_back(halp::dynamic_geometry::attribute{ + .binding = geom.attributes.back().binding + 1, + .location = halp::dynamic_geometry::attribute::normal, + .format = halp::dynamic_geometry::attribute::float3, + .offset = 0}); + } + + if (m.colors) + { + geom.attributes.push_back(halp::dynamic_geometry::attribute{ + .binding = geom.attributes.back().binding + 1, + .location = halp::dynamic_geometry::attribute::color, + .format = halp::dynamic_geometry::attribute::float3, + .offset = 0}); + } + + // Vertex input + using input_t = struct halp::dynamic_geometry::input; + geom.input.push_back( + input_t{.buffer = 0, .offset = m.pos_offset * (int)sizeof(float)}); + + if (m.texcoord) + { + geom.input.push_back( + input_t{.buffer = 0, .offset = m.texcoord_offset * (int)sizeof(float)}); + } + + if (m.normals) + { + geom.input.push_back( + input_t{.buffer = 0, .offset = m.normal_offset * (int)sizeof(float)}); + } + + if (m.colors) + { + geom.input.push_back( + input_t{.buffer = 0, .offset = m.color_offset * (int)sizeof(float)}); + } + outputs.geometry.mesh.push_back(std::move(geom)); + outputs.geometry.dirty_mesh = true; + } +} + +static bool check_file_extension(std::string_view filename, std::string_view expected) +{ + if (filename.size() < expected.size()) + return false; + auto ext = filename.substr(filename.size() - expected.size(), expected.size()); + for (int i = 0; i < expected.size(); i++) + if (std::tolower(ext[i]) != std::tolower(expected[i])) + return false; + return true; +} + +std::function ObjLoader::ins::obj_t::process(file_type tv) +{ + auto upload = [](auto&& mesh, auto&& buf) + { + return [mesh = std::move(mesh), buf = std::move(buf)](ObjLoader& o) mutable + { + // This part happens in the execution thread + std::swap(o.meshinfo, mesh); + std::swap(o.complete, buf); + + o.rebuild_geometry(); + }; + }; + + Threedim::float_vec buf; + if (check_file_extension(tv.filename, "obj")) + { + // This part happens in a separate thread + if (auto mesh = Threedim::ObjFromString(tv.bytes, buf); !mesh.empty()) + { + return upload(std::move(mesh), std::move(buf)); + } + } + else if (check_file_extension(tv.filename, "ply")) + { + // This part happens in a separate thread + if (auto mesh = Threedim::PlyFromFile(tv.filename, buf); !mesh.empty()) + { + return upload(std::move(mesh), std::move(buf)); + } + } + return {}; +} +} diff --git a/src/plugins/score-plugin-threedim/Threedim/ObjLoader.hpp b/src/plugins/score-plugin-threedim/Threedim/ObjLoader.hpp new file mode 100644 index 0000000000..e0e764706b --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/ObjLoader.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Threedim +{ + +class ObjLoader +{ +public: + halp_meta(name, "Object loader") + halp_meta(category, "Visuals/3D") + halp_meta(c_name, "obj_loader") + halp_meta( + authors, + "Jean-Michaël Celerier, TinyOBJ authors, miniPLY authors, Eigen authors") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/meshes.html#obj-loader") + halp_meta(uuid, "5df71765-505f-4ab7-98c1-f305d10a01ef") + + struct ins + { + struct obj_t : halp::file_port<"3D file"> + { + halp_meta(extensions, "3D files (*.obj *.ply)"); + static std::function process(file_type data); + } obj; + PositionControl position; + RotationControl rotation; + ScaleControl scale; + } inputs; + + struct + { + struct : halp::mesh + { + halp_meta(name, "Geometry"); + std::vector mesh; + } geometry; + } outputs; + + void operator()(); + + void rebuild_geometry(); + + std::vector meshinfo{}; + float_vec complete; +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.cpp b/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.cpp new file mode 100644 index 0000000000..ac1aee5196 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.cpp @@ -0,0 +1,46 @@ +#include "PCLToGeometry.hpp" + +#include +#include +#include + +#include +#include + +namespace Threedim +{ +void PCLToMesh::operator()() +{ + auto& tex = this->inputs.in.texture; + if (!tex.changed) + return; + + float* data = reinterpret_cast(tex.bytes); + create_mesh(std::span(data, tex.bytesize / sizeof(float))); +} +void PCLToMesh::create_mesh(std::span v) +{ + { + // std::size_t vertices = v.size() / 3; + + // this->complete.clear(); + // this->complete.resize(std::ceil((v.size() / 3.) * (3 + 3 + 2))); + // std::copy_n(v.begin(), v.size(), complete.begin()); + + // auto& pch = rnd::fast_random_device(); + // this->complete.resize(6 * 25000); + // for (float& v : this->complete) + // v = std::uniform_real_distribution<>{0.f, 1.f}(pch); + + complete.assign(v.begin(), v.end()); + + outputs.geometry.mesh.buffers.main_buffer.data = complete.data(); + outputs.geometry.mesh.buffers.main_buffer.size = complete.size(); + outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + outputs.geometry.mesh.input.input0.offset = 0; + outputs.geometry.mesh.vertices = v.size() / 6; + outputs.geometry.dirty_mesh = true; + } +} +} diff --git a/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.hpp b/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.hpp new file mode 100644 index 0000000000..0a006722ed --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/PCLToGeometry.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Threedim +{ +struct raw_texture +{ + unsigned char* bytes{}; + std::size_t bytesize{}; + + enum format + { + Raw + }; + bool changed{}; +}; + +class PCLToMesh +{ +public: + halp_meta(name, "Pointcloud to mesh") + halp_meta(category, "Visuals/3D") + halp_meta(c_name, "pointcloud_to_mesh") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/pointcloud-to-mesh.html") + halp_meta(uuid, "2450ffbf-04ed-4b42-8848-69f200d2742a") + + struct ins + { + struct pcl_in + { + static constexpr auto name() { return "Texture"; } + raw_texture texture; + } in; + // halp::texture_input<"Texture"> inx; + PositionControl position; + RotationControl rotation; + ScaleControl scale; + } inputs; + + struct + { + struct + { + halp_meta(name, "Geometry"); + halp::position_color_packed_geometry mesh; + float transform[16]{}; + bool dirty_mesh = false; + bool dirty_transform = false; + } geometry; + } outputs; + + PCLToMesh() { rebuild_transform(inputs, outputs); } + void create_mesh(std::span v); + void operator()(); + + std::vector complete; +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Ply.cpp b/src/plugins/score-plugin-threedim/Threedim/Ply.cpp new file mode 100644 index 0000000000..3f96d9d041 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Ply.cpp @@ -0,0 +1,216 @@ +#include "Ply.hpp" + +#include +namespace Threedim +{ + +struct TriMesh +{ + float_vec storage; + float* pos = nullptr; + float* uv = nullptr; + float* norm = nullptr; + float* color = nullptr; + uint32_t numVerts = 0; + + int* indices = nullptr; + uint32_t numIndices = 0; +}; + +static bool print_ply_header(const char* filename) +{ + static const char* kPropertyTypes[] = { + "char", + "uchar", + "short", + "ushort", + "int", + "uint", + "float", + "double", + }; + + miniply::PLYReader reader(filename); + if (!reader.valid()) + { + return false; + } + using namespace miniply; + for (uint32_t i = 0, endI = reader.num_elements(); i < endI; i++) + { + const miniply::PLYElement* elem = reader.get_element(i); + fprintf(stderr, "element %s %u\n", elem->name.c_str(), elem->count); + for (const miniply::PLYProperty& prop : elem->properties) + { + if (prop.countType != miniply::PLYPropertyType::None) + { + fprintf( + stderr, + "property list %s %s %s\n", + kPropertyTypes[uint32_t(prop.countType)], + kPropertyTypes[uint32_t(prop.type)], + prop.name.c_str()); + } + else + { + fprintf( + stderr, + "property %s %s\n", + kPropertyTypes[uint32_t(prop.type)], + prop.name.c_str()); + } + } + } + fprintf(stderr, "end_header\n"); + + return true; +} + +static bool +load_vert_from_ply(miniply::PLYReader& reader, TriMesh* trimesh, float_vec& buf) +{ + uint32_t pos_indices[3]; + uint32_t uv_indices[3]; + uint32_t n_indices[3]; + uint32_t col_indices[3]; + + if (reader.element_is(miniply::kPLYVertexElement) && reader.load_element()) + { + const auto N = reader.num_rows(); + if (N <= 0) + return false; + + trimesh->numVerts = N; + + bool pos = reader.find_pos(pos_indices); + bool uv = reader.find_texcoord(uv_indices); + bool norms = reader.find_normal(n_indices); + bool col = reader.find_color(col_indices); + if (!col) + { + col = reader.find_properties( + col_indices, 3, "diffuse_red", "diffuse_green", "diffuse_blue"); + } + + int num_elements = 0; + if (pos) + num_elements += 3; + if (uv) + num_elements += 2; + if (norms) + num_elements += 3; + if (col) + num_elements += 3; + + if (!pos) + return false; + + buf.resize(num_elements * trimesh->numVerts, boost::container::default_init); + + float* cur = buf.data(); + if (pos) + { + trimesh->pos = cur; + reader.extract_properties( + pos_indices, 3, miniply::PLYPropertyType::Float, trimesh->pos); + cur += 3 * N; + } + + if (uv) + { + trimesh->uv = cur; + reader.extract_properties( + uv_indices, 2, miniply::PLYPropertyType::Float, trimesh->uv); + cur += 2 * N; + } + + if (norms) + { + trimesh->norm = cur; + reader.extract_properties( + n_indices, 3, miniply::PLYPropertyType::Float, trimesh->norm); + cur += 3 * N; + } + + if (col) + { + const auto t = reader.element()->properties[col_indices[0]].type; + trimesh->color = cur; + reader.extract_properties( + col_indices, 3, miniply::PLYPropertyType::Float, trimesh->color); + switch (t) + { + case miniply::PLYPropertyType::Float: + break; + case miniply::PLYPropertyType::Char: + break; + case miniply::PLYPropertyType::UChar: + { + for (float *begin = trimesh->color, *end = trimesh->color + 3 * N; + begin != end; + ++begin) + *begin /= 255.f; + break; + } + default: + break; + } + } + return true; + } + return false; +} + +static TriMesh load_vertices_from_ply(miniply::PLYReader& reader, float_vec& buf) +{ + TriMesh mesh; + + while (reader.has_element()) + { + if (load_vert_from_ply(reader, &mesh, buf)) + return mesh; + reader.next_element(); + } + + return {}; +} + +std::vector PlyFromFile(std::string_view filename, float_vec& buf) +{ + print_ply_header(filename.data()); + + std::vector meshes; + miniply::PLYReader reader{filename.data()}; + if (!reader.valid()) + return {}; + + auto res = load_vertices_from_ply(reader, buf); + if (!res.pos) + return {}; + + auto begin = buf.data(); + mesh m{}; + m.vertices = res.numVerts; + m.points = true; + m.pos_offset = 0; + if (res.uv) + { + m.texcoord_offset = res.uv - begin; + m.texcoord = true; + } + if (res.norm) + { + m.normal_offset = res.norm - begin; + m.normals = true; + } + if (res.color) + { + m.color_offset = res.color - begin; + m.colors = true; + } + + meshes.push_back(m); + return meshes; +} + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Ply.hpp b/src/plugins/score-plugin-threedim/Threedim/Ply.hpp new file mode 100644 index 0000000000..7d1f59e43a --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Ply.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace Threedim +{ +std::vector PlyFromFile(std::string_view filename, float_vec& data); +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Primitive.cpp b/src/plugins/score-plugin-threedim/Threedim/Primitive.cpp new file mode 100644 index 0000000000..6b8b769023 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Primitive.cpp @@ -0,0 +1,182 @@ +#include "Primitive.hpp" + +#include +#include + +#include +#include + +namespace Threedim +{ + +void loadTriMesh(TMesh& mesh, std::vector& complete, PrimitiveOutputs& outputs) +{ + vcg::tri::Clean::RemoveUnreferencedVertex(mesh); + vcg::tri::Clean::RemoveZeroAreaFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::Clean::RemoveNonManifoldFace(mesh); + vcg::tri::UpdateTopology::FaceFace(mesh); + vcg::tri::UpdateNormal::PerVertexNormalized(mesh); + vcg::tri::UpdateTexture::WedgeTexFromPlane( + mesh, vcg::Point3f{0., 0., 0.}, vcg::Point3f{1., 1., 1.}, 1.); + + vcg::tri::RequirePerVertexNormal(mesh); + vcg::tri::RequirePerVertexTexCoord(mesh); + + complete.clear(); + const auto vertices = mesh.vert.size(); + const auto floats + = vertices * (3 + 3 + 2); // 3 float for pos, 3 float for normal, 2 float for UV + complete.resize(floats); + float* pos_start = complete.data(); + float* norm_start = complete.data() + vertices * 3; + float* uv_start = complete.data() + vertices * 3 + vertices * 3; + + for (auto& fi : mesh.face) + { // iterate each faces + + auto v0 = fi.V(0); + auto v1 = fi.V(1); + auto v2 = fi.V(2); + + auto p0 = v0->P(); + (*pos_start++) = p0.X(); + (*pos_start++) = p0.Y(); + (*pos_start++) = p0.Z(); + + auto p1 = v1->P(); + (*pos_start++) = p1.X(); + (*pos_start++) = p1.Y(); + (*pos_start++) = p1.Z(); + + auto p2 = v2->P(); + (*pos_start++) = p2.X(); + (*pos_start++) = p2.Y(); + (*pos_start++) = p2.Z(); + + auto n0 = v0->N(); + (*norm_start++) = n0.X(); + (*norm_start++) = n0.Y(); + (*norm_start++) = n0.Z(); + + auto n1 = v1->N(); + (*norm_start++) = n1.X(); + (*norm_start++) = n1.Y(); + (*norm_start++) = n1.Z(); + + auto n2 = v2->N(); + (*norm_start++) = n2.X(); + (*norm_start++) = n2.Y(); + (*norm_start++) = n2.Z(); + +#if 0 + auto uv0 = fi.WT(0); + (*uv_start++) = uv0.U(); + (*uv_start++) = uv0.V(); + + auto uv1 = fi.WT(1); + (*uv_start++) = uv1.U(); + (*uv_start++) = uv1.V(); + + auto uv2 = fi.WT(2); + (*uv_start++) = uv2.U(); + (*uv_start++) = uv2.V(); +#endif + (*uv_start++) = p0.X(); + (*uv_start++) = p0.Y(); + + (*uv_start++) = p1.X(); + (*uv_start++) = p1.Y(); + + (*uv_start++) = p2.X(); + (*uv_start++) = p2.Y(); + } + outputs.geometry.mesh.buffers.main_buffer.data = complete.data(); + outputs.geometry.mesh.buffers.main_buffer.size = complete.size(); + outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + outputs.geometry.mesh.input.input0.offset = 0; + outputs.geometry.mesh.input.input1.offset = sizeof(float) * vertices * 3; + outputs.geometry.mesh.input.input2.offset = sizeof(float) * vertices * (3 + 3); + outputs.geometry.mesh.vertices = vertices; + outputs.geometry.dirty_mesh = true; +} + +static thread_local TMesh mesh; +void Plane::update() +{ + /* + // clang-format off + static const constexpr float data[] = { + // positions + -1, -1, 0, + +1, -1, 0, + -1, +1, 0, + +1, +1, 0, + // tex coords + 0, 0, + 1, 0, + 0, 1, + 1, 1 + }; + // clang-format on + + outputs.geometry.mesh.buffers.main_buffer.data = (float*)data; + outputs.geometry.mesh.buffers.main_buffer.size = std::ssize(data); + outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + outputs.geometry.mesh.input.input1.offset = 12 * sizeof(float); + outputs.geometry.mesh.vertices = 4; + outputs.geometry.dirty_mesh = true; + */ + mesh.Clear(); + vcg::tri::Grid(mesh, inputs.hdivs, inputs.vdivs, 1., 1.); + loadTriMesh(mesh, complete, outputs); +} + +void Cube::update() +{ + mesh.Clear(); + vcg::Box3 box; + box.min = {-1, -1, -1}; + box.max = {1, 1, 1}; + vcg::tri::Box(mesh, box); + loadTriMesh(mesh, complete, outputs); +} + +void Sphere::update() +{ + mesh.Clear(); + vcg::tri::Sphere(mesh, inputs.subdiv); + loadTriMesh(mesh, complete, outputs); +} + +void Icosahedron::update() +{ + mesh.Clear(); + vcg::tri::Icosahedron(mesh); + loadTriMesh(mesh, complete, outputs); +} + +void Cone::update() +{ + mesh.Clear(); + vcg::tri::Cone(mesh, inputs.r1, inputs.r2, inputs.h, inputs.subdiv); + loadTriMesh(mesh, complete, outputs); +} + +void Cylinder::update() +{ + mesh.Clear(); + vcg::tri::Cylinder(inputs.slices, inputs.stacks, mesh, true); + loadTriMesh(mesh, complete, outputs); +} + +void Torus::update() +{ + mesh.Clear(); + vcg::tri::Torus(mesh, inputs.r1, inputs.r2, inputs.hdiv, inputs.vdiv); + loadTriMesh(mesh, complete, outputs); +} + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/Primitive.hpp b/src/plugins/score-plugin-threedim/Threedim/Primitive.hpp new file mode 100644 index 0000000000..4de0be52d9 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/Primitive.hpp @@ -0,0 +1,200 @@ +#pragma once + +#include +#include +#include +#include + +namespace Threedim +{ +struct Primitive +{ + halp_meta(category, "Visuals/3D/Primitives") + halp_meta(author, "Jean-Michaël Celerier, vcglib") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/meshes.html#primitive") + + void operator()() { } + PrimitiveOutputs outputs; + std::vector complete; +}; + +struct Plane : Primitive +{ +public: + halp_meta(name, "Plane") + halp_meta(c_name, "3d_plane") + halp_meta(uuid, "1e923d52-3494-49e8-8698-b001405000da") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + halp::spinbox_i32<"H divs.", halp::range{1, 1000, 16}> hdivs; + halp::spinbox_i32<"V divs.", halp::range{1, 1000, 16}> vdivs; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Cube : Primitive +{ +public: + halp_meta(name, "Cube") + halp_meta(c_name, "3d_cube") + halp_meta(uuid, "cf8a328a-1ba6-47f8-929f-2168bdec90b0") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Sphere : Primitive +{ +public: + halp_meta(name, "Sphere") + halp_meta(c_name, "3d_sphere") + halp_meta(uuid, "fc0df335-d0e9-4ebf-b438-6ba334741c1a") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + struct + : halp::hslider_i32<"Subdivisions", halp::range{1, 5, 2}> + , Update + { + } subdiv; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Icosahedron : Primitive +{ + halp_meta(name, "Icosahedron") + halp_meta(c_name, "3d_ico") + halp_meta(uuid, "3ea9f69f-1a0e-49c2-ad16-a88e9ca628a7") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Cone : Primitive +{ + halp_meta(name, "Cone") + halp_meta(c_name, "3d_cone") + halp_meta(uuid, "8a5718c4-07f0-476b-b720-1c99e5a379a5") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + struct + : halp::hslider_i32<"Subdivisions", halp::range{1, 500, 36}> + , Update + { + } subdiv; + struct + : halp::hslider_f32<"R1", halp::range{0, 1000, 1}> + , Update + { + } r1; + struct + : halp::hslider_f32<"R2", halp::range{0, 1000, 10}> + , Update + { + } r2; + struct + : halp::hslider_f32<"Height", halp::range{0, 1000, 5}> + , Update + { + } h; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Cylinder : Primitive +{ + halp_meta(name, "Cylinder") + halp_meta(c_name, "3d_cylinder") + halp_meta(uuid, "5992830e-80fe-4461-b357-2c9b5c5e48ae") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + struct + : halp::hslider_i32<"Slices", halp::range{1, 1000, 64}> + , Update + { + } slices; + struct + : halp::hslider_i32<"Stacks", halp::range{1, 1000, 64}> + , Update + { + } stacks; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +struct Torus : Primitive +{ + halp_meta(name, "Torus") + halp_meta(c_name, "3d_torus") + halp_meta(uuid, "85c5983c-3f4f-4bfe-b8cf-fccdf6ec5faf") + + struct + { + PositionControl position; + RotationControl rotation; + ScaleControl scale; + struct + : halp::hslider_f32<"R1", halp::range{0, 100, 10}> + , Update + { + } r1; + struct + : halp::hslider_f32<"R2", halp::range{0, 100, 1}> + , Update + { + } r2; + struct + : halp::hslider_i32<"H Divisions", halp::range{1, 50, 24}> + , Update + { + } hdiv; + struct + : halp::hslider_i32<"V Divisions", halp::range{1, 50, 12}> + , Update + { + } vdiv; + } inputs; + + void prepare(halp::setup) { update(); } + void update(); +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/StructureSynth.cpp b/src/plugins/score-plugin-threedim/Threedim/StructureSynth.cpp new file mode 100644 index 0000000000..0bca207837 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/StructureSynth.cpp @@ -0,0 +1,101 @@ +#include "StructureSynth.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace Threedim +{ +static auto CreateObj(const QString& input) +try +{ + /* + QString input = R"_(set maxdepth 2000 +{ a 0.9 hue 30 } R1 + +rule R1 w 10 { +{ x 1 rz 3 ry 5 } R1 +{ s 1 1 0.1 sat 0.9 } box +} + +rule R1 w 10 { +{ x 1 rz -3 ry 5 } R1 +{ s 1 1 0.1 } box +} +)_"; +*/ + ssynth::Parser::Preprocessor p; + auto preprocessed = p.Process(input); + + ssynth::Parser::Tokenizer t{std::move(preprocessed)}; + ssynth::Parser::EisenParser e{t}; + + auto ruleset = std::unique_ptr{e.parseRuleset()}; + ruleset->resolveNames(); + ruleset->dumpInfo(); + + ssynth::Model::Rendering::ObjRenderer obj{10, 10, true, false}; + ssynth::Model::Builder b(&obj, ruleset.get(), true); + b.build(); + + QByteArray data; + { + QTextStream ts(&data); + obj.writeToStream(ts); + ts.flush(); + } + + return data.toStdString(); +} +catch (const std::exception& e) +{ + qDebug() << e.what(); + return std::string{}; +} +catch (...) +{ + return std::string{}; +} + +void StrucSynth::operator()() { } + +std::function StrucSynth::worker::work(std::string_view in) +{ + if (in.empty()) + return {}; + + auto input = CreateObj(QString::fromUtf8(in.data(), in.size())); + if (input.empty()) + return {}; + + Threedim::float_vec buf; + if (auto mesh = Threedim::ObjFromString(input, buf); !mesh.empty()) + { + return [b = std::move(buf)](StrucSynth& s) mutable + { + std::swap(b, s.m_vertexData); + s.outputs.geometry.mesh.buffers.main_buffer.data = s.m_vertexData.data(); + s.outputs.geometry.mesh.buffers.main_buffer.size = s.m_vertexData.size(); + s.outputs.geometry.mesh.buffers.main_buffer.dirty = true; + + s.outputs.geometry.mesh.input.input1.offset + = sizeof(float) * (s.m_vertexData.size() / 2); + s.outputs.geometry.mesh.vertices = s.m_vertexData.size() / (2 * 3); + s.outputs.geometry.dirty_mesh = true; + }; + } + else + { + return {}; + } +} + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/StructureSynth.hpp b/src/plugins/score-plugin-threedim/Threedim/StructureSynth.hpp new file mode 100644 index 0000000000..6fa1c12298 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/StructureSynth.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Threedim +{ + +class StrucSynth +{ +public: + halp_meta(name, "Structure Synth") + halp_meta(category, "Visuals/3D") + halp_meta(c_name, "structure_synth") + halp_meta(manual_url, "https://ossia.io/score-docs/processes/structure-synth.html") + halp_meta(uuid, "bb8f3d77-4cfd-44ce-9c43-b64c54a748ab") + + struct ins + { + struct : halp::lineedit<"Program", ""> + { + halp_meta(language, "eisenscript") + // Request a computation according to the currently defined program + void update(StrucSynth& g) { g.worker.request(this->value); } + } program; + + PositionControl position; + RotationControl rotation; + ScaleControl scale; + struct : halp::impulse_button<"Regenerate"> + { + void update(StrucSynth& g) { g.inputs.program.update(g); } + } regen; + } inputs; + + struct + { + struct : halp::mesh + { + halp_meta(name, "Geometry"); + halp::position_normals_geometry mesh; + } geometry; + } outputs; + + void operator()(); + + struct worker + { + std::function request; + + // Called back in a worker thread + // The returned function will be later applied in this object's processing thread + static std::function work(std::string_view s); + } worker; + + using float_vec = boost::container::vector>; + float_vec m_vertexData; +}; + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/TinyObj.cpp b/src/plugins/score-plugin-threedim/Threedim/TinyObj.cpp new file mode 100644 index 0000000000..d5ffcdce5c --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/TinyObj.cpp @@ -0,0 +1,194 @@ +#include "TinyObj.hpp" + +#define TINYOBJLOADER_IMPLEMENTATION +// #define TINYOBJLOADER_USE_MAPBOX_EARCUT +#include "../3rdparty/tiny_obj_loader.h" + +#include +#include +namespace Threedim +{ + +std::vector +ObjFromString(std::string_view obj_data, std::string_view mtl_data, float_vec& buf) +{ + tinyobj::ObjReaderConfig reader_config; + + tinyobj::ObjReader reader; + + QElapsedTimer e; + e.start(); + if (!reader.ParseFromString(obj_data, mtl_data, reader_config)) + { + if (!reader.Error().empty()) + { + qDebug() << "TinyObjReader: " << reader.Error().c_str(); + } + return {}; + } + + if (!reader.Warning().empty()) + { + qDebug() << "TinyObjReader: " << reader.Warning().c_str(); + } + + auto& attrib = reader.GetAttrib(); + auto& shapes = reader.GetShapes(); + if (shapes.empty()) + return {}; + + int64_t total_vertices = 0; + for (auto& shape : shapes) + { + total_vertices += shape.mesh.num_face_vertices.size() * 3; + } + + const bool texcoords = !attrib.texcoords.empty(); + const bool normals = !attrib.normals.empty(); + + std::size_t float_count = total_vertices * 3 + (normals ? total_vertices * 3 : 0) + + (texcoords ? total_vertices * 2 : 0); + + // 3 float per vertex for position, 3 per vertex for normal + buf.clear(); + buf.resize(float_count, 0.); // boost::container::default_init); + + int64_t pos_offset = 0; + int64_t texcoord_offset = 0; + int64_t normal_offset = 0; + if (texcoords) + { + texcoord_offset = total_vertices * 3; + if (normals) + normal_offset = texcoord_offset + total_vertices * 2; + } + else if (normals) + { + normal_offset = total_vertices * 3; + } + float* pos = buf.data() + pos_offset; + float* tc = buf.data() + texcoord_offset; + float* norm = buf.data() + normal_offset; + + std::vector res; + for (auto& shape : shapes) + { + const auto faces = shape.mesh.num_face_vertices.size(); + const auto vertices = faces * 3; + + res.push_back( + {.vertices = int64_t(vertices), + .pos_offset = pos_offset, + .texcoord_offset = texcoord_offset, + .normal_offset = normal_offset, + .texcoord = texcoords, + .normals = normals}); + + size_t index_offset = 0; + + if (texcoords && normals) + { + for (auto fv : shape.mesh.num_face_vertices) + { + if (fv != 3) + return {}; + for (auto v = 0; v < 3; v++) + { + const auto idx = shape.mesh.indices[index_offset + v]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 0]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 1]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 2]; + + *tc++ = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0]; + *tc++ = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1]; + + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 0]; + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 1]; + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 2]; + } + index_offset += 3; + } + + pos_offset += vertices * 3; + texcoord_offset += vertices * 2; + normal_offset += vertices * 3; + } + else if (normals) + { + for (size_t fv : shape.mesh.num_face_vertices) + { + if (fv != 3) + return {}; + for (size_t v = 0; v < 3; v++) + { + const auto idx = shape.mesh.indices[index_offset + v]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 0]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 1]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 2]; + + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 0]; + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 1]; + *norm++ = attrib.normals[3 * size_t(idx.normal_index) + 2]; + } + index_offset += 3; + } + pos_offset += vertices * 3; + normal_offset += vertices * 3; + } + else if (texcoords) + { + for (size_t fv : shape.mesh.num_face_vertices) + { + if (fv != 3) + return {}; + for (size_t v = 0; v < 3; v++) + { + const auto idx = shape.mesh.indices[index_offset + v]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 0]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 1]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 2]; + + *tc++ = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0]; + *tc++ = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1]; + } + index_offset += 3; + } + pos_offset += vertices * 3; + texcoord_offset += vertices * 2; + } + else + { + for (size_t fv : shape.mesh.num_face_vertices) + { + if (fv != 3) + return {}; + for (size_t v = 0; v < 3; v++) + { + const auto idx = shape.mesh.indices[index_offset + v]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 0]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 1]; + *pos++ = attrib.vertices[3 * size_t(idx.vertex_index) + 2]; + } + index_offset += 3; + } + pos_offset += vertices * 3; + } + } + + return res; +} + +std::vector ObjFromString(std::string_view obj_data, float_vec& data) +{ + std::string default_mtl = R"(newmtl default +Ka 0.1986 0.0000 0.0000 +Kd 0.5922 0.0166 0.0000 +Ks 0.5974 0.2084 0.2084 +illum 2 +Ns 100.2237 +)"; + + return ObjFromString(obj_data, default_mtl, data); +} + +} diff --git a/src/plugins/score-plugin-threedim/Threedim/TinyObj.hpp b/src/plugins/score-plugin-threedim/Threedim/TinyObj.hpp new file mode 100644 index 0000000000..e2f2f0b521 --- /dev/null +++ b/src/plugins/score-plugin-threedim/Threedim/TinyObj.hpp @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +namespace Threedim +{ +using float_vec = boost::container::vector>; + +struct mesh { + int64_t vertices{}; + // offset are in "elements", not bytes + int64_t pos_offset{}, texcoord_offset{}, normal_offset{}, color_offset{}; + bool texcoord{}; + bool normals{}; + bool colors{}; + bool points{}; +}; + +std::vector ObjFromString( + std::string_view obj_data + , std::string_view mtl_data + , float_vec& data); + +std::vector ObjFromString( + std::string_view obj_data + , float_vec& data); + +template +static void fromGL(float (&from)[N], auto& to) +{ + memcpy(to.data(), from, sizeof(float[N])); +} +template +static void toGL(auto& from, float (&to)[N]) +{ + memcpy(to, from.data(), sizeof(float[N])); +} + +inline void rebuild_transform(auto& inputs, auto& outputs) +{ + QMatrix4x4 model{}; + auto& pos = inputs.position; + auto& rot = inputs.rotation; + auto& sc = inputs.scale; + + model.translate(pos.value.x, pos.value.y, pos.value.z); + model.rotate(QQuaternion::fromEulerAngles(rot.value.x, rot.value.y, rot.value.z)); + model.scale(sc.value.x, sc.value.y, sc.value.z); + + toGL(model, outputs.geometry.transform); + outputs.geometry.dirty_transform = true; +} +struct PositionControl : halp::xyz_spinboxes_f32<"Position", halp::free_range_min<>> +{ + void update(auto& o) { rebuild_transform(o.inputs, o.outputs); } +}; +struct RotationControl + : halp::xyz_spinboxes_f32<"Rotation", halp::range{0., 359.9999999, 0.}> +{ + void update(auto& o) { rebuild_transform(o.inputs, o.outputs); } +}; +struct ScaleControl : halp::xyz_spinboxes_f32<"Scale", halp::range{0.00001, 1000., 1.}> +{ + void update(auto& o) { rebuild_transform(o.inputs, o.outputs); } +}; + +struct Update +{ + void update(auto& obj) { obj.update(); } +}; + +struct PrimitiveOutputs +{ + struct + { + halp_meta(name, "Geometry"); + halp::position_normals_texcoords_geometry mesh; + float transform[16]{}; + bool dirty_mesh = false; + bool dirty_transform = false; + } geometry; +}; +} diff --git a/src/plugins/score-plugin-threedim/score_plugin_threedim.cpp b/src/plugins/score-plugin-threedim/score_plugin_threedim.cpp new file mode 100644 index 0000000000..7410c1ebe3 --- /dev/null +++ b/src/plugins/score-plugin-threedim/score_plugin_threedim.cpp @@ -0,0 +1,208 @@ +#include "score_plugin_threedim.hpp" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Threedim +{ +class SSynthLibraryHandler final + : public QObject + , public Library::LibraryInterface +{ + SCORE_CONCRETE("623f6c12-f661-474d-9bba-dee208e9a6a4") + + QSet acceptedFiles() const noexcept override { return {"es"}; } + + Library::Subcategories categories; + + using proc = oscr::ProcessModel; + void setup(Library::ProcessesItemModel& model, const score::GUIApplicationContext& ctx) + override + { + // TODO relaunch whenever library path changes... + const auto& key = Metadata::get(); + QModelIndex node = model.find(key); + if (node == QModelIndex{}) + { + return; + } + + categories.init(node, ctx); + } + + void addPath(std::string_view path) override + { + QFileInfo file{QString::fromUtf8(path.data(), path.length())}; + QFile f{file.absoluteFilePath()}; + if (!f.open(QIODevice::ReadOnly)) + return; + + Library::ProcessData pdata; + pdata.prettyName = file.completeBaseName(); + pdata.key = Metadata::get(); + pdata.author = "Structure Synth"; + pdata.customData = score::readFileAsQString(f); + categories.add(file, std::move(pdata)); + } +}; + +class SSynthDropHandler final : public Process::ProcessDropHandler +{ + SCORE_CONCRETE("8b7892e1-f8ac-4579-bbf5-d71eb9810598") + + QSet fileExtensions() const noexcept override { return {"es"}; } + + using proc = oscr::ProcessModel; + void dropData( + std::vector& vec, + const DroppedFile& data, + const score::DocumentContext& ctx) const noexcept override + { + const auto& [filename, content] = data; + + { + Process::ProcessDropHandler::ProcessDrop p; + p.creation.key = Metadata::get(); + p.creation.prettyName = filename.basename; + p.setup = [s = content](Process::ProcessModel& m, score::Dispatcher& disp) mutable + { + auto& pp = static_cast(m); + auto& inl = *safe_cast(pp.inlets()[0]); + disp.submit(new Process::SetControlValue{inl, s.toStdString()}); + }; + vec.push_back(std::move(p)); + } + } +}; + +class OBJLibraryHandler final + : public QObject + , public Library::LibraryInterface +{ + SCORE_CONCRETE("da4af155-3cb6-41df-8c10-5a002b9d97ca") + + QSet acceptedFiles() const noexcept override { return {"obj"}; } + + Library::Subcategories categories; + + using proc = oscr::ProcessModel; + void setup(Library::ProcessesItemModel& model, const score::GUIApplicationContext& ctx) + override + { + // TODO relaunch whenever library path changes... + const auto& key = Metadata::get(); + QModelIndex node = model.find(key); + if (node == QModelIndex{}) + return; + + categories.init(node, ctx); + } + + void addPath(std::string_view path) override + { + auto p = QString::fromUtf8(path.data(), path.length()); + QFileInfo file{p}; + + Library::ProcessData pdata; + pdata.prettyName = file.completeBaseName(); + pdata.key = Metadata::get(); + pdata.author = "OBJ 3D File"; + pdata.customData = p; + categories.add(file, std::move(pdata)); + } +}; + +class OBJDropHandler final : public Process::ProcessDropHandler +{ + SCORE_CONCRETE("1d6cac56-2059-4fb8-9cef-19301a1fba3d") + + QSet fileExtensions() const noexcept override { return {"obj"}; } + + using proc = oscr::ProcessModel; + void dropData( + std::vector& vec, + const DroppedFile& data, + const score::DocumentContext& ctx) const noexcept override + { + const auto& [filename, content] = data; + + { + Process::ProcessDropHandler::ProcessDrop p; + p.creation.key = Metadata::get(); + p.creation.prettyName = filename.basename; + p.setup = [s = filename.relative]( + Process::ProcessModel& m, score::Dispatcher& disp) mutable + { + auto& pp = static_cast(m); + auto& inl = *safe_cast(pp.inlets()[0]); + disp.submit(new Process::SetControlValue{inl, s.toStdString()}); + }; + vec.push_back(std::move(p)); + } + } +}; + +} +/** + * This file instantiates the classes that are provided by this plug-in. + */ +score_plugin_threedim::score_plugin_threedim() = default; +score_plugin_threedim::~score_plugin_threedim() = default; + +std::vector score_plugin_threedim::factories( + const score::ApplicationContext& ctx, const score::InterfaceKey& key) const +{ + std::vector fx; + Avnd::instantiate_fx< + Threedim::ArrayToMesh, + Threedim::Noise, + Threedim::StrucSynth, + Threedim::ObjLoader, + Threedim::Plane, + Threedim::Cube, + Threedim::Sphere, + Threedim::Icosahedron, + Threedim::Cylinder, + Threedim::Cone, + Threedim::Torus>(fx, ctx, key); + auto add = instantiate_factories< + score::ApplicationContext, + FW, + FW, + FW, + FW>(ctx, key); + fx.insert( + fx.end(), + std::make_move_iterator(add.begin()), + std::make_move_iterator(add.end())); + return fx; +} + +std::vector score_plugin_threedim::required() const +{ + return {score_plugin_engine::static_key()}; +} + +#include +SCORE_EXPORT_PLUGIN(score_plugin_threedim) diff --git a/src/plugins/score-plugin-threedim/score_plugin_threedim.hpp b/src/plugins/score-plugin-threedim/score_plugin_threedim.hpp new file mode 100644 index 0000000000..e677885ac2 --- /dev/null +++ b/src/plugins/score-plugin-threedim/score_plugin_threedim.hpp @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include + +#include + +class score_plugin_threedim final + : public score::FactoryInterface_QtInterface + , public score::Plugin_QtInterface +{ + SCORE_PLUGIN_METADATA(1, "9f461313-af58-4365-a71f-b92fddc691cf") +public: + score_plugin_threedim(); + ~score_plugin_threedim() override; + +private: + std::vector factories( + const score::ApplicationContext&, + const score::InterfaceKey& factoryName) const override; + + std::vector required() const override; +};